mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-10 16:48:30 +00:00
fix: address PR feedback with improved validation, documentation, and tests
Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
@@ -32,6 +32,113 @@ agent = Agent(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Real-World Examples
|
||||||
|
|
||||||
|
### Example 1: Research Assistant with Web Search Tool
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai import Agent
|
||||||
|
from crewai_tools import SearchTool
|
||||||
|
from crewai.tools import ToolWithInstruction
|
||||||
|
|
||||||
|
search_tool = SearchTool()
|
||||||
|
search_with_instructions = ToolWithInstruction(
|
||||||
|
tool=search_tool,
|
||||||
|
instructions="""
|
||||||
|
Use this tool ONLY for factual information that requires up-to-date data.
|
||||||
|
ALWAYS verify information by searching multiple sources.
|
||||||
|
DO NOT use this tool for speculative questions or opinions.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
research_agent = Agent(
|
||||||
|
role="Research Analyst",
|
||||||
|
goal="Provide accurate and well-sourced information",
|
||||||
|
backstory="You are a meticulous research analyst with attention to detail and fact-checking.",
|
||||||
|
tools=[search_with_instructions],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Data Scientist with Multiple Analysis Tools
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai import Agent
|
||||||
|
from crewai_tools import PythonTool, DataVisualizationTool
|
||||||
|
from crewai.tools import ToolWithInstruction
|
||||||
|
|
||||||
|
# Python tool for data processing
|
||||||
|
python_tool = PythonTool()
|
||||||
|
python_with_instructions = ToolWithInstruction(
|
||||||
|
tool=python_tool,
|
||||||
|
instructions="""
|
||||||
|
Use this tool for data cleaning, transformation, and statistical analysis.
|
||||||
|
ALWAYS include comments in your code.
|
||||||
|
DO NOT use this tool for creating visualizations.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Visualization tool
|
||||||
|
viz_tool = DataVisualizationTool()
|
||||||
|
viz_with_instructions = ToolWithInstruction(
|
||||||
|
tool=viz_tool,
|
||||||
|
instructions="""
|
||||||
|
Use this tool ONLY for creating data visualizations.
|
||||||
|
ALWAYS label axes and include titles in your charts.
|
||||||
|
PREFER simple visualizations that clearly communicate the main insight.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
data_scientist = Agent(
|
||||||
|
role="Data Scientist",
|
||||||
|
goal="Analyze data and create insightful visualizations",
|
||||||
|
backstory="You are an experienced data scientist who excels at finding patterns in data.",
|
||||||
|
tools=[python_with_instructions, viz_with_instructions],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Instructions Are Presented to Agents
|
||||||
|
|
||||||
|
When an agent considers using a tool, the instructions are included in the tool's description. For example, a tool with instructions might appear to the agent like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tool: search_web
|
||||||
|
Description: Search the web for information on a given topic.
|
||||||
|
Instructions: Use this tool ONLY for factual information that requires up-to-date data.
|
||||||
|
ALWAYS verify information by searching multiple sources.
|
||||||
|
DO NOT use this tool for speculative questions or opinions.
|
||||||
|
```
|
||||||
|
|
||||||
|
This clear presentation helps the agent understand when and how to use the tool appropriately.
|
||||||
|
|
||||||
|
## Dynamically Updating Instructions
|
||||||
|
|
||||||
|
You can update tool instructions dynamically during execution:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create a tool with initial instructions
|
||||||
|
search_with_instructions = ToolWithInstruction(
|
||||||
|
tool=search_tool,
|
||||||
|
instructions="Initial instructions for tool usage"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Later, update the instructions based on new requirements
|
||||||
|
search_with_instructions.update_instructions("Updated instructions for tool usage")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling and Best Practices
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
The `ToolWithInstruction` class includes validation to ensure instructions are not empty and don't exceed a maximum length. If you provide invalid instructions, a `ValueError` will be raised.
|
||||||
|
|
||||||
|
### Best Practices for Writing Instructions
|
||||||
|
|
||||||
|
1. **Be specific and clear** about when to use and when not to use the tool
|
||||||
|
2. **Use imperative language** like "ALWAYS", "NEVER", "USE", "DO NOT USE"
|
||||||
|
3. **Keep instructions concise** but comprehensive
|
||||||
|
4. **Include examples** of good and bad usage scenarios when possible
|
||||||
|
5. **Format instructions** with line breaks for readability
|
||||||
|
|
||||||
## When to Use Tool Instructions
|
## When to Use Tool Instructions
|
||||||
|
|
||||||
Tool instructions are useful when:
|
Tool instructions are useful when:
|
||||||
@@ -40,5 +147,7 @@ Tool instructions are useful when:
|
|||||||
2. You have multiple similar tools that should be used in different situations
|
2. You have multiple similar tools that should be used in different situations
|
||||||
3. You want to keep the agent's backstory focused on its role and personality,
|
3. You want to keep the agent's backstory focused on its role and personality,
|
||||||
not technical details about tools
|
not technical details about tools
|
||||||
|
4. You need to provide technical guidance on how to format inputs or interpret outputs
|
||||||
|
5. You want to enforce consistent tool usage across multiple agents
|
||||||
|
|
||||||
Tool instructions are semantically more correct than putting tool usage guidelines in the agent's backstory.
|
Tool instructions are semantically more correct than putting tool usage guidelines in the agent's backstory.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Any, List, Optional, Dict, Callable, Union
|
from typing import Any, List, Optional, Dict, Callable, Union, ClassVar
|
||||||
|
|
||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator, field_validator, ConfigDict
|
||||||
|
|
||||||
from crewai.tools.base_tool import BaseTool
|
from crewai.tools.base_tool import BaseTool
|
||||||
from crewai.tools.structured_tool import CrewStructuredTool
|
from crewai.tools.structured_tool import CrewStructuredTool
|
||||||
@@ -19,25 +19,90 @@ class ToolWithInstruction(BaseTool):
|
|||||||
description: Description of the tool (inherited from the wrapped tool with instructions)
|
description: Description of the tool (inherited from the wrapped tool with instructions)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MAX_INSTRUCTION_LENGTH: ClassVar[int] = 2000
|
||||||
|
|
||||||
name: str = Field(default="", description="Name of the tool")
|
name: str = Field(default="", description="Name of the tool")
|
||||||
description: str = Field(default="", description="Description of the tool")
|
description: str = Field(default="", description="Description of the tool")
|
||||||
tool: BaseTool = Field(description="The tool to wrap")
|
tool: BaseTool = Field(description="The tool to wrap")
|
||||||
instructions: str = Field(description="Instructions about when and how to use this tool")
|
instructions: str = Field(description="Instructions about when and how to use this tool")
|
||||||
|
|
||||||
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
@field_validator("instructions")
|
||||||
|
@classmethod
|
||||||
|
def validate_instructions(cls, value: str) -> str:
|
||||||
|
"""Validate that instructions are not empty and not too long.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The instructions string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The validated and sanitized instructions
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If instructions are empty or exceed maximum length
|
||||||
|
"""
|
||||||
|
if not value or not value.strip():
|
||||||
|
raise ValueError("Instructions cannot be empty")
|
||||||
|
|
||||||
|
if len(value) > cls.MAX_INSTRUCTION_LENGTH:
|
||||||
|
raise ValueError(
|
||||||
|
f"Instructions exceed maximum length of {cls.MAX_INSTRUCTION_LENGTH} characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def set_tool_attributes(self) -> "ToolWithInstruction":
|
def set_tool_attributes(self) -> "ToolWithInstruction":
|
||||||
"""Set attributes from the wrapped tool."""
|
"""Sets name, description, and args_schema from the wrapped tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolWithInstruction: The validated instance with updated attributes.
|
||||||
|
"""
|
||||||
self.name = self.tool.name
|
self.name = self.tool.name
|
||||||
self.description = f"{self.tool.description}\nInstructions: {self.instructions}"
|
self.description = f"{self.tool.description}\nInstructions: {self.instructions}"
|
||||||
self.args_schema = self.tool.args_schema
|
self.args_schema = self.tool.args_schema
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def update_instructions(self, new_instructions: str) -> None:
|
||||||
|
"""Updates the tool's usage instructions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_instructions (str): New instructions for tool usage.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If new instructions are empty or exceed maximum length
|
||||||
|
"""
|
||||||
|
if not new_instructions or not new_instructions.strip():
|
||||||
|
raise ValueError("Instructions cannot be empty")
|
||||||
|
|
||||||
|
if len(new_instructions) > self.MAX_INSTRUCTION_LENGTH:
|
||||||
|
raise ValueError(
|
||||||
|
f"Instructions exceed maximum length of {self.MAX_INSTRUCTION_LENGTH} characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.instructions = new_instructions.strip()
|
||||||
|
|
||||||
|
self.description = f"{self.tool.description}\nInstructions: {self.instructions}"
|
||||||
|
|
||||||
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""Run the wrapped tool."""
|
"""Run the wrapped tool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args: Positional arguments to pass to the wrapped tool
|
||||||
|
**kwargs: Keyword arguments to pass to the wrapped tool
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: The result from the wrapped tool's _run method
|
||||||
|
"""
|
||||||
return self.tool._run(*args, **kwargs)
|
return self.tool._run(*args, **kwargs)
|
||||||
|
|
||||||
def to_structured_tool(self) -> CrewStructuredTool:
|
def to_structured_tool(self) -> CrewStructuredTool:
|
||||||
"""Convert this tool to a CrewStructuredTool instance."""
|
"""Convert this tool to a CrewStructuredTool instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CrewStructuredTool: A structured tool with instructions included in the description
|
||||||
|
"""
|
||||||
structured_tool = self.tool.to_structured_tool()
|
structured_tool = self.tool.to_structured_tool()
|
||||||
|
|
||||||
structured_tool.description = f"{structured_tool.description}\nInstructions: {self.instructions}"
|
structured_tool.description = f"{structured_tool.description}\nInstructions: {self.instructions}"
|
||||||
|
|||||||
@@ -65,3 +65,46 @@ class TestToolWithInstruction:
|
|||||||
|
|
||||||
assert wrapped_tool.name == tool.name
|
assert wrapped_tool.name == tool.name
|
||||||
assert "Instructions: Only use this tool for XYZ" in wrapped_tool.description
|
assert "Instructions: Only use this tool for XYZ" in wrapped_tool.description
|
||||||
|
|
||||||
|
def test_empty_instructions(self):
|
||||||
|
"""Test that empty instructions raise ValueError."""
|
||||||
|
tool = MockTool()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Instructions cannot be empty"):
|
||||||
|
ToolWithInstruction(tool=tool, instructions="")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Instructions cannot be empty"):
|
||||||
|
ToolWithInstruction(tool=tool, instructions=" ")
|
||||||
|
|
||||||
|
def test_too_long_instructions(self):
|
||||||
|
"""Test that instructions exceeding maximum length raise ValueError."""
|
||||||
|
tool = MockTool()
|
||||||
|
long_instructions = "x" * (ToolWithInstruction.MAX_INSTRUCTION_LENGTH + 1)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Instructions exceed maximum length"):
|
||||||
|
ToolWithInstruction(tool=tool, instructions=long_instructions)
|
||||||
|
|
||||||
|
def test_update_instructions(self):
|
||||||
|
"""Test updating instructions dynamically."""
|
||||||
|
tool = MockTool()
|
||||||
|
initial_instructions = "Initial instructions"
|
||||||
|
new_instructions = "Updated instructions"
|
||||||
|
|
||||||
|
wrapped_tool = ToolWithInstruction(tool=tool, instructions=initial_instructions)
|
||||||
|
assert "Instructions: Initial instructions" in wrapped_tool.description
|
||||||
|
|
||||||
|
wrapped_tool.update_instructions(new_instructions)
|
||||||
|
assert "Instructions: Updated instructions" in wrapped_tool.description
|
||||||
|
assert wrapped_tool.instructions == new_instructions
|
||||||
|
|
||||||
|
def test_update_instructions_validation(self):
|
||||||
|
"""Test validation when updating instructions."""
|
||||||
|
tool = MockTool()
|
||||||
|
wrapped_tool = ToolWithInstruction(tool=tool, instructions="Valid instructions")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Instructions cannot be empty"):
|
||||||
|
wrapped_tool.update_instructions("")
|
||||||
|
|
||||||
|
long_instructions = "x" * (ToolWithInstruction.MAX_INSTRUCTION_LENGTH + 1)
|
||||||
|
with pytest.raises(ValueError, match="Instructions exceed maximum length"):
|
||||||
|
wrapped_tool.update_instructions(long_instructions)
|
||||||
|
|||||||
Reference in New Issue
Block a user