pydantic-ai-slim 1.9.0__py3-none-any.whl → 1.12.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 (48) hide show
  1. pydantic_ai/_agent_graph.py +18 -14
  2. pydantic_ai/_output.py +20 -105
  3. pydantic_ai/_run_context.py +8 -2
  4. pydantic_ai/_tool_manager.py +30 -11
  5. pydantic_ai/_utils.py +18 -0
  6. pydantic_ai/agent/__init__.py +34 -32
  7. pydantic_ai/agent/abstract.py +155 -3
  8. pydantic_ai/agent/wrapper.py +5 -0
  9. pydantic_ai/common_tools/duckduckgo.py +1 -1
  10. pydantic_ai/durable_exec/dbos/_agent.py +28 -0
  11. pydantic_ai/durable_exec/prefect/_agent.py +25 -0
  12. pydantic_ai/durable_exec/temporal/_agent.py +25 -0
  13. pydantic_ai/durable_exec/temporal/_function_toolset.py +23 -73
  14. pydantic_ai/durable_exec/temporal/_mcp_server.py +30 -30
  15. pydantic_ai/durable_exec/temporal/_run_context.py +9 -3
  16. pydantic_ai/durable_exec/temporal/_toolset.py +67 -3
  17. pydantic_ai/mcp.py +4 -4
  18. pydantic_ai/messages.py +11 -2
  19. pydantic_ai/models/__init__.py +80 -35
  20. pydantic_ai/models/anthropic.py +27 -8
  21. pydantic_ai/models/bedrock.py +3 -3
  22. pydantic_ai/models/cohere.py +5 -3
  23. pydantic_ai/models/fallback.py +25 -4
  24. pydantic_ai/models/function.py +8 -0
  25. pydantic_ai/models/gemini.py +3 -3
  26. pydantic_ai/models/google.py +25 -22
  27. pydantic_ai/models/groq.py +5 -3
  28. pydantic_ai/models/huggingface.py +3 -3
  29. pydantic_ai/models/instrumented.py +29 -13
  30. pydantic_ai/models/mistral.py +6 -4
  31. pydantic_ai/models/openai.py +15 -6
  32. pydantic_ai/models/outlines.py +21 -12
  33. pydantic_ai/models/wrapper.py +1 -1
  34. pydantic_ai/output.py +3 -2
  35. pydantic_ai/profiles/openai.py +5 -2
  36. pydantic_ai/providers/anthropic.py +2 -2
  37. pydantic_ai/providers/openrouter.py +3 -0
  38. pydantic_ai/result.py +159 -4
  39. pydantic_ai/tools.py +12 -10
  40. pydantic_ai/ui/_adapter.py +2 -2
  41. pydantic_ai/ui/_event_stream.py +4 -4
  42. pydantic_ai/ui/ag_ui/_event_stream.py +11 -2
  43. pydantic_ai/ui/ag_ui/app.py +8 -1
  44. {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/METADATA +9 -7
  45. {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/RECORD +48 -48
  46. {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/WHEEL +0 -0
  47. {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/entry_points.txt +0 -0
  48. {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -374,9 +374,10 @@ async def _prepare_request_parameters(
374
374
  ) -> models.ModelRequestParameters:
375
375
  """Build tools and create an agent model."""
376
376
  output_schema = ctx.deps.output_schema
377
- output_object = None
378
- if isinstance(output_schema, _output.NativeOutputSchema):
379
- output_object = output_schema.object_def
377
+
378
+ prompted_output_template = (
379
+ output_schema.template if isinstance(output_schema, _output.PromptedOutputSchema) else None
380
+ )
380
381
 
381
382
  function_tools: list[ToolDefinition] = []
382
383
  output_tools: list[ToolDefinition] = []
@@ -391,7 +392,8 @@ async def _prepare_request_parameters(
391
392
  builtin_tools=ctx.deps.builtin_tools,
392
393
  output_mode=output_schema.mode,
393
394
  output_tools=output_tools,
394
- output_object=output_object,
395
+ output_object=output_schema.object_def,
396
+ prompted_output_template=prompted_output_template,
395
397
  allow_text_output=output_schema.allows_text,
396
398
  allow_image_output=output_schema.allows_image,
397
399
  )
@@ -489,7 +491,6 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
489
491
  message_history = _clean_message_history(message_history)
490
492
 
491
493
  model_request_parameters = await _prepare_request_parameters(ctx)
492
- model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
493
494
 
494
495
  model_settings = ctx.deps.model_settings
495
496
  usage = ctx.state.usage
@@ -570,7 +571,7 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
570
571
  # we got an empty response.
571
572
  # this sometimes happens with anthropic (and perhaps other models)
572
573
  # when the model has already returned text along side tool calls
573
- if text_processor := output_schema.text_processor:
574
+ if text_processor := output_schema.text_processor: # pragma: no branch
574
575
  # in this scenario, if text responses are allowed, we return text from the most recent model
575
576
  # response, if any
576
577
  for message in reversed(ctx.state.message_history):
@@ -584,8 +585,12 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
584
585
  # not part of the final result output, so we reset the accumulated text
585
586
  text = '' # pragma: no cover
586
587
  if text:
587
- self._next_node = await self._handle_text_response(ctx, text, text_processor)
588
- return
588
+ try:
589
+ self._next_node = await self._handle_text_response(ctx, text, text_processor)
590
+ return
591
+ except ToolRetryError:
592
+ # If the text from the preview response was invalid, ignore it.
593
+ pass
589
594
 
590
595
  # Go back to the model request node with an empty request, which means we'll essentially
591
596
  # resubmit the most recent request that resulted in an empty response,
@@ -622,11 +627,11 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
622
627
  else:
623
628
  assert_never(part)
624
629
 
625
- # At the moment, we prioritize at least executing tool calls if they are present.
626
- # In the future, we'd consider making this configurable at the agent or run level.
627
- # This accounts for cases like anthropic returns that might contain a text response
628
- # and a tool call response, where the text response just indicates the tool call will happen.
629
630
  try:
631
+ # At the moment, we prioritize at least executing tool calls if they are present.
632
+ # In the future, we'd consider making this configurable at the agent or run level.
633
+ # This accounts for cases like anthropic returns that might contain a text response
634
+ # and a tool call response, where the text response just indicates the tool call will happen.
630
635
  alternatives: list[str] = []
631
636
  if tool_calls:
632
637
  async for event in self._handle_tool_calls(ctx, tool_calls):
@@ -770,7 +775,6 @@ def build_run_context(ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT
770
775
  if ctx.deps.instrumentation_settings
771
776
  else DEFAULT_INSTRUMENTATION_VERSION,
772
777
  run_step=ctx.state.run_step,
773
- tool_call_approved=ctx.state.run_step == 0,
774
778
  )
775
779
 
776
780
 
@@ -1034,7 +1038,7 @@ async def _call_tool(
1034
1038
  elif isinstance(tool_call_result, ToolApproved):
1035
1039
  if tool_call_result.override_args is not None:
1036
1040
  tool_call = dataclasses.replace(tool_call, args=tool_call_result.override_args)
1037
- tool_result = await tool_manager.handle_call(tool_call)
1041
+ tool_result = await tool_manager.handle_call(tool_call, approved=True)
1038
1042
  elif isinstance(tool_call_result, ToolDenied):
1039
1043
  return _messages.ToolReturnPart(
1040
1044
  tool_name=tool_call.tool_name,
pydantic_ai/_output.py CHANGED
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
10
10
 
11
11
  from pydantic import Json, TypeAdapter, ValidationError
12
12
  from pydantic_core import SchemaValidator, to_json
13
- from typing_extensions import Self, TypedDict, TypeVar, assert_never
13
+ from typing_extensions import Self, TypedDict, TypeVar
14
14
 
15
15
  from pydantic_ai._instrumentation import InstrumentationNames
16
16
 
@@ -26,7 +26,6 @@ from .output import (
26
26
  OutputSpec,
27
27
  OutputTypeOrFunction,
28
28
  PromptedOutput,
29
- StructuredOutputMode,
30
29
  TextOutput,
31
30
  TextOutputFunc,
32
31
  ToolOutput,
@@ -36,7 +35,7 @@ from .tools import GenerateToolJsonSchema, ObjectJsonSchema, ToolDefinition
36
35
  from .toolsets.abstract import AbstractToolset, ToolsetTool
37
36
 
38
37
  if TYPE_CHECKING:
39
- from .profiles import ModelProfile
38
+ pass
40
39
 
41
40
  T = TypeVar('T')
42
41
  """An invariant TypeVar."""
@@ -212,59 +211,30 @@ class OutputValidator(Generic[AgentDepsT, OutputDataT_inv]):
212
211
 
213
212
 
214
213
  @dataclass(kw_only=True)
215
- class BaseOutputSchema(ABC, Generic[OutputDataT]):
214
+ class OutputSchema(ABC, Generic[OutputDataT]):
216
215
  text_processor: BaseOutputProcessor[OutputDataT] | None = None
217
216
  toolset: OutputToolset[Any] | None = None
217
+ object_def: OutputObjectDefinition | None = None
218
218
  allows_deferred_tools: bool = False
219
219
  allows_image: bool = False
220
220
 
221
- @abstractmethod
222
- def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
221
+ @property
222
+ def mode(self) -> OutputMode:
223
223
  raise NotImplementedError()
224
224
 
225
225
  @property
226
226
  def allows_text(self) -> bool:
227
227
  return self.text_processor is not None
228
228
 
229
-
230
- @dataclass(init=False)
231
- class OutputSchema(BaseOutputSchema[OutputDataT], ABC):
232
- """Model the final output from an agent run."""
233
-
234
- @classmethod
235
- @overload
236
- def build(
237
- cls,
238
- output_spec: OutputSpec[OutputDataT],
239
- *,
240
- default_mode: StructuredOutputMode,
241
- name: str | None = None,
242
- description: str | None = None,
243
- strict: bool | None = None,
244
- ) -> OutputSchema[OutputDataT]: ...
245
-
246
- @classmethod
247
- @overload
248
- def build(
249
- cls,
250
- output_spec: OutputSpec[OutputDataT],
251
- *,
252
- default_mode: None = None,
253
- name: str | None = None,
254
- description: str | None = None,
255
- strict: bool | None = None,
256
- ) -> BaseOutputSchema[OutputDataT]: ...
257
-
258
229
  @classmethod
259
230
  def build( # noqa: C901
260
231
  cls,
261
232
  output_spec: OutputSpec[OutputDataT],
262
233
  *,
263
- default_mode: StructuredOutputMode | None = None,
264
234
  name: str | None = None,
265
235
  description: str | None = None,
266
236
  strict: bool | None = None,
267
- ) -> BaseOutputSchema[OutputDataT]:
237
+ ) -> OutputSchema[OutputDataT]:
268
238
  """Build an OutputSchema dataclass from an output type."""
269
239
  outputs = _flatten_output_spec(output_spec)
270
240
 
@@ -382,15 +352,12 @@ class OutputSchema(BaseOutputSchema[OutputDataT], ABC):
382
352
  )
383
353
 
384
354
  if len(other_outputs) > 0:
385
- schema = OutputSchemaWithoutMode(
355
+ return AutoOutputSchema(
386
356
  processor=cls._build_processor(other_outputs, name=name, description=description, strict=strict),
387
357
  toolset=toolset,
388
358
  allows_deferred_tools=allows_deferred_tools,
389
359
  allows_image=allows_image,
390
360
  )
391
- if default_mode:
392
- schema = schema.with_default_mode(default_mode)
393
- return schema
394
361
 
395
362
  if allows_image:
396
363
  return ImageOutputSchema(allows_deferred_tools=allows_deferred_tools)
@@ -410,22 +377,9 @@ class OutputSchema(BaseOutputSchema[OutputDataT], ABC):
410
377
 
411
378
  return UnionOutputProcessor(outputs=outputs, strict=strict, name=name, description=description)
412
379
 
413
- @property
414
- @abstractmethod
415
- def mode(self) -> OutputMode:
416
- raise NotImplementedError()
417
-
418
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
419
- """Raise an error if the mode is not supported by this model."""
420
- if self.allows_image and not profile.supports_image_output:
421
- raise UserError('Image output is not supported by this model.')
422
-
423
- def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
424
- return self
425
-
426
380
 
427
381
  @dataclass(init=False)
428
- class OutputSchemaWithoutMode(BaseOutputSchema[OutputDataT]):
382
+ class AutoOutputSchema(OutputSchema[OutputDataT]):
429
383
  processor: BaseObjectOutputProcessor[OutputDataT]
430
384
 
431
385
  def __init__(
@@ -439,32 +393,17 @@ class OutputSchemaWithoutMode(BaseOutputSchema[OutputDataT]):
439
393
  # At that point we may not know yet what output mode we're going to use if no model was provided or it was deferred until agent.run time,
440
394
  # but we cover ourselves just in case we end up using the tool output mode.
441
395
  super().__init__(
442
- allows_deferred_tools=allows_deferred_tools,
443
396
  toolset=toolset,
397
+ object_def=processor.object_def,
444
398
  text_processor=processor,
399
+ allows_deferred_tools=allows_deferred_tools,
445
400
  allows_image=allows_image,
446
401
  )
447
402
  self.processor = processor
448
403
 
449
- def with_default_mode(self, mode: StructuredOutputMode) -> OutputSchema[OutputDataT]:
450
- if mode == 'native':
451
- return NativeOutputSchema(
452
- processor=self.processor,
453
- allows_deferred_tools=self.allows_deferred_tools,
454
- allows_image=self.allows_image,
455
- )
456
- elif mode == 'prompted':
457
- return PromptedOutputSchema(
458
- processor=self.processor,
459
- allows_deferred_tools=self.allows_deferred_tools,
460
- allows_image=self.allows_image,
461
- )
462
- elif mode == 'tool':
463
- return ToolOutputSchema(
464
- toolset=self.toolset, allows_deferred_tools=self.allows_deferred_tools, allows_image=self.allows_image
465
- )
466
- else:
467
- assert_never(mode)
404
+ @property
405
+ def mode(self) -> OutputMode:
406
+ return 'auto'
468
407
 
469
408
 
470
409
  @dataclass(init=False)
@@ -486,10 +425,6 @@ class TextOutputSchema(OutputSchema[OutputDataT]):
486
425
  def mode(self) -> OutputMode:
487
426
  return 'text'
488
427
 
489
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
490
- """Raise an error if the mode is not supported by this model."""
491
- super().raise_if_unsupported(profile)
492
-
493
428
 
494
429
  class ImageOutputSchema(OutputSchema[OutputDataT]):
495
430
  def __init__(self, *, allows_deferred_tools: bool):
@@ -499,11 +434,6 @@ class ImageOutputSchema(OutputSchema[OutputDataT]):
499
434
  def mode(self) -> OutputMode:
500
435
  return 'image'
501
436
 
502
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
503
- """Raise an error if the mode is not supported by this model."""
504
- # This already raises if image output is not supported by this model.
505
- super().raise_if_unsupported(profile)
506
-
507
437
 
508
438
  @dataclass(init=False)
509
439
  class StructuredTextOutputSchema(OutputSchema[OutputDataT], ABC):
@@ -513,25 +443,19 @@ class StructuredTextOutputSchema(OutputSchema[OutputDataT], ABC):
513
443
  self, *, processor: BaseObjectOutputProcessor[OutputDataT], allows_deferred_tools: bool, allows_image: bool
514
444
  ):
515
445
  super().__init__(
516
- text_processor=processor, allows_deferred_tools=allows_deferred_tools, allows_image=allows_image
446
+ text_processor=processor,
447
+ object_def=processor.object_def,
448
+ allows_deferred_tools=allows_deferred_tools,
449
+ allows_image=allows_image,
517
450
  )
518
451
  self.processor = processor
519
452
 
520
- @property
521
- def object_def(self) -> OutputObjectDefinition:
522
- return self.processor.object_def
523
-
524
453
 
525
454
  class NativeOutputSchema(StructuredTextOutputSchema[OutputDataT]):
526
455
  @property
527
456
  def mode(self) -> OutputMode:
528
457
  return 'native'
529
458
 
530
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
531
- """Raise an error if the mode is not supported by this model."""
532
- if not profile.supports_json_schema_output:
533
- raise UserError('Native structured output is not supported by this model.')
534
-
535
459
 
536
460
  @dataclass(init=False)
537
461
  class PromptedOutputSchema(StructuredTextOutputSchema[OutputDataT]):
@@ -570,14 +494,11 @@ class PromptedOutputSchema(StructuredTextOutputSchema[OutputDataT]):
570
494
 
571
495
  return template.format(schema=json.dumps(schema))
572
496
 
573
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
574
- """Raise an error if the mode is not supported by this model."""
575
- super().raise_if_unsupported(profile)
576
-
577
- def instructions(self, default_template: str) -> str:
497
+ def instructions(self, default_template: str) -> str: # pragma: no cover
578
498
  """Get instructions to tell model to output JSON matching the schema."""
579
499
  template = self.template or default_template
580
500
  object_def = self.object_def
501
+ assert object_def is not None
581
502
  return self.build_instructions(template, object_def)
582
503
 
583
504
 
@@ -602,12 +523,6 @@ class ToolOutputSchema(OutputSchema[OutputDataT]):
602
523
  def mode(self) -> OutputMode:
603
524
  return 'tool'
604
525
 
605
- def raise_if_unsupported(self, profile: ModelProfile) -> None:
606
- """Raise an error if the mode is not supported by this model."""
607
- super().raise_if_unsupported(profile)
608
- if not profile.supports_tools:
609
- raise UserError('Tool output is not supported by this model.')
610
-
611
526
 
612
527
  class BaseOutputProcessor(ABC, Generic[OutputDataT]):
613
528
  @abstractmethod
@@ -16,15 +16,19 @@ if TYPE_CHECKING:
16
16
  from .models import Model
17
17
  from .result import RunUsage
18
18
 
19
+ # TODO (v2): Change the default for all typevars like this from `None` to `object`
19
20
  AgentDepsT = TypeVar('AgentDepsT', default=None, contravariant=True)
20
21
  """Type variable for agent dependencies."""
21
22
 
23
+ RunContextAgentDepsT = TypeVar('RunContextAgentDepsT', default=None, covariant=True)
24
+ """Type variable for the agent dependencies in `RunContext`."""
25
+
22
26
 
23
27
  @dataclasses.dataclass(repr=False, kw_only=True)
24
- class RunContext(Generic[AgentDepsT]):
28
+ class RunContext(Generic[RunContextAgentDepsT]):
25
29
  """Information about the current call."""
26
30
 
27
- deps: AgentDepsT
31
+ deps: RunContextAgentDepsT
28
32
  """Dependencies for the agent."""
29
33
  model: Model
30
34
  """The model used in this run."""
@@ -54,6 +58,8 @@ class RunContext(Generic[AgentDepsT]):
54
58
  """The current step in the run."""
55
59
  tool_call_approved: bool = False
56
60
  """Whether a tool call that required approval has now been approved."""
61
+ partial_output: bool = False
62
+ """Whether the output passed to an output validator is partial."""
57
63
 
58
64
  @property
59
65
  def last_attempt(self) -> bool:
@@ -93,6 +93,8 @@ class ToolManager(Generic[AgentDepsT]):
93
93
  call: ToolCallPart,
94
94
  allow_partial: bool = False,
95
95
  wrap_validation_errors: bool = True,
96
+ *,
97
+ approved: bool = False,
96
98
  ) -> Any:
97
99
  """Handle a tool call by validating the arguments, calling the tool, and handling retries.
98
100
 
@@ -100,30 +102,38 @@ class ToolManager(Generic[AgentDepsT]):
100
102
  call: The tool call part to handle.
101
103
  allow_partial: Whether to allow partial validation of the tool arguments.
102
104
  wrap_validation_errors: Whether to wrap validation errors in a retry prompt part.
103
- usage_limits: Optional usage limits to check before executing tools.
105
+ approved: Whether the tool call has been approved.
104
106
  """
105
107
  if self.tools is None or self.ctx is None:
106
108
  raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
107
109
 
108
110
  if (tool := self.tools.get(call.tool_name)) and tool.tool_def.kind == 'output':
109
111
  # Output tool calls are not traced and not counted
110
- return await self._call_tool(call, allow_partial, wrap_validation_errors)
112
+ return await self._call_tool(
113
+ call,
114
+ allow_partial=allow_partial,
115
+ wrap_validation_errors=wrap_validation_errors,
116
+ approved=approved,
117
+ )
111
118
  else:
112
119
  return await self._call_function_tool(
113
120
  call,
114
- allow_partial,
115
- wrap_validation_errors,
116
- self.ctx.tracer,
117
- self.ctx.trace_include_content,
118
- self.ctx.instrumentation_version,
119
- self.ctx.usage,
121
+ allow_partial=allow_partial,
122
+ wrap_validation_errors=wrap_validation_errors,
123
+ approved=approved,
124
+ tracer=self.ctx.tracer,
125
+ include_content=self.ctx.trace_include_content,
126
+ instrumentation_version=self.ctx.instrumentation_version,
127
+ usage=self.ctx.usage,
120
128
  )
121
129
 
122
130
  async def _call_tool(
123
131
  self,
124
132
  call: ToolCallPart,
133
+ *,
125
134
  allow_partial: bool,
126
135
  wrap_validation_errors: bool,
136
+ approved: bool,
127
137
  ) -> Any:
128
138
  if self.tools is None or self.ctx is None:
129
139
  raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
@@ -138,8 +148,8 @@ class ToolManager(Generic[AgentDepsT]):
138
148
  msg = 'No tools available.'
139
149
  raise ModelRetry(f'Unknown tool name: {name!r}. {msg}')
140
150
 
141
- if tool.tool_def.defer:
142
- raise RuntimeError('Deferred tools cannot be called')
151
+ if tool.tool_def.kind == 'external':
152
+ raise RuntimeError('External tools cannot be called')
143
153
 
144
154
  ctx = replace(
145
155
  self.ctx,
@@ -147,6 +157,8 @@ class ToolManager(Generic[AgentDepsT]):
147
157
  tool_call_id=call.tool_call_id,
148
158
  retry=self.ctx.retries.get(name, 0),
149
159
  max_retries=tool.max_retries,
160
+ tool_call_approved=approved,
161
+ partial_output=allow_partial,
150
162
  )
151
163
 
152
164
  pyd_allow_partial = 'trailing-strings' if allow_partial else 'off'
@@ -193,8 +205,10 @@ class ToolManager(Generic[AgentDepsT]):
193
205
  async def _call_function_tool(
194
206
  self,
195
207
  call: ToolCallPart,
208
+ *,
196
209
  allow_partial: bool,
197
210
  wrap_validation_errors: bool,
211
+ approved: bool,
198
212
  tracer: Tracer,
199
213
  include_content: bool,
200
214
  instrumentation_version: int,
@@ -233,7 +247,12 @@ class ToolManager(Generic[AgentDepsT]):
233
247
  attributes=span_attributes,
234
248
  ) as span:
235
249
  try:
236
- tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors)
250
+ tool_result = await self._call_tool(
251
+ call,
252
+ allow_partial=allow_partial,
253
+ wrap_validation_errors=wrap_validation_errors,
254
+ approved=approved,
255
+ )
237
256
  usage.tool_calls += 1
238
257
 
239
258
  except ToolRetryError as e:
pydantic_ai/_utils.py CHANGED
@@ -234,6 +234,15 @@ def sync_anext(iterator: Iterator[T]) -> T:
234
234
  raise StopAsyncIteration() from e
235
235
 
236
236
 
237
+ def sync_async_iterator(async_iter: AsyncIterator[T]) -> Iterator[T]:
238
+ loop = get_event_loop()
239
+ while True:
240
+ try:
241
+ yield loop.run_until_complete(anext(async_iter))
242
+ except StopAsyncIteration:
243
+ break
244
+
245
+
237
246
  def now_utc() -> datetime:
238
247
  return datetime.now(tz=timezone.utc)
239
248
 
@@ -489,3 +498,12 @@ def get_union_args(tp: Any) -> tuple[Any, ...]:
489
498
  return tuple(_unwrap_annotated(arg) for arg in get_args(tp))
490
499
  else:
491
500
  return ()
501
+
502
+
503
+ def get_event_loop():
504
+ try:
505
+ event_loop = asyncio.get_event_loop()
506
+ except RuntimeError: # pragma: lax no cover
507
+ event_loop = asyncio.new_event_loop()
508
+ asyncio.set_event_loop(event_loop)
509
+ return event_loop