lmnr 0.6.19__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 (42) 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/client/asynchronous/async_client.py +13 -0
  27. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -0
  28. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  29. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  30. lmnr/sdk/client/synchronous/resources/__init__.py +2 -1
  31. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  32. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  33. lmnr/sdk/client/synchronous/sync_client.py +14 -0
  34. lmnr/sdk/decorators.py +39 -4
  35. lmnr/sdk/evaluations.py +23 -9
  36. lmnr/sdk/laminar.py +75 -48
  37. lmnr/sdk/utils.py +23 -0
  38. lmnr/version.py +1 -1
  39. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/METADATA +8 -7
  40. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/RECORD +42 -25
  41. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/WHEEL +1 -1
  42. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  from functools import wraps
2
- import json
3
2
  import logging
4
3
  import pydantic
4
+ import orjson
5
5
  import types
6
- from typing import Any, Literal
6
+ from typing import Any, AsyncGenerator, Callable, Generator, Literal
7
7
 
8
8
  from opentelemetry import trace
9
9
  from opentelemetry import context as context_api
@@ -19,35 +19,150 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
19
19
  SPAN_TYPE,
20
20
  )
21
21
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
22
- from lmnr.opentelemetry_lib.utils.json_encoder import JSONEncoder
22
+ from lmnr.sdk.log import get_default_logger
23
23
 
24
+ logger = get_default_logger(__name__)
24
25
 
25
- class CustomJSONEncoder(JSONEncoder):
26
- def default(self, o: Any) -> Any:
27
- if isinstance(o, pydantic.BaseModel):
28
- return o.model_dump_json()
29
- try:
30
- return super().default(o)
31
- except TypeError:
32
- return str(o) # Fallback to string representation for unsupported types
26
+ DEFAULT_PLACEHOLDER = {}
27
+
28
+
29
+ def default_json(o):
30
+ if isinstance(o, pydantic.BaseModel):
31
+ return o.model_dump()
32
+
33
+ # Handle various sequence types, but not strings or bytes
34
+ if isinstance(o, (list, tuple, set, frozenset)):
35
+ return list(o)
36
+
37
+ try:
38
+ return str(o)
39
+ except Exception:
40
+ pass
41
+ return DEFAULT_PLACEHOLDER
33
42
 
34
43
 
35
44
  def json_dumps(data: dict) -> str:
36
45
  try:
37
- return json.dumps(data, cls=CustomJSONEncoder)
46
+ return orjson.dumps(
47
+ data,
48
+ default=default_json,
49
+ option=orjson.OPT_SERIALIZE_DATACLASS
50
+ | orjson.OPT_SERIALIZE_UUID
51
+ | orjson.OPT_UTC_Z
52
+ | orjson.OPT_NON_STR_KEYS,
53
+ ).decode("utf-8")
38
54
  except Exception:
39
55
  # Log the exception and return a placeholder if serialization completely fails
40
56
  logging.warning("Failed to serialize data to JSON, type: %s", type(data))
41
57
  return "{}" # Return an empty JSON object as a fallback
42
58
 
43
59
 
44
- def entity_method(
60
+ def _setup_span(
61
+ span_name: str, span_type: str, association_properties: dict[str, Any] | None
62
+ ):
63
+ """Set up a span with the given name, type, and association properties."""
64
+ with get_tracer() as tracer:
65
+ span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
66
+
67
+ if association_properties is not None:
68
+ for key, value in association_properties.items():
69
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
70
+
71
+ return span
72
+
73
+
74
+ def _process_input(
75
+ span: Span,
76
+ fn: Callable,
77
+ args: tuple,
78
+ kwargs: dict,
79
+ ignore_input: bool,
80
+ ignore_inputs: list[str] | None,
81
+ input_formatter: Callable[..., str] | None,
82
+ ):
83
+ """Process and set input attributes on the span."""
84
+ if ignore_input:
85
+ return
86
+
87
+ try:
88
+ if input_formatter is not None:
89
+ inp = input_formatter(*args, **kwargs)
90
+ if not isinstance(inp, str):
91
+ inp = json_dumps(inp)
92
+ else:
93
+ inp = json_dumps(
94
+ get_input_from_func_args(
95
+ fn,
96
+ is_method=is_method(fn),
97
+ func_args=args,
98
+ func_kwargs=kwargs,
99
+ ignore_inputs=ignore_inputs,
100
+ )
101
+ )
102
+
103
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
104
+ span.set_attribute(SPAN_INPUT, "Laminar: input too large to record")
105
+ else:
106
+ span.set_attribute(SPAN_INPUT, inp)
107
+ except Exception:
108
+ msg = "Failed to process input, ignoring"
109
+ if input_formatter is not None:
110
+ # Only warn the user if they provided an input formatter
111
+ # because it's their responsibility to make sure it works.
112
+ logger.warning(msg, exc_info=True)
113
+ else:
114
+ logger.debug(msg, exc_info=True)
115
+ pass
116
+
117
+
118
+ def _process_output(
119
+ span: Span,
120
+ result: Any,
121
+ ignore_output: bool,
122
+ output_formatter: Callable[..., str] | None,
123
+ ):
124
+ """Process and set output attributes on the span."""
125
+ if ignore_output:
126
+ return
127
+
128
+ try:
129
+ if output_formatter is not None:
130
+ output = output_formatter(result)
131
+ if not isinstance(output, str):
132
+ output = json_dumps(output)
133
+ else:
134
+ output = json_dumps(result)
135
+
136
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
137
+ span.set_attribute(SPAN_OUTPUT, "Laminar: output too large to record")
138
+ else:
139
+ span.set_attribute(SPAN_OUTPUT, output)
140
+ except Exception:
141
+ msg = "Failed to process output, ignoring"
142
+ if output_formatter is not None:
143
+ # Only warn the user if they provided an output formatter
144
+ # because it's their responsibility to make sure it works.
145
+ logger.warning(msg, exc_info=True)
146
+ else:
147
+ logger.debug(msg, exc_info=True)
148
+ pass
149
+
150
+
151
+ def _cleanup_span(span: Span, ctx_token):
152
+ """Clean up span and context."""
153
+ span.end()
154
+ context_api.detach(ctx_token)
155
+
156
+
157
+ def observe_base(
45
158
  name: str | None = None,
46
159
  ignore_input: bool = False,
47
160
  ignore_inputs: list[str] | None = None,
48
161
  ignore_output: bool = False,
49
162
  span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
50
163
  association_properties: dict[str, Any] | None = None,
164
+ input_formatter: Callable[..., str] | None = None,
165
+ output_formatter: Callable[..., str] | None = None,
51
166
  ):
52
167
  def decorate(fn):
53
168
  @wraps(fn)
@@ -57,70 +172,37 @@ def entity_method(
57
172
 
58
173
  span_name = name or fn.__name__
59
174
 
60
- with get_tracer() as tracer:
61
- span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
62
- if association_properties is not None:
63
- for key, value in association_properties.items():
64
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
65
-
66
- ctx = trace.set_span_in_context(span, context_api.get_current())
67
- ctx_token = context_api.attach(ctx)
68
-
69
- try:
70
- if not ignore_input:
71
- inp = json_dumps(
72
- get_input_from_func_args(
73
- fn,
74
- is_method=is_method(fn),
75
- func_args=args,
76
- func_kwargs=kwargs,
77
- ignore_inputs=ignore_inputs,
78
- )
79
- )
80
- if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
81
- span.set_attribute(
82
- SPAN_INPUT, "Laminar: input too large to record"
83
- )
84
- else:
85
- span.set_attribute(SPAN_INPUT, inp)
86
- except TypeError:
87
- pass
88
-
89
- try:
90
- res = fn(*args, **kwargs)
91
- except Exception as e:
92
- _process_exception(span, e)
93
- span.end()
94
- raise e
95
-
96
- # span will be ended in the generator
97
- if isinstance(res, types.GeneratorType):
98
- return _handle_generator(span, ctx_token, res)
99
- if isinstance(res, types.AsyncGeneratorType):
100
- # async def foo() -> AsyncGenerator[int, None]:
101
- # is not considered async in a classical sense in Python,
102
- # so we handle this inside the sync wrapper.
103
- # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
104
- # Flags are listed from LSB here:
105
- # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
106
- # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
107
- return _ahandle_generator(span, ctx_token, res)
108
-
109
- try:
110
- if not ignore_output:
111
- output = json_dumps(res)
112
- if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
113
- span.set_attribute(
114
- SPAN_OUTPUT, "Laminar: output too large to record"
115
- )
116
- else:
117
- span.set_attribute(SPAN_OUTPUT, output)
118
- except TypeError:
119
- pass
120
-
121
- span.end()
122
- context_api.detach(ctx_token)
123
- return res
175
+ span = _setup_span(span_name, span_type, association_properties)
176
+ ctx = trace.set_span_in_context(span, context_api.get_current())
177
+ ctx_token = context_api.attach(ctx)
178
+
179
+ _process_input(
180
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
181
+ )
182
+
183
+ try:
184
+ res = fn(*args, **kwargs)
185
+ except Exception as e:
186
+ _process_exception(span, e)
187
+ _cleanup_span(span, ctx_token)
188
+ raise e
189
+
190
+ # span will be ended in the generator
191
+ if isinstance(res, types.GeneratorType):
192
+ return _handle_generator(span, ctx_token, res)
193
+ if isinstance(res, types.AsyncGeneratorType):
194
+ # async def foo() -> AsyncGenerator[int, None]:
195
+ # is not considered async in a classical sense in Python,
196
+ # so we handle this inside the sync wrapper.
197
+ # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
198
+ # Flags are listed from LSB here:
199
+ # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
200
+ # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
201
+ return _ahandle_generator(span, ctx_token, res)
202
+
203
+ _process_output(span, res, ignore_output, output_formatter)
204
+ _cleanup_span(span, ctx_token)
205
+ return res
124
206
 
125
207
  return wrap
126
208
 
@@ -128,13 +210,15 @@ def entity_method(
128
210
 
129
211
 
130
212
  # Async Decorators
131
- def aentity_method(
213
+ def async_observe_base(
132
214
  name: str | None = None,
133
215
  ignore_input: bool = False,
134
216
  ignore_inputs: list[str] | None = None,
135
217
  ignore_output: bool = False,
136
218
  span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
137
219
  association_properties: dict[str, Any] | None = None,
220
+ input_formatter: Callable[..., str] | None = None,
221
+ output_formatter: Callable[..., str] | None = None,
138
222
  ):
139
223
  def decorate(fn):
140
224
  @wraps(fn)
@@ -144,71 +228,37 @@ def aentity_method(
144
228
 
145
229
  span_name = name or fn.__name__
146
230
 
147
- with get_tracer() as tracer:
148
- span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
149
- if association_properties is not None:
150
- for key, value in association_properties.items():
151
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
152
-
153
- ctx = trace.set_span_in_context(span, context_api.get_current())
154
- ctx_token = context_api.attach(ctx)
155
-
156
- try:
157
- if not ignore_input:
158
- inp = json_dumps(
159
- get_input_from_func_args(
160
- fn,
161
- is_method=is_method(fn),
162
- func_args=args,
163
- func_kwargs=kwargs,
164
- ignore_inputs=ignore_inputs,
165
- )
166
- )
167
- if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
168
- span.set_attribute(
169
- SPAN_INPUT, "Laminar: input too large to record"
170
- )
171
- else:
172
- span.set_attribute(SPAN_INPUT, inp)
173
- except TypeError:
174
- pass
175
-
176
- try:
177
- res = await fn(*args, **kwargs)
178
- except Exception as e:
179
- _process_exception(span, e)
180
- span.end()
181
- raise e
182
-
183
- # span will be ended in the generator
184
- if isinstance(res, types.AsyncGeneratorType):
185
- # probably unreachable, read the comment in the similar
186
- # part of the sync wrapper.
187
- return await _ahandle_generator(span, ctx_token, res)
188
-
189
- try:
190
- if not ignore_output:
191
- output = json_dumps(res)
192
- if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
193
- span.set_attribute(
194
- SPAN_OUTPUT, "Laminar: output too large to record"
195
- )
196
- else:
197
- span.set_attribute(SPAN_OUTPUT, output)
198
- except TypeError:
199
- pass
200
-
201
- span.end()
202
- context_api.detach(ctx_token)
203
-
204
- return res
231
+ span = _setup_span(span_name, span_type, association_properties)
232
+ ctx = trace.set_span_in_context(span, context_api.get_current())
233
+ ctx_token = context_api.attach(ctx)
234
+
235
+ _process_input(
236
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
237
+ )
238
+
239
+ try:
240
+ res = await fn(*args, **kwargs)
241
+ except Exception as e:
242
+ _process_exception(span, e)
243
+ _cleanup_span(span, ctx_token)
244
+ raise e
245
+
246
+ # span will be ended in the generator
247
+ if isinstance(res, types.AsyncGeneratorType):
248
+ # probably unreachable, read the comment in the similar
249
+ # part of the sync wrapper.
250
+ return await _ahandle_generator(span, ctx_token, res)
251
+
252
+ _process_output(span, res, ignore_output, output_formatter)
253
+ _cleanup_span(span, ctx_token)
254
+ return res
205
255
 
206
256
  return wrap
207
257
 
208
258
  return decorate
209
259
 
210
260
 
211
- def _handle_generator(span, ctx_token, res):
261
+ def _handle_generator(span: Span, ctx_token, res: Generator[Any, Any, Any]):
212
262
  yield from res
213
263
 
214
264
  span.end()
@@ -216,7 +266,7 @@ def _handle_generator(span, ctx_token, res):
216
266
  context_api.detach(ctx_token)
217
267
 
218
268
 
219
- async def _ahandle_generator(span, ctx_token, res):
269
+ async def _ahandle_generator(span: Span, ctx_token, res: AsyncGenerator[Any, Any]):
220
270
  # async with contextlib.aclosing(res) as closing_gen:
221
271
  async for part in res:
222
272
  yield part