pydantic-ai-slim 0.2.3__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.3 → pydantic_ai_slim-0.2.5}/.gitignore +2 -0
- pydantic_ai_slim-0.2.5/LICENSE +21 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/PKG-INFO +7 -5
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_agent_graph.py +8 -6
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_cli.py +32 -24
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_output.py +7 -7
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_parts_manager.py +1 -1
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/agent.py +19 -13
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/direct.py +2 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/exceptions.py +2 -2
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/messages.py +29 -11
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/__init__.py +42 -5
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/anthropic.py +17 -12
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/bedrock.py +10 -9
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/cohere.py +4 -4
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/fallback.py +2 -2
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/function.py +1 -1
- {pydantic_ai_slim-0.2.3 → 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.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/groq.py +12 -6
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/instrumented.py +43 -33
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/mistral.py +15 -9
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/openai.py +45 -7
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/test.py +1 -1
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/wrapper.py +1 -1
- {pydantic_ai_slim-0.2.3 → 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.3 → 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.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/result.py +13 -21
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/tools.py +2 -2
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/usage.py +1 -1
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pyproject.toml +2 -1
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/README.md +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_a2a.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/format_prompt.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/models/_json_schema.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.2.3 → pydantic_ai_slim-0.2.5}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.2.3 → 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
|
|
@@ -26,15 +27,14 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
26
27
|
Requires-Python: >=3.9
|
|
27
28
|
Requires-Dist: eval-type-backport>=0.2.0
|
|
28
29
|
Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
29
|
-
Requires-Dist: fasta2a==0.2.3
|
|
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.2.
|
|
33
|
+
Requires-Dist: pydantic-graph==0.2.5
|
|
34
34
|
Requires-Dist: pydantic>=2.10
|
|
35
35
|
Requires-Dist: typing-inspection>=0.4.0
|
|
36
36
|
Provides-Extra: a2a
|
|
37
|
-
Requires-Dist: fasta2a==0.2.
|
|
37
|
+
Requires-Dist: fasta2a==0.2.5; extra == 'a2a'
|
|
38
38
|
Provides-Extra: anthropic
|
|
39
39
|
Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
|
|
40
40
|
Provides-Extra: bedrock
|
|
@@ -48,7 +48,9 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
48
48
|
Provides-Extra: duckduckgo
|
|
49
49
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
50
50
|
Provides-Extra: evals
|
|
51
|
-
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'
|
|
52
54
|
Provides-Extra: groq
|
|
53
55
|
Requires-Dist: groq>=0.15.0; extra == 'groq'
|
|
54
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
|
|
@@ -102,7 +102,7 @@ def cli_exit(prog_name: str = 'pai'): # pragma: no cover
|
|
|
102
102
|
sys.exit(cli(prog_name=prog_name))
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def cli(args_list: Sequence[str] | None = None, *, prog_name: str = 'pai') -> int:
|
|
105
|
+
def cli(args_list: Sequence[str] | None = None, *, prog_name: str = 'pai') -> int: # noqa: C901
|
|
106
106
|
"""Run the CLI and return the exit code for the process."""
|
|
107
107
|
parser = argparse.ArgumentParser(
|
|
108
108
|
prog=prog_name,
|
|
@@ -122,7 +122,6 @@ Special prompts:
|
|
|
122
122
|
'--model',
|
|
123
123
|
nargs='?',
|
|
124
124
|
help='Model to use, in format "<provider>:<model>" e.g. "openai:gpt-4o" or "anthropic:claude-3-7-sonnet-latest". Defaults to "openai:gpt-4o".',
|
|
125
|
-
default='openai:gpt-4o',
|
|
126
125
|
)
|
|
127
126
|
# we don't want to autocomplete or list models that don't include the provider,
|
|
128
127
|
# e.g. we want to show `openai:gpt-4o` but not `gpt-4o`
|
|
@@ -153,40 +152,49 @@ Special prompts:
|
|
|
153
152
|
args = parser.parse_args(args_list)
|
|
154
153
|
|
|
155
154
|
console = Console()
|
|
156
|
-
|
|
157
|
-
f'[green]{prog_name} - PydanticAI CLI v{__version__} using[/green] [magenta]{args.model}[/magenta]',
|
|
158
|
-
highlight=False,
|
|
159
|
-
)
|
|
155
|
+
name_version = f'[green]{prog_name} - PydanticAI CLI v{__version__}[/green]'
|
|
160
156
|
if args.version:
|
|
157
|
+
console.print(name_version, highlight=False)
|
|
161
158
|
return 0
|
|
162
159
|
if args.list_models:
|
|
163
|
-
console.print('Available models:
|
|
160
|
+
console.print(f'{name_version}\n\n[green]Available models:[/green]')
|
|
164
161
|
for model in qualified_model_names:
|
|
165
162
|
console.print(f' {model}', highlight=False)
|
|
166
163
|
return 0
|
|
167
164
|
|
|
168
165
|
agent: Agent[None, str] = cli_agent
|
|
169
166
|
if args.agent:
|
|
167
|
+
sys.path.append(os.getcwd())
|
|
170
168
|
try:
|
|
171
|
-
current_path = os.getcwd()
|
|
172
|
-
sys.path.append(current_path)
|
|
173
|
-
|
|
174
169
|
module_path, variable_name = args.agent.split(':')
|
|
175
|
-
module = importlib.import_module(module_path)
|
|
176
|
-
agent = getattr(module, variable_name)
|
|
177
|
-
if not isinstance(agent, Agent):
|
|
178
|
-
console.print(f'[red]Error: {args.agent} is not an Agent instance[/red]')
|
|
179
|
-
return 1
|
|
180
|
-
console.print(f'[green]Using custom agent:[/green] [magenta]{args.agent}[/magenta]', highlight=False)
|
|
181
170
|
except ValueError:
|
|
182
171
|
console.print('[red]Error: Agent must be specified in "module:variable" format[/red]')
|
|
183
172
|
return 1
|
|
184
173
|
|
|
185
|
-
|
|
186
|
-
agent
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
module = importlib.import_module(module_path)
|
|
175
|
+
agent = getattr(module, variable_name)
|
|
176
|
+
if not isinstance(agent, Agent):
|
|
177
|
+
console.print(f'[red]Error: {args.agent} is not an Agent instance[/red]')
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
model_arg_set = args.model is not None
|
|
181
|
+
if agent.model is None or model_arg_set:
|
|
182
|
+
try:
|
|
183
|
+
agent.model = infer_model(args.model or 'openai:gpt-4o')
|
|
184
|
+
except UserError as e:
|
|
185
|
+
console.print(f'Error initializing [magenta]{args.model}[/magenta]:\n[red]{e}[/red]')
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
model_name = agent.model if isinstance(agent.model, str) else f'{agent.model.system}:{agent.model.model_name}'
|
|
189
|
+
if args.agent and model_arg_set:
|
|
190
|
+
console.print(
|
|
191
|
+
f'{name_version} using custom agent [magenta]{args.agent}[/magenta] with [magenta]{model_name}[/magenta]',
|
|
192
|
+
highlight=False,
|
|
193
|
+
)
|
|
194
|
+
elif args.agent:
|
|
195
|
+
console.print(f'{name_version} using custom agent [magenta]{args.agent}[/magenta]', highlight=False)
|
|
196
|
+
else:
|
|
197
|
+
console.print(f'{name_version} with [magenta]{model_name}[/magenta]', highlight=False)
|
|
190
198
|
|
|
191
199
|
stream = not args.no_stream
|
|
192
200
|
if args.code_theme == 'light':
|
|
@@ -194,7 +202,7 @@ Special prompts:
|
|
|
194
202
|
elif args.code_theme == 'dark':
|
|
195
203
|
code_theme = 'monokai'
|
|
196
204
|
else:
|
|
197
|
-
code_theme = args.code_theme
|
|
205
|
+
code_theme = args.code_theme # pragma: no cover
|
|
198
206
|
|
|
199
207
|
if prompt := cast(str, args.prompt):
|
|
200
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:
|