Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions examples/guardrail_example_fixed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Fixed example demonstrating proper guardrail usage with PraisonAI Agents.
This addresses the issues reported in issue #875.
"""

from praisonaiagents import Agent, Task, GuardrailResult, PraisonAIAgents, TaskOutput
from typing import Tuple, Any
import trafilatura

# Example 1: Using GuardrailResult return type (now supported!)
def validate_length_guardrailresult(output: TaskOutput) -> GuardrailResult:
"""Ensure output is between 100-500 characters using GuardrailResult"""
# Extract the raw text from the TaskOutput object
text = output.raw if hasattr(output, 'raw') else str(output)
length = len(text)

if 100 <= length <= 500:
return GuardrailResult(
success=True,
result=output, # Pass through the original output
error=""
)
else:
return GuardrailResult(
success=False,
result=None,
error=f"Output must be 100-500 chars, got {length}"
)

# Example 2: Using Tuple[bool, Any] return type (original method)
def validate_length_tuple(output: TaskOutput) -> Tuple[bool, Any]:
"""Ensure output is between 100-500 characters using tuple"""
text = output.raw if hasattr(output, 'raw') else str(output)
length = len(text)

if 100 <= length <= 500:
return True, output
else:
return False, f"Output must be 100-500 chars, got {length}"

# Tool function
def get_url_context(url):
"""Fetch and extract content from a URL"""
downloaded = trafilatura.fetch_url(url)
if not downloaded:
return "Sorry, I couldn't fetch the content from that URL."

extracted = trafilatura.extract(
downloaded,
include_comments=False,
include_links=True,
output_format='json',
with_metadata=True,
url=url
)

if not extracted:
return "Sorry, I couldn't extract readable content from that page."

return extracted # returns JSON string

# Create agent with FIXED tools parameter (must be a list!)
agent = Agent(
name="Content Summarizer",
role="Content Analysis Expert",
goal="Summarize web content concisely",
instructions="You are a helpful assistant that summarizes web content",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
self_reflect=False,
verbose=True,
tools=[get_url_context] # FIX: tools must be a list, not a single function
)

# Create task with GuardrailResult guardrail
task_with_guardrailresult = Task(
name="summarise article with GuardrailResult",
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary below 500 characters",
agent=agent,
guardrail=validate_length_guardrailresult, # Using GuardrailResult
expected_output="summary of the article below 500 characters",
max_retries=3 # Will retry up to 3 times if guardrail fails
)

# Alternative: Create task with tuple guardrail
task_with_tuple = Task(
name="summarise article with tuple",
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary below 500 characters",
agent=agent,
guardrail=validate_length_tuple, # Using Tuple[bool, Any]
expected_output="summary of the article below 500 characters",
max_retries=3
)

# Example with string-based LLM guardrail
task_with_llm_guardrail = Task(
name="summarise with LLM guardrail",
description="get the context of this url: https://blog.google/technology/ai/dolphingemma/ and produce a summary",
agent=agent,
guardrail="Ensure the summary is professional, factual, and between 100-500 characters",
expected_output="professional summary of the article"
)

# Run with GuardrailResult example
print("=== Running with GuardrailResult guardrail ===")
agents_gr = PraisonAIAgents(
agents=[agent],
tasks=[task_with_guardrailresult]
)

# Uncomment to run:
# result_gr = agents_gr.start()

# Run with Tuple example
print("\n=== Running with Tuple[bool, Any] guardrail ===")
agents_tuple = PraisonAIAgents(
agents=[agent],
tasks=[task_with_tuple]
)

# Uncomment to run:
# result_tuple = agents_tuple.start()

# Run with LLM guardrail example
print("\n=== Running with LLM-based guardrail ===")
agents_llm = PraisonAIAgents(
agents=[agent],
tasks=[task_with_llm_guardrail]
)

# Uncomment to run:
# result_llm = agents_llm.start()

print("""
Key fixes applied:
1. GuardrailResult is now accepted as a valid return type annotation
2. tools parameter must be a list: tools=[get_url_context] not tools=get_url_context
3. Both GuardrailResult and Tuple[bool, Any] return types are supported
4. String-based LLM guardrails are also supported

The guardrail will automatically retry (up to max_retries times) if validation fails.
""")
50 changes: 46 additions & 4 deletions src/praisonai-agents/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,32 @@ task = Task(
#### Task-Level Guardrails
```python
from typing import Tuple, Any
from praisonaiagents import GuardrailResult

# Function-based guardrail
def validate_output(task_output: TaskOutput) -> Tuple[bool, Any]:
"""Custom validation function."""
# Function-based guardrail (Option 1: Using GuardrailResult)
def validate_output(task_output: TaskOutput) -> GuardrailResult:
"""Custom validation function returning GuardrailResult."""
if "error" in task_output.raw.lower():
return GuardrailResult(
success=False,
result=None,
error="Output contains errors"
)
if len(task_output.raw) < 10:
return GuardrailResult(
success=False,
result=None,
error="Output is too short"
)
return GuardrailResult(
success=True,
result=task_output,
error=""
)

# Function-based guardrail (Option 2: Using Tuple[bool, Any])
def validate_output_tuple(task_output: TaskOutput) -> Tuple[bool, Any]:
"""Custom validation function returning tuple."""
if "error" in task_output.raw.lower():
return False, "Output contains errors"
if len(task_output.raw) < 10:
Expand Down Expand Up @@ -170,7 +192,27 @@ task = Task(
#### Agent-Level Guardrails
```python
# Agent guardrails apply to ALL outputs from that agent
def validate_professional_tone(task_output: TaskOutput) -> Tuple[bool, Any]:

# Option 1: Using GuardrailResult
def validate_professional_tone(task_output: TaskOutput) -> GuardrailResult:
"""Ensure professional tone in all agent responses."""
content = task_output.raw.lower()
casual_words = ['yo', 'dude', 'awesome', 'cool']
for word in casual_words:
if word in content:
return GuardrailResult(
success=False,
result=None,
error=f"Unprofessional language detected: {word}"
)
return GuardrailResult(
success=True,
result=task_output,
error=""
)

# Option 2: Using Tuple[bool, Any]
def validate_professional_tone_tuple(task_output: TaskOutput) -> Tuple[bool, Any]:
"""Ensure professional tone in all agent responses."""
content = task_output.raw.lower()
casual_words = ['yo', 'dude', 'awesome', 'cool']
Expand Down
21 changes: 17 additions & 4 deletions src/praisonai-agents/praisonaiagents/task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,15 @@ def _setup_guardrail(self):
# Check return annotation if present
return_annotation = sig.return_annotation
if return_annotation != inspect.Signature.empty:
# Import GuardrailResult for checking
from ..guardrails import GuardrailResult

# Check if it's a GuardrailResult type
is_guardrail_result = return_annotation is GuardrailResult

# Check for tuple return type
return_annotation_args = get_args(return_annotation)
if not (
is_tuple = (
get_origin(return_annotation) is tuple
and len(return_annotation_args) == 2
and return_annotation_args[0] is bool
Expand All @@ -183,9 +190,11 @@ def _setup_guardrail(self):
or return_annotation_args[1] is TaskOutput
or return_annotation_args[1] == Union[str, TaskOutput]
)
):
)

if not (is_guardrail_result or is_tuple):
raise ValueError(
"If return type is annotated, it must be Tuple[bool, Any]"
"If return type is annotated, it must be GuardrailResult or Tuple[bool, Any]"
)

self._guardrail_fn = self.guardrail
Expand Down Expand Up @@ -447,7 +456,11 @@ def _process_guardrail(self, task_output: TaskOutput):
# Call the guardrail function
result = self._guardrail_fn(task_output)

# Convert the result to a GuardrailResult
# Check if result is already a GuardrailResult
if isinstance(result, GuardrailResult):
return result

# Otherwise, convert the tuple result to a GuardrailResult
return GuardrailResult.from_tuple(result)

except Exception as e:
Expand Down
Loading
Loading