lmnr 0.6.20__py3-none-any.whl → 0.7.0__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 (46) hide show
  1. lmnr/__init__.py +0 -4
  2. lmnr/opentelemetry_lib/decorators/__init__.py +211 -151
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +678 -0
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +256 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +4 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +3 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +3 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +3 -3
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +3 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +7 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
  26. lmnr/opentelemetry_lib/tracing/__init__.py +90 -2
  27. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
  28. lmnr/opentelemetry_lib/tracing/context.py +109 -0
  29. lmnr/opentelemetry_lib/tracing/processor.py +6 -7
  30. lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
  31. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  32. lmnr/sdk/browser/browser_use_otel.py +9 -7
  33. lmnr/sdk/browser/patchright_otel.py +14 -26
  34. lmnr/sdk/browser/playwright_otel.py +72 -73
  35. lmnr/sdk/browser/pw_utils.py +436 -119
  36. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  37. lmnr/sdk/decorators.py +39 -4
  38. lmnr/sdk/evaluations.py +23 -9
  39. lmnr/sdk/laminar.py +181 -209
  40. lmnr/sdk/types.py +0 -6
  41. lmnr/version.py +1 -1
  42. {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/METADATA +10 -8
  43. {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/RECORD +45 -29
  44. {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/WHEEL +1 -1
  45. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  46. {lmnr-0.6.20.dist-info → lmnr-0.7.0.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py CHANGED
@@ -9,7 +9,6 @@ from .sdk.types import (
9
9
  HumanEvaluator,
10
10
  RunAgentResponseChunk,
11
11
  StepChunkContent,
12
- TracingLevel,
13
12
  )
14
13
  from .sdk.decorators import observe
15
14
  from .sdk.types import LaminarSpanContext
@@ -18,7 +17,6 @@ from .opentelemetry_lib.tracing.attributes import Attributes
18
17
  from .opentelemetry_lib.tracing.instruments import Instruments
19
18
  from .opentelemetry_lib.tracing.processor import LaminarSpanProcessor
20
19
  from .opentelemetry_lib.tracing.tracer import get_laminar_tracer_provider, get_tracer
21
- from opentelemetry.trace import use_span
22
20
 
23
21
  __all__ = [
24
22
  "AgentOutput",
@@ -36,10 +34,8 @@ __all__ = [
36
34
  "LaminarSpanProcessor",
37
35
  "RunAgentResponseChunk",
38
36
  "StepChunkContent",
39
- "TracingLevel",
40
37
  "get_laminar_tracer_provider",
41
38
  "get_tracer",
42
39
  "evaluate",
43
40
  "observe",
44
- "use_span",
45
41
  ]
@@ -1,17 +1,16 @@
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
- from opentelemetry import trace
9
8
  from opentelemetry import context as context_api
10
9
  from opentelemetry.trace import Span
11
10
 
12
11
  from lmnr.sdk.utils import get_input_from_func_args, is_method
13
12
  from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
14
- from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
13
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
15
14
  from lmnr.opentelemetry_lib.tracing.attributes import (
16
15
  ASSOCIATION_PROPERTIES,
17
16
  SPAN_INPUT,
@@ -19,35 +18,156 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
19
18
  SPAN_TYPE,
20
19
  )
21
20
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
22
- from lmnr.opentelemetry_lib.utils.json_encoder import JSONEncoder
21
+ from lmnr.sdk.log import get_default_logger
23
22
 
23
+ logger = get_default_logger(__name__)
24
24
 
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
25
+ DEFAULT_PLACEHOLDER = {}
26
+
27
+
28
+ def default_json(o):
29
+ if isinstance(o, pydantic.BaseModel):
30
+ return o.model_dump()
31
+
32
+ # Handle various sequence types, but not strings or bytes
33
+ if isinstance(o, (list, tuple, set, frozenset)):
34
+ return list(o)
35
+
36
+ try:
37
+ return str(o)
38
+ except Exception:
39
+ logger.debug("Failed to serialize data to JSON, inner type: %s", type(o))
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_with_context() as (tracer, isolated_context):
65
+ # Create span in isolated context
66
+ span = tracer.start_span(
67
+ span_name,
68
+ context=isolated_context,
69
+ attributes={SPAN_TYPE: span_type},
70
+ )
71
+
72
+ if association_properties is not None:
73
+ for key, value in association_properties.items():
74
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
75
+
76
+ return span
77
+
78
+
79
+ def _process_input(
80
+ span: Span,
81
+ fn: Callable,
82
+ args: tuple,
83
+ kwargs: dict,
84
+ ignore_input: bool,
85
+ ignore_inputs: list[str] | None,
86
+ input_formatter: Callable[..., str] | None,
87
+ ):
88
+ """Process and set input attributes on the span."""
89
+ if ignore_input:
90
+ return
91
+
92
+ try:
93
+ if input_formatter is not None:
94
+ inp = input_formatter(*args, **kwargs)
95
+ if not isinstance(inp, str):
96
+ inp = json_dumps(inp)
97
+ else:
98
+ inp = json_dumps(
99
+ get_input_from_func_args(
100
+ fn,
101
+ is_method=is_method(fn),
102
+ func_args=args,
103
+ func_kwargs=kwargs,
104
+ ignore_inputs=ignore_inputs,
105
+ )
106
+ )
107
+
108
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
109
+ span.set_attribute(SPAN_INPUT, "Laminar: input too large to record")
110
+ else:
111
+ span.set_attribute(SPAN_INPUT, inp)
112
+ except Exception:
113
+ msg = "Failed to process input, ignoring"
114
+ if input_formatter is not None:
115
+ # Only warn the user if they provided an input formatter
116
+ # because it's their responsibility to make sure it works.
117
+ logger.warning(msg, exc_info=True)
118
+ else:
119
+ logger.debug(msg, exc_info=True)
120
+ pass
121
+
122
+
123
+ def _process_output(
124
+ span: Span,
125
+ result: Any,
126
+ ignore_output: bool,
127
+ output_formatter: Callable[..., str] | None,
128
+ ):
129
+ """Process and set output attributes on the span."""
130
+ if ignore_output:
131
+ return
132
+
133
+ try:
134
+ if output_formatter is not None:
135
+ output = output_formatter(result)
136
+ if not isinstance(output, str):
137
+ output = json_dumps(output)
138
+ else:
139
+ output = json_dumps(result)
140
+
141
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
142
+ span.set_attribute(SPAN_OUTPUT, "Laminar: output too large to record")
143
+ else:
144
+ span.set_attribute(SPAN_OUTPUT, output)
145
+ except Exception:
146
+ msg = "Failed to process output, ignoring"
147
+ if output_formatter is not None:
148
+ # Only warn the user if they provided an output formatter
149
+ # because it's their responsibility to make sure it works.
150
+ logger.warning(msg, exc_info=True)
151
+ else:
152
+ logger.debug(msg, exc_info=True)
153
+ pass
154
+
155
+
156
+ def _cleanup_span(span: Span, wrapper: TracerWrapper):
157
+ """Clean up span and context."""
158
+ span.end()
159
+ wrapper.pop_span_context()
160
+
161
+
162
+ def observe_base(
45
163
  name: str | None = None,
46
164
  ignore_input: bool = False,
47
165
  ignore_inputs: list[str] | None = None,
48
166
  ignore_output: bool = False,
49
167
  span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
50
168
  association_properties: dict[str, Any] | None = None,
169
+ input_formatter: Callable[..., str] | None = None,
170
+ output_formatter: Callable[..., str] | None = None,
51
171
  ):
52
172
  def decorate(fn):
53
173
  @wraps(fn)
@@ -56,71 +176,42 @@ def entity_method(
56
176
  return fn(*args, **kwargs)
57
177
 
58
178
  span_name = name or fn.__name__
59
-
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()
179
+ wrapper = TracerWrapper()
180
+
181
+ span = _setup_span(span_name, span_type, association_properties)
182
+ new_context = wrapper.push_span_context(span)
183
+ ctx_token = context_api.attach(new_context)
184
+
185
+ _process_input(
186
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
187
+ )
188
+
189
+ try:
190
+ res = fn(*args, **kwargs)
191
+ except Exception as e:
192
+ _process_exception(span, e)
193
+ _cleanup_span(span, wrapper)
194
+ raise e
195
+ finally:
196
+ # Always restore global context
122
197
  context_api.detach(ctx_token)
123
- return res
198
+
199
+ # span will be ended in the generator
200
+ if isinstance(res, types.GeneratorType):
201
+ return _handle_generator(span, ctx_token, res)
202
+ if isinstance(res, types.AsyncGeneratorType):
203
+ # async def foo() -> AsyncGenerator[int, None]:
204
+ # is not considered async in a classical sense in Python,
205
+ # so we handle this inside the sync wrapper.
206
+ # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
207
+ # Flags are listed from LSB here:
208
+ # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
209
+ # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
210
+ return _ahandle_generator(span, ctx_token, res)
211
+
212
+ _process_output(span, res, ignore_output, output_formatter)
213
+ _cleanup_span(span, wrapper)
214
+ return res
124
215
 
125
216
  return wrap
126
217
 
@@ -128,13 +219,15 @@ def entity_method(
128
219
 
129
220
 
130
221
  # Async Decorators
131
- def aentity_method(
222
+ def async_observe_base(
132
223
  name: str | None = None,
133
224
  ignore_input: bool = False,
134
225
  ignore_inputs: list[str] | None = None,
135
226
  ignore_output: bool = False,
136
227
  span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
137
228
  association_properties: dict[str, Any] | None = None,
229
+ input_formatter: Callable[..., str] | None = None,
230
+ output_formatter: Callable[..., str] | None = None,
138
231
  ):
139
232
  def decorate(fn):
140
233
  @wraps(fn)
@@ -143,87 +236,54 @@ def aentity_method(
143
236
  return await fn(*args, **kwargs)
144
237
 
145
238
  span_name = name or fn.__name__
146
-
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()
239
+ wrapper = TracerWrapper()
240
+
241
+ span = _setup_span(span_name, span_type, association_properties)
242
+ new_context = wrapper.push_span_context(span)
243
+ ctx_token = context_api.attach(new_context)
244
+
245
+ _process_input(
246
+ span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
247
+ )
248
+
249
+ try:
250
+ res = await fn(*args, **kwargs)
251
+ except Exception as e:
252
+ _process_exception(span, e)
253
+ _cleanup_span(span, wrapper)
254
+ raise e
255
+ finally:
256
+ # Always restore global context
202
257
  context_api.detach(ctx_token)
203
258
 
204
- return res
259
+ # span will be ended in the generator
260
+ if isinstance(res, types.AsyncGeneratorType):
261
+ # probably unreachable, read the comment in the similar
262
+ # part of the sync wrapper.
263
+ return await _ahandle_generator(span, ctx_token, res)
264
+
265
+ _process_output(span, res, ignore_output, output_formatter)
266
+ _cleanup_span(span, wrapper)
267
+ return res
205
268
 
206
269
  return wrap
207
270
 
208
271
  return decorate
209
272
 
210
273
 
211
- def _handle_generator(span, ctx_token, res):
212
- yield from res
213
-
214
- span.end()
215
- if ctx_token is not None:
216
- context_api.detach(ctx_token)
217
-
274
+ def _handle_generator(span: Span, wrapper: TracerWrapper, res: Generator):
275
+ try:
276
+ yield from res
277
+ finally:
278
+ _cleanup_span(span, wrapper)
218
279
 
219
- async def _ahandle_generator(span, ctx_token, res):
220
- # async with contextlib.aclosing(res) as closing_gen:
221
- async for part in res:
222
- yield part
223
280
 
224
- span.end()
225
- if ctx_token is not None:
226
- context_api.detach(ctx_token)
281
+ async def _ahandle_generator(span: Span, wrapper: TracerWrapper, res: AsyncGenerator):
282
+ try:
283
+ async for part in res:
284
+ yield part
285
+ finally:
286
+ _cleanup_span(span, wrapper)
227
287
 
228
288
 
229
289
  def _process_exception(span: Span, e: Exception):