mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-05 23:19:22 +00:00
Add typed output schemas for CrewAI tools (#6236)
Some checks failed
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Some checks failed
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Currently, tools have a strong input contract through `args_schema`, but no
output contract. This means that anything a tool outputs is converted to
string.
Not only the contract is weak, but the "invisible" conversion to string can
have unexpected effects when the tool returns complex objects like dicts and
arrays.
With this PR, a tool can _optionally_ define an output contract with
`output_schema`. CrewAI validates the raw result and sends the agent JSON.
```python
class ProductResult(BaseModel):
sku: str
name: str
in_stock: bool
class ProductLookupTool(BaseTool):
name: str = "Product Lookup"
description: str = "Look up product availability by SKU."
def _run(self, sku: str) -> ProductResult:
return ProductResult(sku=sku, name="USB-C dock", in_stock=True)
```
If the result does not match the schema, CrewAI warns and falls back to
`str(raw_result)` instead of failing the run:
```python
@tool("Product Lookup", output_schema=ProductResult)
def product_lookup(sku: str) -> dict[str, object]:
return {"sku": sku, "name": "USB-C dock", "in_stock": True}
#=> RuntimeWarning: Failed to validate or serialize output from tool 'Bad Product Lookup' using output_schema 'ProductResult'... Falling back to str(raw_result).
```
This is additive and non-breaking. Existing tools do not need to change. Tools
without `output_schema` keep the old string behavior. Invalid typed outputs
warn and fall back to the old formatting path.
This commit is contained in:
@@ -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**: Uses optional Pydantic models to give agents clear JSON fields while direct Python calls still receive the tool's normal return value.
|
||||
|
||||
## Using CrewAI Tools
|
||||
|
||||
@@ -184,6 +185,55 @@ class MyCustomTool(BaseTool):
|
||||
return "Tool's result"
|
||||
```
|
||||
|
||||
### Typed Tool Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This gives the agent field names it can trust, such as `sku`, `quantity`, or `needs_reorder`.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent a JSON string based on the output model.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str
|
||||
quantity: int
|
||||
needs_reorder: bool
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
|
||||
# Direct calls receive the raw Pydantic object.
|
||||
result = tool.run(sku="SKU-123")
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
To send Markdown or another short text format to the agent, override `format_output_for_agent`. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
```python Code
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Checks current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
```
|
||||
|
||||
If you do not override `format_output_for_agent`, typed outputs are sent to the agent as JSON. Plain string results work as before.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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,67 @@ 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 `result_schema`
|
||||
|
||||
If your tool returns structured data, define a Pydantic output model. This is a good default for published tools because users and agents can rely on named fields.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses the tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
CrewAI can 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:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
```
|
||||
|
||||
Set `result_schema` explicitly when your tool returns a dictionary:
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
result_schema: type[BaseModel] = GeolocateResult
|
||||
|
||||
def _run(self, address: str) -> dict[str, float]:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return {"latitude": 38.8977, "longitude": -77.0365}
|
||||
return {"latitude": 40.7128, "longitude": -74.0060}
|
||||
```
|
||||
|
||||
If agents should receive a short text summary instead of JSON, override `format_output_for_agent` on your `BaseTool` subclass.
|
||||
|
||||
```python
|
||||
class GeolocateTool(BaseTool):
|
||||
name: str = "Geolocate"
|
||||
description: str = "Converts a street address into latitude/longitude coordinates."
|
||||
|
||||
def _run(self, address: str) -> GeolocateResult:
|
||||
if "1600 Pennsylvania" in address:
|
||||
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
|
||||
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = GeolocateResult.model_validate(raw_result)
|
||||
return f"Latitude {result.latitude}, longitude {result.longitude}"
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct users of your package still receive the normal value from `tool.run(...)`.
|
||||
|
||||
### 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 +302,4 @@ agent = Agent(
|
||||
tools=[GeolocateTool()],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
@@ -53,6 +53,111 @@ def my_simple_tool(question: str) -> str:
|
||||
return "Tool output"
|
||||
```
|
||||
|
||||
### Best Practice: Define Typed Outputs
|
||||
|
||||
When a tool returns structured data, define a Pydantic output model. This helps the agent read the result as clear fields instead of guessing from plain text.
|
||||
|
||||
Typed outputs are useful for results with stable fields, such as IDs, status values, scores, prices, or lists. Plain strings are still fine for short prose results.
|
||||
|
||||
Direct Python calls still receive the value your tool returns. When an agent uses a typed tool, CrewAI sends the agent JSON based on the output model.
|
||||
|
||||
#### Return a Pydantic Model
|
||||
|
||||
CrewAI infers the output schema when your `BaseTool` has a Pydantic return annotation.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
When an agent calls `InventoryTool`, it receives JSON like this:
|
||||
|
||||
```json
|
||||
{"sku":"SKU-123","quantity":14,"needs_reorder":false}
|
||||
```
|
||||
|
||||
#### Use `result_schema` with Dictionary Results
|
||||
|
||||
If your tool returns a dictionary, set `result_schema` explicitly. You can do this on a `BaseTool` subclass or with the `@tool` decorator:
|
||||
|
||||
```python Code
|
||||
from crewai.tools import tool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ProductResult(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", result_schema=ProductResult)
|
||||
def product_lookup(sku: str) -> dict[str, object]:
|
||||
"""Look up product availability by SKU."""
|
||||
catalog = {
|
||||
"SKU-123": ("Noise-canceling headset", True),
|
||||
"SKU-456": ("USB-C dock", False),
|
||||
}
|
||||
name, in_stock = catalog.get(sku, ("Unknown product", False))
|
||||
return {
|
||||
"sku": sku,
|
||||
"name": name,
|
||||
"in_stock": in_stock,
|
||||
}
|
||||
```
|
||||
|
||||
#### Customize the Text Sent to the Agent
|
||||
|
||||
By default, typed tool outputs are sent to the agent as JSON. If the agent should receive a short summary instead, subclass `BaseTool` and override `format_output_for_agent`.
|
||||
|
||||
```python Code
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class InventoryResult(BaseModel):
|
||||
sku: str = Field(description="The product SKU.")
|
||||
quantity: int = Field(description="Units available.")
|
||||
needs_reorder: bool = Field(description="Whether the item should be reordered.")
|
||||
|
||||
class InventoryTool(BaseTool):
|
||||
name: str = "Inventory Check"
|
||||
description: str = "Check current stock for a product SKU."
|
||||
|
||||
def _run(self, sku: str) -> InventoryResult:
|
||||
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
|
||||
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = InventoryResult.model_validate(raw_result)
|
||||
status = "reorder needed" if result.needs_reorder else "stock is healthy"
|
||||
return f"{result.sku}: {result.quantity} units. {status}."
|
||||
|
||||
tool = InventoryTool()
|
||||
result = tool.run(sku="SKU-123")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.quantity)
|
||||
```
|
||||
|
||||
The override only changes what the agent sees. Direct calls to `tool.run(...)` still return the normal Python value.
|
||||
|
||||
### Defining a Cache Function for the Tool
|
||||
|
||||
To optimize tool performance with caching, define custom caching strategies using the `cache_function` attribute.
|
||||
|
||||
@@ -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 string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. `raw_tool_result` is the original Python value returned by the tool.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Safety and Validation
|
||||
|
||||
@@ -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 string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. Use `raw_tool_result` when your hook needs the typed object or dictionary.
|
||||
|
||||
### Modifying Tool Inputs
|
||||
|
||||
**Important:** Always modify tool inputs in-place:
|
||||
|
||||
Reference in New Issue
Block a user