Skip to content

Commit facb804

Browse files
committed
Address code review comments
1 parent ce3edf8 commit facb804

13 files changed

Lines changed: 233 additions & 145 deletions

File tree

docs/edge/en/concepts/tools.mdx

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The Enterprise Tools Repository includes:
3939
- **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation.
4040
- **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations.
4141
- **Asynchronous Support**: Handles both synchronous and asynchronous tools, enabling non-blocking operations.
42-
- **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.
42+
- **Typed Outputs**: Uses optional Pydantic models to give agents clear JSON fields while direct Python calls still receive the tool's normal return value.
4343

4444
## Using CrewAI Tools
4545

@@ -187,48 +187,52 @@ class MyCustomTool(BaseTool):
187187

188188
### Typed Tool Outputs
189189

190-
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.
190+
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`.
191+
192+
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.
191193

192194
```python Code
193195
from crewai.tools import BaseTool
194196
from pydantic import BaseModel
195197

196-
class SearchResult(BaseModel):
197-
query: str
198-
score: float
198+
class InventoryResult(BaseModel):
199+
sku: str
200+
quantity: int
201+
needs_reorder: bool
199202

200-
class SearchTool(BaseTool):
201-
name: str = "Search"
202-
description: str = "Searches for a query and returns the top match score."
203+
class InventoryTool(BaseTool):
204+
name: str = "Inventory Check"
205+
description: str = "Checks current stock for a product SKU."
203206

204-
def _run(self, query: str) -> SearchResult:
205-
return SearchResult(query=query, score=0.97)
207+
def _run(self, sku: str) -> InventoryResult:
208+
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
209+
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
206210

207-
tool = SearchTool()
211+
tool = InventoryTool()
208212

209213
# Direct calls receive the raw Pydantic object.
210-
raw_result = tool.run(query="CrewAI")
211-
print(raw_result.score)
214+
result = tool.run(sku="SKU-123")
215+
print(result.quantity)
212216
```
213217

214-
To send a custom representation to the agent, such as Markdown, override `format_output_for_agent` on your `BaseTool` subclass. This does not change direct execution: `tool.run(...)` still returns the raw Python value.
218+
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.
215219

216220
```python Code
217-
class SearchTool(BaseTool):
218-
name: str = "Search"
219-
description: str = "Searches for a query and returns the top match score."
221+
class InventoryTool(BaseTool):
222+
name: str = "Inventory Check"
223+
description: str = "Checks current stock for a product SKU."
220224

221-
def _run(self, query: str) -> SearchResult:
222-
return SearchResult(query=query, score=0.97)
225+
def _run(self, sku: str) -> InventoryResult:
226+
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
227+
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
223228

224229
def format_output_for_agent(self, raw_result: object) -> str:
225-
result = SearchResult.model_validate(raw_result)
226-
return f"### Search result\n\n- Query: `{result.query}`\n- Score: {result.score}"
230+
result = InventoryResult.model_validate(raw_result)
231+
status = "reorder needed" if result.needs_reorder else "stock is healthy"
232+
return f"{result.sku}: {result.quantity} units. {status}."
227233
```
228234

229-
If you do not override `format_output_for_agent`, CrewAI uses the default typed-output behavior: Pydantic outputs become JSON for the agent, and untyped outputs use `str(raw_result)`.
230-
231-
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.
235+
If you do not override `format_output_for_agent`, typed outputs are sent to the agent as JSON. Plain string results work as before.
232236

233237
## Asynchronous Tool Support
234238

docs/edge/en/guides/tools/publish-custom-tools.mdx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ Explicit schemas are recommended for published tools — they produce better age
106106

107107
### Optional: Typed Outputs with `output_schema`
108108

109-
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.
109+
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.
110110

111-
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.
111+
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.
112112

113-
You can let CrewAI infer the output schema from a Pydantic return annotation:
113+
CrewAI can infer the output schema from a Pydantic return annotation:
114114

115115
```python
116116
from crewai.tools import BaseTool
@@ -127,10 +127,12 @@ class GeolocateTool(BaseTool):
127127
description: str = "Converts a street address into latitude/longitude coordinates."
128128

129129
def _run(self, address: str) -> GeolocateResult:
130+
if "1600 Pennsylvania" in address:
131+
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
130132
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
131133
```
132134

133-
Or set `output_schema` explicitly when your implementation returns a dictionary:
135+
Set `output_schema` explicitly when your tool returns a dictionary:
134136

135137
```python
136138
class GeolocateTool(BaseTool):
@@ -139,29 +141,29 @@ class GeolocateTool(BaseTool):
139141
output_schema: type[BaseModel] = GeolocateResult
140142

141143
def _run(self, address: str) -> dict[str, float]:
144+
if "1600 Pennsylvania" in address:
145+
return {"latitude": 38.8977, "longitude": -77.0365}
142146
return {"latitude": 40.7128, "longitude": -74.0060}
143147
```
144148

145-
If agents should receive a custom text format instead of JSON, override `format_output_for_agent` on your `BaseTool` subclass. This is useful when the best agent-facing representation is Markdown, a terse summary, or another format derived from the same raw result.
149+
If agents should receive a short text summary instead of JSON, override `format_output_for_agent` on your `BaseTool` subclass.
146150

147151
```python
148152
class GeolocateTool(BaseTool):
149153
name: str = "Geolocate"
150154
description: str = "Converts a street address into latitude/longitude coordinates."
151155

152156
def _run(self, address: str) -> GeolocateResult:
157+
if "1600 Pennsylvania" in address:
158+
return GeolocateResult(latitude=38.8977, longitude=-77.0365)
153159
return GeolocateResult(latitude=40.7128, longitude=-74.0060)
154160

155161
def format_output_for_agent(self, raw_result: object) -> str:
156162
result = GeolocateResult.model_validate(raw_result)
157-
return (
158-
f"### Coordinates\n\n"
159-
f"- Latitude: `{result.latitude}`\n"
160-
f"- Longitude: `{result.longitude}`"
161-
)
163+
return f"Latitude {result.latitude}, longitude {result.longitude}"
162164
```
163165

164-
The override only controls the text sent to the agent. Direct users of your package still receive the raw value from `tool.run(...)`.
166+
The override only changes what the agent sees. Direct users of your package still receive the normal value from `tool.run(...)`.
165167

166168
### Optional: Environment Variables
167169

docs/edge/en/learn/create-custom-tools.mdx

Lines changed: 47 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -55,143 +55,108 @@ def my_simple_tool(question: str) -> str:
5555

5656
### Best Practice: Define Typed Outputs
5757

58-
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.
58+
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.
5959

60-
Typed outputs create a clear split between direct Python usage and agent execution:
60+
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.
6161

62-
- `tool.run(...)` returns the raw Python value from your tool.
63-
- Agent execution validates the raw value with the tool's output schema and sends the agent an LLM-safe string.
64-
- Valid Pydantic outputs are serialized to JSON for the agent.
65-
- If validation or serialization fails, CrewAI emits a runtime warning and falls back to `str(raw_result)` only for the agent-facing text.
62+
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.
6663

6764
#### Return a Pydantic Model
6865

69-
CrewAI infers the output schema when your `BaseTool` or `@tool` function has a Pydantic return annotation.
66+
CrewAI infers the output schema when your `BaseTool` has a Pydantic return annotation.
7067

7168
```python Code
7269
from crewai.tools import BaseTool
7370
from pydantic import BaseModel, Field
7471

75-
class SentimentResult(BaseModel):
76-
label: str = Field(description="The sentiment label, such as positive, neutral, or negative.")
77-
confidence: float = Field(description="Confidence score from 0 to 1.")
72+
class InventoryResult(BaseModel):
73+
sku: str = Field(description="The product SKU.")
74+
quantity: int = Field(description="Units available.")
75+
needs_reorder: bool = Field(description="Whether the item should be reordered.")
7876

79-
class SentimentTool(BaseTool):
80-
name: str = "Sentiment Analyzer"
81-
description: str = "Analyze the sentiment of a short text passage."
77+
class InventoryTool(BaseTool):
78+
name: str = "Inventory Check"
79+
description: str = "Check current stock for a product SKU."
8280

83-
def _run(self, text: str) -> SentimentResult:
84-
# Replace this with your model, API call, or business logic.
85-
return SentimentResult(label="positive", confidence=0.92)
81+
def _run(self, sku: str) -> InventoryResult:
82+
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
83+
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
8684

87-
tool = SentimentTool()
88-
result = tool.run(text="CrewAI makes multi-agent workflows easier.")
85+
tool = InventoryTool()
86+
result = tool.run(sku="SKU-123")
8987

9088
# Direct Python calls receive the raw Pydantic object.
91-
print(result.label)
92-
print(result.confidence)
89+
print(result.quantity)
9390
```
9491

95-
When an agent calls `SentimentTool`, it receives JSON like this:
92+
When an agent calls `InventoryTool`, it receives JSON like this:
9693

9794
```json
98-
{"label":"positive","confidence":0.92}
95+
{"sku":"SKU-123","quantity":14,"needs_reorder":false}
9996
```
10097

101-
This is easier for the agent to reason over than a Python object representation.
102-
103-
#### Use `output_schema` for Dictionary Results
104-
105-
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.
106-
107-
```python Code
108-
from crewai.tools import BaseTool
109-
from pydantic import BaseModel, Field
110-
111-
class ProductLookupResult(BaseModel):
112-
sku: str = Field(description="The product SKU.")
113-
name: str = Field(description="The product name.")
114-
in_stock: bool = Field(description="Whether the product is available.")
115-
116-
class ProductLookupTool(BaseTool):
117-
name: str = "Product Lookup"
118-
description: str = "Look up product availability by SKU."
119-
output_schema: type[BaseModel] = ProductLookupResult
120-
121-
def _run(self, sku: str) -> dict[str, object]:
122-
return {
123-
"sku": sku,
124-
"name": "CrewAI Enterprise License",
125-
"in_stock": True,
126-
}
127-
```
98+
#### Use `output_schema` with Dictionary Results
12899

129-
You can use the same pattern with the `@tool` decorator:
100+
If your tool returns a dictionary, set `output_schema` explicitly. You can do this on a `BaseTool` subclass or with the `@tool` decorator:
130101

131102
```python Code
132103
from crewai.tools import tool
133104
from pydantic import BaseModel, Field
134105

135-
class ProductLookupResult(BaseModel):
106+
class ProductResult(BaseModel):
136107
sku: str = Field(description="The product SKU.")
137108
name: str = Field(description="The product name.")
138109
in_stock: bool = Field(description="Whether the product is available.")
139110

140-
@tool("Product Lookup", output_schema=ProductLookupResult)
111+
@tool("Product Lookup", output_schema=ProductResult)
141112
def product_lookup(sku: str) -> dict[str, object]:
142113
"""Look up product availability by SKU."""
114+
catalog = {
115+
"SKU-123": ("Noise-canceling headset", True),
116+
"SKU-456": ("USB-C dock", False),
117+
}
118+
name, in_stock = catalog.get(sku, ("Unknown product", False))
143119
return {
144120
"sku": sku,
145-
"name": "CrewAI Enterprise License",
146-
"in_stock": True,
121+
"name": name,
122+
"in_stock": in_stock,
147123
}
148124
```
149125

150126
#### Customize the Text Sent to the Agent
151127

152-
By default, typed tool outputs are sent to the agent as JSON. If your agent should receive Markdown, XML, or a compact human-readable summary instead, subclass `BaseTool` and override `format_output_for_agent`.
153-
154-
This only changes the agent-facing text. Direct calls to `tool.run(...)` still return the raw Python value from `_run`.
128+
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`.
155129

156130
```python Code
157131
from crewai.tools import BaseTool
158132
from pydantic import BaseModel, Field
159133

160-
class ProductLookupResult(BaseModel):
134+
class InventoryResult(BaseModel):
161135
sku: str = Field(description="The product SKU.")
162-
name: str = Field(description="The product name.")
163-
in_stock: bool = Field(description="Whether the product is available.")
136+
quantity: int = Field(description="Units available.")
137+
needs_reorder: bool = Field(description="Whether the item should be reordered.")
164138

165-
class ProductLookupTool(BaseTool):
166-
name: str = "Product Lookup"
167-
description: str = "Look up product availability by SKU."
139+
class InventoryTool(BaseTool):
140+
name: str = "Inventory Check"
141+
description: str = "Check current stock for a product SKU."
168142

169-
def _run(self, sku: str) -> ProductLookupResult:
170-
return ProductLookupResult(
171-
sku=sku,
172-
name="CrewAI Enterprise License",
173-
in_stock=True,
174-
)
143+
def _run(self, sku: str) -> InventoryResult:
144+
quantity = {"SKU-123": 14, "SKU-456": 0}.get(sku, 0)
145+
return InventoryResult(sku=sku, quantity=quantity, needs_reorder=quantity < 5)
175146

176147
def format_output_for_agent(self, raw_result: object) -> str:
177-
result = ProductLookupResult.model_validate(raw_result)
178-
status = "in stock" if result.in_stock else "out of stock"
179-
return (
180-
f"### {result.name}\n\n"
181-
f"- SKU: `{result.sku}`\n"
182-
f"- Status: **{status}**"
183-
)
148+
result = InventoryResult.model_validate(raw_result)
149+
status = "reorder needed" if result.needs_reorder else "stock is healthy"
150+
return f"{result.sku}: {result.quantity} units. {status}."
184151

185-
tool = ProductLookupTool()
186-
result = tool.run(sku="CREW-ENT")
152+
tool = InventoryTool()
153+
result = tool.run(sku="SKU-123")
187154

188155
# Direct Python calls receive the raw Pydantic object.
189-
print(result.name)
156+
print(result.quantity)
190157
```
191158

192-
When an agent calls `ProductLookupTool`, it receives the Markdown returned by `format_output_for_agent`. When you do not override this method, CrewAI uses the default behavior: validate typed outputs and serialize them to JSON, or use `str(raw_result)` for untyped outputs.
193-
194-
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.
159+
The override only changes what the agent sees. Direct calls to `tool.run(...)` still return the normal Python value.
195160

196161
### Defining a Cache Function for the Tool
197162

docs/edge/en/learn/execution-hooks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class ToolCallHookContext:
199199
raw_tool_result: Any | None # Raw Python result (after hooks)
200200
```
201201

202-
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.
202+
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.
203203

204204
## Common Patterns
205205

docs/edge/en/learn/tool-hooks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class ToolCallHookContext:
6464
raw_tool_result: Any | None # Raw Python result (after hooks only)
6565
```
6666

67-
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.
67+
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.
6868

6969
### Modifying Tool Inputs
7070

0 commit comments

Comments
 (0)