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 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
- try:
246
- for event in complete_response_events:
247
- index = event.get("index")
248
- prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
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, f"{prefix}.finish_reason", event.get("finish_reason")
286
+ span,
287
+ f"{prefix}.tool_calls.0.name",
288
+ event.get("name"),
251
289
  )
252
- role = "thinking" if event.get("type") == "thinking" else "assistant"
253
- set_span_attribute(span, f"{prefix}.role", role)
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
- elif item.type == "content_block_delta" and item.delta.type in [
44
- "thinking_delta",
45
- "text_delta",
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 isinstance(tool_call, ChatCompletionMessageToolCall):
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:
@@ -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
- // Fallback to string method
290
- const jsonString = await stringifyNonBlocking(data);
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
- return new Promise((resolve, reject) => {
293
- if (!compressionWorker) {
294
- compressionWorker = createCompressionWorker();
295
- compressionWorker.onmessage = (e) => {
296
- const { id, success, data: result, error } = e.data;
297
- const promise = workerPromises.get(id);
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
- if (success) {
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.onerror = (error) => {
309
- console.error('Compression worker error:', error);
310
- cleanupWorker();
311
- };
312
- }
313
-
314
- const id = ++workerId;
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
- await window.lmnrSendEvents(events);
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
- try:
491
- if page.evaluate("""() => typeof window.lmnrSendEvents !== 'undefined'"""):
492
- return
493
- except Exception:
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
- if events and len(events) > 0:
499
- client._browser_events.send(session_id, trace_id, events)
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
- async def send_events_from_browser(events):
711
+ # Buffer for reassembling chunks
712
+ chunk_buffers = {}
713
+
714
+ async def send_events_from_browser(chunk):
537
715
  try:
538
- if events and len(events) > 0:
539
- await client._browser_events.send(session_id, trace_id, events)
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
- association_properties["tags"] = tags
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
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", tags)
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
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.7.1"
6
+ __version__ = "0.7.3"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmnr
3
- Version: 0.7.1
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.9
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.40.12 ; extra == 'alephalpha'
30
- Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.40.12 ; extra == 'all'
31
- Requires-Dist: opentelemetry-instrumentation-bedrock>=0.40.12 ; extra == 'all'
32
- Requires-Dist: opentelemetry-instrumentation-chromadb>=0.40.12 ; extra == 'all'
33
- Requires-Dist: opentelemetry-instrumentation-cohere>=0.40.12 ; extra == 'all'
34
- Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.12 ; extra == 'all'
35
- Requires-Dist: opentelemetry-instrumentation-google-generativeai<0.40.10 ; extra == 'all'
36
- Requires-Dist: opentelemetry-instrumentation-haystack>=0.40.12 ; extra == 'all'
37
- Requires-Dist: opentelemetry-instrumentation-lancedb>=0.40.12 ; extra == 'all'
38
- Requires-Dist: opentelemetry-instrumentation-langchain>=0.40.12 ; extra == 'all'
39
- Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.40.12 ; extra == 'all'
40
- Requires-Dist: opentelemetry-instrumentation-marqo>=0.40.12 ; extra == 'all'
41
- Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.12 ; extra == 'all'
42
- Requires-Dist: opentelemetry-instrumentation-milvus>=0.40.12 ; extra == 'all'
43
- Requires-Dist: opentelemetry-instrumentation-mistralai>=0.40.12 ; extra == 'all'
44
- Requires-Dist: opentelemetry-instrumentation-ollama>=0.40.12 ; extra == 'all'
45
- Requires-Dist: opentelemetry-instrumentation-pinecone>=0.40.12 ; extra == 'all'
46
- Requires-Dist: opentelemetry-instrumentation-qdrant>=0.40.12 ; extra == 'all'
47
- Requires-Dist: opentelemetry-instrumentation-replicate>=0.40.12 ; extra == 'all'
48
- Requires-Dist: opentelemetry-instrumentation-sagemaker>=0.40.12 ; extra == 'all'
49
- Requires-Dist: opentelemetry-instrumentation-together>=0.40.12 ; extra == 'all'
50
- Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.12 ; extra == 'all'
51
- Requires-Dist: opentelemetry-instrumentation-vertexai>=0.40.12 ; extra == 'all'
52
- Requires-Dist: opentelemetry-instrumentation-watsonx>=0.40.12 ; extra == 'all'
53
- Requires-Dist: opentelemetry-instrumentation-weaviate>=0.40.12 ; extra == 'all'
54
- Requires-Dist: opentelemetry-instrumentation-bedrock>=0.40.12 ; extra == 'bedrock'
55
- Requires-Dist: opentelemetry-instrumentation-chromadb>=0.40.12 ; extra == 'chromadb'
56
- Requires-Dist: opentelemetry-instrumentation-cohere>=0.40.12 ; extra == 'cohere'
57
- Requires-Dist: opentelemetry-instrumentation-crewai>=0.40.12 ; extra == 'crewai'
58
- Requires-Dist: opentelemetry-instrumentation-google-generativeai<0.40.10 ; extra == 'google-generativeai'
59
- Requires-Dist: opentelemetry-instrumentation-haystack>=0.40.12 ; extra == 'haystack'
60
- Requires-Dist: opentelemetry-instrumentation-lancedb>=0.40.12 ; extra == 'lancedb'
61
- Requires-Dist: opentelemetry-instrumentation-langchain>=0.40.12 ; extra == 'langchain'
62
- Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.40.12 ; extra == 'llamaindex'
63
- Requires-Dist: opentelemetry-instrumentation-marqo>=0.40.12 ; extra == 'marqo'
64
- Requires-Dist: opentelemetry-instrumentation-mcp>=0.40.12 ; extra == 'mcp'
65
- Requires-Dist: opentelemetry-instrumentation-milvus>=0.40.12 ; extra == 'milvus'
66
- Requires-Dist: opentelemetry-instrumentation-mistralai>=0.40.12 ; extra == 'mistralai'
67
- Requires-Dist: opentelemetry-instrumentation-ollama>=0.40.12 ; extra == 'ollama'
68
- Requires-Dist: opentelemetry-instrumentation-pinecone>=0.40.12 ; extra == 'pinecone'
69
- Requires-Dist: opentelemetry-instrumentation-qdrant>=0.40.12 ; extra == 'qdrant'
70
- Requires-Dist: opentelemetry-instrumentation-replicate>=0.40.12 ; extra == 'replicate'
71
- Requires-Dist: opentelemetry-instrumentation-sagemaker>=0.40.12 ; extra == 'sagemaker'
72
- Requires-Dist: opentelemetry-instrumentation-together>=0.40.12 ; extra == 'together'
73
- Requires-Dist: opentelemetry-instrumentation-transformers>=0.40.12 ; extra == 'transformers'
74
- Requires-Dist: opentelemetry-instrumentation-vertexai>=0.40.12 ; extra == 'vertexai'
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=47422a1fd58f5be3e7870ccb3ed7de4f1ac520d942e0c83cbcf903b0600a08f6,1278
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=6962aca915d485586ed814b9e799ced898594ac2bc6d35329405705b26eab861,2160
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=984b8f97eb31d4345ea4c52237451f791af189c4a94aaf2625edfd05107d8a6e,20696
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=9d0bb3825a6b5c28ac3778d49b7e67dc829530b2ebe34ef3f0e273f51caebcea,9422
13
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py,sha256=e999ad093275c5195b5d31dfea456726afd5f474cd779be7af892f54d7b416b8,10129
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=6042b3bcf94f38c90bdebaa2c6c9ac1a4723d1801b8e7c20cf8cc3926cef83ad,38657
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=a39e9a48f8a842ce7f7ec53364d793c1a303dcfd485aee7a72ade07d1b3635a2,9662
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=f6cb4b7cb59d6b488298f462cf88aa967a424f5bfdfd91c39998df930b212c2c,20420
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=0c6b95b92ec8023f28cd15ddc47849888fa91f2534d575f626e3557f5f0a0c02,6451
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=c38590ec5d65d5dedad37258f13f4f88f989e9ae10cbdb30bd1acdad5443e1d6,34427
88
+ lmnr/sdk/laminar.py,sha256=f9dbacc6549d19701db7a986cb29c82114b1a054900a282b1ae9dbe6a488129b,34974
89
89
  lmnr/sdk/log.py,sha256=9edfd83263f0d4845b1b2d1beeae2b4ed3f8628de941f371a893d72b79c348d4,2213
90
- lmnr/sdk/types.py,sha256=c4868d7d1df2fbd108fe5990900675bff2e595f6ff207afcf166ad4853f5eb0a,12670
90
+ lmnr/sdk/types.py,sha256=ded526d1289442672236265bd6cbb4381fc55d8ce0fce1d6e7b2541e54c758c4,12948
91
91
  lmnr/sdk/utils.py,sha256=4beb884ae6fbbc7d8cf639b036b726ea6a2a658f0a6386faf5735a13d706a2d8,5039
92
- lmnr/version.py,sha256=05f60eaaaa74f6161e0194247bb2244c9a3b5c1176c8902c6075ebef93276235,1321
93
- lmnr-0.7.1.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
94
- lmnr-0.7.1.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
95
- lmnr-0.7.1.dist-info/METADATA,sha256=847134a28ee45d56155835c491fbeb4a12cd6650709991550fff10976a328212,14473
96
- lmnr-0.7.1.dist-info/RECORD,,
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