pydantic-ai-slim 0.2.2__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.2 → 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.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_cli.py +8 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/agent.py +68 -10
- pydantic_ai_slim-0.2.3/pydantic_ai/direct.py +215 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/messages.py +61 -82
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/__init__.py +3 -3
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/instrumented.py +13 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/test.py +1 -1
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pyproject.toml +4 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/.gitignore +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/README.md +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/__main__.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_agent_graph.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_output.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_parts_manager.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/format_prompt.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/mcp.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/_json_schema.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/cohere.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/function.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/gemini.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/groq.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/openai.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/__init__.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/anthropic.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/azure.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/bedrock.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/cohere.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/groq.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/mistral.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/result.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/settings.py +0 -0
- {pydantic_ai_slim-0.2.2 → pydantic_ai_slim-0.2.3}/pydantic_ai/tools.py +0 -0
- {pydantic_ai_slim-0.2.2 → 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
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations as _annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
5
5
|
import importlib
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from asyncio import CancelledError
|
|
8
9
|
from collections.abc import Sequence
|
|
@@ -167,6 +168,9 @@ Special prompts:
|
|
|
167
168
|
agent: Agent[None, str] = cli_agent
|
|
168
169
|
if args.agent:
|
|
169
170
|
try:
|
|
171
|
+
current_path = os.getcwd()
|
|
172
|
+
sys.path.append(current_path)
|
|
173
|
+
|
|
170
174
|
module_path, variable_name = args.agent.split(':')
|
|
171
175
|
module = importlib.import_module(module_path)
|
|
172
176
|
agent = getattr(module, variable_name)
|
|
@@ -199,6 +203,10 @@ Special prompts:
|
|
|
199
203
|
pass
|
|
200
204
|
return 0
|
|
201
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
|
+
|
|
202
210
|
# doing this instead of `PromptSession[Any](history=` allows mocking of PromptSession in tests
|
|
203
211
|
session: PromptSession[Any] = PromptSession(history=FileHistory(str(PROMPT_HISTORY_PATH)))
|
|
204
212
|
try:
|
|
@@ -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,62 @@ 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
|
+
|
|
1691
1749
|
async def to_cli(self: Self, deps: AgentDepsT = None) -> None:
|
|
1692
1750
|
"""Run the agent in a CLI chat interface.
|
|
1693
1751
|
|
|
@@ -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
|