pydantic-ai-slim 1.2.1__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. pydantic_ai/__init__.py +6 -0
  2. pydantic_ai/_agent_graph.py +67 -20
  3. pydantic_ai/_cli.py +2 -2
  4. pydantic_ai/_output.py +20 -12
  5. pydantic_ai/_run_context.py +6 -2
  6. pydantic_ai/_utils.py +26 -8
  7. pydantic_ai/ag_ui.py +50 -696
  8. pydantic_ai/agent/__init__.py +13 -25
  9. pydantic_ai/agent/abstract.py +146 -9
  10. pydantic_ai/builtin_tools.py +106 -4
  11. pydantic_ai/direct.py +16 -4
  12. pydantic_ai/durable_exec/dbos/_agent.py +3 -0
  13. pydantic_ai/durable_exec/prefect/_agent.py +3 -0
  14. pydantic_ai/durable_exec/temporal/__init__.py +11 -0
  15. pydantic_ai/durable_exec/temporal/_agent.py +3 -0
  16. pydantic_ai/durable_exec/temporal/_function_toolset.py +23 -72
  17. pydantic_ai/durable_exec/temporal/_mcp_server.py +30 -30
  18. pydantic_ai/durable_exec/temporal/_run_context.py +7 -2
  19. pydantic_ai/durable_exec/temporal/_toolset.py +67 -3
  20. pydantic_ai/exceptions.py +6 -1
  21. pydantic_ai/mcp.py +1 -22
  22. pydantic_ai/messages.py +46 -8
  23. pydantic_ai/models/__init__.py +87 -38
  24. pydantic_ai/models/anthropic.py +132 -11
  25. pydantic_ai/models/bedrock.py +4 -4
  26. pydantic_ai/models/cohere.py +0 -7
  27. pydantic_ai/models/gemini.py +9 -2
  28. pydantic_ai/models/google.py +26 -23
  29. pydantic_ai/models/groq.py +13 -5
  30. pydantic_ai/models/huggingface.py +2 -2
  31. pydantic_ai/models/openai.py +251 -52
  32. pydantic_ai/models/outlines.py +563 -0
  33. pydantic_ai/models/test.py +6 -3
  34. pydantic_ai/profiles/openai.py +7 -0
  35. pydantic_ai/providers/__init__.py +25 -12
  36. pydantic_ai/providers/anthropic.py +2 -2
  37. pydantic_ai/providers/bedrock.py +60 -16
  38. pydantic_ai/providers/gateway.py +60 -72
  39. pydantic_ai/providers/google.py +91 -24
  40. pydantic_ai/providers/openrouter.py +3 -0
  41. pydantic_ai/providers/outlines.py +40 -0
  42. pydantic_ai/providers/ovhcloud.py +95 -0
  43. pydantic_ai/result.py +173 -8
  44. pydantic_ai/run.py +40 -24
  45. pydantic_ai/settings.py +8 -0
  46. pydantic_ai/tools.py +10 -6
  47. pydantic_ai/toolsets/fastmcp.py +215 -0
  48. pydantic_ai/ui/__init__.py +16 -0
  49. pydantic_ai/ui/_adapter.py +386 -0
  50. pydantic_ai/ui/_event_stream.py +591 -0
  51. pydantic_ai/ui/_messages_builder.py +28 -0
  52. pydantic_ai/ui/ag_ui/__init__.py +9 -0
  53. pydantic_ai/ui/ag_ui/_adapter.py +187 -0
  54. pydantic_ai/ui/ag_ui/_event_stream.py +236 -0
  55. pydantic_ai/ui/ag_ui/app.py +148 -0
  56. pydantic_ai/ui/vercel_ai/__init__.py +16 -0
  57. pydantic_ai/ui/vercel_ai/_adapter.py +199 -0
  58. pydantic_ai/ui/vercel_ai/_event_stream.py +187 -0
  59. pydantic_ai/ui/vercel_ai/_utils.py +16 -0
  60. pydantic_ai/ui/vercel_ai/request_types.py +275 -0
  61. pydantic_ai/ui/vercel_ai/response_types.py +230 -0
  62. pydantic_ai/usage.py +13 -2
  63. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/METADATA +23 -5
  64. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/RECORD +67 -49
  65. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/WHEEL +0 -0
  66. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/entry_points.txt +0 -0
  67. {pydantic_ai_slim-1.2.1.dist-info → pydantic_ai_slim-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,6 @@ from pydantic.json_schema import GenerateJsonSchema
15
15
  from typing_extensions import Self, TypeVar, deprecated
16
16
 
17
17
  from pydantic_ai._instrumentation import DEFAULT_INSTRUMENTATION_VERSION, InstrumentationNames
18
- from pydantic_graph import Graph
19
18
 
20
19
  from .. import (
21
20
  _agent_graph,
@@ -41,7 +40,6 @@ from ..builtin_tools import AbstractBuiltinTool
41
40
  from ..models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
42
41
  from ..output import OutputDataT, OutputSpec
43
42
  from ..profiles import ModelProfile
44
- from ..result import FinalResult
45
43
  from ..run import AgentRun, AgentRunResult
46
44
  from ..settings import ModelSettings, merge_model_settings
47
45
  from ..tools import (
@@ -542,6 +540,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
542
540
  """
543
541
  if infer_name and self.name is None:
544
542
  self._infer_name(inspect.currentframe())
543
+
545
544
  model_used = self._get_model(model)
546
545
  del model
547
546
 
@@ -565,9 +564,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
565
564
  tool_manager = ToolManager[AgentDepsT](toolset)
566
565
 
567
566
  # Build the graph
568
- graph: Graph[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any], FinalResult[Any]] = (
569
- _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
570
- )
567
+ graph = _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
571
568
 
572
569
  # Build the initial state
573
570
  usage = usage or _usage.RunUsage()
@@ -607,16 +604,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
607
604
  else:
608
605
  instrumentation_settings = None
609
606
  tracer = NoOpTracer()
610
- if builtin_tools:
611
- # Deduplicate builtin tools passed to the agent and the run based on type
612
- builtin_tools = list(
613
- {
614
- **({type(tool): tool for tool in self._builtin_tools or []}),
615
- **({type(tool): tool for tool in builtin_tools}),
616
- }.values()
617
- )
618
- else:
619
- builtin_tools = list(self._builtin_tools)
607
+
620
608
  graph_deps = _agent_graph.GraphAgentDeps[AgentDepsT, RunOutputDataT](
621
609
  user_deps=deps,
622
610
  prompt=user_prompt,
@@ -629,14 +617,14 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
629
617
  output_schema=output_schema,
630
618
  output_validators=output_validators,
631
619
  history_processors=self.history_processors,
632
- builtin_tools=builtin_tools,
620
+ builtin_tools=[*self._builtin_tools, *(builtin_tools or [])],
633
621
  tool_manager=tool_manager,
634
622
  tracer=tracer,
635
623
  get_instructions=get_instructions,
636
624
  instrumentation_settings=instrumentation_settings,
637
625
  )
638
626
 
639
- start_node = _agent_graph.UserPromptNode[AgentDepsT](
627
+ user_prompt_node = _agent_graph.UserPromptNode[AgentDepsT](
640
628
  user_prompt=user_prompt,
641
629
  deferred_tool_results=deferred_tool_results,
642
630
  instructions=instructions_literal,
@@ -662,14 +650,14 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
662
650
  )
663
651
 
664
652
  try:
665
- async with toolset:
666
- async with graph.iter(
667
- start_node,
668
- state=state,
669
- deps=graph_deps,
670
- span=use_span(run_span) if run_span.is_recording() else None,
671
- infer_name=False,
672
- ) as graph_run:
653
+ async with graph.iter(
654
+ inputs=user_prompt_node,
655
+ state=state,
656
+ deps=graph_deps,
657
+ span=use_span(run_span) if run_span.is_recording() else None,
658
+ infer_name=False,
659
+ ) as graph_run:
660
+ async with toolset:
673
661
  agent_run = AgentRun(graph_run)
674
662
  yield agent_run
675
663
  if (final_result := agent_run.result) is not None and run_span.is_recording():
@@ -12,7 +12,6 @@ import anyio
12
12
  from typing_extensions import Self, TypeIs, TypeVar
13
13
 
14
14
  from pydantic_graph import End
15
- from pydantic_graph._utils import get_event_loop
16
15
 
17
16
  from .. import (
18
17
  _agent_graph,
@@ -49,7 +48,7 @@ if TYPE_CHECKING:
49
48
  from starlette.routing import BaseRoute, Route
50
49
  from starlette.types import ExceptionHandler, Lifespan
51
50
 
52
- from ..ag_ui import AGUIApp
51
+ from pydantic_ai.ui.ag_ui.app import AGUIApp
53
52
 
54
53
 
55
54
  T = TypeVar('T')
@@ -335,7 +334,7 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
335
334
  if infer_name and self.name is None:
336
335
  self._infer_name(inspect.currentframe())
337
336
 
338
- return get_event_loop().run_until_complete(
337
+ return _utils.get_event_loop().run_until_complete(
339
338
  self.run(
340
339
  user_prompt,
341
340
  output_type=output_type,
@@ -581,6 +580,133 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
581
580
  if not yielded:
582
581
  raise exceptions.AgentRunError('Agent run finished without producing a final result') # pragma: no cover
583
582
 
583
+ @overload
584
+ def run_stream_sync(
585
+ self,
586
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
587
+ *,
588
+ output_type: None = None,
589
+ message_history: Sequence[_messages.ModelMessage] | None = None,
590
+ deferred_tool_results: DeferredToolResults | None = None,
591
+ model: models.Model | models.KnownModelName | str | None = None,
592
+ deps: AgentDepsT = None,
593
+ model_settings: ModelSettings | None = None,
594
+ usage_limits: _usage.UsageLimits | None = None,
595
+ usage: _usage.RunUsage | None = None,
596
+ infer_name: bool = True,
597
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
598
+ builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
599
+ event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
600
+ ) -> result.StreamedRunResultSync[AgentDepsT, OutputDataT]: ...
601
+
602
+ @overload
603
+ def run_stream_sync(
604
+ self,
605
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
606
+ *,
607
+ output_type: OutputSpec[RunOutputDataT],
608
+ message_history: Sequence[_messages.ModelMessage] | None = None,
609
+ deferred_tool_results: DeferredToolResults | None = None,
610
+ model: models.Model | models.KnownModelName | str | None = None,
611
+ deps: AgentDepsT = None,
612
+ model_settings: ModelSettings | None = None,
613
+ usage_limits: _usage.UsageLimits | None = None,
614
+ usage: _usage.RunUsage | None = None,
615
+ infer_name: bool = True,
616
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
617
+ builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
618
+ event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
619
+ ) -> result.StreamedRunResultSync[AgentDepsT, RunOutputDataT]: ...
620
+
621
+ def run_stream_sync(
622
+ self,
623
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
624
+ *,
625
+ output_type: OutputSpec[RunOutputDataT] | None = None,
626
+ message_history: Sequence[_messages.ModelMessage] | None = None,
627
+ deferred_tool_results: DeferredToolResults | None = None,
628
+ model: models.Model | models.KnownModelName | str | None = None,
629
+ deps: AgentDepsT = None,
630
+ model_settings: ModelSettings | None = None,
631
+ usage_limits: _usage.UsageLimits | None = None,
632
+ usage: _usage.RunUsage | None = None,
633
+ infer_name: bool = True,
634
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
635
+ builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
636
+ event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
637
+ ) -> result.StreamedRunResultSync[AgentDepsT, Any]:
638
+ """Run the agent with a user prompt in sync streaming mode.
639
+
640
+ This is a convenience method that wraps [`run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] with `loop.run_until_complete(...)`.
641
+ You therefore can't use this method inside async code or if there's an active event loop.
642
+
643
+ This method builds an internal agent graph (using system prompts, tools and output schemas) and then
644
+ runs the graph until the model produces output matching the `output_type`, for example text or structured data.
645
+ At this point, a streaming run result object is yielded from which you can stream the output as it comes in,
646
+ and -- once this output has completed streaming -- get the complete output, message history, and usage.
647
+
648
+ As this method will consider the first output matching the `output_type` to be the final output,
649
+ it will stop running the agent graph and will not execute any tool calls made by the model after this "final" output.
650
+ If you want to always run the agent graph to completion and stream events and output at the same time,
651
+ use [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] with an `event_stream_handler` or [`agent.iter()`][pydantic_ai.agent.AbstractAgent.iter] instead.
652
+
653
+ Example:
654
+ ```python
655
+ from pydantic_ai import Agent
656
+
657
+ agent = Agent('openai:gpt-4o')
658
+
659
+ def main():
660
+ response = agent.run_stream_sync('What is the capital of the UK?')
661
+ print(response.get_output())
662
+ #> The capital of the UK is London.
663
+ ```
664
+
665
+ Args:
666
+ user_prompt: User input to start/continue the conversation.
667
+ output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
668
+ output validators since output validators would expect an argument that matches the agent's output type.
669
+ message_history: History of the conversation so far.
670
+ deferred_tool_results: Optional results for deferred tool calls in the message history.
671
+ model: Optional model to use for this run, required if `model` was not set when creating the agent.
672
+ deps: Optional dependencies to use for this run.
673
+ model_settings: Optional settings to use for this model's request.
674
+ usage_limits: Optional limits on model request count or token usage.
675
+ usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
676
+ infer_name: Whether to try to infer the agent name from the call frame if it's not set.
677
+ toolsets: Optional additional toolsets for this run.
678
+ builtin_tools: Optional additional builtin tools for this run.
679
+ event_stream_handler: Optional handler for events from the model's streaming response and the agent's execution of tools to use for this run.
680
+ It will receive all the events up until the final result is found, which you can then read or stream from inside the context manager.
681
+ Note that it does _not_ receive any events after the final result is found.
682
+
683
+ Returns:
684
+ The result of the run.
685
+ """
686
+ if infer_name and self.name is None:
687
+ self._infer_name(inspect.currentframe())
688
+
689
+ async def _consume_stream():
690
+ async with self.run_stream(
691
+ user_prompt,
692
+ output_type=output_type,
693
+ message_history=message_history,
694
+ deferred_tool_results=deferred_tool_results,
695
+ model=model,
696
+ deps=deps,
697
+ model_settings=model_settings,
698
+ usage_limits=usage_limits,
699
+ usage=usage,
700
+ infer_name=infer_name,
701
+ toolsets=toolsets,
702
+ builtin_tools=builtin_tools,
703
+ event_stream_handler=event_stream_handler,
704
+ ) as stream_result:
705
+ yield stream_result
706
+
707
+ async_result = _utils.get_event_loop().run_until_complete(anext(_consume_stream()))
708
+ return result.StreamedRunResultSync(async_result)
709
+
584
710
  @overload
585
711
  def run_stream_events(
586
712
  self,
@@ -654,6 +780,9 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
654
780
  PartStartEvent(index=0, part=TextPart(content='The capital of ')),
655
781
  FinalResultEvent(tool_name=None, tool_call_id=None),
656
782
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='France is Paris. ')),
783
+ PartEndEvent(
784
+ index=0, part=TextPart(content='The capital of France is Paris. ')
785
+ ),
657
786
  AgentRunResultEvent(
658
787
  result=AgentRunResult(output='The capital of France is Paris. ')
659
788
  ),
@@ -683,6 +812,9 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
683
812
  An async iterable of stream events `AgentStreamEvent` and finally a `AgentRunResultEvent` with the final
684
813
  run result.
685
814
  """
815
+ if infer_name and self.name is None:
816
+ self._infer_name(inspect.currentframe())
817
+
686
818
  # unfortunately this hack of returning a generator rather than defining it right here is
687
819
  # required to allow overloads of this method to work in python's typing system, or at least with pyright
688
820
  # or at least I couldn't make it work without
@@ -696,7 +828,6 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
696
828
  model_settings=model_settings,
697
829
  usage_limits=usage_limits,
698
830
  usage=usage,
699
- infer_name=infer_name,
700
831
  toolsets=toolsets,
701
832
  builtin_tools=builtin_tools,
702
833
  )
@@ -713,7 +844,6 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
713
844
  model_settings: ModelSettings | None = None,
714
845
  usage_limits: _usage.UsageLimits | None = None,
715
846
  usage: _usage.RunUsage | None = None,
716
- infer_name: bool = True,
717
847
  toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
718
848
  builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
719
849
  ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]:
@@ -739,7 +869,7 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
739
869
  model_settings=model_settings,
740
870
  usage_limits=usage_limits,
741
871
  usage=usage,
742
- infer_name=infer_name,
872
+ infer_name=False,
743
873
  toolsets=toolsets,
744
874
  builtin_tools=builtin_tools,
745
875
  event_stream_handler=event_stream_handler,
@@ -989,11 +1119,14 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
989
1119
  async def __aexit__(self, *args: Any) -> bool | None:
990
1120
  raise NotImplementedError
991
1121
 
1122
+ # TODO (v2): Remove in favor of using `AGUIApp` directly -- we don't have `to_temporal()` or `to_vercel_ai()` either.
992
1123
  def to_ag_ui(
993
1124
  self,
994
1125
  *,
995
1126
  # Agent.iter parameters
996
1127
  output_type: OutputSpec[OutputDataT] | None = None,
1128
+ message_history: Sequence[_messages.ModelMessage] | None = None,
1129
+ deferred_tool_results: DeferredToolResults | None = None,
997
1130
  model: models.Model | models.KnownModelName | str | None = None,
998
1131
  deps: AgentDepsT = None,
999
1132
  model_settings: ModelSettings | None = None,
@@ -1034,12 +1167,14 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
1034
1167
  uvicorn app:app --host 0.0.0.0 --port 8000
1035
1168
  ```
1036
1169
 
1037
- See [AG-UI docs](../ag-ui.md) for more information.
1170
+ See [AG-UI docs](../ui/ag-ui.md) for more information.
1038
1171
 
1039
1172
  Args:
1040
1173
  output_type: Custom output type to use for this run, `output_type` may only be used if the agent has
1041
1174
  no output validators since output validators would expect an argument that matches the agent's
1042
1175
  output type.
1176
+ message_history: History of the conversation so far.
1177
+ deferred_tool_results: Optional results for deferred tool calls in the message history.
1043
1178
  model: Optional model to use for this run, required if `model` was not set when creating the agent.
1044
1179
  deps: Optional dependencies to use for this run.
1045
1180
  model_settings: Optional settings to use for this model's request.
@@ -1069,12 +1204,14 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
1069
1204
  Returns:
1070
1205
  An ASGI application for running Pydantic AI agents with AG-UI protocol support.
1071
1206
  """
1072
- from ..ag_ui import AGUIApp
1207
+ from pydantic_ai.ui.ag_ui.app import AGUIApp
1073
1208
 
1074
1209
  return AGUIApp(
1075
1210
  agent=self,
1076
1211
  # Agent.iter parameters
1077
1212
  output_type=output_type,
1213
+ message_history=message_history,
1214
+ deferred_tool_results=deferred_tool_results,
1078
1215
  model=model,
1079
1216
  deps=deps,
1080
1217
  model_settings=model_settings,
@@ -1206,6 +1343,6 @@ class AbstractAgent(Generic[AgentDepsT, OutputDataT], ABC):
1206
1343
  agent.to_cli_sync(prog_name='assistant')
1207
1344
  ```
1208
1345
  """
1209
- return get_event_loop().run_until_complete(
1346
+ return _utils.get_event_loop().run_until_complete(
1210
1347
  self.to_cli(deps=deps, prog_name=prog_name, message_history=message_history)
1211
1348
  )
@@ -2,13 +2,12 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  from abc import ABC
4
4
  from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Literal
5
+ from typing import Annotated, Any, Literal, Union
6
6
 
7
+ import pydantic
8
+ from pydantic_core import core_schema
7
9
  from typing_extensions import TypedDict
8
10
 
9
- if TYPE_CHECKING:
10
- from .builtin_tools import AbstractBuiltinTool
11
-
12
11
  __all__ = (
13
12
  'AbstractBuiltinTool',
14
13
  'WebSearchTool',
@@ -17,8 +16,11 @@ __all__ = (
17
16
  'UrlContextTool',
18
17
  'ImageGenerationTool',
19
18
  'MemoryTool',
19
+ 'MCPServerTool',
20
20
  )
21
21
 
22
+ _BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
23
+
22
24
 
23
25
  @dataclass(kw_only=True)
24
26
  class AbstractBuiltinTool(ABC):
@@ -32,6 +34,34 @@ class AbstractBuiltinTool(ABC):
32
34
  kind: str = 'unknown_builtin_tool'
33
35
  """Built-in tool identifier, this should be available on all built-in tools as a discriminator."""
34
36
 
37
+ @property
38
+ def unique_id(self) -> str:
39
+ """A unique identifier for the builtin tool.
40
+
41
+ If multiple instances of the same builtin tool can be passed to the model, subclasses should override this property to allow them to be distinguished.
42
+ """
43
+ return self.kind
44
+
45
+ def __init_subclass__(cls, **kwargs: Any) -> None:
46
+ super().__init_subclass__(**kwargs)
47
+ _BUILTIN_TOOL_TYPES[cls.kind] = cls
48
+
49
+ @classmethod
50
+ def __get_pydantic_core_schema__(
51
+ cls, _source_type: Any, handler: pydantic.GetCoreSchemaHandler
52
+ ) -> core_schema.CoreSchema:
53
+ if cls is not AbstractBuiltinTool:
54
+ return handler(cls)
55
+
56
+ tools = _BUILTIN_TOOL_TYPES.values()
57
+ if len(tools) == 1: # pragma: no cover
58
+ tools_type = next(iter(tools))
59
+ else:
60
+ tools_annotated = [Annotated[tool, pydantic.Tag(tool.kind)] for tool in tools]
61
+ tools_type = Annotated[Union[tuple(tools_annotated)], pydantic.Discriminator(_tool_discriminator)] # noqa: UP007
62
+
63
+ return handler(tools_type)
64
+
35
65
 
36
66
  @dataclass(kw_only=True)
37
67
  class WebSearchTool(AbstractBuiltinTool):
@@ -120,6 +150,7 @@ class WebSearchUserLocation(TypedDict, total=False):
120
150
  """The timezone of the user's location."""
121
151
 
122
152
 
153
+ @dataclass(kw_only=True)
123
154
  class CodeExecutionTool(AbstractBuiltinTool):
124
155
  """A builtin tool that allows your agent to execute code.
125
156
 
@@ -134,6 +165,7 @@ class CodeExecutionTool(AbstractBuiltinTool):
134
165
  """The kind of tool."""
135
166
 
136
167
 
168
+ @dataclass(kw_only=True)
137
169
  class UrlContextTool(AbstractBuiltinTool):
138
170
  """Allows your agent to access contents from URLs.
139
171
 
@@ -227,6 +259,7 @@ class ImageGenerationTool(AbstractBuiltinTool):
227
259
  """The kind of tool."""
228
260
 
229
261
 
262
+ @dataclass(kw_only=True)
230
263
  class MemoryTool(AbstractBuiltinTool):
231
264
  """A builtin tool that allows your agent to use memory.
232
265
 
@@ -237,3 +270,72 @@ class MemoryTool(AbstractBuiltinTool):
237
270
 
238
271
  kind: str = 'memory'
239
272
  """The kind of tool."""
273
+
274
+
275
+ @dataclass(kw_only=True)
276
+ class MCPServerTool(AbstractBuiltinTool):
277
+ """A builtin tool that allows your agent to use MCP servers.
278
+
279
+ Supported by:
280
+
281
+ * OpenAI Responses
282
+ * Anthropic
283
+ """
284
+
285
+ id: str
286
+ """A unique identifier for the MCP server."""
287
+
288
+ url: str
289
+ """The URL of the MCP server to use.
290
+
291
+ For OpenAI Responses, it is possible to use `connector_id` by providing it as `x-openai-connector:<connector_id>`.
292
+ """
293
+
294
+ authorization_token: str | None = None
295
+ """Authorization header to use when making requests to the MCP server.
296
+
297
+ Supported by:
298
+
299
+ * OpenAI Responses
300
+ * Anthropic
301
+ """
302
+
303
+ description: str | None = None
304
+ """A description of the MCP server.
305
+
306
+ Supported by:
307
+
308
+ * OpenAI Responses
309
+ """
310
+
311
+ allowed_tools: list[str] | None = None
312
+ """A list of tools that the MCP server can use.
313
+
314
+ Supported by:
315
+
316
+ * OpenAI Responses
317
+ * Anthropic
318
+ """
319
+
320
+ headers: dict[str, str] | None = None
321
+ """Optional HTTP headers to send to the MCP server.
322
+
323
+ Use for authentication or other purposes.
324
+
325
+ Supported by:
326
+
327
+ * OpenAI Responses
328
+ """
329
+
330
+ kind: str = 'mcp_server'
331
+
332
+ @property
333
+ def unique_id(self) -> str:
334
+ return ':'.join([self.kind, self.id])
335
+
336
+
337
+ def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
338
+ if isinstance(tool_data, dict):
339
+ return tool_data.get('kind', AbstractBuiltinTool.kind)
340
+ else:
341
+ return tool_data.kind
pydantic_ai/direct.py CHANGED
@@ -50,7 +50,7 @@ async def model_request(
50
50
 
51
51
  async def main():
52
52
  model_response = await model_request(
53
- 'anthropic:claude-3-5-haiku-latest',
53
+ 'anthropic:claude-haiku-4-5',
54
54
  [ModelRequest.user_text_prompt('What is the capital of France?')] # (1)!
55
55
  )
56
56
  print(model_response)
@@ -58,7 +58,7 @@ async def model_request(
58
58
  ModelResponse(
59
59
  parts=[TextPart(content='The capital of France is Paris.')],
60
60
  usage=RequestUsage(input_tokens=56, output_tokens=7),
61
- model_name='claude-3-5-haiku-latest',
61
+ model_name='claude-haiku-4-5',
62
62
  timestamp=datetime.datetime(...),
63
63
  )
64
64
  '''
@@ -103,7 +103,7 @@ def model_request_sync(
103
103
  from pydantic_ai.direct import model_request_sync
104
104
 
105
105
  model_response = model_request_sync(
106
- 'anthropic:claude-3-5-haiku-latest',
106
+ 'anthropic:claude-haiku-4-5',
107
107
  [ModelRequest.user_text_prompt('What is the capital of France?')] # (1)!
108
108
  )
109
109
  print(model_response)
@@ -111,7 +111,7 @@ def model_request_sync(
111
111
  ModelResponse(
112
112
  parts=[TextPart(content='The capital of France is Paris.')],
113
113
  usage=RequestUsage(input_tokens=56, output_tokens=7),
114
- model_name='claude-3-5-haiku-latest',
114
+ model_name='claude-haiku-4-5',
115
115
  timestamp=datetime.datetime(...),
116
116
  )
117
117
  '''
@@ -172,6 +172,12 @@ def model_request_stream(
172
172
  index=0, delta=TextPartDelta(content_delta='a German-born theoretical ')
173
173
  ),
174
174
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='physicist.')),
175
+ PartEndEvent(
176
+ index=0,
177
+ part=TextPart(
178
+ content='Albert Einstein was a German-born theoretical physicist.'
179
+ ),
180
+ ),
175
181
  ]
176
182
  '''
177
183
  ```
@@ -229,6 +235,12 @@ def model_request_stream_sync(
229
235
  index=0, delta=TextPartDelta(content_delta='a German-born theoretical ')
230
236
  ),
231
237
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='physicist.')),
238
+ PartEndEvent(
239
+ index=0,
240
+ part=TextPart(
241
+ content='Albert Einstein was a German-born theoretical physicist.'
242
+ ),
243
+ ),
232
244
  ]
233
245
  '''
234
246
  ```
@@ -640,6 +640,9 @@ class DBOSAgent(WrapperAgent[AgentDepsT, OutputDataT], DBOSConfiguredInstance):
640
640
  PartStartEvent(index=0, part=TextPart(content='The capital of ')),
641
641
  FinalResultEvent(tool_name=None, tool_call_id=None),
642
642
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='France is Paris. ')),
643
+ PartEndEvent(
644
+ index=0, part=TextPart(content='The capital of France is Paris. ')
645
+ ),
643
646
  AgentRunResultEvent(
644
647
  result=AgentRunResult(output='The capital of France is Paris. ')
645
648
  ),
@@ -598,6 +598,9 @@ class PrefectAgent(WrapperAgent[AgentDepsT, OutputDataT]):
598
598
  PartStartEvent(index=0, part=TextPart(content='The capital of ')),
599
599
  FinalResultEvent(tool_name=None, tool_call_id=None),
600
600
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='France is Paris. ')),
601
+ PartEndEvent(
602
+ index=0, part=TextPart(content='The capital of France is Paris. ')
603
+ ),
601
604
  AgentRunResultEvent(
602
605
  result=AgentRunResult(output='The capital of France is Paris. ')
603
606
  ),
@@ -36,6 +36,17 @@ __all__ = [
36
36
  'TemporalWrapperToolset',
37
37
  ]
38
38
 
39
+ # We need eagerly import the anyio backends or it will happens inside workflow code and temporal has issues
40
+ # Note: It's difficult to add a test that covers this because pytest presumably does these imports itself
41
+ # when you have a @pytest.mark.anyio somewhere.
42
+ # I suppose we could add a test that runs a python script in a separate process, but I have not done that...
43
+ import anyio._backends._asyncio # pyright: ignore[reportUnusedImport]
44
+
45
+ try:
46
+ import anyio._backends._trio # noqa F401 # pyright: ignore[reportUnusedImport]
47
+ except ImportError:
48
+ pass
49
+
39
50
 
40
51
  class PydanticAIPlugin(ClientPlugin, WorkerPlugin):
41
52
  """Temporal client and worker plugin for Pydantic AI."""
@@ -669,6 +669,9 @@ class TemporalAgent(WrapperAgent[AgentDepsT, OutputDataT]):
669
669
  PartStartEvent(index=0, part=TextPart(content='The capital of ')),
670
670
  FinalResultEvent(tool_name=None, tool_call_id=None),
671
671
  PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='France is Paris. ')),
672
+ PartEndEvent(
673
+ index=0, part=TextPart(content='The capital of France is Paris. ')
674
+ ),
672
675
  AgentRunResultEvent(
673
676
  result=AgentRunResult(output='The capital of France is Paris. ')
674
677
  ),