From d8571dc196a119475045ded098b35808203f1d35 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:04:12 +0000 Subject: [PATCH] feat: add ToolWithInstruction wrapper for tool-specific usage instructions (issue #2515) Co-Authored-By: Joe Moura --- README.md | 1 + docs/how-to/ToolInstructions.md | 44 +++++++++++++++ src/crewai/tools/__init__.py | 1 + src/crewai/tools/tool_with_instruction.py | 45 +++++++++++++++ tests/tools/test_tool_with_instruction.py | 67 +++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 docs/how-to/ToolInstructions.md create mode 100644 src/crewai/tools/tool_with_instruction.py create mode 100644 tests/tools/test_tool_with_instruction.py diff --git a/README.md b/README.md index 5669c71a2..6725b4712 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ In addition to the sequential process, you can use the hierarchical process, whi - **Role-Based Agent Design**: Customize agents with specific roles, goals, and tools. - **Autonomous Inter-Agent Delegation**: Agents can autonomously delegate tasks and inquire amongst themselves, enhancing problem-solving efficiency. - **Flexible Task Management**: Define tasks with customizable tools and assign them to agents dynamically. +- **Tool Instructions**: Attach specific usage instructions to tools for better control over when and how agents use them. - **Processes Driven**: Currently only supports `sequential` task execution and `hierarchical` processes, but more complex processes like consensual and autonomous are being worked on. - **Save output as file**: Save the output of individual tasks as a file, so you can use it later. - **Parse output as Pydantic or Json**: Parse the output of individual tasks as a Pydantic model or as a Json if you want to. diff --git a/docs/how-to/ToolInstructions.md b/docs/how-to/ToolInstructions.md new file mode 100644 index 000000000..a96ac4b5f --- /dev/null +++ b/docs/how-to/ToolInstructions.md @@ -0,0 +1,44 @@ +# Tool Instructions + +CrewAI allows you to provide specific instructions for when and how to use tools. This is useful when you want to guide agents on proper tool usage without cluttering their backstory. + +## Basic Usage + +```python +from crewai import Agent +from crewai_tools import ScrapeWebsiteTool +from crewai.tools import ToolWithInstruction + +# Create a tool with instructions +scrape_tool = ScrapeWebsiteTool() +scrape_with_instructions = ToolWithInstruction( + tool=scrape_tool, + instructions=""" + ALWAYS use this tool when making a joke. + NEVER use this tool when making joke about someone's mom. + """ +) + +# Use the tool with an agent +agent = Agent( + role="Comedian", + goal="Create hilarious and engaging jokes", + backstory=""" + You are a professional stand-up comedian with years of experience in crafting jokes. + You have a great sense of humor and can create jokes about any topic + while keeping them appropriate and entertaining. + """, + tools=[scrape_with_instructions], +) +``` + +## When to Use Tool Instructions + +Tool instructions are useful when: + +1. You want to specify precise conditions for tool usage +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, + not technical details about tools + +Tool instructions are semantically more correct than putting tool usage guidelines in the agent's backstory. diff --git a/src/crewai/tools/__init__.py b/src/crewai/tools/__init__.py index 41819ccbc..5e417347d 100644 --- a/src/crewai/tools/__init__.py +++ b/src/crewai/tools/__init__.py @@ -1 +1,2 @@ from .base_tool import BaseTool, tool +from .tool_with_instruction import ToolWithInstruction diff --git a/src/crewai/tools/tool_with_instruction.py b/src/crewai/tools/tool_with_instruction.py new file mode 100644 index 000000000..c49da6b5a --- /dev/null +++ b/src/crewai/tools/tool_with_instruction.py @@ -0,0 +1,45 @@ +from typing import Any, List, Optional, Dict, Callable, Union + +from pydantic import Field, model_validator + +from crewai.tools.base_tool import BaseTool +from crewai.tools.structured_tool import CrewStructuredTool + + +class ToolWithInstruction(BaseTool): + """A wrapper for tools that adds specific usage instructions. + + This allows users to provide specific instructions on when and how to use a tool, + without having to include these instructions in the agent's backstory. + + Attributes: + tool: The tool to wrap + instructions: Specific instructions about when and how to use this tool + name: Name of the tool (inherited from the wrapped tool) + description: Description of the tool (inherited from the wrapped tool with instructions) + """ + + name: str = Field(default="", description="Name of the tool") + description: str = Field(default="", description="Description of the tool") + tool: BaseTool = Field(description="The tool to wrap") + instructions: str = Field(description="Instructions about when and how to use this tool") + + @model_validator(mode="after") + def set_tool_attributes(self) -> "ToolWithInstruction": + """Set attributes from the wrapped tool.""" + self.name = self.tool.name + self.description = f"{self.tool.description}\nInstructions: {self.instructions}" + self.args_schema = self.tool.args_schema + return self + + def _run(self, *args: Any, **kwargs: Any) -> Any: + """Run the wrapped tool.""" + return self.tool._run(*args, **kwargs) + + def to_structured_tool(self) -> CrewStructuredTool: + """Convert this tool to a CrewStructuredTool instance.""" + structured_tool = self.tool.to_structured_tool() + + structured_tool.description = f"{structured_tool.description}\nInstructions: {self.instructions}" + + return structured_tool diff --git a/tests/tools/test_tool_with_instruction.py b/tests/tools/test_tool_with_instruction.py new file mode 100644 index 000000000..eaf925919 --- /dev/null +++ b/tests/tools/test_tool_with_instruction.py @@ -0,0 +1,67 @@ +import pytest +from unittest.mock import MagicMock, patch +from typing import Any, Dict, Optional + +from crewai.tools.base_tool import BaseTool, Tool +from crewai.tools.tool_with_instruction import ToolWithInstruction + + +class MockTool(BaseTool): + """Mock tool for testing.""" + name: str = "mock_tool" + description: str = "A mock tool for testing" + + def _run(self, *args: Any, **kwargs: Any) -> str: + return "mock result" + + +class TestToolWithInstruction: + """Test suite for ToolWithInstruction.""" + + def test_initialization(self): + """Test tool initialization with instructions.""" + tool = MockTool() + instructions = "Only use this tool for XYZ" + + wrapped_tool = ToolWithInstruction(tool=tool, instructions=instructions) + + assert wrapped_tool.name == tool.name + assert "Instructions: Only use this tool for XYZ" in wrapped_tool.description + assert wrapped_tool.args_schema == tool.args_schema + + def test_run_method(self): + """Test that the run method delegates to the original tool.""" + tool = MockTool() + instructions = "Only use this tool for XYZ" + + wrapped_tool = ToolWithInstruction(tool=tool, instructions=instructions) + result = wrapped_tool.run() + + assert result == "mock result" + + def test_to_structured_tool(self): + """Test that to_structured_tool includes instructions.""" + tool = MockTool() + instructions = "Only use this tool for XYZ" + + wrapped_tool = ToolWithInstruction(tool=tool, instructions=instructions) + structured_tool = wrapped_tool.to_structured_tool() + + assert "Instructions: Only use this tool for XYZ" in structured_tool.description + + def test_with_function_tool(self): + """Test tool wrapping with a function tool.""" + def sample_func(): + return "sample result" + + tool = Tool( + name="sample_tool", + description="A sample tool", + func=sample_func + ) + + instructions = "Only use this tool for XYZ" + wrapped_tool = ToolWithInstruction(tool=tool, instructions=instructions) + + assert wrapped_tool.name == tool.name + assert "Instructions: Only use this tool for XYZ" in wrapped_tool.description