- Notifications
You must be signed in to change notification settings - Fork1k
Support functions as output_type, as well as lists of functions and other types#1785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
github-actionsbot commentedMay 20, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Docs Preview
|
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Generally looks good, I think we should just confirm the changes to FunctionSchema are okay with Samuel (he'll probably just rubber stamp but still), and I'd like to get@Kludex's take on the crazy OutputType types but I'm okay with it if he is.
Oh we also need to add some typing-related tests |
…checked, not executed
# Conflicts:#pydantic_ai_slim/pydantic_ai/_output.py#pydantic_ai_slim/pydantic_ai/tools.py
…er to rerun when the length of the example changes
burningion commentedMay 23, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
One thing I'd also like to be able to do here is do asingle LLM call with my Agent. Ideally it'd do the same behavior as direct model call: https://ai.pydantic.dev/direct/ Except expose the MCP server, and (optionally) force a Pydantic model response in a single call. Am I missing something or does that interface not exist in the Agent yet? |
DouweM commentedMay 23, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
What I've done here to make Specifically, none of the three support from __future__importannotationsfromdataclassesimportdataclassfromtyping_extensionsimport (Generic,Sequence,TypeVar,assert_type,)T=TypeVar("T")@dataclassclassAgent(Generic[T]):output_type:Sequence[type[T]]classFoo:passclassBar:pass# pyright - works# mypy - error: Expression is of type "Agent[object]", not "Agent[Foo | Bar]" [assert-type]# pyrefly - assert_type(Agent[Foo], Agent[Bar | Foo]) failed + Argument `list[type[Bar] | type[Foo]]` is not assignable to parameter `output_type` with type `Sequence[type[Foo]]` in function `Agent.__init__`# ty - `Agent[Foo | Bar]` and `Agent[Unknown]` are not equivalent typesassert_type(Agent([Foo,Bar]),Agent[Foo|Bar])# pyright - works# mypy - error: Expression is of type "Agent[Never]", not "Agent[int | str]" [assert-type]# pyrefly - assert_type(Agent[int], Agent[str | int]) failed + Argument `list[type[str] | type[int]]` is not assignable to parameter `output_type` with type `Sequence[type[int]]` in function `Agent.__init__`# ty - `Agent[int | str]` and `Agent[Unknown]` are not equivalent typesassert_type(Agent([int,str]),Agent[int|str])# worksassert_type(Agent[Foo|Bar]([Foo,Bar]),Agent[Foo|Bar])# worksassert_type(Agent[int|str]([int,str]),Agent[int|str]) Ty doesn't support fromdataclassesimportdataclassfromtyping_extensionsimport (Callable,Generic,TypeVar,assert_type,)T=TypeVar("T")@dataclassclassAgent(Generic[T]):output_type:Callable[...,T]deffunc()->int:return1# pyright, mypy, pyrefly - works# ty - `Agent[int]` and `Agent[Unknown]` are not equivalent types + Expected `((...) -> T) | ((...) -> Awaitable[T])`, found `def func() -> int`assert_type(Agent(func),Agent[int])# worksassert_type(Agent[int](func),Agent[int]) And mypy (and ty, because of the above issue) don't support fromdataclassesimportdataclassfromtyping_extensionsimport (Awaitable,Callable,Generic,TypeVar,assert_type,)T=TypeVar("T")@dataclassclassAgent(Generic[T]):output_type:Callable[...,T]|Callable[...,Awaitable[T]]asyncdefcoro()->bool:returnTruedeffunc()->int:return1# pyright, mypy, pyrefly - works# ty - `Agent[int]` and `Agent[Unknown]` are not equivalent types + Expected `((...) -> T) | ((...) -> Awaitable[T])`, found `def func() -> int`assert_type(Agent(func),Agent[int])# mypy - error: Argument 1 to "Agent" has incompatible type "Callable[[], Coroutine[Any, Any, bool]]"; expected "Callable[..., Never] | Callable[..., Awaitable[Never]]" [arg-type]coro_agent=Agent(coro)# pyright, pyrefly - works# mypy - error: Expression is of type "Agent[Any]", not "Agent[bool]"# ty - `Agent[bool]` and `Agent[Unknown]` are not equivalent typesassert_type(coro_agent,Agent[bool])# worksassert_type(Agent[bool](coro),Agent[bool]) The issue with @dataclassclassOutput(Generic[T]):output_type:type[T]defor_[S](self,output_type:type[S])->Output[T|S]:returnOutput(self.output_type|output_type)# type: ignore# ordefoutput_type[T](*args:type[T])->Output[T]:raiseNotImplementedError The issue with We could also decide we're fine with all of this because it works correctly on all typecheckers when you explicitly specify the generic parameters with I've filed some issues with the type checkers to see what their teams say. |
In discussion with@dmontagu we decided to merge this as is, with
|
45d0ff2
intomainUh oh!
There was an error while loading.Please reload this page.
HamzaFarhan commentedMay 29, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
When would this be released? |
bootloop-noah commentedJun 4, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@DouweM in the case of using output_type for agent delegation like in the router scenario (as described in the docshere andhere), we'd expect it to operate like a directed graph but with a "decision point". The final Is there way to add the results of the delegated agent back into the main message history so it's something more like |
fromdotenvimportload_dotenvfrompydantic_aiimportAgent,RunContextload_dotenv()maths_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a maths tutor. Given a question, you will provide a step by step solution.",)asyncdefhand_off_to_maths_agent(ctx:RunContext,query:str)->str:res=awaitmaths_agent.run(query)ctx.messages+=res.new_messages()returnres.outputpoet_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a poet. Given a topic, you will provide a poem.",)asyncdefhand_off_to_poet_agent(ctx:RunContext,query:str)->str:res=awaitpoet_agent.run(query)ctx.messages+=res.new_messages()returnres.outputrouter_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a router. Given a user query, you will route it to the appropriate agent.",output_type=[hand_off_to_maths_agent,hand_off_to_poet_agent],)asyncdefmain():query="Calculate 10 + 10"result=awaitrouter_agent.run(query)formessageinresult.all_messages():print(message,"\n")print(result.output)if__name__=="__main__":importasyncioasyncio.run(main()) |
bootloop-noah commentedJun 4, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@DouweM@HamzaFarhan great thank-you! After implementing this, there seems to be an error if we want to maintain state across multiple runs of the router in a case like:
This raises a ModelHTTPError for both Gemini and OpenAI models (haven't tried any others). It looks like a validator maybe isn't looking ahead? The messages definitely contain a matching
|
Ooh right |
For what it's worth, here's a hack: fromdataclassesimportreplacefromdotenvimportload_dotenvfrompydantic_aiimportAgent,RunContextfrompydantic_ai.messagesimportModelMessage,ModelRequest,ToolCallPart,ToolReturnPartload_dotenv()maths_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a maths tutor. Given a question, you will provide a step by step solution.",)asyncdefhand_off_to_maths_agent(ctx:RunContext,query:str)->str:res=awaitmaths_agent.run(query)ctx.messages+=res.new_messages()returnres.outputpoet_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a poet. Given a topic, you will provide a poem.",)asyncdefhand_off_to_poet_agent(ctx:RunContext,query:str)->str:res=awaitpoet_agent.run(query)ctx.messages+=res.new_messages()returnres.outputrouter_agent=Agent(model="google-gla:gemini-2.0-flash",instructions="You are a router. Given a user query, you will route it to the appropriate agent.",output_type=[hand_off_to_maths_agent,hand_off_to_poet_agent],)deffilter_tool_parts(messages:list[ModelMessage],filter_str:str)->list[ModelMessage]:filtered_messages:list[ModelMessage]= []formessageinmessages:ifisinstance(message,ModelRequest):filtered_parts= [partforpartinmessage.partsifnot (isinstance(part,ToolReturnPart)andfilter_strinpart.tool_name) ]iffiltered_parts:filtered_messages.append(replace(message,parts=filtered_parts))else:filtered_parts= [partforpartinmessage.partsifnot (isinstance(part,ToolCallPart)andfilter_strinpart.tool_name) ]iffiltered_parts:filtered_messages.append(replace(message,parts=filtered_parts))returnfiltered_messagesasyncdefmain():query="Calculate 10 + 10"result=awaitrouter_agent.run(query)message_history=filter_tool_parts(result.all_messages(),"hand_off")sep="\n"+"-"*100+"\n"print(sep)formessageinmessage_history:print(message,"\n")print(f"{sep}{result.output}{sep}")query="Write a poem about your answer"result=awaitrouter_agent.run(query,message_history=message_history)print(result.output)if__name__=="__main__":importasyncioasyncio.run(main()) |
Uh oh!
There was an error while loading.Please reload this page.
Consider reviewing the first commit by itself and then the rest as one diff!
Commit 1 brings in some output handling refactoring borrowed from#1628, to make sure we don't hard-code this against the tool-call output mode (as the original PR did). Also makes it less of a rebase hell for me :) This commit does not change any behavior.
Example:
To do:
RunContext
; this value should be injected, not obtained from the model.