pydantic-ai-slim 0.8.1__py3-none-any.whl → 1.0.0b1__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.
Potentially problematic release.
This version of pydantic-ai-slim might be problematic. Click here for more details.
- pydantic_ai/__init__.py +28 -2
- pydantic_ai/_agent_graph.py +310 -140
- pydantic_ai/_function_schema.py +5 -5
- pydantic_ai/_griffe.py +2 -1
- pydantic_ai/_otel_messages.py +2 -2
- pydantic_ai/_output.py +31 -35
- pydantic_ai/_parts_manager.py +4 -4
- pydantic_ai/_run_context.py +3 -1
- pydantic_ai/_system_prompt.py +2 -2
- pydantic_ai/_tool_manager.py +3 -22
- pydantic_ai/_utils.py +14 -26
- pydantic_ai/ag_ui.py +7 -8
- pydantic_ai/agent/__init__.py +70 -9
- pydantic_ai/agent/abstract.py +35 -4
- pydantic_ai/agent/wrapper.py +6 -0
- pydantic_ai/builtin_tools.py +2 -2
- pydantic_ai/common_tools/duckduckgo.py +4 -2
- pydantic_ai/durable_exec/temporal/__init__.py +4 -2
- pydantic_ai/durable_exec/temporal/_agent.py +23 -2
- pydantic_ai/durable_exec/temporal/_function_toolset.py +53 -6
- pydantic_ai/durable_exec/temporal/_logfire.py +1 -1
- pydantic_ai/durable_exec/temporal/_mcp_server.py +2 -1
- pydantic_ai/durable_exec/temporal/_model.py +2 -2
- pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
- pydantic_ai/durable_exec/temporal/_toolset.py +2 -1
- pydantic_ai/exceptions.py +45 -2
- pydantic_ai/format_prompt.py +2 -2
- pydantic_ai/mcp.py +2 -2
- pydantic_ai/messages.py +73 -25
- pydantic_ai/models/__init__.py +5 -4
- pydantic_ai/models/anthropic.py +5 -5
- pydantic_ai/models/bedrock.py +58 -56
- pydantic_ai/models/cohere.py +3 -3
- pydantic_ai/models/fallback.py +2 -2
- pydantic_ai/models/function.py +25 -23
- pydantic_ai/models/gemini.py +9 -12
- pydantic_ai/models/google.py +3 -3
- pydantic_ai/models/groq.py +4 -4
- pydantic_ai/models/huggingface.py +4 -4
- pydantic_ai/models/instrumented.py +30 -16
- pydantic_ai/models/mcp_sampling.py +3 -1
- pydantic_ai/models/mistral.py +6 -6
- pydantic_ai/models/openai.py +18 -27
- pydantic_ai/models/test.py +24 -4
- pydantic_ai/output.py +27 -32
- pydantic_ai/profiles/__init__.py +3 -3
- pydantic_ai/profiles/groq.py +1 -1
- pydantic_ai/profiles/openai.py +25 -4
- pydantic_ai/providers/anthropic.py +2 -3
- pydantic_ai/providers/bedrock.py +3 -2
- pydantic_ai/result.py +144 -41
- pydantic_ai/retries.py +10 -29
- pydantic_ai/run.py +12 -5
- pydantic_ai/tools.py +126 -22
- pydantic_ai/toolsets/__init__.py +4 -1
- pydantic_ai/toolsets/_dynamic.py +4 -4
- pydantic_ai/toolsets/abstract.py +18 -2
- pydantic_ai/toolsets/approval_required.py +32 -0
- pydantic_ai/toolsets/combined.py +7 -12
- pydantic_ai/toolsets/{deferred.py → external.py} +11 -5
- pydantic_ai/toolsets/filtered.py +1 -1
- pydantic_ai/toolsets/function.py +13 -4
- pydantic_ai/toolsets/wrapper.py +2 -1
- pydantic_ai/usage.py +7 -5
- {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/METADATA +5 -6
- pydantic_ai_slim-1.0.0b1.dist-info/RECORD +120 -0
- pydantic_ai_slim-0.8.1.dist-info/RECORD +0 -119
- {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/retries.py
CHANGED
|
@@ -24,17 +24,17 @@ from httpx import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
try:
|
|
27
|
-
from tenacity import
|
|
27
|
+
from tenacity import RetryCallState, RetryError, retry, wait_exponential
|
|
28
28
|
except ImportError as _import_error:
|
|
29
29
|
raise ImportError(
|
|
30
30
|
'Please install `tenacity` to use the retries utilities, '
|
|
31
31
|
'you can use the `retries` optional group — `pip install "pydantic-ai-slim[retries]"`'
|
|
32
32
|
) from _import_error
|
|
33
33
|
|
|
34
|
-
from collections.abc import Awaitable
|
|
34
|
+
from collections.abc import Awaitable, Callable
|
|
35
35
|
from datetime import datetime, timezone
|
|
36
36
|
from email.utils import parsedate_to_datetime
|
|
37
|
-
from typing import TYPE_CHECKING, Any,
|
|
37
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
38
38
|
|
|
39
39
|
from typing_extensions import TypedDict
|
|
40
40
|
|
|
@@ -134,8 +134,9 @@ class TenacityTransport(BaseTransport):
|
|
|
134
134
|
|
|
135
135
|
Example:
|
|
136
136
|
```python
|
|
137
|
-
from httpx import Client,
|
|
138
|
-
from tenacity import
|
|
137
|
+
from httpx import Client, HTTPStatusError, HTTPTransport
|
|
138
|
+
from tenacity import retry_if_exception_type, stop_after_attempt
|
|
139
|
+
|
|
139
140
|
from pydantic_ai.retries import RetryConfig, TenacityTransport, wait_retry_after
|
|
140
141
|
|
|
141
142
|
transport = TenacityTransport(
|
|
@@ -157,18 +158,7 @@ class TenacityTransport(BaseTransport):
|
|
|
157
158
|
config: RetryConfig,
|
|
158
159
|
wrapped: BaseTransport | None = None,
|
|
159
160
|
validate_response: Callable[[Response], Any] | None = None,
|
|
160
|
-
**kwargs: NoReturn,
|
|
161
161
|
):
|
|
162
|
-
# TODO: Remove the following checks (and **kwargs) during v1 release
|
|
163
|
-
if 'controller' in kwargs: # pragma: no cover
|
|
164
|
-
raise TypeError('The `controller` argument has been renamed to `config`, and now requires a `RetryConfig`.')
|
|
165
|
-
if kwargs: # pragma: no cover
|
|
166
|
-
raise TypeError(f'Unexpected keyword arguments: {", ".join(kwargs)}')
|
|
167
|
-
if isinstance(config, Retrying): # pragma: no cover
|
|
168
|
-
raise ValueError(
|
|
169
|
-
'Passing a Retrying instance is no longer supported; the `config` argument must be a `pydantic_ai.retries.RetryConfig`.'
|
|
170
|
-
)
|
|
171
|
-
|
|
172
162
|
self.config = config
|
|
173
163
|
self.wrapped = wrapped or HTTPTransport()
|
|
174
164
|
self.validate_response = validate_response
|
|
@@ -224,7 +214,8 @@ class AsyncTenacityTransport(AsyncBaseTransport):
|
|
|
224
214
|
Example:
|
|
225
215
|
```python
|
|
226
216
|
from httpx import AsyncClient, HTTPStatusError
|
|
227
|
-
from tenacity import
|
|
217
|
+
from tenacity import retry_if_exception_type, stop_after_attempt
|
|
218
|
+
|
|
228
219
|
from pydantic_ai.retries import AsyncTenacityTransport, RetryConfig, wait_retry_after
|
|
229
220
|
|
|
230
221
|
transport = AsyncTenacityTransport(
|
|
@@ -245,18 +236,7 @@ class AsyncTenacityTransport(AsyncBaseTransport):
|
|
|
245
236
|
config: RetryConfig,
|
|
246
237
|
wrapped: AsyncBaseTransport | None = None,
|
|
247
238
|
validate_response: Callable[[Response], Any] | None = None,
|
|
248
|
-
**kwargs: NoReturn,
|
|
249
239
|
):
|
|
250
|
-
# TODO: Remove the following checks (and **kwargs) during v1 release
|
|
251
|
-
if 'controller' in kwargs: # pragma: no cover
|
|
252
|
-
raise TypeError('The `controller` argument has been renamed to `config`, and now requires a `RetryConfig`.')
|
|
253
|
-
if kwargs: # pragma: no cover
|
|
254
|
-
raise TypeError(f'Unexpected keyword arguments: {", ".join(kwargs)}')
|
|
255
|
-
if isinstance(config, AsyncRetrying): # pragma: no cover
|
|
256
|
-
raise ValueError(
|
|
257
|
-
'Passing an AsyncRetrying instance is no longer supported; the `config` argument must be a `pydantic_ai.retries.RetryConfig`.'
|
|
258
|
-
)
|
|
259
|
-
|
|
260
240
|
self.config = config
|
|
261
241
|
self.wrapped = wrapped or AsyncHTTPTransport()
|
|
262
242
|
self.validate_response = validate_response
|
|
@@ -314,7 +294,8 @@ def wait_retry_after(
|
|
|
314
294
|
Example:
|
|
315
295
|
```python
|
|
316
296
|
from httpx import AsyncClient, HTTPStatusError
|
|
317
|
-
from tenacity import
|
|
297
|
+
from tenacity import retry_if_exception_type, stop_after_attempt
|
|
298
|
+
|
|
318
299
|
from pydantic_ai.retries import AsyncTenacityTransport, RetryConfig, wait_retry_after
|
|
319
300
|
|
|
320
301
|
transport = AsyncTenacityTransport(
|
pydantic_ai/run.py
CHANGED
|
@@ -3,9 +3,8 @@ from __future__ import annotations as _annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
5
|
from copy import deepcopy
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from typing_extensions import Literal
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
|
9
8
|
|
|
10
9
|
from pydantic_graph import End, GraphRun, GraphRunContext
|
|
11
10
|
|
|
@@ -16,9 +15,11 @@ from . import (
|
|
|
16
15
|
usage as _usage,
|
|
17
16
|
)
|
|
18
17
|
from .output import OutputDataT
|
|
19
|
-
from .result import FinalResult
|
|
20
18
|
from .tools import AgentDepsT
|
|
21
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .result import FinalResult
|
|
22
|
+
|
|
22
23
|
|
|
23
24
|
@dataclasses.dataclass(repr=False)
|
|
24
25
|
class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
@@ -100,7 +101,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
|
100
101
|
def ctx(self) -> GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any]]:
|
|
101
102
|
"""The current context of the agent run."""
|
|
102
103
|
return GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any]](
|
|
103
|
-
self._graph_run.state, self._graph_run.deps
|
|
104
|
+
state=self._graph_run.state, deps=self._graph_run.deps
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
@property
|
|
@@ -348,3 +349,9 @@ class AgentRunResult(Generic[OutputDataT]):
|
|
|
348
349
|
def usage(self) -> _usage.RunUsage:
|
|
349
350
|
"""Return the usage of the whole run."""
|
|
350
351
|
return self._state.usage
|
|
352
|
+
|
|
353
|
+
def timestamp(self) -> datetime:
|
|
354
|
+
"""Return the timestamp of last response."""
|
|
355
|
+
model_response = self.all_messages()[-1]
|
|
356
|
+
assert isinstance(model_response, _messages.ModelResponse)
|
|
357
|
+
return model_response.timestamp
|
pydantic_ai/tools.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Awaitable, Sequence
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Any,
|
|
3
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
4
|
+
from dataclasses import KW_ONLY, dataclass, field, replace
|
|
5
|
+
from typing import Annotated, Any, Concatenate, Generic, Literal, TypeAlias, cast
|
|
6
6
|
|
|
7
|
+
from pydantic import Discriminator, Tag
|
|
7
8
|
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
|
|
8
9
|
from pydantic_core import SchemaValidator, core_schema
|
|
9
|
-
from typing_extensions import
|
|
10
|
+
from typing_extensions import ParamSpec, Self, TypeVar
|
|
10
11
|
|
|
11
12
|
from . import _function_schema, _utils
|
|
12
13
|
from ._run_context import AgentDepsT, RunContext
|
|
14
|
+
from .exceptions import ModelRetry
|
|
15
|
+
from .messages import RetryPromptPart, ToolCallPart, ToolReturn
|
|
13
16
|
|
|
14
17
|
__all__ = (
|
|
15
18
|
'AgentDepsT',
|
|
@@ -25,18 +28,22 @@ __all__ = (
|
|
|
25
28
|
'Tool',
|
|
26
29
|
'ObjectJsonSchema',
|
|
27
30
|
'ToolDefinition',
|
|
31
|
+
'DeferredToolRequests',
|
|
32
|
+
'DeferredToolResults',
|
|
33
|
+
'ToolApproved',
|
|
34
|
+
'ToolDenied',
|
|
28
35
|
)
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
ToolParams = ParamSpec('ToolParams', default=...)
|
|
32
39
|
"""Retrieval function param spec."""
|
|
33
40
|
|
|
34
|
-
SystemPromptFunc: TypeAlias =
|
|
35
|
-
Callable[[RunContext[AgentDepsT]], str]
|
|
36
|
-
Callable[[RunContext[AgentDepsT]], Awaitable[str]]
|
|
37
|
-
Callable[[], str]
|
|
38
|
-
Callable[[], Awaitable[str]]
|
|
39
|
-
|
|
41
|
+
SystemPromptFunc: TypeAlias = (
|
|
42
|
+
Callable[[RunContext[AgentDepsT]], str]
|
|
43
|
+
| Callable[[RunContext[AgentDepsT]], Awaitable[str]]
|
|
44
|
+
| Callable[[], str]
|
|
45
|
+
| Callable[[], Awaitable[str]]
|
|
46
|
+
)
|
|
40
47
|
"""A function that may or maybe not take `RunContext` as an argument, and may or may not be async.
|
|
41
48
|
|
|
42
49
|
Usage `SystemPromptFunc[AgentDepsT]`.
|
|
@@ -52,7 +59,7 @@ ToolFuncPlain: TypeAlias = Callable[ToolParams, Any]
|
|
|
52
59
|
|
|
53
60
|
Usage `ToolPlainFunc[ToolParams]`.
|
|
54
61
|
"""
|
|
55
|
-
ToolFuncEither: TypeAlias =
|
|
62
|
+
ToolFuncEither: TypeAlias = ToolFuncContext[AgentDepsT, ToolParams] | ToolFuncPlain[ToolParams]
|
|
56
63
|
"""Either kind of tool function.
|
|
57
64
|
|
|
58
65
|
This is just a union of [`ToolFuncContext`][pydantic_ai.tools.ToolFuncContext] and
|
|
@@ -68,14 +75,12 @@ See [tool docs](../tools.md#tool-prepare) for more information.
|
|
|
68
75
|
Example — here `only_if_42` is valid as a `ToolPrepareFunc`:
|
|
69
76
|
|
|
70
77
|
```python {noqa="I001"}
|
|
71
|
-
from typing import Union
|
|
72
|
-
|
|
73
78
|
from pydantic_ai import RunContext, Tool
|
|
74
79
|
from pydantic_ai.tools import ToolDefinition
|
|
75
80
|
|
|
76
81
|
async def only_if_42(
|
|
77
82
|
ctx: RunContext[int], tool_def: ToolDefinition
|
|
78
|
-
) ->
|
|
83
|
+
) -> ToolDefinition | None:
|
|
79
84
|
if ctx.deps == 42:
|
|
80
85
|
return tool_def
|
|
81
86
|
|
|
@@ -99,7 +104,6 @@ Example — here `turn_on_strict_if_openai` is valid as a `ToolsPrepareFunc`:
|
|
|
99
104
|
|
|
100
105
|
```python {noqa="I001"}
|
|
101
106
|
from dataclasses import replace
|
|
102
|
-
from typing import Union
|
|
103
107
|
|
|
104
108
|
from pydantic_ai import Agent, RunContext
|
|
105
109
|
from pydantic_ai.tools import ToolDefinition
|
|
@@ -107,7 +111,7 @@ from pydantic_ai.tools import ToolDefinition
|
|
|
107
111
|
|
|
108
112
|
async def turn_on_strict_if_openai(
|
|
109
113
|
ctx: RunContext[None], tool_defs: list[ToolDefinition]
|
|
110
|
-
) ->
|
|
114
|
+
) -> list[ToolDefinition] | None:
|
|
111
115
|
if ctx.model.system == 'openai':
|
|
112
116
|
return [replace(tool_def, strict=True) for tool_def in tool_defs]
|
|
113
117
|
return tool_defs
|
|
@@ -127,6 +131,88 @@ DocstringFormat: TypeAlias = Literal['google', 'numpy', 'sphinx', 'auto']
|
|
|
127
131
|
* `'auto'` — Automatically infer the format based on the structure of the docstring.
|
|
128
132
|
"""
|
|
129
133
|
|
|
134
|
+
|
|
135
|
+
@dataclass(kw_only=True)
|
|
136
|
+
class DeferredToolRequests:
|
|
137
|
+
"""Tool calls that require approval or external execution.
|
|
138
|
+
|
|
139
|
+
This can be used as an agent's `output_type` and will be used as the output of the agent run if the model called any deferred tools.
|
|
140
|
+
|
|
141
|
+
Results can be passed to the next agent run using a [`DeferredToolResults`][pydantic_ai.tools.DeferredToolResults] object with the same tool call IDs.
|
|
142
|
+
|
|
143
|
+
See [deferred tools docs](../tools.md#deferred-tools) for more information.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
calls: list[ToolCallPart] = field(default_factory=list)
|
|
147
|
+
"""Tool calls that require external execution."""
|
|
148
|
+
approvals: list[ToolCallPart] = field(default_factory=list)
|
|
149
|
+
"""Tool calls that require human-in-the-loop approval."""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(kw_only=True)
|
|
153
|
+
class ToolApproved:
|
|
154
|
+
"""Indicates that a tool call has been approved and that the tool function should be executed."""
|
|
155
|
+
|
|
156
|
+
override_args: dict[str, Any] | None = None
|
|
157
|
+
"""Optional tool call arguments to use instead of the original arguments."""
|
|
158
|
+
|
|
159
|
+
kind: Literal['tool-approved'] = 'tool-approved'
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class ToolDenied:
|
|
164
|
+
"""Indicates that a tool call has been denied and that a denial message should be returned to the model."""
|
|
165
|
+
|
|
166
|
+
message: str = 'The tool call was denied.'
|
|
167
|
+
"""The message to return to the model."""
|
|
168
|
+
|
|
169
|
+
_: KW_ONLY
|
|
170
|
+
|
|
171
|
+
kind: Literal['tool-denied'] = 'tool-denied'
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _deferred_tool_call_result_discriminator(x: Any) -> str | None:
|
|
175
|
+
if isinstance(x, dict):
|
|
176
|
+
if 'kind' in x:
|
|
177
|
+
return cast(str, x['kind'])
|
|
178
|
+
elif 'part_kind' in x:
|
|
179
|
+
return cast(str, x['part_kind'])
|
|
180
|
+
else:
|
|
181
|
+
if hasattr(x, 'kind'):
|
|
182
|
+
return cast(str, x.kind)
|
|
183
|
+
elif hasattr(x, 'part_kind'):
|
|
184
|
+
return cast(str, x.part_kind)
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
DeferredToolApprovalResult: TypeAlias = Annotated[ToolApproved | ToolDenied, Discriminator('kind')]
|
|
189
|
+
"""Result for a tool call that required human-in-the-loop approval."""
|
|
190
|
+
DeferredToolCallResult: TypeAlias = Annotated[
|
|
191
|
+
Annotated[ToolReturn, Tag('tool-return')]
|
|
192
|
+
| Annotated[ModelRetry, Tag('model-retry')]
|
|
193
|
+
| Annotated[RetryPromptPart, Tag('retry-prompt')],
|
|
194
|
+
Discriminator(_deferred_tool_call_result_discriminator),
|
|
195
|
+
]
|
|
196
|
+
"""Result for a tool call that required external execution."""
|
|
197
|
+
DeferredToolResult = DeferredToolApprovalResult | DeferredToolCallResult
|
|
198
|
+
"""Result for a tool call that required approval or external execution."""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(kw_only=True)
|
|
202
|
+
class DeferredToolResults:
|
|
203
|
+
"""Results for deferred tool calls from a previous run that required approval or external execution.
|
|
204
|
+
|
|
205
|
+
The tool call IDs need to match those from the [`DeferredToolRequests`][pydantic_ai.output.DeferredToolRequests] output object from the previous run.
|
|
206
|
+
|
|
207
|
+
See [deferred tools docs](../tools.md#deferred-tools) for more information.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
calls: dict[str, DeferredToolCallResult | Any] = field(default_factory=dict)
|
|
211
|
+
"""Map of tool call IDs to results for tool calls that required external execution."""
|
|
212
|
+
approvals: dict[str, bool | DeferredToolApprovalResult] = field(default_factory=dict)
|
|
213
|
+
"""Map of tool call IDs to results for tool calls that required human-in-the-loop approval."""
|
|
214
|
+
|
|
215
|
+
|
|
130
216
|
A = TypeVar('A')
|
|
131
217
|
|
|
132
218
|
|
|
@@ -167,6 +253,7 @@ class Tool(Generic[AgentDepsT]):
|
|
|
167
253
|
docstring_format: DocstringFormat
|
|
168
254
|
require_parameter_descriptions: bool
|
|
169
255
|
strict: bool | None
|
|
256
|
+
requires_approval: bool
|
|
170
257
|
function_schema: _function_schema.FunctionSchema
|
|
171
258
|
"""
|
|
172
259
|
The base JSON schema for the tool's parameters.
|
|
@@ -187,6 +274,7 @@ class Tool(Generic[AgentDepsT]):
|
|
|
187
274
|
require_parameter_descriptions: bool = False,
|
|
188
275
|
schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
|
|
189
276
|
strict: bool | None = None,
|
|
277
|
+
requires_approval: bool = False,
|
|
190
278
|
function_schema: _function_schema.FunctionSchema | None = None,
|
|
191
279
|
):
|
|
192
280
|
"""Create a new tool instance.
|
|
@@ -205,7 +293,6 @@ class Tool(Generic[AgentDepsT]):
|
|
|
205
293
|
or with a custom prepare method:
|
|
206
294
|
|
|
207
295
|
```python {noqa="I001"}
|
|
208
|
-
from typing import Union
|
|
209
296
|
|
|
210
297
|
from pydantic_ai import Agent, RunContext, Tool
|
|
211
298
|
from pydantic_ai.tools import ToolDefinition
|
|
@@ -215,7 +302,7 @@ class Tool(Generic[AgentDepsT]):
|
|
|
215
302
|
|
|
216
303
|
async def prep_my_tool(
|
|
217
304
|
ctx: RunContext[int], tool_def: ToolDefinition
|
|
218
|
-
) ->
|
|
305
|
+
) -> ToolDefinition | None:
|
|
219
306
|
# only register the tool if `deps == 42`
|
|
220
307
|
if ctx.deps == 42:
|
|
221
308
|
return tool_def
|
|
@@ -240,6 +327,8 @@ class Tool(Generic[AgentDepsT]):
|
|
|
240
327
|
schema_generator: The JSON schema generator class to use. Defaults to `GenerateToolJsonSchema`.
|
|
241
328
|
strict: Whether to enforce JSON schema compliance (only affects OpenAI).
|
|
242
329
|
See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
|
|
330
|
+
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
|
|
331
|
+
See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
|
|
243
332
|
function_schema: The function schema to use for the tool. If not provided, it will be generated.
|
|
244
333
|
"""
|
|
245
334
|
self.function = function
|
|
@@ -258,6 +347,7 @@ class Tool(Generic[AgentDepsT]):
|
|
|
258
347
|
self.docstring_format = docstring_format
|
|
259
348
|
self.require_parameter_descriptions = require_parameter_descriptions
|
|
260
349
|
self.strict = strict
|
|
350
|
+
self.requires_approval = requires_approval
|
|
261
351
|
|
|
262
352
|
@classmethod
|
|
263
353
|
def from_schema(
|
|
@@ -320,6 +410,10 @@ class Tool(Generic[AgentDepsT]):
|
|
|
320
410
|
return a `ToolDefinition` or `None` if the tools should not be registered for this run.
|
|
321
411
|
"""
|
|
322
412
|
base_tool_def = self.tool_def
|
|
413
|
+
|
|
414
|
+
if self.requires_approval and not ctx.tool_call_approved:
|
|
415
|
+
base_tool_def = replace(base_tool_def, kind='unapproved')
|
|
416
|
+
|
|
323
417
|
if self.prepare is not None:
|
|
324
418
|
return await self.prepare(ctx, base_tool_def)
|
|
325
419
|
else:
|
|
@@ -334,11 +428,11 @@ This type is used to define tools parameters (aka arguments) in [ToolDefinition]
|
|
|
334
428
|
With PEP-728 this should be a TypedDict with `type: Literal['object']`, and `extra_parts=Any`
|
|
335
429
|
"""
|
|
336
430
|
|
|
337
|
-
ToolKind: TypeAlias = Literal['function', 'output', '
|
|
431
|
+
ToolKind: TypeAlias = Literal['function', 'output', 'external', 'unapproved']
|
|
338
432
|
"""Kind of tool."""
|
|
339
433
|
|
|
340
434
|
|
|
341
|
-
@dataclass(repr=False)
|
|
435
|
+
@dataclass(repr=False, kw_only=True)
|
|
342
436
|
class ToolDefinition:
|
|
343
437
|
"""Definition of a tool passed to a model.
|
|
344
438
|
|
|
@@ -377,8 +471,18 @@ class ToolDefinition:
|
|
|
377
471
|
|
|
378
472
|
- `'function'`: a tool that will be executed by Pydantic AI during an agent run and has its result returned to the model
|
|
379
473
|
- `'output'`: a tool that passes through an output value that ends the run
|
|
380
|
-
- `'
|
|
381
|
-
|
|
474
|
+
- `'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.
|
|
475
|
+
See the [tools documentation](../tools.md#deferred-tools) for more info.
|
|
476
|
+
- `'unapproved'`: a tool that requires human-in-the-loop approval.
|
|
477
|
+
See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
|
|
382
478
|
"""
|
|
383
479
|
|
|
480
|
+
@property
|
|
481
|
+
def defer(self) -> bool:
|
|
482
|
+
"""Whether calls to this tool will be deferred.
|
|
483
|
+
|
|
484
|
+
See the [tools documentation](../tools.md#deferred-tools) for more info.
|
|
485
|
+
"""
|
|
486
|
+
return self.kind in ('external', 'unapproved')
|
|
487
|
+
|
|
384
488
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
pydantic_ai/toolsets/__init__.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from ._dynamic import ToolsetFunc
|
|
2
2
|
from .abstract import AbstractToolset, ToolsetTool
|
|
3
|
+
from .approval_required import ApprovalRequiredToolset
|
|
3
4
|
from .combined import CombinedToolset
|
|
4
|
-
from .
|
|
5
|
+
from .external import DeferredToolset, ExternalToolset # pyright: ignore[reportDeprecated]
|
|
5
6
|
from .filtered import FilteredToolset
|
|
6
7
|
from .function import FunctionToolset
|
|
7
8
|
from .prefixed import PrefixedToolset
|
|
@@ -14,6 +15,7 @@ __all__ = (
|
|
|
14
15
|
'ToolsetFunc',
|
|
15
16
|
'ToolsetTool',
|
|
16
17
|
'CombinedToolset',
|
|
18
|
+
'ExternalToolset',
|
|
17
19
|
'DeferredToolset',
|
|
18
20
|
'FilteredToolset',
|
|
19
21
|
'FunctionToolset',
|
|
@@ -21,4 +23,5 @@ __all__ = (
|
|
|
21
23
|
'RenamedToolset',
|
|
22
24
|
'PreparedToolset',
|
|
23
25
|
'WrapperToolset',
|
|
26
|
+
'ApprovalRequiredToolset',
|
|
24
27
|
)
|
pydantic_ai/toolsets/_dynamic.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from collections.abc import Awaitable
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
5
|
from dataclasses import dataclass, replace
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, TypeAlias
|
|
7
7
|
|
|
8
|
-
from typing_extensions import Self
|
|
8
|
+
from typing_extensions import Self
|
|
9
9
|
|
|
10
10
|
from .._run_context import AgentDepsT, RunContext
|
|
11
11
|
from .abstract import AbstractToolset, ToolsetTool
|
|
12
12
|
|
|
13
13
|
ToolsetFunc: TypeAlias = Callable[
|
|
14
14
|
[RunContext[AgentDepsT]],
|
|
15
|
-
|
|
15
|
+
AbstractToolset[AgentDepsT] | None | Awaitable[AbstractToolset[AgentDepsT] | None],
|
|
16
16
|
]
|
|
17
17
|
"""A sync/async function which takes a run context and returns a toolset."""
|
|
18
18
|
|
pydantic_ai/toolsets/abstract.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from dataclasses import dataclass
|
|
5
|
-
from typing import TYPE_CHECKING, Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, Protocol
|
|
6
7
|
|
|
7
8
|
from pydantic_core import SchemaValidator
|
|
8
9
|
from typing_extensions import Self
|
|
@@ -11,6 +12,7 @@ from .._run_context import AgentDepsT, RunContext
|
|
|
11
12
|
from ..tools import ToolDefinition, ToolsPrepareFunc
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
15
|
+
from .approval_required import ApprovalRequiredToolset
|
|
14
16
|
from .filtered import FilteredToolset
|
|
15
17
|
from .prefixed import PrefixedToolset
|
|
16
18
|
from .prepared import PreparedToolset
|
|
@@ -33,7 +35,7 @@ class SchemaValidatorProt(Protocol):
|
|
|
33
35
|
) -> Any: ...
|
|
34
36
|
|
|
35
37
|
|
|
36
|
-
@dataclass
|
|
38
|
+
@dataclass(kw_only=True)
|
|
37
39
|
class ToolsetTool(Generic[AgentDepsT]):
|
|
38
40
|
"""Definition of a tool available on a toolset.
|
|
39
41
|
|
|
@@ -173,3 +175,17 @@ class AbstractToolset(ABC, Generic[AgentDepsT]):
|
|
|
173
175
|
from .renamed import RenamedToolset
|
|
174
176
|
|
|
175
177
|
return RenamedToolset(self, name_map)
|
|
178
|
+
|
|
179
|
+
def approval_required(
|
|
180
|
+
self,
|
|
181
|
+
approval_required_func: Callable[[RunContext[AgentDepsT], ToolDefinition, dict[str, Any]], bool] = (
|
|
182
|
+
lambda ctx, tool_def, tool_args: True
|
|
183
|
+
),
|
|
184
|
+
) -> ApprovalRequiredToolset[AgentDepsT]:
|
|
185
|
+
"""Returns a new toolset that requires (some) calls to tools it contains to be approved.
|
|
186
|
+
|
|
187
|
+
See [toolset docs](../toolsets.md#requiring-tool-approval) for more information.
|
|
188
|
+
"""
|
|
189
|
+
from .approval_required import ApprovalRequiredToolset
|
|
190
|
+
|
|
191
|
+
return ApprovalRequiredToolset(self, approval_required_func)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic_ai.exceptions import ApprovalRequired
|
|
8
|
+
|
|
9
|
+
from .._run_context import AgentDepsT, RunContext
|
|
10
|
+
from ..tools import ToolDefinition
|
|
11
|
+
from .abstract import ToolsetTool
|
|
12
|
+
from .wrapper import WrapperToolset
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ApprovalRequiredToolset(WrapperToolset[AgentDepsT]):
|
|
17
|
+
"""A toolset that requires (some) calls to tools it contains to be approved.
|
|
18
|
+
|
|
19
|
+
See [toolset docs](../toolsets.md#requiring-tool-approval) for more information.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
approval_required_func: Callable[[RunContext[AgentDepsT], ToolDefinition, dict[str, Any]], bool] = (
|
|
23
|
+
lambda ctx, tool_def, tool_args: True
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def call_tool(
|
|
27
|
+
self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
|
|
28
|
+
) -> Any:
|
|
29
|
+
if not ctx.tool_call_approved and self.approval_required_func(ctx, tool.tool_def, tool_args):
|
|
30
|
+
raise ApprovalRequired
|
|
31
|
+
|
|
32
|
+
return await super().call_tool(name, tool_args, ctx, tool)
|
pydantic_ai/toolsets/combined.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
from collections.abc import Sequence
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
5
|
from contextlib import AsyncExitStack
|
|
6
6
|
from dataclasses import dataclass, field, replace
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
|
+
import anyio
|
|
9
10
|
from typing_extensions import Self
|
|
10
11
|
|
|
11
12
|
from .._run_context import AgentDepsT, RunContext
|
|
12
|
-
from .._utils import get_async_lock
|
|
13
13
|
from ..exceptions import UserError
|
|
14
14
|
from .abstract import AbstractToolset, ToolsetTool
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
@dataclass
|
|
17
|
+
@dataclass(kw_only=True)
|
|
18
18
|
class _CombinedToolsetTool(ToolsetTool[AgentDepsT]):
|
|
19
19
|
"""A tool definition for a combined toolset tools that keeps track of the source toolset and tool."""
|
|
20
20
|
|
|
@@ -31,14 +31,9 @@ class CombinedToolset(AbstractToolset[AgentDepsT]):
|
|
|
31
31
|
|
|
32
32
|
toolsets: Sequence[AbstractToolset[AgentDepsT]]
|
|
33
33
|
|
|
34
|
-
_enter_lock:
|
|
35
|
-
_entered_count: int = field(init=False)
|
|
36
|
-
_exit_stack: AsyncExitStack | None = field(init=False)
|
|
37
|
-
|
|
38
|
-
def __post_init__(self):
|
|
39
|
-
self._enter_lock = get_async_lock()
|
|
40
|
-
self._entered_count = 0
|
|
41
|
-
self._exit_stack = None
|
|
34
|
+
_enter_lock: anyio.Lock = field(compare=False, init=False, default_factory=anyio.Lock)
|
|
35
|
+
_entered_count: int = field(init=False, default=0)
|
|
36
|
+
_exit_stack: AsyncExitStack | None = field(init=False, default=None)
|
|
42
37
|
|
|
43
38
|
@property
|
|
44
39
|
def id(self) -> str | None:
|
|
@@ -4,6 +4,7 @@ from dataclasses import replace
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pydantic_core import SchemaValidator, core_schema
|
|
7
|
+
from typing_extensions import deprecated
|
|
7
8
|
|
|
8
9
|
from .._run_context import AgentDepsT, RunContext
|
|
9
10
|
from ..tools import ToolDefinition
|
|
@@ -12,10 +13,10 @@ from .abstract import AbstractToolset, ToolsetTool
|
|
|
12
13
|
TOOL_SCHEMA_VALIDATOR = SchemaValidator(schema=core_schema.any_schema())
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class
|
|
16
|
-
"""A toolset that holds
|
|
16
|
+
class ExternalToolset(AbstractToolset[AgentDepsT]):
|
|
17
|
+
"""A toolset that holds tools whose results will be produced outside of the Pydantic AI agent run in which they were called.
|
|
17
18
|
|
|
18
|
-
See [toolset docs](../toolsets.md#
|
|
19
|
+
See [toolset docs](../toolsets.md#external-toolset) for more information.
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
tool_defs: list[ToolDefinition]
|
|
@@ -33,7 +34,7 @@ class DeferredToolset(AbstractToolset[AgentDepsT]):
|
|
|
33
34
|
return {
|
|
34
35
|
tool_def.name: ToolsetTool(
|
|
35
36
|
toolset=self,
|
|
36
|
-
tool_def=replace(tool_def, kind='
|
|
37
|
+
tool_def=replace(tool_def, kind='external'),
|
|
37
38
|
max_retries=0,
|
|
38
39
|
args_validator=TOOL_SCHEMA_VALIDATOR,
|
|
39
40
|
)
|
|
@@ -43,4 +44,9 @@ class DeferredToolset(AbstractToolset[AgentDepsT]):
|
|
|
43
44
|
async def call_tool(
|
|
44
45
|
self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
|
|
45
46
|
) -> Any:
|
|
46
|
-
raise NotImplementedError('
|
|
47
|
+
raise NotImplementedError('External tools cannot be called directly')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@deprecated('`DeferredToolset` is deprecated, use `ExternalToolset` instead')
|
|
51
|
+
class DeferredToolset(ExternalToolset):
|
|
52
|
+
"""Deprecated alias for `ExternalToolset`."""
|
pydantic_ai/toolsets/filtered.py
CHANGED