pydantic-ai-slim 1.0.0b1__py3-none-any.whl → 1.0.2__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 (38) hide show
  1. pydantic_ai/_a2a.py +1 -1
  2. pydantic_ai/_agent_graph.py +65 -49
  3. pydantic_ai/_parts_manager.py +3 -1
  4. pydantic_ai/_tool_manager.py +33 -6
  5. pydantic_ai/ag_ui.py +75 -43
  6. pydantic_ai/agent/__init__.py +10 -7
  7. pydantic_ai/durable_exec/dbos/__init__.py +6 -0
  8. pydantic_ai/durable_exec/dbos/_agent.py +718 -0
  9. pydantic_ai/durable_exec/dbos/_mcp_server.py +89 -0
  10. pydantic_ai/durable_exec/dbos/_model.py +137 -0
  11. pydantic_ai/durable_exec/dbos/_utils.py +10 -0
  12. pydantic_ai/durable_exec/temporal/_agent.py +71 -10
  13. pydantic_ai/exceptions.py +2 -2
  14. pydantic_ai/mcp.py +14 -26
  15. pydantic_ai/messages.py +90 -19
  16. pydantic_ai/models/__init__.py +9 -0
  17. pydantic_ai/models/anthropic.py +28 -11
  18. pydantic_ai/models/bedrock.py +6 -14
  19. pydantic_ai/models/gemini.py +3 -1
  20. pydantic_ai/models/google.py +58 -5
  21. pydantic_ai/models/groq.py +122 -34
  22. pydantic_ai/models/instrumented.py +29 -11
  23. pydantic_ai/models/openai.py +84 -29
  24. pydantic_ai/providers/__init__.py +4 -0
  25. pydantic_ai/providers/bedrock.py +11 -3
  26. pydantic_ai/providers/google_vertex.py +2 -1
  27. pydantic_ai/providers/groq.py +21 -2
  28. pydantic_ai/providers/litellm.py +134 -0
  29. pydantic_ai/retries.py +42 -2
  30. pydantic_ai/tools.py +18 -7
  31. pydantic_ai/toolsets/combined.py +2 -2
  32. pydantic_ai/toolsets/function.py +54 -19
  33. pydantic_ai/usage.py +37 -3
  34. {pydantic_ai_slim-1.0.0b1.dist-info → pydantic_ai_slim-1.0.2.dist-info}/METADATA +9 -8
  35. {pydantic_ai_slim-1.0.0b1.dist-info → pydantic_ai_slim-1.0.2.dist-info}/RECORD +38 -32
  36. {pydantic_ai_slim-1.0.0b1.dist-info → pydantic_ai_slim-1.0.2.dist-info}/WHEEL +0 -0
  37. {pydantic_ai_slim-1.0.0b1.dist-info → pydantic_ai_slim-1.0.2.dist-info}/entry_points.txt +0 -0
  38. {pydantic_ai_slim-1.0.0b1.dist-info → pydantic_ai_slim-1.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ from typing import overload
4
+
5
+ from httpx import AsyncClient as AsyncHTTPClient
6
+ from openai import AsyncOpenAI
7
+
8
+ from pydantic_ai.models import cached_async_http_client
9
+ from pydantic_ai.profiles import ModelProfile
10
+ from pydantic_ai.profiles.amazon import amazon_model_profile
11
+ from pydantic_ai.profiles.anthropic import anthropic_model_profile
12
+ from pydantic_ai.profiles.cohere import cohere_model_profile
13
+ from pydantic_ai.profiles.deepseek import deepseek_model_profile
14
+ from pydantic_ai.profiles.google import google_model_profile
15
+ from pydantic_ai.profiles.grok import grok_model_profile
16
+ from pydantic_ai.profiles.groq import groq_model_profile
17
+ from pydantic_ai.profiles.meta import meta_model_profile
18
+ from pydantic_ai.profiles.mistral import mistral_model_profile
19
+ from pydantic_ai.profiles.moonshotai import moonshotai_model_profile
20
+ from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer, OpenAIModelProfile, openai_model_profile
21
+ from pydantic_ai.profiles.qwen import qwen_model_profile
22
+ from pydantic_ai.providers import Provider
23
+
24
+ try:
25
+ from openai import AsyncOpenAI
26
+ except ImportError as _import_error: # pragma: no cover
27
+ raise ImportError(
28
+ 'Please install the `openai` package to use the LiteLLM provider, '
29
+ 'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`'
30
+ ) from _import_error
31
+
32
+
33
+ class LiteLLMProvider(Provider[AsyncOpenAI]):
34
+ """Provider for LiteLLM API."""
35
+
36
+ @property
37
+ def name(self) -> str:
38
+ return 'litellm'
39
+
40
+ @property
41
+ def base_url(self) -> str:
42
+ return str(self.client.base_url)
43
+
44
+ @property
45
+ def client(self) -> AsyncOpenAI:
46
+ return self._client
47
+
48
+ def model_profile(self, model_name: str) -> ModelProfile | None:
49
+ # Map provider prefixes to their profile functions
50
+ provider_to_profile = {
51
+ 'anthropic': anthropic_model_profile,
52
+ 'openai': openai_model_profile,
53
+ 'google': google_model_profile,
54
+ 'mistralai': mistral_model_profile,
55
+ 'mistral': mistral_model_profile,
56
+ 'cohere': cohere_model_profile,
57
+ 'amazon': amazon_model_profile,
58
+ 'bedrock': amazon_model_profile,
59
+ 'meta-llama': meta_model_profile,
60
+ 'meta': meta_model_profile,
61
+ 'groq': groq_model_profile,
62
+ 'deepseek': deepseek_model_profile,
63
+ 'moonshotai': moonshotai_model_profile,
64
+ 'x-ai': grok_model_profile,
65
+ 'qwen': qwen_model_profile,
66
+ }
67
+
68
+ profile = None
69
+
70
+ # Check if model name contains a provider prefix (e.g., "anthropic/claude-3")
71
+ if '/' in model_name:
72
+ provider_prefix, model_suffix = model_name.split('/', 1)
73
+ if provider_prefix in provider_to_profile:
74
+ profile = provider_to_profile[provider_prefix](model_suffix)
75
+
76
+ # If no profile found, default to OpenAI profile
77
+ if profile is None:
78
+ profile = openai_model_profile(model_name)
79
+
80
+ # As LiteLLMProvider is used with OpenAIModel, which uses OpenAIJsonSchemaTransformer,
81
+ # we maintain that behavior
82
+ return OpenAIModelProfile(json_schema_transformer=OpenAIJsonSchemaTransformer).update(profile)
83
+
84
+ @overload
85
+ def __init__(
86
+ self,
87
+ *,
88
+ api_key: str | None = None,
89
+ api_base: str | None = None,
90
+ ) -> None: ...
91
+
92
+ @overload
93
+ def __init__(
94
+ self,
95
+ *,
96
+ api_key: str | None = None,
97
+ api_base: str | None = None,
98
+ http_client: AsyncHTTPClient,
99
+ ) -> None: ...
100
+
101
+ @overload
102
+ def __init__(self, *, openai_client: AsyncOpenAI) -> None: ...
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ api_key: str | None = None,
108
+ api_base: str | None = None,
109
+ openai_client: AsyncOpenAI | None = None,
110
+ http_client: AsyncHTTPClient | None = None,
111
+ ) -> None:
112
+ """Initialize a LiteLLM provider.
113
+
114
+ Args:
115
+ api_key: API key for the model provider. If None, LiteLLM will try to get it from environment variables.
116
+ api_base: Base URL for the model provider. Use this for custom endpoints or self-hosted models.
117
+ openai_client: Pre-configured OpenAI client. If provided, other parameters are ignored.
118
+ http_client: Custom HTTP client to use.
119
+ """
120
+ if openai_client is not None:
121
+ self._client = openai_client
122
+ return
123
+
124
+ # Create OpenAI client that will be used with LiteLLM's completion function
125
+ # The actual API calls will be intercepted and routed through LiteLLM
126
+ if http_client is not None:
127
+ self._client = AsyncOpenAI(
128
+ base_url=api_base, api_key=api_key or 'litellm-placeholder', http_client=http_client
129
+ )
130
+ else:
131
+ http_client = cached_async_http_client(provider='litellm')
132
+ self._client = AsyncOpenAI(
133
+ base_url=api_base, api_key=api_key or 'litellm-placeholder', http_client=http_client
134
+ )
pydantic_ai/retries.py CHANGED
@@ -13,6 +13,8 @@ The module includes:
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ from types import TracebackType
17
+
16
18
  from httpx import (
17
19
  AsyncBaseTransport,
18
20
  AsyncHTTPTransport,
@@ -185,11 +187,30 @@ class TenacityTransport(BaseTransport):
185
187
  response.request = req
186
188
 
187
189
  if self.validate_response:
188
- self.validate_response(response)
190
+ try:
191
+ self.validate_response(response)
192
+ except Exception:
193
+ response.close()
194
+ raise
189
195
  return response
190
196
 
191
197
  return handle_request(request)
192
198
 
199
+ def __enter__(self) -> TenacityTransport:
200
+ self.wrapped.__enter__()
201
+ return self
202
+
203
+ def __exit__(
204
+ self,
205
+ exc_type: type[BaseException] | None = None,
206
+ exc_value: BaseException | None = None,
207
+ traceback: TracebackType | None = None,
208
+ ) -> None:
209
+ self.wrapped.__exit__(exc_type, exc_value, traceback)
210
+
211
+ def close(self) -> None:
212
+ self.wrapped.close() # pragma: no cover
213
+
193
214
 
194
215
  class AsyncTenacityTransport(AsyncBaseTransport):
195
216
  """Asynchronous HTTP transport with tenacity-based retry functionality.
@@ -263,11 +284,30 @@ class AsyncTenacityTransport(AsyncBaseTransport):
263
284
  response.request = req
264
285
 
265
286
  if self.validate_response:
266
- self.validate_response(response)
287
+ try:
288
+ self.validate_response(response)
289
+ except Exception:
290
+ await response.aclose()
291
+ raise
267
292
  return response
268
293
 
269
294
  return await handle_async_request(request)
270
295
 
296
+ async def __aenter__(self) -> AsyncTenacityTransport:
297
+ await self.wrapped.__aenter__()
298
+ return self
299
+
300
+ async def __aexit__(
301
+ self,
302
+ exc_type: type[BaseException] | None = None,
303
+ exc_value: BaseException | None = None,
304
+ traceback: TracebackType | None = None,
305
+ ) -> None:
306
+ await self.wrapped.__aexit__(exc_type, exc_value, traceback)
307
+
308
+ async def aclose(self) -> None:
309
+ await self.wrapped.aclose()
310
+
271
311
 
272
312
  def wait_retry_after(
273
313
  fallback_strategy: Callable[[RetryCallState], float] | None = None, max_wait: float = 300
pydantic_ai/tools.py CHANGED
@@ -70,7 +70,7 @@ Usage `ToolFuncEither[AgentDepsT, ToolParams]`.
70
70
  ToolPrepareFunc: TypeAlias = Callable[[RunContext[AgentDepsT], 'ToolDefinition'], Awaitable['ToolDefinition | None']]
71
71
  """Definition of a function that can prepare a tool definition at call time.
72
72
 
73
- See [tool docs](../tools.md#tool-prepare) for more information.
73
+ See [tool docs](../tools-advanced.md#tool-prepare) for more information.
74
74
 
75
75
  Example — here `only_if_42` is valid as a `ToolPrepareFunc`:
76
76
 
@@ -140,7 +140,7 @@ class DeferredToolRequests:
140
140
 
141
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
142
 
143
- See [deferred tools docs](../tools.md#deferred-tools) for more information.
143
+ See [deferred tools docs](../deferred-tools.md#deferred-tools) for more information.
144
144
  """
145
145
 
146
146
  calls: list[ToolCallPart] = field(default_factory=list)
@@ -204,7 +204,7 @@ class DeferredToolResults:
204
204
 
205
205
  The tool call IDs need to match those from the [`DeferredToolRequests`][pydantic_ai.output.DeferredToolRequests] output object from the previous run.
206
206
 
207
- See [deferred tools docs](../tools.md#deferred-tools) for more information.
207
+ See [deferred tools docs](../deferred-tools.md#deferred-tools) for more information.
208
208
  """
209
209
 
210
210
  calls: dict[str, DeferredToolCallResult | Any] = field(default_factory=dict)
@@ -253,6 +253,7 @@ class Tool(Generic[AgentDepsT]):
253
253
  docstring_format: DocstringFormat
254
254
  require_parameter_descriptions: bool
255
255
  strict: bool | None
256
+ sequential: bool
256
257
  requires_approval: bool
257
258
  function_schema: _function_schema.FunctionSchema
258
259
  """
@@ -274,6 +275,7 @@ class Tool(Generic[AgentDepsT]):
274
275
  require_parameter_descriptions: bool = False,
275
276
  schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
276
277
  strict: bool | None = None,
278
+ sequential: bool = False,
277
279
  requires_approval: bool = False,
278
280
  function_schema: _function_schema.FunctionSchema | None = None,
279
281
  ):
@@ -327,8 +329,9 @@ class Tool(Generic[AgentDepsT]):
327
329
  schema_generator: The JSON schema generator class to use. Defaults to `GenerateToolJsonSchema`.
328
330
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
329
331
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
332
+ sequential: Whether the function requires a sequential/serial execution environment. Defaults to False.
330
333
  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.
334
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
332
335
  function_schema: The function schema to use for the tool. If not provided, it will be generated.
333
336
  """
334
337
  self.function = function
@@ -347,6 +350,7 @@ class Tool(Generic[AgentDepsT]):
347
350
  self.docstring_format = docstring_format
348
351
  self.require_parameter_descriptions = require_parameter_descriptions
349
352
  self.strict = strict
353
+ self.sequential = sequential
350
354
  self.requires_approval = requires_approval
351
355
 
352
356
  @classmethod
@@ -357,6 +361,7 @@ class Tool(Generic[AgentDepsT]):
357
361
  description: str | None,
358
362
  json_schema: JsonSchemaValue,
359
363
  takes_ctx: bool = False,
364
+ sequential: bool = False,
360
365
  ) -> Self:
361
366
  """Creates a Pydantic tool from a function and a JSON schema.
362
367
 
@@ -370,6 +375,7 @@ class Tool(Generic[AgentDepsT]):
370
375
  json_schema: The schema for the function arguments
371
376
  takes_ctx: An optional boolean parameter indicating whether the function
372
377
  accepts the context object as an argument.
378
+ sequential: Whether the function requires a sequential/serial execution environment. Defaults to False.
373
379
 
374
380
  Returns:
375
381
  A Pydantic tool that calls the function
@@ -389,6 +395,7 @@ class Tool(Generic[AgentDepsT]):
389
395
  name=name,
390
396
  description=description,
391
397
  function_schema=function_schema,
398
+ sequential=sequential,
392
399
  )
393
400
 
394
401
  @property
@@ -398,6 +405,7 @@ class Tool(Generic[AgentDepsT]):
398
405
  description=self.description,
399
406
  parameters_json_schema=self.function_schema.json_schema,
400
407
  strict=self.strict,
408
+ sequential=self.sequential,
401
409
  )
402
410
 
403
411
  async def prepare_tool_def(self, ctx: RunContext[AgentDepsT]) -> ToolDefinition | None:
@@ -466,22 +474,25 @@ class ToolDefinition:
466
474
  Note: this is currently only supported by OpenAI models.
467
475
  """
468
476
 
477
+ sequential: bool = False
478
+ """Whether this tool requires a sequential/serial execution environment."""
479
+
469
480
  kind: ToolKind = field(default='function')
470
481
  """The kind of tool:
471
482
 
472
483
  - `'function'`: a tool that will be executed by Pydantic AI during an agent run and has its result returned to the model
473
484
  - `'output'`: a tool that passes through an output value that ends the run
474
485
  - `'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.
486
+ See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
476
487
  - `'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.
488
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
478
489
  """
479
490
 
480
491
  @property
481
492
  def defer(self) -> bool:
482
493
  """Whether calls to this tool will be deferred.
483
494
 
484
- See the [tools documentation](../tools.md#deferred-tools) for more info.
495
+ See the [tools documentation](../deferred-tools.md#deferred-tools) for more info.
485
496
  """
486
497
  return self.kind in ('external', 'unapproved')
487
498
 
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from asyncio import Lock
4
5
  from collections.abc import Callable, Sequence
5
6
  from contextlib import AsyncExitStack
6
7
  from dataclasses import dataclass, field, replace
7
8
  from typing import Any
8
9
 
9
- import anyio
10
10
  from typing_extensions import Self
11
11
 
12
12
  from .._run_context import AgentDepsT, RunContext
@@ -31,7 +31,7 @@ class CombinedToolset(AbstractToolset[AgentDepsT]):
31
31
 
32
32
  toolsets: Sequence[AbstractToolset[AgentDepsT]]
33
33
 
34
- _enter_lock: anyio.Lock = field(compare=False, init=False, default_factory=anyio.Lock)
34
+ _enter_lock: Lock = field(compare=False, init=False, default_factory=Lock)
35
35
  _entered_count: int = field(init=False, default=0)
36
36
  _exit_stack: AsyncExitStack | None = field(init=False, default=None)
37
37
 
@@ -33,9 +33,12 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
33
33
  See [toolset docs](../toolsets.md#function-toolset) for more information.
34
34
  """
35
35
 
36
- max_retries: int
37
36
  tools: dict[str, Tool[Any]]
37
+ max_retries: int
38
38
  _id: str | None
39
+ docstring_format: DocstringFormat
40
+ require_parameter_descriptions: bool
41
+ schema_generator: type[GenerateJsonSchema]
39
42
 
40
43
  def __init__(
41
44
  self,
@@ -43,16 +46,30 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
43
46
  *,
44
47
  max_retries: int = 1,
45
48
  id: str | None = None,
49
+ docstring_format: DocstringFormat = 'auto',
50
+ require_parameter_descriptions: bool = False,
51
+ schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
46
52
  ):
47
53
  """Build a new function toolset.
48
54
 
49
55
  Args:
50
56
  tools: The tools to add to the toolset.
51
57
  max_retries: The maximum number of retries for each tool during a run.
52
- id: An optional unique ID for the toolset. A toolset needs to have an ID in order to be used in a durable execution environment like Temporal, in which case the ID will be used to identify the toolset's activities within the workflow.
58
+ id: An optional unique ID for the toolset. A toolset needs to have an ID in order to be used in a durable execution environment like Temporal,
59
+ in which case the ID will be used to identify the toolset's activities within the workflow.
60
+ docstring_format: Format of tool docstring, see [`DocstringFormat`][pydantic_ai.tools.DocstringFormat].
61
+ Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
62
+ Applies to all tools, unless overridden when adding a tool.
63
+ require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
64
+ Applies to all tools, unless overridden when adding a tool.
65
+ schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
66
+ Applies to all tools, unless overridden when adding a tool.
53
67
  """
54
68
  self.max_retries = max_retries
55
69
  self._id = id
70
+ self.docstring_format = docstring_format
71
+ self.require_parameter_descriptions = require_parameter_descriptions
72
+ self.schema_generator = schema_generator
56
73
 
57
74
  self.tools = {}
58
75
  for tool in tools:
@@ -76,10 +93,11 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
76
93
  name: str | None = None,
77
94
  retries: int | None = None,
78
95
  prepare: ToolPrepareFunc[AgentDepsT] | None = None,
79
- docstring_format: DocstringFormat = 'auto',
80
- require_parameter_descriptions: bool = False,
81
- schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
96
+ docstring_format: DocstringFormat | None = None,
97
+ require_parameter_descriptions: bool | None = None,
98
+ schema_generator: type[GenerateJsonSchema] | None = None,
82
99
  strict: bool | None = None,
100
+ sequential: bool = False,
83
101
  requires_approval: bool = False,
84
102
  ) -> Callable[[ToolFuncEither[AgentDepsT, ToolParams]], ToolFuncEither[AgentDepsT, ToolParams]]: ...
85
103
 
@@ -91,10 +109,11 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
91
109
  name: str | None = None,
92
110
  retries: int | None = None,
93
111
  prepare: ToolPrepareFunc[AgentDepsT] | None = None,
94
- docstring_format: DocstringFormat = 'auto',
95
- require_parameter_descriptions: bool = False,
96
- schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
112
+ docstring_format: DocstringFormat | None = None,
113
+ require_parameter_descriptions: bool | None = None,
114
+ schema_generator: type[GenerateJsonSchema] | None = None,
97
115
  strict: bool | None = None,
116
+ sequential: bool = False,
98
117
  requires_approval: bool = False,
99
118
  ) -> Any:
100
119
  """Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.
@@ -137,13 +156,16 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
137
156
  tool from a given step. This is useful if you want to customise a tool at call time,
138
157
  or omit it completely from a step. See [`ToolPrepareFunc`][pydantic_ai.tools.ToolPrepareFunc].
139
158
  docstring_format: The format of the docstring, see [`DocstringFormat`][pydantic_ai.tools.DocstringFormat].
140
- Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
141
- require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
142
- schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
159
+ If `None`, the default value is determined by the toolset.
160
+ require_parameter_descriptions: If True, raise an error if a parameter description is missing.
161
+ If `None`, the default value is determined by the toolset.
162
+ schema_generator: The JSON schema generator class to use for this tool.
163
+ If `None`, the default value is determined by the toolset.
143
164
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
144
165
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
166
+ sequential: Whether the function requires a sequential/serial execution environment. Defaults to False.
145
167
  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.
168
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
147
169
  """
148
170
 
149
171
  def tool_decorator(
@@ -160,6 +182,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
160
182
  require_parameter_descriptions,
161
183
  schema_generator,
162
184
  strict,
185
+ sequential,
163
186
  requires_approval,
164
187
  )
165
188
  return func_
@@ -173,10 +196,11 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
173
196
  name: str | None = None,
174
197
  retries: int | None = None,
175
198
  prepare: ToolPrepareFunc[AgentDepsT] | None = None,
176
- docstring_format: DocstringFormat = 'auto',
177
- require_parameter_descriptions: bool = False,
178
- schema_generator: type[GenerateJsonSchema] = GenerateToolJsonSchema,
199
+ docstring_format: DocstringFormat | None = None,
200
+ require_parameter_descriptions: bool | None = None,
201
+ schema_generator: type[GenerateJsonSchema] | None = None,
179
202
  strict: bool | None = None,
203
+ sequential: bool = False,
180
204
  requires_approval: bool = False,
181
205
  ) -> None:
182
206
  """Add a function as a tool to the toolset.
@@ -196,14 +220,24 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
196
220
  tool from a given step. This is useful if you want to customise a tool at call time,
197
221
  or omit it completely from a step. See [`ToolPrepareFunc`][pydantic_ai.tools.ToolPrepareFunc].
198
222
  docstring_format: The format of the docstring, see [`DocstringFormat`][pydantic_ai.tools.DocstringFormat].
199
- Defaults to `'auto'`, such that the format is inferred from the structure of the docstring.
200
- require_parameter_descriptions: If True, raise an error if a parameter description is missing. Defaults to False.
201
- schema_generator: The JSON schema generator class to use for this tool. Defaults to `GenerateToolJsonSchema`.
223
+ If `None`, the default value is determined by the toolset.
224
+ require_parameter_descriptions: If True, raise an error if a parameter description is missing.
225
+ If `None`, the default value is determined by the toolset.
226
+ schema_generator: The JSON schema generator class to use for this tool.
227
+ If `None`, the default value is determined by the toolset.
202
228
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
203
229
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
230
+ sequential: Whether the function requires a sequential/serial execution environment. Defaults to False.
204
231
  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.
232
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
206
233
  """
234
+ if docstring_format is None:
235
+ docstring_format = self.docstring_format
236
+ if require_parameter_descriptions is None:
237
+ require_parameter_descriptions = self.require_parameter_descriptions
238
+ if schema_generator is None:
239
+ schema_generator = self.schema_generator
240
+
207
241
  tool = Tool[AgentDepsT](
208
242
  func,
209
243
  takes_ctx=takes_ctx,
@@ -214,6 +248,7 @@ class FunctionToolset(AbstractToolset[AgentDepsT]):
214
248
  require_parameter_descriptions=require_parameter_descriptions,
215
249
  schema_generator=schema_generator,
216
250
  strict=strict,
251
+ sequential=sequential,
217
252
  requires_approval=requires_approval,
218
253
  )
219
254
  self.add_tool(tool)
pydantic_ai/usage.py CHANGED
@@ -3,7 +3,9 @@ from __future__ import annotations as _annotations
3
3
  import dataclasses
4
4
  from copy import copy
5
5
  from dataclasses import dataclass, fields
6
+ from typing import Annotated
6
7
 
8
+ from pydantic import AliasChoices, BeforeValidator, Field
7
9
  from typing_extensions import deprecated, overload
8
10
 
9
11
  from . import _utils
@@ -14,7 +16,11 @@ __all__ = 'RequestUsage', 'RunUsage', 'Usage', 'UsageLimits'
14
16
 
15
17
  @dataclass(repr=False, kw_only=True)
16
18
  class UsageBase:
17
- input_tokens: int = 0
19
+ input_tokens: Annotated[
20
+ int,
21
+ # `request_tokens` is deprecated, but we still want to support deserializing model responses stored in a DB before the name was changed
22
+ Field(validation_alias=AliasChoices('input_tokens', 'request_tokens')),
23
+ ] = 0
18
24
  """Number of input/prompt tokens."""
19
25
 
20
26
  cache_write_tokens: int = 0
@@ -22,7 +28,11 @@ class UsageBase:
22
28
  cache_read_tokens: int = 0
23
29
  """Number of tokens read from the cache."""
24
30
 
25
- output_tokens: int = 0
31
+ output_tokens: Annotated[
32
+ int,
33
+ # `response_tokens` is deprecated, but we still want to support deserializing model responses stored in a DB before the name was changed
34
+ Field(validation_alias=AliasChoices('output_tokens', 'response_tokens')),
35
+ ] = 0
26
36
  """Number of output/completion tokens."""
27
37
 
28
38
  input_audio_tokens: int = 0
@@ -32,7 +42,11 @@ class UsageBase:
32
42
  output_audio_tokens: int = 0
33
43
  """Number of audio output tokens."""
34
44
 
35
- details: dict[str, int] = dataclasses.field(default_factory=dict)
45
+ details: Annotated[
46
+ dict[str, int],
47
+ # `details` can not be `None` any longer, but we still want to support deserializing model responses stored in a DB before this was changed
48
+ BeforeValidator(lambda d: d or {}),
49
+ ] = dataclasses.field(default_factory=dict)
36
50
  """Any extra details returned by the model."""
37
51
 
38
52
  @property
@@ -117,6 +131,9 @@ class RunUsage(UsageBase):
117
131
  requests: int = 0
118
132
  """Number of requests made to the LLM API."""
119
133
 
134
+ tool_calls: int = 0
135
+ """Number of successful tool calls executed during the run."""
136
+
120
137
  input_tokens: int = 0
121
138
  """Total number of text input/prompt tokens."""
122
139
 
@@ -146,6 +163,7 @@ class RunUsage(UsageBase):
146
163
  """
147
164
  if isinstance(incr_usage, RunUsage):
148
165
  self.requests += incr_usage.requests
166
+ self.tool_calls += incr_usage.tool_calls
149
167
  return _incr_usage_tokens(self, incr_usage)
150
168
 
151
169
  def __add__(self, other: RunUsage | RequestUsage) -> RunUsage:
@@ -194,6 +212,8 @@ class UsageLimits:
194
212
 
195
213
  request_limit: int | None = 50
196
214
  """The maximum number of requests allowed to the model."""
215
+ tool_calls_limit: int | None = None
216
+ """The maximum number of successful tool calls allowed to be executed."""
197
217
  input_tokens_limit: int | None = None
198
218
  """The maximum number of input/prompt tokens allowed."""
199
219
  output_tokens_limit: int | None = None
@@ -220,12 +240,14 @@ class UsageLimits:
220
240
  self,
221
241
  *,
222
242
  request_limit: int | None = 50,
243
+ tool_calls_limit: int | None = None,
223
244
  input_tokens_limit: int | None = None,
224
245
  output_tokens_limit: int | None = None,
225
246
  total_tokens_limit: int | None = None,
226
247
  count_tokens_before_request: bool = False,
227
248
  ) -> None:
228
249
  self.request_limit = request_limit
250
+ self.tool_calls_limit = tool_calls_limit
229
251
  self.input_tokens_limit = input_tokens_limit
230
252
  self.output_tokens_limit = output_tokens_limit
231
253
  self.total_tokens_limit = total_tokens_limit
@@ -239,12 +261,14 @@ class UsageLimits:
239
261
  self,
240
262
  *,
241
263
  request_limit: int | None = 50,
264
+ tool_calls_limit: int | None = None,
242
265
  request_tokens_limit: int | None = None,
243
266
  response_tokens_limit: int | None = None,
244
267
  total_tokens_limit: int | None = None,
245
268
  count_tokens_before_request: bool = False,
246
269
  ) -> None:
247
270
  self.request_limit = request_limit
271
+ self.tool_calls_limit = tool_calls_limit
248
272
  self.input_tokens_limit = request_tokens_limit
249
273
  self.output_tokens_limit = response_tokens_limit
250
274
  self.total_tokens_limit = total_tokens_limit
@@ -254,6 +278,7 @@ class UsageLimits:
254
278
  self,
255
279
  *,
256
280
  request_limit: int | None = 50,
281
+ tool_calls_limit: int | None = None,
257
282
  input_tokens_limit: int | None = None,
258
283
  output_tokens_limit: int | None = None,
259
284
  total_tokens_limit: int | None = None,
@@ -263,6 +288,7 @@ class UsageLimits:
263
288
  response_tokens_limit: int | None = None,
264
289
  ):
265
290
  self.request_limit = request_limit
291
+ self.tool_calls_limit = tool_calls_limit
266
292
  self.input_tokens_limit = input_tokens_limit or request_tokens_limit
267
293
  self.output_tokens_limit = output_tokens_limit or response_tokens_limit
268
294
  self.total_tokens_limit = total_tokens_limit
@@ -314,4 +340,12 @@ class UsageLimits:
314
340
  if self.total_tokens_limit is not None and total_tokens > self.total_tokens_limit:
315
341
  raise UsageLimitExceeded(f'Exceeded the total_tokens_limit of {self.total_tokens_limit} ({total_tokens=})')
316
342
 
343
+ def check_before_tool_call(self, usage: RunUsage) -> None:
344
+ """Raises a `UsageLimitExceeded` exception if the next tool call would exceed the tool call limit."""
345
+ tool_calls_limit = self.tool_calls_limit
346
+ if tool_calls_limit is not None and usage.tool_calls >= tool_calls_limit:
347
+ raise UsageLimitExceeded(
348
+ f'The next tool call would exceed the tool_calls_limit of {tool_calls_limit} (tool_calls={usage.tool_calls})'
349
+ )
350
+
317
351
  __repr__ = _utils.dataclasses_no_defaults_repr