pydantic-ai-slim 0.6.1__py3-none-any.whl → 0.7.0__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 (63) hide show
  1. pydantic_ai/__init__.py +5 -0
  2. pydantic_ai/_a2a.py +6 -4
  3. pydantic_ai/_agent_graph.py +32 -32
  4. pydantic_ai/_cli.py +3 -3
  5. pydantic_ai/_output.py +8 -0
  6. pydantic_ai/_tool_manager.py +3 -0
  7. pydantic_ai/_utils.py +7 -1
  8. pydantic_ai/ag_ui.py +25 -14
  9. pydantic_ai/{agent.py → agent/__init__.py} +217 -1026
  10. pydantic_ai/agent/abstract.py +942 -0
  11. pydantic_ai/agent/wrapper.py +227 -0
  12. pydantic_ai/builtin_tools.py +105 -0
  13. pydantic_ai/direct.py +9 -9
  14. pydantic_ai/durable_exec/__init__.py +0 -0
  15. pydantic_ai/durable_exec/temporal/__init__.py +83 -0
  16. pydantic_ai/durable_exec/temporal/_agent.py +699 -0
  17. pydantic_ai/durable_exec/temporal/_function_toolset.py +92 -0
  18. pydantic_ai/durable_exec/temporal/_logfire.py +48 -0
  19. pydantic_ai/durable_exec/temporal/_mcp_server.py +145 -0
  20. pydantic_ai/durable_exec/temporal/_model.py +168 -0
  21. pydantic_ai/durable_exec/temporal/_run_context.py +50 -0
  22. pydantic_ai/durable_exec/temporal/_toolset.py +77 -0
  23. pydantic_ai/ext/aci.py +10 -9
  24. pydantic_ai/ext/langchain.py +4 -2
  25. pydantic_ai/mcp.py +203 -75
  26. pydantic_ai/messages.py +75 -13
  27. pydantic_ai/models/__init__.py +66 -8
  28. pydantic_ai/models/anthropic.py +135 -18
  29. pydantic_ai/models/bedrock.py +16 -5
  30. pydantic_ai/models/cohere.py +11 -4
  31. pydantic_ai/models/fallback.py +4 -2
  32. pydantic_ai/models/function.py +18 -4
  33. pydantic_ai/models/gemini.py +20 -9
  34. pydantic_ai/models/google.py +53 -15
  35. pydantic_ai/models/groq.py +47 -11
  36. pydantic_ai/models/huggingface.py +26 -11
  37. pydantic_ai/models/instrumented.py +3 -1
  38. pydantic_ai/models/mcp_sampling.py +3 -1
  39. pydantic_ai/models/mistral.py +27 -17
  40. pydantic_ai/models/openai.py +97 -33
  41. pydantic_ai/models/test.py +12 -0
  42. pydantic_ai/models/wrapper.py +6 -2
  43. pydantic_ai/profiles/groq.py +23 -0
  44. pydantic_ai/profiles/openai.py +1 -1
  45. pydantic_ai/providers/google.py +7 -7
  46. pydantic_ai/providers/groq.py +2 -0
  47. pydantic_ai/result.py +21 -55
  48. pydantic_ai/run.py +357 -0
  49. pydantic_ai/tools.py +0 -1
  50. pydantic_ai/toolsets/__init__.py +2 -0
  51. pydantic_ai/toolsets/_dynamic.py +87 -0
  52. pydantic_ai/toolsets/abstract.py +23 -3
  53. pydantic_ai/toolsets/combined.py +19 -4
  54. pydantic_ai/toolsets/deferred.py +10 -2
  55. pydantic_ai/toolsets/function.py +23 -8
  56. pydantic_ai/toolsets/prefixed.py +4 -0
  57. pydantic_ai/toolsets/wrapper.py +14 -1
  58. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/METADATA +7 -5
  59. pydantic_ai_slim-0.7.0.dist-info/RECORD +115 -0
  60. pydantic_ai_slim-0.6.1.dist-info/RECORD +0 -100
  61. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/WHEEL +0 -0
  62. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/entry_points.txt +0 -0
  63. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/result.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable
3
+ from collections.abc import AsyncIterator, Awaitable, Callable
4
4
  from copy import copy
5
5
  from dataclasses import dataclass, field
6
6
  from datetime import datetime
@@ -22,7 +22,7 @@ from ._output import (
22
22
  ToolOutputSchema,
23
23
  )
24
24
  from ._run_context import AgentDepsT, RunContext
25
- from .messages import AgentStreamEvent, FinalResultEvent
25
+ from .messages import AgentStreamEvent
26
26
  from .output import (
27
27
  OutputDataT,
28
28
  ToolOutput,
@@ -45,13 +45,13 @@ T = TypeVar('T')
45
45
  class AgentStream(Generic[AgentDepsT, OutputDataT]):
46
46
  _raw_stream_response: models.StreamedResponse
47
47
  _output_schema: OutputSchema[OutputDataT]
48
+ _model_request_parameters: models.ModelRequestParameters
48
49
  _output_validators: list[OutputValidator[AgentDepsT, OutputDataT]]
49
50
  _run_ctx: RunContext[AgentDepsT]
50
51
  _usage_limits: UsageLimits | None
51
52
  _tool_manager: ToolManager[AgentDepsT]
52
53
 
53
54
  _agent_stream_iterator: AsyncIterator[AgentStreamEvent] | None = field(default=None, init=False)
54
- _final_result_event: FinalResultEvent | None = field(default=None, init=False)
55
55
  _initial_run_ctx_usage: Usage = field(init=False)
56
56
 
57
57
  def __post_init__(self):
@@ -60,12 +60,12 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
60
60
  async def stream_output(self, *, debounce_by: float | None = 0.1) -> AsyncIterator[OutputDataT]:
61
61
  """Asynchronously stream the (validated) agent outputs."""
62
62
  async for response in self.stream_responses(debounce_by=debounce_by):
63
- if self._final_result_event is not None:
63
+ if self._raw_stream_response.final_result_event is not None:
64
64
  try:
65
65
  yield await self._validate_response(response, allow_partial=True)
66
66
  except ValidationError:
67
67
  pass
68
- if self._final_result_event is not None: # pragma: no branch
68
+ if self._raw_stream_response.final_result_event is not None: # pragma: no branch
69
69
  yield await self._validate_response(self._raw_stream_response.get())
70
70
 
71
71
  async def stream_responses(self, *, debounce_by: float | None = 0.1) -> AsyncIterator[_messages.ModelResponse]:
@@ -131,10 +131,11 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
131
131
 
132
132
  async def _validate_response(self, message: _messages.ModelResponse, *, allow_partial: bool = False) -> OutputDataT:
133
133
  """Validate a structured result message."""
134
- if self._final_result_event is None:
134
+ final_result_event = self._raw_stream_response.final_result_event
135
+ if final_result_event is None:
135
136
  raise exceptions.UnexpectedModelBehavior('Invalid response, unable to find output') # pragma: no cover
136
137
 
137
- output_tool_name = self._final_result_event.tool_name
138
+ output_tool_name = final_result_event.tool_name
138
139
 
139
140
  if isinstance(self._output_schema, ToolOutputSchema) and output_tool_name is not None:
140
141
  tool_call = next(
@@ -195,7 +196,7 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
195
196
  and isinstance(event.part, _messages.TextPart)
196
197
  and event.part.content
197
198
  ):
198
- yield event.part.content, event.index # pragma: no cover
199
+ yield event.part.content, event.index
199
200
  elif ( # pragma: no branch
200
201
  isinstance(event, _messages.PartDeltaEvent)
201
202
  and isinstance(event.delta, _messages.TextPartDelta)
@@ -221,52 +222,12 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
221
222
  yield ''.join(deltas)
222
223
 
223
224
  def __aiter__(self) -> AsyncIterator[AgentStreamEvent]:
224
- """Stream [`AgentStreamEvent`][pydantic_ai.messages.AgentStreamEvent]s.
225
-
226
- This proxies the _raw_stream_response and sends all events to the agent stream, while also checking for matches
227
- on the result schema and emitting a [`FinalResultEvent`][pydantic_ai.messages.FinalResultEvent] if/when the
228
- first match is found.
229
- """
230
- if self._agent_stream_iterator is not None:
231
- return self._agent_stream_iterator
232
-
233
- async def aiter():
234
- output_schema = self._output_schema
235
-
236
- def _get_final_result_event(e: _messages.ModelResponseStreamEvent) -> _messages.FinalResultEvent | None:
237
- """Return an appropriate FinalResultEvent if `e` corresponds to a part that will produce a final result."""
238
- if isinstance(e, _messages.PartStartEvent):
239
- new_part = e.part
240
- if isinstance(new_part, _messages.TextPart) and isinstance(
241
- output_schema, TextOutputSchema
242
- ): # pragma: no branch
243
- return _messages.FinalResultEvent(tool_name=None, tool_call_id=None)
244
- elif isinstance(new_part, _messages.ToolCallPart) and (
245
- tool_def := self._tool_manager.get_tool_def(new_part.tool_name)
246
- ):
247
- if tool_def.kind == 'output':
248
- return _messages.FinalResultEvent(
249
- tool_name=new_part.tool_name, tool_call_id=new_part.tool_call_id
250
- )
251
- elif tool_def.kind == 'deferred':
252
- return _messages.FinalResultEvent(tool_name=None, tool_call_id=None)
253
-
254
- usage_checking_stream = _get_usage_checking_stream_response(
225
+ """Stream [`AgentStreamEvent`][pydantic_ai.messages.AgentStreamEvent]s."""
226
+ if self._agent_stream_iterator is None:
227
+ self._agent_stream_iterator = _get_usage_checking_stream_response(
255
228
  self._raw_stream_response, self._usage_limits, self.usage
256
229
  )
257
- async for event in usage_checking_stream:
258
- yield event
259
- if (final_result_event := _get_final_result_event(event)) is not None:
260
- self._final_result_event = final_result_event
261
- yield final_result_event
262
- break
263
-
264
- # If we broke out of the above loop, we need to yield the rest of the events
265
- # If we didn't, this will just be a no-op
266
- async for event in usage_checking_stream:
267
- yield event
268
-
269
- self._agent_stream_iterator = aiter()
230
+
270
231
  return self._agent_stream_iterator
271
232
 
272
233
 
@@ -462,10 +423,10 @@ class FinalResult(Generic[OutputDataT]):
462
423
 
463
424
 
464
425
  def _get_usage_checking_stream_response(
465
- stream_response: AsyncIterable[_messages.ModelResponseStreamEvent],
426
+ stream_response: models.StreamedResponse,
466
427
  limits: UsageLimits | None,
467
428
  get_usage: Callable[[], Usage],
468
- ) -> AsyncIterable[_messages.ModelResponseStreamEvent]:
429
+ ) -> AsyncIterator[AgentStreamEvent]:
469
430
  if limits is not None and limits.has_token_limits():
470
431
 
471
432
  async def _usage_checking_iterator():
@@ -475,4 +436,9 @@ def _get_usage_checking_stream_response(
475
436
 
476
437
  return _usage_checking_iterator()
477
438
  else:
478
- return stream_response
439
+ # TODO: Use `return aiter(stream_response)` once we drop support for Python 3.9
440
+ async def _iterator():
441
+ async for item in stream_response:
442
+ yield item
443
+
444
+ return _iterator()
pydantic_ai/run.py ADDED
@@ -0,0 +1,357 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ import dataclasses
4
+ from collections.abc import AsyncIterator
5
+ from copy import deepcopy
6
+ from typing import Any, Generic, overload
7
+
8
+ from typing_extensions import Literal
9
+
10
+ from pydantic_graph import End, GraphRun, GraphRunContext
11
+
12
+ from . import (
13
+ _agent_graph,
14
+ exceptions,
15
+ messages as _messages,
16
+ usage as _usage,
17
+ )
18
+ from .output import OutputDataT
19
+ from .result import FinalResult
20
+ from .tools import AgentDepsT
21
+
22
+
23
+ @dataclasses.dataclass(repr=False)
24
+ class AgentRun(Generic[AgentDepsT, OutputDataT]):
25
+ """A stateful, async-iterable run of an [`Agent`][pydantic_ai.agent.Agent].
26
+
27
+ You generally obtain an `AgentRun` instance by calling `async with my_agent.iter(...) as agent_run:`.
28
+
29
+ Once you have an instance, you can use it to iterate through the run's nodes as they execute. When an
30
+ [`End`][pydantic_graph.nodes.End] is reached, the run finishes and [`result`][pydantic_ai.agent.AgentRun.result]
31
+ becomes available.
32
+
33
+ Example:
34
+ ```python
35
+ from pydantic_ai import Agent
36
+
37
+ agent = Agent('openai:gpt-4o')
38
+
39
+ async def main():
40
+ nodes = []
41
+ # Iterate through the run, recording each node along the way:
42
+ async with agent.iter('What is the capital of France?') as agent_run:
43
+ async for node in agent_run:
44
+ nodes.append(node)
45
+ print(nodes)
46
+ '''
47
+ [
48
+ UserPromptNode(
49
+ user_prompt='What is the capital of France?',
50
+ instructions=None,
51
+ instructions_functions=[],
52
+ system_prompts=(),
53
+ system_prompt_functions=[],
54
+ system_prompt_dynamic_functions={},
55
+ ),
56
+ ModelRequestNode(
57
+ request=ModelRequest(
58
+ parts=[
59
+ UserPromptPart(
60
+ content='What is the capital of France?',
61
+ timestamp=datetime.datetime(...),
62
+ )
63
+ ]
64
+ )
65
+ ),
66
+ CallToolsNode(
67
+ model_response=ModelResponse(
68
+ parts=[TextPart(content='The capital of France is Paris.')],
69
+ usage=Usage(
70
+ requests=1, request_tokens=56, response_tokens=7, total_tokens=63
71
+ ),
72
+ model_name='gpt-4o',
73
+ timestamp=datetime.datetime(...),
74
+ )
75
+ ),
76
+ End(data=FinalResult(output='The capital of France is Paris.')),
77
+ ]
78
+ '''
79
+ print(agent_run.result.output)
80
+ #> The capital of France is Paris.
81
+ ```
82
+
83
+ You can also manually drive the iteration using the [`next`][pydantic_ai.agent.AgentRun.next] method for
84
+ more granular control.
85
+ """
86
+
87
+ _graph_run: GraphRun[
88
+ _agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any], FinalResult[OutputDataT]
89
+ ]
90
+
91
+ @overload
92
+ def _traceparent(self, *, required: Literal[False]) -> str | None: ...
93
+ @overload
94
+ def _traceparent(self) -> str: ...
95
+ def _traceparent(self, *, required: bool = True) -> str | None:
96
+ traceparent = self._graph_run._traceparent(required=False) # type: ignore[reportPrivateUsage]
97
+ if traceparent is None and required: # pragma: no cover
98
+ raise AttributeError('No span was created for this agent run')
99
+ return traceparent
100
+
101
+ @property
102
+ def ctx(self) -> GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any]]:
103
+ """The current context of the agent run."""
104
+ return GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any]](
105
+ self._graph_run.state, self._graph_run.deps
106
+ )
107
+
108
+ @property
109
+ def next_node(
110
+ self,
111
+ ) -> _agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]:
112
+ """The next node that will be run in the agent graph.
113
+
114
+ This is the next node that will be used during async iteration, or if a node is not passed to `self.next(...)`.
115
+ """
116
+ next_node = self._graph_run.next_node
117
+ if isinstance(next_node, End):
118
+ return next_node
119
+ if _agent_graph.is_agent_node(next_node):
120
+ return next_node
121
+ raise exceptions.AgentRunError(f'Unexpected node type: {type(next_node)}') # pragma: no cover
122
+
123
+ @property
124
+ def result(self) -> AgentRunResult[OutputDataT] | None:
125
+ """The final result of the run if it has ended, otherwise `None`.
126
+
127
+ Once the run returns an [`End`][pydantic_graph.nodes.End] node, `result` is populated
128
+ with an [`AgentRunResult`][pydantic_ai.agent.AgentRunResult].
129
+ """
130
+ graph_run_result = self._graph_run.result
131
+ if graph_run_result is None:
132
+ return None
133
+ return AgentRunResult(
134
+ graph_run_result.output.output,
135
+ graph_run_result.output.tool_name,
136
+ graph_run_result.state,
137
+ self._graph_run.deps.new_message_index,
138
+ self._traceparent(required=False),
139
+ )
140
+
141
+ def __aiter__(
142
+ self,
143
+ ) -> AsyncIterator[_agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]]:
144
+ """Provide async-iteration over the nodes in the agent run."""
145
+ return self
146
+
147
+ async def __anext__(
148
+ self,
149
+ ) -> _agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]:
150
+ """Advance to the next node automatically based on the last returned node."""
151
+ next_node = await self._graph_run.__anext__()
152
+ if _agent_graph.is_agent_node(node=next_node):
153
+ return next_node
154
+ assert isinstance(next_node, End), f'Unexpected node type: {type(next_node)}'
155
+ return next_node
156
+
157
+ async def next(
158
+ self,
159
+ node: _agent_graph.AgentNode[AgentDepsT, OutputDataT],
160
+ ) -> _agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]:
161
+ """Manually drive the agent run by passing in the node you want to run next.
162
+
163
+ This lets you inspect or mutate the node before continuing execution, or skip certain nodes
164
+ under dynamic conditions. The agent run should be stopped when you return an [`End`][pydantic_graph.nodes.End]
165
+ node.
166
+
167
+ Example:
168
+ ```python
169
+ from pydantic_ai import Agent
170
+ from pydantic_graph import End
171
+
172
+ agent = Agent('openai:gpt-4o')
173
+
174
+ async def main():
175
+ async with agent.iter('What is the capital of France?') as agent_run:
176
+ next_node = agent_run.next_node # start with the first node
177
+ nodes = [next_node]
178
+ while not isinstance(next_node, End):
179
+ next_node = await agent_run.next(next_node)
180
+ nodes.append(next_node)
181
+ # Once `next_node` is an End, we've finished:
182
+ print(nodes)
183
+ '''
184
+ [
185
+ UserPromptNode(
186
+ user_prompt='What is the capital of France?',
187
+ instructions=None,
188
+ instructions_functions=[],
189
+ system_prompts=(),
190
+ system_prompt_functions=[],
191
+ system_prompt_dynamic_functions={},
192
+ ),
193
+ ModelRequestNode(
194
+ request=ModelRequest(
195
+ parts=[
196
+ UserPromptPart(
197
+ content='What is the capital of France?',
198
+ timestamp=datetime.datetime(...),
199
+ )
200
+ ]
201
+ )
202
+ ),
203
+ CallToolsNode(
204
+ model_response=ModelResponse(
205
+ parts=[TextPart(content='The capital of France is Paris.')],
206
+ usage=Usage(
207
+ requests=1,
208
+ request_tokens=56,
209
+ response_tokens=7,
210
+ total_tokens=63,
211
+ ),
212
+ model_name='gpt-4o',
213
+ timestamp=datetime.datetime(...),
214
+ )
215
+ ),
216
+ End(data=FinalResult(output='The capital of France is Paris.')),
217
+ ]
218
+ '''
219
+ print('Final result:', agent_run.result.output)
220
+ #> Final result: The capital of France is Paris.
221
+ ```
222
+
223
+ Args:
224
+ node: The node to run next in the graph.
225
+
226
+ Returns:
227
+ The next node returned by the graph logic, or an [`End`][pydantic_graph.nodes.End] node if
228
+ the run has completed.
229
+ """
230
+ # Note: It might be nice to expose a synchronous interface for iteration, but we shouldn't do it
231
+ # on this class, or else IDEs won't warn you if you accidentally use `for` instead of `async for` to iterate.
232
+ next_node = await self._graph_run.next(node)
233
+ if _agent_graph.is_agent_node(next_node):
234
+ return next_node
235
+ assert isinstance(next_node, End), f'Unexpected node type: {type(next_node)}'
236
+ return next_node
237
+
238
+ def usage(self) -> _usage.Usage:
239
+ """Get usage statistics for the run so far, including token usage, model requests, and so on."""
240
+ return self._graph_run.state.usage
241
+
242
+ def __repr__(self) -> str: # pragma: no cover
243
+ result = self._graph_run.result
244
+ result_repr = '<run not finished>' if result is None else repr(result.output)
245
+ return f'<{type(self).__name__} result={result_repr} usage={self.usage()}>'
246
+
247
+
248
+ @dataclasses.dataclass
249
+ class AgentRunResult(Generic[OutputDataT]):
250
+ """The final result of an agent run."""
251
+
252
+ output: OutputDataT
253
+ """The output data from the agent run."""
254
+
255
+ _output_tool_name: str | None = dataclasses.field(repr=False)
256
+ _state: _agent_graph.GraphAgentState = dataclasses.field(repr=False)
257
+ _new_message_index: int = dataclasses.field(repr=False)
258
+ _traceparent_value: str | None = dataclasses.field(repr=False)
259
+
260
+ @overload
261
+ def _traceparent(self, *, required: Literal[False]) -> str | None: ...
262
+ @overload
263
+ def _traceparent(self) -> str: ...
264
+ def _traceparent(self, *, required: bool = True) -> str | None:
265
+ if self._traceparent_value is None and required: # pragma: no cover
266
+ raise AttributeError('No span was created for this agent run')
267
+ return self._traceparent_value
268
+
269
+ def _set_output_tool_return(self, return_content: str) -> list[_messages.ModelMessage]:
270
+ """Set return content for the output tool.
271
+
272
+ Useful if you want to continue the conversation and want to set the response to the output tool call.
273
+ """
274
+ if not self._output_tool_name:
275
+ raise ValueError('Cannot set output tool return content when the return type is `str`.')
276
+
277
+ messages = self._state.message_history
278
+ last_message = messages[-1]
279
+ for idx, part in enumerate(last_message.parts):
280
+ if isinstance(part, _messages.ToolReturnPart) and part.tool_name == self._output_tool_name:
281
+ # Only do deepcopy when we have to modify
282
+ copied_messages = list(messages)
283
+ copied_last = deepcopy(last_message)
284
+ copied_last.parts[idx].content = return_content # type: ignore[misc]
285
+ copied_messages[-1] = copied_last
286
+ return copied_messages
287
+
288
+ raise LookupError(f'No tool call found with tool name {self._output_tool_name!r}.')
289
+
290
+ def all_messages(self, *, output_tool_return_content: str | None = None) -> list[_messages.ModelMessage]:
291
+ """Return the history of _messages.
292
+
293
+ Args:
294
+ output_tool_return_content: The return content of the tool call to set in the last message.
295
+ This provides a convenient way to modify the content of the output tool call if you want to continue
296
+ the conversation and want to set the response to the output tool call. If `None`, the last message will
297
+ not be modified.
298
+
299
+ Returns:
300
+ List of messages.
301
+ """
302
+ if output_tool_return_content is not None:
303
+ return self._set_output_tool_return(output_tool_return_content)
304
+ else:
305
+ return self._state.message_history
306
+
307
+ def all_messages_json(self, *, output_tool_return_content: str | None = None) -> bytes:
308
+ """Return all messages from [`all_messages`][pydantic_ai.agent.AgentRunResult.all_messages] as JSON bytes.
309
+
310
+ Args:
311
+ output_tool_return_content: The return content of the tool call to set in the last message.
312
+ This provides a convenient way to modify the content of the output tool call if you want to continue
313
+ the conversation and want to set the response to the output tool call. If `None`, the last message will
314
+ not be modified.
315
+
316
+ Returns:
317
+ JSON bytes representing the messages.
318
+ """
319
+ return _messages.ModelMessagesTypeAdapter.dump_json(
320
+ self.all_messages(output_tool_return_content=output_tool_return_content)
321
+ )
322
+
323
+ def new_messages(self, *, output_tool_return_content: str | None = None) -> list[_messages.ModelMessage]:
324
+ """Return new messages associated with this run.
325
+
326
+ Messages from older runs are excluded.
327
+
328
+ Args:
329
+ output_tool_return_content: The return content of the tool call to set in the last message.
330
+ This provides a convenient way to modify the content of the output tool call if you want to continue
331
+ the conversation and want to set the response to the output tool call. If `None`, the last message will
332
+ not be modified.
333
+
334
+ Returns:
335
+ List of new messages.
336
+ """
337
+ return self.all_messages(output_tool_return_content=output_tool_return_content)[self._new_message_index :]
338
+
339
+ def new_messages_json(self, *, output_tool_return_content: str | None = None) -> bytes:
340
+ """Return new messages from [`new_messages`][pydantic_ai.agent.AgentRunResult.new_messages] as JSON bytes.
341
+
342
+ Args:
343
+ output_tool_return_content: The return content of the tool call to set in the last message.
344
+ This provides a convenient way to modify the content of the output tool call if you want to continue
345
+ the conversation and want to set the response to the output tool call. If `None`, the last message will
346
+ not be modified.
347
+
348
+ Returns:
349
+ JSON bytes representing the new messages.
350
+ """
351
+ return _messages.ModelMessagesTypeAdapter.dump_json(
352
+ self.new_messages(output_tool_return_content=output_tool_return_content)
353
+ )
354
+
355
+ def usage(self) -> _usage.Usage:
356
+ """Return the usage of the whole run."""
357
+ return self._state.usage
pydantic_ai/tools.py CHANGED
@@ -118,7 +118,6 @@ agent = Agent('openai:gpt-4o', prepare_tools=turn_on_strict_if_openai)
118
118
  Usage `ToolsPrepareFunc[AgentDepsT]`.
119
119
  """
120
120
 
121
-
122
121
  DocstringFormat = Literal['google', 'numpy', 'sphinx', 'auto']
123
122
  """Supported docstring formats.
124
123
 
@@ -1,3 +1,4 @@
1
+ from ._dynamic import ToolsetFunc
1
2
  from .abstract import AbstractToolset, ToolsetTool
2
3
  from .combined import CombinedToolset
3
4
  from .deferred import DeferredToolset
@@ -10,6 +11,7 @@ from .wrapper import WrapperToolset
10
11
 
11
12
  __all__ = (
12
13
  'AbstractToolset',
14
+ 'ToolsetFunc',
13
15
  'ToolsetTool',
14
16
  'CombinedToolset',
15
17
  'DeferredToolset',
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import Awaitable
5
+ from dataclasses import dataclass, replace
6
+ from typing import Any, Callable, Union
7
+
8
+ from typing_extensions import Self, TypeAlias
9
+
10
+ from .._run_context import AgentDepsT, RunContext
11
+ from .abstract import AbstractToolset, ToolsetTool
12
+
13
+ ToolsetFunc: TypeAlias = Callable[
14
+ [RunContext[AgentDepsT]],
15
+ Union[AbstractToolset[AgentDepsT], None, Awaitable[Union[AbstractToolset[AgentDepsT], None]]],
16
+ ]
17
+ """A sync/async function which takes a run context and returns a toolset."""
18
+
19
+
20
+ @dataclass
21
+ class DynamicToolset(AbstractToolset[AgentDepsT]):
22
+ """A toolset that dynamically builds a toolset using a function that takes the run context.
23
+
24
+ It should only be used during a single agent run as it stores the generated toolset.
25
+ To use it multiple times, copy it using `dataclasses.replace`.
26
+ """
27
+
28
+ toolset_func: ToolsetFunc[AgentDepsT]
29
+ per_run_step: bool = True
30
+
31
+ _toolset: AbstractToolset[AgentDepsT] | None = None
32
+ _run_step: int | None = None
33
+
34
+ @property
35
+ def id(self) -> str | None:
36
+ return None # pragma: no cover
37
+
38
+ async def __aenter__(self) -> Self:
39
+ return self
40
+
41
+ async def __aexit__(self, *args: Any) -> bool | None:
42
+ try:
43
+ if self._toolset is not None:
44
+ return await self._toolset.__aexit__(*args)
45
+ finally:
46
+ self._toolset = None
47
+ self._run_step = None
48
+
49
+ async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]:
50
+ if self._toolset is None or (self.per_run_step and ctx.run_step != self._run_step):
51
+ if self._toolset is not None:
52
+ await self._toolset.__aexit__()
53
+
54
+ toolset = self.toolset_func(ctx)
55
+ if inspect.isawaitable(toolset):
56
+ toolset = await toolset
57
+
58
+ if toolset is not None:
59
+ await toolset.__aenter__()
60
+
61
+ self._toolset = toolset
62
+ self._run_step = ctx.run_step
63
+
64
+ if self._toolset is None:
65
+ return {}
66
+
67
+ return await self._toolset.get_tools(ctx)
68
+
69
+ async def call_tool(
70
+ self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
71
+ ) -> Any:
72
+ assert self._toolset is not None
73
+ return await self._toolset.call_tool(name, tool_args, ctx, tool)
74
+
75
+ def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], None]) -> None:
76
+ if self._toolset is None:
77
+ super().apply(visitor)
78
+ else:
79
+ self._toolset.apply(visitor)
80
+
81
+ def visit_and_replace(
82
+ self, visitor: Callable[[AbstractToolset[AgentDepsT]], AbstractToolset[AgentDepsT]]
83
+ ) -> AbstractToolset[AgentDepsT]:
84
+ if self._toolset is None:
85
+ return super().visit_and_replace(visitor)
86
+ else:
87
+ return replace(self, _toolset=self._toolset.visit_and_replace(visitor))
@@ -70,9 +70,23 @@ class AbstractToolset(ABC, Generic[AgentDepsT]):
70
70
  """
71
71
 
72
72
  @property
73
- def name(self) -> str:
73
+ @abstractmethod
74
+ def id(self) -> str | None:
75
+ """An ID for the toolset that is unique among all toolsets registered with the same agent.
76
+
77
+ If you're implementing a concrete implementation that users can instantiate more than once, you should let them optionally pass a custom ID to the constructor and return that here.
78
+
79
+ 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.
80
+ """
81
+ raise NotImplementedError()
82
+
83
+ @property
84
+ def label(self) -> str:
74
85
  """The name of the toolset for use in error messages."""
75
- return self.__class__.__name__.replace('Toolset', ' toolset')
86
+ label = self.__class__.__name__
87
+ if self.id: # pragma: no branch
88
+ label += f' {self.id!r}'
89
+ return label
76
90
 
77
91
  @property
78
92
  def tool_name_conflict_hint(self) -> str:
@@ -113,9 +127,15 @@ class AbstractToolset(ABC, Generic[AgentDepsT]):
113
127
  raise NotImplementedError()
114
128
 
115
129
  def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], None]) -> None:
116
- """Run a visitor function on all concrete toolsets that are not wrappers (i.e. they implement their own tool listing and calling)."""
130
+ """Run a visitor function on all "leaf" toolsets (i.e. those that implement their own tool listing and calling)."""
117
131
  visitor(self)
118
132
 
133
+ def visit_and_replace(
134
+ self, visitor: Callable[[AbstractToolset[AgentDepsT]], AbstractToolset[AgentDepsT]]
135
+ ) -> AbstractToolset[AgentDepsT]:
136
+ """Run a visitor function on all "leaf" toolsets (i.e. those that implement their own tool listing and calling) and replace them in the hierarchy with the result of the function."""
137
+ return visitor(self)
138
+
119
139
  def filtered(
120
140
  self, filter_func: Callable[[RunContext[AgentDepsT], ToolDefinition], bool]
121
141
  ) -> FilteredToolset[AgentDepsT]: