mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-28 02:08:29 +00:00
Compare commits
21 Commits
fix/python
...
pr-1762
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f26833f751 | ||
|
|
5fe15a8dba | ||
|
|
d8f5a9fb71 | ||
|
|
55883c6083 | ||
|
|
072f0cbef6 | ||
|
|
7beb511206 | ||
|
|
5b2e41b8eb | ||
|
|
22e5d39884 | ||
|
|
e6f620877d | ||
|
|
9ee6824ccd | ||
|
|
da73865f25 | ||
|
|
627b9f1abb | ||
|
|
1b8001bf98 | ||
|
|
e59e07e4f7 | ||
|
|
43cb2d1f66 | ||
|
|
4e9b70201e | ||
|
|
059b0cf5b4 | ||
|
|
652ddcc1c5 | ||
|
|
964d4bfdbf | ||
|
|
c103d7eab7 | ||
|
|
4fe9f5d8bd |
@@ -29,7 +29,7 @@ Large Language Models (LLMs) are the core intelligence behind CrewAI agents. The
|
||||
|
||||
## Available Models and Their Capabilities
|
||||
|
||||
Here's a detailed breakdown of supported models and their capabilities, you can compare performance at [lmarena.ai](https://lmarena.ai/):
|
||||
Here's a detailed breakdown of supported models and their capabilities, you can compare performance at [lmarena.ai](https://lmarena.ai/?leaderboard) and [artificialanalysis.ai](https://artificialanalysis.ai/):
|
||||
|
||||
<Tabs>
|
||||
<Tab title="OpenAI">
|
||||
@@ -121,12 +121,18 @@ Here's a detailed breakdown of supported models and their capabilities, you can
|
||||
<Tab title="Gemini">
|
||||
| Model | Context Window | Best For |
|
||||
|-------|---------------|-----------|
|
||||
| Gemini 1.5 Flash | 1M tokens | Balanced multimodal model, good for most tasks |
|
||||
| Gemini 1.5 Flash 8B | 1M tokens | Fastest, most cost-efficient, good for high-frequency tasks |
|
||||
| Gemini 1.5 Pro | 2M tokens | Best performing, wide variety of reasoning tasks including logical reasoning, coding, and creative collaboration |
|
||||
| gemini-2.0-flash-exp | 1M tokens | Higher quality at faster speed, multimodal model, good for most tasks |
|
||||
| gemini-1.5-flash | 1M tokens | Balanced multimodal model, good for most tasks |
|
||||
| gemini-1.5-flash-8B | 1M tokens | Fastest, most cost-efficient, good for high-frequency tasks |
|
||||
| gemini-1.5-pro | 2M tokens | Best performing, wide variety of reasoning tasks including logical reasoning, coding, and creative collaboration |
|
||||
|
||||
<Tip>
|
||||
Google's Gemini models are all multimodal, supporting audio, images, video and text, supporting context caching, json schema, function calling, etc.
|
||||
|
||||
These models are available via API_KEY from
|
||||
[The Gemini API](https://ai.google.dev/gemini-api/docs) and also from
|
||||
[Google Cloud Vertex](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-google-ai) as part of the
|
||||
[Model Garden](https://cloud.google.com/vertex-ai/generative-ai/docs/model-garden/explore-models).
|
||||
</Tip>
|
||||
</Tab>
|
||||
<Tab title="Groq">
|
||||
@@ -135,7 +141,6 @@ Here's a detailed breakdown of supported models and their capabilities, you can
|
||||
| Llama 3.1 70B/8B | 131,072 tokens | High-performance, large context tasks |
|
||||
| Llama 3.2 Series | 8,192 tokens | General-purpose tasks |
|
||||
| Mixtral 8x7B | 32,768 tokens | Balanced performance and context |
|
||||
| Gemma Series | 8,192 tokens | Efficient, smaller-scale tasks |
|
||||
|
||||
<Tip>
|
||||
Groq is known for its fast inference speeds, making it suitable for real-time applications.
|
||||
@@ -146,7 +151,7 @@ Here's a detailed breakdown of supported models and their capabilities, you can
|
||||
|----------|---------------|--------------|
|
||||
| Deepseek Chat | 128,000 tokens | Specialized in technical discussions |
|
||||
| Claude 3 | Up to 200K tokens | Strong reasoning, code understanding |
|
||||
| Gemini | Varies by model | Multimodal capabilities |
|
||||
| Gemma Series | 8,192 tokens | Efficient, smaller-scale tasks |
|
||||
|
||||
<Info>
|
||||
Provider selection should consider factors like:
|
||||
|
||||
@@ -6,7 +6,7 @@ icon: list-check
|
||||
|
||||
## Overview of a Task
|
||||
|
||||
In the CrewAI framework, a `Task` is a specific assignment completed by an `Agent`.
|
||||
In the CrewAI framework, a `Task` is a specific assignment completed by an `Agent`.
|
||||
|
||||
Tasks provide all necessary details for execution, such as a description, the agent responsible, required tools, and more, facilitating a wide range of action complexities.
|
||||
|
||||
@@ -263,8 +263,148 @@ analysis_task = Task(
|
||||
)
|
||||
```
|
||||
|
||||
## Task Guardrails
|
||||
|
||||
Task guardrails provide a way to validate and transform task outputs before they
|
||||
are passed to the next task. This feature helps ensure data quality and provides
|
||||
efeedback to agents when their output doesn't meet specific criteria.
|
||||
|
||||
### Using Task Guardrails
|
||||
|
||||
To add a guardrail to a task, provide a validation function through the `guardrail` parameter:
|
||||
|
||||
```python Code
|
||||
from typing import Tuple, Union, Dict, Any
|
||||
|
||||
def validate_blog_content(result: str) -> Tuple[bool, Union[Dict[str, Any], str]]:
|
||||
"""Validate blog content meets requirements."""
|
||||
try:
|
||||
# Check word count
|
||||
word_count = len(result.split())
|
||||
if word_count > 200:
|
||||
return (False, {
|
||||
"error": "Blog content exceeds 200 words",
|
||||
"code": "WORD_COUNT_ERROR",
|
||||
"context": {"word_count": word_count}
|
||||
})
|
||||
|
||||
# Additional validation logic here
|
||||
return (True, result.strip())
|
||||
except Exception as e:
|
||||
return (False, {
|
||||
"error": "Unexpected error during validation",
|
||||
"code": "SYSTEM_ERROR"
|
||||
})
|
||||
|
||||
blog_task = Task(
|
||||
description="Write a blog post about AI",
|
||||
expected_output="A blog post under 200 words",
|
||||
agent=blog_agent,
|
||||
guardrail=validate_blog_content # Add the guardrail function
|
||||
)
|
||||
```
|
||||
|
||||
### Guardrail Function Requirements
|
||||
|
||||
1. **Function Signature**:
|
||||
- Must accept exactly one parameter (the task output)
|
||||
- Should return a tuple of `(bool, Any)`
|
||||
- Type hints are recommended but optional
|
||||
|
||||
2. **Return Values**:
|
||||
- Success: Return `(True, validated_result)`
|
||||
- Failure: Return `(False, error_details)`
|
||||
|
||||
### Error Handling Best Practices
|
||||
|
||||
1. **Structured Error Responses**:
|
||||
```python Code
|
||||
def validate_with_context(result: str) -> Tuple[bool, Union[Dict[str, Any], str]]:
|
||||
try:
|
||||
# Main validation logic
|
||||
validated_data = perform_validation(result)
|
||||
return (True, validated_data)
|
||||
except ValidationError as e:
|
||||
return (False, {
|
||||
"error": str(e),
|
||||
"code": "VALIDATION_ERROR",
|
||||
"context": {"input": result}
|
||||
})
|
||||
except Exception as e:
|
||||
return (False, {
|
||||
"error": "Unexpected error",
|
||||
"code": "SYSTEM_ERROR"
|
||||
})
|
||||
```
|
||||
|
||||
2. **Error Categories**:
|
||||
- Use specific error codes
|
||||
- Include relevant context
|
||||
- Provide actionable feedback
|
||||
|
||||
3. **Validation Chain**:
|
||||
```python Code
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
def complex_validation(result: str) -> Tuple[bool, Union[str, Dict[str, Any]]]:
|
||||
"""Chain multiple validation steps."""
|
||||
# Step 1: Basic validation
|
||||
if not result:
|
||||
return (False, {"error": "Empty result", "code": "EMPTY_INPUT"})
|
||||
|
||||
# Step 2: Content validation
|
||||
try:
|
||||
validated = validate_content(result)
|
||||
if not validated:
|
||||
return (False, {"error": "Invalid content", "code": "CONTENT_ERROR"})
|
||||
|
||||
# Step 3: Format validation
|
||||
formatted = format_output(validated)
|
||||
return (True, formatted)
|
||||
except Exception as e:
|
||||
return (False, {
|
||||
"error": str(e),
|
||||
"code": "VALIDATION_ERROR",
|
||||
"context": {"step": "content_validation"}
|
||||
})
|
||||
```
|
||||
|
||||
### Handling Guardrail Results
|
||||
|
||||
When a guardrail returns `(False, error)`:
|
||||
1. The error is sent back to the agent
|
||||
2. The agent attempts to fix the issue
|
||||
3. The process repeats until:
|
||||
- The guardrail returns `(True, result)`
|
||||
- Maximum retries are reached
|
||||
|
||||
Example with retry handling:
|
||||
```python Code
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
def validate_json_output(result: str) -> Tuple[bool, Union[Dict[str, Any], str]]:
|
||||
"""Validate and parse JSON output."""
|
||||
try:
|
||||
# Try to parse as JSON
|
||||
data = json.loads(result)
|
||||
return (True, data)
|
||||
except json.JSONDecodeError as e:
|
||||
return (False, {
|
||||
"error": "Invalid JSON format",
|
||||
"code": "JSON_ERROR",
|
||||
"context": {"line": e.lineno, "column": e.colno}
|
||||
})
|
||||
|
||||
task = Task(
|
||||
description="Generate a JSON report",
|
||||
expected_output="A valid JSON object",
|
||||
agent=analyst,
|
||||
guardrail=validate_json_output,
|
||||
max_retries=3 # Limit retry attempts
|
||||
)
|
||||
```
|
||||
|
||||
## Getting Structured Consistent Outputs from Tasks
|
||||
When you need to ensure that a task outputs a structured and consistent format, you can use the `output_pydantic` or `output_json` properties on a task. These properties allow you to define the expected output structure, making it easier to parse and utilize the results in your application.
|
||||
|
||||
<Note>
|
||||
It's also important to note that the output of the final task of a crew becomes the final output of the actual crew itself.
|
||||
@@ -608,6 +748,114 @@ While creating and executing tasks, certain validation mechanisms are in place t
|
||||
|
||||
These validations help in maintaining the consistency and reliability of task executions within the crewAI framework.
|
||||
|
||||
## Task Guardrails
|
||||
|
||||
Task guardrails provide a powerful way to validate, transform, or filter task outputs before they are passed to the next task. Guardrails are optional functions that execute before the next task starts, allowing you to ensure that task outputs meet specific requirements or formats.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python Code
|
||||
from typing import Tuple, Union
|
||||
from crewai import Task
|
||||
|
||||
def validate_json_output(result: str) -> Tuple[bool, Union[dict, str]]:
|
||||
"""Validate that the output is valid JSON."""
|
||||
try:
|
||||
json_data = json.loads(result)
|
||||
return (True, json_data)
|
||||
except json.JSONDecodeError:
|
||||
return (False, "Output must be valid JSON")
|
||||
|
||||
task = Task(
|
||||
description="Generate JSON data",
|
||||
expected_output="Valid JSON object",
|
||||
guardrail=validate_json_output
|
||||
)
|
||||
```
|
||||
|
||||
### How Guardrails Work
|
||||
|
||||
1. **Optional Attribute**: Guardrails are an optional attribute at the task level, allowing you to add validation only where needed.
|
||||
2. **Execution Timing**: The guardrail function is executed before the next task starts, ensuring valid data flow between tasks.
|
||||
3. **Return Format**: Guardrails must return a tuple of `(success, data)`:
|
||||
- If `success` is `True`, `data` is the validated/transformed result
|
||||
- If `success` is `False`, `data` is the error message
|
||||
4. **Result Routing**:
|
||||
- On success (`True`), the result is automatically passed to the next task
|
||||
- On failure (`False`), the error is sent back to the agent to generate a new answer
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
#### Data Format Validation
|
||||
```python Code
|
||||
def validate_email_format(result: str) -> Tuple[bool, Union[str, str]]:
|
||||
"""Ensure the output contains a valid email address."""
|
||||
import re
|
||||
email_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
|
||||
if re.match(email_pattern, result.strip()):
|
||||
return (True, result.strip())
|
||||
return (False, "Output must be a valid email address")
|
||||
```
|
||||
|
||||
#### Content Filtering
|
||||
```python Code
|
||||
def filter_sensitive_info(result: str) -> Tuple[bool, Union[str, str]]:
|
||||
"""Remove or validate sensitive information."""
|
||||
sensitive_patterns = ['SSN:', 'password:', 'secret:']
|
||||
for pattern in sensitive_patterns:
|
||||
if pattern.lower() in result.lower():
|
||||
return (False, f"Output contains sensitive information ({pattern})")
|
||||
return (True, result)
|
||||
```
|
||||
|
||||
#### Data Transformation
|
||||
```python Code
|
||||
def normalize_phone_number(result: str) -> Tuple[bool, Union[str, str]]:
|
||||
"""Ensure phone numbers are in a consistent format."""
|
||||
import re
|
||||
digits = re.sub(r'\D', '', result)
|
||||
if len(digits) == 10:
|
||||
formatted = f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
|
||||
return (True, formatted)
|
||||
return (False, "Output must be a 10-digit phone number")
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
#### Chaining Multiple Validations
|
||||
```python Code
|
||||
def chain_validations(*validators):
|
||||
"""Chain multiple validators together."""
|
||||
def combined_validator(result):
|
||||
for validator in validators:
|
||||
success, data = validator(result)
|
||||
if not success:
|
||||
return (False, data)
|
||||
result = data
|
||||
return (True, result)
|
||||
return combined_validator
|
||||
|
||||
# Usage
|
||||
task = Task(
|
||||
description="Get user contact info",
|
||||
expected_output="Email and phone",
|
||||
guardrail=chain_validations(
|
||||
validate_email_format,
|
||||
filter_sensitive_info
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
#### Custom Retry Logic
|
||||
```python Code
|
||||
task = Task(
|
||||
description="Generate data",
|
||||
expected_output="Valid data",
|
||||
guardrail=validate_data,
|
||||
max_retries=5 # Override default retry limit
|
||||
)
|
||||
```
|
||||
|
||||
## Creating Directories when Saving Files
|
||||
|
||||
You can now specify if a task should create directories when saving its output to a file. This is particularly useful for organizing outputs and ensuring that file paths are correctly structured.
|
||||
@@ -629,7 +877,7 @@ save_output_task = Task(
|
||||
|
||||
## Conclusion
|
||||
|
||||
Tasks are the driving force behind the actions of agents in CrewAI.
|
||||
By properly defining tasks and their outcomes, you set the stage for your AI agents to work effectively, either independently or as a collaborative unit.
|
||||
Equipping tasks with appropriate tools, understanding the execution process, and following robust validation practices are crucial for maximizing CrewAI's potential,
|
||||
Tasks are the driving force behind the actions of agents in CrewAI.
|
||||
By properly defining tasks and their outcomes, you set the stage for your AI agents to work effectively, either independently or as a collaborative unit.
|
||||
Equipping tasks with appropriate tools, understanding the execution process, and following robust validation practices are crucial for maximizing CrewAI's potential,
|
||||
ensuring agents are effectively prepared for their assignments and that tasks are executed as intended.
|
||||
|
||||
@@ -18,3 +18,6 @@ test = "{{folder_name}}.main:test"
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
@@ -5,7 +5,7 @@ from pydantic import BaseModel
|
||||
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
|
||||
from .crews.poem_crew.poem_crew import PoemCrew
|
||||
from {{folder_name}}.crews.poem_crew.poem_crew import PoemCrew
|
||||
|
||||
|
||||
class PoemState(BaseModel):
|
||||
|
||||
@@ -15,3 +15,6 @@ plot = "{{folder_name}}.main:plot"
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
|
||||
@@ -8,3 +8,5 @@ dependencies = [
|
||||
"crewai[tools]>=0.86.0"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
type = "tool"
|
||||
|
||||
@@ -44,6 +44,7 @@ LLM_CONTEXT_WINDOW_SIZES = {
|
||||
"o1-preview": 128000,
|
||||
"o1-mini": 128000,
|
||||
# gemini
|
||||
"gemini-2.0-flash": 1048576,
|
||||
"gemini-1.5-pro": 2097152,
|
||||
"gemini-1.5-flash": 1048576,
|
||||
"gemini-1.5-flash-8b": 1048576,
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import datetime
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
from concurrent.futures import Future
|
||||
from copy import copy
|
||||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from opentelemetry.trace import Span
|
||||
from pydantic import (
|
||||
@@ -20,6 +33,7 @@ from pydantic import (
|
||||
from pydantic_core import PydanticCustomError
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.tasks.guardrail_result import GuardrailResult
|
||||
from crewai.tasks.output_format import OutputFormat
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
@@ -49,6 +63,7 @@ class Task(BaseModel):
|
||||
"""
|
||||
|
||||
__hash__ = object.__hash__ # type: ignore
|
||||
logger: ClassVar[logging.Logger] = logging.getLogger(__name__)
|
||||
used_tools: int = 0
|
||||
tools_errors: int = 0
|
||||
delegations: int = 0
|
||||
@@ -110,6 +125,55 @@ class Task(BaseModel):
|
||||
default=None,
|
||||
)
|
||||
processed_by_agents: Set[str] = Field(default_factory=set)
|
||||
guardrail: Optional[Callable[[TaskOutput], Tuple[bool, Any]]] = Field(
|
||||
default=None,
|
||||
description="Function to validate task output before proceeding to next task"
|
||||
)
|
||||
max_retries: int = Field(
|
||||
default=3,
|
||||
description="Maximum number of retries when guardrail fails"
|
||||
)
|
||||
retry_count: int = Field(
|
||||
default=0,
|
||||
description="Current number of retries"
|
||||
)
|
||||
|
||||
@field_validator("guardrail")
|
||||
@classmethod
|
||||
def validate_guardrail_function(cls, v: Optional[Callable]) -> Optional[Callable]:
|
||||
"""Validate that the guardrail function has the correct signature and behavior.
|
||||
|
||||
While type hints provide static checking, this validator ensures runtime safety by:
|
||||
1. Verifying the function accepts exactly one parameter (the TaskOutput)
|
||||
2. Checking return type annotations match Tuple[bool, Any] if present
|
||||
3. Providing clear, immediate error messages for debugging
|
||||
|
||||
This runtime validation is crucial because:
|
||||
- Type hints are optional and can be ignored at runtime
|
||||
- Function signatures need immediate validation before task execution
|
||||
- Clear error messages help users debug guardrail implementation issues
|
||||
|
||||
Args:
|
||||
v: The guardrail function to validate
|
||||
|
||||
Returns:
|
||||
The validated guardrail function
|
||||
|
||||
Raises:
|
||||
ValueError: If the function signature is invalid or return annotation
|
||||
doesn't match Tuple[bool, Any]
|
||||
"""
|
||||
if v is not None:
|
||||
sig = inspect.signature(v)
|
||||
if len(sig.parameters) != 1:
|
||||
raise ValueError("Guardrail function must accept exactly one parameter")
|
||||
|
||||
# Check return annotation if present, but don't require it
|
||||
return_annotation = sig.return_annotation
|
||||
if return_annotation != inspect.Signature.empty:
|
||||
if not (return_annotation == Tuple[bool, Any] or str(return_annotation) == 'Tuple[bool, Any]'):
|
||||
raise ValueError("If return type is annotated, it must be Tuple[bool, Any]")
|
||||
return v
|
||||
|
||||
_telemetry: Telemetry = PrivateAttr(default_factory=Telemetry)
|
||||
_execution_span: Optional[Span] = PrivateAttr(default=None)
|
||||
@@ -254,7 +318,6 @@ class Task(BaseModel):
|
||||
)
|
||||
|
||||
pydantic_output, json_output = self._export_output(result)
|
||||
|
||||
task_output = TaskOutput(
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
@@ -265,6 +328,37 @@ class Task(BaseModel):
|
||||
agent=agent.role,
|
||||
output_format=self._get_output_format(),
|
||||
)
|
||||
|
||||
if self.guardrail:
|
||||
guardrail_result = GuardrailResult.from_tuple(self.guardrail(task_output))
|
||||
if not guardrail_result.success:
|
||||
if self.retry_count >= self.max_retries:
|
||||
raise Exception(
|
||||
f"Task failed guardrail validation after {self.max_retries} retries. "
|
||||
f"Last error: {guardrail_result.error}"
|
||||
)
|
||||
|
||||
self.retry_count += 1
|
||||
context = (
|
||||
f"### Previous attempt failed validation: {guardrail_result.error}\n\n\n"
|
||||
f"### Previous result:\n{task_output.raw}\n\n\n"
|
||||
"Try again, making sure to address the validation error."
|
||||
)
|
||||
return self._execute_core(agent, context, tools)
|
||||
|
||||
if guardrail_result.result is None:
|
||||
raise Exception(
|
||||
"Task guardrail returned None as result. This is not allowed."
|
||||
)
|
||||
|
||||
if isinstance(guardrail_result.result, str):
|
||||
task_output.raw = guardrail_result.result
|
||||
pydantic_output, json_output = self._export_output(guardrail_result.result)
|
||||
task_output.pydantic = pydantic_output
|
||||
task_output.json_dict = json_output
|
||||
elif isinstance(guardrail_result.result, TaskOutput):
|
||||
task_output = guardrail_result.result
|
||||
|
||||
self.output = task_output
|
||||
|
||||
self._set_end_execution_time(start_time)
|
||||
@@ -308,7 +402,18 @@ class Task(BaseModel):
|
||||
|
||||
if inputs:
|
||||
self.description = self._original_description.format(**inputs)
|
||||
self.expected_output = self._original_expected_output.format(**inputs)
|
||||
self.expected_output = self.interpolate_only(
|
||||
input_string=self._original_expected_output, inputs=inputs
|
||||
)
|
||||
|
||||
def interpolate_only(self, input_string: str, inputs: Dict[str, Any]) -> str:
|
||||
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched."""
|
||||
escaped_string = input_string.replace("{", "{{").replace("}", "}}")
|
||||
|
||||
for key in inputs.keys():
|
||||
escaped_string = escaped_string.replace(f"{{{{{key}}}}}", f"{{{key}}}")
|
||||
|
||||
return escaped_string.format(**inputs)
|
||||
|
||||
def increment_tools_errors(self) -> None:
|
||||
"""Increment the tools errors counter."""
|
||||
@@ -390,22 +495,33 @@ class Task(BaseModel):
|
||||
return OutputFormat.RAW
|
||||
|
||||
def _save_file(self, result: Any) -> None:
|
||||
"""Save task output to a file.
|
||||
|
||||
Args:
|
||||
result: The result to save to the file. Can be a dict or any stringifiable object.
|
||||
|
||||
Raises:
|
||||
ValueError: If output_file is not set
|
||||
RuntimeError: If there is an error writing to the file
|
||||
"""
|
||||
if self.output_file is None:
|
||||
raise ValueError("output_file is not set.")
|
||||
|
||||
resolved_path = Path(self.output_file).expanduser().resolve()
|
||||
directory = resolved_path.parent
|
||||
try:
|
||||
resolved_path = Path(self.output_file).expanduser().resolve()
|
||||
directory = resolved_path.parent
|
||||
|
||||
if not directory.exists():
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
if not directory.exists():
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with resolved_path.open("w", encoding="utf-8") as file:
|
||||
if isinstance(result, dict):
|
||||
import json
|
||||
|
||||
json.dump(result, file, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
file.write(str(result))
|
||||
with resolved_path.open("w", encoding="utf-8") as file:
|
||||
if isinstance(result, dict):
|
||||
import json
|
||||
json.dump(result, file, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
file.write(str(result))
|
||||
except (OSError, IOError) as e:
|
||||
raise RuntimeError(f"Failed to save output file: {e}")
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
56
src/crewai/tasks/guardrail_result.py
Normal file
56
src/crewai/tasks/guardrail_result.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Module for handling task guardrail validation results.
|
||||
|
||||
This module provides the GuardrailResult class which standardizes
|
||||
the way task guardrails return their validation results.
|
||||
"""
|
||||
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
|
||||
|
||||
class GuardrailResult(BaseModel):
|
||||
"""Result from a task guardrail execution.
|
||||
|
||||
This class standardizes the return format of task guardrails,
|
||||
converting tuple responses into a structured format that can
|
||||
be easily handled by the task execution system.
|
||||
|
||||
Attributes:
|
||||
success (bool): Whether the guardrail validation passed
|
||||
result (Any, optional): The validated/transformed result if successful
|
||||
error (str, optional): Error message if validation failed
|
||||
"""
|
||||
success: bool
|
||||
result: Optional[Any] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
@field_validator("result", "error")
|
||||
@classmethod
|
||||
def validate_result_error_exclusivity(cls, v: Any, info) -> Any:
|
||||
values = info.data
|
||||
if "success" in values:
|
||||
if values["success"] and v and "error" in values and values["error"]:
|
||||
raise ValueError("Cannot have both result and error when success is True")
|
||||
if not values["success"] and v and "result" in values and values["result"]:
|
||||
raise ValueError("Cannot have both result and error when success is False")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, result: Tuple[bool, Union[Any, str]]) -> "GuardrailResult":
|
||||
"""Create a GuardrailResult from a validation tuple.
|
||||
|
||||
Args:
|
||||
result: A tuple of (success, data) where data is either
|
||||
the validated result or error message.
|
||||
|
||||
Returns:
|
||||
GuardrailResult: A new instance with the tuple data.
|
||||
"""
|
||||
success, data = result
|
||||
return cls(
|
||||
success=success,
|
||||
result=data if success else None,
|
||||
error=data if not success else None
|
||||
)
|
||||
@@ -12,7 +12,7 @@
|
||||
"tools": "\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\n{tools}\n\nUse the following format:\n\nThought: you should always think about what to do\nAction: the action to take, only one name of [{tool_names}], just the name, exactly as it's written.\nAction Input: the input to the action, just a simple python dictionary, enclosed in curly braces, using \" to wrap keys and values.\nObservation: the result of the action\n\nOnce all necessary information is gathered:\n\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n",
|
||||
"no_tools": "\nTo give my best complete final answer to the task use the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!",
|
||||
"format": "I MUST either use a tool (use one at time) OR give my best final answer not both at the same time. To Use the following format:\n\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action, dictionary enclosed in curly braces\nObservation: the result of the action\n... (this Thought/Action/Action Input/Result can repeat N times)\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described\n\n",
|
||||
"final_answer_format": "If you don't need to use any more tools, you must give your best complete final answer, make sure it satisfy the expect criteria, use the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer: my best complete final answer to the task.\n\n",
|
||||
"final_answer_format": "If you don't need to use any more tools, you must give your best complete final answer, make sure it satisfies the expected criteria, use the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer: my best complete final answer to the task.\n\n",
|
||||
"format_without_tools": "\nSorry, I didn't use the right format. I MUST either use a tool (among the available ones), OR give my best final answer.\nI just remembered the expected format I must follow:\n\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n... (this Thought/Action/Action Input/Result can repeat N times)\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described\n\n",
|
||||
"task_with_context": "{task}\n\nThis is the context you're working with:\n{context}",
|
||||
"expected_output": "\nThis is the expect criteria for your final answer: {expected_output}\nyou MUST return the actual complete content as the final answer, not a summary.",
|
||||
|
||||
@@ -26237,7 +26237,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}], "model": "gpt-4o"}'
|
||||
headers:
|
||||
@@ -26590,7 +26590,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -26941,7 +26941,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -27292,7 +27292,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -27647,7 +27647,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -28005,7 +28005,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -28364,7 +28364,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -28718,7 +28718,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -29082,7 +29082,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -29441,7 +29441,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -29802,7 +29802,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -30170,7 +30170,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -30533,7 +30533,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -30907,7 +30907,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -31273,7 +31273,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -31644,7 +31644,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
@@ -32015,7 +32015,7 @@ interactions:
|
||||
answer."}, {"role": "user", "content": "I did it wrong. Invalid Format: I missed
|
||||
the ''Action:'' after ''Thought:''. I will do right next, and don''t use a tool
|
||||
I have already used.\n\nIf you don''t need to use any more tools, you must give
|
||||
your best complete final answer, make sure it satisfy the expect criteria, use
|
||||
your best complete final answer, make sure it satisfies the expected criteria, use
|
||||
the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer:
|
||||
my best complete final answer to the task.\n\n"}, {"role": "user", "content":
|
||||
"I did it wrong. Tried to both perform Action and give a Final Answer at the
|
||||
|
||||
@@ -247,7 +247,7 @@ interactions:
|
||||
{"role": "user", "content": "I did it wrong. Invalid Format: I missed the ''Action:''
|
||||
after ''Thought:''. I will do right next, and don''t use a tool I have already
|
||||
used.\n\nIf you don''t need to use any more tools, you must give your best complete
|
||||
final answer, make sure it satisfy the expect criteria, use the EXACT format
|
||||
final answer, make sure it satisfies the expected criteria, use the EXACT format
|
||||
below:\n\nThought: I now can give a great answer\nFinal Answer: my best complete
|
||||
final answer to the task.\n\n"}], "model": "o1-preview"}'
|
||||
headers:
|
||||
|
||||
@@ -736,6 +736,48 @@ def test_interpolate_inputs():
|
||||
assert task.expected_output == "Bullet point list of 5 interesting ideas about ML."
|
||||
|
||||
|
||||
def test_interpolate_only():
|
||||
"""Test the interpolate_only method for various scenarios including JSON structure preservation."""
|
||||
task = Task(
|
||||
description="Unused in this test",
|
||||
expected_output="Unused in this test"
|
||||
)
|
||||
|
||||
# Test JSON structure preservation
|
||||
json_string = '{"info": "Look at {placeholder}", "nested": {"val": "{nestedVal}"}}'
|
||||
result = task.interpolate_only(
|
||||
input_string=json_string,
|
||||
inputs={"placeholder": "the data", "nestedVal": "something else"}
|
||||
)
|
||||
assert '"info": "Look at the data"' in result
|
||||
assert '"val": "something else"' in result
|
||||
assert "{placeholder}" not in result
|
||||
assert "{nestedVal}" not in result
|
||||
|
||||
# Test normal string interpolation
|
||||
normal_string = "Hello {name}, welcome to {place}!"
|
||||
result = task.interpolate_only(
|
||||
input_string=normal_string,
|
||||
inputs={"name": "John", "place": "CrewAI"}
|
||||
)
|
||||
assert result == "Hello John, welcome to CrewAI!"
|
||||
|
||||
# Test empty string
|
||||
result = task.interpolate_only(
|
||||
input_string="",
|
||||
inputs={"unused": "value"}
|
||||
)
|
||||
assert result == ""
|
||||
|
||||
# Test string with no placeholders
|
||||
no_placeholders = "Hello, this is a test"
|
||||
result = task.interpolate_only(
|
||||
input_string=no_placeholders,
|
||||
inputs={"unused": "value"}
|
||||
)
|
||||
assert result == no_placeholders
|
||||
|
||||
|
||||
def test_task_output_str_with_pydantic():
|
||||
from crewai.tasks.output_format import OutputFormat
|
||||
|
||||
|
||||
134
tests/test_task_guardrails.py
Normal file
134
tests/test_task_guardrails.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Tests for task guardrails functionality."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.task import Task
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
|
||||
|
||||
def test_task_without_guardrail():
|
||||
"""Test that tasks work normally without guardrails (backward compatibility)."""
|
||||
agent = Mock()
|
||||
agent.role = "test_agent"
|
||||
agent.execute_task.return_value = "test result"
|
||||
agent.crew = None
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Output"
|
||||
)
|
||||
|
||||
result = task.execute_sync(agent=agent)
|
||||
assert isinstance(result, TaskOutput)
|
||||
assert result.raw == "test result"
|
||||
|
||||
|
||||
def test_task_with_successful_guardrail():
|
||||
"""Test that successful guardrail validation passes transformed result."""
|
||||
def guardrail(result: TaskOutput):
|
||||
return (True, result.raw.upper())
|
||||
|
||||
agent = Mock()
|
||||
agent.role = "test_agent"
|
||||
agent.execute_task.return_value = "test result"
|
||||
agent.crew = None
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Output",
|
||||
guardrail=guardrail
|
||||
)
|
||||
|
||||
result = task.execute_sync(agent=agent)
|
||||
assert isinstance(result, TaskOutput)
|
||||
assert result.raw == "TEST RESULT"
|
||||
|
||||
|
||||
def test_task_with_failing_guardrail():
|
||||
"""Test that failing guardrail triggers retry with error context."""
|
||||
def guardrail(result: TaskOutput):
|
||||
return (False, "Invalid format")
|
||||
|
||||
agent = Mock()
|
||||
agent.role = "test_agent"
|
||||
agent.execute_task.side_effect = [
|
||||
"bad result",
|
||||
"good result"
|
||||
]
|
||||
agent.crew = None
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Output",
|
||||
guardrail=guardrail,
|
||||
max_retries=1
|
||||
)
|
||||
|
||||
# First execution fails guardrail, second succeeds
|
||||
agent.execute_task.side_effect = ["bad result", "good result"]
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
task.execute_sync(agent=agent)
|
||||
|
||||
assert "Task failed guardrail validation" in str(exc_info.value)
|
||||
assert task.retry_count == 1
|
||||
|
||||
|
||||
def test_task_with_guardrail_retries():
|
||||
"""Test that guardrail respects max_retries configuration."""
|
||||
def guardrail(result: TaskOutput):
|
||||
return (False, "Invalid format")
|
||||
|
||||
agent = Mock()
|
||||
agent.role = "test_agent"
|
||||
agent.execute_task.return_value = "bad result"
|
||||
agent.crew = None
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Output",
|
||||
guardrail=guardrail,
|
||||
max_retries=2
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
task.execute_sync(agent=agent)
|
||||
|
||||
assert task.retry_count == 2
|
||||
assert "Task failed guardrail validation after 2 retries" in str(exc_info.value)
|
||||
assert "Invalid format" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_guardrail_error_in_context():
|
||||
"""Test that guardrail error is passed in context for retry."""
|
||||
def guardrail(result: TaskOutput):
|
||||
return (False, "Expected JSON, got string")
|
||||
|
||||
agent = Mock()
|
||||
agent.role = "test_agent"
|
||||
agent.crew = None
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Output",
|
||||
guardrail=guardrail,
|
||||
max_retries=1
|
||||
)
|
||||
|
||||
# Mock execute_task to succeed on second attempt
|
||||
first_call = True
|
||||
def execute_task(task, context, tools):
|
||||
nonlocal first_call
|
||||
if first_call:
|
||||
first_call = False
|
||||
return "invalid"
|
||||
return '{"valid": "json"}'
|
||||
|
||||
agent.execute_task.side_effect = execute_task
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
task.execute_sync(agent=agent)
|
||||
|
||||
assert "Task failed guardrail validation" in str(exc_info.value)
|
||||
assert "Expected JSON, got string" in str(exc_info.value)
|
||||
Reference in New Issue
Block a user