mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 05:38:12 +00:00
Use custom format_output_for_agent override for tool output
When a `BaseTool` subclass overrides `format_output_for_agent`, route the agent-facing text through it instead of the default JSON/`str()` serialization. The structured tool wrapper now delegates to the original tool via `_original_tool`, so a tool can present Markdown or any custom representation to the agent while `tool.run(...)` still returns the raw Python value.
This commit is contained in:
@@ -211,6 +211,23 @@ raw_result = tool.run(query="CrewAI")
|
||||
print(raw_result.score)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```python Code
|
||||
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)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = SearchResult.model_validate(raw_result)
|
||||
return f"### Search result\n\n- Query: `{result.query}`\n- Score: {result.score}"
|
||||
```
|
||||
|
||||
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)`.
|
||||
|
||||
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
|
||||
|
||||
@@ -142,6 +142,27 @@ class GeolocateTool(BaseTool):
|
||||
return {"latitude": 40.7128, "longitude": -74.0060}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = GeolocateResult.model_validate(raw_result)
|
||||
return (
|
||||
f"### Coordinates\n\n"
|
||||
f"- Latitude: `{result.latitude}`\n"
|
||||
f"- Longitude: `{result.longitude}`"
|
||||
)
|
||||
```
|
||||
|
||||
The override only controls the text sent to the agent. Direct users of your package still receive the raw 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:
|
||||
|
||||
@@ -147,6 +147,50 @@ def product_lookup(sku: str) -> dict[str, object]:
|
||||
}
|
||||
```
|
||||
|
||||
#### Customize the Text Sent to the Agent
|
||||
|
||||
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`.
|
||||
|
||||
This only changes the agent-facing text. Direct calls to `tool.run(...)` still return the raw Python value from `_run`.
|
||||
|
||||
```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."
|
||||
|
||||
def _run(self, sku: str) -> ProductLookupResult:
|
||||
return ProductLookupResult(
|
||||
sku=sku,
|
||||
name="CrewAI Enterprise License",
|
||||
in_stock=True,
|
||||
)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = ProductLookupResult.model_validate(raw_result)
|
||||
status = "in stock" if result.in_stock else "out of stock"
|
||||
return (
|
||||
f"### {result.name}\n\n"
|
||||
f"- SKU: `{result.sku}`\n"
|
||||
f"- Status: **{status}**"
|
||||
)
|
||||
|
||||
tool = ProductLookupTool()
|
||||
result = tool.run(sku="CREW-ENT")
|
||||
|
||||
# Direct Python calls receive the raw Pydantic object.
|
||||
print(result.name)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -52,6 +52,10 @@ def _infer_output_schema_from_callable(
|
||||
|
||||
|
||||
def _format_tool_output_for_agent(tool: Any, raw_result: Any) -> str:
|
||||
original_tool = getattr(tool, "_original_tool", None)
|
||||
if original_tool is not None:
|
||||
return cast(str, original_tool.format_output_for_agent(raw_result))
|
||||
|
||||
output_schema = getattr(tool, "output_schema", None)
|
||||
if output_schema is None:
|
||||
return str(raw_result)
|
||||
|
||||
@@ -584,6 +584,30 @@ class TestToolOutputSchema:
|
||||
"score": 0.8,
|
||||
}
|
||||
|
||||
def test_custom_agent_output_formatter_carries_over_to_structured_tool(
|
||||
self,
|
||||
) -> None:
|
||||
class MarkdownSearchTool(BaseTool):
|
||||
name: str = "markdown_search"
|
||||
description: str = "Search for information"
|
||||
output_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.8)
|
||||
|
||||
def format_output_for_agent(self, raw_result: object) -> str:
|
||||
result = self.output_schema.model_validate(raw_result)
|
||||
return f"### Search result\n\n- Query: `{result.query}`\n- Score: {result.score}"
|
||||
|
||||
structured = MarkdownSearchTool().to_structured_tool()
|
||||
|
||||
raw_result = structured.invoke({"query": "crew"})
|
||||
|
||||
assert raw_result == SearchOutput(query="crew", score=0.8)
|
||||
assert structured.format_output_for_agent(raw_result) == (
|
||||
"### Search result\n\n- Query: `crew`\n- Score: 0.8"
|
||||
)
|
||||
|
||||
# Async arun() Schema Validation Tests
|
||||
|
||||
|
||||
|
||||
@@ -1079,6 +1079,51 @@ class TestExecuteSingleNativeToolCall:
|
||||
"score": 0.9,
|
||||
}
|
||||
|
||||
def test_custom_agent_output_formatter_is_used_from_structured_tool(
|
||||
self,
|
||||
) -> None:
|
||||
clear_before_tool_call_hooks()
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
class SearchOutput(BaseModel):
|
||||
query: str
|
||||
score: float
|
||||
|
||||
class MarkdownSearchTool(BaseTool):
|
||||
name: str = "markdown_search"
|
||||
description: str = "Search for a query"
|
||||
output_schema: type[BaseModel] = SearchOutput
|
||||
|
||||
def _run(self, query: str) -> SearchOutput:
|
||||
return SearchOutput(query=query, score=0.9)
|
||||
|
||||
def format_output_for_agent(self, raw_result: Any) -> str:
|
||||
result = self.output_schema.model_validate(raw_result)
|
||||
return f"### {result.query}\n\nScore: **{result.score}**"
|
||||
|
||||
tool = MarkdownSearchTool()
|
||||
tool_call = MagicMock()
|
||||
tool_call.id = "call_1"
|
||||
tool_call.function.name = "markdown_search"
|
||||
tool_call.function.arguments = '{"query": "crew"}'
|
||||
|
||||
result = execute_single_native_tool_call(
|
||||
tool_call,
|
||||
available_functions={"markdown_search": tool._run},
|
||||
original_tools=[],
|
||||
structured_tools=[tool.to_structured_tool()],
|
||||
tools_handler=None,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
event_source=MagicMock(),
|
||||
printer=None,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
assert result.result == "### crew\n\nScore: **0.9**"
|
||||
assert result.tool_message["content"] == "### crew\n\nScore: **0.9**"
|
||||
|
||||
def test_after_hook_includes_raw_tool_result_for_typed_output(self) -> None:
|
||||
clear_after_tool_call_hooks()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user