Skip to content

Commit 996781d

Browse files
alexmojakiDouweM
andauthored
Default InstrumentationSettings version to 2 (#2726)
Co-authored-by: Douwe Maan <[email protected]>
1 parent 49d2829 commit 996781d

File tree

5 files changed

+275
-143
lines changed

5 files changed

+275
-143
lines changed

docs/logfire.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,24 +267,22 @@ The following providers have dedicated documentation on Pydantic AI:
267267

268268
### Configuring data format
269269

270-
Pydantic AI follows the [OpenTelemetry Semantic Conventions for Generative AI systems](https://opentelemetry.io/docs/specs/semconv/gen-ai/), with one caveat. The semantic conventions specify that messages should be captured as individual events (logs) that are children of the request span. By default, Pydantic AI instead collects these events into a JSON array which is set as a single large attribute called `events` on the request span. To change this, use `event_mode='logs'`:
270+
Pydantic AI follows the [OpenTelemetry Semantic Conventions for Generative AI systems](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Specifically, it follows version 1.37.0 of the conventions by default. To use [version 1.36.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/gen-ai/README.md) or older, pass [`InstrumentationSettings(version=1)`][pydantic_ai.models.instrumented.InstrumentationSettings] (the default is `version=2`). Moreover, those semantic conventions specify that messages should be captured as individual events (logs) that are children of the request span, whereas by default, Pydantic AI instead collects these events into a JSON array which is set as a single large attribute called `events` on the request span. To change this, use `event_mode='logs'`:
271271

272272
```python {title="instrumentation_settings_event_mode.py"}
273273
import logfire
274274

275275
from pydantic_ai import Agent
276276

277277
logfire.configure()
278-
logfire.instrument_pydantic_ai(event_mode='logs')
278+
logfire.instrument_pydantic_ai(version=1, event_mode='logs')
279279
agent = Agent('openai:gpt-4o')
280280
result = agent.run_sync('What is the capital of France?')
281281
print(result.output)
282282
#> The capital of France is Paris.
283283
```
284284

285-
For now, this won't look as good in the Logfire UI, but we're working on it.
286-
287-
If you have very long conversations, the `events` span attribute may be truncated. Using `event_mode='logs'` will help avoid this issue.
285+
This won't look as good in the Logfire UI, and will also be removed from Pydantic AI in a future release, but may be useful for backwards compatibility.
288286

289287
Note that the OpenTelemetry Semantic Conventions are still experimental and are likely to change.
290288

pydantic_ai_slim/pydantic_ai/models/instrumented.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import itertools
44
import json
5+
import warnings
56
from collections.abc import AsyncIterator, Callable, Iterator, Mapping
67
from contextlib import asynccontextmanager, contextmanager
78
from dataclasses import dataclass, field
@@ -93,36 +94,41 @@ class InstrumentationSettings:
9394
def __init__(
9495
self,
9596
*,
96-
event_mode: Literal['attributes', 'logs'] = 'attributes',
9797
tracer_provider: TracerProvider | None = None,
9898
meter_provider: MeterProvider | None = None,
99-
event_logger_provider: EventLoggerProvider | None = None,
10099
include_binary_content: bool = True,
101100
include_content: bool = True,
102-
version: Literal[1, 2] = 1,
101+
version: Literal[1, 2] = 2,
102+
event_mode: Literal['attributes', 'logs'] = 'attributes',
103+
event_logger_provider: EventLoggerProvider | None = None,
103104
):
104105
"""Create instrumentation options.
105106
106107
Args:
107-
event_mode: The mode for emitting events. If `'attributes'`, events are attached to the span as attributes.
108-
If `'logs'`, events are emitted as OpenTelemetry log-based events.
109108
tracer_provider: The OpenTelemetry tracer provider to use.
110109
If not provided, the global tracer provider is used.
111110
Calling `logfire.configure()` sets the global tracer provider, so most users don't need this.
112111
meter_provider: The OpenTelemetry meter provider to use.
113112
If not provided, the global meter provider is used.
114113
Calling `logfire.configure()` sets the global meter provider, so most users don't need this.
115-
event_logger_provider: The OpenTelemetry event logger provider to use.
116-
If not provided, the global event logger provider is used.
117-
Calling `logfire.configure()` sets the global event logger provider, so most users don't need this.
118-
This is only used if `event_mode='logs'`.
119114
include_binary_content: Whether to include binary content in the instrumentation events.
120115
include_content: Whether to include prompts, completions, and tool call arguments and responses
121116
in the instrumentation events.
122-
version: Version of the data format.
123-
Version 1 is based on the legacy event-based OpenTelemetry GenAI spec.
124-
Version 2 stores messages in the attributes `gen_ai.input.messages` and `gen_ai.output.messages`.
125-
Version 2 is still WIP and experimental, but will become the default in Pydantic AI v1.
117+
version: Version of the data format. This is unrelated to the Pydantic AI package version.
118+
Version 1 is based on the legacy event-based OpenTelemetry GenAI spec
119+
and will be removed in a future release.
120+
The parameters `event_mode` and `event_logger_provider` are only relevant for version 1.
121+
Version 2 uses the newer OpenTelemetry GenAI spec and stores messages in the following attributes:
122+
- `gen_ai.system_instructions` for instructions passed to the agent.
123+
- `gen_ai.input.messages` and `gen_ai.output.messages` on model request spans.
124+
- `pydantic_ai.all_messages` on agent run spans.
125+
event_mode: The mode for emitting events in version 1.
126+
If `'attributes'`, events are attached to the span as attributes.
127+
If `'logs'`, events are emitted as OpenTelemetry log-based events.
128+
event_logger_provider: The OpenTelemetry event logger provider to use.
129+
If not provided, the global event logger provider is used.
130+
Calling `logfire.configure()` sets the global event logger provider, so most users don't need this.
131+
This is only used if `event_mode='logs'` and `version=1`.
126132
"""
127133
from pydantic_ai import __version__
128134

@@ -136,6 +142,14 @@ def __init__(
136142
self.event_mode = event_mode
137143
self.include_binary_content = include_binary_content
138144
self.include_content = include_content
145+
146+
if event_mode == 'logs' and version != 1:
147+
warnings.warn(
148+
'event_mode is only relevant for version=1 which is deprecated and will be removed in a future release.',
149+
stacklevel=2,
150+
)
151+
version = 1
152+
139153
self.version = version
140154

141155
# As specified in the OpenTelemetry GenAI metrics spec:

tests/models/test_fallback.py

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def test_first_failed_instrumented(capfire: CaptureLogfire) -> None:
122122
),
123123
]
124124
)
125-
assert capfire.exporter.exported_spans_as_dict() == snapshot(
125+
assert capfire.exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot(
126126
[
127127
{
128128
'name': 'chat function:success_response:',
@@ -132,16 +132,33 @@ def test_first_failed_instrumented(capfire: CaptureLogfire) -> None:
132132
'end_time': 3000000000,
133133
'attributes': {
134134
'gen_ai.operation.name': 'chat',
135-
'model_request_parameters': '{"function_tools": [], "builtin_tools": [], "output_mode": "text", "output_object": null, "output_tools": [], "allow_text_output": true}',
135+
'model_request_parameters': {
136+
'function_tools': [],
137+
'builtin_tools': [],
138+
'output_mode': 'text',
139+
'output_object': None,
140+
'output_tools': [],
141+
'allow_text_output': True,
142+
},
136143
'logfire.span_type': 'span',
137144
'logfire.msg': 'chat fallback:function:failure_response:,function:success_response:',
138145
'gen_ai.system': 'function',
139146
'gen_ai.request.model': 'function:success_response:',
147+
'gen_ai.input.messages': [{'role': 'user', 'parts': [{'type': 'text', 'content': 'hello'}]}],
148+
'gen_ai.output.messages': [
149+
{'role': 'assistant', 'parts': [{'type': 'text', 'content': 'success'}]}
150+
],
140151
'gen_ai.usage.input_tokens': 51,
141152
'gen_ai.usage.output_tokens': 1,
142153
'gen_ai.response.model': 'function:success_response:',
143-
'events': '[{"content": "hello", "role": "user", "gen_ai.system": "function", "gen_ai.message.index": 0, "event.name": "gen_ai.user.message"}, {"index": 0, "message": {"role": "assistant", "content": "success"}, "gen_ai.system": "function", "event.name": "gen_ai.choice"}]',
144-
'logfire.json_schema': '{"type": "object", "properties": {"events": {"type": "array"}, "model_request_parameters": {"type": "object"}}}',
154+
'logfire.json_schema': {
155+
'type': 'object',
156+
'properties': {
157+
'gen_ai.input.messages': {'type': 'array'},
158+
'gen_ai.output.messages': {'type': 'array'},
159+
'model_request_parameters': {'type': 'object'},
160+
},
161+
},
145162
},
146163
},
147164
{
@@ -156,10 +173,19 @@ def test_first_failed_instrumented(capfire: CaptureLogfire) -> None:
156173
'logfire.msg': 'agent run',
157174
'logfire.span_type': 'span',
158175
'gen_ai.usage.input_tokens': 51,
159-
'all_messages_events': '[{"content": "hello", "role": "user", "gen_ai.message.index": 0, "event.name": "gen_ai.user.message"}, {"role": "assistant", "content": "success", "gen_ai.message.index": 1, "event.name": "gen_ai.assistant.message"}]',
160176
'gen_ai.usage.output_tokens': 1,
177+
'pydantic_ai.all_messages': [
178+
{'role': 'user', 'parts': [{'type': 'text', 'content': 'hello'}]},
179+
{'role': 'assistant', 'parts': [{'type': 'text', 'content': 'success'}]},
180+
],
161181
'final_result': 'success',
162-
'logfire.json_schema': '{"type": "object", "properties": {"all_messages_events": {"type": "array"}, "final_result": {"type": "object"}}}',
182+
'logfire.json_schema': {
183+
'type': 'object',
184+
'properties': {
185+
'pydantic_ai.all_messages': {'type': 'array'},
186+
'final_result': {'type': 'object'},
187+
},
188+
},
163189
},
164190
},
165191
]
@@ -195,7 +221,7 @@ async def test_first_failed_instrumented_stream(capfire: CaptureLogfire) -> None
195221
)
196222
assert result.is_complete
197223

198-
assert capfire.exporter.exported_spans_as_dict() == snapshot(
224+
assert capfire.exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot(
199225
[
200226
{
201227
'name': 'chat function::success_response_stream',
@@ -205,16 +231,33 @@ async def test_first_failed_instrumented_stream(capfire: CaptureLogfire) -> None
205231
'end_time': 3000000000,
206232
'attributes': {
207233
'gen_ai.operation.name': 'chat',
208-
'model_request_parameters': '{"function_tools": [], "builtin_tools": [], "output_mode": "text", "output_object": null, "output_tools": [], "allow_text_output": true}',
234+
'model_request_parameters': {
235+
'function_tools': [],
236+
'builtin_tools': [],
237+
'output_mode': 'text',
238+
'output_object': None,
239+
'output_tools': [],
240+
'allow_text_output': True,
241+
},
209242
'logfire.span_type': 'span',
210243
'logfire.msg': 'chat fallback:function::failure_response_stream,function::success_response_stream',
211244
'gen_ai.system': 'function',
212245
'gen_ai.request.model': 'function::success_response_stream',
246+
'gen_ai.input.messages': [{'role': 'user', 'parts': [{'type': 'text', 'content': 'input'}]}],
247+
'gen_ai.output.messages': [
248+
{'role': 'assistant', 'parts': [{'type': 'text', 'content': 'hello world'}]}
249+
],
213250
'gen_ai.usage.input_tokens': 50,
214251
'gen_ai.usage.output_tokens': 2,
215252
'gen_ai.response.model': 'function::success_response_stream',
216-
'events': '[{"content": "input", "role": "user", "gen_ai.system": "function", "gen_ai.message.index": 0, "event.name": "gen_ai.user.message"}, {"index": 0, "message": {"role": "assistant", "content": "hello world"}, "gen_ai.system": "function", "event.name": "gen_ai.choice"}]',
217-
'logfire.json_schema': '{"type": "object", "properties": {"events": {"type": "array"}, "model_request_parameters": {"type": "object"}}}',
253+
'logfire.json_schema': {
254+
'type': 'object',
255+
'properties': {
256+
'gen_ai.input.messages': {'type': 'array'},
257+
'gen_ai.output.messages': {'type': 'array'},
258+
'model_request_parameters': {'type': 'object'},
259+
},
260+
},
218261
},
219262
},
220263
{
@@ -230,8 +273,17 @@ async def test_first_failed_instrumented_stream(capfire: CaptureLogfire) -> None
230273
'logfire.span_type': 'span',
231274
'gen_ai.usage.input_tokens': 50,
232275
'gen_ai.usage.output_tokens': 2,
233-
'all_messages_events': '[{"content": "input", "role": "user", "gen_ai.message.index": 0, "event.name": "gen_ai.user.message"}, {"role": "assistant", "content": "hello world", "gen_ai.message.index": 1, "event.name": "gen_ai.assistant.message"}]',
234-
'logfire.json_schema': '{"type": "object", "properties": {"all_messages_events": {"type": "array"}, "final_result": {"type": "object"}}}',
276+
'pydantic_ai.all_messages': [
277+
{'role': 'user', 'parts': [{'type': 'text', 'content': 'input'}]},
278+
{'role': 'assistant', 'parts': [{'type': 'text', 'content': 'hello world'}]},
279+
],
280+
'logfire.json_schema': {
281+
'type': 'object',
282+
'properties': {
283+
'pydantic_ai.all_messages': {'type': 'array'},
284+
'final_result': {'type': 'object'},
285+
},
286+
},
235287
},
236288
},
237289
]
@@ -273,7 +325,7 @@ def test_all_failed_instrumented(capfire: CaptureLogfire) -> None:
273325
assert exceptions[0].status_code == 500
274326
assert exceptions[0].model_name == 'test-function-model'
275327
assert exceptions[0].body == {'error': 'test error'}
276-
assert add_missing_response_model(capfire.exporter.exported_spans_as_dict()) == snapshot(
328+
assert add_missing_response_model(capfire.exporter.exported_spans_as_dict(parse_json_attributes=True)) == snapshot(
277329
[
278330
{
279331
'name': 'chat fallback:function:failure_response:,function:failure_response:',
@@ -285,8 +337,18 @@ def test_all_failed_instrumented(capfire: CaptureLogfire) -> None:
285337
'gen_ai.operation.name': 'chat',
286338
'gen_ai.system': 'fallback:function,function',
287339
'gen_ai.request.model': 'fallback:function:failure_response:,function:failure_response:',
288-
'model_request_parameters': '{"function_tools": [], "builtin_tools": [], "output_mode": "text", "output_object": null, "output_tools": [], "allow_text_output": true}',
289-
'logfire.json_schema': '{"type": "object", "properties": {"model_request_parameters": {"type": "object"}}}',
340+
'model_request_parameters': {
341+
'function_tools': [],
342+
'builtin_tools': [],
343+
'output_mode': 'text',
344+
'output_object': None,
345+
'output_tools': [],
346+
'allow_text_output': True,
347+
},
348+
'logfire.json_schema': {
349+
'type': 'object',
350+
'properties': {'model_request_parameters': {'type': 'object'}},
351+
},
290352
'logfire.span_type': 'span',
291353
'logfire.msg': 'chat fallback:function:failure_response:,function:failure_response:',
292354
'logfire.level_num': 17,
@@ -316,8 +378,14 @@ def test_all_failed_instrumented(capfire: CaptureLogfire) -> None:
316378
'agent_name': 'agent',
317379
'logfire.msg': 'agent run',
318380
'logfire.span_type': 'span',
319-
'all_messages_events': '[{"content": "hello", "role": "user", "gen_ai.message.index": 0, "event.name": "gen_ai.user.message"}]',
320-
'logfire.json_schema': '{"type": "object", "properties": {"all_messages_events": {"type": "array"}, "final_result": {"type": "object"}}}',
381+
'pydantic_ai.all_messages': [{'role': 'user', 'parts': [{'type': 'text', 'content': 'hello'}]}],
382+
'logfire.json_schema': {
383+
'type': 'object',
384+
'properties': {
385+
'pydantic_ai.all_messages': {'type': 'array'},
386+
'final_result': {'type': 'object'},
387+
},
388+
},
321389
'logfire.level_num': 17,
322390
},
323391
'events': [

0 commit comments

Comments
 (0)