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.

Files changed (43) hide show
  1. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/PKG-INFO +6 -2
  2. pydantic_ai_slim-0.0.34/pydantic_ai/_cli.py +225 -0
  3. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/agent.py +29 -9
  4. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/messages.py +11 -2
  5. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/instrumented.py +43 -9
  6. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pyproject.toml +7 -2
  7. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/.gitignore +0 -0
  8. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/README.md +0 -0
  9. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/__init__.py +0 -0
  10. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_agent_graph.py +0 -0
  11. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_griffe.py +0 -0
  12. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_parts_manager.py +0 -0
  13. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_pydantic.py +0 -0
  14. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_result.py +0 -0
  15. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_system_prompt.py +0 -0
  16. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/_utils.py +0 -0
  17. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/__init__.py +0 -0
  18. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  19. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/common_tools/tavily.py +0 -0
  20. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/exceptions.py +0 -0
  21. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/format_as_xml.py +0 -0
  22. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/__init__.py +0 -0
  23. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/anthropic.py +0 -0
  24. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/cohere.py +0 -0
  25. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/fallback.py +0 -0
  26. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/function.py +0 -0
  27. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/gemini.py +0 -0
  28. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/groq.py +0 -0
  29. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/mistral.py +0 -0
  30. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/openai.py +0 -0
  31. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/test.py +0 -0
  32. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/vertexai.py +0 -0
  33. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/models/wrapper.py +0 -0
  34. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/__init__.py +0 -0
  35. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/deepseek.py +0 -0
  36. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/google_gla.py +0 -0
  37. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/google_vertex.py +0 -0
  38. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/providers/openai.py +0 -0
  39. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/py.typed +0 -0
  40. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/result.py +0 -0
  41. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/settings.py +0 -0
  42. {pydantic_ai_slim-0.0.33 → pydantic_ai_slim-0.0.34}/pydantic_ai/tools.py +0 -0
  43. {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.33
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.33
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
- """Automatically instrument with OpenTelemetry. Will use Logfire if it's configured."""
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 = False,
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: Automatically instrument with OpenTelemetry. Will use Logfire if it's configured.
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
- if self.instrument and not isinstance(model_, InstrumentedModel):
1123
- model_ = InstrumentedModel(model_)
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('gen_ai.tool.message', body={'content': self.content, 'role': 'tool', 'id': self.tool_call_id})
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', body={'content': self.model_response(), 'role': 'tool', 'id': self.tool_call_id}
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 InstrumentedModel(WrapperModel):
48
- """Model which is instrumented with OpenTelemetry."""
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
- wrapped: Model | KnownModelName,
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.33"
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.33",
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