pydantic-ai-slim 0.0.33__tar.gz → 0.0.34__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.0.33 → pydantic_ai_slim-0.0.34}/PKG-INFO +6 -2
- pydantic_ai_slim-0.0.34/pydantic_ai/_cli.py +225 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/agent.py +29 -9
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/messages.py +11 -2
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/instrumented.py +43 -9
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pyproject.toml +7 -2
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/.gitignore +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/README.md +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/__init__.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_agent_graph.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_griffe.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_parts_manager.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_pydantic.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_result.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_system_prompt.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_utils.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/__init__.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/duckduckgo.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/tavily.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/exceptions.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/format_as_xml.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/__init__.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/anthropic.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/cohere.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/fallback.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/function.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/gemini.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/groq.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/mistral.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/openai.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/test.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/vertexai.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/wrapper.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/__init__.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/deepseek.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/google_gla.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/google_vertex.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/openai.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/py.typed +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/result.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/settings.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/tools.py +0 -0
- {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/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.0.
|
|
3
|
+
Version: 0.0.34
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Author-email: Samuel Colvin <samuel@pydantic.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,11 +29,15 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
29
29
|
Requires-Dist: griffe>=1.3.2
|
|
30
30
|
Requires-Dist: httpx>=0.27
|
|
31
31
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
32
|
-
Requires-Dist: pydantic-graph==0.0.
|
|
32
|
+
Requires-Dist: pydantic-graph==0.0.34
|
|
33
33
|
Requires-Dist: pydantic>=2.10
|
|
34
34
|
Requires-Dist: typing-inspection>=0.4.0
|
|
35
35
|
Provides-Extra: anthropic
|
|
36
36
|
Requires-Dist: anthropic>=0.49.0; extra == 'anthropic'
|
|
37
|
+
Provides-Extra: cli
|
|
38
|
+
Requires-Dist: argcomplete>=3.5.0; extra == 'cli'
|
|
39
|
+
Requires-Dist: prompt-toolkit>=3; extra == 'cli'
|
|
40
|
+
Requires-Dist: rich>=13; extra == 'cli'
|
|
37
41
|
Provides-Extra: cohere
|
|
38
42
|
Requires-Dist: cohere>=5.13.11; extra == 'cohere'
|
|
39
43
|
Provides-Extra: duckduckgo
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
from typing_inspection.introspection import get_literal_values
|
|
13
|
+
|
|
14
|
+
from pydantic_ai.exceptions import UserError
|
|
15
|
+
from pydantic_ai.models import KnownModelName
|
|
16
|
+
from pydantic_graph.nodes import End
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import argcomplete
|
|
20
|
+
from prompt_toolkit import PromptSession
|
|
21
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
|
|
22
|
+
from prompt_toolkit.buffer import Buffer
|
|
23
|
+
from prompt_toolkit.document import Document
|
|
24
|
+
from prompt_toolkit.history import FileHistory
|
|
25
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
26
|
+
from rich.live import Live
|
|
27
|
+
from rich.markdown import CodeBlock, Markdown
|
|
28
|
+
from rich.status import Status
|
|
29
|
+
from rich.syntax import Syntax
|
|
30
|
+
from rich.text import Text
|
|
31
|
+
except ImportError as _import_error:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
'Please install `rich`, `prompt-toolkit` and `argcomplete` to use the PydanticAI CLI, '
|
|
34
|
+
"you can use the `cli` optional group — `pip install 'pydantic-ai-slim[cli]'`"
|
|
35
|
+
) from _import_error
|
|
36
|
+
|
|
37
|
+
from pydantic_ai.agent import Agent
|
|
38
|
+
from pydantic_ai.messages import ModelMessage, PartDeltaEvent, TextPartDelta
|
|
39
|
+
|
|
40
|
+
__version__ = version('pydantic-ai')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SimpleCodeBlock(CodeBlock):
|
|
44
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: # pragma: no cover
|
|
45
|
+
code = str(self.text).rstrip()
|
|
46
|
+
yield Text(self.lexer_name, style='dim')
|
|
47
|
+
yield Syntax(code, self.lexer_name, theme=self.theme, background_color='default', word_wrap=True)
|
|
48
|
+
yield Text(f'/{self.lexer_name}', style='dim')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Markdown.elements['fence'] = SimpleCodeBlock
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cli(args_list: Sequence[str] | None = None) -> int: # noqa: C901 # pragma: no cover
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog='pai',
|
|
57
|
+
description=f"""\
|
|
58
|
+
PydanticAI CLI v{__version__}\n\n
|
|
59
|
+
|
|
60
|
+
Special prompt:
|
|
61
|
+
* `/exit` - exit the interactive mode
|
|
62
|
+
* `/markdown` - show the last markdown output of the last question
|
|
63
|
+
* `/multiline` - toggle multiline mode
|
|
64
|
+
""",
|
|
65
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument('prompt', nargs='?', help='AI Prompt, if omitted fall into interactive mode')
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
'--model',
|
|
70
|
+
nargs='?',
|
|
71
|
+
help='Model to use, it should be "<provider>:<model>" e.g. "openai:gpt-4o". If omitted it will default to "openai:gpt-4o"',
|
|
72
|
+
default='openai:gpt-4o',
|
|
73
|
+
).completer = argcomplete.ChoicesCompleter(list(get_literal_values(KnownModelName))) # type: ignore[reportPrivateUsage]
|
|
74
|
+
parser.add_argument('--no-stream', action='store_true', help='Whether to stream responses from OpenAI')
|
|
75
|
+
parser.add_argument('--version', action='store_true', help='Show version and exit')
|
|
76
|
+
|
|
77
|
+
argcomplete.autocomplete(parser)
|
|
78
|
+
args = parser.parse_args(args_list)
|
|
79
|
+
|
|
80
|
+
console = Console()
|
|
81
|
+
console.print(f'pai - PydanticAI CLI v{__version__}', style='green bold', highlight=False)
|
|
82
|
+
if args.version:
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
now_utc = datetime.now(timezone.utc)
|
|
86
|
+
tzname = now_utc.astimezone().tzinfo.tzname(now_utc) # type: ignore
|
|
87
|
+
try:
|
|
88
|
+
agent = Agent(
|
|
89
|
+
model=args.model or 'openai:gpt-4o',
|
|
90
|
+
system_prompt=f"""\
|
|
91
|
+
Help the user by responding to their request, the output should be concise and always written in markdown.
|
|
92
|
+
The current date and time is {datetime.now()} {tzname}.
|
|
93
|
+
The user is running {sys.platform}.""",
|
|
94
|
+
)
|
|
95
|
+
except UserError:
|
|
96
|
+
console.print(f'[red]Invalid model "{args.model}"[/red]')
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
stream = not args.no_stream
|
|
100
|
+
|
|
101
|
+
if prompt := cast(str, args.prompt):
|
|
102
|
+
try:
|
|
103
|
+
asyncio.run(ask_agent(agent, prompt, stream, console))
|
|
104
|
+
except KeyboardInterrupt:
|
|
105
|
+
pass
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
history = Path.home() / '.pai-prompt-history.txt'
|
|
109
|
+
session = PromptSession(history=FileHistory(str(history))) # type: ignore
|
|
110
|
+
multiline = False
|
|
111
|
+
messages: list[ModelMessage] = []
|
|
112
|
+
|
|
113
|
+
while True:
|
|
114
|
+
try:
|
|
115
|
+
auto_suggest = CustomAutoSuggest(['/markdown', '/multiline', '/exit'])
|
|
116
|
+
text = cast(str, session.prompt('pai ➤ ', auto_suggest=auto_suggest, multiline=multiline))
|
|
117
|
+
except (KeyboardInterrupt, EOFError):
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
if not text.strip():
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
ident_prompt = text.lower().strip(' ').replace(' ', '-').lstrip(' ')
|
|
124
|
+
if ident_prompt == '/markdown':
|
|
125
|
+
try:
|
|
126
|
+
parts = messages[-1].parts
|
|
127
|
+
except IndexError:
|
|
128
|
+
console.print('[dim]No markdown output available.[/dim]')
|
|
129
|
+
continue
|
|
130
|
+
for part in parts:
|
|
131
|
+
if part.part_kind == 'text':
|
|
132
|
+
last_content = part.content
|
|
133
|
+
console.print('[dim]Last markdown output of last question:[/dim]\n')
|
|
134
|
+
console.print(Syntax(last_content, lexer='markdown', background_color='default'))
|
|
135
|
+
|
|
136
|
+
continue
|
|
137
|
+
if ident_prompt == '/multiline':
|
|
138
|
+
multiline = not multiline
|
|
139
|
+
if multiline:
|
|
140
|
+
console.print(
|
|
141
|
+
'Enabling multiline mode. '
|
|
142
|
+
'[dim]Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.[/dim]'
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
console.print('Disabling multiline mode.')
|
|
146
|
+
continue
|
|
147
|
+
if ident_prompt == '/exit':
|
|
148
|
+
console.print('[dim]Exiting…[/dim]')
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
messages = asyncio.run(ask_agent(agent, text, stream, console, messages))
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def ask_agent(
|
|
158
|
+
agent: Agent,
|
|
159
|
+
prompt: str,
|
|
160
|
+
stream: bool,
|
|
161
|
+
console: Console,
|
|
162
|
+
messages: list[ModelMessage] | None = None,
|
|
163
|
+
) -> list[ModelMessage]: # pragma: no cover
|
|
164
|
+
status: None | Status = Status('[dim]Working on it…[/dim]', console=console)
|
|
165
|
+
live = Live('', refresh_per_second=15, console=console)
|
|
166
|
+
status.start()
|
|
167
|
+
|
|
168
|
+
async with agent.iter(prompt, message_history=messages) as agent_run:
|
|
169
|
+
console.print('\nResponse:', style='green')
|
|
170
|
+
|
|
171
|
+
content: str = ''
|
|
172
|
+
interrupted = False
|
|
173
|
+
try:
|
|
174
|
+
node = agent_run.next_node
|
|
175
|
+
while not isinstance(node, End):
|
|
176
|
+
node = await agent_run.next(node)
|
|
177
|
+
if Agent.is_model_request_node(node):
|
|
178
|
+
async with node.stream(agent_run.ctx) as handle_stream:
|
|
179
|
+
# NOTE(Marcelo): It took me a lot of time to figure out how to stop `status` and start `live`
|
|
180
|
+
# in a context manager, so I had to do it manually with `stop` and `start` methods.
|
|
181
|
+
# PR welcome to simplify this code.
|
|
182
|
+
if status is not None:
|
|
183
|
+
status.stop()
|
|
184
|
+
status = None
|
|
185
|
+
if not live.is_started:
|
|
186
|
+
live.start()
|
|
187
|
+
async for event in handle_stream:
|
|
188
|
+
if isinstance(event, PartDeltaEvent) and isinstance(event.delta, TextPartDelta):
|
|
189
|
+
if stream:
|
|
190
|
+
content += event.delta.content_delta
|
|
191
|
+
live.update(Markdown(content))
|
|
192
|
+
except KeyboardInterrupt:
|
|
193
|
+
interrupted = True
|
|
194
|
+
finally:
|
|
195
|
+
live.stop()
|
|
196
|
+
|
|
197
|
+
if interrupted:
|
|
198
|
+
console.print('[dim]Interrupted[/dim]')
|
|
199
|
+
|
|
200
|
+
assert agent_run.result
|
|
201
|
+
if not stream:
|
|
202
|
+
content = agent_run.result.data
|
|
203
|
+
console.print(Markdown(content))
|
|
204
|
+
return agent_run.result.all_messages()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class CustomAutoSuggest(AutoSuggestFromHistory):
|
|
208
|
+
def __init__(self, special_suggestions: list[str] | None = None): # pragma: no cover
|
|
209
|
+
super().__init__()
|
|
210
|
+
self.special_suggestions = special_suggestions or []
|
|
211
|
+
|
|
212
|
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None: # pragma: no cover
|
|
213
|
+
# Get the suggestion from history
|
|
214
|
+
suggestion = super().get_suggestion(buffer, document)
|
|
215
|
+
|
|
216
|
+
# Check for custom suggestions
|
|
217
|
+
text = document.text_before_cursor.strip()
|
|
218
|
+
for special in self.special_suggestions:
|
|
219
|
+
if special.startswith(text):
|
|
220
|
+
return Suggestion(special[len(text) :])
|
|
221
|
+
return suggestion
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def app(): # pragma: no cover
|
|
225
|
+
sys.exit(cli())
|
|
@@ -6,7 +6,7 @@ from collections.abc import AsyncIterator, Awaitable, Iterator, Sequence
|
|
|
6
6
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager
|
|
7
7
|
from copy import deepcopy
|
|
8
8
|
from types import FrameType
|
|
9
|
-
from typing import Any, Callable, Generic, cast, final, overload
|
|
9
|
+
from typing import Any, Callable, ClassVar, Generic, cast, final, overload
|
|
10
10
|
|
|
11
11
|
from opentelemetry.trace import NoOpTracer, use_span
|
|
12
12
|
from typing_extensions import TypeGuard, TypeVar, deprecated
|
|
@@ -25,7 +25,7 @@ from . import (
|
|
|
25
25
|
result,
|
|
26
26
|
usage as _usage,
|
|
27
27
|
)
|
|
28
|
-
from .models.instrumented import InstrumentedModel
|
|
28
|
+
from .models.instrumented import InstrumentationSettings, InstrumentedModel
|
|
29
29
|
from .result import FinalResult, ResultDataT, StreamedRunResult
|
|
30
30
|
from .settings import ModelSettings, merge_model_settings
|
|
31
31
|
from .tools import (
|
|
@@ -56,6 +56,7 @@ __all__ = (
|
|
|
56
56
|
'CallToolsNode',
|
|
57
57
|
'ModelRequestNode',
|
|
58
58
|
'UserPromptNode',
|
|
59
|
+
'InstrumentationSettings',
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
|
|
@@ -112,8 +113,10 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
112
113
|
The type of the result data, used to validate the result data, defaults to `str`.
|
|
113
114
|
"""
|
|
114
115
|
|
|
115
|
-
instrument: bool
|
|
116
|
-
"""
|
|
116
|
+
instrument: InstrumentationSettings | bool | None
|
|
117
|
+
"""Options to automatically instrument with OpenTelemetry."""
|
|
118
|
+
|
|
119
|
+
_instrument_default: ClassVar[InstrumentationSettings | bool] = False
|
|
117
120
|
|
|
118
121
|
_deps_type: type[AgentDepsT] = dataclasses.field(repr=False)
|
|
119
122
|
_result_tool_name: str = dataclasses.field(repr=False)
|
|
@@ -147,7 +150,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
147
150
|
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
|
|
148
151
|
defer_model_check: bool = False,
|
|
149
152
|
end_strategy: EndStrategy = 'early',
|
|
150
|
-
instrument: bool =
|
|
153
|
+
instrument: InstrumentationSettings | bool | None = None,
|
|
151
154
|
):
|
|
152
155
|
"""Create an agent.
|
|
153
156
|
|
|
@@ -177,7 +180,12 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
177
180
|
[override the model][pydantic_ai.Agent.override] for testing.
|
|
178
181
|
end_strategy: Strategy for handling tool calls that are requested alongside a final result.
|
|
179
182
|
See [`EndStrategy`][pydantic_ai.agent.EndStrategy] for more information.
|
|
180
|
-
instrument:
|
|
183
|
+
instrument: Set to True to automatically instrument with OpenTelemetry,
|
|
184
|
+
which will use Logfire if it's configured.
|
|
185
|
+
Set to an instance of [`InstrumentationSettings`][pydantic_ai.agent.InstrumentationSettings] to customize.
|
|
186
|
+
If this isn't set, then the last value set by
|
|
187
|
+
[`Agent.instrument_all()`][pydantic_ai.Agent.instrument_all]
|
|
188
|
+
will be used, which defaults to False.
|
|
181
189
|
"""
|
|
182
190
|
if model is None or defer_model_check:
|
|
183
191
|
self.model = model
|
|
@@ -213,6 +221,11 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
213
221
|
else:
|
|
214
222
|
self._register_tool(Tool(tool))
|
|
215
223
|
|
|
224
|
+
@staticmethod
|
|
225
|
+
def instrument_all(instrument: InstrumentationSettings | bool = True) -> None:
|
|
226
|
+
"""Set the instrumentation options for all agents where `instrument` is not set."""
|
|
227
|
+
Agent._instrument_default = instrument
|
|
228
|
+
|
|
216
229
|
@overload
|
|
217
230
|
async def run(
|
|
218
231
|
self,
|
|
@@ -422,7 +435,7 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
422
435
|
usage_limits = usage_limits or _usage.UsageLimits()
|
|
423
436
|
|
|
424
437
|
if isinstance(model_used, InstrumentedModel):
|
|
425
|
-
tracer = model_used.tracer
|
|
438
|
+
tracer = model_used.options.tracer
|
|
426
439
|
else:
|
|
427
440
|
tracer = NoOpTracer()
|
|
428
441
|
agent_name = self.name or 'agent'
|
|
@@ -1119,8 +1132,15 @@ class Agent(Generic[AgentDepsT, ResultDataT]):
|
|
|
1119
1132
|
else:
|
|
1120
1133
|
raise exceptions.UserError('`model` must be set either when creating the agent or when calling it.')
|
|
1121
1134
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1135
|
+
instrument = self.instrument
|
|
1136
|
+
if instrument is None:
|
|
1137
|
+
instrument = self._instrument_default
|
|
1138
|
+
|
|
1139
|
+
if instrument and not isinstance(model_, InstrumentedModel):
|
|
1140
|
+
if instrument is True:
|
|
1141
|
+
instrument = InstrumentationSettings()
|
|
1142
|
+
|
|
1143
|
+
model_ = InstrumentedModel(model_, instrument)
|
|
1124
1144
|
|
|
1125
1145
|
return model_
|
|
1126
1146
|
|
|
@@ -189,7 +189,10 @@ class ToolReturnPart:
|
|
|
189
189
|
return {'return_value': tool_return_ta.dump_python(self.content, mode='json')}
|
|
190
190
|
|
|
191
191
|
def otel_event(self) -> Event:
|
|
192
|
-
return Event(
|
|
192
|
+
return Event(
|
|
193
|
+
'gen_ai.tool.message',
|
|
194
|
+
body={'content': self.content, 'role': 'tool', 'id': self.tool_call_id, 'name': self.tool_name},
|
|
195
|
+
)
|
|
193
196
|
|
|
194
197
|
|
|
195
198
|
error_details_ta = pydantic.TypeAdapter(list[pydantic_core.ErrorDetails], config=pydantic.ConfigDict(defer_build=True))
|
|
@@ -244,7 +247,13 @@ class RetryPromptPart:
|
|
|
244
247
|
return Event('gen_ai.user.message', body={'content': self.model_response(), 'role': 'user'})
|
|
245
248
|
else:
|
|
246
249
|
return Event(
|
|
247
|
-
'gen_ai.tool.message',
|
|
250
|
+
'gen_ai.tool.message',
|
|
251
|
+
body={
|
|
252
|
+
'content': self.model_response(),
|
|
253
|
+
'role': 'tool',
|
|
254
|
+
'id': self.tool_call_id,
|
|
255
|
+
'name': self.tool_name,
|
|
256
|
+
},
|
|
248
257
|
)
|
|
249
258
|
|
|
250
259
|
|
|
@@ -43,9 +43,16 @@ MODEL_SETTING_ATTRIBUTES: tuple[
|
|
|
43
43
|
ANY_ADAPTER = TypeAdapter[Any](Any)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@dataclass
|
|
47
|
-
class
|
|
48
|
-
"""
|
|
46
|
+
@dataclass(init=False)
|
|
47
|
+
class InstrumentationSettings:
|
|
48
|
+
"""Options for instrumenting models and agents with OpenTelemetry.
|
|
49
|
+
|
|
50
|
+
Used in:
|
|
51
|
+
|
|
52
|
+
- `Agent(instrument=...)`
|
|
53
|
+
- [`Agent.instrument_all()`][pydantic_ai.agent.Agent.instrument_all]
|
|
54
|
+
- `InstrumentedModel`
|
|
55
|
+
"""
|
|
49
56
|
|
|
50
57
|
tracer: Tracer = field(repr=False)
|
|
51
58
|
event_logger: EventLogger = field(repr=False)
|
|
@@ -53,20 +60,47 @@ class InstrumentedModel(WrapperModel):
|
|
|
53
60
|
|
|
54
61
|
def __init__(
|
|
55
62
|
self,
|
|
56
|
-
|
|
63
|
+
*,
|
|
64
|
+
event_mode: Literal['attributes', 'logs'] = 'attributes',
|
|
57
65
|
tracer_provider: TracerProvider | None = None,
|
|
58
66
|
event_logger_provider: EventLoggerProvider | None = None,
|
|
59
|
-
event_mode: Literal['attributes', 'logs'] = 'attributes',
|
|
60
67
|
):
|
|
68
|
+
"""Create instrumentation options.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
event_mode: The mode for emitting events. If `'attributes'`, events are attached to the span as attributes.
|
|
72
|
+
If `'logs'`, events are emitted as OpenTelemetry log-based events.
|
|
73
|
+
tracer_provider: The OpenTelemetry tracer provider to use.
|
|
74
|
+
If not provided, the global tracer provider is used.
|
|
75
|
+
Calling `logfire.configure()` sets the global tracer provider, so most users don't need this.
|
|
76
|
+
event_logger_provider: The OpenTelemetry event logger provider to use.
|
|
77
|
+
If not provided, the global event logger provider is used.
|
|
78
|
+
Calling `logfire.configure()` sets the global event logger provider, so most users don't need this.
|
|
79
|
+
This is only used if `event_mode='logs'`.
|
|
80
|
+
"""
|
|
61
81
|
from pydantic_ai import __version__
|
|
62
82
|
|
|
63
|
-
super().__init__(wrapped)
|
|
64
83
|
tracer_provider = tracer_provider or get_tracer_provider()
|
|
65
84
|
event_logger_provider = event_logger_provider or get_event_logger_provider()
|
|
66
85
|
self.tracer = tracer_provider.get_tracer('pydantic-ai', __version__)
|
|
67
86
|
self.event_logger = event_logger_provider.get_event_logger('pydantic-ai', __version__)
|
|
68
87
|
self.event_mode = event_mode
|
|
69
88
|
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class InstrumentedModel(WrapperModel):
|
|
92
|
+
"""Model which is instrumented with OpenTelemetry."""
|
|
93
|
+
|
|
94
|
+
options: InstrumentationSettings
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
wrapped: Model | KnownModelName,
|
|
99
|
+
options: InstrumentationSettings | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
super().__init__(wrapped)
|
|
102
|
+
self.options = options or InstrumentationSettings()
|
|
103
|
+
|
|
70
104
|
async def request(
|
|
71
105
|
self,
|
|
72
106
|
messages: list[ModelMessage],
|
|
@@ -123,7 +157,7 @@ class InstrumentedModel(WrapperModel):
|
|
|
123
157
|
if isinstance(value := model_settings.get(key), (float, int)):
|
|
124
158
|
attributes[f'gen_ai.request.{key}'] = value
|
|
125
159
|
|
|
126
|
-
with self.tracer.start_as_current_span(span_name, attributes=attributes) as span:
|
|
160
|
+
with self.options.tracer.start_as_current_span(span_name, attributes=attributes) as span:
|
|
127
161
|
|
|
128
162
|
def finish(response: ModelResponse, usage: Usage):
|
|
129
163
|
if not span.is_recording():
|
|
@@ -156,9 +190,9 @@ class InstrumentedModel(WrapperModel):
|
|
|
156
190
|
def _emit_events(self, system: str, span: Span, events: list[Event]) -> None:
|
|
157
191
|
for event in events:
|
|
158
192
|
event.attributes = {'gen_ai.system': system, **(event.attributes or {})}
|
|
159
|
-
if self.event_mode == 'logs':
|
|
193
|
+
if self.options.event_mode == 'logs':
|
|
160
194
|
for event in events:
|
|
161
|
-
self.event_logger.emit(event)
|
|
195
|
+
self.options.event_logger.emit(event)
|
|
162
196
|
else:
|
|
163
197
|
attr_name = 'events'
|
|
164
198
|
span.set_attributes(
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pydantic-ai-slim"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.34"
|
|
8
8
|
description = "Agent Framework / shim to use Pydantic with LLMs, slim package"
|
|
9
9
|
authors = [{ name = "Samuel Colvin", email = "samuel@pydantic.dev" }]
|
|
10
10
|
license = "MIT"
|
|
@@ -36,7 +36,7 @@ dependencies = [
|
|
|
36
36
|
"griffe>=1.3.2",
|
|
37
37
|
"httpx>=0.27",
|
|
38
38
|
"pydantic>=2.10",
|
|
39
|
-
"pydantic-graph==0.0.
|
|
39
|
+
"pydantic-graph==0.0.34",
|
|
40
40
|
"exceptiongroup; python_version < '3.11'",
|
|
41
41
|
"opentelemetry-api>=1.28.0",
|
|
42
42
|
"typing-inspection>=0.4.0",
|
|
@@ -55,6 +55,8 @@ mistral = ["mistralai>=1.2.5"]
|
|
|
55
55
|
# Tools
|
|
56
56
|
duckduckgo = ["duckduckgo-search>=7.0.0"]
|
|
57
57
|
tavily = ["tavily-python>=0.5.0"]
|
|
58
|
+
# CLI
|
|
59
|
+
cli = ["rich>=13", "prompt-toolkit>=3", "argcomplete>=3.5.0"]
|
|
58
60
|
|
|
59
61
|
[dependency-groups]
|
|
60
62
|
dev = [
|
|
@@ -71,6 +73,9 @@ dev = [
|
|
|
71
73
|
"diff-cover>=9.2.0",
|
|
72
74
|
]
|
|
73
75
|
|
|
76
|
+
[project.scripts]
|
|
77
|
+
pai = "pydantic_ai._cli:app"
|
|
78
|
+
|
|
74
79
|
[tool.hatch.build.targets.wheel]
|
|
75
80
|
packages = ["pydantic_ai"]
|
|
76
81
|
|
|
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
|