-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Description
Feature Request: Include Tool Return Type Schema in Tool Definitions (passed into LLM call as part of tools
json)
Background
We use pydantic-ai as an agent framework in a complex business domain. It's critical for us that in addition to the tool parameters
schema and description, the schema and description of tool return types are also included in the JSON passed to the LLM as part of tool definitions.
Current Problem: The LLM doesn't know which properties/fields are available in a tool's return value without actually executing the tool call. This leads to:
- Unnecessary tool calls just to discover what data is available
- The LLM being unable to plan multi-step operations effectively
- Confusion about which tool provides which data points
Use Cases
-
Rich DTOs as Tool Returns
All of our tools return DTOs (Pydantic models) instead of plain strings or primitives. These models contain detailed information about domain entities like invoices, projects, and contracts. -
Sequential (Chained) Tool Calls
This is foundational for the sequential tool call improvements discussed in Code Mode: Tool Injection for mcp-run-python: Enable Python code to call back to agent tools with request/response flow #2037. If the LLM knows what data a tool returns, it can:- Plan a sequence of tool calls where one tool's output feeds into another
- Determine upfront if a requested data point is available
- Chain operations more intelligently (e.g., "get invoice details, then get the contract details for that invoice")
-
Field Descriptions in Complex Domains
In complex business domains, field names alone are often insufficient for the LLM to understand what data represents. For example:audited_amount
vsapproved_amount
vspaid_amount
on an invoicedate_received
vsdate_posted
vsdate_due
- Domain-specific terminology that needs context
By including field descriptions from Pydantic models in the return schema, the LLM can:
- Understand the semantic meaning of each field
- Provide more accurate answers about what data is available
- Better explain differences between similar-sounding fields to users
-
Better User Experience
With return schemas available, the LLM can:- Politely decline requests for data that isn't available in any tool return
- Suggest alternative data points that are available
- Be more precise about what information it can provide
Proposed Solution
Similar to how parameters
work today, each tool definition would include a returns
field containing the JSON schema of the return type.
How this could be implemented
- Auto-generate from type annotations: Since tools must return JSON-serializable data, we can extract the return type from function signatures and generate the schema
- Docstring parsing: Parse return type descriptions from docstrings (similar to current parameter handling)
- Opt-in flag: To avoid breaking changes and manage context length, introduce an opt-in flag (e.g.,
include_return_schema=True
) inToolDefinition
or during tool preparation
I would be curious if you already thought about this and have a preferred way of how this should be implemented in mind.
Example: Current vs. Proposed
Current tool definition:
{
"type": "function",
"function": {
"name": "get_contract_details",
"description": "Get details about a specific contract by ID.",
"parameters": {
"additionalProperties": false,
"properties": {
"contract_id": {
"type": "integer",
"description": "ID of the contract"
}
},
"required": ["contract_id"],
"type": "object",
},
"strict": true
}
}
Proposed with return schema:
{
"type": "function",
"function": {
"name": "get_contract_details",
"description": "Get details about a specific contract by ID.",
"parameters": {
"additionalProperties": false,
"properties": {
"contract_id": {
"type": "integer"
}
},
"required": ["contract_id"],
"type": "object"
},
"strict": true,
"returns": {
// Either with 'properties' or using #/$defs/... which is used for parameters
"type": "object",
"description": "Details about a contract within a project",
"properties": {
"id": {
"type": "integer",
"description": "Unique contract identifier"
},
"project_id": {
"type": "integer",
"description": "ID of the parent project"
},
"contractor_name": {
"type": "string",
"description": "Name of the contractor company"
},
"amount": {
"type": "number",
"description": "Total contract amount"
},
"signed_date": {
"type": "string",
"format": "date",
"description": "Date the contract was signed"
}
},
"required": ["id", "project_id", "contractor_name", "amount", "signed_date"]
}
}
}
Minimal Example
Here's a minimal example demonstrating the need, especially for complex domain models with field descriptions:
from datetime import date
from decimal import Decimal
import pydantic
from pydantic_ai import Agent, RunContext
class InvoiceDetails(pydantic.BaseModel):
"""Details about an invoice for a contract"""
id: int = pydantic.Field(description="Unique invoice identifier")
contract_id: int = pydantic.Field(description="ID of the associated contract")
invoice_number: str = pydantic.Field(description="External invoice number")
# Multiple amount fields - descriptions are crucial for LLM to understand differences
initial_amount: Decimal = pydantic.Field(description="Initial invoice amount as submitted by contractor")
audited_amount: Decimal = pydantic.Field(description="Amount verified after checking the invoice")
approved_amount: Decimal = pydantic.Field(description="Amount released for payment after approval")
paid_amount: Decimal = pydantic.Field(description="Amount actually paid out")
# Multiple date fields - descriptions clarify the workflow
date_received: date = pydantic.Field(description="Date invoice was received")
date_posted: date = pydantic.Field(description="Date invoice was posted in accounting")
status: str = pydantic.Field(description="Invoice status (new, approved, paid)")
def get_invoice_details(ctx: RunContext, invoice_id: int) -> InvoiceDetails:
"""Get details about a specific invoice by ID."""
return InvoiceDetails(
id=invoice_id,
contract_id=1,
invoice_number="INV-2024-001",
initial_amount=Decimal("50000.00"),
audited_amount=Decimal("48000.00"),
approved_amount=Decimal("48000.00"),
paid_amount=Decimal("48000.00"),
date_received=date(2024, 3, 15),
date_posted=date(2024, 3, 20),
status="approved",
)
agent = Agent("openai:gpt-4o", tools=[get_invoice_details])
# Without return schema:
# - LLM doesn't know what fields InvoiceDetails contains
# - Can't distinguish between initial_amount, audited_amount, approved_amount, paid_amount
# - User asks "what's the invoice amount?" - which one do they mean?
# With return schema + field descriptions:
# - LLM knows exactly what fields are available
# - Understands the difference between amounts (initial vs audited vs approved vs paid)
# - Can ask clarifying questions or provide the most relevant amount based on context
Related Issues
- Code Mode: Tool Injection for mcp-run-python: Enable Python code to call back to agent tools with request/response flow #2037 - Sequential tool calls (this feature would be foundational for that)
Open Questions
- Opt-in vs. opt-out: Should this be enabled by default or require explicit configuration?
- Context length concerns: Should there be automatic truncation/summarization for very large return schemas?
- Docstring parsing: How should we handle return type descriptions from docstrings?
I'm happy to contribute to implementing this feature if there's interest! Let me know if you have preferences on the approach.
Note: This feature would significantly improve pydantic-ai's capabilities for complex, multi-step agentic workflows in production environments, especially in domains where field descriptions are essential for LLM comprehension.
References
No response