pydantic-ai-slim 1.0.8__tar.gz → 1.0.10__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-1.0.8 → pydantic_ai_slim-1.0.10}/PKG-INFO +5 -5
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_agent_graph.py +67 -55
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_cli.py +1 -1
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_otel_messages.py +2 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_parts_manager.py +82 -12
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_run_context.py +8 -1
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_tool_manager.py +1 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ag_ui.py +86 -33
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/__init__.py +2 -1
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/builtin_tools.py +12 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_model.py +14 -6
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
- pydantic_ai_slim-1.0.10/pydantic_ai/format_prompt.py +205 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/messages.py +65 -30
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/anthropic.py +119 -45
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/function.py +17 -8
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/google.py +132 -33
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/groq.py +68 -17
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/openai.py +262 -41
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/__init__.py +1 -1
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/result.py +21 -3
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/function.py +8 -2
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pyproject.toml +2 -2
- pydantic_ai_slim-1.0.8/pydantic_ai/format_prompt.py +0 -113
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/.gitignore +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/LICENSE +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/README.md +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_a2a.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_function_schema.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_mcp.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_output.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_thinking_part.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/abstract.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/wrapper.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/direct.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_model.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/aci.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/langchain.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/bedrock.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/cohere.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/gemini.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/huggingface.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/instrumented.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/mcp_sampling.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/test.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/output.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/_json_schema.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/amazon.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/anthropic.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/cohere.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/deepseek.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/google.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/grok.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/groq.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/harmony.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/meta.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/mistral.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/moonshotai.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/openai.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/qwen.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/cerebras.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/fireworks.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/gateway.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/github.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/grok.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/heroku.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/huggingface.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/litellm.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/moonshotai.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/ollama.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/openrouter.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/together.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/vercel.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/retries.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/run.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/settings.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/tools.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/__init__.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/_dynamic.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/abstract.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/approval_required.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/combined.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/external.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/filtered.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/prefixed.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/prepared.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/renamed.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/wrapper.py +0 -0
- {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.10
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Project-URL: Homepage, https://github.com/pydantic/pydantic-ai/tree/main/pydantic_ai_slim
|
|
6
6
|
Project-URL: Source, https://github.com/pydantic/pydantic-ai/tree/main/pydantic_ai_slim
|
|
@@ -33,7 +33,7 @@ Requires-Dist: genai-prices>=0.0.23
|
|
|
33
33
|
Requires-Dist: griffe>=1.3.2
|
|
34
34
|
Requires-Dist: httpx>=0.27
|
|
35
35
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
36
|
-
Requires-Dist: pydantic-graph==1.0.
|
|
36
|
+
Requires-Dist: pydantic-graph==1.0.10
|
|
37
37
|
Requires-Dist: pydantic>=2.10
|
|
38
38
|
Requires-Dist: typing-inspection>=0.4.0
|
|
39
39
|
Provides-Extra: a2a
|
|
@@ -53,11 +53,11 @@ Requires-Dist: rich>=13; extra == 'cli'
|
|
|
53
53
|
Provides-Extra: cohere
|
|
54
54
|
Requires-Dist: cohere>=5.18.0; (platform_system != 'Emscripten') and extra == 'cohere'
|
|
55
55
|
Provides-Extra: dbos
|
|
56
|
-
Requires-Dist: dbos>=1.
|
|
56
|
+
Requires-Dist: dbos>=1.14.0; extra == 'dbos'
|
|
57
57
|
Provides-Extra: duckduckgo
|
|
58
58
|
Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
|
|
59
59
|
Provides-Extra: evals
|
|
60
|
-
Requires-Dist: pydantic-evals==1.0.
|
|
60
|
+
Requires-Dist: pydantic-evals==1.0.10; extra == 'evals'
|
|
61
61
|
Provides-Extra: google
|
|
62
62
|
Requires-Dist: google-genai>=1.31.0; extra == 'google'
|
|
63
63
|
Provides-Extra: groq
|
|
@@ -71,7 +71,7 @@ Requires-Dist: mcp>=1.12.3; extra == 'mcp'
|
|
|
71
71
|
Provides-Extra: mistral
|
|
72
72
|
Requires-Dist: mistralai>=1.9.10; extra == 'mistral'
|
|
73
73
|
Provides-Extra: openai
|
|
74
|
-
Requires-Dist: openai>=1.
|
|
74
|
+
Requires-Dist: openai>=1.107.2; extra == 'openai'
|
|
75
75
|
Provides-Extra: retries
|
|
76
76
|
Requires-Dist: tenacity>=8.2.3; extra == 'retries'
|
|
77
77
|
Provides-Extra: tavily
|
|
@@ -545,23 +545,26 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
545
545
|
# Ensure that the stream is only run once
|
|
546
546
|
|
|
547
547
|
async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa: C901
|
|
548
|
-
|
|
548
|
+
text = ''
|
|
549
549
|
tool_calls: list[_messages.ToolCallPart] = []
|
|
550
|
-
|
|
550
|
+
invisible_parts: bool = False
|
|
551
551
|
|
|
552
552
|
for part in self.model_response.parts:
|
|
553
553
|
if isinstance(part, _messages.TextPart):
|
|
554
|
-
|
|
555
|
-
if part.content:
|
|
556
|
-
texts.append(part.content)
|
|
554
|
+
text += part.content
|
|
557
555
|
elif isinstance(part, _messages.ToolCallPart):
|
|
558
556
|
tool_calls.append(part)
|
|
559
557
|
elif isinstance(part, _messages.BuiltinToolCallPart):
|
|
560
|
-
|
|
558
|
+
# Text parts before a built-in tool call are essentially thoughts,
|
|
559
|
+
# not part of the final result output, so we reset the accumulated text
|
|
560
|
+
text = ''
|
|
561
|
+
invisible_parts = True
|
|
562
|
+
yield _messages.BuiltinToolCallEvent(part) # pyright: ignore[reportDeprecated]
|
|
561
563
|
elif isinstance(part, _messages.BuiltinToolReturnPart):
|
|
562
|
-
|
|
564
|
+
invisible_parts = True
|
|
565
|
+
yield _messages.BuiltinToolResultEvent(part) # pyright: ignore[reportDeprecated]
|
|
563
566
|
elif isinstance(part, _messages.ThinkingPart):
|
|
564
|
-
|
|
567
|
+
invisible_parts = True
|
|
565
568
|
else:
|
|
566
569
|
assert_never(part)
|
|
567
570
|
|
|
@@ -569,36 +572,51 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
569
572
|
# In the future, we'd consider making this configurable at the agent or run level.
|
|
570
573
|
# This accounts for cases like anthropic returns that might contain a text response
|
|
571
574
|
# and a tool call response, where the text response just indicates the tool call will happen.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
_messages.
|
|
584
|
-
|
|
575
|
+
try:
|
|
576
|
+
if tool_calls:
|
|
577
|
+
async for event in self._handle_tool_calls(ctx, tool_calls):
|
|
578
|
+
yield event
|
|
579
|
+
elif text:
|
|
580
|
+
# No events are emitted during the handling of text responses, so we don't need to yield anything
|
|
581
|
+
self._next_node = await self._handle_text_response(ctx, text)
|
|
582
|
+
elif invisible_parts:
|
|
583
|
+
# handle responses with only thinking or built-in tool parts.
|
|
584
|
+
# this can happen with models that support thinking mode when they don't provide
|
|
585
|
+
# actionable output alongside their thinking content. so we tell the model to try again.
|
|
586
|
+
m = _messages.RetryPromptPart(
|
|
587
|
+
content='Responses without text or tool calls are not permitted.',
|
|
585
588
|
)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
589
|
+
raise ToolRetryError(m)
|
|
590
|
+
else:
|
|
591
|
+
# we got an empty response with no tool calls, text, thinking, or built-in tool calls.
|
|
592
|
+
# this sometimes happens with anthropic (and perhaps other models)
|
|
593
|
+
# when the model has already returned text along side tool calls
|
|
594
|
+
# in this scenario, if text responses are allowed, we return text from the most recent model
|
|
595
|
+
# response, if any
|
|
596
|
+
if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
|
|
597
|
+
for message in reversed(ctx.state.message_history):
|
|
598
|
+
if isinstance(message, _messages.ModelResponse):
|
|
599
|
+
text = ''
|
|
600
|
+
for part in message.parts:
|
|
601
|
+
if isinstance(part, _messages.TextPart):
|
|
602
|
+
text += part.content
|
|
603
|
+
elif isinstance(part, _messages.BuiltinToolCallPart):
|
|
604
|
+
# Text parts before a built-in tool call are essentially thoughts,
|
|
605
|
+
# not part of the final result output, so we reset the accumulated text
|
|
606
|
+
text = '' # pragma: no cover
|
|
607
|
+
if text:
|
|
608
|
+
self._next_node = await self._handle_text_response(ctx, text)
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
# Go back to the model request node with an empty request, which means we'll essentially
|
|
612
|
+
# resubmit the most recent request that resulted in an empty response,
|
|
613
|
+
# as the empty response and request will not create any items in the API payload,
|
|
614
|
+
# in the hope the model will return a non-empty response this time.
|
|
615
|
+
ctx.state.increment_retries(ctx.deps.max_result_retries)
|
|
616
|
+
self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[]))
|
|
617
|
+
except ToolRetryError as e:
|
|
618
|
+
ctx.state.increment_retries(ctx.deps.max_result_retries, e)
|
|
619
|
+
self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[e.tool_retry]))
|
|
602
620
|
|
|
603
621
|
self._events_iterator = _run_stream()
|
|
604
622
|
|
|
@@ -655,28 +673,22 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
655
673
|
async def _handle_text_response(
|
|
656
674
|
self,
|
|
657
675
|
ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]],
|
|
658
|
-
|
|
676
|
+
text: str,
|
|
659
677
|
) -> ModelRequestNode[DepsT, NodeRunEndT] | End[result.FinalResult[NodeRunEndT]]:
|
|
660
678
|
output_schema = ctx.deps.output_schema
|
|
679
|
+
run_context = build_run_context(ctx)
|
|
661
680
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
run_context = build_run_context(ctx)
|
|
665
|
-
if isinstance(output_schema, _output.TextOutputSchema):
|
|
666
|
-
result_data = await output_schema.process(text, run_context)
|
|
667
|
-
else:
|
|
668
|
-
m = _messages.RetryPromptPart(
|
|
669
|
-
content='Plain text responses are not permitted, please include your response in a tool call',
|
|
670
|
-
)
|
|
671
|
-
raise ToolRetryError(m)
|
|
672
|
-
|
|
673
|
-
for validator in ctx.deps.output_validators:
|
|
674
|
-
result_data = await validator.validate(result_data, run_context)
|
|
675
|
-
except ToolRetryError as e:
|
|
676
|
-
ctx.state.increment_retries(ctx.deps.max_result_retries, e)
|
|
677
|
-
return ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[e.tool_retry]))
|
|
681
|
+
if isinstance(output_schema, _output.TextOutputSchema):
|
|
682
|
+
result_data = await output_schema.process(text, run_context)
|
|
678
683
|
else:
|
|
679
|
-
|
|
684
|
+
m = _messages.RetryPromptPart(
|
|
685
|
+
content='Plain text responses are not permitted, please include your response in a tool call',
|
|
686
|
+
)
|
|
687
|
+
raise ToolRetryError(m)
|
|
688
|
+
|
|
689
|
+
for validator in ctx.deps.output_validators:
|
|
690
|
+
result_data = await validator.validate(result_data, run_context)
|
|
691
|
+
return self._handle_final_result(ctx, result.FinalResult(result_data), [])
|
|
680
692
|
|
|
681
693
|
__repr__ = dataclasses_no_defaults_repr
|
|
682
694
|
|
|
@@ -356,7 +356,7 @@ def handle_slash_command(
|
|
|
356
356
|
except IndexError:
|
|
357
357
|
console.print('[dim]No output available to copy.[/dim]')
|
|
358
358
|
else:
|
|
359
|
-
text_to_copy = '
|
|
359
|
+
text_to_copy = ''.join(part.content for part in parts if isinstance(part, TextPart))
|
|
360
360
|
text_to_copy = text_to_copy.strip()
|
|
361
361
|
if text_to_copy:
|
|
362
362
|
pyperclip.copy(text_to_copy)
|
|
@@ -21,6 +21,7 @@ class ToolCallPart(TypedDict):
|
|
|
21
21
|
id: str
|
|
22
22
|
name: str
|
|
23
23
|
arguments: NotRequired[JsonValue]
|
|
24
|
+
builtin: NotRequired[bool] # Not (currently?) part of the spec, used by Logfire
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class ToolCallResponsePart(TypedDict):
|
|
@@ -28,6 +29,7 @@ class ToolCallResponsePart(TypedDict):
|
|
|
28
29
|
id: str
|
|
29
30
|
name: str
|
|
30
31
|
result: NotRequired[JsonValue]
|
|
32
|
+
builtin: NotRequired[bool] # Not (currently?) part of the spec, used by Logfire
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class MediaUrlPart(TypedDict):
|
|
@@ -19,6 +19,8 @@ from typing import Any
|
|
|
19
19
|
|
|
20
20
|
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
21
21
|
from pydantic_ai.messages import (
|
|
22
|
+
BuiltinToolCallPart,
|
|
23
|
+
BuiltinToolReturnPart,
|
|
22
24
|
ModelResponsePart,
|
|
23
25
|
ModelResponseStreamEvent,
|
|
24
26
|
PartDeltaEvent,
|
|
@@ -226,11 +228,11 @@ class ModelResponsePartsManager:
|
|
|
226
228
|
self,
|
|
227
229
|
*,
|
|
228
230
|
vendor_part_id: Hashable | None,
|
|
229
|
-
tool_name: str | None,
|
|
230
|
-
args: str | dict[str, Any] | None,
|
|
231
|
-
tool_call_id: str | None,
|
|
231
|
+
tool_name: str | None = None,
|
|
232
|
+
args: str | dict[str, Any] | None = None,
|
|
233
|
+
tool_call_id: str | None = None,
|
|
232
234
|
) -> ModelResponseStreamEvent | None:
|
|
233
|
-
"""Handle or update a tool call, creating or updating a `ToolCallPart` or `ToolCallPartDelta`.
|
|
235
|
+
"""Handle or update a tool call, creating or updating a `ToolCallPart`, `BuiltinToolCallPart`, or `ToolCallPartDelta`.
|
|
234
236
|
|
|
235
237
|
Managed items remain as `ToolCallPartDelta`s until they have at least a tool_name, at which
|
|
236
238
|
point they are upgraded to `ToolCallPart`s.
|
|
@@ -247,15 +249,17 @@ class ModelResponsePartsManager:
|
|
|
247
249
|
tool_call_id: An optional string representing an identifier for this tool call.
|
|
248
250
|
|
|
249
251
|
Returns:
|
|
250
|
-
- A `PartStartEvent` if a new ToolCallPart is created.
|
|
252
|
+
- A `PartStartEvent` if a new ToolCallPart or BuiltinToolCallPart is created.
|
|
251
253
|
- A `PartDeltaEvent` if an existing part is updated.
|
|
252
254
|
- `None` if no new event is emitted (e.g., the part is still incomplete).
|
|
253
255
|
|
|
254
256
|
Raises:
|
|
255
257
|
UnexpectedModelBehavior: If attempting to apply a tool call delta to a part that is not
|
|
256
|
-
a ToolCallPart or ToolCallPartDelta.
|
|
258
|
+
a ToolCallPart, BuiltinToolCallPart, or ToolCallPartDelta.
|
|
257
259
|
"""
|
|
258
|
-
existing_matching_part_and_index: tuple[ToolCallPartDelta | ToolCallPart, int] | None =
|
|
260
|
+
existing_matching_part_and_index: tuple[ToolCallPartDelta | ToolCallPart | BuiltinToolCallPart, int] | None = (
|
|
261
|
+
None
|
|
262
|
+
)
|
|
259
263
|
|
|
260
264
|
if vendor_part_id is None:
|
|
261
265
|
# vendor_part_id is None, so check if the latest part is a matching tool call or delta to update
|
|
@@ -264,14 +268,14 @@ class ModelResponsePartsManager:
|
|
|
264
268
|
if tool_name is None and self._parts:
|
|
265
269
|
part_index = len(self._parts) - 1
|
|
266
270
|
latest_part = self._parts[part_index]
|
|
267
|
-
if isinstance(latest_part, ToolCallPart | ToolCallPartDelta): # pragma: no branch
|
|
271
|
+
if isinstance(latest_part, ToolCallPart | BuiltinToolCallPart | ToolCallPartDelta): # pragma: no branch
|
|
268
272
|
existing_matching_part_and_index = latest_part, part_index
|
|
269
273
|
else:
|
|
270
274
|
# vendor_part_id is provided, so look up the corresponding part or delta
|
|
271
275
|
part_index = self._vendor_id_to_part_index.get(vendor_part_id)
|
|
272
276
|
if part_index is not None:
|
|
273
277
|
existing_part = self._parts[part_index]
|
|
274
|
-
if not isinstance(existing_part, ToolCallPartDelta | ToolCallPart):
|
|
278
|
+
if not isinstance(existing_part, ToolCallPartDelta | ToolCallPart | BuiltinToolCallPart):
|
|
275
279
|
raise UnexpectedModelBehavior(f'Cannot apply a tool call delta to {existing_part=}')
|
|
276
280
|
existing_matching_part_and_index = existing_part, part_index
|
|
277
281
|
|
|
@@ -284,7 +288,7 @@ class ModelResponsePartsManager:
|
|
|
284
288
|
new_part_index = len(self._parts)
|
|
285
289
|
self._parts.append(part)
|
|
286
290
|
# Only emit a PartStartEvent if we have enough information to produce a full ToolCallPart
|
|
287
|
-
if isinstance(part, ToolCallPart):
|
|
291
|
+
if isinstance(part, ToolCallPart | BuiltinToolCallPart):
|
|
288
292
|
return PartStartEvent(index=new_part_index, part=part)
|
|
289
293
|
else:
|
|
290
294
|
# Update the existing part or delta with the new information
|
|
@@ -292,7 +296,7 @@ class ModelResponsePartsManager:
|
|
|
292
296
|
delta = ToolCallPartDelta(tool_name_delta=tool_name, args_delta=args, tool_call_id=tool_call_id)
|
|
293
297
|
updated_part = delta.apply(existing_part)
|
|
294
298
|
self._parts[part_index] = updated_part
|
|
295
|
-
if isinstance(updated_part, ToolCallPart):
|
|
299
|
+
if isinstance(updated_part, ToolCallPart | BuiltinToolCallPart):
|
|
296
300
|
if isinstance(existing_part, ToolCallPartDelta):
|
|
297
301
|
# We just upgraded a delta to a full part, so emit a PartStartEvent
|
|
298
302
|
return PartStartEvent(index=part_index, part=updated_part)
|
|
@@ -337,7 +341,7 @@ class ModelResponsePartsManager:
|
|
|
337
341
|
else:
|
|
338
342
|
# vendor_part_id is provided, so find and overwrite or create a new ToolCallPart.
|
|
339
343
|
maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
|
|
340
|
-
if maybe_part_index is not None:
|
|
344
|
+
if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], ToolCallPart):
|
|
341
345
|
new_part_index = maybe_part_index
|
|
342
346
|
self._parts[new_part_index] = new_part
|
|
343
347
|
else:
|
|
@@ -345,3 +349,69 @@ class ModelResponsePartsManager:
|
|
|
345
349
|
self._parts.append(new_part)
|
|
346
350
|
self._vendor_id_to_part_index[vendor_part_id] = new_part_index
|
|
347
351
|
return PartStartEvent(index=new_part_index, part=new_part)
|
|
352
|
+
|
|
353
|
+
def handle_builtin_tool_call_part(
|
|
354
|
+
self,
|
|
355
|
+
*,
|
|
356
|
+
vendor_part_id: Hashable | None,
|
|
357
|
+
part: BuiltinToolCallPart,
|
|
358
|
+
) -> ModelResponseStreamEvent:
|
|
359
|
+
"""Create or overwrite a BuiltinToolCallPart.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
vendor_part_id: The vendor's ID for this tool call part. If not
|
|
363
|
+
None and an existing part is found, that part is overwritten.
|
|
364
|
+
part: The BuiltinToolCallPart.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
ModelResponseStreamEvent: A `PartStartEvent` indicating that a new tool call part
|
|
368
|
+
has been added to the manager, or replaced an existing part.
|
|
369
|
+
"""
|
|
370
|
+
if vendor_part_id is None:
|
|
371
|
+
# vendor_part_id is None, so we unconditionally append a new BuiltinToolCallPart to the end of the list
|
|
372
|
+
new_part_index = len(self._parts)
|
|
373
|
+
self._parts.append(part)
|
|
374
|
+
else:
|
|
375
|
+
# vendor_part_id is provided, so find and overwrite or create a new BuiltinToolCallPart.
|
|
376
|
+
maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
|
|
377
|
+
if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], BuiltinToolCallPart):
|
|
378
|
+
new_part_index = maybe_part_index
|
|
379
|
+
self._parts[new_part_index] = part
|
|
380
|
+
else:
|
|
381
|
+
new_part_index = len(self._parts)
|
|
382
|
+
self._parts.append(part)
|
|
383
|
+
self._vendor_id_to_part_index[vendor_part_id] = new_part_index
|
|
384
|
+
return PartStartEvent(index=new_part_index, part=part)
|
|
385
|
+
|
|
386
|
+
def handle_builtin_tool_return_part(
|
|
387
|
+
self,
|
|
388
|
+
*,
|
|
389
|
+
vendor_part_id: Hashable | None,
|
|
390
|
+
part: BuiltinToolReturnPart,
|
|
391
|
+
) -> ModelResponseStreamEvent:
|
|
392
|
+
"""Create or overwrite a BuiltinToolReturnPart.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
vendor_part_id: The vendor's ID for this tool call part. If not
|
|
396
|
+
None and an existing part is found, that part is overwritten.
|
|
397
|
+
part: The BuiltinToolReturnPart.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
ModelResponseStreamEvent: A `PartStartEvent` indicating that a new tool call part
|
|
401
|
+
has been added to the manager, or replaced an existing part.
|
|
402
|
+
"""
|
|
403
|
+
if vendor_part_id is None:
|
|
404
|
+
# vendor_part_id is None, so we unconditionally append a new BuiltinToolReturnPart to the end of the list
|
|
405
|
+
new_part_index = len(self._parts)
|
|
406
|
+
self._parts.append(part)
|
|
407
|
+
else:
|
|
408
|
+
# vendor_part_id is provided, so find and overwrite or create a new BuiltinToolReturnPart.
|
|
409
|
+
maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
|
|
410
|
+
if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], BuiltinToolReturnPart):
|
|
411
|
+
new_part_index = maybe_part_index
|
|
412
|
+
self._parts[new_part_index] = part
|
|
413
|
+
else:
|
|
414
|
+
new_part_index = len(self._parts)
|
|
415
|
+
self._parts.append(part)
|
|
416
|
+
self._vendor_id_to_part_index[vendor_part_id] = new_part_index
|
|
417
|
+
return PartStartEvent(index=new_part_index, part=part)
|
|
@@ -43,10 +43,17 @@ class RunContext(Generic[AgentDepsT]):
|
|
|
43
43
|
tool_name: str | None = None
|
|
44
44
|
"""Name of the tool being called."""
|
|
45
45
|
retry: int = 0
|
|
46
|
-
"""Number of retries so far."""
|
|
46
|
+
"""Number of retries of this tool so far."""
|
|
47
|
+
max_retries: int = 0
|
|
48
|
+
"""The maximum number of retries of this tool."""
|
|
47
49
|
run_step: int = 0
|
|
48
50
|
"""The current step in the run."""
|
|
49
51
|
tool_call_approved: bool = False
|
|
50
52
|
"""Whether a tool call that required approval has now been approved."""
|
|
51
53
|
|
|
54
|
+
@property
|
|
55
|
+
def last_attempt(self) -> bool:
|
|
56
|
+
"""Whether this is the last attempt at running this tool before an error is raised."""
|
|
57
|
+
return self.retry == self.max_retries
|
|
58
|
+
|
|
52
59
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import json
|
|
10
10
|
import uuid
|
|
11
11
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Mapping, Sequence
|
|
12
|
-
from dataclasses import Field, dataclass, replace
|
|
12
|
+
from dataclasses import Field, dataclass, field, replace
|
|
13
13
|
from http import HTTPStatus
|
|
14
14
|
from typing import (
|
|
15
15
|
Any,
|
|
@@ -23,13 +23,15 @@ from typing import (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel, ValidationError
|
|
26
|
-
from typing_extensions import assert_never
|
|
27
26
|
|
|
28
27
|
from . import _utils
|
|
29
28
|
from ._agent_graph import CallToolsNode, ModelRequestNode
|
|
30
29
|
from .agent import AbstractAgent, AgentRun, AgentRunResult
|
|
31
30
|
from .exceptions import UserError
|
|
32
31
|
from .messages import (
|
|
32
|
+
BaseToolCallPart,
|
|
33
|
+
BuiltinToolCallPart,
|
|
34
|
+
BuiltinToolReturnPart,
|
|
33
35
|
FunctionToolResultEvent,
|
|
34
36
|
ModelMessage,
|
|
35
37
|
ModelRequest,
|
|
@@ -123,6 +125,8 @@ SSE_CONTENT_TYPE: Final[str] = 'text/event-stream'
|
|
|
123
125
|
OnCompleteFunc: TypeAlias = Callable[[AgentRunResult[Any]], None] | Callable[[AgentRunResult[Any]], Awaitable[None]]
|
|
124
126
|
"""Callback function type that receives the `AgentRunResult` of the completed run. Can be sync or async."""
|
|
125
127
|
|
|
128
|
+
_BUILTIN_TOOL_CALL_ID_PREFIX: Final[str] = 'pyd_ai_builtin'
|
|
129
|
+
|
|
126
130
|
|
|
127
131
|
class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
|
|
128
132
|
"""ASGI application for running Pydantic AI agents with AG-UI protocol support."""
|
|
@@ -487,20 +491,37 @@ async def _handle_model_request_event( # noqa: C901
|
|
|
487
491
|
stream_ctx.part_end = TextMessageEndEvent(
|
|
488
492
|
message_id=message_id,
|
|
489
493
|
)
|
|
490
|
-
elif isinstance(part,
|
|
494
|
+
elif isinstance(part, BaseToolCallPart):
|
|
495
|
+
tool_call_id = part.tool_call_id
|
|
496
|
+
if isinstance(part, BuiltinToolCallPart):
|
|
497
|
+
builtin_tool_call_id = '|'.join(
|
|
498
|
+
[_BUILTIN_TOOL_CALL_ID_PREFIX, part.provider_name or '', tool_call_id]
|
|
499
|
+
)
|
|
500
|
+
stream_ctx.builtin_tool_call_ids[tool_call_id] = builtin_tool_call_id
|
|
501
|
+
tool_call_id = builtin_tool_call_id
|
|
502
|
+
|
|
491
503
|
message_id = stream_ctx.message_id or stream_ctx.new_message_id()
|
|
492
504
|
yield ToolCallStartEvent(
|
|
493
|
-
tool_call_id=
|
|
505
|
+
tool_call_id=tool_call_id,
|
|
494
506
|
tool_call_name=part.tool_name,
|
|
495
507
|
parent_message_id=message_id,
|
|
496
508
|
)
|
|
497
509
|
if part.args:
|
|
498
510
|
yield ToolCallArgsEvent(
|
|
499
|
-
tool_call_id=
|
|
500
|
-
delta=part.
|
|
511
|
+
tool_call_id=tool_call_id,
|
|
512
|
+
delta=part.args_as_json_str(),
|
|
501
513
|
)
|
|
502
514
|
stream_ctx.part_end = ToolCallEndEvent(
|
|
503
|
-
tool_call_id=
|
|
515
|
+
tool_call_id=tool_call_id,
|
|
516
|
+
)
|
|
517
|
+
elif isinstance(part, BuiltinToolReturnPart): # pragma: no branch
|
|
518
|
+
tool_call_id = stream_ctx.builtin_tool_call_ids[part.tool_call_id]
|
|
519
|
+
yield ToolCallResultEvent(
|
|
520
|
+
message_id=stream_ctx.new_message_id(),
|
|
521
|
+
type=EventType.TOOL_CALL_RESULT,
|
|
522
|
+
role='tool',
|
|
523
|
+
tool_call_id=tool_call_id,
|
|
524
|
+
content=part.model_response_str(),
|
|
504
525
|
)
|
|
505
526
|
|
|
506
527
|
elif isinstance(agent_event, PartDeltaEvent):
|
|
@@ -512,9 +533,12 @@ async def _handle_model_request_event( # noqa: C901
|
|
|
512
533
|
delta=delta.content_delta,
|
|
513
534
|
)
|
|
514
535
|
elif isinstance(delta, ToolCallPartDelta): # pragma: no branch
|
|
515
|
-
|
|
536
|
+
tool_call_id = delta.tool_call_id
|
|
537
|
+
assert tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
|
|
538
|
+
if tool_call_id in stream_ctx.builtin_tool_call_ids:
|
|
539
|
+
tool_call_id = stream_ctx.builtin_tool_call_ids[tool_call_id]
|
|
516
540
|
yield ToolCallArgsEvent(
|
|
517
|
-
tool_call_id=
|
|
541
|
+
tool_call_id=tool_call_id,
|
|
518
542
|
delta=delta.args_delta if isinstance(delta.args_delta, str) else json.dumps(delta.args_delta),
|
|
519
543
|
)
|
|
520
544
|
elif isinstance(delta, ThinkingPartDelta): # pragma: no branch
|
|
@@ -550,9 +574,8 @@ async def _handle_tool_result_event(
|
|
|
550
574
|
if not isinstance(result, ToolReturnPart):
|
|
551
575
|
return
|
|
552
576
|
|
|
553
|
-
message_id = stream_ctx.new_message_id()
|
|
554
577
|
yield ToolCallResultEvent(
|
|
555
|
-
message_id=
|
|
578
|
+
message_id=stream_ctx.new_message_id(),
|
|
556
579
|
type=EventType.TOOL_CALL_RESULT,
|
|
557
580
|
role='tool',
|
|
558
581
|
tool_call_id=result.tool_call_id,
|
|
@@ -579,7 +602,9 @@ def _messages_from_ag_ui(messages: list[Message]) -> list[ModelMessage]:
|
|
|
579
602
|
request_parts: list[ModelRequestPart] | None = None
|
|
580
603
|
response_parts: list[ModelResponsePart] | None = None
|
|
581
604
|
for msg in messages:
|
|
582
|
-
if isinstance(msg, UserMessage | SystemMessage | DeveloperMessage
|
|
605
|
+
if isinstance(msg, UserMessage | SystemMessage | DeveloperMessage) or (
|
|
606
|
+
isinstance(msg, ToolMessage) and not msg.tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX)
|
|
607
|
+
):
|
|
583
608
|
if request_parts is None:
|
|
584
609
|
request_parts = []
|
|
585
610
|
result.append(ModelRequest(parts=request_parts))
|
|
@@ -589,44 +614,71 @@ def _messages_from_ag_ui(messages: list[Message]) -> list[ModelMessage]:
|
|
|
589
614
|
request_parts.append(UserPromptPart(content=msg.content))
|
|
590
615
|
elif isinstance(msg, SystemMessage | DeveloperMessage):
|
|
591
616
|
request_parts.append(SystemPromptPart(content=msg.content))
|
|
592
|
-
|
|
593
|
-
|
|
617
|
+
else:
|
|
618
|
+
tool_call_id = msg.tool_call_id
|
|
619
|
+
tool_name = tool_calls.get(tool_call_id)
|
|
594
620
|
if tool_name is None: # pragma: no cover
|
|
595
|
-
raise _ToolCallNotFoundError(tool_call_id=
|
|
621
|
+
raise _ToolCallNotFoundError(tool_call_id=tool_call_id)
|
|
596
622
|
|
|
597
623
|
request_parts.append(
|
|
598
624
|
ToolReturnPart(
|
|
599
625
|
tool_name=tool_name,
|
|
600
626
|
content=msg.content,
|
|
601
|
-
tool_call_id=
|
|
627
|
+
tool_call_id=tool_call_id,
|
|
602
628
|
)
|
|
603
629
|
)
|
|
604
|
-
else:
|
|
605
|
-
assert_never(msg)
|
|
606
630
|
|
|
607
|
-
elif isinstance(msg, AssistantMessage):
|
|
631
|
+
elif isinstance(msg, AssistantMessage) or ( # pragma: no branch
|
|
632
|
+
isinstance(msg, ToolMessage) and msg.tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX)
|
|
633
|
+
):
|
|
608
634
|
if response_parts is None:
|
|
609
635
|
response_parts = []
|
|
610
636
|
result.append(ModelResponse(parts=response_parts))
|
|
611
637
|
request_parts = None
|
|
612
638
|
|
|
613
|
-
if msg
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
639
|
+
if isinstance(msg, AssistantMessage):
|
|
640
|
+
if msg.content:
|
|
641
|
+
response_parts.append(TextPart(content=msg.content))
|
|
642
|
+
|
|
643
|
+
if msg.tool_calls:
|
|
644
|
+
for tool_call in msg.tool_calls:
|
|
645
|
+
tool_call_id = tool_call.id
|
|
646
|
+
tool_name = tool_call.function.name
|
|
647
|
+
tool_calls[tool_call_id] = tool_name
|
|
648
|
+
|
|
649
|
+
if tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX):
|
|
650
|
+
_, provider_name, tool_call_id = tool_call_id.split('|', 2)
|
|
651
|
+
response_parts.append(
|
|
652
|
+
BuiltinToolCallPart(
|
|
653
|
+
tool_name=tool_name,
|
|
654
|
+
args=tool_call.function.arguments,
|
|
655
|
+
tool_call_id=tool_call_id,
|
|
656
|
+
provider_name=provider_name,
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
else:
|
|
660
|
+
response_parts.append(
|
|
661
|
+
ToolCallPart(
|
|
662
|
+
tool_name=tool_name,
|
|
663
|
+
tool_call_id=tool_call_id,
|
|
664
|
+
args=tool_call.function.arguments,
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
else:
|
|
668
|
+
tool_call_id = msg.tool_call_id
|
|
669
|
+
tool_name = tool_calls.get(tool_call_id)
|
|
670
|
+
if tool_name is None: # pragma: no cover
|
|
671
|
+
raise _ToolCallNotFoundError(tool_call_id=tool_call_id)
|
|
672
|
+
_, provider_name, tool_call_id = tool_call_id.split('|', 2)
|
|
619
673
|
|
|
620
|
-
response_parts.
|
|
621
|
-
|
|
622
|
-
tool_name=
|
|
623
|
-
|
|
624
|
-
|
|
674
|
+
response_parts.append(
|
|
675
|
+
BuiltinToolReturnPart(
|
|
676
|
+
tool_name=tool_name,
|
|
677
|
+
content=msg.content,
|
|
678
|
+
tool_call_id=tool_call_id,
|
|
679
|
+
provider_name=provider_name,
|
|
625
680
|
)
|
|
626
|
-
for tool_call in msg.tool_calls
|
|
627
681
|
)
|
|
628
|
-
else:
|
|
629
|
-
assert_never(msg)
|
|
630
682
|
|
|
631
683
|
return result
|
|
632
684
|
|
|
@@ -687,6 +739,7 @@ class _RequestStreamContext:
|
|
|
687
739
|
message_id: str = ''
|
|
688
740
|
part_end: BaseEvent | None = None
|
|
689
741
|
thinking: bool = False
|
|
742
|
+
builtin_tool_call_ids: dict[str, str] = field(default_factory=dict)
|
|
690
743
|
|
|
691
744
|
def new_message_id(self) -> str:
|
|
692
745
|
"""Generate a new message ID for the request stream.
|
|
@@ -259,7 +259,8 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
|
|
|
259
259
|
name: The name of the agent, used for logging. If `None`, we try to infer the agent name from the call frame
|
|
260
260
|
when the agent is first run.
|
|
261
261
|
model_settings: Optional model request settings to use for this agent's runs, by default.
|
|
262
|
-
retries: The default number of retries to allow before raising an error.
|
|
262
|
+
retries: The default number of retries to allow for tool calls and output validation, before raising an error.
|
|
263
|
+
For model request retries, see the [HTTP Request Retries](../retries.md) documentation.
|
|
263
264
|
output_retries: The maximum number of retries to allow for output validation, defaults to `retries`.
|
|
264
265
|
tools: Tools to register with the agent, you can also register tools via the decorators
|
|
265
266
|
[`@agent.tool`][pydantic_ai.Agent.tool] and [`@agent.tool_plain`][pydantic_ai.Agent.tool_plain].
|