pydantic-ai-slim 0.4.10__py3-none-any.whl → 0.4.11__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.

@@ -17,7 +17,6 @@ from collections.abc import Hashable
17
17
  from dataclasses import dataclass, field, replace
18
18
  from typing import Any, Union
19
19
 
20
- from pydantic_ai._thinking_part import END_THINK_TAG, START_THINK_TAG
21
20
  from pydantic_ai.exceptions import UnexpectedModelBehavior
22
21
  from pydantic_ai.messages import (
23
22
  ModelResponsePart,
@@ -72,7 +71,7 @@ class ModelResponsePartsManager:
72
71
  *,
73
72
  vendor_part_id: VendorId | None,
74
73
  content: str,
75
- extract_think_tags: bool = False,
74
+ thinking_tags: tuple[str, str] | None = None,
76
75
  ) -> ModelResponseStreamEvent | None:
77
76
  """Handle incoming text content, creating or updating a TextPart in the manager as appropriate.
78
77
 
@@ -85,7 +84,7 @@ class ModelResponsePartsManager:
85
84
  of text. If None, a new part will be created unless the latest part is already
86
85
  a TextPart.
87
86
  content: The text content to append to the appropriate TextPart.
88
- extract_think_tags: Whether to extract `<think>` tags from the text content and handle them as thinking parts.
87
+ thinking_tags: If provided, will handle content between the thinking tags as thinking parts.
89
88
 
90
89
  Returns:
91
90
  - A `PartStartEvent` if a new part was created.
@@ -110,10 +109,10 @@ class ModelResponsePartsManager:
110
109
  if part_index is not None:
111
110
  existing_part = self._parts[part_index]
112
111
 
113
- if extract_think_tags and isinstance(existing_part, ThinkingPart):
114
- # We may be building a thinking part instead of a text part if we had previously seen a `<think>` tag
115
- if content == END_THINK_TAG:
116
- # When we see `</think>`, we're done with the thinking part and the next text delta will need a new part
112
+ if thinking_tags and isinstance(existing_part, ThinkingPart):
113
+ # We may be building a thinking part instead of a text part if we had previously seen a thinking tag
114
+ if content == thinking_tags[1]:
115
+ # When we see the thinking end tag, we're done with the thinking part and the next text delta will need a new part
117
116
  self._vendor_id_to_part_index.pop(vendor_part_id)
118
117
  return None
119
118
  else:
@@ -123,8 +122,8 @@ class ModelResponsePartsManager:
123
122
  else:
124
123
  raise UnexpectedModelBehavior(f'Cannot apply a text delta to {existing_part=}')
125
124
 
126
- if extract_think_tags and content == START_THINK_TAG:
127
- # When we see a `<think>` tag (which is a single token), we'll build a new thinking part instead
125
+ if thinking_tags and content == thinking_tags[0]:
126
+ # When we see a thinking start tag (which is a single token), we'll build a new thinking part instead
128
127
  self._vendor_id_to_part_index.pop(vendor_part_id, None)
129
128
  return self.handle_thinking_delta(vendor_part_id=vendor_part_id, content='')
130
129
 
@@ -2,35 +2,30 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  from pydantic_ai.messages import TextPart, ThinkingPart
4
4
 
5
- START_THINK_TAG = '<think>'
6
- END_THINK_TAG = '</think>'
7
5
 
8
-
9
- def split_content_into_text_and_thinking(content: str) -> list[ThinkingPart | TextPart]:
6
+ def split_content_into_text_and_thinking(content: str, thinking_tags: tuple[str, str]) -> list[ThinkingPart | TextPart]:
10
7
  """Split a string into text and thinking parts.
11
8
 
12
9
  Some models don't return the thinking part as a separate part, but rather as a tag in the content.
13
10
  This function splits the content into text and thinking parts.
14
-
15
- We use the `<think>` tag because that's how Groq uses it in the `raw` format, so instead of using `<Thinking>` or
16
- something else, we just match the tag to make it easier for other models that don't support the `ThinkingPart`.
17
11
  """
12
+ start_tag, end_tag = thinking_tags
18
13
  parts: list[ThinkingPart | TextPart] = []
19
14
 
20
- start_index = content.find(START_THINK_TAG)
15
+ start_index = content.find(start_tag)
21
16
  while start_index >= 0:
22
- before_think, content = content[:start_index], content[start_index + len(START_THINK_TAG) :]
17
+ before_think, content = content[:start_index], content[start_index + len(start_tag) :]
23
18
  if before_think:
24
19
  parts.append(TextPart(content=before_think))
25
- end_index = content.find(END_THINK_TAG)
20
+ end_index = content.find(end_tag)
26
21
  if end_index >= 0:
27
- think_content, content = content[:end_index], content[end_index + len(END_THINK_TAG) :]
22
+ think_content, content = content[:end_index], content[end_index + len(end_tag) :]
28
23
  parts.append(ThinkingPart(content=think_content))
29
24
  else:
30
25
  # We lose the `<think>` tag, but it shouldn't matter.
31
26
  parts.append(TextPart(content=content))
32
27
  content = ''
33
- start_index = content.find(START_THINK_TAG)
28
+ start_index = content.find(start_tag)
34
29
  if content:
35
30
  parts.append(TextPart(content=content))
36
31
  return parts
pydantic_ai/ag_ui.py CHANGED
@@ -8,11 +8,10 @@ from __future__ import annotations
8
8
 
9
9
  import json
10
10
  import uuid
11
- from collections.abc import Iterable, Mapping, Sequence
12
- from dataclasses import Field, dataclass, field, replace
11
+ from collections.abc import AsyncIterator, Iterable, Mapping, Sequence
12
+ from dataclasses import Field, dataclass, replace
13
13
  from http import HTTPStatus
14
14
  from typing import (
15
- TYPE_CHECKING,
16
15
  Any,
17
16
  Callable,
18
17
  ClassVar,
@@ -23,10 +22,36 @@ from typing import (
23
22
  runtime_checkable,
24
23
  )
25
24
 
26
- from pydantic_ai.exceptions import UserError
25
+ from pydantic import BaseModel, ValidationError
27
26
 
28
- if TYPE_CHECKING:
29
- pass
27
+ from ._agent_graph import CallToolsNode, ModelRequestNode
28
+ from .agent import Agent, AgentRun
29
+ from .exceptions import UserError
30
+ from .messages import (
31
+ AgentStreamEvent,
32
+ FunctionToolResultEvent,
33
+ ModelMessage,
34
+ ModelRequest,
35
+ ModelResponse,
36
+ PartDeltaEvent,
37
+ PartStartEvent,
38
+ SystemPromptPart,
39
+ TextPart,
40
+ TextPartDelta,
41
+ ThinkingPart,
42
+ ThinkingPartDelta,
43
+ ToolCallPart,
44
+ ToolCallPartDelta,
45
+ ToolReturnPart,
46
+ UserPromptPart,
47
+ )
48
+ from .models import KnownModelName, Model
49
+ from .output import DeferredToolCalls, OutputDataT, OutputSpec
50
+ from .settings import ModelSettings
51
+ from .tools import AgentDepsT, ToolDefinition
52
+ from .toolsets import AbstractToolset
53
+ from .toolsets.deferred import DeferredToolset
54
+ from .usage import Usage, UsageLimits
30
55
 
31
56
  try:
32
57
  from ag_ui.core import (
@@ -74,43 +99,13 @@ except ImportError as e: # pragma: no cover
74
99
  'you can use the `ag-ui` optional group — `pip install "pydantic-ai-slim[ag-ui]"`'
75
100
  ) from e
76
101
 
77
- from collections.abc import AsyncGenerator
78
-
79
- from pydantic import BaseModel, ValidationError
80
-
81
- from ._agent_graph import CallToolsNode, ModelRequestNode
82
- from .agent import Agent, AgentRun, RunOutputDataT
83
- from .messages import (
84
- AgentStreamEvent,
85
- FunctionToolResultEvent,
86
- ModelMessage,
87
- ModelRequest,
88
- ModelResponse,
89
- PartDeltaEvent,
90
- PartStartEvent,
91
- SystemPromptPart,
92
- TextPart,
93
- TextPartDelta,
94
- ThinkingPart,
95
- ThinkingPartDelta,
96
- ToolCallPart,
97
- ToolCallPartDelta,
98
- ToolReturnPart,
99
- UserPromptPart,
100
- )
101
- from .models import KnownModelName, Model
102
- from .output import DeferredToolCalls, OutputDataT, OutputSpec
103
- from .settings import ModelSettings
104
- from .tools import AgentDepsT, ToolDefinition
105
- from .toolsets import AbstractToolset
106
- from .toolsets.deferred import DeferredToolset
107
- from .usage import Usage, UsageLimits
108
-
109
102
  __all__ = [
110
103
  'SSE_CONTENT_TYPE',
111
104
  'StateDeps',
112
105
  'StateHandler',
113
106
  'AGUIApp',
107
+ 'handle_ag_ui_request',
108
+ 'run_ag_ui',
114
109
  ]
115
110
 
116
111
  SSE_CONTENT_TYPE: Final[str] = 'text/event-stream'
@@ -125,7 +120,7 @@ class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
125
120
  agent: Agent[AgentDepsT, OutputDataT],
126
121
  *,
127
122
  # Agent.iter parameters.
128
- output_type: OutputSpec[OutputDataT] | None = None,
123
+ output_type: OutputSpec[Any] | None = None,
129
124
  model: Model | KnownModelName | str | None = None,
130
125
  deps: AgentDepsT = None,
131
126
  model_settings: ModelSettings | None = None,
@@ -142,10 +137,16 @@ class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
142
137
  on_shutdown: Sequence[Callable[[], Any]] | None = None,
143
138
  lifespan: Lifespan[AGUIApp[AgentDepsT, OutputDataT]] | None = None,
144
139
  ) -> None:
145
- """Initialise the AG-UI application.
140
+ """An ASGI application that handles every AG-UI request by running the agent.
141
+
142
+ Note that the `deps` will be the same for each request, with the exception of the AG-UI state that's
143
+ injected into the `state` field of a `deps` object that implements the [`StateHandler`][pydantic_ai.ag_ui.StateHandler] protocol.
144
+ To provide different `deps` for each request (e.g. based on the authenticated user),
145
+ use [`pydantic_ai.ag_ui.run_ag_ui`][pydantic_ai.ag_ui.run_ag_ui] or
146
+ [`pydantic_ai.ag_ui.handle_ag_ui_request`][pydantic_ai.ag_ui.handle_ag_ui_request] instead.
146
147
 
147
148
  Args:
148
- agent: The Pydantic AI `Agent` to adapt.
149
+ agent: The agent to run.
149
150
 
150
151
  output_type: Custom output type to use for this run, `output_type` may only be used if the agent has
151
152
  no output validators since output validators would expect an argument that matches the agent's
@@ -156,7 +157,7 @@ class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
156
157
  usage_limits: Optional limits on model request count or token usage.
157
158
  usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
158
159
  infer_name: Whether to try to infer the agent name from the call frame if it's not set.
159
- toolsets: Optional list of toolsets to use for this agent, defaults to the agent's toolset.
160
+ toolsets: Optional additional toolsets for this run.
160
161
 
161
162
  debug: Boolean indicating if debug tracebacks should be returned on errors.
162
163
  routes: A list of routes to serve incoming HTTP and WebSocket requests.
@@ -185,320 +186,349 @@ class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
185
186
  on_shutdown=on_shutdown,
186
187
  lifespan=lifespan,
187
188
  )
188
- adapter = _Adapter(agent=agent)
189
189
 
190
- async def endpoint(request: Request) -> Response | StreamingResponse:
190
+ async def endpoint(request: Request) -> Response:
191
191
  """Endpoint to run the agent with the provided input data."""
192
- accept = request.headers.get('accept', SSE_CONTENT_TYPE)
193
- try:
194
- input_data = RunAgentInput.model_validate(await request.json())
195
- except ValidationError as e: # pragma: no cover
196
- return Response(
197
- content=json.dumps(e.json()),
198
- media_type='application/json',
199
- status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
200
- )
201
-
202
- return StreamingResponse(
203
- adapter.run(
204
- input_data,
205
- accept,
206
- output_type=output_type,
207
- model=model,
208
- deps=deps,
209
- model_settings=model_settings,
210
- usage_limits=usage_limits,
211
- usage=usage,
212
- infer_name=infer_name,
213
- toolsets=toolsets,
214
- ),
215
- media_type=SSE_CONTENT_TYPE,
192
+ return await handle_ag_ui_request(
193
+ agent,
194
+ request,
195
+ output_type=output_type,
196
+ model=model,
197
+ deps=deps,
198
+ model_settings=model_settings,
199
+ usage_limits=usage_limits,
200
+ usage=usage,
201
+ infer_name=infer_name,
202
+ toolsets=toolsets,
216
203
  )
217
204
 
218
205
  self.router.add_route('/', endpoint, methods=['POST'], name='run_agent')
219
206
 
220
207
 
221
- @dataclass(repr=False)
222
- class _Adapter(Generic[AgentDepsT, OutputDataT]):
223
- """An agent adapter providing AG-UI protocol support for Pydantic AI agents.
224
-
225
- This class manages the agent runs, tool calls, state storage and providing
226
- an adapter for running agents with Server-Sent Event (SSE) streaming
227
- responses using the AG-UI protocol.
208
+ async def handle_ag_ui_request(
209
+ agent: Agent[AgentDepsT, Any],
210
+ request: Request,
211
+ *,
212
+ output_type: OutputSpec[Any] | None = None,
213
+ model: Model | KnownModelName | str | None = None,
214
+ deps: AgentDepsT = None,
215
+ model_settings: ModelSettings | None = None,
216
+ usage_limits: UsageLimits | None = None,
217
+ usage: Usage | None = None,
218
+ infer_name: bool = True,
219
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
220
+ ) -> Response:
221
+ """Handle an AG-UI request by running the agent and returning a streaming response.
228
222
 
229
223
  Args:
230
- agent: The Pydantic AI `Agent` to adapt.
224
+ agent: The agent to run.
225
+ request: The Starlette request (e.g. from FastAPI) containing the AG-UI run input.
226
+
227
+ output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
228
+ output validators since output validators would expect an argument that matches the agent's output type.
229
+ model: Optional model to use for this run, required if `model` was not set when creating the agent.
230
+ deps: Optional dependencies to use for this run.
231
+ model_settings: Optional settings to use for this model's request.
232
+ usage_limits: Optional limits on model request count or token usage.
233
+ usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
234
+ infer_name: Whether to try to infer the agent name from the call frame if it's not set.
235
+ toolsets: Optional additional toolsets for this run.
236
+
237
+ Returns:
238
+ A streaming Starlette response with AG-UI protocol events.
231
239
  """
240
+ accept = request.headers.get('accept', SSE_CONTENT_TYPE)
241
+ try:
242
+ input_data = RunAgentInput.model_validate(await request.json())
243
+ except ValidationError as e: # pragma: no cover
244
+ return Response(
245
+ content=json.dumps(e.json()),
246
+ media_type='application/json',
247
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
248
+ )
232
249
 
233
- agent: Agent[AgentDepsT, OutputDataT] = field(repr=False)
234
-
235
- async def run(
236
- self,
237
- run_input: RunAgentInput,
238
- accept: str = SSE_CONTENT_TYPE,
239
- *,
240
- output_type: OutputSpec[RunOutputDataT] | None = None,
241
- model: Model | KnownModelName | str | None = None,
242
- deps: AgentDepsT = None,
243
- model_settings: ModelSettings | None = None,
244
- usage_limits: UsageLimits | None = None,
245
- usage: Usage | None = None,
246
- infer_name: bool = True,
247
- toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
248
- ) -> AsyncGenerator[str, None]:
249
- """Run the agent with streaming response using AG-UI protocol events.
250
+ return StreamingResponse(
251
+ run_ag_ui(
252
+ agent,
253
+ input_data,
254
+ accept,
255
+ output_type=output_type,
256
+ model=model,
257
+ deps=deps,
258
+ model_settings=model_settings,
259
+ usage_limits=usage_limits,
260
+ usage=usage,
261
+ infer_name=infer_name,
262
+ toolsets=toolsets,
263
+ ),
264
+ media_type=accept,
265
+ )
250
266
 
251
- The first two arguments are specific to `Adapter` the rest map directly to the `Agent.iter` method.
252
267
 
253
- Args:
254
- run_input: The AG-UI run input containing thread_id, run_id, messages, etc.
255
- accept: The accept header value for the run.
268
+ async def run_ag_ui(
269
+ agent: Agent[AgentDepsT, Any],
270
+ run_input: RunAgentInput,
271
+ accept: str = SSE_CONTENT_TYPE,
272
+ *,
273
+ output_type: OutputSpec[Any] | None = None,
274
+ model: Model | KnownModelName | str | None = None,
275
+ deps: AgentDepsT = None,
276
+ model_settings: ModelSettings | None = None,
277
+ usage_limits: UsageLimits | None = None,
278
+ usage: Usage | None = None,
279
+ infer_name: bool = True,
280
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
281
+ ) -> AsyncIterator[str]:
282
+ """Run the agent with the AG-UI run input and stream AG-UI protocol events.
256
283
 
257
- output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
258
- output validators since output validators would expect an argument that matches the agent's output type.
259
- model: Optional model to use for this run, required if `model` was not set when creating the agent.
260
- deps: Optional dependencies to use for this run.
261
- model_settings: Optional settings to use for this model's request.
262
- usage_limits: Optional limits on model request count or token usage.
263
- usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
264
- infer_name: Whether to try to infer the agent name from the call frame if it's not set.
265
- toolsets: Optional list of toolsets to use for this agent, defaults to the agent's toolset.
284
+ Args:
285
+ agent: The agent to run.
286
+ run_input: The AG-UI run input containing thread_id, run_id, messages, etc.
287
+ accept: The accept header value for the run.
288
+
289
+ output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
290
+ output validators since output validators would expect an argument that matches the agent's output type.
291
+ model: Optional model to use for this run, required if `model` was not set when creating the agent.
292
+ deps: Optional dependencies to use for this run.
293
+ model_settings: Optional settings to use for this model's request.
294
+ usage_limits: Optional limits on model request count or token usage.
295
+ usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
296
+ infer_name: Whether to try to infer the agent name from the call frame if it's not set.
297
+ toolsets: Optional additional toolsets for this run.
298
+
299
+ Yields:
300
+ Streaming event chunks encoded as strings according to the accept header value.
301
+ """
302
+ encoder = EventEncoder(accept=accept)
303
+ if run_input.tools:
304
+ # AG-UI tools can't be prefixed as that would result in a mismatch between the tool names in the
305
+ # Pydantic AI events and actual AG-UI tool names, preventing the tool from being called. If any
306
+ # conflicts arise, the AG-UI tool should be renamed or a `PrefixedToolset` used for local toolsets.
307
+ toolset = DeferredToolset[AgentDepsT](
308
+ [
309
+ ToolDefinition(
310
+ name=tool.name,
311
+ description=tool.description,
312
+ parameters_json_schema=tool.parameters,
313
+ )
314
+ for tool in run_input.tools
315
+ ]
316
+ )
317
+ toolsets = [*toolsets, toolset] if toolsets else [toolset]
318
+
319
+ try:
320
+ yield encoder.encode(
321
+ RunStartedEvent(
322
+ thread_id=run_input.thread_id,
323
+ run_id=run_input.run_id,
324
+ ),
325
+ )
266
326
 
267
- Yields:
268
- Streaming SSE-formatted event chunks.
269
- """
270
- encoder = EventEncoder(accept=accept)
271
- if run_input.tools:
272
- # AG-UI tools can't be prefixed as that would result in a mismatch between the tool names in the
273
- # Pydantic AI events and actual AG-UI tool names, preventing the tool from being called. If any
274
- # conflicts arise, the AG-UI tool should be renamed or a `PrefixedToolset` used for local toolsets.
275
- toolset = DeferredToolset[AgentDepsT](
276
- [
277
- ToolDefinition(
278
- name=tool.name,
279
- description=tool.description,
280
- parameters_json_schema=tool.parameters,
281
- )
282
- for tool in run_input.tools
283
- ]
284
- )
285
- toolsets = [*toolsets, toolset] if toolsets else [toolset]
286
-
287
- try:
288
- yield encoder.encode(
289
- RunStartedEvent(
290
- thread_id=run_input.thread_id,
291
- run_id=run_input.run_id,
292
- ),
293
- )
327
+ if not run_input.messages:
328
+ raise _NoMessagesError
294
329
 
295
- if not run_input.messages:
296
- raise _NoMessagesError
297
-
298
- raw_state: dict[str, Any] = run_input.state or {}
299
- if isinstance(deps, StateHandler):
300
- if isinstance(deps.state, BaseModel):
301
- try:
302
- state = type(deps.state).model_validate(raw_state)
303
- except ValidationError as e: # pragma: no cover
304
- raise _InvalidStateError from e
305
- else:
306
- state = raw_state
307
-
308
- deps = replace(deps, state=state)
309
- elif raw_state:
310
- raise UserError(
311
- f'AG-UI state is provided but `deps` of type `{type(deps).__name__}` does not implement the `StateHandler` protocol: it needs to be a dataclass with a non-optional `state` field.'
312
- )
330
+ raw_state: dict[str, Any] = run_input.state or {}
331
+ if isinstance(deps, StateHandler):
332
+ if isinstance(deps.state, BaseModel):
333
+ try:
334
+ state = type(deps.state).model_validate(raw_state)
335
+ except ValidationError as e: # pragma: no cover
336
+ raise _InvalidStateError from e
313
337
  else:
314
- # `deps` not being a `StateHandler` is OK if there is no state.
315
- pass
316
-
317
- messages = _messages_from_ag_ui(run_input.messages)
338
+ state = raw_state
318
339
 
319
- async with self.agent.iter(
320
- user_prompt=None,
321
- output_type=[output_type or self.agent.output_type, DeferredToolCalls],
322
- message_history=messages,
323
- model=model,
324
- deps=deps,
325
- model_settings=model_settings,
326
- usage_limits=usage_limits,
327
- usage=usage,
328
- infer_name=infer_name,
329
- toolsets=toolsets,
330
- ) as run:
331
- async for event in self._agent_stream(run):
332
- yield encoder.encode(event)
333
- except _RunError as e:
334
- yield encoder.encode(
335
- RunErrorEvent(message=e.message, code=e.code),
336
- )
337
- except Exception as e:
338
- yield encoder.encode(
339
- RunErrorEvent(message=str(e)),
340
+ deps = replace(deps, state=state)
341
+ elif raw_state:
342
+ raise UserError(
343
+ f'AG-UI state is provided but `deps` of type `{type(deps).__name__}` does not implement the `StateHandler` protocol: it needs to be a dataclass with a non-optional `state` field.'
340
344
  )
341
- raise e
342
345
  else:
343
- yield encoder.encode(
344
- RunFinishedEvent(
345
- thread_id=run_input.thread_id,
346
- run_id=run_input.run_id,
347
- ),
348
- )
346
+ # `deps` not being a `StateHandler` is OK if there is no state.
347
+ pass
349
348
 
350
- async def _agent_stream(
351
- self,
352
- run: AgentRun[AgentDepsT, Any],
353
- ) -> AsyncGenerator[BaseEvent, None]:
354
- """Run the agent streaming responses using AG-UI protocol events.
349
+ messages = _messages_from_ag_ui(run_input.messages)
350
+
351
+ async with agent.iter(
352
+ user_prompt=None,
353
+ output_type=[output_type or agent.output_type, DeferredToolCalls],
354
+ message_history=messages,
355
+ model=model,
356
+ deps=deps,
357
+ model_settings=model_settings,
358
+ usage_limits=usage_limits,
359
+ usage=usage,
360
+ infer_name=infer_name,
361
+ toolsets=toolsets,
362
+ ) as run:
363
+ async for event in _agent_stream(run):
364
+ yield encoder.encode(event)
365
+ except _RunError as e:
366
+ yield encoder.encode(
367
+ RunErrorEvent(message=e.message, code=e.code),
368
+ )
369
+ except Exception as e:
370
+ yield encoder.encode(
371
+ RunErrorEvent(message=str(e)),
372
+ )
373
+ raise e
374
+ else:
375
+ yield encoder.encode(
376
+ RunFinishedEvent(
377
+ thread_id=run_input.thread_id,
378
+ run_id=run_input.run_id,
379
+ ),
380
+ )
355
381
 
356
- Args:
357
- run: The agent run to process.
358
382
 
359
- Yields:
360
- AG-UI Server-Sent Events (SSE).
361
- """
362
- async for node in run:
363
- stream_ctx = _RequestStreamContext()
364
- if isinstance(node, ModelRequestNode):
365
- async with node.stream(run.ctx) as request_stream:
366
- async for agent_event in request_stream:
367
- async for msg in self._handle_model_request_event(stream_ctx, agent_event):
383
+ async def _agent_stream(run: AgentRun[AgentDepsT, Any]) -> AsyncIterator[BaseEvent]:
384
+ """Run the agent streaming responses using AG-UI protocol events.
385
+
386
+ Args:
387
+ run: The agent run to process.
388
+
389
+ Yields:
390
+ AG-UI Server-Sent Events (SSE).
391
+ """
392
+ async for node in run:
393
+ stream_ctx = _RequestStreamContext()
394
+ if isinstance(node, ModelRequestNode):
395
+ async with node.stream(run.ctx) as request_stream:
396
+ async for agent_event in request_stream:
397
+ async for msg in _handle_model_request_event(stream_ctx, agent_event):
398
+ yield msg
399
+
400
+ if stream_ctx.part_end: # pragma: no branch
401
+ yield stream_ctx.part_end
402
+ stream_ctx.part_end = None
403
+ elif isinstance(node, CallToolsNode):
404
+ async with node.stream(run.ctx) as handle_stream:
405
+ async for event in handle_stream:
406
+ if isinstance(event, FunctionToolResultEvent):
407
+ async for msg in _handle_tool_result_event(stream_ctx, event):
368
408
  yield msg
369
409
 
370
- if stream_ctx.part_end: # pragma: no branch
371
- yield stream_ctx.part_end
372
- stream_ctx.part_end = None
373
- elif isinstance(node, CallToolsNode):
374
- async with node.stream(run.ctx) as handle_stream:
375
- async for event in handle_stream:
376
- if isinstance(event, FunctionToolResultEvent):
377
- async for msg in self._handle_tool_result_event(stream_ctx, event):
378
- yield msg
379
-
380
- async def _handle_model_request_event(
381
- self,
382
- stream_ctx: _RequestStreamContext,
383
- agent_event: AgentStreamEvent,
384
- ) -> AsyncGenerator[BaseEvent, None]:
385
- """Handle an agent event and yield AG-UI protocol events.
386
410
 
387
- Args:
388
- stream_ctx: The request stream context to manage state.
389
- agent_event: The agent event to process.
411
+ async def _handle_model_request_event(
412
+ stream_ctx: _RequestStreamContext,
413
+ agent_event: AgentStreamEvent,
414
+ ) -> AsyncIterator[BaseEvent]:
415
+ """Handle an agent event and yield AG-UI protocol events.
390
416
 
391
- Yields:
392
- AG-UI Server-Sent Events (SSE) based on the agent event.
393
- """
394
- if isinstance(agent_event, PartStartEvent):
395
- if stream_ctx.part_end:
396
- # End the previous part.
397
- yield stream_ctx.part_end
398
- stream_ctx.part_end = None
399
-
400
- part = agent_event.part
401
- if isinstance(part, TextPart):
402
- message_id = stream_ctx.new_message_id()
403
- yield TextMessageStartEvent(
404
- message_id=message_id,
405
- )
406
- if part.content: # pragma: no branch
407
- yield TextMessageContentEvent(
408
- message_id=message_id,
409
- delta=part.content,
410
- )
411
- stream_ctx.part_end = TextMessageEndEvent(
417
+ Args:
418
+ stream_ctx: The request stream context to manage state.
419
+ agent_event: The agent event to process.
420
+
421
+ Yields:
422
+ AG-UI Server-Sent Events (SSE) based on the agent event.
423
+ """
424
+ if isinstance(agent_event, PartStartEvent):
425
+ if stream_ctx.part_end:
426
+ # End the previous part.
427
+ yield stream_ctx.part_end
428
+ stream_ctx.part_end = None
429
+
430
+ part = agent_event.part
431
+ if isinstance(part, TextPart):
432
+ message_id = stream_ctx.new_message_id()
433
+ yield TextMessageStartEvent(
434
+ message_id=message_id,
435
+ )
436
+ if part.content: # pragma: no branch
437
+ yield TextMessageContentEvent(
412
438
  message_id=message_id,
439
+ delta=part.content,
413
440
  )
414
- elif isinstance(part, ToolCallPart): # pragma: no branch
415
- message_id = stream_ctx.message_id or stream_ctx.new_message_id()
416
- yield ToolCallStartEvent(
417
- tool_call_id=part.tool_call_id,
418
- tool_call_name=part.tool_name,
419
- parent_message_id=message_id,
420
- )
421
- if part.args:
422
- yield ToolCallArgsEvent(
423
- tool_call_id=part.tool_call_id,
424
- delta=part.args if isinstance(part.args, str) else json.dumps(part.args),
425
- )
426
- stream_ctx.part_end = ToolCallEndEvent(
441
+ stream_ctx.part_end = TextMessageEndEvent(
442
+ message_id=message_id,
443
+ )
444
+ elif isinstance(part, ToolCallPart): # pragma: no branch
445
+ message_id = stream_ctx.message_id or stream_ctx.new_message_id()
446
+ yield ToolCallStartEvent(
447
+ tool_call_id=part.tool_call_id,
448
+ tool_call_name=part.tool_name,
449
+ parent_message_id=message_id,
450
+ )
451
+ if part.args:
452
+ yield ToolCallArgsEvent(
427
453
  tool_call_id=part.tool_call_id,
454
+ delta=part.args if isinstance(part.args, str) else json.dumps(part.args),
428
455
  )
456
+ stream_ctx.part_end = ToolCallEndEvent(
457
+ tool_call_id=part.tool_call_id,
458
+ )
429
459
 
430
- elif isinstance(part, ThinkingPart): # pragma: no branch
431
- yield ThinkingTextMessageStartEvent(
432
- type=EventType.THINKING_TEXT_MESSAGE_START,
433
- )
434
- # Always send the content even if it's empty, as it may be
435
- # used to indicate the start of thinking.
460
+ elif isinstance(part, ThinkingPart): # pragma: no branch
461
+ yield ThinkingTextMessageStartEvent(
462
+ type=EventType.THINKING_TEXT_MESSAGE_START,
463
+ )
464
+ # Always send the content even if it's empty, as it may be
465
+ # used to indicate the start of thinking.
466
+ yield ThinkingTextMessageContentEvent(
467
+ type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
468
+ delta=part.content,
469
+ )
470
+ stream_ctx.part_end = ThinkingTextMessageEndEvent(
471
+ type=EventType.THINKING_TEXT_MESSAGE_END,
472
+ )
473
+
474
+ elif isinstance(agent_event, PartDeltaEvent):
475
+ delta = agent_event.delta
476
+ if isinstance(delta, TextPartDelta):
477
+ yield TextMessageContentEvent(
478
+ message_id=stream_ctx.message_id,
479
+ delta=delta.content_delta,
480
+ )
481
+ elif isinstance(delta, ToolCallPartDelta): # pragma: no branch
482
+ assert delta.tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
483
+ yield ToolCallArgsEvent(
484
+ tool_call_id=delta.tool_call_id,
485
+ delta=delta.args_delta if isinstance(delta.args_delta, str) else json.dumps(delta.args_delta),
486
+ )
487
+ elif isinstance(delta, ThinkingPartDelta): # pragma: no branch
488
+ if delta.content_delta: # pragma: no branch
436
489
  yield ThinkingTextMessageContentEvent(
437
490
  type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
438
- delta=part.content,
439
- )
440
- stream_ctx.part_end = ThinkingTextMessageEndEvent(
441
- type=EventType.THINKING_TEXT_MESSAGE_END,
442
- )
443
-
444
- elif isinstance(agent_event, PartDeltaEvent):
445
- delta = agent_event.delta
446
- if isinstance(delta, TextPartDelta):
447
- yield TextMessageContentEvent(
448
- message_id=stream_ctx.message_id,
449
491
  delta=delta.content_delta,
450
492
  )
451
- elif isinstance(delta, ToolCallPartDelta): # pragma: no branch
452
- assert delta.tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
453
- yield ToolCallArgsEvent(
454
- tool_call_id=delta.tool_call_id,
455
- delta=delta.args_delta if isinstance(delta.args_delta, str) else json.dumps(delta.args_delta),
456
- )
457
- elif isinstance(delta, ThinkingPartDelta): # pragma: no branch
458
- if delta.content_delta: # pragma: no branch
459
- yield ThinkingTextMessageContentEvent(
460
- type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
461
- delta=delta.content_delta,
462
- )
463
493
 
464
- async def _handle_tool_result_event(
465
- self,
466
- stream_ctx: _RequestStreamContext,
467
- event: FunctionToolResultEvent,
468
- ) -> AsyncGenerator[BaseEvent, None]:
469
- """Convert a tool call result to AG-UI events.
470
494
 
471
- Args:
472
- stream_ctx: The request stream context to manage state.
473
- event: The tool call result event to process.
495
+ async def _handle_tool_result_event(
496
+ stream_ctx: _RequestStreamContext,
497
+ event: FunctionToolResultEvent,
498
+ ) -> AsyncIterator[BaseEvent]:
499
+ """Convert a tool call result to AG-UI events.
474
500
 
475
- Yields:
476
- AG-UI Server-Sent Events (SSE).
477
- """
478
- result = event.result
479
- if not isinstance(result, ToolReturnPart):
480
- return
481
-
482
- message_id = stream_ctx.new_message_id()
483
- yield ToolCallResultEvent(
484
- message_id=message_id,
485
- type=EventType.TOOL_CALL_RESULT,
486
- role='tool',
487
- tool_call_id=result.tool_call_id,
488
- content=result.model_response_str(),
489
- )
501
+ Args:
502
+ stream_ctx: The request stream context to manage state.
503
+ event: The tool call result event to process.
490
504
 
491
- # Now check for AG-UI events returned by the tool calls.
492
- content = result.content
493
- if isinstance(content, BaseEvent):
494
- yield content
495
- elif isinstance(content, (str, bytes)): # pragma: no branch
496
- # Avoid iterable check for strings and bytes.
497
- pass
498
- elif isinstance(content, Iterable): # pragma: no branch
499
- for item in content: # type: ignore[reportUnknownMemberType]
500
- if isinstance(item, BaseEvent): # pragma: no branch
501
- yield item
505
+ Yields:
506
+ AG-UI Server-Sent Events (SSE).
507
+ """
508
+ result = event.result
509
+ if not isinstance(result, ToolReturnPart):
510
+ return
511
+
512
+ message_id = stream_ctx.new_message_id()
513
+ yield ToolCallResultEvent(
514
+ message_id=message_id,
515
+ type=EventType.TOOL_CALL_RESULT,
516
+ role='tool',
517
+ tool_call_id=result.tool_call_id,
518
+ content=result.model_response_str(),
519
+ )
520
+
521
+ # Now check for AG-UI events returned by the tool calls.
522
+ content = result.content
523
+ if isinstance(content, BaseEvent):
524
+ yield content
525
+ elif isinstance(content, (str, bytes)): # pragma: no branch
526
+ # Avoid iterable check for strings and bytes.
527
+ pass
528
+ elif isinstance(content, Iterable): # pragma: no branch
529
+ for item in content: # type: ignore[reportUnknownMemberType]
530
+ if isinstance(item, BaseEvent): # pragma: no branch
531
+ yield item
502
532
 
503
533
 
504
534
  def _messages_from_ag_ui(messages: list[Message]) -> list[ModelMessage]:
pydantic_ai/agent.py CHANGED
@@ -1870,9 +1870,13 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
1870
1870
  on_shutdown: Sequence[Callable[[], Any]] | None = None,
1871
1871
  lifespan: Lifespan[AGUIApp[AgentDepsT, OutputDataT]] | None = None,
1872
1872
  ) -> AGUIApp[AgentDepsT, OutputDataT]:
1873
- """Convert the agent to an AG-UI application.
1873
+ """Returns an ASGI application that handles every AG-UI request by running the agent.
1874
1874
 
1875
- This allows you to use the agent with a compatible AG-UI frontend.
1875
+ Note that the `deps` will be the same for each request, with the exception of the AG-UI state that's
1876
+ injected into the `state` field of a `deps` object that implements the [`StateHandler`][pydantic_ai.ag_ui.StateHandler] protocol.
1877
+ To provide different `deps` for each request (e.g. based on the authenticated user),
1878
+ use [`pydantic_ai.ag_ui.run_ag_ui`][pydantic_ai.ag_ui.run_ag_ui] or
1879
+ [`pydantic_ai.ag_ui.handle_ag_ui_request`][pydantic_ai.ag_ui.handle_ag_ui_request] instead.
1876
1880
 
1877
1881
  Example:
1878
1882
  ```python
@@ -1882,8 +1886,6 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
1882
1886
  app = agent.to_ag_ui()
1883
1887
  ```
1884
1888
 
1885
- The `app` is an ASGI application that can be used with any ASGI server.
1886
-
1887
1889
  To run the application, you can use the following command:
1888
1890
 
1889
1891
  ```bash
@@ -1902,7 +1904,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
1902
1904
  usage_limits: Optional limits on model request count or token usage.
1903
1905
  usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
1904
1906
  infer_name: Whether to try to infer the agent name from the call frame if it's not set.
1905
- toolsets: Optional list of toolsets to use for this agent, defaults to the agent's toolset.
1907
+ toolsets: Optional additional toolsets for this run.
1906
1908
 
1907
1909
  debug: Boolean indicating if debug tracebacks should be returned on errors.
1908
1910
  routes: A list of routes to serve incoming HTTP and WebSocket requests.
@@ -137,12 +137,12 @@ KnownModelName = TypeAliasType(
137
137
  'google-gla:gemini-2.0-flash',
138
138
  'google-gla:gemini-2.0-flash-lite',
139
139
  'google-gla:gemini-2.5-flash',
140
- 'google-gla:gemini-2.5-flash-lite-preview-06-17',
140
+ 'google-gla:gemini-2.5-flash-lite',
141
141
  'google-gla:gemini-2.5-pro',
142
142
  'google-vertex:gemini-2.0-flash',
143
143
  'google-vertex:gemini-2.0-flash-lite',
144
144
  'google-vertex:gemini-2.5-flash',
145
- 'google-vertex:gemini-2.5-flash-lite-preview-06-17',
145
+ 'google-vertex:gemini-2.5-flash-lite',
146
146
  'google-vertex:gemini-2.5-pro',
147
147
  'gpt-3.5-turbo',
148
148
  'gpt-3.5-turbo-0125',
@@ -192,7 +192,7 @@ class CohereModel(Model):
192
192
  # While Cohere's API returns a list, it only does that for future proofing
193
193
  # and currently only one item is being returned.
194
194
  choice = response.message.content[0]
195
- parts.extend(split_content_into_text_and_thinking(choice.text))
195
+ parts.extend(split_content_into_text_and_thinking(choice.text, self.profile.thinking_tags))
196
196
  for c in response.message.tool_calls or []:
197
197
  if c.function and c.function.name and c.function.arguments: # pragma: no branch
198
198
  parts.append(
@@ -51,7 +51,7 @@ LatestGeminiModelNames = Literal[
51
51
  'gemini-2.0-flash',
52
52
  'gemini-2.0-flash-lite',
53
53
  'gemini-2.5-flash',
54
- 'gemini-2.5-flash-lite-preview-06-17',
54
+ 'gemini-2.5-flash-lite',
55
55
  'gemini-2.5-pro',
56
56
  ]
57
57
  """Latest Gemini models."""
@@ -76,7 +76,7 @@ LatestGoogleModelNames = Literal[
76
76
  'gemini-2.0-flash',
77
77
  'gemini-2.0-flash-lite',
78
78
  'gemini-2.5-flash',
79
- 'gemini-2.5-flash-lite-preview-06-17',
79
+ 'gemini-2.5-flash-lite',
80
80
  'gemini-2.5-pro',
81
81
  ]
82
82
  """Latest Gemini models."""
@@ -30,7 +30,7 @@ from ..messages import (
30
30
  ToolReturnPart,
31
31
  UserPromptPart,
32
32
  )
33
- from ..profiles import ModelProfileSpec
33
+ from ..profiles import ModelProfile, ModelProfileSpec
34
34
  from ..providers import Provider, infer_provider
35
35
  from ..settings import ModelSettings
36
36
  from ..tools import ToolDefinition
@@ -261,7 +261,7 @@ class GroqModel(Model):
261
261
  items.append(ThinkingPart(content=choice.message.reasoning))
262
262
  if choice.message.content is not None:
263
263
  # NOTE: The `<think>` tag is only present if `groq_reasoning_format` is set to `raw`.
264
- items.extend(split_content_into_text_and_thinking(choice.message.content))
264
+ items.extend(split_content_into_text_and_thinking(choice.message.content, self.profile.thinking_tags))
265
265
  if choice.message.tool_calls is not None:
266
266
  for c in choice.message.tool_calls:
267
267
  items.append(ToolCallPart(tool_name=c.function.name, args=c.function.arguments, tool_call_id=c.id))
@@ -281,6 +281,7 @@ class GroqModel(Model):
281
281
  return GroqStreamedResponse(
282
282
  _response=peekable_response,
283
283
  _model_name=self._model_name,
284
+ _model_profile=self.profile,
284
285
  _timestamp=number_to_datetime(first_chunk.created),
285
286
  )
286
287
 
@@ -400,6 +401,7 @@ class GroqStreamedResponse(StreamedResponse):
400
401
  """Implementation of `StreamedResponse` for Groq models."""
401
402
 
402
403
  _model_name: GroqModelName
404
+ _model_profile: ModelProfile
403
405
  _response: AsyncIterable[chat.ChatCompletionChunk]
404
406
  _timestamp: datetime
405
407
 
@@ -416,7 +418,9 @@ class GroqStreamedResponse(StreamedResponse):
416
418
  content = choice.delta.content
417
419
  if content is not None:
418
420
  maybe_event = self._parts_manager.handle_text_delta(
419
- vendor_part_id='content', content=content, extract_think_tags=True
421
+ vendor_part_id='content',
422
+ content=content,
423
+ thinking_tags=self._model_profile.thinking_tags,
420
424
  )
421
425
  if maybe_event is not None: # pragma: no branch
422
426
  yield maybe_event
@@ -33,6 +33,7 @@ from ..messages import (
33
33
  UserPromptPart,
34
34
  VideoUrl,
35
35
  )
36
+ from ..profiles import ModelProfile
36
37
  from ..settings import ModelSettings
37
38
  from ..tools import ToolDefinition
38
39
  from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests
@@ -244,7 +245,7 @@ class HuggingFaceModel(Model):
244
245
  items: list[ModelResponsePart] = []
245
246
 
246
247
  if content is not None:
247
- items.extend(split_content_into_text_and_thinking(content))
248
+ items.extend(split_content_into_text_and_thinking(content, self.profile.thinking_tags))
248
249
  if tool_calls is not None:
249
250
  for c in tool_calls:
250
251
  items.append(ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id))
@@ -267,6 +268,7 @@ class HuggingFaceModel(Model):
267
268
 
268
269
  return HuggingFaceStreamedResponse(
269
270
  _model_name=self._model_name,
271
+ _model_profile=self.profile,
270
272
  _response=peekable_response,
271
273
  _timestamp=datetime.fromtimestamp(first_chunk.created, tz=timezone.utc),
272
274
  )
@@ -412,6 +414,7 @@ class HuggingFaceStreamedResponse(StreamedResponse):
412
414
  """Implementation of `StreamedResponse` for Hugging Face models."""
413
415
 
414
416
  _model_name: str
417
+ _model_profile: ModelProfile
415
418
  _response: AsyncIterable[ChatCompletionStreamOutput]
416
419
  _timestamp: datetime
417
420
 
@@ -428,7 +431,9 @@ class HuggingFaceStreamedResponse(StreamedResponse):
428
431
  content = choice.delta.content
429
432
  if content:
430
433
  maybe_event = self._parts_manager.handle_text_delta(
431
- vendor_part_id='content', content=content, extract_think_tags=True
434
+ vendor_part_id='content',
435
+ content=content,
436
+ thinking_tags=self._model_profile.thinking_tags,
432
437
  )
433
438
  if maybe_event is not None: # pragma: no branch
434
439
  yield maybe_event
@@ -333,7 +333,7 @@ class MistralModel(Model):
333
333
 
334
334
  parts: list[ModelResponsePart] = []
335
335
  if text := _map_content(content):
336
- parts.extend(split_content_into_text_and_thinking(text))
336
+ parts.extend(split_content_into_text_and_thinking(text, self.profile.thinking_tags))
337
337
 
338
338
  if isinstance(tool_calls, list):
339
339
  for tool_call in tool_calls:
@@ -37,7 +37,7 @@ from ..messages import (
37
37
  UserPromptPart,
38
38
  VideoUrl,
39
39
  )
40
- from ..profiles import ModelProfileSpec
40
+ from ..profiles import ModelProfile, ModelProfileSpec
41
41
  from ..settings import ModelSettings
42
42
  from ..tools import ToolDefinition
43
43
  from . import (
@@ -407,7 +407,7 @@ class OpenAIModel(Model):
407
407
  }
408
408
 
409
409
  if choice.message.content is not None:
410
- items.extend(split_content_into_text_and_thinking(choice.message.content))
410
+ items.extend(split_content_into_text_and_thinking(choice.message.content, self.profile.thinking_tags))
411
411
  if choice.message.tool_calls is not None:
412
412
  for c in choice.message.tool_calls:
413
413
  part = ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id)
@@ -433,6 +433,7 @@ class OpenAIModel(Model):
433
433
 
434
434
  return OpenAIStreamedResponse(
435
435
  _model_name=self._model_name,
436
+ _model_profile=self.profile,
436
437
  _response=peekable_response,
437
438
  _timestamp=number_to_datetime(first_chunk.created),
438
439
  )
@@ -1009,6 +1010,7 @@ class OpenAIStreamedResponse(StreamedResponse):
1009
1010
  """Implementation of `StreamedResponse` for OpenAI models."""
1010
1011
 
1011
1012
  _model_name: OpenAIModelName
1013
+ _model_profile: ModelProfile
1012
1014
  _response: AsyncIterable[ChatCompletionChunk]
1013
1015
  _timestamp: datetime
1014
1016
 
@@ -1025,7 +1027,9 @@ class OpenAIStreamedResponse(StreamedResponse):
1025
1027
  content = choice.delta.content
1026
1028
  if content:
1027
1029
  maybe_event = self._parts_manager.handle_text_delta(
1028
- vendor_part_id='content', content=content, extract_think_tags=True
1030
+ vendor_part_id='content',
1031
+ content=content,
1032
+ thinking_tags=self._model_profile.thinking_tags,
1029
1033
  )
1030
1034
  if maybe_event is not None: # pragma: no branch
1031
1035
  yield maybe_event
@@ -123,7 +123,9 @@ class TestModel(Model):
123
123
 
124
124
  model_response = self._request(messages, model_settings, model_request_parameters)
125
125
  yield TestStreamedResponse(
126
- _model_name=self._model_name, _structured_response=model_response, _messages=messages
126
+ _model_name=self._model_name,
127
+ _structured_response=model_response,
128
+ _messages=messages,
127
129
  )
128
130
 
129
131
  @property
@@ -35,6 +35,9 @@ class ModelProfile:
35
35
  json_schema_transformer: type[JsonSchemaTransformer] | None = None
36
36
  """The transformer to use to make JSON schemas for tools and structured output compatible with the model."""
37
37
 
38
+ thinking_tags: tuple[str, str] = ('<think>', '</think>')
39
+ """The tags used to indicate thinking parts in the model's output. Defaults to ('<think>', '</think>')."""
40
+
38
41
  @classmethod
39
42
  def from_profile(cls, profile: ModelProfile | None) -> Self:
40
43
  """Build a ModelProfile subclass instance from a ModelProfile instance."""
@@ -5,4 +5,4 @@ from . import ModelProfile
5
5
 
6
6
  def anthropic_model_profile(model_name: str) -> ModelProfile | None:
7
7
  """Get the model profile for an Anthropic model."""
8
- return None
8
+ return ModelProfile(thinking_tags=('<thinking>', '</thinking>'))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.4.10
3
+ Version: 0.4.11
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.4.10
33
+ Requires-Dist: pydantic-graph==0.4.11
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.4.10; extra == 'evals'
54
+ Requires-Dist: pydantic-evals==0.4.11; extra == 'evals'
55
55
  Provides-Extra: google
56
56
  Requires-Dist: google-genai>=1.24.0; extra == 'google'
57
57
  Provides-Extra: groq
@@ -7,14 +7,14 @@ pydantic_ai/_function_schema.py,sha256=6Xuash0DVpfPF0rWQce0bhtgti8YRyk3B1-OK_n6d
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=2k-nxfPNLJEb-wjnPhQo63lh-yQH1XsIhNG1hjsrim0,37462
10
- pydantic_ai/_parts_manager.py,sha256=T4nlxaS697KeikJoqc1I9kRoIN5-_t5TEv-ovpMlzZg,17856
10
+ pydantic_ai/_parts_manager.py,sha256=lWXN75zLy_MSDz4Wib65lqIPHk1SY8KDU8_OYaxG3yw,17788
11
11
  pydantic_ai/_run_context.py,sha256=pqb_HPXytE1Z9zZRRuBboRYes_tVTC75WGTpZgnb2Ko,1691
12
12
  pydantic_ai/_system_prompt.py,sha256=lUSq-gDZjlYTGtd6BUm54yEvTIvgdwBmJ8mLsNZZtYU,1142
13
- pydantic_ai/_thinking_part.py,sha256=mzx2RZSfiQxAKpljEflrcXRXmFKxtp6bKVyorY3UYZk,1554
13
+ pydantic_ai/_thinking_part.py,sha256=x80-Vkon16GOyq3W6f2qzafTVPC5dCgF7QD3k8ZMmYU,1304
14
14
  pydantic_ai/_tool_manager.py,sha256=BdjPntbSshNvYVpYZUNxb-yib5n4GPqcDinbNpzhBVo,8960
15
15
  pydantic_ai/_utils.py,sha256=0Pte4mjir4YFZJTa6i-H6Cra9NbVwSKjOKegArzUggk,16283
16
- pydantic_ai/ag_ui.py,sha256=T7plrZ-pRDptL3r9kNlB2Vit2mVWMCZ1nhxG0ZTfH5M,25562
17
- pydantic_ai/agent.py,sha256=cN2QbfdPKIj60YfxShfyAe_k4YombGHAG1TYa3uQzOk,106873
16
+ pydantic_ai/ag_ui.py,sha256=snIBVMcUUm3WWZ5P5orikyAzvM-7vGunNMgIudhvK-A,26156
17
+ pydantic_ai/agent.py,sha256=VJOvadfilVm60BxWqxtXrzmNTnN8tuhapvFk2r13RO4,107234
18
18
  pydantic_ai/direct.py,sha256=WRfgke3zm-eeR39LTuh9XI2TrdHXAqO81eDvFwih4Ko,14803
19
19
  pydantic_ai/exceptions.py,sha256=o0l6fBrWI5UhosICVZ2yaT-JEJF05eqBlKlQCW8i9UM,3462
20
20
  pydantic_ai/format_as_xml.py,sha256=IINfh1evWDphGahqHNLBArB5dQ4NIqS3S-kru35ztGg,372
@@ -34,26 +34,26 @@ pydantic_ai/common_tools/tavily.py,sha256=Q1xxSF5HtXAaZ10Pp-OaDOHXwJf2mco9wScGEQ
34
34
  pydantic_ai/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  pydantic_ai/ext/aci.py,sha256=vUaNIj6pRM52x6RkPW_DohSYxJPm75pPUfOMw2i5Xx0,2515
36
36
  pydantic_ai/ext/langchain.py,sha256=GemxfhpyG1JPxj69PbRiSJANnY8Q5s4hSB7wqt-uTbo,2266
37
- pydantic_ai/models/__init__.py,sha256=GZ2YE5qcqI8tNpovlO0_6Ryx92bo8sQTsLmRKiYnSU4,30912
37
+ pydantic_ai/models/__init__.py,sha256=UDi-zXjRt_Zb8kaN5OKMxGXnJtDkROpfa66tSz_wNdI,30884
38
38
  pydantic_ai/models/anthropic.py,sha256=dMPFqIeYCIhoeU_4uk9PmZYQWL1NbkSVmVrBKXplTiI,24167
39
39
  pydantic_ai/models/bedrock.py,sha256=O6wKZDu4L18L1L2Nsa-XMW4ch073FjcLKRA5t_NXcHU,29511
40
- pydantic_ai/models/cohere.py,sha256=lKUXEPqTMqxIJfouDj-Fr1bnfkrPu-JK3Xth7CL03kU,12800
40
+ pydantic_ai/models/cohere.py,sha256=GYhQ6jkCYDHf3ca1835aig9o59XTvsyw4ISAVThYejA,12828
41
41
  pydantic_ai/models/fallback.py,sha256=URaV-dTQWkg99xrlkmknue5lXZWDcEt7cJ1Vsky4oB4,5130
42
42
  pydantic_ai/models/function.py,sha256=iHhG6GYN14XDo3_qbdliv_umY10B7-k11aoDoVF4xP8,13563
43
- pydantic_ai/models/gemini.py,sha256=BMFEiDJXbB0DPj5DKK4kCwXuQHifT2WU-WuthJOqPsI,38138
44
- pydantic_ai/models/google.py,sha256=NNcr2jJlK3eFlSRyaTgDPuSjG2GxOOj0vYDrAfD6rbo,24394
45
- pydantic_ai/models/groq.py,sha256=8-sh8h2sJNZE6TiNUoiTKjmWghtCncxa3BX_xN979XQ,18854
46
- pydantic_ai/models/huggingface.py,sha256=06Rh0Q-p_2LPuny5RIooMx-NWD1rbbhLWP2wL4E6aq0,19019
43
+ pydantic_ai/models/gemini.py,sha256=J-05fngctXSqk3NzLaemt0h6r3S6jmr9ArvlWQE5Q0A,38124
44
+ pydantic_ai/models/google.py,sha256=PNN5Z5VYPioT0-FzS4PoZ33es26AfUqwMBLfHhrElnw,24380
45
+ pydantic_ai/models/groq.py,sha256=JX3Hi8tUJTsTj2A6CGoDhpW4IwNUxgOk0Ta58OCEL_A,19035
46
+ pydantic_ai/models/huggingface.py,sha256=g4Z2C_e_OddYyGKLSOtP4nCL-AbWxmOdkW4zFcFtLq0,19222
47
47
  pydantic_ai/models/instrumented.py,sha256=aqvzspcGexn1Molbu6Mn4EEPRBSoQCCCS_yknJvJJ-8,16205
48
48
  pydantic_ai/models/mcp_sampling.py,sha256=q9nnjNEAAbhrfRc_Qw5z9TtCHMG_SwlCWW9FvKWjh8k,3395
49
- pydantic_ai/models/mistral.py,sha256=u3LcPVqvdI2WHckffbj7zRT5oQn9yYdTRbtEN20Gqpw,31427
50
- pydantic_ai/models/openai.py,sha256=PnbsqSrOUrj3et_2Og7BUMNQF7nOlMdOOWOlWzJH_L0,55941
51
- pydantic_ai/models/test.py,sha256=tkm6K0-G5Mc_iSqVzVIpU9VLil9dfkE1-5az8GGWwTI,18457
49
+ pydantic_ai/models/mistral.py,sha256=bj56Meckuji8r4vowiFJMDSli-HZktocqSqtbzgpXa0,31455
50
+ pydantic_ai/models/openai.py,sha256=Soqb7kZpQLBS6En7hVlhzBMlS07rjISJ9IlH96bBBBU,56122
51
+ pydantic_ai/models/test.py,sha256=lGMblastixKF_f5MhP3TcvLWx7jj94H4ohmL7DMpdGo,18482
52
52
  pydantic_ai/models/wrapper.py,sha256=A5-ncYhPF8c9S_czGoXkd55s2KOQb65p3jbVpwZiFPA,2043
53
- pydantic_ai/profiles/__init__.py,sha256=BXMqUpgRfosmYgcxjKAI9ESCj47JTSa30DhKXEgVLzM,2419
53
+ pydantic_ai/profiles/__init__.py,sha256=uC1_64Pb0O1IMt_SwzvU3W7a2_T3pvdoSDcm8_WI7hw,2592
54
54
  pydantic_ai/profiles/_json_schema.py,sha256=sTNHkaK0kbwmbldZp9JRGQNax0f5Qvwy0HkWuu_nGxU,7179
55
55
  pydantic_ai/profiles/amazon.py,sha256=O4ijm1Lpz01vaSiHrkSeGQhbCKV5lyQVtHYqh0pCW_k,339
56
- pydantic_ai/profiles/anthropic.py,sha256=DtTGh85tbkTrrrn2OrJ4FJKXWUIxUH_1Vw6y5fyMRyM,222
56
+ pydantic_ai/profiles/anthropic.py,sha256=J9N46G8eOjHdQ5CwZSLiwGdPb0eeIMdsMjwosDpvNhI,275
57
57
  pydantic_ai/profiles/cohere.py,sha256=lcL34Ht1jZopwuqoU6OV9l8vN4zwF-jiPjlsEABbSRo,215
58
58
  pydantic_ai/profiles/deepseek.py,sha256=DS_idprnXpMliKziKF0k1neLDJOwUvpatZ3YLaiYnCM,219
59
59
  pydantic_ai/profiles/google.py,sha256=cd5zwtx0MU1Xwm8c-oqi2_OJ2-PMJ8Vy23mxvSJF7ik,4856
@@ -94,8 +94,8 @@ pydantic_ai/toolsets/prefixed.py,sha256=MIStkzUdiU0rk2Y6P19IrTBxspH5pTstGxsqCBt-
94
94
  pydantic_ai/toolsets/prepared.py,sha256=Zjfz6S8In6PBVxoKFN9sKPN984zO6t0awB7Lnq5KODw,1431
95
95
  pydantic_ai/toolsets/renamed.py,sha256=JuLHpi-hYPiSPlaTpN8WiXLiGsywYK0axi2lW2Qs75k,1637
96
96
  pydantic_ai/toolsets/wrapper.py,sha256=WjLoiM1WDuffSJ4mDS6pZrEZGHgZ421fjrqFcB66W94,1205
97
- pydantic_ai_slim-0.4.10.dist-info/METADATA,sha256=k2Hx78rIovi1HJ0FiRIAMd6EXoroqmOSe68hb4-VASg,4176
98
- pydantic_ai_slim-0.4.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
99
- pydantic_ai_slim-0.4.10.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
100
- pydantic_ai_slim-0.4.10.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
101
- pydantic_ai_slim-0.4.10.dist-info/RECORD,,
97
+ pydantic_ai_slim-0.4.11.dist-info/METADATA,sha256=csPdqlBPAN-VkDDT1zV3WRxgwQLM_6saa0dNDI9Iyqs,4176
98
+ pydantic_ai_slim-0.4.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
99
+ pydantic_ai_slim-0.4.11.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
100
+ pydantic_ai_slim-0.4.11.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
101
+ pydantic_ai_slim-0.4.11.dist-info/RECORD,,