pydantic-ai-slim 0.0.52__tar.gz → 0.0.53__tar.gz
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_slim-0.0.52 → pydantic_ai_slim-0.0.53}/.gitignore +1 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/PKG-INFO +3 -3
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_agent_graph.py +1 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_cli.py +3 -5
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_utils.py +5 -1
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/agent.py +30 -2
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/__init__.py +9 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/gemini.py +12 -2
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/openai.py +151 -6
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/settings.py +0 -5
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/tools.py +27 -4
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/README.md +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_parts_manager.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_result.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/messages.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/anthropic.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/bedrock.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/cohere.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/function.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/groq.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/instrumented.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/test.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/__init__.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/result.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pydantic_ai/usage.py +0 -0
- {pydantic_ai_slim-0.0.52 → pydantic_ai_slim-0.0.53}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.53
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Author-email: Samuel Colvin <samuel@pydantic.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,7 +29,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
29
29
|
Requires-Dist: griffe>=1.3.2
|
|
30
30
|
Requires-Dist: httpx>=0.27
|
|
31
31
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
32
|
-
Requires-Dist: pydantic-graph==0.0.
|
|
32
|
+
Requires-Dist: pydantic-graph==0.0.53
|
|
33
33
|
Requires-Dist: pydantic>=2.10
|
|
34
34
|
Requires-Dist: typing-inspection>=0.4.0
|
|
35
35
|
Provides-Extra: anthropic
|
|
@@ -45,7 +45,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
45
45
|
Provides-Extra: duckduckgo
|
|
46
46
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
47
47
|
Provides-Extra: evals
|
|
48
|
-
Requires-Dist: pydantic-evals==0.0.
|
|
48
|
+
Requires-Dist: pydantic-evals==0.0.53; extra == 'evals'
|
|
49
49
|
Provides-Extra: groq
|
|
50
50
|
Requires-Dist: groq>=0.15.0; extra == 'groq'
|
|
51
51
|
Provides-Extra: logfire
|
|
@@ -311,6 +311,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
311
311
|
return self._result
|
|
312
312
|
|
|
313
313
|
model_settings, model_request_parameters = await self._prepare_request(ctx)
|
|
314
|
+
model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
|
|
314
315
|
model_response, request_usage = await ctx.deps.model.request(
|
|
315
316
|
ctx.state.message_history, model_settings, model_request_parameters
|
|
316
317
|
)
|
|
@@ -15,7 +15,7 @@ from typing_inspection.introspection import get_literal_values
|
|
|
15
15
|
|
|
16
16
|
from pydantic_ai.agent import Agent
|
|
17
17
|
from pydantic_ai.exceptions import UserError
|
|
18
|
-
from pydantic_ai.messages import ModelMessage
|
|
18
|
+
from pydantic_ai.messages import ModelMessage
|
|
19
19
|
from pydantic_ai.models import KnownModelName, infer_model
|
|
20
20
|
|
|
21
21
|
try:
|
|
@@ -222,10 +222,8 @@ async def ask_agent(
|
|
|
222
222
|
status.stop() # stopping multiple times is idempotent
|
|
223
223
|
stack.enter_context(live) # entering multiple times is idempotent
|
|
224
224
|
|
|
225
|
-
async for
|
|
226
|
-
|
|
227
|
-
content += event.delta.content_delta
|
|
228
|
-
live.update(Markdown(content, code_theme=code_theme))
|
|
225
|
+
async for content in handle_stream.stream_output():
|
|
226
|
+
live.update(Markdown(content, code_theme=code_theme))
|
|
229
227
|
|
|
230
228
|
assert agent_run.result is not None
|
|
231
229
|
return agent_run.result.all_messages()
|
|
@@ -50,7 +50,11 @@ def check_object_json_schema(schema: JsonSchemaValue) -> ObjectJsonSchema:
|
|
|
50
50
|
if schema.get('type') == 'object':
|
|
51
51
|
return schema
|
|
52
52
|
elif schema.get('$ref') is not None:
|
|
53
|
-
|
|
53
|
+
maybe_result = schema.get('$defs', {}).get(schema['$ref'][8:]) # This removes the initial "#/$defs/".
|
|
54
|
+
|
|
55
|
+
if "'$ref': '#/$defs/" in str(maybe_result):
|
|
56
|
+
return schema # We can't remove the $defs because the schema contains other references
|
|
57
|
+
return maybe_result
|
|
54
58
|
else:
|
|
55
59
|
raise UserError('Schema must be an object')
|
|
56
60
|
|
|
@@ -940,6 +940,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
940
940
|
docstring_format: DocstringFormat = 'auto',
|
|
941
941
|
require_parameter_descriptions: bool = False,
|
|
942
942
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
943
|
+
strict: bool | None = None,
|
|
943
944
|
) -> Callable[[ToolFuncContext[AgentDepsT, ToolParams]], ToolFuncContext[AgentDepsT, ToolParams]]: ...
|
|
944
945
|
|
|
945
946
|
def tool(
|
|
@@ -953,6 +954,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
953
954
|
docstring_format: DocstringFormat = 'auto',
|
|
954
955
|
require_parameter_descriptions: bool = False,
|
|
955
956
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
957
|
+
strict: bool | None = None,
|
|
956
958
|
) -> Any:
|
|
957
959
|
"""Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.
|
|
958
960
|
|
|
@@ -995,6 +997,8 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
995
997
|
Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
|
|
996
998
|
require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
|
|
997
999
|
schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
|
|
1000
|
+
strict: Whether to enforce JSON schema compliance (only affects OpenAI).
|
|
1001
|
+
See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
|
|
998
1002
|
"""
|
|
999
1003
|
if func is None:
|
|
1000
1004
|
|
|
@@ -1011,6 +1015,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1011
1015
|
docstring_format,
|
|
1012
1016
|
require_parameter_descriptions,
|
|
1013
1017
|
schema_generator,
|
|
1018
|
+
strict,
|
|
1014
1019
|
)
|
|
1015
1020
|
return func_
|
|
1016
1021
|
|
|
@@ -1018,7 +1023,15 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1018
1023
|
else:
|
|
1019
1024
|
# noinspection PyTypeChecker
|
|
1020
1025
|
self._register_function(
|
|
1021
|
-
func,
|
|
1026
|
+
func,
|
|
1027
|
+
True,
|
|
1028
|
+
name,
|
|
1029
|
+
retries,
|
|
1030
|
+
prepare,
|
|
1031
|
+
docstring_format,
|
|
1032
|
+
require_parameter_descriptions,
|
|
1033
|
+
schema_generator,
|
|
1034
|
+
strict,
|
|
1022
1035
|
)
|
|
1023
1036
|
return func
|
|
1024
1037
|
|
|
@@ -1036,6 +1049,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1036
1049
|
docstring_format: DocstringFormat = 'auto',
|
|
1037
1050
|
require_parameter_descriptions: bool = False,
|
|
1038
1051
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
1052
|
+
strict: bool | None = None,
|
|
1039
1053
|
) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ...
|
|
1040
1054
|
|
|
1041
1055
|
def tool_plain(
|
|
@@ -1049,6 +1063,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1049
1063
|
docstring_format: DocstringFormat = 'auto',
|
|
1050
1064
|
require_parameter_descriptions: bool = False,
|
|
1051
1065
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
1066
|
+
strict: bool | None = None,
|
|
1052
1067
|
) -> Any:
|
|
1053
1068
|
"""Decorator to register a tool function which DOES NOT take `RunContext` as an argument.
|
|
1054
1069
|
|
|
@@ -1091,6 +1106,8 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1091
1106
|
Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
|
|
1092
1107
|
require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
|
|
1093
1108
|
schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
|
|
1109
|
+
strict: Whether to enforce JSON schema compliance (only affects OpenAI).
|
|
1110
|
+
See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
|
|
1094
1111
|
"""
|
|
1095
1112
|
if func is None:
|
|
1096
1113
|
|
|
@@ -1105,13 +1122,22 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1105
1122
|
docstring_format,
|
|
1106
1123
|
require_parameter_descriptions,
|
|
1107
1124
|
schema_generator,
|
|
1125
|
+
strict,
|
|
1108
1126
|
)
|
|
1109
1127
|
return func_
|
|
1110
1128
|
|
|
1111
1129
|
return tool_decorator
|
|
1112
1130
|
else:
|
|
1113
1131
|
self._register_function(
|
|
1114
|
-
func,
|
|
1132
|
+
func,
|
|
1133
|
+
False,
|
|
1134
|
+
name,
|
|
1135
|
+
retries,
|
|
1136
|
+
prepare,
|
|
1137
|
+
docstring_format,
|
|
1138
|
+
require_parameter_descriptions,
|
|
1139
|
+
schema_generator,
|
|
1140
|
+
strict,
|
|
1115
1141
|
)
|
|
1116
1142
|
return func
|
|
1117
1143
|
|
|
@@ -1125,6 +1151,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1125
1151
|
docstring_format: DocstringFormat,
|
|
1126
1152
|
require_parameter_descriptions: bool,
|
|
1127
1153
|
schema_generator: type[GenerateJsonSchema],
|
|
1154
|
+
strict: bool | None,
|
|
1128
1155
|
) -> None:
|
|
1129
1156
|
"""Private utility to register a function as a tool."""
|
|
1130
1157
|
retries_ = retries if retries is not None else self._default_retries
|
|
@@ -1137,6 +1164,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1137
1164
|
docstring_format=docstring_format,
|
|
1138
1165
|
require_parameter_descriptions=require_parameter_descriptions,
|
|
1139
1166
|
schema_generator=schema_generator,
|
|
1167
|
+
strict=strict,
|
|
1140
1168
|
)
|
|
1141
1169
|
self._register_tool(tool)
|
|
1142
1170
|
|
|
@@ -274,6 +274,15 @@ class Model(ABC):
|
|
|
274
274
|
# noinspection PyUnreachableCode
|
|
275
275
|
yield # pragma: no cover
|
|
276
276
|
|
|
277
|
+
def customize_request_parameters(self, model_request_parameters: ModelRequestParameters) -> ModelRequestParameters:
|
|
278
|
+
"""Customize the request parameters for the model.
|
|
279
|
+
|
|
280
|
+
This method can be overridden by subclasses to modify the request parameters before sending them to the model.
|
|
281
|
+
In particular, this method can be used to make modifications to the generated tool JSON schemas if necessary
|
|
282
|
+
for vendor/model-specific reasons.
|
|
283
|
+
"""
|
|
284
|
+
return model_request_parameters
|
|
285
|
+
|
|
277
286
|
@property
|
|
278
287
|
@abstractmethod
|
|
279
288
|
def model_name(self) -> str:
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from collections.abc import AsyncIterator, Sequence
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
from copy import deepcopy
|
|
8
|
-
from dataclasses import dataclass, field
|
|
8
|
+
from dataclasses import dataclass, field, replace
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Annotated, Any, Literal, Protocol, Union, cast
|
|
11
11
|
from uuid import uuid4
|
|
@@ -152,6 +152,16 @@ class GeminiModel(Model):
|
|
|
152
152
|
) as http_response:
|
|
153
153
|
yield await self._process_streamed_response(http_response)
|
|
154
154
|
|
|
155
|
+
def customize_request_parameters(self, model_request_parameters: ModelRequestParameters) -> ModelRequestParameters:
|
|
156
|
+
def _customize_tool_def(t: ToolDefinition):
|
|
157
|
+
return replace(t, parameters_json_schema=_GeminiJsonSchema(t.parameters_json_schema).simplify())
|
|
158
|
+
|
|
159
|
+
return ModelRequestParameters(
|
|
160
|
+
function_tools=[_customize_tool_def(tool) for tool in model_request_parameters.function_tools],
|
|
161
|
+
allow_text_result=model_request_parameters.allow_text_result,
|
|
162
|
+
result_tools=[_customize_tool_def(tool) for tool in model_request_parameters.result_tools],
|
|
163
|
+
)
|
|
164
|
+
|
|
155
165
|
@property
|
|
156
166
|
def model_name(self) -> GeminiModelName:
|
|
157
167
|
"""The model name."""
|
|
@@ -640,7 +650,7 @@ class _GeminiFunction(TypedDict):
|
|
|
640
650
|
|
|
641
651
|
|
|
642
652
|
def _function_from_abstract_tool(tool: ToolDefinition) -> _GeminiFunction:
|
|
643
|
-
json_schema =
|
|
653
|
+
json_schema = tool.parameters_json_schema
|
|
644
654
|
f = _GeminiFunction(name=tool.name, description=tool.description)
|
|
645
655
|
if json_schema.get('properties'):
|
|
646
656
|
f['parameters'] = json_schema
|
|
@@ -4,9 +4,9 @@ import base64
|
|
|
4
4
|
import warnings
|
|
5
5
|
from collections.abc import AsyncIterable, AsyncIterator, Sequence
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
|
-
from dataclasses import dataclass, field
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
|
-
from typing import Literal, Union, cast, overload
|
|
9
|
+
from typing import Any, Literal, Union, cast, overload
|
|
10
10
|
|
|
11
11
|
from typing_extensions import assert_never
|
|
12
12
|
|
|
@@ -150,7 +150,7 @@ class OpenAIModel(Model):
|
|
|
150
150
|
"""
|
|
151
151
|
|
|
152
152
|
client: AsyncOpenAI = field(repr=False)
|
|
153
|
-
system_prompt_role: OpenAISystemPromptRole | None = field(default=None)
|
|
153
|
+
system_prompt_role: OpenAISystemPromptRole | None = field(default=None, repr=False)
|
|
154
154
|
|
|
155
155
|
_model_name: OpenAIModelName = field(repr=False)
|
|
156
156
|
_system: str = field(default='openai', repr=False)
|
|
@@ -208,6 +208,9 @@ class OpenAIModel(Model):
|
|
|
208
208
|
async with response:
|
|
209
209
|
yield await self._process_streamed_response(response)
|
|
210
210
|
|
|
211
|
+
def customize_request_parameters(self, model_request_parameters: ModelRequestParameters) -> ModelRequestParameters:
|
|
212
|
+
return _customize_request_parameters(model_request_parameters)
|
|
213
|
+
|
|
211
214
|
@property
|
|
212
215
|
def model_name(self) -> OpenAIModelName:
|
|
213
216
|
"""The model name."""
|
|
@@ -351,7 +354,7 @@ class OpenAIModel(Model):
|
|
|
351
354
|
|
|
352
355
|
@staticmethod
|
|
353
356
|
def _map_tool_definition(f: ToolDefinition) -> chat.ChatCompletionToolParam:
|
|
354
|
-
|
|
357
|
+
tool_param: chat.ChatCompletionToolParam = {
|
|
355
358
|
'type': 'function',
|
|
356
359
|
'function': {
|
|
357
360
|
'name': f.name,
|
|
@@ -359,6 +362,9 @@ class OpenAIModel(Model):
|
|
|
359
362
|
'parameters': f.parameters_json_schema,
|
|
360
363
|
},
|
|
361
364
|
}
|
|
365
|
+
if f.strict:
|
|
366
|
+
tool_param['function']['strict'] = f.strict
|
|
367
|
+
return tool_param
|
|
362
368
|
|
|
363
369
|
async def _map_user_message(self, message: ModelRequest) -> AsyncIterable[chat.ChatCompletionMessageParam]:
|
|
364
370
|
for part in message.parts:
|
|
@@ -522,6 +528,9 @@ class OpenAIResponsesModel(Model):
|
|
|
522
528
|
async with response:
|
|
523
529
|
yield await self._process_streamed_response(response)
|
|
524
530
|
|
|
531
|
+
def customize_request_parameters(self, model_request_parameters: ModelRequestParameters) -> ModelRequestParameters:
|
|
532
|
+
return _customize_request_parameters(model_request_parameters)
|
|
533
|
+
|
|
525
534
|
def _process_response(self, response: responses.Response) -> ModelResponse:
|
|
526
535
|
"""Process a non-streamed response, and prepare a message to return."""
|
|
527
536
|
timestamp = datetime.fromtimestamp(response.created_at, tz=timezone.utc)
|
|
@@ -630,8 +639,8 @@ class OpenAIResponsesModel(Model):
|
|
|
630
639
|
'parameters': f.parameters_json_schema,
|
|
631
640
|
'type': 'function',
|
|
632
641
|
'description': f.description,
|
|
633
|
-
#
|
|
634
|
-
'strict': False,
|
|
642
|
+
# NOTE: f.strict should already be a boolean thanks to customize_request_parameters
|
|
643
|
+
'strict': f.strict or False,
|
|
635
644
|
}
|
|
636
645
|
|
|
637
646
|
async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[responses.ResponseInputItemParam]]:
|
|
@@ -907,3 +916,139 @@ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.R
|
|
|
907
916
|
total_tokens=response_usage.total_tokens,
|
|
908
917
|
details=details,
|
|
909
918
|
)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
class _StrictSchemaHelper:
|
|
922
|
+
def make_schema_strict(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
923
|
+
"""Recursively handle the schema to make it compatible with OpenAI strict mode.
|
|
924
|
+
|
|
925
|
+
See https://platform.openai.com/docs/guides/function-calling?api-mode=responses#strict-mode for more details,
|
|
926
|
+
but this basically just requires:
|
|
927
|
+
* `additionalProperties` must be set to false for each object in the parameters
|
|
928
|
+
* all fields in properties must be marked as required
|
|
929
|
+
"""
|
|
930
|
+
assert isinstance(schema, dict), 'Schema must be a dictionary, this is probably a bug'
|
|
931
|
+
|
|
932
|
+
# Create a copy to avoid modifying the original schema
|
|
933
|
+
schema = schema.copy()
|
|
934
|
+
|
|
935
|
+
# Handle $defs
|
|
936
|
+
if defs := schema.get('$defs'):
|
|
937
|
+
schema['$defs'] = {k: self.make_schema_strict(v) for k, v in defs.items()}
|
|
938
|
+
|
|
939
|
+
# Process schema based on its type
|
|
940
|
+
schema_type = schema.get('type')
|
|
941
|
+
if schema_type == 'object':
|
|
942
|
+
# Handle object type by setting additionalProperties to false
|
|
943
|
+
# and adding all properties to required list
|
|
944
|
+
self._make_object_schema_strict(schema)
|
|
945
|
+
elif schema_type == 'array':
|
|
946
|
+
# Handle array types by processing their items
|
|
947
|
+
if 'items' in schema:
|
|
948
|
+
items: Any = schema['items']
|
|
949
|
+
schema['items'] = self.make_schema_strict(items)
|
|
950
|
+
if 'prefixItems' in schema:
|
|
951
|
+
prefix_items: list[Any] = schema['prefixItems']
|
|
952
|
+
schema['prefixItems'] = [self.make_schema_strict(item) for item in prefix_items]
|
|
953
|
+
|
|
954
|
+
elif schema_type in {'string', 'number', 'integer', 'boolean', 'null'}:
|
|
955
|
+
pass # Primitive types need no special handling
|
|
956
|
+
elif 'oneOf' in schema:
|
|
957
|
+
schema['oneOf'] = [self.make_schema_strict(item) for item in schema['oneOf']]
|
|
958
|
+
elif 'anyOf' in schema:
|
|
959
|
+
schema['anyOf'] = [self.make_schema_strict(item) for item in schema['anyOf']]
|
|
960
|
+
|
|
961
|
+
return schema
|
|
962
|
+
|
|
963
|
+
def _make_object_schema_strict(self, schema: dict[str, Any]) -> None:
|
|
964
|
+
schema['additionalProperties'] = False
|
|
965
|
+
|
|
966
|
+
# Handle patternProperties; note this may not be compatible with strict mode but is included for completeness
|
|
967
|
+
if 'patternProperties' in schema and isinstance(schema['patternProperties'], dict):
|
|
968
|
+
pattern_props: dict[str, Any] = schema['patternProperties']
|
|
969
|
+
schema['patternProperties'] = {str(k): self.make_schema_strict(v) for k, v in pattern_props.items()}
|
|
970
|
+
|
|
971
|
+
# Handle properties — update their schemas recursively, and make all properties required
|
|
972
|
+
if 'properties' in schema and isinstance(schema['properties'], dict):
|
|
973
|
+
properties: dict[str, Any] = schema['properties']
|
|
974
|
+
schema['properties'] = {k: self.make_schema_strict(v) for k, v in properties.items()}
|
|
975
|
+
schema['required'] = list(properties.keys())
|
|
976
|
+
|
|
977
|
+
def is_schema_strict(self, schema: dict[str, Any]) -> bool:
|
|
978
|
+
"""Check if the schema is strict-mode-compatible.
|
|
979
|
+
|
|
980
|
+
A schema is compatible if:
|
|
981
|
+
* `additionalProperties` is set to false for each object in the parameters
|
|
982
|
+
* all fields in properties are marked as required
|
|
983
|
+
|
|
984
|
+
See https://platform.openai.com/docs/guides/function-calling?api-mode=responses#strict-mode for more details.
|
|
985
|
+
"""
|
|
986
|
+
assert isinstance(schema, dict), 'Schema must be a dictionary, this is probably a bug'
|
|
987
|
+
|
|
988
|
+
# Note that checking the defs first is usually the fastest way to proceed, but
|
|
989
|
+
# it makes it hard/impossible to hit coverage below, hence all the pragma no covers.
|
|
990
|
+
# I still included the handling below because I'm not _confident_ those code paths can't be hit.
|
|
991
|
+
if defs := schema.get('$defs'):
|
|
992
|
+
if not all(self.is_schema_strict(v) for v in defs.values()): # pragma: no branch
|
|
993
|
+
return False
|
|
994
|
+
|
|
995
|
+
schema_type = schema.get('type')
|
|
996
|
+
if schema_type == 'object':
|
|
997
|
+
if not self._is_object_schema_strict(schema):
|
|
998
|
+
return False
|
|
999
|
+
elif schema_type == 'array':
|
|
1000
|
+
if 'items' in schema:
|
|
1001
|
+
items: Any = schema['items']
|
|
1002
|
+
if not self.is_schema_strict(items): # pragma: no cover
|
|
1003
|
+
return False
|
|
1004
|
+
if 'prefixItems' in schema:
|
|
1005
|
+
prefix_items: list[Any] = schema['prefixItems']
|
|
1006
|
+
if not all(self.is_schema_strict(item) for item in prefix_items): # pragma: no cover
|
|
1007
|
+
return False
|
|
1008
|
+
elif schema_type in {'string', 'number', 'integer', 'boolean', 'null'}:
|
|
1009
|
+
pass
|
|
1010
|
+
elif 'oneOf' in schema: # pragma: no cover
|
|
1011
|
+
if not all(self.is_schema_strict(item) for item in schema['oneOf']):
|
|
1012
|
+
return False
|
|
1013
|
+
|
|
1014
|
+
elif 'anyOf' in schema: # pragma: no cover
|
|
1015
|
+
if not all(self.is_schema_strict(item) for item in schema['anyOf']):
|
|
1016
|
+
return False
|
|
1017
|
+
|
|
1018
|
+
return True
|
|
1019
|
+
|
|
1020
|
+
def _is_object_schema_strict(self, schema: dict[str, Any]) -> bool:
|
|
1021
|
+
"""Check if the schema is an object and has additionalProperties set to false."""
|
|
1022
|
+
if schema.get('additionalProperties') is not False:
|
|
1023
|
+
return False
|
|
1024
|
+
if 'properties' not in schema: # pragma: no cover
|
|
1025
|
+
return False
|
|
1026
|
+
if 'required' not in schema: # pragma: no cover
|
|
1027
|
+
return False
|
|
1028
|
+
|
|
1029
|
+
for k, v in schema['properties'].items():
|
|
1030
|
+
if k not in schema['required']:
|
|
1031
|
+
return False
|
|
1032
|
+
if not self.is_schema_strict(v): # pragma: no cover
|
|
1033
|
+
return False
|
|
1034
|
+
|
|
1035
|
+
return True
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _customize_request_parameters(model_request_parameters: ModelRequestParameters) -> ModelRequestParameters:
|
|
1039
|
+
"""Customize the request parameters for OpenAI models."""
|
|
1040
|
+
|
|
1041
|
+
def _customize_tool_def(t: ToolDefinition):
|
|
1042
|
+
if t.strict is True:
|
|
1043
|
+
parameters_json_schema = _StrictSchemaHelper().make_schema_strict(t.parameters_json_schema)
|
|
1044
|
+
return replace(t, parameters_json_schema=parameters_json_schema)
|
|
1045
|
+
elif t.strict is None:
|
|
1046
|
+
strict = _StrictSchemaHelper().is_schema_strict(t.parameters_json_schema)
|
|
1047
|
+
return replace(t, strict=strict)
|
|
1048
|
+
return t
|
|
1049
|
+
|
|
1050
|
+
return ModelRequestParameters(
|
|
1051
|
+
function_tools=[_customize_tool_def(tool) for tool in model_request_parameters.function_tools],
|
|
1052
|
+
allow_text_result=model_request_parameters.allow_text_result,
|
|
1053
|
+
result_tools=[_customize_tool_def(tool) for tool in model_request_parameters.result_tools],
|
|
1054
|
+
)
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
3
|
from httpx import Timeout
|
|
6
4
|
from typing_extensions import TypedDict
|
|
7
5
|
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
6
|
|
|
12
7
|
class ModelSettings(TypedDict, total=False):
|
|
13
8
|
"""Settings to configure an LLM.
|
|
@@ -173,12 +173,18 @@ class Tool(Generic[AgentDepsT]):
|
|
|
173
173
|
prepare: ToolPrepareFunc[AgentDepsT] | None
|
|
174
174
|
docstring_format: DocstringFormat
|
|
175
175
|
require_parameter_descriptions: bool
|
|
176
|
+
strict: bool | None
|
|
176
177
|
_is_async: bool = field(init=False)
|
|
177
178
|
_single_arg_name: str | None = field(init=False)
|
|
178
179
|
_positional_fields: list[str] = field(init=False)
|
|
179
180
|
_var_positional_field: str | None = field(init=False)
|
|
180
181
|
_validator: SchemaValidator = field(init=False, repr=False)
|
|
181
|
-
|
|
182
|
+
_base_parameters_json_schema: ObjectJsonSchema = field(init=False)
|
|
183
|
+
"""
|
|
184
|
+
The base JSON schema for the tool's parameters.
|
|
185
|
+
|
|
186
|
+
This schema may be modified by the `prepare` function or by the Model class prior to including it in an API request.
|
|
187
|
+
"""
|
|
182
188
|
|
|
183
189
|
# TODO: Move this state off the Tool class, which is otherwise stateless.
|
|
184
190
|
# This should be tracked inside a specific agent run, not the tool.
|
|
@@ -196,6 +202,7 @@ class Tool(Generic[AgentDepsT]):
|
|
|
196
202
|
docstring_format: DocstringFormat = 'auto',
|
|
197
203
|
require_parameter_descriptions: bool = False,
|
|
198
204
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
205
|
+
strict: bool | None = None,
|
|
199
206
|
):
|
|
200
207
|
"""Create a new tool instance.
|
|
201
208
|
|
|
@@ -246,6 +253,8 @@ class Tool(Generic[AgentDepsT]):
|
|
|
246
253
|
Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
|
|
247
254
|
require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
|
|
248
255
|
schema_generator: The JSON schema generator class to use. Defaults to `GenerateToolJsonSchema`.
|
|
256
|
+
strict: Whether to enforce JSON schema compliance (only affects OpenAI).
|
|
257
|
+
See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
|
|
249
258
|
"""
|
|
250
259
|
if takes_ctx is None:
|
|
251
260
|
takes_ctx = _pydantic.takes_ctx(function)
|
|
@@ -261,12 +270,13 @@ class Tool(Generic[AgentDepsT]):
|
|
|
261
270
|
self.prepare = prepare
|
|
262
271
|
self.docstring_format = docstring_format
|
|
263
272
|
self.require_parameter_descriptions = require_parameter_descriptions
|
|
273
|
+
self.strict = strict
|
|
264
274
|
self._is_async = inspect.iscoroutinefunction(self.function)
|
|
265
275
|
self._single_arg_name = f['single_arg_name']
|
|
266
276
|
self._positional_fields = f['positional_fields']
|
|
267
277
|
self._var_positional_field = f['var_positional_field']
|
|
268
278
|
self._validator = f['validator']
|
|
269
|
-
self.
|
|
279
|
+
self._base_parameters_json_schema = f['json_schema']
|
|
270
280
|
|
|
271
281
|
async def prepare_tool_def(self, ctx: RunContext[AgentDepsT]) -> ToolDefinition | None:
|
|
272
282
|
"""Get the tool definition.
|
|
@@ -280,7 +290,8 @@ class Tool(Generic[AgentDepsT]):
|
|
|
280
290
|
tool_def = ToolDefinition(
|
|
281
291
|
name=self.name,
|
|
282
292
|
description=self.description,
|
|
283
|
-
parameters_json_schema=self.
|
|
293
|
+
parameters_json_schema=self._base_parameters_json_schema,
|
|
294
|
+
strict=self.strict,
|
|
284
295
|
)
|
|
285
296
|
if self.prepare is not None:
|
|
286
297
|
return await self.prepare(ctx, tool_def)
|
|
@@ -400,7 +411,7 @@ With PEP-728 this should be a TypedDict with `type: Literal['object']`, and `ext
|
|
|
400
411
|
class ToolDefinition:
|
|
401
412
|
"""Definition of a tool passed to a model.
|
|
402
413
|
|
|
403
|
-
This is used for both function tools result tools.
|
|
414
|
+
This is used for both function tools and result tools.
|
|
404
415
|
"""
|
|
405
416
|
|
|
406
417
|
name: str
|
|
@@ -417,3 +428,15 @@ class ToolDefinition:
|
|
|
417
428
|
|
|
418
429
|
This will only be set for result tools which don't have an `object` JSON schema.
|
|
419
430
|
"""
|
|
431
|
+
|
|
432
|
+
strict: bool | None = None
|
|
433
|
+
"""Whether to enforce (vendor-specific) strict JSON schema validation for tool calls.
|
|
434
|
+
|
|
435
|
+
Setting this to `True` while using a supported model generally imposes some restrictions on the tool's JSON schema
|
|
436
|
+
in exchange for guaranteeing the API responses strictly match that schema.
|
|
437
|
+
|
|
438
|
+
When `False`, the model may be free to generate other properties or types (depending on the vendor).
|
|
439
|
+
When `None` (the default), the value will be inferred based on the compatibility of the parameters_json_schema.
|
|
440
|
+
|
|
441
|
+
Note: this is currently only supported by OpenAI models.
|
|
442
|
+
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|