pydantic-ai-slim 0.2.1__tar.gz → 0.2.3__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.
Potentially problematic release.
This version of pydantic-ai-slim might be problematic. Click here for more details.
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/PKG-INFO +6 -3
- pydantic_ai_slim-0.2.3/pydantic_ai/_a2a.py +191 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_cli.py +47 -9
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/agent.py +114 -11
- pydantic_ai_slim-0.2.3/pydantic_ai/direct.py +215 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/messages.py +61 -82
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/__init__.py +3 -3
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/instrumented.py +13 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/test.py +1 -1
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pyproject.toml +4 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/.gitignore +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/README.md +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_agent_graph.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_output.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_parts_manager.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/format_prompt.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/_json_schema.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/cohere.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/function.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/gemini.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/groq.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/openai.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/__init__.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/result.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/settings.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/tools.py +0 -0
- {pydantic_ai_slim-0.2.1 → pydantic_ai_slim-0.2.3}/pydantic_ai/usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -26,12 +26,15 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
26
26
|
Requires-Python: >=3.9
|
|
27
27
|
Requires-Dist: eval-type-backport>=0.2.0
|
|
28
28
|
Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
29
|
+
Requires-Dist: fasta2a==0.2.3
|
|
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.3
|
|
33
34
|
Requires-Dist: pydantic>=2.10
|
|
34
35
|
Requires-Dist: typing-inspection>=0.4.0
|
|
36
|
+
Provides-Extra: a2a
|
|
37
|
+
Requires-Dist: fasta2a==0.2.3; extra == 'a2a'
|
|
35
38
|
Provides-Extra: anthropic
|
|
36
39
|
Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
|
|
37
40
|
Provides-Extra: bedrock
|
|
@@ -45,7 +48,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
45
48
|
Provides-Extra: duckduckgo
|
|
46
49
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
47
50
|
Provides-Extra: evals
|
|
48
|
-
Requires-Dist: pydantic-evals==0.2.
|
|
51
|
+
Requires-Dist: pydantic-evals==0.2.3; extra == 'evals'
|
|
49
52
|
Provides-Extra: groq
|
|
50
53
|
Requires-Dist: groq>=0.15.0; extra == 'groq'
|
|
51
54
|
Provides-Extra: logfire
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations, annotations as _annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Sequence
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any, Generic
|
|
8
|
+
|
|
9
|
+
from typing_extensions import assert_never
|
|
10
|
+
|
|
11
|
+
from pydantic_ai.messages import (
|
|
12
|
+
AudioUrl,
|
|
13
|
+
BinaryContent,
|
|
14
|
+
DocumentUrl,
|
|
15
|
+
ImageUrl,
|
|
16
|
+
ModelMessage,
|
|
17
|
+
ModelRequest,
|
|
18
|
+
ModelRequestPart,
|
|
19
|
+
ModelResponse,
|
|
20
|
+
ModelResponsePart,
|
|
21
|
+
TextPart,
|
|
22
|
+
UserPromptPart,
|
|
23
|
+
VideoUrl,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .agent import Agent, AgentDepsT, OutputDataT
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from starlette.middleware import Middleware
|
|
30
|
+
from starlette.routing import Route
|
|
31
|
+
from starlette.types import ExceptionHandler, Lifespan
|
|
32
|
+
|
|
33
|
+
from fasta2a.applications import FastA2A
|
|
34
|
+
from fasta2a.broker import Broker, InMemoryBroker
|
|
35
|
+
from fasta2a.schema import (
|
|
36
|
+
Artifact,
|
|
37
|
+
Message,
|
|
38
|
+
Part,
|
|
39
|
+
Provider,
|
|
40
|
+
Skill,
|
|
41
|
+
TaskIdParams,
|
|
42
|
+
TaskSendParams,
|
|
43
|
+
TextPart as A2ATextPart,
|
|
44
|
+
)
|
|
45
|
+
from fasta2a.storage import InMemoryStorage, Storage
|
|
46
|
+
from fasta2a.worker import Worker
|
|
47
|
+
except ImportError as _import_error:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
'Please install the `fasta2a` package to use `Agent.to_a2a()` method, '
|
|
50
|
+
'you can use the `a2a` optional group — `pip install "pydantic-ai-slim[a2a]"`'
|
|
51
|
+
) from _import_error
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def worker_lifespan(app: FastA2A, worker: Worker) -> AsyncIterator[None]:
|
|
56
|
+
"""Custom lifespan that runs the worker during application startup.
|
|
57
|
+
|
|
58
|
+
This ensures the worker is started and ready to process tasks as soon as the application starts.
|
|
59
|
+
"""
|
|
60
|
+
async with app.task_manager:
|
|
61
|
+
async with worker.run():
|
|
62
|
+
yield
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def agent_to_a2a(
|
|
66
|
+
agent: Agent[AgentDepsT, OutputDataT],
|
|
67
|
+
*,
|
|
68
|
+
storage: Storage | None = None,
|
|
69
|
+
broker: Broker | None = None,
|
|
70
|
+
# Agent card
|
|
71
|
+
name: str | None = None,
|
|
72
|
+
url: str = 'http://localhost:8000',
|
|
73
|
+
version: str = '1.0.0',
|
|
74
|
+
description: str | None = None,
|
|
75
|
+
provider: Provider | None = None,
|
|
76
|
+
skills: list[Skill] | None = None,
|
|
77
|
+
# Starlette
|
|
78
|
+
debug: bool = False,
|
|
79
|
+
routes: Sequence[Route] | None = None,
|
|
80
|
+
middleware: Sequence[Middleware] | None = None,
|
|
81
|
+
exception_handlers: dict[Any, ExceptionHandler] | None = None,
|
|
82
|
+
lifespan: Lifespan[FastA2A] | None = None,
|
|
83
|
+
) -> FastA2A:
|
|
84
|
+
"""Create a FastA2A server from an agent."""
|
|
85
|
+
storage = storage or InMemoryStorage()
|
|
86
|
+
broker = broker or InMemoryBroker()
|
|
87
|
+
worker = AgentWorker(agent=agent, broker=broker, storage=storage)
|
|
88
|
+
|
|
89
|
+
lifespan = lifespan or partial(worker_lifespan, worker=worker)
|
|
90
|
+
|
|
91
|
+
return FastA2A(
|
|
92
|
+
storage=storage,
|
|
93
|
+
broker=broker,
|
|
94
|
+
name=name or agent.name,
|
|
95
|
+
url=url,
|
|
96
|
+
version=version,
|
|
97
|
+
description=description,
|
|
98
|
+
provider=provider,
|
|
99
|
+
skills=skills,
|
|
100
|
+
debug=debug,
|
|
101
|
+
routes=routes,
|
|
102
|
+
middleware=middleware,
|
|
103
|
+
exception_handlers=exception_handlers,
|
|
104
|
+
lifespan=lifespan,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class AgentWorker(Worker, Generic[AgentDepsT, OutputDataT]):
|
|
110
|
+
"""A worker that uses an agent to execute tasks."""
|
|
111
|
+
|
|
112
|
+
agent: Agent[AgentDepsT, OutputDataT]
|
|
113
|
+
|
|
114
|
+
async def run_task(self, params: TaskSendParams) -> None:
|
|
115
|
+
task = await self.storage.load_task(params['id'], history_length=params.get('history_length'))
|
|
116
|
+
assert task is not None, f'Task {params["id"]} not found'
|
|
117
|
+
assert 'session_id' in task, 'Task must have a session_id'
|
|
118
|
+
|
|
119
|
+
await self.storage.update_task(task['id'], state='working')
|
|
120
|
+
|
|
121
|
+
# TODO(Marcelo): We need to have a way to communicate when the task is set to `input-required`. Maybe
|
|
122
|
+
# a custom `output_type` with a `more_info_required` field, or something like that.
|
|
123
|
+
|
|
124
|
+
task_history = task.get('history', [])
|
|
125
|
+
message_history = self.build_message_history(task_history=task_history)
|
|
126
|
+
|
|
127
|
+
# TODO(Marcelo): We need to make this more customizable e.g. pass deps.
|
|
128
|
+
result = await self.agent.run(message_history=message_history) # type: ignore
|
|
129
|
+
|
|
130
|
+
artifacts = self.build_artifacts(result.output)
|
|
131
|
+
await self.storage.update_task(task['id'], state='completed', artifacts=artifacts)
|
|
132
|
+
|
|
133
|
+
async def cancel_task(self, params: TaskIdParams) -> None:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def build_artifacts(self, result: Any) -> list[Artifact]:
|
|
137
|
+
# TODO(Marcelo): We need to send the json schema of the result on the metadata of the message.
|
|
138
|
+
return [Artifact(name='result', index=0, parts=[A2ATextPart(type='text', text=str(result))])]
|
|
139
|
+
|
|
140
|
+
def build_message_history(self, task_history: list[Message]) -> list[ModelMessage]:
|
|
141
|
+
model_messages: list[ModelMessage] = []
|
|
142
|
+
for message in task_history:
|
|
143
|
+
if message['role'] == 'user':
|
|
144
|
+
model_messages.append(ModelRequest(parts=self._map_request_parts(message['parts'])))
|
|
145
|
+
else:
|
|
146
|
+
model_messages.append(ModelResponse(parts=self._map_response_parts(message['parts'])))
|
|
147
|
+
return model_messages
|
|
148
|
+
|
|
149
|
+
def _map_request_parts(self, parts: list[Part]) -> list[ModelRequestPart]:
|
|
150
|
+
model_parts: list[ModelRequestPart] = []
|
|
151
|
+
for part in parts:
|
|
152
|
+
if part['type'] == 'text':
|
|
153
|
+
model_parts.append(UserPromptPart(content=part['text']))
|
|
154
|
+
elif part['type'] == 'file':
|
|
155
|
+
file = part['file']
|
|
156
|
+
if 'data' in file:
|
|
157
|
+
data = file['data'].encode('utf-8')
|
|
158
|
+
content = BinaryContent(data=data, media_type=file['mime_type'])
|
|
159
|
+
model_parts.append(UserPromptPart(content=[content]))
|
|
160
|
+
else:
|
|
161
|
+
url = file['url']
|
|
162
|
+
for url_cls in (DocumentUrl, AudioUrl, ImageUrl, VideoUrl):
|
|
163
|
+
content = url_cls(url=url)
|
|
164
|
+
try:
|
|
165
|
+
content.media_type
|
|
166
|
+
except ValueError: # pragma: no cover
|
|
167
|
+
continue
|
|
168
|
+
else:
|
|
169
|
+
break
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError(f'Unknown file type: {file["mime_type"]}') # pragma: no cover
|
|
172
|
+
model_parts.append(UserPromptPart(content=[content]))
|
|
173
|
+
elif part['type'] == 'data':
|
|
174
|
+
# TODO(Marcelo): Maybe we should use this for `ToolReturnPart`, and `RetryPromptPart`.
|
|
175
|
+
raise NotImplementedError('Data parts are not supported yet.')
|
|
176
|
+
else:
|
|
177
|
+
assert_never(part)
|
|
178
|
+
return model_parts
|
|
179
|
+
|
|
180
|
+
def _map_response_parts(self, parts: list[Part]) -> list[ModelResponsePart]:
|
|
181
|
+
model_parts: list[ModelResponsePart] = []
|
|
182
|
+
for part in parts:
|
|
183
|
+
if part['type'] == 'text':
|
|
184
|
+
model_parts.append(TextPart(content=part['text']))
|
|
185
|
+
elif part['type'] == 'file': # pragma: no cover
|
|
186
|
+
raise NotImplementedError('File parts are not supported yet.')
|
|
187
|
+
elif part['type'] == 'data': # pragma: no cover
|
|
188
|
+
raise NotImplementedError('Data parts are not supported yet.')
|
|
189
|
+
else: # pragma: no cover
|
|
190
|
+
assert_never(part)
|
|
191
|
+
return model_parts
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations as _annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
5
7
|
import sys
|
|
6
8
|
from asyncio import CancelledError
|
|
7
9
|
from collections.abc import Sequence
|
|
@@ -12,6 +14,9 @@ from typing import Any, cast
|
|
|
12
14
|
|
|
13
15
|
from typing_inspection.introspection import get_literal_values
|
|
14
16
|
|
|
17
|
+
from pydantic_ai.result import OutputDataT
|
|
18
|
+
from pydantic_ai.tools import AgentDepsT
|
|
19
|
+
|
|
15
20
|
from . import __version__
|
|
16
21
|
from .agent import Agent
|
|
17
22
|
from .exceptions import UserError
|
|
@@ -123,6 +128,11 @@ Special prompts:
|
|
|
123
128
|
# e.g. we want to show `openai:gpt-4o` but not `gpt-4o`
|
|
124
129
|
qualified_model_names = [n for n in get_literal_values(KnownModelName.__value__) if ':' in n]
|
|
125
130
|
arg.completer = argcomplete.ChoicesCompleter(qualified_model_names) # type: ignore[reportPrivateUsage]
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
'-a',
|
|
133
|
+
'--agent',
|
|
134
|
+
help='Custom Agent to use, in format "module:variable", e.g. "mymodule.submodule:my_agent"',
|
|
135
|
+
)
|
|
126
136
|
parser.add_argument(
|
|
127
137
|
'-l',
|
|
128
138
|
'--list-models',
|
|
@@ -155,8 +165,25 @@ Special prompts:
|
|
|
155
165
|
console.print(f' {model}', highlight=False)
|
|
156
166
|
return 0
|
|
157
167
|
|
|
168
|
+
agent: Agent[None, str] = cli_agent
|
|
169
|
+
if args.agent:
|
|
170
|
+
try:
|
|
171
|
+
current_path = os.getcwd()
|
|
172
|
+
sys.path.append(current_path)
|
|
173
|
+
|
|
174
|
+
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
|
+
except ValueError:
|
|
182
|
+
console.print('[red]Error: Agent must be specified in "module:variable" format[/red]')
|
|
183
|
+
return 1
|
|
184
|
+
|
|
158
185
|
try:
|
|
159
|
-
|
|
186
|
+
agent.model = infer_model(args.model)
|
|
160
187
|
except UserError as e:
|
|
161
188
|
console.print(f'Error initializing [magenta]{args.model}[/magenta]:\n[red]{e}[/red]')
|
|
162
189
|
return 1
|
|
@@ -171,21 +198,31 @@ Special prompts:
|
|
|
171
198
|
|
|
172
199
|
if prompt := cast(str, args.prompt):
|
|
173
200
|
try:
|
|
174
|
-
asyncio.run(ask_agent(
|
|
201
|
+
asyncio.run(ask_agent(agent, prompt, stream, console, code_theme))
|
|
175
202
|
except KeyboardInterrupt:
|
|
176
203
|
pass
|
|
177
204
|
return 0
|
|
178
205
|
|
|
206
|
+
# Ensure the history directory and file exist
|
|
207
|
+
PROMPT_HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
PROMPT_HISTORY_PATH.touch(exist_ok=True)
|
|
209
|
+
|
|
179
210
|
# doing this instead of `PromptSession[Any](history=` allows mocking of PromptSession in tests
|
|
180
211
|
session: PromptSession[Any] = PromptSession(history=FileHistory(str(PROMPT_HISTORY_PATH)))
|
|
181
212
|
try:
|
|
182
|
-
return asyncio.run(run_chat(session, stream,
|
|
213
|
+
return asyncio.run(run_chat(session, stream, agent, console, code_theme, prog_name))
|
|
183
214
|
except KeyboardInterrupt: # pragma: no cover
|
|
184
215
|
return 0
|
|
185
216
|
|
|
186
217
|
|
|
187
218
|
async def run_chat(
|
|
188
|
-
session: PromptSession[Any],
|
|
219
|
+
session: PromptSession[Any],
|
|
220
|
+
stream: bool,
|
|
221
|
+
agent: Agent[AgentDepsT, OutputDataT],
|
|
222
|
+
console: Console,
|
|
223
|
+
code_theme: str,
|
|
224
|
+
prog_name: str,
|
|
225
|
+
deps: AgentDepsT = None,
|
|
189
226
|
) -> int:
|
|
190
227
|
multiline = False
|
|
191
228
|
messages: list[ModelMessage] = []
|
|
@@ -207,30 +244,31 @@ async def run_chat(
|
|
|
207
244
|
return exit_value
|
|
208
245
|
else:
|
|
209
246
|
try:
|
|
210
|
-
messages = await ask_agent(agent, text, stream, console, code_theme, messages)
|
|
247
|
+
messages = await ask_agent(agent, text, stream, console, code_theme, deps, messages)
|
|
211
248
|
except CancelledError: # pragma: no cover
|
|
212
249
|
console.print('[dim]Interrupted[/dim]')
|
|
213
250
|
|
|
214
251
|
|
|
215
252
|
async def ask_agent(
|
|
216
|
-
agent: Agent,
|
|
253
|
+
agent: Agent[AgentDepsT, OutputDataT],
|
|
217
254
|
prompt: str,
|
|
218
255
|
stream: bool,
|
|
219
256
|
console: Console,
|
|
220
257
|
code_theme: str,
|
|
258
|
+
deps: AgentDepsT = None,
|
|
221
259
|
messages: list[ModelMessage] | None = None,
|
|
222
260
|
) -> list[ModelMessage]:
|
|
223
261
|
status = Status('[dim]Working on it…[/dim]', console=console)
|
|
224
262
|
|
|
225
263
|
if not stream:
|
|
226
264
|
with status:
|
|
227
|
-
result = await agent.run(prompt, message_history=messages)
|
|
228
|
-
content = result.output
|
|
265
|
+
result = await agent.run(prompt, message_history=messages, deps=deps)
|
|
266
|
+
content = str(result.output)
|
|
229
267
|
console.print(Markdown(content, code_theme=code_theme))
|
|
230
268
|
return result.all_messages()
|
|
231
269
|
|
|
232
270
|
with status, ExitStack() as stack:
|
|
233
|
-
async with agent.iter(prompt, message_history=messages) as agent_run:
|
|
271
|
+
async with agent.iter(prompt, message_history=messages, deps=deps) as agent_run:
|
|
234
272
|
live = Live('', refresh_per_second=15, console=console, vertical_overflow='ellipsis')
|
|
235
273
|
async for node in agent_run:
|
|
236
274
|
if Agent.is_model_request_node(node):
|
|
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, cast, final,
|
|
|
12
12
|
|
|
13
13
|
from opentelemetry.trace import NoOpTracer, use_span
|
|
14
14
|
from pydantic.json_schema import GenerateJsonSchema
|
|
15
|
-
from typing_extensions import Literal, Never, TypeIs, TypeVar, deprecated
|
|
15
|
+
from typing_extensions import Literal, Never, Self, TypeIs, TypeVar, deprecated
|
|
16
16
|
|
|
17
17
|
from pydantic_graph import End, Graph, GraphRun, GraphRunContext
|
|
18
18
|
from pydantic_graph._utils import get_event_loop
|
|
@@ -28,7 +28,7 @@ from . import (
|
|
|
28
28
|
result,
|
|
29
29
|
usage as _usage,
|
|
30
30
|
)
|
|
31
|
-
from .models.instrumented import InstrumentationSettings, InstrumentedModel
|
|
31
|
+
from .models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
|
|
32
32
|
from .result import FinalResult, OutputDataT, StreamedRunResult, ToolOutput
|
|
33
33
|
from .settings import ModelSettings, merge_model_settings
|
|
34
34
|
from .tools import (
|
|
@@ -52,6 +52,14 @@ ModelRequestNode = _agent_graph.ModelRequestNode
|
|
|
52
52
|
UserPromptNode = _agent_graph.UserPromptNode
|
|
53
53
|
|
|
54
54
|
if TYPE_CHECKING:
|
|
55
|
+
from starlette.middleware import Middleware
|
|
56
|
+
from starlette.routing import Route
|
|
57
|
+
from starlette.types import ExceptionHandler, Lifespan
|
|
58
|
+
|
|
59
|
+
from fasta2a.applications import FastA2A
|
|
60
|
+
from fasta2a.broker import Broker
|
|
61
|
+
from fasta2a.schema import Provider, Skill
|
|
62
|
+
from fasta2a.storage import Storage
|
|
55
63
|
from pydantic_ai.mcp import MCPServer
|
|
56
64
|
|
|
57
65
|
|
|
@@ -100,7 +108,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
100
108
|
model: models.Model | models.KnownModelName | str | None
|
|
101
109
|
"""The default model configured for this agent.
|
|
102
110
|
|
|
103
|
-
We allow str here since the actual list of allowed models changes frequently.
|
|
111
|
+
We allow `str` here since the actual list of allowed models changes frequently.
|
|
104
112
|
"""
|
|
105
113
|
|
|
106
114
|
name: str | None
|
|
@@ -225,7 +233,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
225
233
|
|
|
226
234
|
Args:
|
|
227
235
|
model: The default model to use for this agent, if not provide,
|
|
228
|
-
you must provide the model when calling it. We allow str here since the actual list of allowed models changes frequently.
|
|
236
|
+
you must provide the model when calling it. We allow `str` here since the actual list of allowed models changes frequently.
|
|
229
237
|
output_type: The type of the output data, used to validate the data returned by the model,
|
|
230
238
|
defaults to `str`.
|
|
231
239
|
instructions: Instructions to use for this agent, you can also register instructions via a function with
|
|
@@ -1574,13 +1582,7 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1574
1582
|
if instrument is None:
|
|
1575
1583
|
instrument = self._instrument_default
|
|
1576
1584
|
|
|
1577
|
-
|
|
1578
|
-
if instrument is True:
|
|
1579
|
-
instrument = InstrumentationSettings()
|
|
1580
|
-
|
|
1581
|
-
model_ = InstrumentedModel(model_, instrument)
|
|
1582
|
-
|
|
1583
|
-
return model_
|
|
1585
|
+
return instrument_model(model_, instrument)
|
|
1584
1586
|
|
|
1585
1587
|
def _get_deps(self: Agent[T, OutputDataT], deps: T) -> T:
|
|
1586
1588
|
"""Get deps for a run.
|
|
@@ -1688,6 +1690,107 @@ class Agent(Generic[AgentDepsT, OutputDataT]):
|
|
|
1688
1690
|
finally:
|
|
1689
1691
|
await exit_stack.aclose()
|
|
1690
1692
|
|
|
1693
|
+
def to_a2a(
|
|
1694
|
+
self,
|
|
1695
|
+
*,
|
|
1696
|
+
storage: Storage | None = None,
|
|
1697
|
+
broker: Broker | None = None,
|
|
1698
|
+
# Agent card
|
|
1699
|
+
name: str | None = None,
|
|
1700
|
+
url: str = 'http://localhost:8000',
|
|
1701
|
+
version: str = '1.0.0',
|
|
1702
|
+
description: str | None = None,
|
|
1703
|
+
provider: Provider | None = None,
|
|
1704
|
+
skills: list[Skill] | None = None,
|
|
1705
|
+
# Starlette
|
|
1706
|
+
debug: bool = False,
|
|
1707
|
+
routes: Sequence[Route] | None = None,
|
|
1708
|
+
middleware: Sequence[Middleware] | None = None,
|
|
1709
|
+
exception_handlers: dict[Any, ExceptionHandler] | None = None,
|
|
1710
|
+
lifespan: Lifespan[FastA2A] | None = None,
|
|
1711
|
+
) -> FastA2A:
|
|
1712
|
+
"""Convert the agent to a FastA2A application.
|
|
1713
|
+
|
|
1714
|
+
Example:
|
|
1715
|
+
```python
|
|
1716
|
+
from pydantic_ai import Agent
|
|
1717
|
+
|
|
1718
|
+
agent = Agent('openai:gpt-4o')
|
|
1719
|
+
app = agent.to_a2a()
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
The `app` is an ASGI application that can be used with any ASGI server.
|
|
1723
|
+
|
|
1724
|
+
To run the application, you can use the following command:
|
|
1725
|
+
|
|
1726
|
+
```bash
|
|
1727
|
+
uvicorn app:app --host 0.0.0.0 --port 8000
|
|
1728
|
+
```
|
|
1729
|
+
"""
|
|
1730
|
+
from ._a2a import agent_to_a2a
|
|
1731
|
+
|
|
1732
|
+
return agent_to_a2a(
|
|
1733
|
+
self,
|
|
1734
|
+
storage=storage,
|
|
1735
|
+
broker=broker,
|
|
1736
|
+
name=name,
|
|
1737
|
+
url=url,
|
|
1738
|
+
version=version,
|
|
1739
|
+
description=description,
|
|
1740
|
+
provider=provider,
|
|
1741
|
+
skills=skills,
|
|
1742
|
+
debug=debug,
|
|
1743
|
+
routes=routes,
|
|
1744
|
+
middleware=middleware,
|
|
1745
|
+
exception_handlers=exception_handlers,
|
|
1746
|
+
lifespan=lifespan,
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
async def to_cli(self: Self, deps: AgentDepsT = None) -> None:
|
|
1750
|
+
"""Run the agent in a CLI chat interface.
|
|
1751
|
+
|
|
1752
|
+
Example:
|
|
1753
|
+
```python {title="agent_to_cli.py" test="skip"}
|
|
1754
|
+
from pydantic_ai import Agent
|
|
1755
|
+
|
|
1756
|
+
agent = Agent('openai:gpt-4o', instructions='You always respond in Italian.')
|
|
1757
|
+
|
|
1758
|
+
async def main():
|
|
1759
|
+
await agent.to_cli()
|
|
1760
|
+
```
|
|
1761
|
+
"""
|
|
1762
|
+
from prompt_toolkit import PromptSession
|
|
1763
|
+
from prompt_toolkit.history import FileHistory
|
|
1764
|
+
from rich.console import Console
|
|
1765
|
+
|
|
1766
|
+
from pydantic_ai._cli import PROMPT_HISTORY_PATH, run_chat
|
|
1767
|
+
|
|
1768
|
+
# TODO(Marcelo): We need to refactor the CLI code to be able to be able to just pass `agent`, `deps` and
|
|
1769
|
+
# `prog_name` from here.
|
|
1770
|
+
|
|
1771
|
+
session: PromptSession[Any] = PromptSession(history=FileHistory(str(PROMPT_HISTORY_PATH)))
|
|
1772
|
+
await run_chat(
|
|
1773
|
+
session=session,
|
|
1774
|
+
stream=True,
|
|
1775
|
+
agent=self,
|
|
1776
|
+
deps=deps,
|
|
1777
|
+
console=Console(),
|
|
1778
|
+
code_theme='monokai',
|
|
1779
|
+
prog_name='pydantic-ai',
|
|
1780
|
+
)
|
|
1781
|
+
|
|
1782
|
+
def to_cli_sync(self: Self, deps: AgentDepsT = None) -> None:
|
|
1783
|
+
"""Run the agent in a CLI chat interface with the non-async interface.
|
|
1784
|
+
|
|
1785
|
+
```python {title="agent_to_cli_sync.py" test="skip"}
|
|
1786
|
+
from pydantic_ai import Agent
|
|
1787
|
+
|
|
1788
|
+
agent = Agent('openai:gpt-4o', instructions='You always respond in Italian.')
|
|
1789
|
+
agent.to_cli_sync()
|
|
1790
|
+
```
|
|
1791
|
+
"""
|
|
1792
|
+
return get_event_loop().run_until_complete(self.to_cli(deps=deps))
|
|
1793
|
+
|
|
1691
1794
|
|
|
1692
1795
|
@dataclasses.dataclass(repr=False)
|
|
1693
1796
|
class AgentRun(Generic[AgentDepsT, OutputDataT]):
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Methods for making imperative requests to language models with minimal abstraction.
|
|
2
|
+
|
|
3
|
+
These methods allow you to make requests to LLMs where the only abstraction is input and output schema
|
|
4
|
+
translation so you can use all models with the same API.
|
|
5
|
+
|
|
6
|
+
These methods are thin wrappers around [`Model`][pydantic_ai.models.Model] implementations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations as _annotations
|
|
10
|
+
|
|
11
|
+
from contextlib import AbstractAsyncContextManager
|
|
12
|
+
|
|
13
|
+
from pydantic_graph._utils import get_event_loop as _get_event_loop
|
|
14
|
+
|
|
15
|
+
from . import agent, messages, models, settings
|
|
16
|
+
from .models import instrumented as instrumented_models
|
|
17
|
+
|
|
18
|
+
__all__ = 'model_request', 'model_request_sync', 'model_request_stream'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def model_request(
|
|
22
|
+
model: models.Model | models.KnownModelName | str,
|
|
23
|
+
messages: list[messages.ModelMessage],
|
|
24
|
+
*,
|
|
25
|
+
model_settings: settings.ModelSettings | None = None,
|
|
26
|
+
model_request_parameters: models.ModelRequestParameters | None = None,
|
|
27
|
+
instrument: instrumented_models.InstrumentationSettings | bool | None = None,
|
|
28
|
+
) -> messages.ModelResponse:
|
|
29
|
+
"""Make a non-streamed request to a model.
|
|
30
|
+
|
|
31
|
+
```py title="model_request_example.py"
|
|
32
|
+
from pydantic_ai.direct import model_request
|
|
33
|
+
from pydantic_ai.messages import ModelRequest
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
model_response = await model_request(
|
|
38
|
+
'anthropic:claude-3-5-haiku-latest',
|
|
39
|
+
[ModelRequest.user_text_prompt('What is the capital of France?')] # (1)!
|
|
40
|
+
)
|
|
41
|
+
print(model_response)
|
|
42
|
+
'''
|
|
43
|
+
ModelResponse(
|
|
44
|
+
parts=[TextPart(content='Paris', part_kind='text')],
|
|
45
|
+
usage=Usage(
|
|
46
|
+
requests=1,
|
|
47
|
+
request_tokens=56,
|
|
48
|
+
response_tokens=1,
|
|
49
|
+
total_tokens=57,
|
|
50
|
+
details=None,
|
|
51
|
+
),
|
|
52
|
+
model_name='claude-3-5-haiku-latest',
|
|
53
|
+
timestamp=datetime.datetime(...),
|
|
54
|
+
kind='response',
|
|
55
|
+
)
|
|
56
|
+
'''
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
1. See [`ModelRequest.user_text_prompt`][pydantic_ai.messages.ModelRequest.user_text_prompt] for details.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
model: The model to make a request to. We allow `str` here since the actual list of allowed models changes frequently.
|
|
63
|
+
messages: Messages to send to the model
|
|
64
|
+
model_settings: optional model settings
|
|
65
|
+
model_request_parameters: optional model request parameters
|
|
66
|
+
instrument: Whether to instrument the request with OpenTelemetry/Logfire, if `None` the value from
|
|
67
|
+
[`logfire.instrument_pydantic_ai`][logfire.Logfire.instrument_pydantic_ai] is used.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The model response and token usage associated with the request.
|
|
71
|
+
"""
|
|
72
|
+
model_instance = _prepare_model(model, instrument)
|
|
73
|
+
return await model_instance.request(
|
|
74
|
+
messages,
|
|
75
|
+
model_settings,
|
|
76
|
+
model_instance.customize_request_parameters(model_request_parameters or models.ModelRequestParameters()),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def model_request_sync(
|
|
81
|
+
model: models.Model | models.KnownModelName | str,
|
|
82
|
+
messages: list[messages.ModelMessage],
|
|
83
|
+
*,
|
|
84
|
+
model_settings: settings.ModelSettings | None = None,
|
|
85
|
+
model_request_parameters: models.ModelRequestParameters | None = None,
|
|
86
|
+
instrument: instrumented_models.InstrumentationSettings | bool | None = None,
|
|
87
|
+
) -> messages.ModelResponse:
|
|
88
|
+
"""Make a Synchronous, non-streamed request to a model.
|
|
89
|
+
|
|
90
|
+
This is a convenience method that wraps [`model_request`][pydantic_ai.direct.model_request] with
|
|
91
|
+
`loop.run_until_complete(...)`. You therefore can't use this method inside async code or if there's an active event loop.
|
|
92
|
+
|
|
93
|
+
```py title="model_request_sync_example.py"
|
|
94
|
+
from pydantic_ai.direct import model_request_sync
|
|
95
|
+
from pydantic_ai.messages import ModelRequest
|
|
96
|
+
|
|
97
|
+
model_response = model_request_sync(
|
|
98
|
+
'anthropic:claude-3-5-haiku-latest',
|
|
99
|
+
[ModelRequest.user_text_prompt('What is the capital of France?')] # (1)!
|
|
100
|
+
)
|
|
101
|
+
print(model_response)
|
|
102
|
+
'''
|
|
103
|
+
ModelResponse(
|
|
104
|
+
parts=[TextPart(content='Paris', part_kind='text')],
|
|
105
|
+
usage=Usage(
|
|
106
|
+
requests=1, request_tokens=56, response_tokens=1, total_tokens=57, details=None
|
|
107
|
+
),
|
|
108
|
+
model_name='claude-3-5-haiku-latest',
|
|
109
|
+
timestamp=datetime.datetime(...),
|
|
110
|
+
kind='response',
|
|
111
|
+
)
|
|
112
|
+
'''
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
1. See [`ModelRequest.user_text_prompt`][pydantic_ai.messages.ModelRequest.user_text_prompt] for details.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
model: The model to make a request to. We allow `str` here since the actual list of allowed models changes frequently.
|
|
119
|
+
messages: Messages to send to the model
|
|
120
|
+
model_settings: optional model settings
|
|
121
|
+
model_request_parameters: optional model request parameters
|
|
122
|
+
instrument: Whether to instrument the request with OpenTelemetry/Logfire, if `None` the value from
|
|
123
|
+
[`logfire.instrument_pydantic_ai`][logfire.Logfire.instrument_pydantic_ai] is used.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The model response and token usage associated with the request.
|
|
127
|
+
"""
|
|
128
|
+
return _get_event_loop().run_until_complete(
|
|
129
|
+
model_request(
|
|
130
|
+
model,
|
|
131
|
+
messages,
|
|
132
|
+
model_settings=model_settings,
|
|
133
|
+
model_request_parameters=model_request_parameters,
|
|
134
|
+
instrument=instrument,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def model_request_stream(
|
|
140
|
+
model: models.Model | models.KnownModelName | str,
|
|
141
|
+
messages: list[messages.ModelMessage],
|
|
142
|
+
*,
|
|
143
|
+
model_settings: settings.ModelSettings | None = None,
|
|
144
|
+
model_request_parameters: models.ModelRequestParameters | None = None,
|
|
145
|
+
instrument: instrumented_models.InstrumentationSettings | bool | None = None,
|
|
146
|
+
) -> AbstractAsyncContextManager[models.StreamedResponse]:
|
|
147
|
+
"""Make a streamed async request to a model.
|
|
148
|
+
|
|
149
|
+
```py {title="model_request_stream_example.py"}
|
|
150
|
+
|
|
151
|
+
from pydantic_ai.direct import model_request_stream
|
|
152
|
+
from pydantic_ai.messages import ModelRequest
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
messages = [ModelRequest.user_text_prompt('Who was Albert Einstein?')] # (1)!
|
|
157
|
+
async with model_request_stream( 'openai:gpt-4.1-mini', messages) as stream:
|
|
158
|
+
chunks = []
|
|
159
|
+
async for chunk in stream:
|
|
160
|
+
chunks.append(chunk)
|
|
161
|
+
print(chunks)
|
|
162
|
+
'''
|
|
163
|
+
[
|
|
164
|
+
PartStartEvent(
|
|
165
|
+
index=0,
|
|
166
|
+
part=TextPart(content='Albert Einstein was ', part_kind='text'),
|
|
167
|
+
event_kind='part_start',
|
|
168
|
+
),
|
|
169
|
+
PartDeltaEvent(
|
|
170
|
+
index=0,
|
|
171
|
+
delta=TextPartDelta(
|
|
172
|
+
content_delta='a German-born theoretical ', part_delta_kind='text'
|
|
173
|
+
),
|
|
174
|
+
event_kind='part_delta',
|
|
175
|
+
),
|
|
176
|
+
PartDeltaEvent(
|
|
177
|
+
index=0,
|
|
178
|
+
delta=TextPartDelta(content_delta='physicist.', part_delta_kind='text'),
|
|
179
|
+
event_kind='part_delta',
|
|
180
|
+
),
|
|
181
|
+
]
|
|
182
|
+
'''
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
1. See [`ModelRequest.user_text_prompt`][pydantic_ai.messages.ModelRequest.user_text_prompt] for details.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
model: The model to make a request to. We allow `str` here since the actual list of allowed models changes frequently.
|
|
189
|
+
messages: Messages to send to the model
|
|
190
|
+
model_settings: optional model settings
|
|
191
|
+
model_request_parameters: optional model request parameters
|
|
192
|
+
instrument: Whether to instrument the request with OpenTelemetry/Logfire, if `None` the value from
|
|
193
|
+
[`logfire.instrument_pydantic_ai`][logfire.Logfire.instrument_pydantic_ai] is used.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A [stream response][pydantic_ai.models.StreamedResponse] async context manager.
|
|
197
|
+
"""
|
|
198
|
+
model_instance = _prepare_model(model, instrument)
|
|
199
|
+
return model_instance.request_stream(
|
|
200
|
+
messages,
|
|
201
|
+
model_settings,
|
|
202
|
+
model_instance.customize_request_parameters(model_request_parameters or models.ModelRequestParameters()),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _prepare_model(
|
|
207
|
+
model: models.Model | models.KnownModelName | str,
|
|
208
|
+
instrument: instrumented_models.InstrumentationSettings | bool | None,
|
|
209
|
+
) -> models.Model:
|
|
210
|
+
model_instance = models.infer_model(model)
|
|
211
|
+
|
|
212
|
+
if instrument is None:
|
|
213
|
+
instrument = agent.Agent._instrument_default # pyright: ignore[reportPrivateUsage]
|
|
214
|
+
|
|
215
|
+
return instrumented_models.instrument_model(model_instance, instrument)
|
|
@@ -83,7 +83,7 @@ class VideoUrl:
|
|
|
83
83
|
"""Type identifier, this is available on all parts as a discriminator."""
|
|
84
84
|
|
|
85
85
|
@property
|
|
86
|
-
def media_type(self) -> VideoMediaType:
|
|
86
|
+
def media_type(self) -> VideoMediaType:
|
|
87
87
|
"""Return the media type of the video, based on the url."""
|
|
88
88
|
if self.url.endswith('.mkv'):
|
|
89
89
|
return 'video/x-matroska'
|
|
@@ -110,7 +110,7 @@ class VideoUrl:
|
|
|
110
110
|
|
|
111
111
|
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
|
|
112
112
|
"""
|
|
113
|
-
return
|
|
113
|
+
return _video_format_lookup[self.media_type]
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
@dataclass
|
|
@@ -133,6 +133,11 @@ class AudioUrl:
|
|
|
133
133
|
else:
|
|
134
134
|
raise ValueError(f'Unknown audio file extension: {self.url}')
|
|
135
135
|
|
|
136
|
+
@property
|
|
137
|
+
def format(self) -> AudioFormat:
|
|
138
|
+
"""The file format of the audio file."""
|
|
139
|
+
return _audio_format_lookup[self.media_type]
|
|
140
|
+
|
|
136
141
|
|
|
137
142
|
@dataclass
|
|
138
143
|
class ImageUrl:
|
|
@@ -164,7 +169,7 @@ class ImageUrl:
|
|
|
164
169
|
|
|
165
170
|
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
|
|
166
171
|
"""
|
|
167
|
-
return
|
|
172
|
+
return _image_format_lookup[self.media_type]
|
|
168
173
|
|
|
169
174
|
|
|
170
175
|
@dataclass
|
|
@@ -182,7 +187,7 @@ class DocumentUrl:
|
|
|
182
187
|
"""Return the media type of the document, based on the url."""
|
|
183
188
|
type_, _ = guess_type(self.url)
|
|
184
189
|
if type_ is None:
|
|
185
|
-
raise
|
|
190
|
+
raise ValueError(f'Unknown document file extension: {self.url}')
|
|
186
191
|
return type_
|
|
187
192
|
|
|
188
193
|
@property
|
|
@@ -191,7 +196,11 @@ class DocumentUrl:
|
|
|
191
196
|
|
|
192
197
|
The choice of supported formats were based on the Bedrock Converse API. Other APIs don't require to use a format.
|
|
193
198
|
"""
|
|
194
|
-
|
|
199
|
+
media_type = self.media_type
|
|
200
|
+
try:
|
|
201
|
+
return _document_format_lookup[media_type]
|
|
202
|
+
except KeyError as e:
|
|
203
|
+
raise ValueError(f'Unknown document media type: {media_type}') from e
|
|
195
204
|
|
|
196
205
|
|
|
197
206
|
@dataclass
|
|
@@ -225,93 +234,58 @@ class BinaryContent:
|
|
|
225
234
|
@property
|
|
226
235
|
def is_document(self) -> bool:
|
|
227
236
|
"""Return `True` if the media type is a document type."""
|
|
228
|
-
return self.media_type in
|
|
229
|
-
'application/pdf',
|
|
230
|
-
'text/plain',
|
|
231
|
-
'text/csv',
|
|
232
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
233
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
234
|
-
'text/html',
|
|
235
|
-
'text/markdown',
|
|
236
|
-
'application/vnd.ms-excel',
|
|
237
|
-
}
|
|
237
|
+
return self.media_type in _document_format_lookup
|
|
238
238
|
|
|
239
239
|
@property
|
|
240
240
|
def format(self) -> str:
|
|
241
241
|
"""The file format of the binary content."""
|
|
242
|
-
|
|
243
|
-
if self.
|
|
244
|
-
return
|
|
245
|
-
elif self.
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
raise ValueError(f'Unknown media type: {self.media_type}')
|
|
242
|
+
try:
|
|
243
|
+
if self.is_audio:
|
|
244
|
+
return _audio_format_lookup[self.media_type]
|
|
245
|
+
elif self.is_image:
|
|
246
|
+
return _image_format_lookup[self.media_type]
|
|
247
|
+
elif self.is_video:
|
|
248
|
+
return _video_format_lookup[self.media_type]
|
|
249
|
+
else:
|
|
250
|
+
return _document_format_lookup[self.media_type]
|
|
251
|
+
except KeyError as e:
|
|
252
|
+
raise ValueError(f'Unknown media type: {self.media_type}') from e
|
|
254
253
|
|
|
255
254
|
|
|
256
255
|
UserContent: TypeAlias = 'str | ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent'
|
|
257
256
|
|
|
258
257
|
# Ideally this would be a Union of types, but Python 3.9 requires it to be a string, and strings don't work with `isinstance``.
|
|
259
258
|
MultiModalContentTypes = (ImageUrl, AudioUrl, DocumentUrl, VideoUrl, BinaryContent)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
elif media_type == 'image/webp':
|
|
291
|
-
return 'webp'
|
|
292
|
-
else:
|
|
293
|
-
raise ValueError(f'Unknown image media type: {media_type}')
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def _video_format(media_type: str) -> VideoFormat:
|
|
297
|
-
if media_type == 'video/x-matroska':
|
|
298
|
-
return 'mkv'
|
|
299
|
-
elif media_type == 'video/quicktime':
|
|
300
|
-
return 'mov'
|
|
301
|
-
elif media_type == 'video/mp4':
|
|
302
|
-
return 'mp4'
|
|
303
|
-
elif media_type == 'video/webm':
|
|
304
|
-
return 'webm'
|
|
305
|
-
elif media_type == 'video/x-flv':
|
|
306
|
-
return 'flv'
|
|
307
|
-
elif media_type == 'video/mpeg':
|
|
308
|
-
return 'mpeg'
|
|
309
|
-
elif media_type == 'video/x-ms-wmv':
|
|
310
|
-
return 'wmv'
|
|
311
|
-
elif media_type == 'video/3gpp':
|
|
312
|
-
return 'three_gp'
|
|
313
|
-
else: # pragma: no cover
|
|
314
|
-
raise ValueError(f'Unknown video media type: {media_type}')
|
|
259
|
+
_document_format_lookup: dict[str, DocumentFormat] = {
|
|
260
|
+
'application/pdf': 'pdf',
|
|
261
|
+
'text/plain': 'txt',
|
|
262
|
+
'text/csv': 'csv',
|
|
263
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
|
264
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
|
|
265
|
+
'text/html': 'html',
|
|
266
|
+
'text/markdown': 'md',
|
|
267
|
+
'application/vnd.ms-excel': 'xls',
|
|
268
|
+
}
|
|
269
|
+
_audio_format_lookup: dict[str, AudioFormat] = {
|
|
270
|
+
'audio/mpeg': 'mp3',
|
|
271
|
+
'audio/wav': 'wav',
|
|
272
|
+
}
|
|
273
|
+
_image_format_lookup: dict[str, ImageFormat] = {
|
|
274
|
+
'image/jpeg': 'jpeg',
|
|
275
|
+
'image/png': 'png',
|
|
276
|
+
'image/gif': 'gif',
|
|
277
|
+
'image/webp': 'webp',
|
|
278
|
+
}
|
|
279
|
+
_video_format_lookup: dict[str, VideoFormat] = {
|
|
280
|
+
'video/x-matroska': 'mkv',
|
|
281
|
+
'video/quicktime': 'mov',
|
|
282
|
+
'video/mp4': 'mp4',
|
|
283
|
+
'video/webm': 'webm',
|
|
284
|
+
'video/x-flv': 'flv',
|
|
285
|
+
'video/mpeg': 'mpeg',
|
|
286
|
+
'video/x-ms-wmv': 'wmv',
|
|
287
|
+
'video/3gpp': 'three_gp',
|
|
288
|
+
}
|
|
315
289
|
|
|
316
290
|
|
|
317
291
|
@dataclass
|
|
@@ -478,6 +452,11 @@ class ModelRequest:
|
|
|
478
452
|
kind: Literal['request'] = 'request'
|
|
479
453
|
"""Message type identifier, this is available on all parts as a discriminator."""
|
|
480
454
|
|
|
455
|
+
@classmethod
|
|
456
|
+
def user_text_prompt(cls, user_prompt: str, *, instructions: str | None = None) -> ModelRequest:
|
|
457
|
+
"""Create a `ModelRequest` with a single user prompt as text."""
|
|
458
|
+
return cls(parts=[UserPromptPart(user_prompt)], instructions=instructions)
|
|
459
|
+
|
|
481
460
|
|
|
482
461
|
@dataclass
|
|
483
462
|
class TextPart:
|
|
@@ -260,9 +260,9 @@ KnownModelName = TypeAliasType(
|
|
|
260
260
|
class ModelRequestParameters:
|
|
261
261
|
"""Configuration for an agent's request to a model, specifically related to tools and output handling."""
|
|
262
262
|
|
|
263
|
-
function_tools: list[ToolDefinition]
|
|
264
|
-
allow_text_output: bool
|
|
265
|
-
output_tools: list[ToolDefinition]
|
|
263
|
+
function_tools: list[ToolDefinition] = field(default_factory=list)
|
|
264
|
+
allow_text_output: bool = True
|
|
265
|
+
output_tools: list[ToolDefinition] = field(default_factory=list)
|
|
266
266
|
|
|
267
267
|
|
|
268
268
|
class Model(ABC):
|
|
@@ -26,6 +26,8 @@ from ..settings import ModelSettings
|
|
|
26
26
|
from . import KnownModelName, Model, ModelRequestParameters, StreamedResponse
|
|
27
27
|
from .wrapper import WrapperModel
|
|
28
28
|
|
|
29
|
+
__all__ = 'instrument_model', 'InstrumentationSettings', 'InstrumentedModel'
|
|
30
|
+
|
|
29
31
|
MODEL_SETTING_ATTRIBUTES: tuple[
|
|
30
32
|
Literal[
|
|
31
33
|
'max_tokens',
|
|
@@ -48,6 +50,17 @@ MODEL_SETTING_ATTRIBUTES: tuple[
|
|
|
48
50
|
ANY_ADAPTER = TypeAdapter[Any](Any)
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
def instrument_model(model: Model, instrument: InstrumentationSettings | bool) -> Model:
|
|
54
|
+
"""Instrument a model with OpenTelemetry/logfire."""
|
|
55
|
+
if instrument and not isinstance(model, InstrumentedModel):
|
|
56
|
+
if instrument is True:
|
|
57
|
+
instrument = InstrumentationSettings()
|
|
58
|
+
|
|
59
|
+
model = InstrumentedModel(model, instrument)
|
|
60
|
+
|
|
61
|
+
return model
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
@dataclass(init=False)
|
|
52
65
|
class InstrumentationSettings:
|
|
53
66
|
"""Options for instrumenting models and agents with OpenTelemetry.
|
|
@@ -50,6 +50,7 @@ dependencies = [
|
|
|
50
50
|
"eval-type-backport>=0.2.0",
|
|
51
51
|
"griffe>=1.3.2",
|
|
52
52
|
"httpx>=0.27",
|
|
53
|
+
"fasta2a=={{ version }}",
|
|
53
54
|
"pydantic>=2.10",
|
|
54
55
|
"pydantic-graph=={{ version }}",
|
|
55
56
|
"exceptiongroup; python_version < '3.11'",
|
|
@@ -77,10 +78,13 @@ cli = ["rich>=13", "prompt-toolkit>=3", "argcomplete>=3.5.0"]
|
|
|
77
78
|
mcp = ["mcp>=1.6.0; python_version >= '3.10'"]
|
|
78
79
|
# Evals
|
|
79
80
|
evals = ["pydantic-evals=={{ version }}"]
|
|
81
|
+
# A2A
|
|
82
|
+
a2a = ["fasta2a=={{ version }}"]
|
|
80
83
|
|
|
81
84
|
[dependency-groups]
|
|
82
85
|
dev = [
|
|
83
86
|
"anyio>=4.5.0",
|
|
87
|
+
"asgi-lifespan>=2.1.0",
|
|
84
88
|
"devtools>=0.12.2",
|
|
85
89
|
"coverage[toml]>=7.6.2",
|
|
86
90
|
"dirty-equals>=0.9.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|