Skip to content

Commit 8dc2592

Browse files
committed
Support returning resolved tools on the ModelRequest
1 parent 365b67b commit 8dc2592

File tree

6 files changed

+137
-79
lines changed

6 files changed

+137
-79
lines changed

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,10 @@ async def _prepare_request(
495495

496496
model_request_parameters = await _prepare_request_parameters(ctx)
497497

498+
# Populate tool tracking on the ModelRequest (the last request in the original history)
499+
self.request.function_tools = model_request_parameters.function_tools
500+
self.request.builtin_tools = model_request_parameters.builtin_tools
501+
498502
model_settings = ctx.deps.model_settings
499503
usage = ctx.state.usage
500504
if ctx.deps.usage_limits.count_tokens_before_request:
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations as _annotations
2+
3+
from dataclasses import dataclass, field
4+
from typing import Any, Literal, TypeAlias
5+
6+
from . import _utils
7+
8+
ObjectJsonSchema: TypeAlias = dict[str, Any]
9+
"""Type representing JSON schema of an object, e.g. where `'type': 'object'`.
10+
11+
This type is used to define tool parameters (aka arguments) in [`ToolDefinition`][pydantic_ai.tools.ToolDefinition].
12+
"""
13+
14+
ToolKind: TypeAlias = Literal['function', 'output', 'external', 'unapproved']
15+
"""Kind of tool."""
16+
17+
18+
@dataclass(repr=False, kw_only=True)
19+
class ToolDefinition:
20+
"""Definition of a tool passed to a model.
21+
22+
This is used for both function tools and output tools.
23+
"""
24+
25+
name: str
26+
"""The name of the tool."""
27+
28+
parameters_json_schema: ObjectJsonSchema = field(default_factory=lambda: {'type': 'object', 'properties': {}})
29+
"""The JSON schema for the tool's parameters."""
30+
31+
description: str | None = None
32+
"""The description of the tool."""
33+
34+
outer_typed_dict_key: str | None = None
35+
"""The key in the outer [TypedDict] that wraps an output tool.
36+
37+
This will only be set for output tools which don't have an `object` JSON schema.
38+
"""
39+
40+
strict: bool | None = None
41+
"""Whether to enforce (vendor-specific) strict JSON schema validation for tool calls.
42+
43+
Setting this to `True` while using a supported model generally imposes some restrictions on the tool's JSON schema
44+
in exchange for guaranteeing the API responses strictly match that schema.
45+
46+
When `False`, the model may be free to generate other properties or types (depending on the vendor).
47+
When `None` (the default), the value will be inferred based on the compatibility of the parameters_json_schema.
48+
49+
Note: this is currently only supported by OpenAI models.
50+
"""
51+
52+
sequential: bool = False
53+
"""Whether this tool requires a sequential/serial execution environment."""
54+
55+
kind: ToolKind = field(default='function')
56+
"""The kind of tool:
57+
58+
- `'function'`: a tool that will be executed by Pydantic AI during an agent run and has its result returned to the model
59+
- `'output'`: a tool that passes through an output value that ends the run
60+
- `'external'`: a tool whose result will be produced outside of the Pydantic AI agent run in which it was called, because it depends on an upstream service (or user) or could take longer to generate than it's reasonable to keep the agent process running.
61+
See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
62+
- `'unapproved'`: a tool that requires human-in-the-loop approval.
63+
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
64+
"""
65+
66+
metadata: dict[str, Any] | None = None
67+
"""Tool metadata that can be set by the toolset this tool came from. It is not sent to the model, but can be used for filtering and tool behavior customization.
68+
69+
For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition.
70+
"""
71+
72+
@property
73+
def defer(self) -> bool:
74+
"""Whether calls to this tool will be deferred.
75+
76+
See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
77+
"""
78+
return self.kind in ('external', 'unapproved')
79+
80+
__repr__ = _utils.dataclasses_no_defaults_repr

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from pydantic_graph import GraphRun, GraphRunResult
3737

3838
from . import messages as _messages
39-
from .tools import ObjectJsonSchema
39+
from ._tool_types import ObjectJsonSchema
4040

4141
_P = ParamSpec('_P')
4242
_R = TypeVar('_R')

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from typing_extensions import deprecated
1717

1818
from . import _otel_messages, _utils
19+
from ._tool_types import ToolDefinition
1920
from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc
21+
from .builtin_tools import AbstractBuiltinTool
2022
from .exceptions import UnexpectedModelBehavior
2123
from .usage import RequestUsage
2224

@@ -945,6 +947,22 @@ class ModelRequest:
945947
instructions: str | None = None
946948
"""The instructions for the model."""
947949

950+
function_tools: Annotated[list[ToolDefinition] | None, pydantic.Field(exclude=True, repr=False)] = field(
951+
default=None, repr=False
952+
)
953+
"""Function tools that were available for this request.
954+
955+
Available for introspection during a run. This field is excluded from serialization.
956+
"""
957+
958+
builtin_tools: Annotated[list[AbstractBuiltinTool] | None, pydantic.Field(exclude=True, repr=False)] = field(
959+
default=None, repr=False
960+
)
961+
"""Builtin tools that were available for this request.
962+
963+
Available for introspection during a run. This field is excluded from serialization.
964+
"""
965+
948966
kind: Literal['request'] = 'request'
949967
"""Message type identifier, this is available on all parts as a discriminator."""
950968

pydantic_ai_slim/pydantic_ai/tools.py

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from . import _function_schema, _utils
1313
from ._run_context import AgentDepsT, RunContext
14+
from ._tool_types import ObjectJsonSchema, ToolDefinition, ToolKind as _ToolKind
1415
from .exceptions import ModelRetry
1516
from .messages import RetryPromptPart, ToolCallPart, ToolReturn
1617

@@ -28,12 +29,15 @@
2829
'Tool',
2930
'ObjectJsonSchema',
3031
'ToolDefinition',
32+
'ToolKind',
3133
'DeferredToolRequests',
3234
'DeferredToolResults',
3335
'ToolApproved',
3436
'ToolDenied',
3537
)
3638

39+
ToolKind = _ToolKind
40+
3741

3842
ToolParams = ParamSpec('ToolParams', default=...)
3943
"""Retrieval function param spec."""
@@ -433,80 +437,3 @@ async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinit
433437
return await self.prepare(ctx, base_tool_def)
434438
else:
435439
return base_tool_def
436-
437-
438-
ObjectJsonSchema: TypeAlias = dict[str, Any]
439-
"""Type representing JSON schema of an object, e.g. where `"type": "object"`.
440-
441-
This type is used to define tools parameters (aka arguments) in [ToolDefinition][pydantic_ai.tools.ToolDefinition].
442-
443-
With PEP-728 this should be a TypedDict with `type: Literal['object']`, and `extra_parts=Any`
444-
"""
445-
446-
ToolKind: TypeAlias = Literal['function', 'output', 'external', 'unapproved']
447-
"""Kind of tool."""
448-
449-
450-
@dataclass(repr=False, kw_only=True)
451-
class ToolDefinition:
452-
"""Definition of a tool passed to a model.
453-
454-
This is used for both function tools and output tools.
455-
"""
456-
457-
name: str
458-
"""The name of the tool."""
459-
460-
parameters_json_schema: ObjectJsonSchema = field(default_factory=lambda: {'type': 'object', 'properties': {}})
461-
"""The JSON schema for the tool's parameters."""
462-
463-
description: str | None = None
464-
"""The description of the tool."""
465-
466-
outer_typed_dict_key: str | None = None
467-
"""The key in the outer [TypedDict] that wraps an output tool.
468-
469-
This will only be set for output tools which don't have an `object` JSON schema.
470-
"""
471-
472-
strict: bool | None = None
473-
"""Whether to enforce (vendor-specific) strict JSON schema validation for tool calls.
474-
475-
Setting this to `True` while using a supported model generally imposes some restrictions on the tool's JSON schema
476-
in exchange for guaranteeing the API responses strictly match that schema.
477-
478-
When `False`, the model may be free to generate other properties or types (depending on the vendor).
479-
When `None` (the default), the value will be inferred based on the compatibility of the parameters_json_schema.
480-
481-
Note: this is currently only supported by OpenAI models.
482-
"""
483-
484-
sequential: bool = False
485-
"""Whether this tool requires a sequential/serial execution environment."""
486-
487-
kind: ToolKind = field(default='function')
488-
"""The kind of tool:
489-
490-
- `'function'`: a tool that will be executed by Pydantic AI during an agent run and has its result returned to the model
491-
- `'output'`: a tool that passes through an output value that ends the run
492-
- `'external'`: a tool whose result will be produced outside of the Pydantic AI agent run in which it was called, because it depends on an upstream service (or user) or could take longer to generate than it's reasonable to keep the agent process running.
493-
See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
494-
- `'unapproved'`: a tool that requires human-in-the-loop approval.
495-
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
496-
"""
497-
498-
metadata: dict[str, Any] | None = None
499-
"""Tool metadata that can be set by the toolset this tool came from. It is not sent to the model, but can be used for filtering and tool behavior customization.
500-
501-
For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition.
502-
"""
503-
504-
@property
505-
def defer(self) -> bool:
506-
"""Whether calls to this tool will be deferred.
507-
508-
See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
509-
"""
510-
return self.kind in ('external', 'unapproved')
511-
512-
__repr__ = _utils.dataclasses_no_defaults_repr

tests/test_messages.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
from datetime import datetime, timezone
33

4+
from pydantic_ai.builtin_tools import ImageGenerationTool
45
import pytest
56
from inline_snapshot import snapshot
67
from pydantic import TypeAdapter
@@ -26,6 +27,7 @@
2627
UserPromptPart,
2728
VideoUrl,
2829
)
30+
from pydantic_ai.models import ToolDefinition
2931

3032
from .conftest import IsDatetime, IsNow, IsStr
3133

@@ -404,7 +406,9 @@ def test_pre_usage_refactor_messages_deserializable():
404406
content='What is the capital of Mexico?',
405407
timestamp=IsNow(tz=timezone.utc),
406408
)
407-
]
409+
],
410+
function_tools=None,
411+
builtin_tools=None,
408412
),
409413
ModelResponse(
410414
parts=[TextPart(content='Mexico City.')],
@@ -605,3 +609,28 @@ def test_binary_content_validation_with_optional_identifier():
605609
'identifier': 'foo',
606610
}
607611
)
612+
613+
614+
def test_model_request_tool_tracking_excluded_from_serialization():
615+
"""Test that function_tools and builtin_tools are not serialized in the request."""
616+
tool_def = ToolDefinition(
617+
name='test_tool',
618+
description='A test tool',
619+
parameters_json_schema={'type': 'object', 'properties': {}},
620+
)
621+
622+
request = ModelRequest(
623+
parts=[UserPromptPart('test prompt')],
624+
instructions='test instructions',
625+
function_tools=[tool_def],
626+
builtin_tools=[ImageGenerationTool()],
627+
)
628+
629+
# Verify the fields are accessible
630+
assert request.function_tools == [tool_def]
631+
assert request.builtin_tools == [ImageGenerationTool()]
632+
633+
# Serialize - fields ARE excluded
634+
serialized = ModelMessagesTypeAdapter.dump_python([request], mode='json')
635+
assert 'function_tools' not in serialized[0]
636+
assert 'builtin_tools' not in serialized[0]

0 commit comments

Comments
 (0)