pydantic-ai-slim 0.8.0__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.

Files changed (70) hide show
  1. pydantic_ai/__init__.py +28 -2
  2. pydantic_ai/_agent_graph.py +310 -140
  3. pydantic_ai/_function_schema.py +5 -5
  4. pydantic_ai/_griffe.py +2 -1
  5. pydantic_ai/_otel_messages.py +2 -2
  6. pydantic_ai/_output.py +31 -35
  7. pydantic_ai/_parts_manager.py +4 -4
  8. pydantic_ai/_run_context.py +3 -1
  9. pydantic_ai/_system_prompt.py +2 -2
  10. pydantic_ai/_tool_manager.py +3 -22
  11. pydantic_ai/_utils.py +14 -26
  12. pydantic_ai/ag_ui.py +7 -8
  13. pydantic_ai/agent/__init__.py +84 -17
  14. pydantic_ai/agent/abstract.py +35 -4
  15. pydantic_ai/agent/wrapper.py +6 -0
  16. pydantic_ai/builtin_tools.py +2 -2
  17. pydantic_ai/common_tools/duckduckgo.py +4 -2
  18. pydantic_ai/durable_exec/temporal/__init__.py +70 -17
  19. pydantic_ai/durable_exec/temporal/_agent.py +23 -2
  20. pydantic_ai/durable_exec/temporal/_function_toolset.py +53 -6
  21. pydantic_ai/durable_exec/temporal/_logfire.py +6 -3
  22. pydantic_ai/durable_exec/temporal/_mcp_server.py +2 -1
  23. pydantic_ai/durable_exec/temporal/_model.py +2 -2
  24. pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
  25. pydantic_ai/durable_exec/temporal/_toolset.py +2 -1
  26. pydantic_ai/exceptions.py +45 -2
  27. pydantic_ai/format_prompt.py +2 -2
  28. pydantic_ai/mcp.py +2 -2
  29. pydantic_ai/messages.py +81 -28
  30. pydantic_ai/models/__init__.py +19 -7
  31. pydantic_ai/models/anthropic.py +6 -6
  32. pydantic_ai/models/bedrock.py +63 -57
  33. pydantic_ai/models/cohere.py +3 -3
  34. pydantic_ai/models/fallback.py +2 -2
  35. pydantic_ai/models/function.py +25 -23
  36. pydantic_ai/models/gemini.py +10 -13
  37. pydantic_ai/models/google.py +4 -4
  38. pydantic_ai/models/groq.py +5 -5
  39. pydantic_ai/models/huggingface.py +5 -5
  40. pydantic_ai/models/instrumented.py +44 -21
  41. pydantic_ai/models/mcp_sampling.py +3 -1
  42. pydantic_ai/models/mistral.py +8 -8
  43. pydantic_ai/models/openai.py +20 -29
  44. pydantic_ai/models/test.py +24 -4
  45. pydantic_ai/output.py +27 -32
  46. pydantic_ai/profiles/__init__.py +3 -3
  47. pydantic_ai/profiles/groq.py +1 -1
  48. pydantic_ai/profiles/openai.py +25 -4
  49. pydantic_ai/providers/anthropic.py +2 -3
  50. pydantic_ai/providers/bedrock.py +3 -2
  51. pydantic_ai/result.py +173 -52
  52. pydantic_ai/retries.py +10 -29
  53. pydantic_ai/run.py +12 -5
  54. pydantic_ai/tools.py +126 -22
  55. pydantic_ai/toolsets/__init__.py +4 -1
  56. pydantic_ai/toolsets/_dynamic.py +4 -4
  57. pydantic_ai/toolsets/abstract.py +18 -2
  58. pydantic_ai/toolsets/approval_required.py +32 -0
  59. pydantic_ai/toolsets/combined.py +7 -12
  60. pydantic_ai/toolsets/{deferred.py → external.py} +11 -5
  61. pydantic_ai/toolsets/filtered.py +1 -1
  62. pydantic_ai/toolsets/function.py +13 -4
  63. pydantic_ai/toolsets/wrapper.py +2 -1
  64. pydantic_ai/usage.py +7 -5
  65. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/METADATA +6 -7
  66. pydantic_ai_slim-1.0.0b1.dist-info/RECORD +120 -0
  67. pydantic_ai_slim-0.8.0.dist-info/RECORD +0 -119
  68. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/WHEEL +0 -0
  69. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/entry_points.txt +0 -0
  70. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0b1.dist-info}/licenses/LICENSE +0 -0
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, Callable, Generic, Literal, Union
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 Concatenate, ParamSpec, Self, TypeAlias, TypeVar
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 = Union[
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 = Union[ToolFuncContext[AgentDepsT, ToolParams], ToolFuncPlain[ToolParams]]
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
- ) -> Union[ToolDefinition, None]:
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
- ) -> Union[list[ToolDefinition], None]:
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
- ) -> Union[ToolDefinition, None]:
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', 'deferred']
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
- - `'deferred'`: 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.
381
- When the model calls a deferred tool, the agent run ends with a `DeferredToolCalls` object and a new run is expected to be started at a later point with the message history and new `ToolReturnPart`s corresponding to each deferred call.
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
@@ -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 .deferred import DeferredToolset
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
  )
@@ -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, Callable, Union
6
+ from typing import Any, TypeAlias
7
7
 
8
- from typing_extensions import Self, TypeAlias
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
- Union[AbstractToolset[AgentDepsT], None, Awaitable[Union[AbstractToolset[AgentDepsT], None]]],
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
 
@@ -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, Callable, Generic, Literal, Protocol
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)
@@ -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, Callable
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: asyncio.Lock = field(compare=False, init=False)
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 DeferredToolset(AbstractToolset[AgentDepsT]):
16
- """A toolset that holds deferred tools whose results will be produced outside of the Pydantic AI agent run in which they were called.
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#deferred-toolset), [`ToolDefinition.kind`][pydantic_ai.tools.ToolDefinition.kind], and [`DeferredToolCalls`][pydantic_ai.output.DeferredToolCalls] for more information.
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='deferred'),
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('Deferred tools cannot be called')
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`."""
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass
4
- from typing import Callable
5
5
 
6
6
  from .._run_context import AgentDepsT, RunContext
7
7
  from ..tools import ToolDefinition
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Awaitable, Sequence
3
+ from collections.abc import Awaitable, Callable, Sequence
4
4
  from dataclasses import dataclass, replace
5
- from typing import Any, Callable, overload
5
+ from typing import Any, overload
6
6
 
7
7
  from pydantic.json_schema import GenerateJsonSchema
8
8
 
@@ -19,7 +19,7 @@ from ..tools import (
19
19
  from .abstract import AbstractToolset, ToolsetTool
20
20
 
21
21
 
22
- @dataclass
22
+ @dataclass(kw_only=True)
23
23
  class FunctionToolsetTool(ToolsetTool[AgentDepsT]):
24
24
  """A tool definition for a function toolset tool that keeps track of the function to call."""
25
25
 
@@ -40,8 +40,8 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
40
40
  def __init__(
41
41
  self,
42
42
  tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = [],
43
- max_retries: int = 1,
44
43
  *,
44
+ max_retries: int = 1,
45
45
  id: str | None = None,
46
46
  ):
47
47
  """Build a new function toolset.
@@ -80,6 +80,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
80
80
  require_parameter_descriptions: bool = False,
81
81
  schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
82
82
  strict: bool | None = None,
83
+ requires_approval: bool = False,
83
84
  ) -> Callable[[ToolFuncEither[AgentDepsT, ToolParams]], ToolFuncEither[AgentDepsT, ToolParams]]: ...
84
85
 
85
86
  def tool(
@@ -94,6 +95,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
94
95
  require_parameter_descriptions: bool = False,
95
96
  schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
96
97
  strict: bool | None = None,
98
+ requires_approval: bool = False,
97
99
  ) -> Any:
98
100
  """Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.
99
101
 
@@ -140,6 +142,8 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
140
142
  schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
141
143
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
142
144
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
145
+ requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
146
+ See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
143
147
  """
144
148
 
145
149
  def tool_decorator(
@@ -156,6 +160,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
156
160
  require_parameter_descriptions,
157
161
  schema_generator,
158
162
  strict,
163
+ requires_approval,
159
164
  )
160
165
  return func_
161
166
 
@@ -172,6 +177,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
172
177
  require_parameter_descriptions: bool = False,
173
178
  schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
174
179
  strict: bool | None = None,
180
+ requires_approval: bool = False,
175
181
  ) -> None:
176
182
  """Add a function as a tool to the toolset.
177
183
 
@@ -195,6 +201,8 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
195
201
  schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
196
202
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
197
203
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
204
+ requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
205
+ See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
198
206
  """
199
207
  tool = Tool[AgentDepsT](
200
208
  func,
@@ -206,6 +214,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
206
214
  require_parameter_descriptions=require_parameter_descriptions,
207
215
  schema_generator=schema_generator,
208
216
  strict=strict,
217
+ requires_approval=requires_approval,
209
218
  )
210
219
  self.add_tool(tool)
211
220
 
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass, replace
4
- from typing import Any, Callable
5
+ from typing import Any
5
6
 
6
7
  from typing_extensions import Self
7
8
 
pydantic_ai/usage.py CHANGED
@@ -12,7 +12,7 @@ from .exceptions import UsageLimitExceeded
12
12
  __all__ = 'RequestUsage', 'RunUsage', 'Usage', 'UsageLimits'
13
13
 
14
14
 
15
- @dataclass(repr=False)
15
+ @dataclass(repr=False, kw_only=True)
16
16
  class UsageBase:
17
17
  input_tokens: int = 0
18
18
  """Number of input/prompt tokens."""
@@ -75,7 +75,7 @@ class UsageBase:
75
75
  return any(dataclasses.asdict(self).values())
76
76
 
77
77
 
78
- @dataclass(repr=False)
78
+ @dataclass(repr=False, kw_only=True)
79
79
  class RequestUsage(UsageBase):
80
80
  """LLM usage associated with a single request.
81
81
 
@@ -107,7 +107,7 @@ class RequestUsage(UsageBase):
107
107
  return new_usage
108
108
 
109
109
 
110
- @dataclass(repr=False)
110
+ @dataclass(repr=False, kw_only=True)
111
111
  class RunUsage(UsageBase):
112
112
  """LLM usage associated with an agent run.
113
113
 
@@ -122,11 +122,13 @@ class RunUsage(UsageBase):
122
122
 
123
123
  cache_write_tokens: int = 0
124
124
  """Total number of tokens written to the cache."""
125
+
125
126
  cache_read_tokens: int = 0
126
127
  """Total number of tokens read from the cache."""
127
128
 
128
129
  input_audio_tokens: int = 0
129
130
  """Total number of audio input tokens."""
131
+
130
132
  cache_audio_read_tokens: int = 0
131
133
  """Total number of audio tokens read from the cache."""
132
134
 
@@ -174,13 +176,13 @@ def _incr_usage_tokens(slf: RunUsage | RequestUsage, incr_usage: RunUsage | Requ
174
176
  slf.details[key] = slf.details.get(key, 0) + value
175
177
 
176
178
 
177
- @dataclass
179
+ @dataclass(repr=False, kw_only=True)
178
180
  @deprecated('`Usage` is deprecated, use `RunUsage` instead')
179
181
  class Usage(RunUsage):
180
182
  """Deprecated alias for `RunUsage`."""
181
183
 
182
184
 
183
- @dataclass(repr=False)
185
+ @dataclass(repr=False, kw_only=True)
184
186
  class UsageLimits:
185
187
  """Limits on model usage.
186
188