lmnr 0.7.1__py3-none-any.whl → 0.7.3__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/__init__.py +3 -0
- lmnr/opentelemetry_lib/__init__.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +85 -6
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +57 -14
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +106 -6
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +12 -1
- lmnr/opentelemetry_lib/tracing/__init__.py +11 -0
- lmnr/sdk/browser/pw_utils.py +281 -61
- lmnr/sdk/decorators.py +2 -1
- lmnr/sdk/laminar.py +9 -1
- lmnr/sdk/types.py +13 -1
- lmnr/version.py +1 -1
- {lmnr-0.7.1.dist-info → lmnr-0.7.3.dist-info}/METADATA +48 -51
- {lmnr-0.7.1.dist-info → lmnr-0.7.3.dist-info}/RECORD +16 -16
- {lmnr-0.7.1.dist-info → lmnr-0.7.3.dist-info}/WHEEL +0 -0
- {lmnr-0.7.1.dist-info → lmnr-0.7.3.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py
CHANGED
@@ -3,6 +3,7 @@ from .sdk.client.asynchronous.async_client import AsyncLaminarClient
|
|
3
3
|
from .sdk.datasets import EvaluationDataset, LaminarDataset
|
4
4
|
from .sdk.evaluations import evaluate
|
5
5
|
from .sdk.laminar import Laminar
|
6
|
+
from .sdk.types import SessionRecordingOptions, MaskInputOptions
|
6
7
|
from .sdk.types import (
|
7
8
|
AgentOutput,
|
8
9
|
FinalOutputChunkContent,
|
@@ -38,4 +39,6 @@ __all__ = [
|
|
38
39
|
"get_tracer",
|
39
40
|
"evaluate",
|
40
41
|
"observe",
|
42
|
+
"SessionRecordingOptions",
|
43
|
+
"MaskInputOptions",
|
41
44
|
]
|
@@ -6,6 +6,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME
|
|
6
6
|
|
7
7
|
from lmnr.opentelemetry_lib.tracing.instruments import Instruments
|
8
8
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
9
|
+
from lmnr.sdk.types import SessionRecordingOptions
|
9
10
|
|
10
11
|
MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 * 10 # 10MB
|
11
12
|
|
@@ -30,6 +31,7 @@ class TracerManager:
|
|
30
31
|
timeout_seconds: int = 30,
|
31
32
|
set_global_tracer_provider: bool = True,
|
32
33
|
otel_logger_level: int = logging.ERROR,
|
34
|
+
session_recording_options: SessionRecordingOptions | None = None,
|
33
35
|
) -> None:
|
34
36
|
enable_content_tracing = True
|
35
37
|
|
@@ -50,6 +52,7 @@ class TracerManager:
|
|
50
52
|
timeout_seconds=timeout_seconds,
|
51
53
|
set_global_tracer_provider=set_global_tracer_provider,
|
52
54
|
otel_logger_level=otel_logger_level,
|
55
|
+
session_recording_options=session_recording_options,
|
53
56
|
)
|
54
57
|
|
55
58
|
@staticmethod
|
@@ -29,6 +29,10 @@ from .utils import (
|
|
29
29
|
shared_metrics_attributes,
|
30
30
|
should_emit_events,
|
31
31
|
)
|
32
|
+
from .streaming import (
|
33
|
+
WrappedAsyncMessageStreamManager,
|
34
|
+
WrappedMessageStreamManager,
|
35
|
+
)
|
32
36
|
from .version import __version__
|
33
37
|
|
34
38
|
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
@@ -52,6 +56,7 @@ logger = logging.getLogger(__name__)
|
|
52
56
|
|
53
57
|
_instruments = ("anthropic >= 0.3.11",)
|
54
58
|
|
59
|
+
|
55
60
|
WRAPPED_METHODS = [
|
56
61
|
{
|
57
62
|
"package": "anthropic.resources.completions",
|
@@ -71,6 +76,15 @@ WRAPPED_METHODS = [
|
|
71
76
|
"method": "stream",
|
72
77
|
"span_name": "anthropic.chat",
|
73
78
|
},
|
79
|
+
# This method is on an async resource, but is meant to be called as
|
80
|
+
# an async context manager (async with), which we don't need to await;
|
81
|
+
# thus, we wrap it with a sync wrapper
|
82
|
+
{
|
83
|
+
"package": "anthropic.resources.messages",
|
84
|
+
"object": "AsyncMessages",
|
85
|
+
"method": "stream",
|
86
|
+
"span_name": "anthropic.chat",
|
87
|
+
},
|
74
88
|
]
|
75
89
|
|
76
90
|
WRAPPED_AMETHODS = [
|
@@ -86,12 +100,6 @@ WRAPPED_AMETHODS = [
|
|
86
100
|
"method": "create",
|
87
101
|
"span_name": "anthropic.chat",
|
88
102
|
},
|
89
|
-
{
|
90
|
-
"package": "anthropic.resources.messages",
|
91
|
-
"object": "AsyncMessages",
|
92
|
-
"method": "stream",
|
93
|
-
"span_name": "anthropic.chat",
|
94
|
-
},
|
95
103
|
]
|
96
104
|
|
97
105
|
|
@@ -99,6 +107,23 @@ def is_streaming_response(response):
|
|
99
107
|
return isinstance(response, Stream) or isinstance(response, AsyncStream)
|
100
108
|
|
101
109
|
|
110
|
+
def is_stream_manager(response):
|
111
|
+
"""Check if response is a MessageStreamManager or AsyncMessageStreamManager"""
|
112
|
+
try:
|
113
|
+
from anthropic.lib.streaming._messages import (
|
114
|
+
MessageStreamManager,
|
115
|
+
AsyncMessageStreamManager,
|
116
|
+
)
|
117
|
+
|
118
|
+
return isinstance(response, (MessageStreamManager, AsyncMessageStreamManager))
|
119
|
+
except ImportError:
|
120
|
+
# Check by class name as fallback
|
121
|
+
return (
|
122
|
+
response.__class__.__name__ == "MessageStreamManager"
|
123
|
+
or response.__class__.__name__ == "AsyncMessageStreamManager"
|
124
|
+
)
|
125
|
+
|
126
|
+
|
102
127
|
@dont_throw
|
103
128
|
async def _aset_token_usage(
|
104
129
|
span,
|
@@ -437,6 +462,33 @@ def _wrap(
|
|
437
462
|
event_logger,
|
438
463
|
kwargs,
|
439
464
|
)
|
465
|
+
elif is_stream_manager(response):
|
466
|
+
if response.__class__.__name__ == "AsyncMessageStreamManager":
|
467
|
+
return WrappedAsyncMessageStreamManager(
|
468
|
+
response,
|
469
|
+
span,
|
470
|
+
instance._client,
|
471
|
+
start_time,
|
472
|
+
token_histogram,
|
473
|
+
choice_counter,
|
474
|
+
duration_histogram,
|
475
|
+
exception_counter,
|
476
|
+
event_logger,
|
477
|
+
kwargs,
|
478
|
+
)
|
479
|
+
else:
|
480
|
+
return WrappedMessageStreamManager(
|
481
|
+
response,
|
482
|
+
span,
|
483
|
+
instance._client,
|
484
|
+
start_time,
|
485
|
+
token_histogram,
|
486
|
+
choice_counter,
|
487
|
+
duration_histogram,
|
488
|
+
exception_counter,
|
489
|
+
event_logger,
|
490
|
+
kwargs,
|
491
|
+
)
|
440
492
|
elif response:
|
441
493
|
try:
|
442
494
|
metric_attributes = shared_metrics_attributes(response)
|
@@ -532,6 +584,33 @@ async def _awrap(
|
|
532
584
|
event_logger,
|
533
585
|
kwargs,
|
534
586
|
)
|
587
|
+
elif is_stream_manager(response):
|
588
|
+
if response.__class__.__name__ == "AsyncMessageStreamManager":
|
589
|
+
return WrappedAsyncMessageStreamManager(
|
590
|
+
response,
|
591
|
+
span,
|
592
|
+
instance._client,
|
593
|
+
start_time,
|
594
|
+
token_histogram,
|
595
|
+
choice_counter,
|
596
|
+
duration_histogram,
|
597
|
+
exception_counter,
|
598
|
+
event_logger,
|
599
|
+
kwargs,
|
600
|
+
)
|
601
|
+
else:
|
602
|
+
return WrappedMessageStreamManager(
|
603
|
+
response,
|
604
|
+
span,
|
605
|
+
instance._client,
|
606
|
+
start_time,
|
607
|
+
token_histogram,
|
608
|
+
choice_counter,
|
609
|
+
duration_histogram,
|
610
|
+
exception_counter,
|
611
|
+
event_logger,
|
612
|
+
kwargs,
|
613
|
+
)
|
535
614
|
elif response:
|
536
615
|
metric_attributes = shared_metrics_attributes(response)
|
537
616
|
|
@@ -113,18 +113,43 @@ async def aset_input_attributes(span, kwargs):
|
|
113
113
|
)
|
114
114
|
for i, message in enumerate(kwargs.get("messages")):
|
115
115
|
prompt_index = i + (1 if has_system_message else 0)
|
116
|
+
content = message.get("content")
|
117
|
+
tool_use_blocks = []
|
118
|
+
other_blocks = []
|
119
|
+
if isinstance(content, list):
|
120
|
+
for block in content:
|
121
|
+
if dict(block).get("type") == "tool_use":
|
122
|
+
tool_use_blocks.append(dict(block))
|
123
|
+
else:
|
124
|
+
other_blocks.append(block)
|
125
|
+
content = other_blocks
|
116
126
|
set_span_attribute(
|
117
127
|
span,
|
118
128
|
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
|
119
|
-
await _dump_content(
|
120
|
-
message_index=i, span=span, content=message.get("content")
|
121
|
-
),
|
129
|
+
await _dump_content(message_index=i, span=span, content=content),
|
122
130
|
)
|
123
131
|
set_span_attribute(
|
124
132
|
span,
|
125
133
|
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
|
126
134
|
message.get("role"),
|
127
135
|
)
|
136
|
+
if tool_use_blocks:
|
137
|
+
for tool_num, tool_use_block in enumerate(tool_use_blocks):
|
138
|
+
set_span_attribute(
|
139
|
+
span,
|
140
|
+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.id",
|
141
|
+
tool_use_block.get("id"),
|
142
|
+
)
|
143
|
+
set_span_attribute(
|
144
|
+
span,
|
145
|
+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.name",
|
146
|
+
tool_use_block.get("name"),
|
147
|
+
)
|
148
|
+
set_span_attribute(
|
149
|
+
span,
|
150
|
+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.arguments",
|
151
|
+
json.dumps(tool_use_block.get("input")),
|
152
|
+
)
|
128
153
|
|
129
154
|
if kwargs.get("tools") is not None:
|
130
155
|
for i, tool in enumerate(kwargs.get("tools")):
|
@@ -160,8 +185,8 @@ def _set_span_completions(span, response):
|
|
160
185
|
content_block_type = content.type
|
161
186
|
# usually, Antrhopic responds with just one text block,
|
162
187
|
# but the API allows for multiple text blocks, so concatenate them
|
163
|
-
if content_block_type == "text":
|
164
|
-
text += content.text
|
188
|
+
if content_block_type == "text" and hasattr(content, "text"):
|
189
|
+
text += content.text or ""
|
165
190
|
elif content_block_type == "thinking":
|
166
191
|
content = dict(content)
|
167
192
|
# override the role to thinking
|
@@ -242,15 +267,33 @@ def set_streaming_response_attributes(span, complete_response_events):
|
|
242
267
|
if not span.is_recording() or not complete_response_events:
|
243
268
|
return
|
244
269
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
270
|
+
index = 0
|
271
|
+
for event in complete_response_events:
|
272
|
+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
|
273
|
+
set_span_attribute(span, f"{prefix}.finish_reason", event.get("finish_reason"))
|
274
|
+
role = "thinking" if event.get("type") == "thinking" else "assistant"
|
275
|
+
# Thinking is added as a separate completion, so we need to increment the index
|
276
|
+
if event.get("type") == "thinking":
|
277
|
+
index += 1
|
278
|
+
set_span_attribute(span, f"{prefix}.role", role)
|
279
|
+
if event.get("type") == "tool_use":
|
280
|
+
set_span_attribute(
|
281
|
+
span,
|
282
|
+
f"{prefix}.tool_calls.0.id",
|
283
|
+
event.get("id"),
|
284
|
+
)
|
249
285
|
set_span_attribute(
|
250
|
-
span,
|
286
|
+
span,
|
287
|
+
f"{prefix}.tool_calls.0.name",
|
288
|
+
event.get("name"),
|
251
289
|
)
|
252
|
-
|
253
|
-
|
290
|
+
tool_arguments = event.get("input")
|
291
|
+
if tool_arguments is not None:
|
292
|
+
set_span_attribute(
|
293
|
+
span,
|
294
|
+
f"{prefix}.tool_calls.0.arguments",
|
295
|
+
# already stringified
|
296
|
+
tool_arguments,
|
297
|
+
)
|
298
|
+
else:
|
254
299
|
set_span_attribute(span, f"{prefix}.content", event.get("text"))
|
255
|
-
except Exception as e:
|
256
|
-
logger.warning("Failed to set completion attributes, error: %s", str(e))
|
@@ -40,15 +40,19 @@ def _process_response_item(item, complete_response):
|
|
40
40
|
complete_response["events"].append(
|
41
41
|
{"index": index, "text": "", "type": item.content_block.type}
|
42
42
|
)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
43
|
+
if item.content_block.type == "tool_use":
|
44
|
+
complete_response["events"][index]["id"] = item.content_block.id
|
45
|
+
complete_response["events"][index]["name"] = item.content_block.name
|
46
|
+
complete_response["events"][index]["input"] = ""
|
47
|
+
|
48
|
+
elif item.type == "content_block_delta":
|
47
49
|
index = item.index
|
48
50
|
if item.delta.type == "thinking_delta":
|
49
|
-
complete_response["events"][index]["text"] += item.delta.thinking
|
51
|
+
complete_response["events"][index]["text"] += item.delta.thinking or ""
|
50
52
|
elif item.delta.type == "text_delta":
|
51
|
-
complete_response["events"][index]["text"] += item.delta.text
|
53
|
+
complete_response["events"][index]["text"] += item.delta.text or ""
|
54
|
+
elif item.delta.type == "input_json_delta":
|
55
|
+
complete_response["events"][index]["input"] += item.delta.partial_json
|
52
56
|
elif item.type == "message_delta":
|
53
57
|
for event in complete_response.get("events", []):
|
54
58
|
event["finish_reason"] = item.delta.stop_reason
|
@@ -293,3 +297,99 @@ async def abuild_from_streaming_response(
|
|
293
297
|
if span.is_recording():
|
294
298
|
span.set_status(Status(StatusCode.OK))
|
295
299
|
span.end()
|
300
|
+
|
301
|
+
|
302
|
+
class WrappedMessageStreamManager:
|
303
|
+
"""Wrapper for MessageStreamManager that handles instrumentation"""
|
304
|
+
|
305
|
+
def __init__(
|
306
|
+
self,
|
307
|
+
stream_manager,
|
308
|
+
span,
|
309
|
+
instance,
|
310
|
+
start_time,
|
311
|
+
token_histogram,
|
312
|
+
choice_counter,
|
313
|
+
duration_histogram,
|
314
|
+
exception_counter,
|
315
|
+
event_logger,
|
316
|
+
kwargs,
|
317
|
+
):
|
318
|
+
self._stream_manager = stream_manager
|
319
|
+
self._span = span
|
320
|
+
self._instance = instance
|
321
|
+
self._start_time = start_time
|
322
|
+
self._token_histogram = token_histogram
|
323
|
+
self._choice_counter = choice_counter
|
324
|
+
self._duration_histogram = duration_histogram
|
325
|
+
self._exception_counter = exception_counter
|
326
|
+
self._event_logger = event_logger
|
327
|
+
self._kwargs = kwargs
|
328
|
+
|
329
|
+
def __enter__(self):
|
330
|
+
# Call the original stream manager's __enter__ to get the actual stream
|
331
|
+
stream = self._stream_manager.__enter__()
|
332
|
+
# Return the wrapped stream
|
333
|
+
return build_from_streaming_response(
|
334
|
+
self._span,
|
335
|
+
stream,
|
336
|
+
self._instance,
|
337
|
+
self._start_time,
|
338
|
+
self._token_histogram,
|
339
|
+
self._choice_counter,
|
340
|
+
self._duration_histogram,
|
341
|
+
self._exception_counter,
|
342
|
+
self._event_logger,
|
343
|
+
self._kwargs,
|
344
|
+
)
|
345
|
+
|
346
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
347
|
+
return self._stream_manager.__exit__(exc_type, exc_val, exc_tb)
|
348
|
+
|
349
|
+
|
350
|
+
class WrappedAsyncMessageStreamManager:
|
351
|
+
"""Wrapper for AsyncMessageStreamManager that handles instrumentation"""
|
352
|
+
|
353
|
+
def __init__(
|
354
|
+
self,
|
355
|
+
stream_manager,
|
356
|
+
span,
|
357
|
+
instance,
|
358
|
+
start_time,
|
359
|
+
token_histogram,
|
360
|
+
choice_counter,
|
361
|
+
duration_histogram,
|
362
|
+
exception_counter,
|
363
|
+
event_logger,
|
364
|
+
kwargs,
|
365
|
+
):
|
366
|
+
self._stream_manager = stream_manager
|
367
|
+
self._span = span
|
368
|
+
self._instance = instance
|
369
|
+
self._start_time = start_time
|
370
|
+
self._token_histogram = token_histogram
|
371
|
+
self._choice_counter = choice_counter
|
372
|
+
self._duration_histogram = duration_histogram
|
373
|
+
self._exception_counter = exception_counter
|
374
|
+
self._event_logger = event_logger
|
375
|
+
self._kwargs = kwargs
|
376
|
+
|
377
|
+
async def __aenter__(self):
|
378
|
+
# Call the original stream manager's __aenter__ to get the actual stream
|
379
|
+
stream = await self._stream_manager.__aenter__()
|
380
|
+
# Return the wrapped stream
|
381
|
+
return abuild_from_streaming_response(
|
382
|
+
self._span,
|
383
|
+
stream,
|
384
|
+
self._instance,
|
385
|
+
self._start_time,
|
386
|
+
self._token_histogram,
|
387
|
+
self._choice_counter,
|
388
|
+
self._duration_histogram,
|
389
|
+
self._exception_counter,
|
390
|
+
self._event_logger,
|
391
|
+
self._kwargs,
|
392
|
+
)
|
393
|
+
|
394
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
395
|
+
return await self._stream_manager.__aexit__(exc_type, exc_val, exc_tb)
|
@@ -1012,7 +1012,7 @@ def _parse_tool_calls(
|
|
1012
1012
|
# Handle dict or ChatCompletionMessageToolCall
|
1013
1013
|
if isinstance(tool_call, dict):
|
1014
1014
|
tool_call_data = copy.deepcopy(tool_call)
|
1015
|
-
elif
|
1015
|
+
elif _is_tool_call_model(tool_call):
|
1016
1016
|
tool_call_data = tool_call.model_dump()
|
1017
1017
|
elif isinstance(tool_call, FunctionCall):
|
1018
1018
|
function_call = tool_call.model_dump()
|
@@ -1029,6 +1029,17 @@ def _parse_tool_calls(
|
|
1029
1029
|
return result
|
1030
1030
|
|
1031
1031
|
|
1032
|
+
def _is_tool_call_model(tool_call):
|
1033
|
+
try:
|
1034
|
+
from openai.types.chat.chat_completion_message_tool_call import (
|
1035
|
+
ChatCompletionMessageFunctionToolCall,
|
1036
|
+
)
|
1037
|
+
|
1038
|
+
return isinstance(tool_call, ChatCompletionMessageFunctionToolCall)
|
1039
|
+
except Exception:
|
1040
|
+
return False
|
1041
|
+
|
1042
|
+
|
1032
1043
|
@singledispatch
|
1033
1044
|
def _parse_choice_event(choice) -> ChoiceEvent:
|
1034
1045
|
has_message = choice.message is not None
|
@@ -5,6 +5,7 @@ import threading
|
|
5
5
|
from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
|
6
6
|
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
7
7
|
from lmnr.sdk.client.synchronous.sync_client import LaminarClient
|
8
|
+
from lmnr.sdk.types import SessionRecordingOptions
|
8
9
|
from lmnr.sdk.log import VerboseColorfulFormatter
|
9
10
|
from lmnr.opentelemetry_lib.tracing.instruments import (
|
10
11
|
Instruments,
|
@@ -38,6 +39,7 @@ MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN = 5000
|
|
38
39
|
class TracerWrapper(object):
|
39
40
|
resource_attributes: dict = {}
|
40
41
|
enable_content_tracing: bool = True
|
42
|
+
session_recording_options: SessionRecordingOptions = {}
|
41
43
|
_lock = threading.Lock()
|
42
44
|
_tracer_provider: TracerProvider | None = None
|
43
45
|
_logger: logging.Logger
|
@@ -62,6 +64,7 @@ class TracerWrapper(object):
|
|
62
64
|
timeout_seconds: int = 30,
|
63
65
|
set_global_tracer_provider: bool = True,
|
64
66
|
otel_logger_level: int = logging.ERROR,
|
67
|
+
session_recording_options: SessionRecordingOptions | None = None,
|
65
68
|
) -> "TracerWrapper":
|
66
69
|
# Silence some opentelemetry warnings
|
67
70
|
logging.getLogger("opentelemetry.trace").setLevel(otel_logger_level)
|
@@ -71,6 +74,9 @@ class TracerWrapper(object):
|
|
71
74
|
if not hasattr(cls, "instance"):
|
72
75
|
cls._initialize_logger(cls)
|
73
76
|
obj = super(TracerWrapper, cls).__new__(cls)
|
77
|
+
|
78
|
+
# Store session recording options
|
79
|
+
cls.session_recording_options = session_recording_options or {}
|
74
80
|
|
75
81
|
obj._client = LaminarClient(
|
76
82
|
base_url=base_http_url,
|
@@ -243,6 +249,11 @@ class TracerWrapper(object):
|
|
243
249
|
self._logger.warning("TracerWrapper not fully initialized, cannot flush")
|
244
250
|
return False
|
245
251
|
return self._span_processor.force_flush()
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
def get_session_recording_options(cls) -> SessionRecordingOptions:
|
255
|
+
"""Get the session recording options set during initialization."""
|
256
|
+
return cls.session_recording_options
|
246
257
|
|
247
258
|
def get_tracer(self):
|
248
259
|
if self._tracer_provider is None:
|
lmnr/sdk/browser/pw_utils.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
import orjson
|
1
2
|
import logging
|
2
3
|
import os
|
4
|
+
import time
|
3
5
|
|
4
6
|
from opentelemetry import trace
|
5
7
|
|
@@ -9,6 +11,8 @@ from lmnr.sdk.browser.utils import retry_sync, retry_async
|
|
9
11
|
from lmnr.sdk.client.synchronous.sync_client import LaminarClient
|
10
12
|
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
11
13
|
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
14
|
+
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
15
|
+
from lmnr.sdk.types import MaskInputOptions
|
12
16
|
|
13
17
|
try:
|
14
18
|
if is_package_installed("playwright"):
|
@@ -32,17 +36,24 @@ except ImportError as e:
|
|
32
36
|
|
33
37
|
logger = logging.getLogger(__name__)
|
34
38
|
|
39
|
+
OLD_BUFFER_TIMEOUT = 60
|
40
|
+
|
35
41
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
36
42
|
with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
|
37
43
|
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
38
44
|
|
39
45
|
INJECT_PLACEHOLDER = """
|
40
|
-
() => {
|
46
|
+
(mask_input_options) => {
|
41
47
|
const BATCH_TIMEOUT = 2000; // Send events after 2 seconds
|
42
48
|
const MAX_WORKER_PROMISES = 50; // Max concurrent worker promises
|
43
49
|
const HEARTBEAT_INTERVAL = 1000;
|
44
|
-
|
50
|
+
const CHUNK_SIZE = 256 * 1024; // 256KB chunks
|
51
|
+
const CHUNK_SEND_DELAY = 100; // 100ms delay between chunks
|
52
|
+
|
45
53
|
window.lmnrRrwebEventsBatch = [];
|
54
|
+
window.lmnrChunkQueue = [];
|
55
|
+
window.lmnrChunkSequence = 0;
|
56
|
+
window.lmnrCurrentBatchId = null;
|
46
57
|
|
47
58
|
// Create a Web Worker for heavy JSON processing with chunked processing
|
48
59
|
const createCompressionWorker = () => {
|
@@ -99,6 +110,25 @@ INJECT_PLACEHOLDER = """
|
|
99
110
|
let compressionWorker = null;
|
100
111
|
let workerPromises = new Map();
|
101
112
|
let workerId = 0;
|
113
|
+
let workerSupported = null; // null = unknown, true = supported, false = blocked by CSP
|
114
|
+
|
115
|
+
// Test if workers are supported (not blocked by CSP)
|
116
|
+
function testWorkerSupport() {
|
117
|
+
if (workerSupported !== null) {
|
118
|
+
return workerSupported;
|
119
|
+
}
|
120
|
+
|
121
|
+
try {
|
122
|
+
const testWorker = createCompressionWorker();
|
123
|
+
testWorker.terminate();
|
124
|
+
workerSupported = true;
|
125
|
+
return true;
|
126
|
+
} catch (error) {
|
127
|
+
console.warn('Web Workers blocked by CSP, will use main thread compression:', error);
|
128
|
+
workerSupported = false;
|
129
|
+
return false;
|
130
|
+
}
|
131
|
+
}
|
102
132
|
|
103
133
|
// Cleanup function for worker
|
104
134
|
const cleanupWorker = () => {
|
@@ -222,6 +252,11 @@ INJECT_PLACEHOLDER = """
|
|
222
252
|
// Alternative: Use transferable objects for maximum efficiency
|
223
253
|
async function compressLargeObjectTransferable(data) {
|
224
254
|
try {
|
255
|
+
// Check if workers are supported first
|
256
|
+
if (!testWorkerSupport()) {
|
257
|
+
return compressSmallObject(data);
|
258
|
+
}
|
259
|
+
|
225
260
|
// Clean up stale promises first
|
226
261
|
cleanupStalePromises();
|
227
262
|
|
@@ -281,49 +316,60 @@ INJECT_PLACEHOLDER = """
|
|
281
316
|
|
282
317
|
// Worker-based compression for large objects
|
283
318
|
async function compressLargeObject(data, isLarge = true) {
|
319
|
+
// Check if workers are supported first - if not, use main thread compression
|
320
|
+
if (!testWorkerSupport()) {
|
321
|
+
return await compressSmallObject(data);
|
322
|
+
}
|
323
|
+
|
284
324
|
try {
|
285
325
|
// Use transferable objects for better performance
|
286
326
|
return await compressLargeObjectTransferable(data);
|
287
327
|
} catch (error) {
|
288
328
|
console.warn('Transferable failed, falling back to string method:', error);
|
289
|
-
|
290
|
-
|
329
|
+
try {
|
330
|
+
// Fallback to string method with worker
|
331
|
+
const jsonString = await stringifyNonBlocking(data);
|
332
|
+
|
333
|
+
return new Promise((resolve, reject) => {
|
334
|
+
if (!compressionWorker) {
|
335
|
+
compressionWorker = createCompressionWorker();
|
336
|
+
compressionWorker.onmessage = (e) => {
|
337
|
+
const { id, success, data: result, error } = e.data;
|
338
|
+
const promise = workerPromises.get(id);
|
339
|
+
if (promise) {
|
340
|
+
workerPromises.delete(id);
|
341
|
+
if (success) {
|
342
|
+
promise.resolve(result);
|
343
|
+
} else {
|
344
|
+
promise.reject(new Error(error));
|
345
|
+
}
|
346
|
+
}
|
347
|
+
};
|
348
|
+
|
349
|
+
compressionWorker.onerror = (error) => {
|
350
|
+
console.error('Compression worker error:', error);
|
351
|
+
cleanupWorker();
|
352
|
+
};
|
353
|
+
}
|
291
354
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
if (promise) {
|
355
|
+
const id = ++workerId;
|
356
|
+
workerPromises.set(id, { resolve, reject });
|
357
|
+
|
358
|
+
// Set timeout to prevent hanging promises
|
359
|
+
setTimeout(() => {
|
360
|
+
if (workerPromises.has(id)) {
|
299
361
|
workerPromises.delete(id);
|
300
|
-
|
301
|
-
promise.resolve(result);
|
302
|
-
} else {
|
303
|
-
promise.reject(new Error(error));
|
304
|
-
}
|
362
|
+
reject(new Error('Compression timeout'));
|
305
363
|
}
|
306
|
-
};
|
364
|
+
}, 10000);
|
307
365
|
|
308
|
-
compressionWorker.
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
workerPromises.set(id, { resolve, reject });
|
316
|
-
|
317
|
-
// Set timeout to prevent hanging promises
|
318
|
-
setTimeout(() => {
|
319
|
-
if (workerPromises.has(id)) {
|
320
|
-
workerPromises.delete(id);
|
321
|
-
reject(new Error('Compression timeout'));
|
322
|
-
}
|
323
|
-
}, 10000);
|
324
|
-
|
325
|
-
compressionWorker.postMessage({ jsonString, id });
|
326
|
-
});
|
366
|
+
compressionWorker.postMessage({ jsonString, id });
|
367
|
+
});
|
368
|
+
} catch (workerError) {
|
369
|
+
console.warn('Worker creation failed, falling back to main thread compression:', workerError);
|
370
|
+
// Final fallback: compress on main thread (may block UI but will work)
|
371
|
+
return await compressSmallObject(data);
|
372
|
+
}
|
327
373
|
}
|
328
374
|
}
|
329
375
|
|
@@ -343,15 +389,82 @@ INJECT_PLACEHOLDER = """
|
|
343
389
|
return false;
|
344
390
|
}
|
345
391
|
|
392
|
+
// Create chunks from a string with metadata
|
393
|
+
function createChunks(str, batchId) {
|
394
|
+
const chunks = [];
|
395
|
+
const totalChunks = Math.ceil(str.length / CHUNK_SIZE);
|
396
|
+
|
397
|
+
for (let i = 0; i < str.length; i += CHUNK_SIZE) {
|
398
|
+
const chunk = str.slice(i, i + CHUNK_SIZE);
|
399
|
+
chunks.push({
|
400
|
+
batchId: batchId,
|
401
|
+
chunkIndex: chunks.length,
|
402
|
+
totalChunks: totalChunks,
|
403
|
+
data: chunk,
|
404
|
+
isFinal: chunks.length === totalChunks - 1
|
405
|
+
});
|
406
|
+
}
|
407
|
+
|
408
|
+
return chunks;
|
409
|
+
}
|
410
|
+
|
411
|
+
// Send chunks with flow control
|
412
|
+
async function sendChunks(chunks) {
|
413
|
+
if (typeof window.lmnrSendEvents !== 'function') {
|
414
|
+
return;
|
415
|
+
}
|
416
|
+
|
417
|
+
window.lmnrChunkQueue.push(...chunks);
|
418
|
+
|
419
|
+
// Process queue
|
420
|
+
while (window.lmnrChunkQueue.length > 0) {
|
421
|
+
const chunk = window.lmnrChunkQueue.shift();
|
422
|
+
try {
|
423
|
+
await window.lmnrSendEvents(chunk);
|
424
|
+
// Small delay between chunks to avoid overwhelming CDP
|
425
|
+
await new Promise(resolve => setTimeout(resolve, CHUNK_SEND_DELAY));
|
426
|
+
} catch (error) {
|
427
|
+
console.error('Failed to send chunk:', error);
|
428
|
+
// On error, clear failed chunk batch from queue
|
429
|
+
window.lmnrChunkQueue = window.lmnrChunkQueue.filter(c => c.batchId !== chunk.batchId);
|
430
|
+
break;
|
431
|
+
}
|
432
|
+
}
|
433
|
+
}
|
434
|
+
|
346
435
|
async function sendBatchIfReady() {
|
347
436
|
if (window.lmnrRrwebEventsBatch.length > 0 && typeof window.lmnrSendEvents === 'function') {
|
348
437
|
const events = window.lmnrRrwebEventsBatch;
|
349
438
|
window.lmnrRrwebEventsBatch = [];
|
350
439
|
|
351
440
|
try {
|
352
|
-
|
441
|
+
// Generate unique batch ID
|
442
|
+
const batchId = `${Date.now()}_${window.lmnrChunkSequence++}`;
|
443
|
+
window.lmnrCurrentBatchId = batchId;
|
444
|
+
|
445
|
+
// Stringify the entire batch
|
446
|
+
const batchString = JSON.stringify(events);
|
447
|
+
|
448
|
+
// Check size and chunk if necessary
|
449
|
+
if (batchString.length <= CHUNK_SIZE) {
|
450
|
+
// Small enough to send as single chunk
|
451
|
+
const chunk = {
|
452
|
+
batchId: batchId,
|
453
|
+
chunkIndex: 0,
|
454
|
+
totalChunks: 1,
|
455
|
+
data: batchString,
|
456
|
+
isFinal: true
|
457
|
+
};
|
458
|
+
await window.lmnrSendEvents(chunk);
|
459
|
+
} else {
|
460
|
+
// Need to chunk
|
461
|
+
const chunks = createChunks(batchString, batchId);
|
462
|
+
await sendChunks(chunks);
|
463
|
+
}
|
353
464
|
} catch (error) {
|
354
465
|
console.error('Failed to send events:', error);
|
466
|
+
// Clear batch to prevent memory buildup
|
467
|
+
window.lmnrRrwebEventsBatch = [];
|
355
468
|
}
|
356
469
|
}
|
357
470
|
}
|
@@ -387,7 +500,16 @@ INJECT_PLACEHOLDER = """
|
|
387
500
|
},
|
388
501
|
recordCanvas: true,
|
389
502
|
collectFonts: true,
|
390
|
-
recordCrossOriginIframes: true
|
503
|
+
recordCrossOriginIframes: true,
|
504
|
+
maskInputOptions: {
|
505
|
+
password: true,
|
506
|
+
textarea: mask_input_options.textarea || false,
|
507
|
+
text: mask_input_options.text || false,
|
508
|
+
number: mask_input_options.number || false,
|
509
|
+
select: mask_input_options.select || false,
|
510
|
+
email: mask_input_options.email || false,
|
511
|
+
tel: mask_input_options.tel || false,
|
512
|
+
}
|
391
513
|
});
|
392
514
|
|
393
515
|
function heartbeat() {
|
@@ -407,6 +529,30 @@ INJECT_PLACEHOLDER = """
|
|
407
529
|
"""
|
408
530
|
|
409
531
|
|
532
|
+
def get_mask_input_setting() -> MaskInputOptions:
|
533
|
+
"""Get the mask_input setting from session recording configuration."""
|
534
|
+
try:
|
535
|
+
config = TracerWrapper.get_session_recording_options()
|
536
|
+
return config.get("mask_input_options", MaskInputOptions(
|
537
|
+
textarea=False,
|
538
|
+
text=False,
|
539
|
+
number=False,
|
540
|
+
select=False,
|
541
|
+
email=False,
|
542
|
+
tel=False,
|
543
|
+
))
|
544
|
+
except (AttributeError, Exception):
|
545
|
+
# Fallback to default configuration if TracerWrapper is not initialized
|
546
|
+
return MaskInputOptions(
|
547
|
+
textarea=False,
|
548
|
+
text=False,
|
549
|
+
number=False,
|
550
|
+
select=False,
|
551
|
+
email=False,
|
552
|
+
tel=False,
|
553
|
+
)
|
554
|
+
|
555
|
+
|
410
556
|
def inject_session_recorder_sync(page: SyncPage):
|
411
557
|
try:
|
412
558
|
try:
|
@@ -435,7 +581,7 @@ def inject_session_recorder_sync(page: SyncPage):
|
|
435
581
|
return
|
436
582
|
|
437
583
|
try:
|
438
|
-
page.evaluate(INJECT_PLACEHOLDER)
|
584
|
+
page.evaluate(INJECT_PLACEHOLDER, get_mask_input_setting())
|
439
585
|
except Exception as e:
|
440
586
|
logger.debug(f"Failed to inject session recorder: {e}")
|
441
587
|
|
@@ -471,7 +617,7 @@ async def inject_session_recorder_async(page: Page):
|
|
471
617
|
return
|
472
618
|
|
473
619
|
try:
|
474
|
-
await page.evaluate(INJECT_PLACEHOLDER)
|
620
|
+
await page.evaluate(INJECT_PLACEHOLDER, get_mask_input_setting())
|
475
621
|
except Exception as e:
|
476
622
|
logger.debug(f"Failed to inject session recorder placeholder: {e}")
|
477
623
|
|
@@ -486,17 +632,54 @@ def start_recording_events_sync(page: SyncPage, session_id: str, client: Laminar
|
|
486
632
|
span = trace.get_current_span(ctx)
|
487
633
|
trace_id = format(span.get_span_context().trace_id, "032x")
|
488
634
|
span.set_attribute("lmnr.internal.has_browser_session", True)
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
pass
|
495
|
-
|
496
|
-
def send_events_from_browser(events):
|
635
|
+
|
636
|
+
# Buffer for reassembling chunks
|
637
|
+
chunk_buffers = {}
|
638
|
+
|
639
|
+
def send_events_from_browser(chunk):
|
497
640
|
try:
|
498
|
-
|
499
|
-
|
641
|
+
# Handle chunked data
|
642
|
+
batch_id = chunk['batchId']
|
643
|
+
chunk_index = chunk['chunkIndex']
|
644
|
+
total_chunks = chunk['totalChunks']
|
645
|
+
data = chunk['data']
|
646
|
+
|
647
|
+
# Initialize buffer for this batch if needed
|
648
|
+
if batch_id not in chunk_buffers:
|
649
|
+
chunk_buffers[batch_id] = {
|
650
|
+
'chunks': {},
|
651
|
+
'total': total_chunks,
|
652
|
+
'timestamp': time.time()
|
653
|
+
}
|
654
|
+
|
655
|
+
# Store chunk
|
656
|
+
chunk_buffers[batch_id]['chunks'][chunk_index] = data
|
657
|
+
|
658
|
+
# Check if we have all chunks
|
659
|
+
if len(chunk_buffers[batch_id]['chunks']) == total_chunks:
|
660
|
+
# Reassemble the full message
|
661
|
+
full_data = ''.join(chunk_buffers[batch_id]['chunks'][i] for i in range(total_chunks))
|
662
|
+
|
663
|
+
# Parse the JSON
|
664
|
+
events = orjson.loads(full_data)
|
665
|
+
|
666
|
+
# Send to server
|
667
|
+
if events and len(events) > 0:
|
668
|
+
client._browser_events.send(session_id, trace_id, events)
|
669
|
+
|
670
|
+
# Clean up buffer
|
671
|
+
del chunk_buffers[batch_id]
|
672
|
+
|
673
|
+
# Clean up old incomplete buffers
|
674
|
+
current_time = time.time()
|
675
|
+
to_delete = []
|
676
|
+
for bid, buffer in chunk_buffers.items():
|
677
|
+
if current_time - buffer['timestamp'] > OLD_BUFFER_TIMEOUT:
|
678
|
+
to_delete.append(bid)
|
679
|
+
for bid in to_delete:
|
680
|
+
logger.debug(f"Cleaning up incomplete chunk buffer: {bid}")
|
681
|
+
del chunk_buffers[bid]
|
682
|
+
|
500
683
|
except Exception as e:
|
501
684
|
logger.debug(f"Could not send events: {e}")
|
502
685
|
|
@@ -524,19 +707,56 @@ async def start_recording_events_async(
|
|
524
707
|
span = trace.get_current_span(ctx)
|
525
708
|
trace_id = format(span.get_span_context().trace_id, "032x")
|
526
709
|
span.set_attribute("lmnr.internal.has_browser_session", True)
|
527
|
-
|
528
|
-
try:
|
529
|
-
if await page.evaluate(
|
530
|
-
"""() => typeof window.lmnrSendEvents !== 'undefined'"""
|
531
|
-
):
|
532
|
-
return
|
533
|
-
except Exception:
|
534
|
-
pass
|
535
710
|
|
536
|
-
|
711
|
+
# Buffer for reassembling chunks
|
712
|
+
chunk_buffers = {}
|
713
|
+
|
714
|
+
async def send_events_from_browser(chunk):
|
537
715
|
try:
|
538
|
-
|
539
|
-
|
716
|
+
# Handle chunked data
|
717
|
+
batch_id = chunk['batchId']
|
718
|
+
chunk_index = chunk['chunkIndex']
|
719
|
+
total_chunks = chunk['totalChunks']
|
720
|
+
data = chunk['data']
|
721
|
+
|
722
|
+
# Initialize buffer for this batch if needed
|
723
|
+
if batch_id not in chunk_buffers:
|
724
|
+
chunk_buffers[batch_id] = {
|
725
|
+
'chunks': {},
|
726
|
+
'total': total_chunks,
|
727
|
+
'timestamp': time.time()
|
728
|
+
}
|
729
|
+
|
730
|
+
# Store chunk
|
731
|
+
chunk_buffers[batch_id]['chunks'][chunk_index] = data
|
732
|
+
|
733
|
+
# Check if we have all chunks
|
734
|
+
if len(chunk_buffers[batch_id]['chunks']) == total_chunks:
|
735
|
+
# Reassemble the full message
|
736
|
+
full_data = ''
|
737
|
+
for i in range(total_chunks):
|
738
|
+
full_data += chunk_buffers[batch_id]['chunks'][i]
|
739
|
+
|
740
|
+
# Parse the JSON
|
741
|
+
events = orjson.loads(full_data)
|
742
|
+
|
743
|
+
# Send to server
|
744
|
+
if events and len(events) > 0:
|
745
|
+
await client._browser_events.send(session_id, trace_id, events)
|
746
|
+
|
747
|
+
# Clean up buffer
|
748
|
+
del chunk_buffers[batch_id]
|
749
|
+
|
750
|
+
# Clean up old incomplete buffers
|
751
|
+
current_time = time.time()
|
752
|
+
to_delete = []
|
753
|
+
for bid, buffer in chunk_buffers.items():
|
754
|
+
if current_time - buffer['timestamp'] > OLD_BUFFER_TIMEOUT:
|
755
|
+
to_delete.append(bid)
|
756
|
+
for bid in to_delete:
|
757
|
+
logger.debug(f"Cleaning up incomplete chunk buffer: {bid}")
|
758
|
+
del chunk_buffers[bid]
|
759
|
+
|
540
760
|
except Exception as e:
|
541
761
|
logger.debug(f"Could not send events: {e}")
|
542
762
|
|
lmnr/sdk/decorators.py
CHANGED
@@ -102,7 +102,8 @@ def observe(
|
|
102
102
|
):
|
103
103
|
logger.warning("Tags must be a list of strings. Tags will be ignored.")
|
104
104
|
else:
|
105
|
-
|
105
|
+
# list(set(tags)) to deduplicate tags
|
106
|
+
association_properties["tags"] = list(set(tags))
|
106
107
|
if input_formatter is not None and ignore_input:
|
107
108
|
logger.warning(
|
108
109
|
f"observe, function {func.__name__}: Input formatter"
|
lmnr/sdk/laminar.py
CHANGED
@@ -45,6 +45,7 @@ from .log import VerboseColorfulFormatter
|
|
45
45
|
|
46
46
|
from .types import (
|
47
47
|
LaminarSpanContext,
|
48
|
+
SessionRecordingOptions,
|
48
49
|
TraceType,
|
49
50
|
)
|
50
51
|
|
@@ -73,6 +74,7 @@ class Laminar:
|
|
73
74
|
export_timeout_seconds: int | None = None,
|
74
75
|
set_global_tracer_provider: bool = True,
|
75
76
|
otel_logger_level: int = logging.ERROR,
|
77
|
+
session_recording_options: SessionRecordingOptions | None = None,
|
76
78
|
):
|
77
79
|
"""Initialize Laminar context across the application.
|
78
80
|
This method must be called before using any other Laminar methods or
|
@@ -119,6 +121,10 @@ class Laminar:
|
|
119
121
|
Defaults to True.
|
120
122
|
otel_logger_level (int, optional): OpenTelemetry logger level. Defaults\
|
121
123
|
to logging.ERROR.
|
124
|
+
session_recording_options (SessionRecordingOptions | None, optional): Options\
|
125
|
+
for browser session recording. Currently supports 'mask_input'\
|
126
|
+
(bool) to control whether input fields are masked during recording.\
|
127
|
+
Defaults to None (uses default masking behavior).
|
122
128
|
|
123
129
|
Raises:
|
124
130
|
ValueError: If project API key is not set
|
@@ -179,6 +185,7 @@ class Laminar:
|
|
179
185
|
timeout_seconds=export_timeout_seconds,
|
180
186
|
set_global_tracer_provider=set_global_tracer_provider,
|
181
187
|
otel_logger_level=otel_logger_level,
|
188
|
+
session_recording_options=session_recording_options,
|
182
189
|
)
|
183
190
|
|
184
191
|
@classmethod
|
@@ -741,7 +748,8 @@ class Laminar:
|
|
741
748
|
"Tags must be a list of strings. Tags will be ignored."
|
742
749
|
)
|
743
750
|
return
|
744
|
-
|
751
|
+
# list(set(tags)) to deduplicate tags
|
752
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", list(set(tags)))
|
745
753
|
|
746
754
|
@classmethod
|
747
755
|
def set_trace_session_id(cls, session_id: str | None = None):
|
lmnr/sdk/types.py
CHANGED
@@ -9,7 +9,7 @@ import uuid
|
|
9
9
|
|
10
10
|
from enum import Enum
|
11
11
|
from opentelemetry.trace import SpanContext, TraceFlags
|
12
|
-
from typing import Any, Awaitable, Callable, Literal, Optional
|
12
|
+
from typing import Any, Awaitable, Callable, Literal, Optional, TypedDict
|
13
13
|
|
14
14
|
from .utils import serialize
|
15
15
|
|
@@ -346,3 +346,15 @@ class RunAgentResponseChunk(pydantic.RootModel):
|
|
346
346
|
| ErrorChunkContent
|
347
347
|
| TimeoutChunkContent
|
348
348
|
)
|
349
|
+
|
350
|
+
|
351
|
+
class MaskInputOptions(TypedDict):
|
352
|
+
textarea: bool | None
|
353
|
+
text: bool | None
|
354
|
+
number: bool | None
|
355
|
+
select: bool | None
|
356
|
+
email: bool | None
|
357
|
+
tel: bool | None
|
358
|
+
|
359
|
+
class SessionRecordingOptions(TypedDict):
|
360
|
+
mask_input_options: MaskInputOptions | None
|
lmnr/version.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lmnr
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.3
|
4
4
|
Summary: Python SDK for Laminar
|
5
5
|
Author: lmnr.ai
|
6
6
|
Author-email: lmnr.ai <founders@lmnr.ai>
|
@@ -19,61 +19,59 @@ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.33.0
|
|
19
19
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.33.0
|
20
20
|
Requires-Dist: opentelemetry-instrumentation>=0.54b0
|
21
21
|
Requires-Dist: opentelemetry-semantic-conventions>=0.54b0
|
22
|
-
Requires-Dist: opentelemetry-semantic-conventions-ai>=0.4.
|
22
|
+
Requires-Dist: opentelemetry-semantic-conventions-ai>=0.4.11
|
23
23
|
Requires-Dist: tqdm>=4.0
|
24
24
|
Requires-Dist: tenacity>=8.0
|
25
25
|
Requires-Dist: grpcio>=1
|
26
26
|
Requires-Dist: httpx>=0.25.0
|
27
27
|
Requires-Dist: orjson>=3.10.18
|
28
28
|
Requires-Dist: packaging>=22.0
|
29
|
-
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.
|
30
|
-
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.
|
31
|
-
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.
|
32
|
-
Requires-Dist: opentelemetry-instrumentation-chromadb>=0.
|
33
|
-
Requires-Dist: opentelemetry-instrumentation-cohere>=0.
|
34
|
-
Requires-Dist: opentelemetry-instrumentation-crewai>=0.
|
35
|
-
Requires-Dist: opentelemetry-instrumentation-
|
36
|
-
Requires-Dist: opentelemetry-instrumentation-
|
37
|
-
Requires-Dist: opentelemetry-instrumentation-
|
38
|
-
Requires-Dist: opentelemetry-instrumentation-
|
39
|
-
Requires-Dist: opentelemetry-instrumentation-
|
40
|
-
Requires-Dist: opentelemetry-instrumentation-
|
41
|
-
Requires-Dist: opentelemetry-instrumentation-
|
42
|
-
Requires-Dist: opentelemetry-instrumentation-
|
43
|
-
Requires-Dist: opentelemetry-instrumentation-
|
44
|
-
Requires-Dist: opentelemetry-instrumentation-
|
45
|
-
Requires-Dist: opentelemetry-instrumentation-
|
46
|
-
Requires-Dist: opentelemetry-instrumentation-
|
47
|
-
Requires-Dist: opentelemetry-instrumentation-
|
48
|
-
Requires-Dist: opentelemetry-instrumentation-
|
49
|
-
Requires-Dist: opentelemetry-instrumentation-
|
50
|
-
Requires-Dist: opentelemetry-instrumentation-
|
51
|
-
Requires-Dist: opentelemetry-instrumentation-
|
52
|
-
Requires-Dist: opentelemetry-instrumentation-
|
53
|
-
Requires-Dist: opentelemetry-instrumentation-
|
54
|
-
Requires-Dist: opentelemetry-instrumentation-
|
55
|
-
Requires-Dist: opentelemetry-instrumentation-
|
56
|
-
Requires-Dist: opentelemetry-instrumentation-
|
57
|
-
Requires-Dist: opentelemetry-instrumentation-
|
58
|
-
Requires-Dist: opentelemetry-instrumentation-
|
59
|
-
Requires-Dist: opentelemetry-instrumentation-
|
60
|
-
Requires-Dist: opentelemetry-instrumentation-
|
61
|
-
Requires-Dist: opentelemetry-instrumentation-
|
62
|
-
Requires-Dist: opentelemetry-instrumentation-
|
63
|
-
Requires-Dist: opentelemetry-instrumentation-
|
64
|
-
Requires-Dist: opentelemetry-instrumentation-
|
65
|
-
Requires-Dist: opentelemetry-instrumentation-
|
66
|
-
Requires-Dist: opentelemetry-instrumentation-
|
67
|
-
Requires-Dist: opentelemetry-instrumentation-
|
68
|
-
Requires-Dist: opentelemetry-instrumentation-
|
69
|
-
Requires-Dist: opentelemetry-instrumentation-
|
70
|
-
Requires-Dist: opentelemetry-instrumentation-
|
71
|
-
Requires-Dist: opentelemetry-instrumentation-
|
72
|
-
Requires-Dist: opentelemetry-instrumentation-
|
73
|
-
Requires-Dist: opentelemetry-instrumentation-
|
74
|
-
Requires-Dist: opentelemetry-instrumentation-
|
75
|
-
Requires-Dist: opentelemetry-instrumentation-watsonx>=0.40.12 ; extra == 'watsonx'
|
76
|
-
Requires-Dist: opentelemetry-instrumentation-weaviate>=0.40.12 ; extra == 'weaviate'
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.44.0 ; extra == 'alephalpha'
|
30
|
+
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.44.0 ; extra == 'all'
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.44.0 ; extra == 'all'
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-chromadb>=0.44.0 ; extra == 'all'
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-cohere>=0.44.0 ; extra == 'all'
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.44.0 ; extra == 'all'
|
35
|
+
Requires-Dist: opentelemetry-instrumentation-haystack>=0.44.0 ; extra == 'all'
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-lancedb>=0.44.0 ; extra == 'all'
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.44.0 ; extra == 'all'
|
38
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.44.0 ; extra == 'all'
|
39
|
+
Requires-Dist: opentelemetry-instrumentation-marqo>=0.44.0 ; extra == 'all'
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-mcp>=0.44.0 ; extra == 'all'
|
41
|
+
Requires-Dist: opentelemetry-instrumentation-milvus>=0.44.0 ; extra == 'all'
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.44.0 ; extra == 'all'
|
43
|
+
Requires-Dist: opentelemetry-instrumentation-ollama>=0.44.0 ; extra == 'all'
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-pinecone>=0.44.0 ; extra == 'all'
|
45
|
+
Requires-Dist: opentelemetry-instrumentation-qdrant>=0.44.0 ; extra == 'all'
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-replicate>=0.44.0 ; extra == 'all'
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-sagemaker>=0.44.0 ; extra == 'all'
|
48
|
+
Requires-Dist: opentelemetry-instrumentation-together>=0.44.0 ; extra == 'all'
|
49
|
+
Requires-Dist: opentelemetry-instrumentation-transformers>=0.44.0 ; extra == 'all'
|
50
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.44.0 ; extra == 'all'
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-watsonx>=0.44.0 ; extra == 'all'
|
52
|
+
Requires-Dist: opentelemetry-instrumentation-weaviate>=0.44.0 ; extra == 'all'
|
53
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.44.0 ; extra == 'bedrock'
|
54
|
+
Requires-Dist: opentelemetry-instrumentation-chromadb>=0.44.0 ; extra == 'chromadb'
|
55
|
+
Requires-Dist: opentelemetry-instrumentation-cohere>=0.44.0 ; extra == 'cohere'
|
56
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.44.0 ; extra == 'crewai'
|
57
|
+
Requires-Dist: opentelemetry-instrumentation-haystack>=0.44.0 ; extra == 'haystack'
|
58
|
+
Requires-Dist: opentelemetry-instrumentation-lancedb>=0.44.0 ; extra == 'lancedb'
|
59
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.44.0 ; extra == 'langchain'
|
60
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.44.0 ; extra == 'llamaindex'
|
61
|
+
Requires-Dist: opentelemetry-instrumentation-marqo>=0.44.0 ; extra == 'marqo'
|
62
|
+
Requires-Dist: opentelemetry-instrumentation-mcp>=0.44.0 ; extra == 'mcp'
|
63
|
+
Requires-Dist: opentelemetry-instrumentation-milvus>=0.44.0 ; extra == 'milvus'
|
64
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.44.0 ; extra == 'mistralai'
|
65
|
+
Requires-Dist: opentelemetry-instrumentation-ollama>=0.44.0 ; extra == 'ollama'
|
66
|
+
Requires-Dist: opentelemetry-instrumentation-pinecone>=0.44.0 ; extra == 'pinecone'
|
67
|
+
Requires-Dist: opentelemetry-instrumentation-qdrant>=0.44.0 ; extra == 'qdrant'
|
68
|
+
Requires-Dist: opentelemetry-instrumentation-replicate>=0.44.0 ; extra == 'replicate'
|
69
|
+
Requires-Dist: opentelemetry-instrumentation-sagemaker>=0.44.0 ; extra == 'sagemaker'
|
70
|
+
Requires-Dist: opentelemetry-instrumentation-together>=0.44.0 ; extra == 'together'
|
71
|
+
Requires-Dist: opentelemetry-instrumentation-transformers>=0.44.0 ; extra == 'transformers'
|
72
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.44.0 ; extra == 'vertexai'
|
73
|
+
Requires-Dist: opentelemetry-instrumentation-watsonx>=0.44.0 ; extra == 'watsonx'
|
74
|
+
Requires-Dist: opentelemetry-instrumentation-weaviate>=0.44.0 ; extra == 'weaviate'
|
77
75
|
Requires-Python: >=3.10, <4
|
78
76
|
Provides-Extra: alephalpha
|
79
77
|
Provides-Extra: all
|
@@ -81,7 +79,6 @@ Provides-Extra: bedrock
|
|
81
79
|
Provides-Extra: chromadb
|
82
80
|
Provides-Extra: cohere
|
83
81
|
Provides-Extra: crewai
|
84
|
-
Provides-Extra: google-generativeai
|
85
82
|
Provides-Extra: haystack
|
86
83
|
Provides-Extra: lancedb
|
87
84
|
Provides-Extra: langchain
|
@@ -1,16 +1,16 @@
|
|
1
|
-
lmnr/__init__.py,sha256=
|
1
|
+
lmnr/__init__.py,sha256=8be7b56ab62735fd54ca90a0642784c6153ed1d6e0f12734619ca0618dd9fbdb,1398
|
2
2
|
lmnr/cli.py,sha256=b8780b51f37fe9e20db5495c41d3ad3837f6b48f408b09a58688d017850c0796,6047
|
3
3
|
lmnr/opentelemetry_lib/.flake8,sha256=6c2c6e0e51b1dd8439e501ca3e21899277076a787da868d0254ba37056b79405,150
|
4
|
-
lmnr/opentelemetry_lib/__init__.py,sha256=
|
4
|
+
lmnr/opentelemetry_lib/__init__.py,sha256=1350e8d12ea2f422751ab3a80d7d32d10c27ad8e4c2989407771dc6e544d9c65,2350
|
5
5
|
lmnr/opentelemetry_lib/decorators/__init__.py,sha256=216536fb3ac8de18e6dfe4dfb2e571074c727466f97e6dcd609339c8458a345a,11511
|
6
6
|
lmnr/opentelemetry_lib/litellm/__init__.py,sha256=8a3679381ca5660cf53e4b7571850906c6635264129149adebda8f3f7c248f68,15127
|
7
7
|
lmnr/opentelemetry_lib/litellm/utils.py,sha256=da8cf0553f82dc7203109f117a4c7b4185e8baf34caad12d7823875515201a27,539
|
8
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py,sha256=
|
8
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py,sha256=2604189b7598edb5404ddbcd0775bdf2dc506dd5e6319eef4e4724e39c420301,23276
|
9
9
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py,sha256=972919b821b9b7e5dc7cd191ba7e78b30b6efa5d63514e8cb301996d6386392c,369
|
10
10
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py,sha256=812b3ea1c5a04412113d4dd770717561861595f9eec5b94dd8174c6ddfb7572a,6831
|
11
11
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py,sha256=3c27c21b1aeb02bc19a91fb8c05717ae1c10ab4b01300c664aba42e0f50cb5a3,876
|
12
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py,sha256=
|
13
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py,sha256=
|
12
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py,sha256=6a931571b4a036cd8711419cadad737ec46cc67b4368f2f662f1a565737f9f9b,11403
|
13
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py,sha256=7ca9f49e4d9a3bac292d13a8ee9827fdfb8a46d13ebdcbbfbac9c5584d11eaf3,13441
|
14
14
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py,sha256=0044f02da8b99322fdbf3f8f6663f04ff5d1295ddae92a635fd16eb685d5fbb6,5386
|
15
15
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py,sha256=5aacde4ca55ef50ed07a239ad8a86889e0621b1cc72be19bd93be7c9e20910a9,23
|
16
16
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=a47d4d1234e0278d1538748130a79c03d6cb3486976cb5d19578fe1b90f28e7b,20524
|
@@ -28,7 +28,7 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py,sha25
|
|
28
28
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py,sha256=9dff6c2595e79edb38818668aed1220efc188d8a982594c04f4ceeb6e3ff47a6,1512
|
29
29
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py,sha256=8b91dc16af927eee75b969c0980c606680b347a87f8533bc0f4a092e5ec6e5c9,2071
|
30
30
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py,sha256=9d182c8cef5ee1e205dc4c2f7c8e49d8403ee9fee66072c5cfdd29a0d54f61a2,15149
|
31
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py,sha256=
|
31
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py,sha256=92aed53560c49e0ec3ef5e20ddb6c2096bb26078cc3e10b2f80a9dcef7a3e520,38937
|
32
32
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py,sha256=3a45c07d9d0f37baf409a48e2a1b577f28041c623c41f59ada1c87b94285ae3b,9537
|
33
33
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py,sha256=8016e4af0291a77484ce88d7d1ca06146b1229ae0e0a0f46e042faf75b456a8f,507
|
34
34
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py,sha256=324eeeaf8dd862f49c15bb7290d414e77ad51cdf532c2cfd74358783cdf654a5,9330
|
@@ -45,7 +45,7 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py,sha256=4f
|
|
45
45
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py,sha256=1f86cdf738e2f68586b0a4569bb1e40edddd85c529f511ef49945ddb7b61fab5,2648
|
46
46
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py,sha256=764e4fe979fb08d7821419a3cc5c3ae89a6664b626ef928259f8f175c939eaea,6334
|
47
47
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py,sha256=90aa8558467d7e469fe1a6c75372c113da403557715f03b522b2fab94b287c40,6320
|
48
|
-
lmnr/opentelemetry_lib/tracing/__init__.py,sha256=
|
48
|
+
lmnr/opentelemetry_lib/tracing/__init__.py,sha256=1019d25eab017547e914aa9703d4c4bc3fabb7ae0327f0dcb266c3d09f9a08e8,10207
|
49
49
|
lmnr/opentelemetry_lib/tracing/_instrument_initializers.py,sha256=a15a46a0515462319195a96f7cdb695e72a1559c3212964f5883ab824031bf70,15125
|
50
50
|
lmnr/opentelemetry_lib/tracing/attributes.py,sha256=32fa30565b977c2a92202dc2bf1ded583a81d02a6bf5ba52958f75a8be08cbbe,1497
|
51
51
|
lmnr/opentelemetry_lib/tracing/context.py,sha256=83f842be0fc29a96647cbf005c39ea761b0fb5913c4102f965411f47906a6135,4103
|
@@ -62,7 +62,7 @@ lmnr/sdk/browser/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b
|
|
62
62
|
lmnr/sdk/browser/browser_use_otel.py,sha256=37d26de1af37f76774af176cb226e0b04988fc3bf419a2fd899ad36e79562fad,5104
|
63
63
|
lmnr/sdk/browser/patchright_otel.py,sha256=9d22ab1f28f1eddbcfd0032a14fe306bfe00bfc7f11128cb99836c4dd15fb7c8,4800
|
64
64
|
lmnr/sdk/browser/playwright_otel.py,sha256=50c0a5a75155a3a7ff5db84790ffb409c9cbd0351eef212d83d923893730223b,9459
|
65
|
-
lmnr/sdk/browser/pw_utils.py,sha256=
|
65
|
+
lmnr/sdk/browser/pw_utils.py,sha256=e8b839d729bf4d80e07db0a8bb616b41da43219a019d2ce9596c1b6e9f848074,29061
|
66
66
|
lmnr/sdk/browser/recorder/record.umd.min.cjs,sha256=f09c09052c2fc474efb0405e63d8d26ed2184b994513ce8aee04efdac8be155d,181235
|
67
67
|
lmnr/sdk/browser/utils.py,sha256=4a668776d2938108d25fbcecd61c8e1710a4da3e56230d5fefca5964dd09e3c1,2371
|
68
68
|
lmnr/sdk/client/asynchronous/async_client.py,sha256=e8feae007506cd2e4b08e72706f5f1bb4ea54492b4aa6b68ef184a129de8f466,4948
|
@@ -82,15 +82,15 @@ lmnr/sdk/client/synchronous/resources/evaluators.py,sha256=3cd6a17e7a9cc0441c2d2
|
|
82
82
|
lmnr/sdk/client/synchronous/resources/tags.py,sha256=123deec43128662c21cb275b2df6a102372f875315b0bd36806555394c1d4b5b,2270
|
83
83
|
lmnr/sdk/client/synchronous/sync_client.py,sha256=0bebe88e3aed689505e9ed3d32036f76df4c3496e4d659162bd41abedc026f16,5299
|
84
84
|
lmnr/sdk/datasets.py,sha256=3fd851c5f97bf88eaa84b1451a053eaff23b4497cbb45eac2f9ea0e5f2886c00,1708
|
85
|
-
lmnr/sdk/decorators.py,sha256=
|
85
|
+
lmnr/sdk/decorators.py,sha256=2ccf9ecd9616ad1d52301febd8af630288ba63db2d36302236f606a460fc08ca,6516
|
86
86
|
lmnr/sdk/eval_control.py,sha256=291394ac385c653ae9b5167e871bebeb4fe8fc6b7ff2ed38e636f87015dcba86,184
|
87
87
|
lmnr/sdk/evaluations.py,sha256=b41f7737b084dc5b64b2952659b729622e0918fd492bfcddde7177d1a1c690ae,22572
|
88
|
-
lmnr/sdk/laminar.py,sha256=
|
88
|
+
lmnr/sdk/laminar.py,sha256=f9dbacc6549d19701db7a986cb29c82114b1a054900a282b1ae9dbe6a488129b,34974
|
89
89
|
lmnr/sdk/log.py,sha256=9edfd83263f0d4845b1b2d1beeae2b4ed3f8628de941f371a893d72b79c348d4,2213
|
90
|
-
lmnr/sdk/types.py,sha256=
|
90
|
+
lmnr/sdk/types.py,sha256=ded526d1289442672236265bd6cbb4381fc55d8ce0fce1d6e7b2541e54c758c4,12948
|
91
91
|
lmnr/sdk/utils.py,sha256=4beb884ae6fbbc7d8cf639b036b726ea6a2a658f0a6386faf5735a13d706a2d8,5039
|
92
|
-
lmnr/version.py,sha256=
|
93
|
-
lmnr-0.7.
|
94
|
-
lmnr-0.7.
|
95
|
-
lmnr-0.7.
|
96
|
-
lmnr-0.7.
|
92
|
+
lmnr/version.py,sha256=65bb6a01739adaee6189918df22c9e8df76cfc10339b704416f686014f066b2b,1321
|
93
|
+
lmnr-0.7.3.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
|
94
|
+
lmnr-0.7.3.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
|
95
|
+
lmnr-0.7.3.dist-info/METADATA,sha256=2952132a1ddd7d9e0f8c50fdd39e159174825b812b898bf48135c1c69ee7d5db,14196
|
96
|
+
lmnr-0.7.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|