-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Initial Checks
- I confirm that I'm using the latest version of Pydantic AI
- I confirm that I searched for my issue in https://github.com/pydantic/pydantic-ai/issues before opening this issue
Description
This is a follow-up from a Slack conversation: https://pydanticlogfire.slack.com/archives/C083V7PMHHA/p1751066192272409
I want my agent to stream text, make tool calls, stream more text if needed, and make a final tool call for its structured output (I'm using Gemini which does not support tool calls and native structured output at the same time). To do so, I have set output_type to [str, FinalOutput] where FinalOutput is my structured output type.
This streams text as expected, but the final_result tool call is not always made. My understanding is that since the end node can either be FinalOutput or text, it will sometimes output text and there is no guarantee it outputs a FinalOutput. In my repro script, I have two example queries. One of them will call the final output tool call, and one of them will not (although this is dependent on the model's output being reproducible which isn't guaranteed).
I think what I would ideally want to do is to set output_type to FinalOutput so that it is always called but still enable streaming text output. That is currently prevented by this line in _agent_graph.py.
Is there a way to have pydantic-ai consistently stream text output AND make a final result tool call? One workaround is to just prompt the LLM to make the final result tool call, but that is not guaranteed.
Example Code
"""
To run: python <name>.py.
Requires GEMINI_API_KEY environment variable (available at https://aistudio.google.com/apikey).
Alternatively you can change the model but this will change the reproducibility of the script.
"""
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.messages import (
FinalResultEvent,
FunctionToolCallEvent,
FunctionToolResultEvent,
PartDeltaEvent,
PartStartEvent,
TextPart,
TextPartDelta,
)
from pydantic_ai.models import KnownModelName
class FinalOutput(BaseModel):
lines: list[str]
QUERY = "Write a short poem about Python." # Does not call final tool call.
# QUERY = "Write a poem about Python." # Does call final tool call.
MODEL: KnownModelName = "google-gla:gemini-2.5-flash"
AGENT = Agent(
model=MODEL,
model_settings={"temperature": 0},
output_type=[str, FinalOutput],
)
async def main():
# Based on https://ai.pydantic.dev/agents/#streaming
async with AGENT.iter(user_prompt=QUERY) as run:
async for node in run:
if Agent.is_user_prompt_node(node):
print("\nUser prompt:", node.user_prompt)
elif Agent.is_model_request_node(node):
async with node.stream(run.ctx) as request_stream:
async for event in request_stream:
if isinstance(event, PartStartEvent) and isinstance(event.part, TextPart):
print("\nText start:", event.part.content)
elif isinstance(event, PartDeltaEvent) and isinstance(event.delta, TextPartDelta):
print("\nText delta:", event.delta.content_delta)
elif isinstance(event, FinalResultEvent):
print("\nFinal output tool call:", event.tool_name)
elif Agent.is_call_tools_node(node):
async with node.stream(run.ctx) as handle_stream:
async for event in handle_stream:
if isinstance(event, FunctionToolCallEvent):
print("\nTool call event:", event)
elif isinstance(event, FunctionToolResultEvent):
print("\nTool result event:", event)
elif Agent.is_end_node(node):
print("\nFinal output:", node.data.output)
if __name__ == "__main__":
import asyncio
asyncio.run(main())Python, Pydantic AI & LLM client version
Python 3.11.13
pydantic-ai 0.4.0