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:
Vinicius Brasil
2026-06-18 21:41:34 -07:00
parent 8c35dedfb5
commit 9b8ecc7df5
6 changed files with 155 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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