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.
- pydantic_ai/_parts_manager.py +8 -9
- pydantic_ai/_thinking_part.py +7 -12
- pydantic_ai/ag_ui.py +346 -316
- pydantic_ai/agent.py +7 -5
- pydantic_ai/models/__init__.py +2 -2
- pydantic_ai/models/cohere.py +1 -1
- pydantic_ai/models/gemini.py +1 -1
- pydantic_ai/models/google.py +1 -1
- pydantic_ai/models/groq.py +7 -3
- pydantic_ai/models/huggingface.py +7 -2
- pydantic_ai/models/mistral.py +1 -1
- pydantic_ai/models/openai.py +7 -3
- pydantic_ai/models/test.py +3 -1
- pydantic_ai/profiles/__init__.py +3 -0
- pydantic_ai/profiles/anthropic.py +1 -1
- {pydantic_ai_slim-0.4.10.dist-info → pydantic_ai_slim-0.4.11.dist-info}/METADATA +3 -3
- {pydantic_ai_slim-0.4.10.dist-info → pydantic_ai_slim-0.4.11.dist-info}/RECORD +20 -20
- {pydantic_ai_slim-0.4.10.dist-info → pydantic_ai_slim-0.4.11.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-0.4.10.dist-info → pydantic_ai_slim-0.4.11.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-0.4.10.dist-info → pydantic_ai_slim-0.4.11.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/_parts_manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
114
|
-
# We may be building a thinking part instead of a text part if we had previously seen a
|
|
115
|
-
if content ==
|
|
116
|
-
# When we see
|
|
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
|
|
127
|
-
# When we see a
|
|
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
|
|
pydantic_ai/_thinking_part.py
CHANGED
|
@@ -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(
|
|
15
|
+
start_index = content.find(start_tag)
|
|
21
16
|
while start_index >= 0:
|
|
22
|
-
before_think, content = content[:start_index], content[start_index + len(
|
|
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(
|
|
20
|
+
end_index = content.find(end_tag)
|
|
26
21
|
if end_index >= 0:
|
|
27
|
-
think_content, content = content[:end_index], content[end_index + len(
|
|
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(
|
|
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,
|
|
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
|
|
25
|
+
from pydantic import BaseModel, ValidationError
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
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[
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
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
|
|
190
|
+
async def endpoint(request: Request) -> Response:
|
|
191
191
|
"""Endpoint to run the agent with the provided input data."""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
315
|
-
pass
|
|
316
|
-
|
|
317
|
-
messages = _messages_from_ag_ui(run_input.messages)
|
|
338
|
+
state = raw_state
|
|
318
339
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
415
|
-
message_id
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
"""
|
|
1873
|
+
"""Returns an ASGI application that handles every AG-UI request by running the agent.
|
|
1874
1874
|
|
|
1875
|
-
|
|
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
|
|
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.
|
pydantic_ai/models/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
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',
|
pydantic_ai/models/cohere.py
CHANGED
|
@@ -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(
|
pydantic_ai/models/gemini.py
CHANGED
pydantic_ai/models/google.py
CHANGED
pydantic_ai/models/groq.py
CHANGED
|
@@ -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',
|
|
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',
|
|
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
|
pydantic_ai/models/mistral.py
CHANGED
|
@@ -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:
|
pydantic_ai/models/openai.py
CHANGED
|
@@ -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',
|
|
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
|
pydantic_ai/models/test.py
CHANGED
|
@@ -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,
|
|
126
|
+
_model_name=self._model_name,
|
|
127
|
+
_structured_response=model_response,
|
|
128
|
+
_messages=messages,
|
|
127
129
|
)
|
|
128
130
|
|
|
129
131
|
@property
|
pydantic_ai/profiles/__init__.py
CHANGED
|
@@ -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."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.4.
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
17
|
-
pydantic_ai/agent.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
44
|
-
pydantic_ai/models/google.py,sha256=
|
|
45
|
-
pydantic_ai/models/groq.py,sha256=
|
|
46
|
-
pydantic_ai/models/huggingface.py,sha256=
|
|
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=
|
|
50
|
-
pydantic_ai/models/openai.py,sha256=
|
|
51
|
-
pydantic_ai/models/test.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
98
|
-
pydantic_ai_slim-0.4.
|
|
99
|
-
pydantic_ai_slim-0.4.
|
|
100
|
-
pydantic_ai_slim-0.4.
|
|
101
|
-
pydantic_ai_slim-0.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|