lmnr 0.5.1a0__py3-none-any.whl → 0.5.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 +2 -10
- lmnr/cli.py +10 -8
- lmnr/{openllmetry_sdk → opentelemetry_lib}/__init__.py +8 -36
- lmnr/{openllmetry_sdk → opentelemetry_lib}/decorators/base.py +27 -20
- lmnr/{openllmetry_sdk → opentelemetry_lib}/instruments.py +2 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +454 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +216 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +1 -0
- lmnr/opentelemetry_lib/tracing/context_manager.py +13 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/tracing.py +253 -257
- lmnr/sdk/browser/browser_use_otel.py +20 -3
- lmnr/sdk/browser/patchright_otel.py +177 -0
- lmnr/sdk/browser/playwright_otel.py +55 -62
- lmnr/sdk/browser/pw_utils.py +122 -116
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +98 -0
- lmnr/sdk/client/asynchronous/async_client.py +0 -34
- lmnr/sdk/client/asynchronous/resources/__init__.py +0 -4
- lmnr/sdk/client/asynchronous/resources/agent.py +115 -6
- lmnr/sdk/client/synchronous/resources/__init__.py +1 -3
- lmnr/sdk/client/synchronous/resources/agent.py +112 -6
- lmnr/sdk/client/synchronous/sync_client.py +0 -36
- lmnr/sdk/decorators.py +19 -5
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +8 -14
- lmnr/sdk/laminar.py +10 -10
- lmnr/sdk/types.py +86 -170
- lmnr/sdk/utils.py +8 -1
- lmnr/version.py +1 -1
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.3.dist-info}/METADATA +58 -58
- lmnr-0.5.3.dist-info/RECORD +55 -0
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.3.dist-info}/WHEEL +1 -1
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -0
- lmnr/sdk/browser/rrweb/rrweb.min.js +0 -18
- lmnr/sdk/client/asynchronous/resources/pipeline.py +0 -89
- lmnr/sdk/client/asynchronous/resources/semantic_search.py +0 -60
- lmnr/sdk/client/synchronous/resources/pipeline.py +0 -89
- lmnr/sdk/client/synchronous/resources/semantic_search.py +0 -60
- lmnr-0.5.1a0.dist-info/RECORD +0 -54
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/config/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/decorators/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/content_allow_list.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/in_memory_span_exporter.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/package_check.py +0 -0
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.3.dist-info}/LICENSE +0 -0
- {lmnr-0.5.1a0.dist-info → lmnr-0.5.3.dist-info}/entry_points.txt +0 -0
lmnr/sdk/browser/pw_utils.py
CHANGED
@@ -3,7 +3,6 @@ import logging
|
|
3
3
|
import os
|
4
4
|
import time
|
5
5
|
import threading
|
6
|
-
from typing import Optional
|
7
6
|
|
8
7
|
from opentelemetry import trace
|
9
8
|
|
@@ -24,23 +23,29 @@ except ImportError as e:
|
|
24
23
|
|
25
24
|
logger = logging.getLogger(__name__)
|
26
25
|
|
27
|
-
# Track pages we've already instrumented to avoid double-instrumentation
|
28
|
-
instrumented_pages = set()
|
29
|
-
async_instrumented_pages = set()
|
30
|
-
client: Optional[LaminarClient] = None
|
31
|
-
async_client: Optional[AsyncLaminarClient] = None
|
32
|
-
|
33
|
-
|
34
26
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
35
|
-
with open(os.path.join(current_dir, "rrweb", "rrweb.min.
|
27
|
+
with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
|
36
28
|
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
37
29
|
|
38
30
|
INJECT_PLACEHOLDER = """
|
39
31
|
() => {
|
40
32
|
const BATCH_SIZE = 1000; // Maximum events to store in memory
|
41
|
-
|
33
|
+
|
42
34
|
window.lmnrRrwebEventsBatch = new Set();
|
43
35
|
|
36
|
+
// Track page focus state
|
37
|
+
window.lmnrPageIsFocused = true;
|
38
|
+
|
39
|
+
window.addEventListener('blur', () => {
|
40
|
+
window.lmnrPageIsFocused = false;
|
41
|
+
console.log('Page lost focus');
|
42
|
+
});
|
43
|
+
|
44
|
+
window.addEventListener('focus', () => {
|
45
|
+
window.lmnrPageIsFocused = true;
|
46
|
+
console.log('Page gained focus');
|
47
|
+
});
|
48
|
+
|
44
49
|
// Utility function to compress individual event data
|
45
50
|
async function compressEventData(data) {
|
46
51
|
const jsonString = JSON.stringify(data);
|
@@ -50,7 +55,7 @@ INJECT_PLACEHOLDER = """
|
|
50
55
|
const compressedData = await compressedResponse.arrayBuffer();
|
51
56
|
return Array.from(new Uint8Array(compressedData));
|
52
57
|
}
|
53
|
-
|
58
|
+
|
54
59
|
window.lmnrGetAndClearEvents = () => {
|
55
60
|
const events = window.lmnrRrwebEventsBatch;
|
56
61
|
window.lmnrRrwebEventsBatch = new Set();
|
@@ -59,26 +64,24 @@ INJECT_PLACEHOLDER = """
|
|
59
64
|
|
60
65
|
// Add heartbeat events
|
61
66
|
setInterval(async () => {
|
62
|
-
|
63
|
-
|
64
|
-
data: await compressEventData({ source: 'heartbeat' }),
|
65
|
-
timestamp: Date.now()
|
66
|
-
};
|
67
|
-
|
68
|
-
window.lmnrRrwebEventsBatch.add(heartbeat);
|
69
|
-
|
70
|
-
// Prevent memory issues by limiting batch size
|
71
|
-
if (window.lmnrRrwebEventsBatch.size > BATCH_SIZE) {
|
72
|
-
window.lmnrRrwebEventsBatch = new Set(Array.from(window.lmnrRrwebEventsBatch).slice(-BATCH_SIZE));
|
67
|
+
if (!window.lmnrPageIsFocused) {
|
68
|
+
return;
|
73
69
|
}
|
70
|
+
|
71
|
+
window.lmnrRrweb.record.addCustomEvent('heartbeat', {
|
72
|
+
title: document.title,
|
73
|
+
url: document.URL,
|
74
|
+
})
|
75
|
+
|
74
76
|
}, 1000);
|
75
77
|
|
76
78
|
window.lmnrRrweb.record({
|
77
79
|
async emit(event) {
|
78
|
-
// Ignore events
|
79
|
-
if (
|
80
|
+
// Ignore events when page is not focused
|
81
|
+
if (!window.lmnrPageIsFocused) {
|
80
82
|
return;
|
81
83
|
}
|
84
|
+
|
82
85
|
// Compress the data field
|
83
86
|
const compressedEvent = {
|
84
87
|
...event,
|
@@ -97,22 +100,26 @@ async def send_events_async(
|
|
97
100
|
"""Fetch events from the page and send them to the server"""
|
98
101
|
try:
|
99
102
|
# Check if function exists first
|
100
|
-
|
101
|
-
|
102
|
-
(
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
103
|
+
events = await page.evaluate("""
|
104
|
+
() => {
|
105
|
+
if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
|
106
|
+
return [];
|
107
|
+
}
|
108
|
+
return window.lmnrGetAndClearEvents();
|
109
|
+
}
|
110
|
+
""")
|
107
111
|
|
108
|
-
events = await page.evaluate("window.lmnrGetAndClearEvents()")
|
109
112
|
if not events or len(events) == 0:
|
110
113
|
return
|
111
114
|
|
112
115
|
await client._browser_events.send(session_id, trace_id, events)
|
113
|
-
|
114
116
|
except Exception as e:
|
115
|
-
|
117
|
+
if str(e).startswith("Page.evaluate: Execution context was destroyed"):
|
118
|
+
logger.info("Execution context was destroyed, injecting rrweb again")
|
119
|
+
await inject_rrweb_async(page)
|
120
|
+
await send_events_async(page, session_id, trace_id, client)
|
121
|
+
else:
|
122
|
+
logger.debug(f"Could not send events: {e}")
|
116
123
|
|
117
124
|
|
118
125
|
def send_events_sync(
|
@@ -120,23 +127,26 @@ def send_events_sync(
|
|
120
127
|
):
|
121
128
|
"""Synchronous version of send_events"""
|
122
129
|
try:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
events = page.evaluate("window.lmnrGetAndClearEvents()")
|
130
|
+
events = page.evaluate("""
|
131
|
+
() => {
|
132
|
+
if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
|
133
|
+
return [];
|
134
|
+
}
|
135
|
+
return window.lmnrGetAndClearEvents();
|
136
|
+
}
|
137
|
+
""")
|
133
138
|
if not events or len(events) == 0:
|
134
139
|
return
|
135
140
|
|
136
141
|
client._browser_events.send(session_id, trace_id, events)
|
137
142
|
|
138
143
|
except Exception as e:
|
139
|
-
|
144
|
+
if str(e).startswith("Page.evaluate: Execution context was destroyed"):
|
145
|
+
logger.info("Execution context was destroyed, injecting rrweb again")
|
146
|
+
inject_rrweb_sync(page)
|
147
|
+
send_events_sync(page, session_id, trace_id, client)
|
148
|
+
else:
|
149
|
+
logger.debug(f"Could not send events: {e}")
|
140
150
|
|
141
151
|
|
142
152
|
def inject_rrweb_sync(page: SyncPage):
|
@@ -157,10 +167,6 @@ def inject_rrweb_sync(page: SyncPage):
|
|
157
167
|
def load_rrweb():
|
158
168
|
try:
|
159
169
|
page.evaluate(RRWEB_CONTENT)
|
160
|
-
page.wait_for_function(
|
161
|
-
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
162
|
-
timeout=5000,
|
163
|
-
)
|
164
170
|
return True
|
165
171
|
except Exception as e:
|
166
172
|
logger.debug(f"Failed to load rrweb: {e}")
|
@@ -198,10 +204,6 @@ async def inject_rrweb_async(page: Page):
|
|
198
204
|
async def load_rrweb():
|
199
205
|
try:
|
200
206
|
await page.evaluate(RRWEB_CONTENT)
|
201
|
-
await page.wait_for_function(
|
202
|
-
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
203
|
-
timeout=5000,
|
204
|
-
)
|
205
207
|
return True
|
206
208
|
except Exception as e:
|
207
209
|
logger.debug(f"Failed to load rrweb: {e}")
|
@@ -223,27 +225,26 @@ async def inject_rrweb_async(page: Page):
|
|
223
225
|
|
224
226
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
225
227
|
def handle_navigation_sync(
|
226
|
-
page: SyncPage,
|
227
|
-
session_id: str,
|
228
|
-
trace_id: str,
|
229
|
-
project_api_key: Optional[str] = None,
|
230
|
-
base_http_url: Optional[str] = None,
|
228
|
+
page: SyncPage, session_id: str, trace_id: str, client: LaminarClient
|
231
229
|
):
|
232
|
-
global client
|
233
|
-
if client is None:
|
234
|
-
client = LaminarClient(base_url=base_http_url, project_api_key=project_api_key)
|
235
|
-
if (base_http_url is not None and base_http_url != client.base_url) or (
|
236
|
-
project_api_key is not None and project_api_key != client.project_api_key
|
237
|
-
):
|
238
|
-
if client is not None:
|
239
|
-
client.close()
|
240
|
-
client = LaminarClient(base_url=base_http_url, project_api_key=project_api_key)
|
241
230
|
trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
231
|
+
original_bring_to_front = page.bring_to_front
|
232
|
+
|
233
|
+
def bring_to_front():
|
234
|
+
original_bring_to_front()
|
235
|
+
page.evaluate(
|
236
|
+
"""() => {
|
237
|
+
if (window.lmnrRrweb) {
|
238
|
+
try {
|
239
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
240
|
+
} catch (e) {
|
241
|
+
console.error("Error taking full snapshot:", e);
|
242
|
+
}
|
243
|
+
}
|
244
|
+
}"""
|
245
|
+
)
|
246
|
+
|
247
|
+
page.bring_to_front = bring_to_front
|
247
248
|
|
248
249
|
def on_load():
|
249
250
|
try:
|
@@ -251,48 +252,43 @@ def handle_navigation_sync(
|
|
251
252
|
except Exception as e:
|
252
253
|
logger.error(f"Error in on_load handler: {e}")
|
253
254
|
|
254
|
-
page.on("load", on_load)
|
255
|
-
inject_rrweb_sync(page)
|
256
|
-
|
257
255
|
def collection_loop():
|
258
|
-
while not page.is_closed():
|
256
|
+
while not page.is_closed(): # Stop when page closes
|
259
257
|
send_events_sync(page, session_id, trace_id, client)
|
260
258
|
time.sleep(2)
|
261
259
|
|
262
|
-
if page_id in instrumented_pages:
|
263
|
-
instrumented_pages.remove(page_id)
|
264
|
-
|
265
260
|
thread = threading.Thread(target=collection_loop, daemon=True)
|
266
261
|
thread.start()
|
267
262
|
|
263
|
+
def on_close():
|
264
|
+
try:
|
265
|
+
send_events_sync(page, session_id, trace_id, client)
|
266
|
+
thread.join()
|
267
|
+
except Exception:
|
268
|
+
pass
|
269
|
+
|
270
|
+
page.on("load", on_load)
|
271
|
+
page.on("close", on_close)
|
272
|
+
inject_rrweb_sync(page)
|
273
|
+
|
268
274
|
|
269
275
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
270
276
|
async def handle_navigation_async(
|
271
|
-
page: Page,
|
272
|
-
session_id: str,
|
273
|
-
trace_id: str,
|
274
|
-
project_api_key: Optional[str] = None,
|
275
|
-
base_http_url: Optional[str] = None,
|
277
|
+
page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
|
276
278
|
):
|
277
|
-
global async_client
|
278
|
-
if async_client is None:
|
279
|
-
async_client = AsyncLaminarClient(
|
280
|
-
base_url=base_http_url, project_api_key=project_api_key
|
281
|
-
)
|
282
|
-
if (base_http_url is not None and base_http_url != async_client.base_url) or (
|
283
|
-
project_api_key is not None and project_api_key != async_client.project_api_key
|
284
|
-
):
|
285
|
-
if async_client is not None:
|
286
|
-
await async_client.close()
|
287
|
-
async_client = AsyncLaminarClient(
|
288
|
-
base_url=base_http_url, project_api_key=project_api_key
|
289
|
-
)
|
290
279
|
trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
280
|
+
|
281
|
+
async def collection_loop():
|
282
|
+
try:
|
283
|
+
while not page.is_closed(): # Stop when page closes
|
284
|
+
await send_events_async(page, session_id, trace_id, client)
|
285
|
+
await asyncio.sleep(2)
|
286
|
+
logger.info("Event collection stopped")
|
287
|
+
except Exception as e:
|
288
|
+
logger.error(f"Event collection stopped: {e}")
|
289
|
+
|
290
|
+
# Create and store task
|
291
|
+
task = asyncio.create_task(collection_loop())
|
296
292
|
|
297
293
|
async def on_load():
|
298
294
|
try:
|
@@ -300,22 +296,32 @@ async def handle_navigation_async(
|
|
300
296
|
except Exception as e:
|
301
297
|
logger.error(f"Error in on_load handler: {e}")
|
302
298
|
|
299
|
+
async def on_close():
|
300
|
+
try:
|
301
|
+
task.cancel()
|
302
|
+
await send_events_async(page, session_id, trace_id, client)
|
303
|
+
except Exception:
|
304
|
+
pass
|
305
|
+
|
303
306
|
page.on("load", lambda: asyncio.create_task(on_load()))
|
304
|
-
|
307
|
+
page.on("close", lambda: asyncio.create_task(on_close()))
|
305
308
|
|
306
|
-
|
307
|
-
try:
|
308
|
-
while not page.is_closed():
|
309
|
-
await send_events_async(page, session_id, trace_id, client)
|
310
|
-
await asyncio.sleep(2)
|
309
|
+
original_bring_to_front = page.bring_to_front
|
311
310
|
|
312
|
-
|
313
|
-
|
314
|
-
except Exception as e:
|
315
|
-
logger.error(f"Event collection stopped: {e}")
|
311
|
+
async def bring_to_front():
|
312
|
+
await original_bring_to_front()
|
316
313
|
|
317
|
-
|
318
|
-
|
314
|
+
await page.evaluate(
|
315
|
+
"""() => {
|
316
|
+
if (window.lmnrRrweb) {
|
317
|
+
try {
|
318
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
319
|
+
} catch (e) {
|
320
|
+
console.error("Error taking full snapshot:", e);
|
321
|
+
}
|
322
|
+
}
|
323
|
+
}"""
|
324
|
+
)
|
319
325
|
|
320
|
-
|
321
|
-
|
326
|
+
page.bring_to_front = bring_to_front
|
327
|
+
await inject_rrweb_async(page)
|