Add tool output schema docs

This commit is contained in:
Vinicius Brasil
2026-06-18 21:29:24 -07:00
parent 58fc692b03
commit 8c35dedfb5
5 changed files with 173 additions and 4 deletions

View File

@@ -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.

View File

@@ -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()],
# ...
)
```
```

View File

@@ -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.

View File

@@ -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

View File

@@ -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: