pydantic-ai-slim 0.2.4__tar.gz → 0.2.5__tar.gz
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.
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/.gitignore +2 -0
- pydantic_ai_slim-0.2.5/LICENSE +21 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/PKG-INFO +7 -4
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_agent_graph.py +8 -6
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_cli.py +3 -3
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_output.py +7 -7
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_parts_manager.py +1 -1
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/agent.py +19 -13
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/direct.py +2 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/exceptions.py +2 -2
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/messages.py +29 -11
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/__init__.py +42 -5
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/anthropic.py +17 -12
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/bedrock.py +10 -9
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/cohere.py +4 -4
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/fallback.py +2 -2
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/function.py +1 -1
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/gemini.py +26 -22
- pydantic_ai_slim-0.2.5/pydantic_ai/models/google.py +570 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/groq.py +12 -6
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/instrumented.py +43 -33
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/mistral.py +15 -9
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/openai.py +45 -7
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/test.py +1 -1
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/wrapper.py +1 -1
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/__init__.py +4 -0
- pydantic_ai_slim-0.2.5/pydantic_ai/providers/google.py +143 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/google_vertex.py +3 -3
- pydantic_ai_slim-0.2.5/pydantic_ai/providers/openrouter.py +69 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/result.py +13 -21
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/tools.py +2 -2
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/usage.py +1 -1
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pyproject.toml +2 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/README.md +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_a2a.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/format_prompt.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/_json_schema.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.2.4 → pydantic_ai_slim-0.2.5}/pydantic_ai/settings.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Pydantic Services Inc. 2024 to present
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
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>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
7
8
|
Classifier: Development Status :: 4 - Beta
|
|
8
9
|
Classifier: Environment :: Console
|
|
9
10
|
Classifier: Environment :: MacOS X
|
|
@@ -29,11 +30,11 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
29
30
|
Requires-Dist: griffe>=1.3.2
|
|
30
31
|
Requires-Dist: httpx>=0.27
|
|
31
32
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
32
|
-
Requires-Dist: pydantic-graph==0.2.
|
|
33
|
+
Requires-Dist: pydantic-graph==0.2.5
|
|
33
34
|
Requires-Dist: pydantic>=2.10
|
|
34
35
|
Requires-Dist: typing-inspection>=0.4.0
|
|
35
36
|
Provides-Extra: a2a
|
|
36
|
-
Requires-Dist: fasta2a==0.2.
|
|
37
|
+
Requires-Dist: fasta2a==0.2.5; extra == 'a2a'
|
|
37
38
|
Provides-Extra: anthropic
|
|
38
39
|
Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
|
|
39
40
|
Provides-Extra: bedrock
|
|
@@ -47,7 +48,9 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
47
48
|
Provides-Extra: duckduckgo
|
|
48
49
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
49
50
|
Provides-Extra: evals
|
|
50
|
-
Requires-Dist: pydantic-evals==0.2.
|
|
51
|
+
Requires-Dist: pydantic-evals==0.2.5; extra == 'evals'
|
|
52
|
+
Provides-Extra: google
|
|
53
|
+
Requires-Dist: google-genai>=1.15.0; extra == 'google'
|
|
51
54
|
Provides-Extra: groq
|
|
52
55
|
Requires-Dist: groq>=0.15.0; extra == 'groq'
|
|
53
56
|
Provides-Extra: logfire
|
|
@@ -196,7 +196,9 @@ class UserPromptNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
196
196
|
for i, part in enumerate(msg.parts):
|
|
197
197
|
if isinstance(part, _messages.SystemPromptPart) and part.dynamic_ref:
|
|
198
198
|
# Look up the runner by its ref
|
|
199
|
-
if runner := self.system_prompt_dynamic_functions.get(
|
|
199
|
+
if runner := self.system_prompt_dynamic_functions.get( # pragma: lax no cover
|
|
200
|
+
part.dynamic_ref
|
|
201
|
+
):
|
|
200
202
|
updated_part_content = await runner.run(run_context)
|
|
201
203
|
msg.parts[i] = _messages.SystemPromptPart(
|
|
202
204
|
updated_part_content, dynamic_ref=part.dynamic_ref
|
|
@@ -265,7 +267,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
265
267
|
if self._did_stream:
|
|
266
268
|
# `self._result` gets set when exiting the `stream` contextmanager, so hitting this
|
|
267
269
|
# means that the stream was started but not finished before `run()` was called
|
|
268
|
-
raise exceptions.AgentRunError('You must finish streaming before calling run()')
|
|
270
|
+
raise exceptions.AgentRunError('You must finish streaming before calling run()') # pragma: no cover
|
|
269
271
|
|
|
270
272
|
return await self._make_request(ctx)
|
|
271
273
|
|
|
@@ -316,7 +318,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
316
318
|
self, ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]]
|
|
317
319
|
) -> CallToolsNode[DepsT, NodeRunEndT]:
|
|
318
320
|
if self._result is not None:
|
|
319
|
-
return self._result
|
|
321
|
+
return self._result # pragma: no cover
|
|
320
322
|
|
|
321
323
|
model_settings, model_request_parameters = await self._prepare_request(ctx)
|
|
322
324
|
model_request_parameters = ctx.deps.model.customize_request_parameters(model_request_parameters)
|
|
@@ -333,7 +335,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
333
335
|
ctx.state.message_history.append(self.request)
|
|
334
336
|
|
|
335
337
|
# Check usage
|
|
336
|
-
if ctx.deps.usage_limits:
|
|
338
|
+
if ctx.deps.usage_limits: # pragma: no branch
|
|
337
339
|
ctx.deps.usage_limits.check_before_request(ctx.state.usage)
|
|
338
340
|
|
|
339
341
|
# Increment run_step
|
|
@@ -350,7 +352,7 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
350
352
|
) -> CallToolsNode[DepsT, NodeRunEndT]:
|
|
351
353
|
# Update usage
|
|
352
354
|
ctx.state.usage.incr(response.usage)
|
|
353
|
-
if ctx.deps.usage_limits:
|
|
355
|
+
if ctx.deps.usage_limits: # pragma: no branch
|
|
354
356
|
ctx.deps.usage_limits.check_tokens(ctx.state.usage)
|
|
355
357
|
|
|
356
358
|
# Append the model response to state.message_history
|
|
@@ -735,7 +737,7 @@ async def _tool_from_mcp_server(
|
|
|
735
737
|
|
|
736
738
|
for server in ctx.deps.mcp_servers:
|
|
737
739
|
tools = await server.list_tools()
|
|
738
|
-
if tool_name in {tool.name for tool in tools}:
|
|
740
|
+
if tool_name in {tool.name for tool in tools}: # pragma: no branch
|
|
739
741
|
return Tool(name=tool_name, function=run_tool, takes_ctx=True, max_retries=ctx.deps.default_retries)
|
|
740
742
|
return None
|
|
741
743
|
|
|
@@ -57,7 +57,7 @@ PROMPT_HISTORY_PATH = PYDANTIC_AI_HOME / 'prompt-history.txt'
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
class SimpleCodeBlock(CodeBlock):
|
|
60
|
-
"""
|
|
60
|
+
"""Customized code blocks in markdown.
|
|
61
61
|
|
|
62
62
|
This avoids a background color which messes up copy-pasting and sets the language name as dim prefix and suffix.
|
|
63
63
|
"""
|
|
@@ -70,7 +70,7 @@ class SimpleCodeBlock(CodeBlock):
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
class LeftHeading(Heading):
|
|
73
|
-
"""
|
|
73
|
+
"""Customized headings in markdown to stop centering and prepend markdown style hashes."""
|
|
74
74
|
|
|
75
75
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
76
76
|
# note we use `Style(bold=True)` not `self.style_name` here to disable underlining which is ugly IMHO
|
|
@@ -202,7 +202,7 @@ Special prompts:
|
|
|
202
202
|
elif args.code_theme == 'dark':
|
|
203
203
|
code_theme = 'monokai'
|
|
204
204
|
else:
|
|
205
|
-
code_theme = args.code_theme
|
|
205
|
+
code_theme = args.code_theme # pragma: no cover
|
|
206
206
|
|
|
207
207
|
if prompt := cast(str, args.prompt):
|
|
208
208
|
try:
|
|
@@ -140,8 +140,8 @@ class OutputSchema(Generic[OutputDataT]):
|
|
|
140
140
|
self, parts: Iterable[_messages.ModelResponsePart], tool_name: str
|
|
141
141
|
) -> tuple[_messages.ToolCallPart, OutputSchemaTool[OutputDataT]] | None:
|
|
142
142
|
"""Find a tool that matches one of the calls, with a specific name."""
|
|
143
|
-
for part in parts:
|
|
144
|
-
if isinstance(part, _messages.ToolCallPart):
|
|
143
|
+
for part in parts: # pragma: no branch
|
|
144
|
+
if isinstance(part, _messages.ToolCallPart): # pragma: no branch
|
|
145
145
|
if part.tool_name == tool_name:
|
|
146
146
|
return part, self.tools[tool_name]
|
|
147
147
|
|
|
@@ -151,7 +151,7 @@ class OutputSchema(Generic[OutputDataT]):
|
|
|
151
151
|
) -> Iterator[tuple[_messages.ToolCallPart, OutputSchemaTool[OutputDataT]]]:
|
|
152
152
|
"""Find a tool that matches one of the calls."""
|
|
153
153
|
for part in parts:
|
|
154
|
-
if isinstance(part, _messages.ToolCallPart):
|
|
154
|
+
if isinstance(part, _messages.ToolCallPart): # pragma: no branch
|
|
155
155
|
if result := self.tools.get(part.tool_name):
|
|
156
156
|
yield part, result
|
|
157
157
|
|
|
@@ -201,7 +201,7 @@ class OutputSchemaTool(Generic[OutputDataT]):
|
|
|
201
201
|
if description is None:
|
|
202
202
|
tool_description = json_schema_description
|
|
203
203
|
else:
|
|
204
|
-
tool_description = f'{description}. {json_schema_description}'
|
|
204
|
+
tool_description = f'{description}. {json_schema_description}' # pragma: no cover
|
|
205
205
|
else:
|
|
206
206
|
tool_description = description or DEFAULT_DESCRIPTION
|
|
207
207
|
if multiple:
|
|
@@ -243,7 +243,7 @@ class OutputSchemaTool(Generic[OutputDataT]):
|
|
|
243
243
|
)
|
|
244
244
|
raise ToolRetryError(m) from e
|
|
245
245
|
else:
|
|
246
|
-
raise
|
|
246
|
+
raise # pragma: lax no cover
|
|
247
247
|
else:
|
|
248
248
|
if k := self.tool_def.outer_typed_dict_key:
|
|
249
249
|
output = output[k]
|
|
@@ -269,11 +269,11 @@ def extract_str_from_union(output_type: Any) -> _utils.Option[Any]:
|
|
|
269
269
|
includes_str = True
|
|
270
270
|
else:
|
|
271
271
|
remain_args.append(arg)
|
|
272
|
-
if includes_str:
|
|
272
|
+
if includes_str: # pragma: no branch
|
|
273
273
|
if len(remain_args) == 1:
|
|
274
274
|
return _utils.Some(remain_args[0])
|
|
275
275
|
else:
|
|
276
|
-
return _utils.Some(Union[tuple(remain_args)])
|
|
276
|
+
return _utils.Some(Union[tuple(remain_args)]) # pragma: no cover
|
|
277
277
|
|
|
278
278
|
|
|
279
279
|
def get_union_args(tp: Any) -> tuple[Any, ...]:
|
|
@@ -164,7 +164,7 @@ class ModelResponsePartsManager:
|
|
|
164
164
|
if tool_name is None and self._parts:
|
|
165
165
|
part_index = len(self._parts) - 1
|
|
166
166
|
latest_part = self._parts[part_index]
|
|
167
|
-
if isinstance(latest_part, (ToolCallPart, ToolCallPartDelta)):
|
|
167
|
+
if isinstance(latest_part, (ToolCallPart, ToolCallPartDelta)): # pragma: no branch
|
|
168
168
|
existing_matching_part_and_index = latest_part, part_index
|
|
169
169
|
else:
|
|
170
170
|
# vendor_part_id is provided, so look up the corresponding part or delta
|
|
@@ -585,6 +585,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
585
585
|
model_name='gpt-4o',
|
|
586
586
|
timestamp=datetime.datetime(...),
|
|
587
587
|
kind='response',
|
|
588
|
+
vendor_id=None,
|
|
588
589
|
)
|
|
589
590
|
),
|
|
590
591
|
End(data=FinalResult(output='Paris', tool_name=None, tool_call_id=None)),
|
|
@@ -654,8 +655,10 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
654
655
|
usage_limits = usage_limits or _usage.UsageLimits()
|
|
655
656
|
|
|
656
657
|
if isinstance(model_used, InstrumentedModel):
|
|
658
|
+
instrumentation_settings = model_used.settings
|
|
657
659
|
tracer = model_used.settings.tracer
|
|
658
660
|
else:
|
|
661
|
+
instrumentation_settings = None
|
|
659
662
|
tracer = NoOpTracer()
|
|
660
663
|
agent_name = self.name or 'agent'
|
|
661
664
|
run_span = tracer.start_span(
|
|
@@ -723,19 +726,18 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
723
726
|
)
|
|
724
727
|
finally:
|
|
725
728
|
try:
|
|
726
|
-
if run_span.is_recording():
|
|
727
|
-
run_span.set_attributes(self._run_span_end_attributes(state, usage))
|
|
729
|
+
if instrumentation_settings and run_span.is_recording():
|
|
730
|
+
run_span.set_attributes(self._run_span_end_attributes(state, usage, instrumentation_settings))
|
|
728
731
|
finally:
|
|
729
732
|
run_span.end()
|
|
730
733
|
|
|
731
|
-
def _run_span_end_attributes(
|
|
734
|
+
def _run_span_end_attributes(
|
|
735
|
+
self, state: _agent_graph.GraphAgentState, usage: _usage.Usage, settings: InstrumentationSettings
|
|
736
|
+
):
|
|
732
737
|
return {
|
|
733
738
|
**usage.opentelemetry_attributes(),
|
|
734
739
|
'all_messages_events': json.dumps(
|
|
735
|
-
[
|
|
736
|
-
InstrumentedModel.event_to_dict(e)
|
|
737
|
-
for e in InstrumentedModel.messages_to_otel_events(state.message_history)
|
|
738
|
-
]
|
|
740
|
+
[InstrumentedModel.event_to_dict(e) for e in settings.messages_to_otel_events(state.message_history)]
|
|
739
741
|
),
|
|
740
742
|
'logfire.json_schema': json.dumps(
|
|
741
743
|
{
|
|
@@ -1001,7 +1003,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1001
1003
|
final_result_details = await stream_to_final(streamed_response)
|
|
1002
1004
|
if final_result_details is not None:
|
|
1003
1005
|
if yielded:
|
|
1004
|
-
raise exceptions.AgentRunError('Agent run produced final results')
|
|
1006
|
+
raise exceptions.AgentRunError('Agent run produced final results') # pragma: no cover
|
|
1005
1007
|
yielded = True
|
|
1006
1008
|
|
|
1007
1009
|
messages = graph_ctx.state.message_history.copy()
|
|
@@ -1048,11 +1050,13 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1048
1050
|
break
|
|
1049
1051
|
next_node = await agent_run.next(node)
|
|
1050
1052
|
if not isinstance(next_node, _agent_graph.AgentNode):
|
|
1051
|
-
raise exceptions.AgentRunError(
|
|
1053
|
+
raise exceptions.AgentRunError( # pragma: no cover
|
|
1054
|
+
'Should have produced a StreamedRunResult before getting here'
|
|
1055
|
+
)
|
|
1052
1056
|
node = cast(_agent_graph.AgentNode[Any, Any], next_node)
|
|
1053
1057
|
|
|
1054
1058
|
if not yielded:
|
|
1055
|
-
raise exceptions.AgentRunError('Agent run finished without producing a final result')
|
|
1059
|
+
raise exceptions.AgentRunError('Agent run finished without producing a final result') # pragma: no cover
|
|
1056
1060
|
|
|
1057
1061
|
@contextmanager
|
|
1058
1062
|
def override(
|
|
@@ -1226,7 +1230,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1226
1230
|
) -> _system_prompt.SystemPromptFunc[AgentDepsT]:
|
|
1227
1231
|
runner = _system_prompt.SystemPromptRunner[AgentDepsT](func_, dynamic=dynamic)
|
|
1228
1232
|
self._system_prompt_functions.append(runner)
|
|
1229
|
-
if dynamic:
|
|
1233
|
+
if dynamic: # pragma: lax no cover
|
|
1230
1234
|
self._system_prompt_dynamic_functions[func_.__qualname__] = runner
|
|
1231
1235
|
return func_
|
|
1232
1236
|
|
|
@@ -1608,7 +1612,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1608
1612
|
if item is self:
|
|
1609
1613
|
self.name = name
|
|
1610
1614
|
return
|
|
1611
|
-
if parent_frame.f_locals != parent_frame.f_globals:
|
|
1615
|
+
if parent_frame.f_locals != parent_frame.f_globals: # pragma: no branch
|
|
1612
1616
|
# if we couldn't find the agent in locals and globals are a different dict, try globals
|
|
1613
1617
|
for name, item in parent_frame.f_globals.items():
|
|
1614
1618
|
if item is self:
|
|
@@ -1851,6 +1855,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
|
1851
1855
|
model_name='gpt-4o',
|
|
1852
1856
|
timestamp=datetime.datetime(...),
|
|
1853
1857
|
kind='response',
|
|
1858
|
+
vendor_id=None,
|
|
1854
1859
|
)
|
|
1855
1860
|
),
|
|
1856
1861
|
End(data=FinalResult(output='Paris', tool_name=None, tool_call_id=None)),
|
|
@@ -1996,6 +2001,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
|
1996
2001
|
model_name='gpt-4o',
|
|
1997
2002
|
timestamp=datetime.datetime(...),
|
|
1998
2003
|
kind='response',
|
|
2004
|
+
vendor_id=None,
|
|
1999
2005
|
)
|
|
2000
2006
|
),
|
|
2001
2007
|
End(data=FinalResult(output='Paris', tool_name=None, tool_call_id=None)),
|
|
@@ -2024,7 +2030,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
|
2024
2030
|
"""Get usage statistics for the run so far, including token usage, model requests, and so on."""
|
|
2025
2031
|
return self._graph_run.state.usage
|
|
2026
2032
|
|
|
2027
|
-
def __repr__(self) -> str:
|
|
2033
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
2028
2034
|
result = self._graph_run.result
|
|
2029
2035
|
result_repr = '<run not finished>' if result is None else repr(result.output)
|
|
2030
2036
|
return f'<{type(self).__name__} result={result_repr} usage={self.usage()}>'
|
|
@@ -52,6 +52,7 @@ async def model_request(
|
|
|
52
52
|
model_name='claude-3-5-haiku-latest',
|
|
53
53
|
timestamp=datetime.datetime(...),
|
|
54
54
|
kind='response',
|
|
55
|
+
vendor_id=None,
|
|
55
56
|
)
|
|
56
57
|
'''
|
|
57
58
|
```
|
|
@@ -108,6 +109,7 @@ def model_request_sync(
|
|
|
108
109
|
model_name='claude-3-5-haiku-latest',
|
|
109
110
|
timestamp=datetime.datetime(...),
|
|
110
111
|
kind='response',
|
|
112
|
+
vendor_id=None,
|
|
111
113
|
)
|
|
112
114
|
'''
|
|
113
115
|
```
|
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
if sys.version_info < (3, 11):
|
|
7
|
-
from exceptiongroup import ExceptionGroup
|
|
7
|
+
from exceptiongroup import ExceptionGroup # pragma: lax no cover
|
|
8
8
|
else:
|
|
9
|
-
ExceptionGroup = ExceptionGroup
|
|
9
|
+
ExceptionGroup = ExceptionGroup # pragma: lax no cover
|
|
10
10
|
|
|
11
11
|
__all__ = (
|
|
12
12
|
'ModelRetry',
|
|
@@ -6,7 +6,7 @@ from collections.abc import Sequence
|
|
|
6
6
|
from dataclasses import dataclass, field, replace
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from mimetypes import guess_type
|
|
9
|
-
from typing import Annotated, Any, Literal, Union, cast, overload
|
|
9
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Union, cast, overload
|
|
10
10
|
|
|
11
11
|
import pydantic
|
|
12
12
|
import pydantic_core
|
|
@@ -17,6 +17,10 @@ from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as
|
|
|
17
17
|
from .exceptions import UnexpectedModelBehavior
|
|
18
18
|
from .usage import Usage
|
|
19
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .models.instrumented import InstrumentationSettings
|
|
22
|
+
|
|
23
|
+
|
|
20
24
|
AudioMediaType: TypeAlias = Literal['audio/wav', 'audio/mpeg']
|
|
21
25
|
ImageMediaType: TypeAlias = Literal['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
|
22
26
|
DocumentMediaType: TypeAlias = Literal[
|
|
@@ -68,7 +72,7 @@ class SystemPromptPart:
|
|
|
68
72
|
part_kind: Literal['system-prompt'] = 'system-prompt'
|
|
69
73
|
"""Part type identifier, this is available on all parts as a discriminator."""
|
|
70
74
|
|
|
71
|
-
def otel_event(self) -> Event:
|
|
75
|
+
def otel_event(self, _settings: InstrumentationSettings) -> Event:
|
|
72
76
|
return Event('gen_ai.system.message', body={'content': self.content, 'role': 'system'})
|
|
73
77
|
|
|
74
78
|
|
|
@@ -305,7 +309,7 @@ class UserPromptPart:
|
|
|
305
309
|
part_kind: Literal['user-prompt'] = 'user-prompt'
|
|
306
310
|
"""Part type identifier, this is available on all parts as a discriminator."""
|
|
307
311
|
|
|
308
|
-
def otel_event(self) -> Event:
|
|
312
|
+
def otel_event(self, settings: InstrumentationSettings) -> Event:
|
|
309
313
|
content: str | list[dict[str, Any] | str]
|
|
310
314
|
if isinstance(self.content, str):
|
|
311
315
|
content = self.content
|
|
@@ -317,10 +321,12 @@ class UserPromptPart:
|
|
|
317
321
|
elif isinstance(part, (ImageUrl, AudioUrl, DocumentUrl, VideoUrl)):
|
|
318
322
|
content.append({'kind': part.kind, 'url': part.url})
|
|
319
323
|
elif isinstance(part, BinaryContent):
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
converted_part = {'kind': part.kind, 'media_type': part.media_type}
|
|
325
|
+
if settings.include_binary_content:
|
|
326
|
+
converted_part['binary_content'] = base64.b64encode(part.data).decode()
|
|
327
|
+
content.append(converted_part)
|
|
322
328
|
else:
|
|
323
|
-
content.append({'kind': part.kind})
|
|
329
|
+
content.append({'kind': part.kind}) # pragma: no cover
|
|
324
330
|
return Event('gen_ai.user.message', body={'content': content, 'role': 'user'})
|
|
325
331
|
|
|
326
332
|
|
|
@@ -357,11 +363,11 @@ class ToolReturnPart:
|
|
|
357
363
|
"""Return a dictionary representation of the content, wrapping non-dict types appropriately."""
|
|
358
364
|
# gemini supports JSON dict return values, but no other JSON types, hence we wrap anything else in a dict
|
|
359
365
|
if isinstance(self.content, dict):
|
|
360
|
-
return tool_return_ta.dump_python(self.content, mode='json') # pyright: ignore[reportUnknownMemberType]
|
|
366
|
+
return tool_return_ta.dump_python(self.content, mode='json') # pyright: ignore[reportUnknownMemberType] # pragma: no cover
|
|
361
367
|
else:
|
|
362
368
|
return {'return_value': tool_return_ta.dump_python(self.content, mode='json')}
|
|
363
369
|
|
|
364
|
-
def otel_event(self) -> Event:
|
|
370
|
+
def otel_event(self, _settings: InstrumentationSettings) -> Event:
|
|
365
371
|
return Event(
|
|
366
372
|
'gen_ai.tool.message',
|
|
367
373
|
body={'content': self.content, 'role': 'tool', 'id': self.tool_call_id, 'name': self.tool_name},
|
|
@@ -418,7 +424,7 @@ class RetryPromptPart:
|
|
|
418
424
|
description = f'{len(self.content)} validation errors: {json_errors.decode()}'
|
|
419
425
|
return f'{description}\n\nFix the errors and try again.'
|
|
420
426
|
|
|
421
|
-
def otel_event(self) -> Event:
|
|
427
|
+
def otel_event(self, _settings: InstrumentationSettings) -> Event:
|
|
422
428
|
if self.tool_name is None:
|
|
423
429
|
return Event('gen_ai.user.message', body={'content': self.model_response(), 'role': 'user'})
|
|
424
430
|
else:
|
|
@@ -556,6 +562,16 @@ class ModelResponse:
|
|
|
556
562
|
kind: Literal['response'] = 'response'
|
|
557
563
|
"""Message type identifier, this is available on all parts as a discriminator."""
|
|
558
564
|
|
|
565
|
+
vendor_details: dict[str, Any] | None = field(default=None, repr=False)
|
|
566
|
+
"""Additional vendor-specific details in a serializable format.
|
|
567
|
+
|
|
568
|
+
This allows storing selected vendor-specific data that isn't mapped to standard ModelResponse fields.
|
|
569
|
+
For OpenAI models, this may include 'logprobs', 'finish_reason', etc.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
vendor_id: str | None = None
|
|
573
|
+
"""Vendor ID as specified by the model provider. This can be used to track the specific request to the model."""
|
|
574
|
+
|
|
559
575
|
def otel_events(self) -> list[Event]:
|
|
560
576
|
"""Return OpenTelemetry events for the response."""
|
|
561
577
|
result: list[Event] = []
|
|
@@ -619,7 +635,7 @@ class TextPartDelta:
|
|
|
619
635
|
ValueError: If `part` is not a `TextPart`.
|
|
620
636
|
"""
|
|
621
637
|
if not isinstance(part, TextPart):
|
|
622
|
-
raise ValueError('Cannot apply TextPartDeltas to non-TextParts')
|
|
638
|
+
raise ValueError('Cannot apply TextPartDeltas to non-TextParts') # pragma: no cover
|
|
623
639
|
return replace(part, content=part.content + self.content_delta)
|
|
624
640
|
|
|
625
641
|
|
|
@@ -682,7 +698,9 @@ class ToolCallPartDelta:
|
|
|
682
698
|
if isinstance(part, ToolCallPartDelta):
|
|
683
699
|
return self._apply_to_delta(part)
|
|
684
700
|
|
|
685
|
-
raise ValueError(
|
|
701
|
+
raise ValueError( # pragma: no cover
|
|
702
|
+
f'Can only apply ToolCallPartDeltas to ToolCallParts or ToolCallPartDeltas, not {part}'
|
|
703
|
+
)
|
|
686
704
|
|
|
687
705
|
def _apply_to_delta(self, delta: ToolCallPartDelta) -> ToolCallPart | ToolCallPartDelta:
|
|
688
706
|
"""Internal helper to apply this delta to another delta."""
|
|
@@ -324,11 +324,48 @@ class Model(ABC):
|
|
|
324
324
|
"""The base URL for the provider API, if available."""
|
|
325
325
|
return None
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
@staticmethod
|
|
328
|
+
def _get_instructions(messages: list[ModelMessage]) -> str | None:
|
|
329
|
+
"""Get instructions from the first ModelRequest found when iterating messages in reverse.
|
|
330
|
+
|
|
331
|
+
In the case that a "mock" request was generated to include a tool-return part for a result tool,
|
|
332
|
+
we want to use the instructions from the second-to-most-recent request (which should correspond to the
|
|
333
|
+
original request that generated the response that resulted in the tool-return part).
|
|
334
|
+
"""
|
|
335
|
+
last_two_requests: list[ModelRequest] = []
|
|
329
336
|
for message in reversed(messages):
|
|
330
337
|
if isinstance(message, ModelRequest):
|
|
331
|
-
|
|
338
|
+
last_two_requests.append(message)
|
|
339
|
+
if len(last_two_requests) == 2:
|
|
340
|
+
break
|
|
341
|
+
if message.instructions is not None:
|
|
342
|
+
return message.instructions
|
|
343
|
+
|
|
344
|
+
# If we don't have two requests, and we didn't already return instructions, there are definitely not any:
|
|
345
|
+
if len(last_two_requests) != 2:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
most_recent_request = last_two_requests[0]
|
|
349
|
+
second_most_recent_request = last_two_requests[1]
|
|
350
|
+
|
|
351
|
+
# If we've gotten this far and the most recent request consists of only tool-return parts or retry-prompt parts,
|
|
352
|
+
# we use the instructions from the second-to-most-recent request. This is necessary because when handling
|
|
353
|
+
# result tools, we generate a "mock" ModelRequest with a tool-return part for it, and that ModelRequest will not
|
|
354
|
+
# have the relevant instructions from the agent.
|
|
355
|
+
|
|
356
|
+
# While it's possible that you could have a message history where the most recent request has only tool returns,
|
|
357
|
+
# I believe there is no way to achieve that would _change_ the instructions without manually crafting the most
|
|
358
|
+
# recent message. That might make sense in principle for some usage pattern, but it's enough of an edge case
|
|
359
|
+
# that I think it's not worth worrying about, since you can work around this by inserting another ModelRequest
|
|
360
|
+
# with no parts at all immediately before the request that has the tool calls (that works because we only look
|
|
361
|
+
# at the two most recent ModelRequests here).
|
|
362
|
+
|
|
363
|
+
# If you have a use case where this causes pain, please open a GitHub issue and we can discuss alternatives.
|
|
364
|
+
|
|
365
|
+
if all(p.part_kind == 'tool-return' or p.part_kind == 'retry-prompt' for p in most_recent_request.parts):
|
|
366
|
+
return second_most_recent_request.instructions
|
|
367
|
+
|
|
368
|
+
return None
|
|
332
369
|
|
|
333
370
|
|
|
334
371
|
@dataclass
|
|
@@ -448,7 +485,7 @@ def infer_model(model: Model | KnownModelName | str) -> Model:
|
|
|
448
485
|
raise UserError(f'Unknown model: {model}')
|
|
449
486
|
|
|
450
487
|
if provider == 'vertexai':
|
|
451
|
-
provider = 'google-vertex'
|
|
488
|
+
provider = 'google-vertex' # pragma: no cover
|
|
452
489
|
|
|
453
490
|
if provider == 'cohere':
|
|
454
491
|
from .cohere import CohereModel
|
|
@@ -479,7 +516,7 @@ def infer_model(model: Model | KnownModelName | str) -> Model:
|
|
|
479
516
|
|
|
480
517
|
return BedrockConverseModel(model_name, provider=provider)
|
|
481
518
|
else:
|
|
482
|
-
raise UserError(f'Unknown model: {model}')
|
|
519
|
+
raise UserError(f'Unknown model: {model}') # pragma: no cover
|
|
483
520
|
|
|
484
521
|
|
|
485
522
|
def cached_async_http_client(*, provider: str | None = None, timeout: int = 600, connect: int = 5) -> httpx.AsyncClient:
|
|
@@ -244,7 +244,7 @@ class AnthropicModel(Model):
|
|
|
244
244
|
except APIStatusError as e:
|
|
245
245
|
if (status_code := e.status_code) >= 400:
|
|
246
246
|
raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
|
|
247
|
-
raise
|
|
247
|
+
raise # pragma: lax no cover
|
|
248
248
|
|
|
249
249
|
def _process_response(self, response: AnthropicMessage) -> ModelResponse:
|
|
250
250
|
"""Process a non-streamed response, and prepare a message to return."""
|
|
@@ -262,13 +262,13 @@ class AnthropicModel(Model):
|
|
|
262
262
|
)
|
|
263
263
|
)
|
|
264
264
|
|
|
265
|
-
return ModelResponse(items, usage=_map_usage(response), model_name=response.model)
|
|
265
|
+
return ModelResponse(items, usage=_map_usage(response), model_name=response.model, vendor_id=response.id)
|
|
266
266
|
|
|
267
267
|
async def _process_streamed_response(self, response: AsyncStream[RawMessageStreamEvent]) -> StreamedResponse:
|
|
268
268
|
peekable_response = _utils.PeekableAsyncStream(response)
|
|
269
269
|
first_chunk = await peekable_response.peek()
|
|
270
270
|
if isinstance(first_chunk, _utils.Unset):
|
|
271
|
-
raise UnexpectedModelBehavior('Streamed response ended without content or tool calls')
|
|
271
|
+
raise UnexpectedModelBehavior('Streamed response ended without content or tool calls') # pragma: no cover
|
|
272
272
|
|
|
273
273
|
# Since Anthropic doesn't provide a timestamp in the message, we'll use the current time
|
|
274
274
|
timestamp = datetime.now(tz=timezone.utc)
|
|
@@ -305,9 +305,10 @@ class AnthropicModel(Model):
|
|
|
305
305
|
is_error=False,
|
|
306
306
|
)
|
|
307
307
|
user_content_params.append(tool_result_block_param)
|
|
308
|
-
elif isinstance(request_part, RetryPromptPart):
|
|
308
|
+
elif isinstance(request_part, RetryPromptPart): # pragma: no branch
|
|
309
309
|
if request_part.tool_name is None:
|
|
310
|
-
|
|
310
|
+
text = request_part.model_response() # pragma: no cover
|
|
311
|
+
retry_param = TextBlockParam(type='text', text=text) # pragma: no cover
|
|
311
312
|
else:
|
|
312
313
|
retry_param = ToolResultBlockParam(
|
|
313
314
|
tool_use_id=_guard_tool_call_id(t=request_part),
|
|
@@ -380,7 +381,7 @@ class AnthropicModel(Model):
|
|
|
380
381
|
else: # pragma: no cover
|
|
381
382
|
raise RuntimeError(f'Unsupported media type: {item.media_type}')
|
|
382
383
|
else:
|
|
383
|
-
raise RuntimeError(f'Unsupported content type: {type(item)}')
|
|
384
|
+
raise RuntimeError(f'Unsupported content type: {type(item)}') # pragma: no cover
|
|
384
385
|
|
|
385
386
|
@staticmethod
|
|
386
387
|
def _map_tool_definition(f: ToolDefinition) -> ToolParam:
|
|
@@ -447,21 +448,25 @@ class AnthropicStreamedResponse(StreamedResponse):
|
|
|
447
448
|
if isinstance(event, RawContentBlockStartEvent):
|
|
448
449
|
current_block = event.content_block
|
|
449
450
|
if isinstance(current_block, TextBlock) and current_block.text:
|
|
450
|
-
yield self._parts_manager.handle_text_delta(
|
|
451
|
-
|
|
451
|
+
yield self._parts_manager.handle_text_delta( # pragma: lax no cover
|
|
452
|
+
vendor_part_id='content', content=current_block.text
|
|
453
|
+
)
|
|
454
|
+
elif isinstance(current_block, ToolUseBlock): # pragma: no branch
|
|
452
455
|
maybe_event = self._parts_manager.handle_tool_call_delta(
|
|
453
456
|
vendor_part_id=current_block.id,
|
|
454
457
|
tool_name=current_block.name,
|
|
455
458
|
args=cast(dict[str, Any], current_block.input),
|
|
456
459
|
tool_call_id=current_block.id,
|
|
457
460
|
)
|
|
458
|
-
if maybe_event is not None:
|
|
461
|
+
if maybe_event is not None: # pragma: no branch
|
|
459
462
|
yield maybe_event
|
|
460
463
|
|
|
461
464
|
elif isinstance(event, RawContentBlockDeltaEvent):
|
|
462
465
|
if isinstance(event.delta, TextDelta):
|
|
463
|
-
yield self._parts_manager.handle_text_delta(
|
|
464
|
-
|
|
466
|
+
yield self._parts_manager.handle_text_delta( # pragma: no cover
|
|
467
|
+
vendor_part_id='content', content=event.delta.text
|
|
468
|
+
)
|
|
469
|
+
elif ( # pragma: no branch
|
|
465
470
|
current_block and event.delta.type == 'input_json_delta' and isinstance(current_block, ToolUseBlock)
|
|
466
471
|
):
|
|
467
472
|
# Try to parse the JSON immediately, otherwise cache the value for later. This handles
|
|
@@ -480,7 +485,7 @@ class AnthropicStreamedResponse(StreamedResponse):
|
|
|
480
485
|
args=parsed_args,
|
|
481
486
|
tool_call_id=current_block.id,
|
|
482
487
|
)
|
|
483
|
-
if maybe_event is not None:
|
|
488
|
+
if maybe_event is not None: # pragma: no branch
|
|
484
489
|
yield maybe_event
|
|
485
490
|
|
|
486
491
|
elif isinstance(event, (RawContentBlockStopEvent, RawMessageStopEvent)):
|