pydantic-ai-slim 0.7.1__py3-none-any.whl → 0.7.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.

@@ -23,7 +23,7 @@ from pydantic_graph.nodes import End, NodeRunEndT
23
23
  from . import _output, _system_prompt, exceptions, messages as _messages, models, result, usage as _usage
24
24
  from .exceptions import ToolRetryError
25
25
  from .output import OutputDataT, OutputSpec
26
- from .settings import ModelSettings, merge_model_settings
26
+ from .settings import ModelSettings
27
27
  from .tools import RunContext, ToolDefinition, ToolKind
28
28
 
29
29
  if TYPE_CHECKING:
@@ -158,28 +158,7 @@ class UserPromptNode(AgentNode[DepsT, NodeRunEndT]):
158
158
 
159
159
  async def run(
160
160
  self, ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]]
161
- ) -> ModelRequestNode[DepsT, NodeRunEndT]:
162
- return ModelRequestNode[DepsT, NodeRunEndT](request=await self._get_first_message(ctx))
163
-
164
- async def _get_first_message(
165
- self, ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]]
166
- ) -> _messages.ModelRequest:
167
- run_context = build_run_context(ctx)
168
- history, next_message = await self._prepare_messages(
169
- self.user_prompt, ctx.state.message_history, ctx.deps.get_instructions, run_context
170
- )
171
- ctx.state.message_history = history
172
- run_context.messages = history
173
-
174
- return next_message
175
-
176
- async def _prepare_messages(
177
- self,
178
- user_prompt: str | Sequence[_messages.UserContent] | None,
179
- message_history: list[_messages.ModelMessage] | None,
180
- get_instructions: Callable[[RunContext[DepsT]], Awaitable[str | None]],
181
- run_context: RunContext[DepsT],
182
- ) -> tuple[list[_messages.ModelMessage], _messages.ModelRequest]:
161
+ ) -> Union[ModelRequestNode[DepsT, NodeRunEndT], CallToolsNode[DepsT, NodeRunEndT]]: # noqa UP007
183
162
  try:
184
163
  ctx_messages = get_captured_run_messages()
185
164
  except LookupError:
@@ -191,29 +170,48 @@ class UserPromptNode(AgentNode[DepsT, NodeRunEndT]):
191
170
  messages = ctx_messages.messages
192
171
  ctx_messages.used = True
193
172
 
173
+ # Add message history to the `capture_run_messages` list, which will be empty at this point
174
+ messages.extend(ctx.state.message_history)
175
+ # Use the `capture_run_messages` list as the message history so that new messages are added to it
176
+ ctx.state.message_history = messages
177
+
178
+ run_context = build_run_context(ctx)
179
+
194
180
  parts: list[_messages.ModelRequestPart] = []
195
- instructions = await get_instructions(run_context)
196
- if message_history:
197
- # Shallow copy messages
198
- messages.extend(message_history)
181
+ if messages:
199
182
  # Reevaluate any dynamic system prompt parts
200
183
  await self._reevaluate_dynamic_prompts(messages, run_context)
201
184
  else:
202
185
  parts.extend(await self._sys_parts(run_context))
203
186
 
204
- if user_prompt is not None:
205
- parts.append(_messages.UserPromptPart(user_prompt))
206
- elif (
207
- len(parts) == 0
208
- and message_history
209
- and (last_message := message_history[-1])
210
- and isinstance(last_message, _messages.ModelRequest)
211
- ):
212
- # Drop last message that came from history and reuse its parts
213
- messages.pop()
214
- parts.extend(last_message.parts)
187
+ if messages and (last_message := messages[-1]):
188
+ if isinstance(last_message, _messages.ModelRequest) and self.user_prompt is None:
189
+ # Drop last message from history and reuse its parts
190
+ messages.pop()
191
+ parts.extend(last_message.parts)
192
+ elif isinstance(last_message, _messages.ModelResponse):
193
+ if self.user_prompt is None:
194
+ # `CallToolsNode` requires the tool manager to be prepared for the run step
195
+ # This will raise errors for any tool name conflicts
196
+ ctx.deps.tool_manager = await ctx.deps.tool_manager.for_run_step(run_context)
197
+
198
+ # Skip ModelRequestNode and go directly to CallToolsNode
199
+ return CallToolsNode[DepsT, NodeRunEndT](model_response=last_message)
200
+ elif any(isinstance(part, _messages.ToolCallPart) for part in last_message.parts):
201
+ raise exceptions.UserError(
202
+ 'Cannot provide a new user prompt when the message history ends with '
203
+ 'a model response containing unprocessed tool calls. Either process the '
204
+ 'tool calls first (by calling `iter` with `user_prompt=None`) or append a '
205
+ '`ModelRequest` with `ToolResultPart`s.'
206
+ )
207
+
208
+ if self.user_prompt is not None:
209
+ parts.append(_messages.UserPromptPart(self.user_prompt))
210
+
211
+ instructions = await ctx.deps.get_instructions(run_context)
212
+ next_message = _messages.ModelRequest(parts, instructions=instructions)
215
213
 
216
- return messages, _messages.ModelRequest(parts, instructions=instructions)
214
+ return ModelRequestNode[DepsT, NodeRunEndT](request=next_message)
217
215
 
218
216
  async def _reevaluate_dynamic_prompts(
219
217
  self, messages: list[_messages.ModelMessage], run_context: RunContext[DepsT]
@@ -250,9 +248,6 @@ async def _prepare_request_parameters(
250
248
  ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]],
251
249
  ) -> models.ModelRequestParameters:
252
250
  """Build tools and create an agent model."""
253
- run_context = build_run_context(ctx)
254
- ctx.deps.tool_manager = await ctx.deps.tool_manager.for_run_step(run_context)
255
-
256
251
  output_schema = ctx.deps.output_schema
257
252
  output_object = None
258
253
  if isinstance(output_schema, _output.NativeOutputSchema):
@@ -355,21 +350,21 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
355
350
 
356
351
  run_context = build_run_context(ctx)
357
352
 
358
- model_settings = merge_model_settings(ctx.deps.model_settings, None)
353
+ # This will raise errors for any tool name conflicts
354
+ ctx.deps.tool_manager = await ctx.deps.tool_manager.for_run_step(run_context)
355
+
356
+ message_history = await _process_message_history(ctx.state, ctx.deps.history_processors, run_context)
359
357
 
360
358
  model_request_parameters = await _prepare_request_parameters(ctx)
361
359
  model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
362
360
 
363
- message_history = await _process_message_history(ctx.state, ctx.deps.history_processors, run_context)
364
-
361
+ model_settings = ctx.deps.model_settings
365
362
  usage = ctx.state.usage
366
363
  if ctx.deps.usage_limits.count_tokens_before_request:
367
364
  # Copy to avoid modifying the original usage object with the counted usage
368
365
  usage = dataclasses.replace(usage)
369
366
 
370
- counted_usage = await ctx.deps.model.count_tokens(
371
- message_history, ctx.deps.model_settings, model_request_parameters
372
- )
367
+ counted_usage = await ctx.deps.model.count_tokens(message_history, model_settings, model_request_parameters)
373
368
  usage.incr(counted_usage)
374
369
 
375
370
  ctx.deps.usage_limits.check_before_request(usage)
@@ -432,9 +427,11 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
432
427
  if self._events_iterator is None:
433
428
  # Ensure that the stream is only run once
434
429
 
435
- async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]:
430
+ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa: C901
436
431
  texts: list[str] = []
437
432
  tool_calls: list[_messages.ToolCallPart] = []
433
+ thinking_parts: list[_messages.ThinkingPart] = []
434
+
438
435
  for part in self.model_response.parts:
439
436
  if isinstance(part, _messages.TextPart):
440
437
  # ignore empty content for text parts, see #437
@@ -447,11 +444,7 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
447
444
  elif isinstance(part, _messages.BuiltinToolReturnPart):
448
445
  yield _messages.BuiltinToolResultEvent(part)
449
446
  elif isinstance(part, _messages.ThinkingPart):
450
- # We don't need to do anything with thinking parts in this tool-calling node.
451
- # We need to handle text parts in case there are no tool calls and/or the desired output comes
452
- # from the text, but thinking parts should not directly influence the execution of tools or
453
- # determination of the next node of graph execution here.
454
- pass
447
+ thinking_parts.append(part)
455
448
  else:
456
449
  assert_never(part)
457
450
 
@@ -465,8 +458,18 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
465
458
  elif texts:
466
459
  # No events are emitted during the handling of text responses, so we don't need to yield anything
467
460
  self._next_node = await self._handle_text_response(ctx, texts)
461
+ elif thinking_parts:
462
+ # handle thinking-only responses (responses that contain only ThinkingPart instances)
463
+ # this can happen with models that support thinking mode when they don't provide
464
+ # actionable output alongside their thinking content.
465
+ self._next_node = ModelRequestNode[DepsT, NodeRunEndT](
466
+ _messages.ModelRequest(
467
+ parts=[_messages.RetryPromptPart('Responses without text or tool calls are not permitted.')]
468
+ )
469
+ )
468
470
  else:
469
- # we've got an empty response, this sometimes happens with anthropic (and perhaps other models)
471
+ # we got an empty response with no tool calls, text, or thinking
472
+ # this sometimes happens with anthropic (and perhaps other models)
470
473
  # when the model has already returned text along side tool calls
471
474
  # in this scenario, if text responses are allowed, we return text from the most recent model
472
475
  # response, if any
@@ -72,6 +72,7 @@ class ModelResponsePartsManager:
72
72
  vendor_part_id: VendorId | None,
73
73
  content: str,
74
74
  thinking_tags: tuple[str, str] | None = None,
75
+ ignore_leading_whitespace: bool = False,
75
76
  ) -> ModelResponseStreamEvent | None:
76
77
  """Handle incoming text content, creating or updating a TextPart in the manager as appropriate.
77
78
 
@@ -85,6 +86,7 @@ class ModelResponsePartsManager:
85
86
  a TextPart.
86
87
  content: The text content to append to the appropriate TextPart.
87
88
  thinking_tags: If provided, will handle content between the thinking tags as thinking parts.
89
+ ignore_leading_whitespace: If True, will ignore leading whitespace in the content.
88
90
 
89
91
  Returns:
90
92
  - A `PartStartEvent` if a new part was created.
@@ -128,10 +130,9 @@ class ModelResponsePartsManager:
128
130
  return self.handle_thinking_delta(vendor_part_id=vendor_part_id, content='')
129
131
 
130
132
  if existing_text_part_and_index is None:
131
- # If the first text delta is all whitespace, don't emit a new part yet.
132
- # This is a workaround for models that emit `<think>\n</think>\n\n` ahead of tool calls (e.g. Ollama + Qwen3),
133
- # which we don't want to end up treating as a final result.
134
- if content.isspace():
133
+ # This is a workaround for models that emit `<think>\n</think>\n\n` or an empty text part ahead of tool calls (e.g. Ollama + Qwen3),
134
+ # which we don't want to end up treating as a final result when using `run_stream` with `str` a valid `output_type`.
135
+ if ignore_leading_whitespace and (len(content) == 0 or content.isspace()):
135
136
  return None
136
137
 
137
138
  # There is no existing text part that should be updated, so create a new one
@@ -5,6 +5,7 @@ from collections.abc import Iterable
5
5
  from dataclasses import dataclass, field, replace
6
6
  from typing import Any, Generic
7
7
 
8
+ from opentelemetry.trace import Tracer
8
9
  from pydantic import ValidationError
9
10
  from typing_extensions import assert_never
10
11
 
@@ -21,41 +22,46 @@ from .toolsets.abstract import AbstractToolset, ToolsetTool
21
22
  class ToolManager(Generic[AgentDepsT]):
22
23
  """Manages tools for an agent run step. It caches the agent run's toolset's tool definitions and handles calling tools and retries."""
23
24
 
24
- ctx: RunContext[AgentDepsT]
25
- """The agent run context for a specific run step."""
26
25
  toolset: AbstractToolset[AgentDepsT]
27
26
  """The toolset that provides the tools for this run step."""
28
- tools: dict[str, ToolsetTool[AgentDepsT]]
27
+ ctx: RunContext[AgentDepsT] | None = None
28
+ """The agent run context for a specific run step."""
29
+ tools: dict[str, ToolsetTool[AgentDepsT]] | None = None
29
30
  """The cached tools for this run step."""
30
31
  failed_tools: set[str] = field(default_factory=set)
31
32
  """Names of tools that failed in this run step."""
32
33
 
33
- @classmethod
34
- async def build(cls, toolset: AbstractToolset[AgentDepsT], ctx: RunContext[AgentDepsT]) -> ToolManager[AgentDepsT]:
35
- """Build a new tool manager for a specific run step."""
36
- return cls(
37
- ctx=ctx,
38
- toolset=toolset,
39
- tools=await toolset.get_tools(ctx),
40
- )
41
-
42
34
  async def for_run_step(self, ctx: RunContext[AgentDepsT]) -> ToolManager[AgentDepsT]:
43
35
  """Build a new tool manager for the next run step, carrying over the retries from the current run step."""
44
- if ctx.run_step == self.ctx.run_step:
45
- return self
46
-
47
- retries = {
48
- failed_tool_name: self.ctx.retries.get(failed_tool_name, 0) + 1 for failed_tool_name in self.failed_tools
49
- }
50
- return await self.__class__.build(self.toolset, replace(ctx, retries=retries))
36
+ if self.ctx is not None:
37
+ if ctx.run_step == self.ctx.run_step:
38
+ return self
39
+
40
+ retries = {
41
+ failed_tool_name: self.ctx.retries.get(failed_tool_name, 0) + 1
42
+ for failed_tool_name in self.failed_tools
43
+ }
44
+ ctx = replace(ctx, retries=retries)
45
+
46
+ return self.__class__(
47
+ toolset=self.toolset,
48
+ ctx=ctx,
49
+ tools=await self.toolset.get_tools(ctx),
50
+ )
51
51
 
52
52
  @property
53
53
  def tool_defs(self) -> list[ToolDefinition]:
54
54
  """The tool definitions for the tools in this tool manager."""
55
+ if self.tools is None:
56
+ raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
57
+
55
58
  return [tool.tool_def for tool in self.tools.values()]
56
59
 
57
60
  def get_tool_def(self, name: str) -> ToolDefinition | None:
58
61
  """Get the tool definition for a given tool name, or `None` if the tool is unknown."""
62
+ if self.tools is None:
63
+ raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
64
+
59
65
  try:
60
66
  return self.tools[name].tool_def
61
67
  except KeyError:
@@ -71,15 +77,25 @@ class ToolManager(Generic[AgentDepsT]):
71
77
  allow_partial: Whether to allow partial validation of the tool arguments.
72
78
  wrap_validation_errors: Whether to wrap validation errors in a retry prompt part.
73
79
  """
80
+ if self.tools is None or self.ctx is None:
81
+ raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
82
+
74
83
  if (tool := self.tools.get(call.tool_name)) and tool.tool_def.kind == 'output':
75
84
  # Output tool calls are not traced
76
85
  return await self._call_tool(call, allow_partial, wrap_validation_errors)
77
86
  else:
78
- return await self._call_tool_traced(call, allow_partial, wrap_validation_errors)
87
+ return await self._call_tool_traced(
88
+ call,
89
+ allow_partial,
90
+ wrap_validation_errors,
91
+ self.ctx.tracer,
92
+ self.ctx.trace_include_content,
93
+ )
94
+
95
+ async def _call_tool(self, call: ToolCallPart, allow_partial: bool, wrap_validation_errors: bool) -> Any:
96
+ if self.tools is None or self.ctx is None:
97
+ raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
79
98
 
80
- async def _call_tool(
81
- self, call: ToolCallPart, allow_partial: bool = False, wrap_validation_errors: bool = True
82
- ) -> Any:
83
99
  name = call.tool_name
84
100
  tool = self.tools.get(name)
85
101
  try:
@@ -137,14 +153,19 @@ class ToolManager(Generic[AgentDepsT]):
137
153
  raise e
138
154
 
139
155
  async def _call_tool_traced(
140
- self, call: ToolCallPart, allow_partial: bool = False, wrap_validation_errors: bool = True
156
+ self,
157
+ call: ToolCallPart,
158
+ allow_partial: bool,
159
+ wrap_validation_errors: bool,
160
+ tracer: Tracer,
161
+ include_content: bool = False,
141
162
  ) -> Any:
142
163
  """See <https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span>."""
143
164
  span_attributes = {
144
165
  'gen_ai.tool.name': call.tool_name,
145
166
  # NOTE: this means `gen_ai.tool.call.id` will be included even if it was generated by pydantic-ai
146
167
  'gen_ai.tool.call.id': call.tool_call_id,
147
- **({'tool_arguments': call.args_as_json_str()} if self.ctx.trace_include_content else {}),
168
+ **({'tool_arguments': call.args_as_json_str()} if include_content else {}),
148
169
  'logfire.msg': f'running tool: {call.tool_name}',
149
170
  # add the JSON schema so these attributes are formatted nicely in Logfire
150
171
  'logfire.json_schema': json.dumps(
@@ -156,7 +177,7 @@ class ToolManager(Generic[AgentDepsT]):
156
177
  'tool_arguments': {'type': 'object'},
157
178
  'tool_response': {'type': 'object'},
158
179
  }
159
- if self.ctx.trace_include_content
180
+ if include_content
160
181
  else {}
161
182
  ),
162
183
  'gen_ai.tool.name': {},
@@ -165,16 +186,16 @@ class ToolManager(Generic[AgentDepsT]):
165
186
  }
166
187
  ),
167
188
  }
168
- with self.ctx.tracer.start_as_current_span('running tool', attributes=span_attributes) as span:
189
+ with tracer.start_as_current_span('running tool', attributes=span_attributes) as span:
169
190
  try:
170
191
  tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors)
171
192
  except ToolRetryError as e:
172
193
  part = e.tool_retry
173
- if self.ctx.trace_include_content and span.is_recording():
194
+ if include_content and span.is_recording():
174
195
  span.set_attribute('tool_response', part.model_response())
175
196
  raise e
176
197
 
177
- if self.ctx.trace_include_content and span.is_recording():
198
+ if include_content and span.is_recording():
178
199
  span.set_attribute(
179
200
  'tool_response',
180
201
  tool_result
@@ -566,6 +566,8 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
566
566
  if output_toolset:
567
567
  output_toolset.max_retries = self._max_result_retries
568
568
  output_toolset.output_validators = output_validators
569
+ toolset = self._get_toolset(output_toolset=output_toolset, additional_toolsets=toolsets)
570
+ tool_manager = ToolManager[AgentDepsT](toolset)
569
571
 
570
572
  # Build the graph
571
573
  graph: Graph[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any], FinalResult[Any]] = (
@@ -581,6 +583,27 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
581
583
  run_step=0,
582
584
  )
583
585
 
586
+ # Merge model settings in order of precedence: run > agent > model
587
+ merged_settings = merge_model_settings(model_used.settings, self.model_settings)
588
+ model_settings = merge_model_settings(merged_settings, model_settings)
589
+ usage_limits = usage_limits or _usage.UsageLimits()
590
+
591
+ async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
592
+ parts = [
593
+ self._instructions,
594
+ *[await func.run(run_context) for func in self._instructions_functions],
595
+ ]
596
+
597
+ model_profile = model_used.profile
598
+ if isinstance(output_schema, _output.PromptedOutputSchema):
599
+ instructions = output_schema.instructions(model_profile.prompted_output_template)
600
+ parts.append(instructions)
601
+
602
+ parts = [p for p in parts if p]
603
+ if not parts:
604
+ return None
605
+ return '\n\n'.join(parts).strip()
606
+
584
607
  if isinstance(model_used, InstrumentedModel):
585
608
  instrumentation_settings = model_used.instrumentation_settings
586
609
  tracer = model_used.instrumentation_settings.tracer
@@ -588,81 +611,45 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
588
611
  instrumentation_settings = None
589
612
  tracer = NoOpTracer()
590
613
 
591
- run_context = RunContext[AgentDepsT](
592
- deps=deps,
593
- model=model_used,
594
- usage=usage,
614
+ graph_deps = _agent_graph.GraphAgentDeps[AgentDepsT, RunOutputDataT](
615
+ user_deps=deps,
595
616
  prompt=user_prompt,
596
- messages=state.message_history,
617
+ new_message_index=new_message_index,
618
+ model=model_used,
619
+ model_settings=model_settings,
620
+ usage_limits=usage_limits,
621
+ max_result_retries=self._max_result_retries,
622
+ end_strategy=self.end_strategy,
623
+ output_schema=output_schema,
624
+ output_validators=output_validators,
625
+ history_processors=self.history_processors,
626
+ builtin_tools=list(self._builtin_tools),
627
+ tool_manager=tool_manager,
597
628
  tracer=tracer,
598
- trace_include_content=instrumentation_settings is not None and instrumentation_settings.include_content,
599
- run_step=state.run_step,
629
+ get_instructions=get_instructions,
630
+ instrumentation_settings=instrumentation_settings,
631
+ )
632
+ start_node = _agent_graph.UserPromptNode[AgentDepsT](
633
+ user_prompt=user_prompt,
634
+ instructions=self._instructions,
635
+ instructions_functions=self._instructions_functions,
636
+ system_prompts=self._system_prompts,
637
+ system_prompt_functions=self._system_prompt_functions,
638
+ system_prompt_dynamic_functions=self._system_prompt_dynamic_functions,
600
639
  )
601
640
 
602
- toolset = self._get_toolset(output_toolset=output_toolset, additional_toolsets=toolsets)
603
-
604
- async with toolset:
605
- # This will raise errors for any name conflicts
606
- tool_manager = await ToolManager[AgentDepsT].build(toolset, run_context)
607
-
608
- # Merge model settings in order of precedence: run > agent > model
609
- merged_settings = merge_model_settings(model_used.settings, self.model_settings)
610
- model_settings = merge_model_settings(merged_settings, model_settings)
611
- usage_limits = usage_limits or _usage.UsageLimits()
612
- agent_name = self.name or 'agent'
613
- run_span = tracer.start_span(
614
- 'agent run',
615
- attributes={
616
- 'model_name': model_used.model_name if model_used else 'no-model',
617
- 'agent_name': agent_name,
618
- 'logfire.msg': f'{agent_name} run',
619
- },
620
- )
621
-
622
- async def get_instructions(run_context: RunContext[AgentDepsT]) -> str | None:
623
- parts = [
624
- self._instructions,
625
- *[await func.run(run_context) for func in self._instructions_functions],
626
- ]
627
-
628
- model_profile = model_used.profile
629
- if isinstance(output_schema, _output.PromptedOutputSchema):
630
- instructions = output_schema.instructions(model_profile.prompted_output_template)
631
- parts.append(instructions)
632
-
633
- parts = [p for p in parts if p]
634
- if not parts:
635
- return None
636
- return '\n\n'.join(parts).strip()
637
-
638
- graph_deps = _agent_graph.GraphAgentDeps[AgentDepsT, RunOutputDataT](
639
- user_deps=deps,
640
- prompt=user_prompt,
641
- new_message_index=new_message_index,
642
- model=model_used,
643
- model_settings=model_settings,
644
- usage_limits=usage_limits,
645
- max_result_retries=self._max_result_retries,
646
- end_strategy=self.end_strategy,
647
- output_schema=output_schema,
648
- output_validators=output_validators,
649
- history_processors=self.history_processors,
650
- builtin_tools=list(self._builtin_tools),
651
- tool_manager=tool_manager,
652
- tracer=tracer,
653
- get_instructions=get_instructions,
654
- instrumentation_settings=instrumentation_settings,
655
- )
656
- start_node = _agent_graph.UserPromptNode[AgentDepsT](
657
- user_prompt=user_prompt,
658
- instructions=self._instructions,
659
- instructions_functions=self._instructions_functions,
660
- system_prompts=self._system_prompts,
661
- system_prompt_functions=self._system_prompt_functions,
662
- system_prompt_dynamic_functions=self._system_prompt_dynamic_functions,
663
- )
641
+ agent_name = self.name or 'agent'
642
+ run_span = tracer.start_span(
643
+ 'agent run',
644
+ attributes={
645
+ 'model_name': model_used.model_name if model_used else 'no-model',
646
+ 'agent_name': agent_name,
647
+ 'logfire.msg': f'{agent_name} run',
648
+ },
649
+ )
664
650
 
665
- try:
651
+ try:
652
+ async with toolset:
666
653
  async with graph.iter(
667
654
  start_node,
668
655
  state=state,
@@ -682,12 +669,12 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
682
669
  else json.dumps(InstrumentedModel.serialize_any(final_result.output))
683
670
  ),
684
671
  )
672
+ finally:
673
+ try:
674
+ if instrumentation_settings and run_span.is_recording():
675
+ run_span.set_attributes(self._run_span_end_attributes(state, usage, instrumentation_settings))
685
676
  finally:
686
- try:
687
- if instrumentation_settings and run_span.is_recording():
688
- run_span.set_attributes(self._run_span_end_attributes(state, usage, instrumentation_settings))
689
- finally:
690
- run_span.end()
677
+ run_span.end()
691
678
 
692
679
  def _run_span_end_attributes(
693
680
  self, state: _agent_graph.GraphAgentState, usage: _usage.Usage, settings: InstrumentationSettings
@@ -8,14 +8,6 @@ from dataclasses import dataclass, field
8
8
  from datetime import datetime, timezone
9
9
  from typing import Any, Literal, Union, cast, overload
10
10
 
11
- from anthropic.types.beta import (
12
- BetaCitationsDelta,
13
- BetaCodeExecutionToolResultBlock,
14
- BetaCodeExecutionToolResultBlockParam,
15
- BetaInputJSONDelta,
16
- BetaServerToolUseBlockParam,
17
- BetaWebSearchToolResultBlockParam,
18
- )
19
11
  from typing_extensions import assert_never
20
12
 
21
13
  from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool
@@ -47,24 +39,21 @@ from ..profiles import ModelProfileSpec
47
39
  from ..providers import Provider, infer_provider
48
40
  from ..settings import ModelSettings
49
41
  from ..tools import ToolDefinition
50
- from . import (
51
- Model,
52
- ModelRequestParameters,
53
- StreamedResponse,
54
- check_allow_model_requests,
55
- download_item,
56
- get_user_agent,
57
- )
42
+ from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent
58
43
 
59
44
  try:
60
45
  from anthropic import NOT_GIVEN, APIStatusError, AsyncAnthropic, AsyncStream
61
46
  from anthropic.types.beta import (
62
47
  BetaBase64PDFBlockParam,
63
48
  BetaBase64PDFSourceParam,
49
+ BetaCitationsDelta,
64
50
  BetaCodeExecutionTool20250522Param,
51
+ BetaCodeExecutionToolResultBlock,
52
+ BetaCodeExecutionToolResultBlockParam,
65
53
  BetaContentBlock,
66
54
  BetaContentBlockParam,
67
55
  BetaImageBlockParam,
56
+ BetaInputJSONDelta,
68
57
  BetaMessage,
69
58
  BetaMessageParam,
70
59
  BetaMetadataParam,
@@ -78,6 +67,7 @@ try:
78
67
  BetaRawMessageStreamEvent,
79
68
  BetaRedactedThinkingBlock,
80
69
  BetaServerToolUseBlock,
70
+ BetaServerToolUseBlockParam,
81
71
  BetaSignatureDelta,
82
72
  BetaTextBlock,
83
73
  BetaTextBlockParam,
@@ -94,6 +84,7 @@ try:
94
84
  BetaToolUseBlockParam,
95
85
  BetaWebSearchTool20250305Param,
96
86
  BetaWebSearchToolResultBlock,
87
+ BetaWebSearchToolResultBlockParam,
97
88
  )
98
89
  from anthropic.types.beta.beta_web_search_tool_20250305_param import UserLocation
99
90
  from anthropic.types.model_param import ModelParam
@@ -246,7 +237,9 @@ class AnthropicModel(Model):
246
237
  ) -> BetaMessage | AsyncStream[BetaRawMessageStreamEvent]:
247
238
  # standalone function to make it easier to override
248
239
  tools = self._get_tools(model_request_parameters)
249
- tools += self._get_builtin_tools(model_request_parameters)
240
+ builtin_tools, tool_headers = self._get_builtin_tools(model_request_parameters)
241
+ tools += builtin_tools
242
+
250
243
  tool_choice: BetaToolChoiceParam | None
251
244
 
252
245
  if not tools:
@@ -264,8 +257,10 @@ class AnthropicModel(Model):
264
257
 
265
258
  try:
266
259
  extra_headers = model_settings.get('extra_headers', {})
260
+ for k, v in tool_headers.items():
261
+ extra_headers.setdefault(k, v)
267
262
  extra_headers.setdefault('User-Agent', get_user_agent())
268
- extra_headers.setdefault('anthropic-beta', 'code-execution-2025-05-22')
263
+
269
264
  return await self.client.beta.messages.create(
270
265
  max_tokens=model_settings.get('max_tokens', 4096),
271
266
  system=system_prompt or NOT_GIVEN,
@@ -352,8 +347,11 @@ class AnthropicModel(Model):
352
347
  def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolParam]:
353
348
  return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()]
354
349
 
355
- def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolUnionParam]:
350
+ def _get_builtin_tools(
351
+ self, model_request_parameters: ModelRequestParameters
352
+ ) -> tuple[list[BetaToolUnionParam], dict[str, str]]:
356
353
  tools: list[BetaToolUnionParam] = []
354
+ extra_headers: dict[str, str] = {}
357
355
  for tool in model_request_parameters.builtin_tools:
358
356
  if isinstance(tool, WebSearchTool):
359
357
  user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None
@@ -361,18 +359,20 @@ class AnthropicModel(Model):
361
359
  BetaWebSearchTool20250305Param(
362
360
  name='web_search',
363
361
  type='web_search_20250305',
362
+ max_uses=tool.max_uses,
364
363
  allowed_domains=tool.allowed_domains,
365
364
  blocked_domains=tool.blocked_domains,
366
365
  user_location=user_location,
367
366
  )
368
367
  )
369
368
  elif isinstance(tool, CodeExecutionTool): # pragma: no branch
369
+ extra_headers['anthropic-beta'] = 'code-execution-2025-05-22'
370
370
  tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522'))
371
371
  else: # pragma: no cover
372
372
  raise UserError(
373
373
  f'`{tool.__class__.__name__}` is not supported by `AnthropicModel`. If it should be, please file an issue.'
374
374
  )
375
- return tools
375
+ return tools, extra_headers
376
376
 
377
377
  async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901
378
378
  """Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`."""
@@ -648,7 +648,7 @@ class BedrockStreamedResponse(StreamedResponse):
648
648
  )
649
649
  if 'text' in delta:
650
650
  maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=index, content=delta['text'])
651
- if maybe_event is not None:
651
+ if maybe_event is not None: # pragma: no branch
652
652
  yield maybe_event
653
653
  if 'toolUse' in delta:
654
654
  tool_use = delta['toolUse']
@@ -457,6 +457,7 @@ class GroqStreamedResponse(StreamedResponse):
457
457
  vendor_part_id='content',
458
458
  content=content,
459
459
  thinking_tags=self._model_profile.thinking_tags,
460
+ ignore_leading_whitespace=self._model_profile.ignore_streamed_leading_whitespace,
460
461
  )
461
462
  if maybe_event is not None: # pragma: no branch
462
463
  yield maybe_event
@@ -35,7 +35,7 @@ from ..messages import (
35
35
  UserPromptPart,
36
36
  VideoUrl,
37
37
  )
38
- from ..profiles import ModelProfile
38
+ from ..profiles import ModelProfile, ModelProfileSpec
39
39
  from ..providers import Provider, infer_provider
40
40
  from ..settings import ModelSettings
41
41
  from ..tools import ToolDefinition
@@ -121,6 +121,8 @@ class HuggingFaceModel(Model):
121
121
  model_name: str,
122
122
  *,
123
123
  provider: Literal['huggingface'] | Provider[AsyncInferenceClient] = 'huggingface',
124
+ profile: ModelProfileSpec | None = None,
125
+ settings: ModelSettings | None = None,
124
126
  ):
125
127
  """Initialize a Hugging Face model.
126
128
 
@@ -128,6 +130,8 @@ class HuggingFaceModel(Model):
128
130
  model_name: The name of the Model to use. You can browse available models [here](https://huggingface.co/models?pipeline_tag=text-generation&inference_provider=all&sort=trending).
129
131
  provider: The provider to use for Hugging Face Inference Providers. Can be either the string 'huggingface' or an
130
132
  instance of `Provider[AsyncInferenceClient]`. If not provided, the other parameters will be used.
133
+ profile: The model profile to use. Defaults to a profile picked by the provider based on the model name.
134
+ settings: Model-specific settings that will be used as defaults for this model.
131
135
  """
132
136
  self._model_name = model_name
133
137
  self._provider = provider
@@ -135,6 +139,8 @@ class HuggingFaceModel(Model):
135
139
  provider = infer_provider(provider)
136
140
  self.client = provider.client
137
141
 
142
+ super().__init__(settings=settings, profile=profile or provider.model_profile)
143
+
138
144
  async def request(
139
145
  self,
140
146
  messages: list[ModelMessage],
@@ -444,11 +450,12 @@ class HuggingFaceStreamedResponse(StreamedResponse):
444
450
 
445
451
  # Handle the text part of the response
446
452
  content = choice.delta.content
447
- if content:
453
+ if content is not None:
448
454
  maybe_event = self._parts_manager.handle_text_delta(
449
455
  vendor_part_id='content',
450
456
  content=content,
451
457
  thinking_tags=self._model_profile.thinking_tags,
458
+ ignore_leading_whitespace=self._model_profile.ignore_streamed_leading_whitespace,
452
459
  )
453
460
  if maybe_event is not None: # pragma: no branch
454
461
  yield maybe_event
@@ -217,6 +217,7 @@ class OpenAIModel(Model):
217
217
  'together',
218
218
  'heroku',
219
219
  'github',
220
+ 'ollama',
220
221
  ]
221
222
  | Provider[AsyncOpenAI] = 'openai',
222
223
  profile: ModelProfileSpec | None = None,
@@ -1094,11 +1095,12 @@ class OpenAIStreamedResponse(StreamedResponse):
1094
1095
 
1095
1096
  # Handle the text part of the response
1096
1097
  content = choice.delta.content
1097
- if content:
1098
+ if content is not None:
1098
1099
  maybe_event = self._parts_manager.handle_text_delta(
1099
1100
  vendor_part_id='content',
1100
1101
  content=content,
1101
1102
  thinking_tags=self._model_profile.thinking_tags,
1103
+ ignore_leading_whitespace=self._model_profile.ignore_streamed_leading_whitespace,
1102
1104
  )
1103
1105
  if maybe_event is not None: # pragma: no branch
1104
1106
  yield maybe_event
@@ -20,7 +20,7 @@ __all__ = [
20
20
 
21
21
  @dataclass
22
22
  class ModelProfile:
23
- """Describes how requests to a specific model or family of models need to be constructed to get the best results, independent of the model and provider classes used."""
23
+ """Describes how requests to and responses from specific models or families of models need to be constructed and processed to get the best results, independent of the model and provider classes used."""
24
24
 
25
25
  supports_tools: bool = True
26
26
  """Whether the model supports tools."""
@@ -46,6 +46,15 @@ class ModelProfile:
46
46
  thinking_tags: tuple[str, str] = ('<think>', '</think>')
47
47
  """The tags used to indicate thinking parts in the model's output. Defaults to ('<think>', '</think>')."""
48
48
 
49
+ ignore_streamed_leading_whitespace: bool = False
50
+ """Whether to ignore leading whitespace when streaming a response.
51
+
52
+ This is a workaround for models that emit `<think>\n</think>\n\n` or an empty text part ahead of tool calls (e.g. Ollama + Qwen3),
53
+ which we don't want to end up treating as a final result when using `run_stream` with `str` a valid `output_type`.
54
+
55
+ This is currently only used by `OpenAIModel`, `HuggingFaceModel`, and `GroqModel`.
56
+ """
57
+
49
58
  @classmethod
50
59
  def from_profile(cls, profile: ModelProfile | None) -> Self:
51
60
  """Build a ModelProfile subclass instance from a ModelProfile instance."""
@@ -5,4 +5,4 @@ from . import ModelProfile
5
5
 
6
6
  def deepseek_model_profile(model_name: str) -> ModelProfile | None:
7
7
  """Get the model profile for a DeepSeek model."""
8
- return None
8
+ return ModelProfile(ignore_streamed_leading_whitespace='r1' in model_name)
@@ -5,4 +5,4 @@ from . import ModelProfile
5
5
 
6
6
  def moonshotai_model_profile(model_name: str) -> ModelProfile | None:
7
7
  """Get the model profile for a MoonshotAI model."""
8
- return None
8
+ return ModelProfile(ignore_streamed_leading_whitespace=True)
@@ -5,4 +5,7 @@ from . import InlineDefsJsonSchemaTransformer, ModelProfile
5
5
 
6
6
  def qwen_model_profile(model_name: str) -> ModelProfile | None:
7
7
  """Get the model profile for a Qwen model."""
8
- return ModelProfile(json_schema_transformer=InlineDefsJsonSchemaTransformer)
8
+ return ModelProfile(
9
+ json_schema_transformer=InlineDefsJsonSchemaTransformer,
10
+ ignore_streamed_leading_whitespace=True,
11
+ )
@@ -123,6 +123,10 @@ def infer_provider_class(provider: str) -> type[Provider[Any]]: # noqa: C901
123
123
  from .huggingface import HuggingFaceProvider
124
124
 
125
125
  return HuggingFaceProvider
126
+ elif provider == 'ollama':
127
+ from .ollama import OllamaProvider
128
+
129
+ return OllamaProvider
126
130
  elif provider == 'github':
127
131
  from .github import GitHubProvider
128
132
 
@@ -6,6 +6,13 @@ from typing import overload
6
6
  from httpx import AsyncClient
7
7
 
8
8
  from pydantic_ai.exceptions import UserError
9
+ from pydantic_ai.profiles import ModelProfile
10
+ from pydantic_ai.profiles.deepseek import deepseek_model_profile
11
+ from pydantic_ai.profiles.google import google_model_profile
12
+ from pydantic_ai.profiles.meta import meta_model_profile
13
+ from pydantic_ai.profiles.mistral import mistral_model_profile
14
+ from pydantic_ai.profiles.moonshotai import moonshotai_model_profile
15
+ from pydantic_ai.profiles.qwen import qwen_model_profile
9
16
 
10
17
  try:
11
18
  from huggingface_hub import AsyncInferenceClient
@@ -33,6 +40,26 @@ class HuggingFaceProvider(Provider[AsyncInferenceClient]):
33
40
  def client(self) -> AsyncInferenceClient:
34
41
  return self._client
35
42
 
43
+ def model_profile(self, model_name: str) -> ModelProfile | None:
44
+ provider_to_profile = {
45
+ 'deepseek-ai': deepseek_model_profile,
46
+ 'google': google_model_profile,
47
+ 'qwen': qwen_model_profile,
48
+ 'meta-llama': meta_model_profile,
49
+ 'mistralai': mistral_model_profile,
50
+ 'moonshotai': moonshotai_model_profile,
51
+ }
52
+
53
+ if '/' not in model_name:
54
+ return None
55
+
56
+ model_name = model_name.lower()
57
+ provider, model_name = model_name.split('/', 1)
58
+ if provider in provider_to_profile:
59
+ return provider_to_profile[provider](model_name)
60
+
61
+ return None
62
+
36
63
  @overload
37
64
  def __init__(self, *, base_url: str, api_key: str | None = None) -> None: ...
38
65
  @overload
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ import os
4
+
5
+ import httpx
6
+ from openai import AsyncOpenAI
7
+
8
+ from pydantic_ai.exceptions import UserError
9
+ from pydantic_ai.models import cached_async_http_client
10
+ from pydantic_ai.profiles import ModelProfile
11
+ from pydantic_ai.profiles.cohere import cohere_model_profile
12
+ from pydantic_ai.profiles.deepseek import deepseek_model_profile
13
+ from pydantic_ai.profiles.google import google_model_profile
14
+ from pydantic_ai.profiles.meta import meta_model_profile
15
+ from pydantic_ai.profiles.mistral import mistral_model_profile
16
+ from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer, OpenAIModelProfile
17
+ from pydantic_ai.profiles.qwen import qwen_model_profile
18
+ from pydantic_ai.providers import Provider
19
+
20
+ try:
21
+ from openai import AsyncOpenAI
22
+ except ImportError as _import_error: # pragma: no cover
23
+ raise ImportError(
24
+ 'Please install the `openai` package to use the Ollama provider, '
25
+ 'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`'
26
+ ) from _import_error
27
+
28
+
29
+ class OllamaProvider(Provider[AsyncOpenAI]):
30
+ """Provider for local or remote Ollama API."""
31
+
32
+ @property
33
+ def name(self) -> str:
34
+ return 'ollama'
35
+
36
+ @property
37
+ def base_url(self) -> str:
38
+ return str(self.client.base_url)
39
+
40
+ @property
41
+ def client(self) -> AsyncOpenAI:
42
+ return self._client
43
+
44
+ def model_profile(self, model_name: str) -> ModelProfile | None:
45
+ prefix_to_profile = {
46
+ 'llama': meta_model_profile,
47
+ 'gemma': google_model_profile,
48
+ 'qwen': qwen_model_profile,
49
+ 'qwq': qwen_model_profile,
50
+ 'deepseek': deepseek_model_profile,
51
+ 'mistral': mistral_model_profile,
52
+ 'command': cohere_model_profile,
53
+ }
54
+
55
+ profile = None
56
+ for prefix, profile_func in prefix_to_profile.items():
57
+ model_name = model_name.lower()
58
+ if model_name.startswith(prefix):
59
+ profile = profile_func(model_name)
60
+
61
+ # As OllamaProvider is always used with OpenAIModel, which used to unconditionally use OpenAIJsonSchemaTransformer,
62
+ # we need to maintain that behavior unless json_schema_transformer is set explicitly
63
+ return OpenAIModelProfile(json_schema_transformer=OpenAIJsonSchemaTransformer).update(profile)
64
+
65
+ def __init__(
66
+ self,
67
+ base_url: str | None = None,
68
+ api_key: str | None = None,
69
+ openai_client: AsyncOpenAI | None = None,
70
+ http_client: httpx.AsyncClient | None = None,
71
+ ) -> None:
72
+ """Create a new Ollama provider.
73
+
74
+ Args:
75
+ base_url: The base url for the Ollama requests. If not provided, the `OLLAMA_BASE_URL` environment variable
76
+ will be used if available.
77
+ api_key: The API key to use for authentication, if not provided, the `OLLAMA_API_KEY` environment variable
78
+ will be used if available.
79
+ openai_client: An existing
80
+ [`AsyncOpenAI`](https://github.com/openai/openai-python?tab=readme-ov-file#async-usage)
81
+ client to use. If provided, `base_url`, `api_key`, and `http_client` must be `None`.
82
+ http_client: An existing `httpx.AsyncClient` to use for making HTTP requests.
83
+ """
84
+ if openai_client is not None:
85
+ assert base_url is None, 'Cannot provide both `openai_client` and `base_url`'
86
+ assert http_client is None, 'Cannot provide both `openai_client` and `http_client`'
87
+ assert api_key is None, 'Cannot provide both `openai_client` and `api_key`'
88
+ self._client = openai_client
89
+ else:
90
+ base_url = base_url or os.getenv('OLLAMA_BASE_URL')
91
+ if not base_url:
92
+ raise UserError(
93
+ 'Set the `OLLAMA_BASE_URL` environment variable or pass it via `OllamaProvider(base_url=...)`'
94
+ 'to use the Ollama provider.'
95
+ )
96
+
97
+ # This is a workaround for the OpenAI client requiring an API key, whilst locally served,
98
+ # openai compatible models do not always need an API key, but a placeholder (non-empty) key is required.
99
+ api_key = api_key or os.getenv('OLLAMA_API_KEY') or 'api-key-not-set'
100
+
101
+ if http_client is not None:
102
+ self._client = AsyncOpenAI(base_url=base_url, api_key=api_key, http_client=http_client)
103
+ else:
104
+ http_client = cached_async_http_client(provider='ollama')
105
+ self._client = AsyncOpenAI(base_url=base_url, api_key=api_key, http_client=http_client)
@@ -17,6 +17,7 @@ from pydantic_ai.profiles.google import google_model_profile
17
17
  from pydantic_ai.profiles.grok import grok_model_profile
18
18
  from pydantic_ai.profiles.meta import meta_model_profile
19
19
  from pydantic_ai.profiles.mistral import mistral_model_profile
20
+ from pydantic_ai.profiles.moonshotai import moonshotai_model_profile
20
21
  from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer, OpenAIModelProfile, openai_model_profile
21
22
  from pydantic_ai.profiles.qwen import qwen_model_profile
22
23
  from pydantic_ai.providers import Provider
@@ -57,6 +58,7 @@ class OpenRouterProvider(Provider[AsyncOpenAI]):
57
58
  'amazon': amazon_model_profile,
58
59
  'deepseek': deepseek_model_profile,
59
60
  'meta-llama': meta_model_profile,
61
+ 'moonshotai': moonshotai_model_profile,
60
62
  }
61
63
 
62
64
  profile = None
pydantic_ai/result.py CHANGED
@@ -196,7 +196,7 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
196
196
  and isinstance(event.part, _messages.TextPart)
197
197
  and event.part.content
198
198
  ):
199
- yield event.part.content, event.index
199
+ yield event.part.content, event.index # pragma: no cover
200
200
  elif ( # pragma: no branch
201
201
  isinstance(event, _messages.PartDeltaEvent)
202
202
  and isinstance(event.delta, _messages.TextPartDelta)
pydantic_ai/tools.py CHANGED
@@ -31,7 +31,7 @@ __all__ = (
31
31
  ToolParams = ParamSpec('ToolParams', default=...)
32
32
  """Retrieval function param spec."""
33
33
 
34
- SystemPromptFunc = Union[
34
+ SystemPromptFunc: TypeAlias = Union[
35
35
  Callable[[RunContext[AgentDepsT]], str],
36
36
  Callable[[RunContext[AgentDepsT]], Awaitable[str]],
37
37
  Callable[[], str],
@@ -42,17 +42,17 @@ SystemPromptFunc = Union[
42
42
  Usage `SystemPromptFunc[AgentDepsT]`.
43
43
  """
44
44
 
45
- ToolFuncContext = Callable[Concatenate[RunContext[AgentDepsT], ToolParams], Any]
45
+ ToolFuncContext: TypeAlias = Callable[Concatenate[RunContext[AgentDepsT], ToolParams], Any]
46
46
  """A tool function that takes `RunContext` as the first argument.
47
47
 
48
48
  Usage `ToolContextFunc[AgentDepsT, ToolParams]`.
49
49
  """
50
- ToolFuncPlain = Callable[ToolParams, Any]
50
+ ToolFuncPlain: TypeAlias = Callable[ToolParams, Any]
51
51
  """A tool function that does not take `RunContext` as the first argument.
52
52
 
53
53
  Usage `ToolPlainFunc[ToolParams]`.
54
54
  """
55
- ToolFuncEither = Union[ToolFuncContext[AgentDepsT, ToolParams], ToolFuncPlain[ToolParams]]
55
+ ToolFuncEither: TypeAlias = Union[ToolFuncContext[AgentDepsT, ToolParams], ToolFuncPlain[ToolParams]]
56
56
  """Either kind of tool function.
57
57
 
58
58
  This is just a union of [`ToolFuncContext`][pydantic_ai.tools.ToolFuncContext] and
@@ -60,7 +60,7 @@ This is just a union of [`ToolFuncContext`][pydantic_ai.tools.ToolFuncContext] a
60
60
 
61
61
  Usage `ToolFuncEither[AgentDepsT, ToolParams]`.
62
62
  """
63
- ToolPrepareFunc: TypeAlias = 'Callable[[RunContext[AgentDepsT], ToolDefinition], Awaitable[ToolDefinition | None]]'
63
+ ToolPrepareFunc: TypeAlias = Callable[[RunContext[AgentDepsT], 'ToolDefinition'], Awaitable['ToolDefinition | None']]
64
64
  """Definition of a function that can prepare a tool definition at call time.
65
65
 
66
66
  See [tool docs](../tools.md#tool-prepare) for more information.
@@ -88,9 +88,9 @@ hitchhiker = Tool(hitchhiker, prepare=only_if_42)
88
88
  Usage `ToolPrepareFunc[AgentDepsT]`.
89
89
  """
90
90
 
91
- ToolsPrepareFunc: TypeAlias = (
92
- 'Callable[[RunContext[AgentDepsT], list[ToolDefinition]], Awaitable[list[ToolDefinition] | None]]'
93
- )
91
+ ToolsPrepareFunc: TypeAlias = Callable[
92
+ [RunContext[AgentDepsT], list['ToolDefinition']], Awaitable['list[ToolDefinition] | None']
93
+ ]
94
94
  """Definition of a function that can prepare the tool definition of all tools for each step.
95
95
  This is useful if you want to customize the definition of multiple tools or you want to register
96
96
  a subset of tools for a given step.
@@ -118,7 +118,7 @@ agent = Agent('openai:gpt-4o', prepare_tools=turn_on_strict_if_openai)
118
118
  Usage `ToolsPrepareFunc[AgentDepsT]`.
119
119
  """
120
120
 
121
- DocstringFormat = Literal['google', 'numpy', 'sphinx', 'auto']
121
+ DocstringFormat: TypeAlias = Literal['google', 'numpy', 'sphinx', 'auto']
122
122
  """Supported docstring formats.
123
123
 
124
124
  * `'google'` — [Google-style](https://google.github.io/styleguide/pyguide.html#381-docstrings) docstrings.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>, Douwe Maan <douwe@pydantic.dev>
6
6
  License-Expression: MIT
@@ -30,7 +30,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
30
30
  Requires-Dist: griffe>=1.3.2
31
31
  Requires-Dist: httpx>=0.27
32
32
  Requires-Dist: opentelemetry-api>=1.28.0
33
- Requires-Dist: pydantic-graph==0.7.1
33
+ Requires-Dist: pydantic-graph==0.7.2
34
34
  Requires-Dist: pydantic>=2.10
35
35
  Requires-Dist: typing-inspection>=0.4.0
36
36
  Provides-Extra: a2a
@@ -51,7 +51,7 @@ Requires-Dist: cohere>=5.16.0; (platform_system != 'Emscripten') and extra == 'c
51
51
  Provides-Extra: duckduckgo
52
52
  Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
53
53
  Provides-Extra: evals
54
- Requires-Dist: pydantic-evals==0.7.1; extra == 'evals'
54
+ Requires-Dist: pydantic-evals==0.7.2; extra == 'evals'
55
55
  Provides-Extra: google
56
56
  Requires-Dist: google-genai>=1.28.0; extra == 'google'
57
57
  Provides-Extra: groq
@@ -1,17 +1,17 @@
1
1
  pydantic_ai/__init__.py,sha256=Uy0J4BgX4CXsa0sUUb5K0FC4uUWGIwBici93QHLkNsk,1478
2
2
  pydantic_ai/__main__.py,sha256=Q_zJU15DUA01YtlJ2mnaLCoId2YmgmreVEERGuQT-Y0,132
3
3
  pydantic_ai/_a2a.py,sha256=wux52DmJQceLJwF71qxb0Uqupk3aS61m005-NmuWZIw,12164
4
- pydantic_ai/_agent_graph.py,sha256=CairpjIY302UegGzzA5C56kDqvXR6l2gcR6vSdai2JI,37714
4
+ pydantic_ai/_agent_graph.py,sha256=QB6J-UI-gUUXPhk1ud39yCWw3U04Ea28HWfIN74iO6M,38488
5
5
  pydantic_ai/_cli.py,sha256=nr3hW7Y4vHzk7oXpfOCupwuJ6Z2SmZLz2dYS6ljCpuc,13281
6
6
  pydantic_ai/_function_schema.py,sha256=YFHxb6bKfhgeY6rNdbuYXgndGCDanveUx2258xkSNlQ,11233
7
7
  pydantic_ai/_griffe.py,sha256=Ugft16ZHw9CN_6-lW0Svn6jESK9zHXO_x4utkGBkbBI,5253
8
8
  pydantic_ai/_mcp.py,sha256=PuvwnlLjv7YYOa9AZJCrklevBug99zGMhwJCBGG7BHQ,5626
9
9
  pydantic_ai/_output.py,sha256=6Vxlw8F9nRWCkjy4qvFF8tmDi2xZn7Dq72T6s4C5kAM,37640
10
- pydantic_ai/_parts_manager.py,sha256=lWXN75zLy_MSDz4Wib65lqIPHk1SY8KDU8_OYaxG3yw,17788
10
+ pydantic_ai/_parts_manager.py,sha256=zrra5yDpAX8cFB_eK0btAp9d6NAR979V1Rmepm83l1w,17980
11
11
  pydantic_ai/_run_context.py,sha256=pqb_HPXytE1Z9zZRRuBboRYes_tVTC75WGTpZgnb2Ko,1691
12
12
  pydantic_ai/_system_prompt.py,sha256=lUSq-gDZjlYTGtd6BUm54yEvTIvgdwBmJ8mLsNZZtYU,1142
13
13
  pydantic_ai/_thinking_part.py,sha256=x80-Vkon16GOyq3W6f2qzafTVPC5dCgF7QD3k8ZMmYU,1304
14
- pydantic_ai/_tool_manager.py,sha256=0_4Ed8kUj_Z_AdTpyBdz5rdYbmlCxf1DUh2vrIk7rYE,9031
14
+ pydantic_ai/_tool_manager.py,sha256=WPMXgHBzyn7UgRKIuqU-oV2GpsAOW0nF2RsxPCKOp7U,9655
15
15
  pydantic_ai/_utils.py,sha256=Ge9rtu8NJvsfSFjx1MduITPr0-9b_I0emDFSpwJbYes,16372
16
16
  pydantic_ai/ag_ui.py,sha256=2cKSSvl1j0pxVNCFQO82l7LVtkJMt0HUaEXGwy3y558,26463
17
17
  pydantic_ai/builtin_tools.py,sha256=mwIq-7m0ZSbCZp1zxswjRfQM648QE01IDfifvqDGxUQ,3023
@@ -22,13 +22,13 @@ pydantic_ai/mcp.py,sha256=n9_ECHmFE-eOZmb1bDh94oy81caefdtSGo1oH2KKWMo,31162
22
22
  pydantic_ai/messages.py,sha256=kLw3yBtUEoRQ43zZ43PpasgC6EeVbSQi4Fl0PB1tQwA,45700
23
23
  pydantic_ai/output.py,sha256=54Cwd1RruXlA5hucZ1h-SxFrzKHJuLvYvLtH9iyg2GI,11988
24
24
  pydantic_ai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- pydantic_ai/result.py,sha256=PgsaEcIFt9rkWs3F3RW2jqI8Qhmf5gEtCHZ7jVr2d34,19765
25
+ pydantic_ai/result.py,sha256=2Z5-wJXO5Iw_qHFoMcshTjiUHFdcT1aycNWt26kyLuY,19785
26
26
  pydantic_ai/retries.py,sha256=Xkj-gZAd3wc12CVsIErVYx2EIdIwD5yJOL4Ou6jDQ2s,10498
27
27
  pydantic_ai/run.py,sha256=VSZEadgzRc_tytnHt2Gemdv9z05e6aEJTNPQ7DmuUck,15130
28
28
  pydantic_ai/settings.py,sha256=yuUZ7-GkdPB-Gbx71kSdh8dSr6gwM9gEwk84qNxPO_I,3552
29
- pydantic_ai/tools.py,sha256=H7pCfLYvtQ9j2I5qQGF_UCzUpufO14vEgLowFE8msNA,14764
29
+ pydantic_ai/tools.py,sha256=1_4kt4HGfpHH4XnYy0lQRzm5Io5SzOjk3AJxa-LJLmE,14821
30
30
  pydantic_ai/usage.py,sha256=UddLBMmytzKBmsLzyGHHbJAnr4VQkMA8-vSjCeifz3w,6801
31
- pydantic_ai/agent/__init__.py,sha256=GrQ_HqEo8Usw4QGzCOqD6zjn_htDs2zK53ocm1Fr4WM,60033
31
+ pydantic_ai/agent/__init__.py,sha256=IB67d_jxZ3LpIdt5X7ng06CUV84dlzyBMse628PQttk,59319
32
32
  pydantic_ai/agent/abstract.py,sha256=m83vk00sV7q3MqIHucExkMsiEF52dANHptGl8TNEkbw,42035
33
33
  pydantic_ai/agent/wrapper.py,sha256=cVIpfPWF63oTD1jeWdFY-OS_ty2nwbeSwsI7upd30Kw,9155
34
34
  pydantic_ai/common_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -47,36 +47,36 @@ pydantic_ai/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  pydantic_ai/ext/aci.py,sha256=sUllKDNO-LOMurbFgxwRHuzNlBkSa3aVBqXfEm-A_vo,2545
48
48
  pydantic_ai/ext/langchain.py,sha256=iLVEZv1kcLkdIHo3us2yfdi0kVqyJ6qTaCt9BoLWm4k,2335
49
49
  pydantic_ai/models/__init__.py,sha256=PQcyiPaSj72mt9kjPgg69eboqHqRl0JlBHTS3eCO5uY,34611
50
- pydantic_ai/models/anthropic.py,sha256=th3jyYMcqa-I_hyOvtH5PKyOCX-IDnriaMT5VPKBVAo,29777
51
- pydantic_ai/models/bedrock.py,sha256=wlw-9zuRnvMW-T5Nt1_rBTLWrYBllhCbu1GzkaBWddk,30819
50
+ pydantic_ai/models/anthropic.py,sha256=8wLXYWzGxD2tEf0V9cy0iApZStZHgup6TUlMjd8pQp4,30019
51
+ pydantic_ai/models/bedrock.py,sha256=IzVpH0rpj63fWnQSwYf13sZ4VFAVn0wzSWW4HRgRaSw,30840
52
52
  pydantic_ai/models/cohere.py,sha256=ZbRvCPgPjDLItUj-r08VPmR8D8e6ARbUiRd8rsZ-1sY,13094
53
53
  pydantic_ai/models/fallback.py,sha256=8d5C2MDbVBQ1NFHIciaIAJd8-DGzpTbMzIPIR1Dj4Xc,5514
54
54
  pydantic_ai/models/function.py,sha256=CKSy_xA__n92BnOzfI9BXRsRVy3WjvuRyzibU26I8Js,14299
55
55
  pydantic_ai/models/gemini.py,sha256=SWeahyDS_8STbofWJoIDvayBIGv-d4CYUuIBERPZkpI,38676
56
56
  pydantic_ai/models/google.py,sha256=xiAbIT0WkfdwwvjGY5WzOgTpkpxXI7-A2cvnyxwmI8s,29636
57
- pydantic_ai/models/groq.py,sha256=xibIAuIpSbdlF5ONAKLpEcU3qYX3JfAJRBh2K7Fh_U4,20840
58
- pydantic_ai/models/huggingface.py,sha256=ONsqk_4YiojEpVzJvAjc5z92Bln71PKvk8eK53309bk,19731
57
+ pydantic_ai/models/groq.py,sha256=iLgei6osGzLSrtxCPlDUR3Qz2sYa6qmeWJfJPrfc-c4,20942
58
+ pydantic_ai/models/huggingface.py,sha256=CMGpAzzaJ-xtDlNAM0IAx97fUagyEO84MMAncBYnwa0,20256
59
59
  pydantic_ai/models/instrumented.py,sha256=vVHO2sHkZnH7kcFr2iozOOuacfwGQYORhSgUtvDYZEU,16315
60
60
  pydantic_ai/models/mcp_sampling.py,sha256=0pAMCTkzmhQuyhik8KG2ZUYGVh4tofjdZBf6WdR78ik,3490
61
61
  pydantic_ai/models/mistral.py,sha256=-nB6VoHvNveLZHRCmXQaX9EFUJlFHXXt7nRyFtI2SIE,32251
62
- pydantic_ai/models/openai.py,sha256=ys4opGwro9SX-JA2-Xul5hV_xydq5LusOR5Ck-D18Uo,61110
62
+ pydantic_ai/models/openai.py,sha256=Ozry-an08lai0aI-PmTuOA2dwpMwnFs3VG9xgaEu7EU,61246
63
63
  pydantic_ai/models/test.py,sha256=XKfJOwjnaMAuGpQwMT-H99vIicFymdJDpAtr0PU0Zoo,19151
64
64
  pydantic_ai/models/wrapper.py,sha256=9MeHW7mXPsEK03IKL0rtjeX6QgXyZROOOzLh72GiX2k,2148
65
- pydantic_ai/profiles/__init__.py,sha256=Ggk_pbNnRcaGnDWEBppsH3sUk8ajckaaXKfJlkLQWVo,2775
65
+ pydantic_ai/profiles/__init__.py,sha256=AdwFrK50_20qJBA_eMXXsV1vdGOvPxLVW82hMQvzXU0,3285
66
66
  pydantic_ai/profiles/_json_schema.py,sha256=CthOGmPSjgEZRRglfvg31zyQ9vjHDdacXoFpmba93dE,7206
67
67
  pydantic_ai/profiles/amazon.py,sha256=IPa2wydpcbFLLvhDK35-pwwoKo0Pg4vP84823fHx0zc,314
68
68
  pydantic_ai/profiles/anthropic.py,sha256=J9N46G8eOjHdQ5CwZSLiwGdPb0eeIMdsMjwosDpvNhI,275
69
69
  pydantic_ai/profiles/cohere.py,sha256=lcL34Ht1jZopwuqoU6OV9l8vN4zwF-jiPjlsEABbSRo,215
70
- pydantic_ai/profiles/deepseek.py,sha256=DS_idprnXpMliKziKF0k1neLDJOwUvpatZ3YLaiYnCM,219
70
+ pydantic_ai/profiles/deepseek.py,sha256=JDwfkr-0YovlB3jEKk7dNFvepxNf_YuLgLkGCtyXHSk,282
71
71
  pydantic_ai/profiles/google.py,sha256=cd5zwtx0MU1Xwm8c-oqi2_OJ2-PMJ8Vy23mxvSJF7ik,4856
72
72
  pydantic_ai/profiles/grok.py,sha256=nBOxOCYCK9aiLmz2Q-esqYhotNbbBC1boAoOYIk1tVw,211
73
73
  pydantic_ai/profiles/groq.py,sha256=5jLNnOuxq3HTrbY-cizJyGa1hIluW7sCPLmDP1C1unc,668
74
74
  pydantic_ai/profiles/meta.py,sha256=JdZcpdRWx8PY1pU9Z2i_TYtA0Cpbg23xyFrV7eXnooY,309
75
75
  pydantic_ai/profiles/mistral.py,sha256=ll01PmcK3szwlTfbaJLQmfd0TADN8lqjov9HpPJzCMQ,217
76
- pydantic_ai/profiles/moonshotai.py,sha256=LL5RacKHKn6rdvhoKjpGgZ8aVriv5NMeL6HCWEANAiU,223
76
+ pydantic_ai/profiles/moonshotai.py,sha256=e1RJnbEvazE6aJAqfmYLYGNtwNwg52XQDRDkcLrv3fU,272
77
77
  pydantic_ai/profiles/openai.py,sha256=YIzZAeJWO8dhmeHcOQk-Kyh6DUd5b0I5EQSTcK0-qy4,7564
78
- pydantic_ai/profiles/qwen.py,sha256=zU19r2lVBxU0v0fXyA9G-VvG3XzBZMZJVxCpQutU9k0,309
79
- pydantic_ai/providers/__init__.py,sha256=yxPgiTJKFYZbDW18tmVM6mmD2Znol3WwniwnhtlN0Ak,4141
78
+ pydantic_ai/profiles/qwen.py,sha256=K4_nJ_oN5NS_9W0Fl-dFgC4emVRTHPXFTtiJ_nycKHo,373
79
+ pydantic_ai/providers/__init__.py,sha256=-jb9Vl4gE7z0katqwLPaKt5UileuPp0Brq0ZueBVJys,4246
80
80
  pydantic_ai/providers/anthropic.py,sha256=D35UXxCPXv8yIbD0fj9Zg2FvNyoMoJMeDUtVM8Sn78I,3046
81
81
  pydantic_ai/providers/azure.py,sha256=y77IHGiSQ9Ttx9f4SGMgdpin2Daq6eYyzUdM9ET22RQ,5819
82
82
  pydantic_ai/providers/bedrock.py,sha256=8jz77ySKv6CzCktN9YbZb1736gZM0d_btcKvXiZSSHI,5784
@@ -90,11 +90,12 @@ pydantic_ai/providers/google_vertex.py,sha256=9wJGctzQTEtmTTr3KCFAubDREMQJ4zOXt9
90
90
  pydantic_ai/providers/grok.py,sha256=dIkpxuuJlZ4pFtTSgeeJgiM_Z3mJU9qvk4f7jvSzi24,3141
91
91
  pydantic_ai/providers/groq.py,sha256=AeG5flZ_n4fRy8RWm0RGvDBEDrdaLfR8gMOTRHQB368,4059
92
92
  pydantic_ai/providers/heroku.py,sha256=NmDIkAdxtWsvCjlX-bKI5FgI4HW1zO9-e0mrNQNGMCk,2990
93
- pydantic_ai/providers/huggingface.py,sha256=LRmJcJpQRRYvam3IAPkYs2fMUJf70GgE3aDgQltGRCU,3821
93
+ pydantic_ai/providers/huggingface.py,sha256=MLAv-Z99Kii5Faolq97_0Ir1LUKH9CwRmJFaI5RvwW4,4914
94
94
  pydantic_ai/providers/mistral.py,sha256=EIUSENjFuGzBhvbdrarUTM4VPkesIMnZrzfnEKHOsc4,3120
95
95
  pydantic_ai/providers/moonshotai.py,sha256=3BAE9eC9QaD3kblVwxtQWEln0-PhgK7bRvrYTCEYXbY,3471
96
+ pydantic_ai/providers/ollama.py,sha256=GNrrjK02fRCv-3l09N2rl6tFTnGVbdDtfbu5j4Wggv8,4629
96
97
  pydantic_ai/providers/openai.py,sha256=7iGij0EaFylab7dTZAZDgXr78tr-HsZrn9EI9AkWBNQ,3091
97
- pydantic_ai/providers/openrouter.py,sha256=NXjNdnlXIBrBMMqbzcWQnowXOuZh4NHikXenBn5h3mc,4061
98
+ pydantic_ai/providers/openrouter.py,sha256=rSJwc_efQlOaGSxN5hjuemg-8llVCEGf5uZaeFwoQm8,4182
98
99
  pydantic_ai/providers/together.py,sha256=zFVSMSm5jXbpkNouvBOTjWrPmlPpCp6sQS5LMSyVjrQ,3482
99
100
  pydantic_ai/providers/vercel.py,sha256=wFIfyfD2RCAVRWtveDDMjumOkP8v9AHy94KV1HXF180,4285
100
101
  pydantic_ai/toolsets/__init__.py,sha256=btvEfRHUzW8E6HiWP-AUKc0xBvIEigW6qWqVfnN11Ag,643
@@ -108,8 +109,8 @@ pydantic_ai/toolsets/prefixed.py,sha256=0KwcDkW8OM36ZUsOLVP5h-Nj2tPq78L3_E2c-1Fb
108
109
  pydantic_ai/toolsets/prepared.py,sha256=Zjfz6S8In6PBVxoKFN9sKPN984zO6t0awB7Lnq5KODw,1431
109
110
  pydantic_ai/toolsets/renamed.py,sha256=JuLHpi-hYPiSPlaTpN8WiXLiGsywYK0axi2lW2Qs75k,1637
110
111
  pydantic_ai/toolsets/wrapper.py,sha256=mMuMPdko9PJUdcsexlRXbwViSwKKJfv6JE58d8HK3ds,1646
111
- pydantic_ai_slim-0.7.1.dist-info/METADATA,sha256=DMu_iRcI1lhpIap8dm_3UX3vapXTkwtAauYAbDVxskk,4252
112
- pydantic_ai_slim-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
113
- pydantic_ai_slim-0.7.1.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
114
- pydantic_ai_slim-0.7.1.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
115
- pydantic_ai_slim-0.7.1.dist-info/RECORD,,
112
+ pydantic_ai_slim-0.7.2.dist-info/METADATA,sha256=ibQxHaOvu0py9dGB6ALtmaIOpK23YHDxXINvJ92bjDE,4252
113
+ pydantic_ai_slim-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
+ pydantic_ai_slim-0.7.2.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
115
+ pydantic_ai_slim-0.7.2.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
116
+ pydantic_ai_slim-0.7.2.dist-info/RECORD,,