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.
- pydantic_ai/_agent_graph.py +18 -14
- pydantic_ai/_output.py +20 -105
- pydantic_ai/_run_context.py +8 -2
- pydantic_ai/_tool_manager.py +30 -11
- pydantic_ai/_utils.py +18 -0
- pydantic_ai/agent/__init__.py +34 -32
- pydantic_ai/agent/abstract.py +155 -3
- pydantic_ai/agent/wrapper.py +5 -0
- pydantic_ai/common_tools/duckduckgo.py +1 -1
- pydantic_ai/durable_exec/dbos/_agent.py +28 -0
- pydantic_ai/durable_exec/prefect/_agent.py +25 -0
- pydantic_ai/durable_exec/temporal/_agent.py +25 -0
- pydantic_ai/durable_exec/temporal/_function_toolset.py +23 -73
- pydantic_ai/durable_exec/temporal/_mcp_server.py +30 -30
- pydantic_ai/durable_exec/temporal/_run_context.py +9 -3
- pydantic_ai/durable_exec/temporal/_toolset.py +67 -3
- pydantic_ai/mcp.py +4 -4
- pydantic_ai/messages.py +11 -2
- pydantic_ai/models/__init__.py +80 -35
- pydantic_ai/models/anthropic.py +27 -8
- pydantic_ai/models/bedrock.py +3 -3
- pydantic_ai/models/cohere.py +5 -3
- pydantic_ai/models/fallback.py +25 -4
- pydantic_ai/models/function.py +8 -0
- pydantic_ai/models/gemini.py +3 -3
- pydantic_ai/models/google.py +25 -22
- pydantic_ai/models/groq.py +5 -3
- pydantic_ai/models/huggingface.py +3 -3
- pydantic_ai/models/instrumented.py +29 -13
- pydantic_ai/models/mistral.py +6 -4
- pydantic_ai/models/openai.py +15 -6
- pydantic_ai/models/outlines.py +21 -12
- pydantic_ai/models/wrapper.py +1 -1
- pydantic_ai/output.py +3 -2
- pydantic_ai/profiles/openai.py +5 -2
- pydantic_ai/providers/anthropic.py +2 -2
- pydantic_ai/providers/openrouter.py +3 -0
- pydantic_ai/result.py +159 -4
- pydantic_ai/tools.py +12 -10
- pydantic_ai/ui/_adapter.py +2 -2
- pydantic_ai/ui/_event_stream.py +4 -4
- pydantic_ai/ui/ag_ui/_event_stream.py +11 -2
- pydantic_ai/ui/ag_ui/app.py +8 -1
- {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/METADATA +9 -7
- {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/RECORD +48 -48
- {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-1.9.0.dist-info → pydantic_ai_slim-1.12.0.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/_agent_graph.py
CHANGED
|
@@ -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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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=
|
|
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
|
-
|
|
588
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
@
|
|
222
|
-
def
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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,
|
|
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
|
|
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
|
pydantic_ai/_run_context.py
CHANGED
|
@@ -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[
|
|
28
|
+
class RunContext(Generic[RunContextAgentDepsT]):
|
|
25
29
|
"""Information about the current call."""
|
|
26
30
|
|
|
27
|
-
deps:
|
|
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:
|
pydantic_ai/_tool_manager.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
117
|
-
self.ctx.
|
|
118
|
-
self.ctx.
|
|
119
|
-
self.ctx.
|
|
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.
|
|
142
|
-
raise RuntimeError('
|
|
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(
|
|
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
|