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.
- lmnr/opentelemetry_lib/decorators/__init__.py +188 -138
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +674 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +256 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +485 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +3 -3
- lmnr/opentelemetry_lib/tracing/__init__.py +1 -1
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
- lmnr/opentelemetry_lib/tracing/processor.py +1 -1
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/sdk/browser/browser_use_otel.py +4 -2
- lmnr/sdk/browser/patchright_otel.py +0 -26
- lmnr/sdk/browser/playwright_otel.py +51 -78
- lmnr/sdk/browser/pw_utils.py +359 -114
- lmnr/sdk/client/asynchronous/async_client.py +13 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -1
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +14 -0
- lmnr/sdk/decorators.py +39 -4
- lmnr/sdk/evaluations.py +23 -9
- lmnr/sdk/laminar.py +75 -48
- lmnr/sdk/utils.py +23 -0
- lmnr/version.py +1 -1
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/METADATA +8 -7
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/RECORD +42 -25
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/WHEEL +1 -1
- {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.
|
22
|
+
from lmnr.sdk.log import get_default_logger
|
23
23
|
|
24
|
+
logger = get_default_logger(__name__)
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
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
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|