lmnr 0.6.19__py3-none-any.whl → 0.6.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. lmnr/opentelemetry_lib/decorators/__init__.py +188 -138
  2. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +674 -0
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +256 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +485 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +3 -3
  18. lmnr/opentelemetry_lib/tracing/__init__.py +1 -1
  19. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
  20. lmnr/opentelemetry_lib/tracing/processor.py +1 -1
  21. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  22. lmnr/sdk/browser/browser_use_otel.py +4 -2
  23. lmnr/sdk/browser/patchright_otel.py +0 -26
  24. lmnr/sdk/browser/playwright_otel.py +51 -78
  25. lmnr/sdk/browser/pw_utils.py +359 -114
  26. lmnr/sdk/client/asynchronous/async_client.py +13 -0
  27. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -0
  28. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  29. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  30. lmnr/sdk/client/synchronous/resources/__init__.py +2 -1
  31. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  32. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  33. lmnr/sdk/client/synchronous/sync_client.py +14 -0
  34. lmnr/sdk/decorators.py +39 -4
  35. lmnr/sdk/evaluations.py +23 -9
  36. lmnr/sdk/laminar.py +75 -48
  37. lmnr/sdk/utils.py +23 -0
  38. lmnr/version.py +1 -1
  39. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/METADATA +8 -7
  40. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/RECORD +42 -25
  41. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/WHEEL +1 -1
  42. {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,5 @@
1
- import asyncio
2
1
  import logging
3
2
  import os
4
- import time
5
- import threading
6
3
 
7
4
  from opentelemetry import trace
8
5
 
@@ -40,25 +37,267 @@ with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
40
37
 
41
38
  INJECT_PLACEHOLDER = """
42
39
  () => {
43
- const BATCH_SIZE = 1000; // Maximum events to store in memory
40
+ const BATCH_TIMEOUT = 2000; // Send events after 2 seconds
44
41
 
45
- window.lmnrRrwebEventsBatch = new Set();
46
-
47
- // Utility function to compress individual event data
48
- async function compressEventData(data) {
42
+ window.lmnrRrwebEventsBatch = [];
43
+
44
+ // Create a Web Worker for heavy JSON processing with chunked processing
45
+ const createCompressionWorker = () => {
46
+ const workerCode = `
47
+ self.onmessage = async function(e) {
48
+ const { jsonString, buffer, id, useBuffer } = e.data;
49
+ try {
50
+ let uint8Array;
51
+
52
+ if (useBuffer && buffer) {
53
+ // Use transferred ArrayBuffer (no copying needed!)
54
+ uint8Array = new Uint8Array(buffer);
55
+ } else {
56
+ // Convert JSON string to bytes
57
+ const textEncoder = new TextEncoder();
58
+ uint8Array = textEncoder.encode(jsonString);
59
+ }
60
+
61
+ const compressionStream = new CompressionStream('gzip');
62
+ const writer = compressionStream.writable.getWriter();
63
+ const reader = compressionStream.readable.getReader();
64
+
65
+ writer.write(uint8Array);
66
+ writer.close();
67
+
68
+ const chunks = [];
69
+ let totalLength = 0;
70
+
71
+ while (true) {
72
+ const { done, value } = await reader.read();
73
+ if (done) break;
74
+ chunks.push(value);
75
+ totalLength += value.length;
76
+ }
77
+
78
+ const compressedData = new Uint8Array(totalLength);
79
+ let offset = 0;
80
+ for (const chunk of chunks) {
81
+ compressedData.set(chunk, offset);
82
+ offset += chunk.length;
83
+ }
84
+
85
+ self.postMessage({ id, success: true, data: compressedData });
86
+ } catch (error) {
87
+ self.postMessage({ id, success: false, error: error.message });
88
+ }
89
+ };
90
+ `;
91
+
92
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
93
+ return new Worker(URL.createObjectURL(blob));
94
+ };
95
+
96
+ let compressionWorker = null;
97
+ let workerPromises = new Map();
98
+ let workerId = 0;
99
+
100
+ // Non-blocking JSON.stringify using chunked processing
101
+ function stringifyNonBlocking(obj, chunkSize = 10000) {
102
+ return new Promise((resolve, reject) => {
103
+ try {
104
+ // For very large objects, we need to be more careful
105
+ // Use requestIdleCallback if available, otherwise setTimeout
106
+ const scheduleWork = window.requestIdleCallback ||
107
+ ((cb) => setTimeout(cb, 0));
108
+
109
+ let result = '';
110
+ let keys = [];
111
+ let keyIndex = 0;
112
+
113
+ // Pre-process to get all keys if it's an object
114
+ if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
115
+ keys = Object.keys(obj);
116
+ }
117
+
118
+ function processChunk() {
119
+ try {
120
+ if (Array.isArray(obj) || typeof obj !== 'object' || obj === null) {
121
+ // For arrays and primitives, just stringify directly
122
+ result = JSON.stringify(obj);
123
+ resolve(result);
124
+ return;
125
+ }
126
+
127
+ // For objects, process in chunks
128
+ const endIndex = Math.min(keyIndex + chunkSize, keys.length);
129
+
130
+ if (keyIndex === 0) {
131
+ result = '{';
132
+ }
133
+
134
+ for (let i = keyIndex; i < endIndex; i++) {
135
+ const key = keys[i];
136
+ const value = obj[key];
137
+
138
+ if (i > 0) result += ',';
139
+ result += JSON.stringify(key) + ':' + JSON.stringify(value);
140
+ }
141
+
142
+ keyIndex = endIndex;
143
+
144
+ if (keyIndex >= keys.length) {
145
+ result += '}';
146
+ resolve(result);
147
+ } else {
148
+ // Schedule next chunk
149
+ scheduleWork(processChunk);
150
+ }
151
+ } catch (error) {
152
+ reject(error);
153
+ }
154
+ }
155
+
156
+ processChunk();
157
+ } catch (error) {
158
+ reject(error);
159
+ }
160
+ });
161
+ }
162
+
163
+ // Fast compression for small objects (main thread)
164
+ async function compressSmallObject(data) {
49
165
  const jsonString = JSON.stringify(data);
50
- const blob = new Blob([jsonString], { type: 'application/json' });
51
- const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
52
- const compressedResponse = new Response(compressedStream);
53
- const compressedData = await compressedResponse.arrayBuffer();
54
- return Array.from(new Uint8Array(compressedData));
166
+ const textEncoder = new TextEncoder();
167
+ const uint8Array = textEncoder.encode(jsonString);
168
+
169
+ const compressionStream = new CompressionStream('gzip');
170
+ const writer = compressionStream.writable.getWriter();
171
+ const reader = compressionStream.readable.getReader();
172
+
173
+ writer.write(uint8Array);
174
+ writer.close();
175
+
176
+ const chunks = [];
177
+ let totalLength = 0;
178
+
179
+ while (true) {
180
+ const { done, value } = await reader.read();
181
+ if (done) break;
182
+ chunks.push(value);
183
+ totalLength += value.length;
184
+ }
185
+
186
+ const compressedData = new Uint8Array(totalLength);
187
+ let offset = 0;
188
+ for (const chunk of chunks) {
189
+ compressedData.set(chunk, offset);
190
+ offset += chunk.length;
191
+ }
192
+
193
+ return compressedData;
55
194
  }
56
195
 
57
- window.lmnrGetAndClearEvents = () => {
58
- const events = window.lmnrRrwebEventsBatch;
59
- window.lmnrRrwebEventsBatch = new Set();
60
- return Array.from(events);
61
- };
196
+ // Alternative: Use transferable objects for maximum efficiency
197
+ async function compressLargeObjectTransferable(data) {
198
+ try {
199
+ // Stringify on main thread but non-blocking
200
+ const jsonString = await stringifyNonBlocking(data);
201
+
202
+ // Convert to ArrayBuffer (transferable)
203
+ const encoder = new TextEncoder();
204
+ const uint8Array = encoder.encode(jsonString);
205
+ const buffer = uint8Array.buffer; // Use the original buffer for transfer
206
+
207
+ return new Promise((resolve, reject) => {
208
+ if (!compressionWorker) {
209
+ compressionWorker = createCompressionWorker();
210
+ compressionWorker.onmessage = (e) => {
211
+ const { id, success, data: result, error } = e.data;
212
+ const promise = workerPromises.get(id);
213
+ if (promise) {
214
+ workerPromises.delete(id);
215
+ if (success) {
216
+ promise.resolve(result);
217
+ } else {
218
+ promise.reject(new Error(error));
219
+ }
220
+ }
221
+ };
222
+ }
223
+
224
+ const id = ++workerId;
225
+ workerPromises.set(id, { resolve, reject });
226
+
227
+ // Transfer the ArrayBuffer (no copying!)
228
+ compressionWorker.postMessage({
229
+ buffer,
230
+ id,
231
+ useBuffer: true
232
+ }, [buffer]);
233
+ });
234
+ } catch (error) {
235
+ console.warn('Failed to process large object with transferable:', error);
236
+ return compressSmallObject(data);
237
+ }
238
+ }
239
+
240
+ // Worker-based compression for large objects
241
+ async function compressLargeObject(data, isLarge = true) {
242
+ try {
243
+ // Use transferable objects for better performance
244
+ return await compressLargeObjectTransferable(data);
245
+ } catch (error) {
246
+ console.warn('Transferable failed, falling back to string method:', error);
247
+ // Fallback to string method
248
+ const jsonString = await stringifyNonBlocking(data);
249
+
250
+ return new Promise((resolve, reject) => {
251
+ if (!compressionWorker) {
252
+ compressionWorker = createCompressionWorker();
253
+ compressionWorker.onmessage = (e) => {
254
+ const { id, success, data: result, error } = e.data;
255
+ const promise = workerPromises.get(id);
256
+ if (promise) {
257
+ workerPromises.delete(id);
258
+ if (success) {
259
+ promise.resolve(result);
260
+ } else {
261
+ promise.reject(new Error(error));
262
+ }
263
+ }
264
+ };
265
+ }
266
+
267
+ const id = ++workerId;
268
+ workerPromises.set(id, { resolve, reject });
269
+ compressionWorker.postMessage({ jsonString, id });
270
+ });
271
+ }
272
+ }
273
+
274
+ function isLargeEvent(type) {
275
+ const LARGE_EVENT_TYPES = [
276
+ 2, // FullSnapshot
277
+ 3, // IncrementalSnapshot
278
+ ];
279
+
280
+ if (LARGE_EVENT_TYPES.includes(type)) {
281
+ return true;
282
+ }
283
+
284
+ return false;
285
+ }
286
+
287
+ async function sendBatchIfReady() {
288
+ if (window.lmnrRrwebEventsBatch.length > 0 && typeof window.lmnrSendEvents === 'function') {
289
+ const events = window.lmnrRrwebEventsBatch;
290
+ window.lmnrRrwebEventsBatch = [];
291
+
292
+ try {
293
+ await window.lmnrSendEvents(events);
294
+ } catch (error) {
295
+ console.error('Failed to send events:', error);
296
+ }
297
+ }
298
+ }
299
+
300
+ setInterval(sendBatchIfReady, BATCH_TIMEOUT);
62
301
 
63
302
  // Add heartbeat events
64
303
  setInterval(async () => {
@@ -66,17 +305,24 @@ INJECT_PLACEHOLDER = """
66
305
  title: document.title,
67
306
  url: document.URL,
68
307
  })
69
-
70
308
  }, 1000);
71
309
 
72
310
  window.lmnrRrweb.record({
73
- async emit(event) {
74
- // Compress the data field
75
- const compressedEvent = {
76
- ...event,
77
- data: await compressEventData(event.data)
78
- };
79
- window.lmnrRrwebEventsBatch.add(compressedEvent);
311
+ async emit(event) {
312
+ try {
313
+ const isLarge = isLargeEvent(event.type);
314
+ const compressedResult = isLarge ?
315
+ await compressLargeObject(event.data, true) :
316
+ await compressSmallObject(event.data);
317
+
318
+ const eventToSend = {
319
+ ...event,
320
+ data: compressedResult,
321
+ };
322
+ window.lmnrRrwebEventsBatch.push(eventToSend);
323
+ } catch (error) {
324
+ console.warn('Failed to push event to batch', error);
325
+ }
80
326
  },
81
327
  recordCanvas: true,
82
328
  collectFonts: true,
@@ -108,16 +354,10 @@ async def send_events_async(
108
354
 
109
355
  await client._browser_events.send(session_id, trace_id, events)
110
356
  except Exception as e:
111
- if str(e).startswith("Page.evaluate: Execution context was destroyed"):
112
- await inject_session_recorder_async(page)
113
- await send_events_async(page, session_id, trace_id, client)
114
- else:
115
- # silence the error if the page has been closed, not an issue
116
- if (
117
- "Page.evaluate: Target page, context or browser has been closed"
118
- not in str(e)
119
- ):
120
- logger.warning(f"Could not send events: {e}")
357
+ if "Page.evaluate: Target page, context or browser has been closed" not in str(
358
+ e
359
+ ):
360
+ logger.debug(f"Could not send events: {e}")
121
361
 
122
362
 
123
363
  def send_events_sync(
@@ -141,23 +381,14 @@ def send_events_sync(
141
381
  client._browser_events.send(session_id, trace_id, events)
142
382
 
143
383
  except Exception as e:
144
- if str(e).startswith("Page.evaluate: Execution context was destroyed"):
145
- inject_session_recorder_sync(page)
146
- send_events_sync(page, session_id, trace_id, client)
147
- else:
148
- # silence the error if the page has been closed, not an issue
149
- if (
150
- "Page.evaluate: Target page, context or browser has been closed"
151
- not in str(e)
152
- ):
153
- logger.warning(f"Could not send events: {e}")
384
+ if "Page.evaluate: Target page, context or browser has been closed" not in str(
385
+ e
386
+ ):
387
+ logger.debug(f"Could not send events: {e}")
154
388
 
155
389
 
156
390
  def inject_session_recorder_sync(page: SyncPage):
157
391
  try:
158
- page.wait_for_load_state("domcontentloaded")
159
-
160
- # Wrap the evaluate call in a try-catch
161
392
  try:
162
393
  is_loaded = page.evaluate(
163
394
  """() => typeof window.lmnrRrweb !== 'undefined'"""
@@ -194,9 +425,6 @@ def inject_session_recorder_sync(page: SyncPage):
194
425
 
195
426
  async def inject_session_recorder_async(page: Page):
196
427
  try:
197
- await page.wait_for_load_state("domcontentloaded")
198
-
199
- # Wrap the evaluate call in a try-catch
200
428
  try:
201
429
  is_loaded = await page.evaluate(
202
430
  """() => typeof window.lmnrRrweb !== 'undefined'"""
@@ -232,27 +460,16 @@ async def inject_session_recorder_async(page: Page):
232
460
 
233
461
 
234
462
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
235
- def handle_navigation_sync(page: SyncPage, session_id: str, client: LaminarClient):
463
+ def start_recording_events_sync(page: SyncPage, session_id: str, client: LaminarClient):
236
464
  span = trace.get_current_span()
237
465
  trace_id = format(span.get_span_context().trace_id, "032x")
238
466
  span.set_attribute("lmnr.internal.has_browser_session", True)
239
- original_bring_to_front = page.bring_to_front
240
-
241
- def bring_to_front():
242
- original_bring_to_front()
243
- page.evaluate(
244
- """() => {
245
- if (window.lmnrRrweb) {
246
- try {
247
- window.lmnrRrweb.record.takeFullSnapshot();
248
- } catch (e) {
249
- console.error("Error taking full snapshot:", e);
250
- }
251
- }
252
- }"""
253
- )
254
467
 
255
- page.bring_to_front = bring_to_front
468
+ try:
469
+ if page.evaluate("""() => typeof window.lmnrSendEvents !== 'undefined'"""):
470
+ return
471
+ except Exception:
472
+ pass
256
473
 
257
474
  def on_load():
258
475
  try:
@@ -260,79 +477,107 @@ def handle_navigation_sync(page: SyncPage, session_id: str, client: LaminarClien
260
477
  except Exception as e:
261
478
  logger.error(f"Error in on_load handler: {e}")
262
479
 
263
- def collection_loop():
264
- while not page.is_closed(): # Stop when page closes
265
- send_events_sync(page, session_id, trace_id, client)
266
- time.sleep(2)
267
-
268
- thread = threading.Thread(target=collection_loop, daemon=True)
269
- thread.start()
270
-
271
480
  def on_close():
272
481
  try:
273
482
  send_events_sync(page, session_id, trace_id, client)
274
- thread.join()
275
483
  except Exception:
276
484
  pass
277
485
 
278
486
  page.on("load", on_load)
279
487
  page.on("close", on_close)
488
+
280
489
  inject_session_recorder_sync(page)
281
490
 
491
+ # Expose function to browser so it can call us when events are ready
492
+ def send_events_from_browser(events):
493
+ try:
494
+ if events and len(events) > 0:
495
+ client._browser_events.send(session_id, trace_id, events)
496
+ except Exception as e:
497
+ logger.debug(f"Could not send events: {e}")
498
+
499
+ try:
500
+ page.expose_function("lmnrSendEvents", send_events_from_browser)
501
+ except Exception as e:
502
+ logger.debug(f"Could not expose function: {e}")
503
+
282
504
 
283
505
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
284
- async def handle_navigation_async(
506
+ async def start_recording_events_async(
285
507
  page: Page, session_id: str, client: AsyncLaminarClient
286
508
  ):
287
-
288
509
  span = trace.get_current_span()
289
510
  trace_id = format(span.get_span_context().trace_id, "032x")
290
511
  span.set_attribute("lmnr.internal.has_browser_session", True)
291
512
 
292
- async def collection_loop():
293
- try:
294
- while not page.is_closed(): # Stop when page closes
295
- await send_events_async(page, session_id, trace_id, client)
296
- await asyncio.sleep(2)
297
- logger.info("Event collection stopped")
298
- except Exception as e:
299
- logger.error(f"Event collection stopped: {e}")
300
-
301
- # Create and store task
302
- task = asyncio.create_task(collection_loop())
513
+ try:
514
+ if await page.evaluate(
515
+ """() => typeof window.lmnrSendEvents !== 'undefined'"""
516
+ ):
517
+ return
518
+ except Exception:
519
+ pass
303
520
 
304
- async def on_load():
521
+ async def on_load(p):
305
522
  try:
306
- await inject_session_recorder_async(page)
523
+ await inject_session_recorder_async(p)
307
524
  except Exception as e:
308
525
  logger.error(f"Error in on_load handler: {e}")
309
526
 
310
- async def on_close():
527
+ async def on_close(p):
311
528
  try:
312
- task.cancel()
313
- await send_events_async(page, session_id, trace_id, client)
529
+ # Send any remaining events before closing
530
+ await send_events_async(p, session_id, trace_id, client)
314
531
  except Exception:
315
532
  pass
316
533
 
317
- page.on("load", lambda: asyncio.create_task(on_load()))
318
- page.on("close", lambda: asyncio.create_task(on_close()))
534
+ page.on("load", on_load)
535
+ page.on("close", on_close)
319
536
 
320
- original_bring_to_front = page.bring_to_front
537
+ await inject_session_recorder_async(page)
321
538
 
322
- async def bring_to_front():
323
- await original_bring_to_front()
539
+ async def send_events_from_browser(events):
540
+ try:
541
+ if events and len(events) > 0:
542
+ await client._browser_events.send(session_id, trace_id, events)
543
+ except Exception as e:
544
+ logger.debug(f"Could not send events: {e}")
324
545
 
325
- await page.evaluate(
326
- """() => {
327
- if (window.lmnrRrweb) {
328
- try {
329
- window.lmnrRrweb.record.takeFullSnapshot();
330
- } catch (e) {
331
- console.error("Error taking full snapshot:", e);
332
- }
546
+ try:
547
+ await page.expose_function("lmnrSendEvents", send_events_from_browser)
548
+ except Exception as e:
549
+ logger.debug(f"Could not expose function: {e}")
550
+
551
+
552
+ def take_full_snapshot(page: Page):
553
+ return page.evaluate(
554
+ """() => {
555
+ if (window.lmnrRrweb) {
556
+ try {
557
+ window.lmnrRrweb.record.takeFullSnapshot();
558
+ return true;
559
+ } catch (e) {
560
+ console.error("Error taking full snapshot:", e);
561
+ return false;
333
562
  }
334
- }"""
335
- )
336
-
337
- page.bring_to_front = bring_to_front
338
- await inject_session_recorder_async(page)
563
+ }
564
+ return false;
565
+ }"""
566
+ )
567
+
568
+
569
+ async def take_full_snapshot_async(page: Page):
570
+ return await page.evaluate(
571
+ """() => {
572
+ if (window.lmnrRrweb) {
573
+ try {
574
+ window.lmnrRrweb.record.takeFullSnapshot();
575
+ return true;
576
+ } catch (e) {
577
+ console.error("Error taking full snapshot:", e);
578
+ return false;
579
+ }
580
+ }
581
+ return false;
582
+ }"""
583
+ )
@@ -12,6 +12,7 @@ from lmnr.sdk.client.asynchronous.resources import (
12
12
  AsyncBrowserEvents,
13
13
  AsyncEvals,
14
14
  AsyncTags,
15
+ AsyncEvaluators
15
16
  )
16
17
  from lmnr.sdk.utils import from_env
17
18
 
@@ -74,6 +75,9 @@ class AsyncLaminarClient:
74
75
  self.__evals = AsyncEvals(
75
76
  self.__client, self.__base_url, self.__project_api_key
76
77
  )
78
+ self.__evaluators = AsyncEvaluators(
79
+ self.__client, self.__base_url, self.__project_api_key
80
+ )
77
81
  self.__browser_events = AsyncBrowserEvents(
78
82
  self.__client, self.__base_url, self.__project_api_key
79
83
  )
@@ -115,6 +119,15 @@ class AsyncLaminarClient:
115
119
  """
116
120
  return self.__tags
117
121
 
122
+ @property
123
+ def evaluators(self) -> AsyncEvaluators:
124
+ """Get the Evaluators resource.
125
+
126
+ Returns:
127
+ Evaluators: The Evaluators resource instance.
128
+ """
129
+ return self.__evaluators
130
+
118
131
  def is_closed(self) -> bool:
119
132
  return self.__client.is_closed
120
133
 
@@ -2,10 +2,12 @@ from lmnr.sdk.client.asynchronous.resources.agent import AsyncAgent
2
2
  from lmnr.sdk.client.asynchronous.resources.browser_events import AsyncBrowserEvents
3
3
  from lmnr.sdk.client.asynchronous.resources.evals import AsyncEvals
4
4
  from lmnr.sdk.client.asynchronous.resources.tags import AsyncTags
5
+ from lmnr.sdk.client.asynchronous.resources.evaluators import AsyncEvaluators
5
6
 
6
7
  __all__ = [
7
8
  "AsyncAgent",
8
9
  "AsyncEvals",
9
10
  "AsyncBrowserEvents",
10
11
  "AsyncTags",
12
+ "AsyncEvaluators"
11
13
  ]