diff --git a/docs/edge/en/concepts/tools.mdx b/docs/edge/en/concepts/tools.mdx index 52e568073..f3506fc32 100644 --- a/docs/edge/en/concepts/tools.mdx +++ b/docs/edge/en/concepts/tools.mdx @@ -39,6 +39,7 @@ The Enterprise Tools Repository includes: - **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation. - **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations. - **Asynchronous Support**: Handles both synchronous and asynchronous tools, enabling non-blocking operations. +- **Typed Outputs**: Optionally validates tool results with Pydantic models and sends agents a JSON-safe representation while preserving the raw Python value for direct calls and hooks. ## Using CrewAI Tools @@ -184,6 +185,34 @@ class MyCustomTool(BaseTool): return "Tool's result" ``` +### Typed Tool Outputs + +As a best practice, define a Pydantic output model when a tool returns structured data. CrewAI keeps `tool.run(...)` unchanged: it returns the raw Python value from `_run`. During agent execution, CrewAI validates that raw value against the tool's `output_schema` and sends the agent a JSON string. + +```python Code +from crewai.tools import BaseTool +from pydantic import BaseModel + +class SearchResult(BaseModel): + query: str + score: float + +class SearchTool(BaseTool): + name: str = "Search" + description: str = "Searches for a query and returns the top match score." + + def _run(self, query: str) -> SearchResult: + return SearchResult(query=query, score=0.97) + +tool = SearchTool() + +# Direct calls receive the raw Pydantic object. +raw_result = tool.run(query="CrewAI") +print(raw_result.score) +``` + +If validation or serialization fails during agent execution, CrewAI emits a runtime warning and falls back to `str(raw_result)` for the agent-facing text. Direct tool calls still receive the raw result. + ## Asynchronous Tool Support CrewAI supports asynchronous tools, allowing you to implement tools that perform non-blocking operations like network requests, file I/O, or other async operations without blocking the main execution thread. diff --git a/docs/edge/en/guides/tools/publish-custom-tools.mdx b/docs/edge/en/guides/tools/publish-custom-tools.mdx index 973856816..de7109f88 100644 --- a/docs/edge/en/guides/tools/publish-custom-tools.mdx +++ b/docs/edge/en/guides/tools/publish-custom-tools.mdx @@ -65,7 +65,7 @@ Regardless of which approach you use, your tool must: - Have a **`description`** — tells the agent when and how to use the tool. This directly affects how well agents use your tool, so be clear and specific. - Implement **`_run`** (BaseTool) or provide a **function body** (@tool) — the synchronous execution logic. - Use **type annotations** on all parameters and return values. -- Return a **string** result (or something that can be meaningfully converted to one). +- Return a **string** result, or define an optional Pydantic output schema for structured results. ### Optional: Async Support @@ -104,6 +104,44 @@ class TranslateInput(BaseModel): Explicit schemas are recommended for published tools — they produce better agent behavior and clearer documentation for your users. +### Optional: Typed Outputs with `output_schema` + +If your tool returns structured data, define a Pydantic output model. This is a best practice for published tools because it gives agents a predictable JSON shape while preserving the raw Python value for direct users of your package. + +CrewAI keeps direct execution unchanged: `tool.run(...)` returns the raw value from your tool. During agent execution, CrewAI validates that raw value against the output schema and sends the agent a JSON string. If validation or serialization fails, CrewAI warns and falls back to `str(raw_result)` for the agent-facing text. + +You can let CrewAI infer the output schema from a Pydantic return annotation: + +```python +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class GeolocateResult(BaseModel): + latitude: float = Field(..., description="Latitude in decimal degrees.") + longitude: float = Field(..., description="Longitude in decimal degrees.") + + +class GeolocateTool(BaseTool): + name: str = "Geolocate" + description: str = "Converts a street address into latitude/longitude coordinates." + + def _run(self, address: str) -> GeolocateResult: + return GeolocateResult(latitude=40.7128, longitude=-74.0060) +``` + +Or set `output_schema` explicitly when your implementation returns a dictionary: + +```python +class GeolocateTool(BaseTool): + name: str = "Geolocate" + description: str = "Converts a street address into latitude/longitude coordinates." + output_schema: type[BaseModel] = GeolocateResult + + def _run(self, address: str) -> dict[str, float]: + return {"latitude": 40.7128, "longitude": -74.0060} +``` + ### Optional: Environment Variables If your tool requires API keys or other configuration, declare them with `env_vars` so users know what to set: @@ -241,4 +279,4 @@ agent = Agent( tools=[GeolocateTool()], # ... ) -``` \ No newline at end of file +``` diff --git a/docs/edge/en/learn/create-custom-tools.mdx b/docs/edge/en/learn/create-custom-tools.mdx index c1246f3fc..14d1b7588 100644 --- a/docs/edge/en/learn/create-custom-tools.mdx +++ b/docs/edge/en/learn/create-custom-tools.mdx @@ -53,6 +53,102 @@ def my_simple_tool(question: str) -> str: return "Tool output" ``` +### Best Practice: Define Typed Outputs + +When a tool returns structured data, define the output as a Pydantic model. This is optional, but recommended because it gives CrewAI a clear contract for the data your tool returns. + +Typed outputs create a clear split between direct Python usage and agent execution: + +- `tool.run(...)` returns the raw Python value from your tool. +- Agent execution validates the raw value with the tool's output schema and sends the agent an LLM-safe string. +- Valid Pydantic outputs are serialized to JSON for the agent. +- If validation or serialization fails, CrewAI emits a runtime warning and falls back to `str(raw_result)` only for the agent-facing text. + +#### Return a Pydantic Model + +CrewAI infers the output schema when your `BaseTool` or `@tool` function has a Pydantic return annotation. + +```python Code +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class SentimentResult(BaseModel): + label: str = Field(description="The sentiment label, such as positive, neutral, or negative.") + confidence: float = Field(description="Confidence score from 0 to 1.") + +class SentimentTool(BaseTool): + name: str = "Sentiment Analyzer" + description: str = "Analyze the sentiment of a short text passage." + + def _run(self, text: str) -> SentimentResult: + # Replace this with your model, API call, or business logic. + return SentimentResult(label="positive", confidence=0.92) + +tool = SentimentTool() +result = tool.run(text="CrewAI makes multi-agent workflows easier.") + +# Direct Python calls receive the raw Pydantic object. +print(result.label) +print(result.confidence) +``` + +When an agent calls `SentimentTool`, it receives JSON like this: + +```json +{"label":"positive","confidence":0.92} +``` + +This is easier for the agent to reason over than a Python object representation. + +#### Use `output_schema` for Dictionary Results + +If your implementation naturally returns a dictionary, set `output_schema` explicitly. CrewAI validates the dictionary and serializes the validated result to JSON for the agent. + +```python Code +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class ProductLookupResult(BaseModel): + sku: str = Field(description="The product SKU.") + name: str = Field(description="The product name.") + in_stock: bool = Field(description="Whether the product is available.") + +class ProductLookupTool(BaseTool): + name: str = "Product Lookup" + description: str = "Look up product availability by SKU." + output_schema: type[BaseModel] = ProductLookupResult + + def _run(self, sku: str) -> dict[str, object]: + return { + "sku": sku, + "name": "CrewAI Enterprise License", + "in_stock": True, + } +``` + +You can use the same pattern with the `@tool` decorator: + +```python Code +from crewai.tools import tool +from pydantic import BaseModel, Field + +class ProductLookupResult(BaseModel): + sku: str = Field(description="The product SKU.") + name: str = Field(description="The product name.") + in_stock: bool = Field(description="Whether the product is available.") + +@tool("Product Lookup", output_schema=ProductLookupResult) +def product_lookup(sku: str) -> dict[str, object]: + """Look up product availability by SKU.""" + return { + "sku": sku, + "name": "CrewAI Enterprise License", + "in_stock": True, + } +``` + +Use typed outputs for tool results that have stable fields, nested data, lists, IDs, status values, scores, or any structure the agent should interpret precisely. Plain strings are still fine for simple prose results. + ### Defining a Cache Function for the Tool To optimize tool performance with caching, define custom caching strategies using the `cache_function` attribute. diff --git a/docs/edge/en/learn/execution-hooks.mdx b/docs/edge/en/learn/execution-hooks.mdx index 74234db97..15ca0b68e 100644 --- a/docs/edge/en/learn/execution-hooks.mdx +++ b/docs/edge/en/learn/execution-hooks.mdx @@ -195,9 +195,12 @@ class ToolCallHookContext: agent: Agent | None # Agent executing task: Task | None # Current task crew: Crew | None # Crew instance - tool_result: str | None # Tool result (after hooks) + tool_result: str | None # Agent-facing result string (after hooks) + raw_tool_result: Any | None # Raw Python result (after hooks) ``` +For typed tool outputs, `tool_result` is the JSON string sent to the agent, while `raw_tool_result` is the original Python value returned by the tool. + ## Common Patterns ### Safety and Validation diff --git a/docs/edge/en/learn/tool-hooks.mdx b/docs/edge/en/learn/tool-hooks.mdx index d1d727a5c..4da3bf8cc 100644 --- a/docs/edge/en/learn/tool-hooks.mdx +++ b/docs/edge/en/learn/tool-hooks.mdx @@ -60,9 +60,12 @@ class ToolCallHookContext: agent: Agent | BaseAgent | None # Agent executing the tool task: Task | None # Current task crew: Crew | None # Crew instance - tool_result: str | None # Tool result (after hooks only) + tool_result: str | None # Agent-facing result string (after hooks only) + raw_tool_result: Any | None # Raw Python result (after hooks only) ``` +For typed tool outputs, `tool_result` is the JSON string sent to the agent, while `raw_tool_result` is the original Python value returned by the tool. Use `raw_tool_result` when your hook needs the typed object or dictionary; return a string from the hook only when you want to change the agent-facing result. + ### Modifying Tool Inputs **Important:** Always modify tool inputs in-place: