lmnr 0.6.20__py3-none-any.whl → 0.6.21__py3-none-any.whl

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.
Files changed (33) hide show
  1. lmnr/opentelemetry_lib/decorators/__init__.py +188 -138
  2. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +674 -0
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +256 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +485 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +3 -3
  18. lmnr/opentelemetry_lib/tracing/__init__.py +1 -1
  19. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
  20. lmnr/opentelemetry_lib/tracing/processor.py +1 -1
  21. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  22. lmnr/sdk/browser/browser_use_otel.py +4 -2
  23. lmnr/sdk/browser/patchright_otel.py +0 -26
  24. lmnr/sdk/browser/playwright_otel.py +51 -78
  25. lmnr/sdk/browser/pw_utils.py +359 -114
  26. lmnr/sdk/decorators.py +39 -4
  27. lmnr/sdk/evaluations.py +23 -9
  28. lmnr/sdk/laminar.py +75 -48
  29. lmnr/version.py +1 -1
  30. {lmnr-0.6.20.dist-info → lmnr-0.6.21.dist-info}/METADATA +8 -7
  31. {lmnr-0.6.20.dist-info → lmnr-0.6.21.dist-info}/RECORD +33 -18
  32. {lmnr-0.6.20.dist-info → lmnr-0.6.21.dist-info}/WHEEL +1 -1
  33. {lmnr-0.6.20.dist-info → lmnr-0.6.21.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,295 @@
1
+ import logging
2
+ import time
3
+ from typing import Optional
4
+
5
+ from opentelemetry._events import EventLogger
6
+ from .config import Config
7
+ from .event_emitter import (
8
+ emit_streaming_response_events,
9
+ )
10
+ from .span_utils import (
11
+ set_streaming_response_attributes,
12
+ )
13
+ from .utils import (
14
+ count_prompt_tokens_from_request,
15
+ dont_throw,
16
+ error_metrics_attributes,
17
+ set_span_attribute,
18
+ shared_metrics_attributes,
19
+ should_emit_events,
20
+ )
21
+ from opentelemetry.metrics import Counter, Histogram
22
+ from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
23
+ GEN_AI_RESPONSE_ID,
24
+ )
25
+ from opentelemetry.semconv_ai import SpanAttributes
26
+ from opentelemetry.trace.status import Status, StatusCode
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dont_throw
32
+ def _process_response_item(item, complete_response):
33
+ if item.type == "message_start":
34
+ complete_response["model"] = item.message.model
35
+ complete_response["usage"] = dict(item.message.usage)
36
+ complete_response["id"] = item.message.id
37
+ elif item.type == "content_block_start":
38
+ index = item.index
39
+ if len(complete_response.get("events")) <= index:
40
+ complete_response["events"].append(
41
+ {"index": index, "text": "", "type": item.content_block.type}
42
+ )
43
+ elif item.type == "content_block_delta" and item.delta.type in [
44
+ "thinking_delta",
45
+ "text_delta",
46
+ ]:
47
+ index = item.index
48
+ if item.delta.type == "thinking_delta":
49
+ complete_response["events"][index]["text"] += item.delta.thinking
50
+ elif item.delta.type == "text_delta":
51
+ complete_response["events"][index]["text"] += item.delta.text
52
+ elif item.type == "message_delta":
53
+ for event in complete_response.get("events", []):
54
+ event["finish_reason"] = item.delta.stop_reason
55
+ if item.usage:
56
+ if "usage" in complete_response:
57
+ item_output_tokens = dict(item.usage).get("output_tokens", 0)
58
+ existing_output_tokens = complete_response["usage"].get(
59
+ "output_tokens", 0
60
+ )
61
+ complete_response["usage"]["output_tokens"] = (
62
+ item_output_tokens + existing_output_tokens
63
+ )
64
+ else:
65
+ complete_response["usage"] = dict(item.usage)
66
+
67
+
68
+ def _set_token_usage(
69
+ span,
70
+ complete_response,
71
+ prompt_tokens,
72
+ completion_tokens,
73
+ metric_attributes: dict = {},
74
+ token_histogram: Histogram = None,
75
+ choice_counter: Counter = None,
76
+ ):
77
+ cache_read_tokens = (
78
+ complete_response.get("usage", {}).get("cache_read_input_tokens", 0) or 0
79
+ )
80
+ cache_creation_tokens = (
81
+ complete_response.get("usage", {}).get("cache_creation_input_tokens", 0) or 0
82
+ )
83
+
84
+ input_tokens = prompt_tokens + cache_read_tokens + cache_creation_tokens
85
+ total_tokens = input_tokens + completion_tokens
86
+
87
+ set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, input_tokens)
88
+ set_span_attribute(
89
+ span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
90
+ )
91
+ set_span_attribute(span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, total_tokens)
92
+
93
+ set_span_attribute(
94
+ span, SpanAttributes.LLM_RESPONSE_MODEL, complete_response.get("model")
95
+ )
96
+ set_span_attribute(
97
+ span, SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS, cache_read_tokens
98
+ )
99
+ set_span_attribute(
100
+ span,
101
+ SpanAttributes.LLM_USAGE_CACHE_CREATION_INPUT_TOKENS,
102
+ cache_creation_tokens,
103
+ )
104
+
105
+ if token_histogram and type(input_tokens) is int and input_tokens >= 0:
106
+ token_histogram.record(
107
+ input_tokens,
108
+ attributes={
109
+ **metric_attributes,
110
+ SpanAttributes.LLM_TOKEN_TYPE: "input",
111
+ },
112
+ )
113
+
114
+ if token_histogram and type(completion_tokens) is int and completion_tokens >= 0:
115
+ token_histogram.record(
116
+ completion_tokens,
117
+ attributes={
118
+ **metric_attributes,
119
+ SpanAttributes.LLM_TOKEN_TYPE: "output",
120
+ },
121
+ )
122
+
123
+ if type(complete_response.get("events")) is list and choice_counter:
124
+ for event in complete_response.get("events"):
125
+ choice_counter.add(
126
+ 1,
127
+ attributes={
128
+ **metric_attributes,
129
+ SpanAttributes.LLM_RESPONSE_FINISH_REASON: event.get(
130
+ "finish_reason"
131
+ ),
132
+ },
133
+ )
134
+
135
+
136
+ def _handle_streaming_response(span, event_logger, complete_response):
137
+ if should_emit_events() and event_logger:
138
+ emit_streaming_response_events(event_logger, complete_response)
139
+ else:
140
+ if not span.is_recording():
141
+ return
142
+ set_streaming_response_attributes(span, complete_response.get("events"))
143
+
144
+
145
+ @dont_throw
146
+ def build_from_streaming_response(
147
+ span,
148
+ response,
149
+ instance,
150
+ start_time,
151
+ token_histogram: Histogram = None,
152
+ choice_counter: Counter = None,
153
+ duration_histogram: Histogram = None,
154
+ exception_counter: Counter = None,
155
+ event_logger: Optional[EventLogger] = None,
156
+ kwargs: dict = {},
157
+ ):
158
+ complete_response = {"events": [], "model": "", "usage": {}, "id": ""}
159
+ for item in response:
160
+ try:
161
+ yield item
162
+ except Exception as e:
163
+ attributes = error_metrics_attributes(e)
164
+ if exception_counter:
165
+ exception_counter.add(1, attributes=attributes)
166
+ raise e
167
+ _process_response_item(item, complete_response)
168
+
169
+ metric_attributes = shared_metrics_attributes(complete_response)
170
+ set_span_attribute(span, GEN_AI_RESPONSE_ID, complete_response.get("id"))
171
+ if duration_histogram:
172
+ duration = time.time() - start_time
173
+ duration_histogram.record(
174
+ duration,
175
+ attributes=metric_attributes,
176
+ )
177
+
178
+ # calculate token usage
179
+ if Config.enrich_token_usage:
180
+ try:
181
+ completion_tokens = -1
182
+ # prompt_usage
183
+ if usage := complete_response.get("usage"):
184
+ prompt_tokens = usage.get("input_tokens", 0) or 0
185
+ else:
186
+ prompt_tokens = count_prompt_tokens_from_request(instance, kwargs)
187
+
188
+ # completion_usage
189
+ if usage := complete_response.get("usage"):
190
+ completion_tokens = usage.get("output_tokens", 0) or 0
191
+ else:
192
+ completion_content = ""
193
+ if complete_response.get("events"):
194
+ model_name = complete_response.get("model") or None
195
+ for event in complete_response.get("events"):
196
+ if event.get("text"):
197
+ completion_content += event.get("text")
198
+
199
+ if model_name and hasattr(instance, "count_tokens"):
200
+ completion_tokens = instance.count_tokens(completion_content)
201
+
202
+ _set_token_usage(
203
+ span,
204
+ complete_response,
205
+ prompt_tokens,
206
+ completion_tokens,
207
+ metric_attributes,
208
+ token_histogram,
209
+ choice_counter,
210
+ )
211
+ except Exception as e:
212
+ logger.warning("Failed to set token usage, error: %s", e)
213
+
214
+ _handle_streaming_response(span, event_logger, complete_response)
215
+
216
+ if span.is_recording():
217
+ span.set_status(Status(StatusCode.OK))
218
+ span.end()
219
+
220
+
221
+ @dont_throw
222
+ async def abuild_from_streaming_response(
223
+ span,
224
+ response,
225
+ instance,
226
+ start_time,
227
+ token_histogram: Histogram = None,
228
+ choice_counter: Counter = None,
229
+ duration_histogram: Histogram = None,
230
+ exception_counter: Counter = None,
231
+ event_logger: Optional[EventLogger] = None,
232
+ kwargs: dict = {},
233
+ ):
234
+ complete_response = {"events": [], "model": "", "usage": {}, "id": ""}
235
+ async for item in response:
236
+ try:
237
+ yield item
238
+ except Exception as e:
239
+ attributes = error_metrics_attributes(e)
240
+ if exception_counter:
241
+ exception_counter.add(1, attributes=attributes)
242
+ raise e
243
+ _process_response_item(item, complete_response)
244
+
245
+ set_span_attribute(span, GEN_AI_RESPONSE_ID, complete_response.get("id"))
246
+
247
+ metric_attributes = shared_metrics_attributes(complete_response)
248
+
249
+ if duration_histogram:
250
+ duration = time.time() - start_time
251
+ duration_histogram.record(
252
+ duration,
253
+ attributes=metric_attributes,
254
+ )
255
+
256
+ # calculate token usage
257
+ if Config.enrich_token_usage:
258
+ try:
259
+ # prompt_usage
260
+ if usage := complete_response.get("usage"):
261
+ prompt_tokens = usage.get("input_tokens", 0)
262
+ else:
263
+ prompt_tokens = count_prompt_tokens_from_request(instance, kwargs)
264
+
265
+ # completion_usage
266
+ if usage := complete_response.get("usage"):
267
+ completion_tokens = usage.get("output_tokens", 0)
268
+ else:
269
+ completion_content = ""
270
+ if complete_response.get("events"):
271
+ model_name = complete_response.get("model") or None
272
+ for event in complete_response.get("events"):
273
+ if event.get("text"):
274
+ completion_content += event.get("text")
275
+
276
+ if model_name and hasattr(instance, "count_tokens"):
277
+ completion_tokens = instance.count_tokens(completion_content)
278
+
279
+ _set_token_usage(
280
+ span,
281
+ complete_response,
282
+ prompt_tokens,
283
+ completion_tokens,
284
+ metric_attributes,
285
+ token_histogram,
286
+ choice_counter,
287
+ )
288
+ except Exception as e:
289
+ logger.warning("Failed to set token usage, error: %s", str(e))
290
+
291
+ _handle_streaming_response(span, event_logger, complete_response)
292
+
293
+ if span.is_recording():
294
+ span.set_status(Status(StatusCode.OK))
295
+ span.end()
@@ -0,0 +1,179 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import threading
6
+ import traceback
7
+ from importlib.metadata import version
8
+
9
+ from opentelemetry import context as context_api
10
+ from .config import Config
11
+ from opentelemetry.semconv_ai import SpanAttributes
12
+
13
+ GEN_AI_SYSTEM = "gen_ai.system"
14
+ GEN_AI_SYSTEM_ANTHROPIC = "anthropic"
15
+ _PYDANTIC_VERSION = version("pydantic")
16
+
17
+ LMNR_TRACE_CONTENT = "LMNR_TRACE_CONTENT"
18
+
19
+
20
+ def set_span_attribute(span, name, value):
21
+ if value is not None:
22
+ if value != "":
23
+ span.set_attribute(name, value)
24
+ return
25
+
26
+
27
+ def should_send_prompts():
28
+ return (
29
+ os.getenv(LMNR_TRACE_CONTENT) or "true"
30
+ ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
31
+
32
+
33
+ def dont_throw(func):
34
+ """
35
+ A decorator that wraps the passed in function and logs exceptions instead of throwing them.
36
+ Works for both synchronous and asynchronous functions.
37
+ """
38
+ logger = logging.getLogger(func.__module__)
39
+
40
+ async def async_wrapper(*args, **kwargs):
41
+ try:
42
+ return await func(*args, **kwargs)
43
+ except Exception as e:
44
+ _handle_exception(e, func, logger)
45
+
46
+ def sync_wrapper(*args, **kwargs):
47
+ try:
48
+ return func(*args, **kwargs)
49
+ except Exception as e:
50
+ _handle_exception(e, func, logger)
51
+
52
+ def _handle_exception(e, func, logger):
53
+ logger.debug(
54
+ "OpenLLMetry failed to trace in %s, error: %s",
55
+ func.__name__,
56
+ traceback.format_exc(),
57
+ )
58
+ if Config.exception_logger:
59
+ Config.exception_logger(e)
60
+
61
+ return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
62
+
63
+
64
+ @dont_throw
65
+ def shared_metrics_attributes(response):
66
+ if not isinstance(response, dict):
67
+ response = response.__dict__
68
+
69
+ common_attributes = Config.get_common_metrics_attributes()
70
+
71
+ return {
72
+ **common_attributes,
73
+ GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
74
+ SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
75
+ }
76
+
77
+
78
+ @dont_throw
79
+ def error_metrics_attributes(exception):
80
+ return {
81
+ GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
82
+ "error.type": exception.__class__.__name__,
83
+ }
84
+
85
+
86
+ @dont_throw
87
+ def count_prompt_tokens_from_request(anthropic, request):
88
+ prompt_tokens = 0
89
+ if hasattr(anthropic, "count_tokens"):
90
+ if request.get("prompt"):
91
+ prompt_tokens = anthropic.count_tokens(request.get("prompt"))
92
+ elif messages := request.get("messages"):
93
+ prompt_tokens = 0
94
+ for m in messages:
95
+ content = m.get("content")
96
+ if isinstance(content, str):
97
+ prompt_tokens += anthropic.count_tokens(content)
98
+ elif isinstance(content, list):
99
+ for item in content:
100
+ # TODO: handle image and tool tokens
101
+ if isinstance(item, dict) and item.get("type") == "text":
102
+ prompt_tokens += anthropic.count_tokens(
103
+ item.get("text", "")
104
+ )
105
+ return prompt_tokens
106
+
107
+
108
+ @dont_throw
109
+ async def acount_prompt_tokens_from_request(anthropic, request):
110
+ prompt_tokens = 0
111
+ if hasattr(anthropic, "count_tokens"):
112
+ if request.get("prompt"):
113
+ prompt_tokens = await anthropic.count_tokens(request.get("prompt"))
114
+ elif messages := request.get("messages"):
115
+ prompt_tokens = 0
116
+ for m in messages:
117
+ content = m.get("content")
118
+ if isinstance(content, str):
119
+ prompt_tokens += await anthropic.count_tokens(content)
120
+ elif isinstance(content, list):
121
+ for item in content:
122
+ # TODO: handle image and tool tokens
123
+ if isinstance(item, dict) and item.get("type") == "text":
124
+ prompt_tokens += await anthropic.count_tokens(
125
+ item.get("text", "")
126
+ )
127
+ return prompt_tokens
128
+
129
+
130
+ def run_async(method):
131
+ try:
132
+ loop = asyncio.get_running_loop()
133
+ except RuntimeError:
134
+ loop = None
135
+
136
+ if loop and loop.is_running():
137
+ thread = threading.Thread(target=lambda: asyncio.run(method))
138
+ thread.start()
139
+ thread.join()
140
+ else:
141
+ asyncio.run(method)
142
+
143
+
144
+ def should_emit_events() -> bool:
145
+ """
146
+ Checks if the instrumentation isn't using the legacy attributes
147
+ and if the event logger is not None.
148
+ """
149
+ return not Config.use_legacy_attributes
150
+
151
+
152
+ class JSONEncoder(json.JSONEncoder):
153
+ def default(self, o):
154
+ if hasattr(o, "to_json"):
155
+ return o.to_json()
156
+
157
+ if hasattr(o, "model_dump_json"):
158
+ return o.model_dump_json()
159
+
160
+ try:
161
+ return str(o)
162
+ except Exception:
163
+ logger = logging.getLogger(__name__)
164
+ logger.debug("Failed to serialize object of type: %s", type(o).__name__)
165
+ return ""
166
+
167
+
168
+ def model_as_dict(model):
169
+ if isinstance(model, dict):
170
+ return model
171
+ if _PYDANTIC_VERSION < "2.0.0" and hasattr(model, "dict"):
172
+ return model.dict()
173
+ if hasattr(model, "model_dump"):
174
+ return model.model_dump()
175
+ else:
176
+ try:
177
+ return dict(model)
178
+ except Exception:
179
+ return model
@@ -0,0 +1 @@
1
+ __version__ = "0.41.0"