lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__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 +32 -11
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/cli/evals.py +111 -0
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +70 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
- lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
- lmnr/opentelemetry_lib/litellm/utils.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
- lmnr/opentelemetry_lib/tracing/processor.py +193 -0
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +18 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/__init__.py +0 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +142 -0
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +151 -0
- lmnr/sdk/browser/playwright_otel.py +322 -0
- lmnr/sdk/browser/pw_utils.py +363 -0
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +70 -0
- lmnr/sdk/client/asynchronous/async_client.py +180 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +263 -0
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/sync_client.py +191 -0
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +163 -26
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +403 -191
- lmnr/sdk/laminar.py +1080 -549
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +246 -134
- lmnr/sdk/utils.py +151 -7
- lmnr/version.py +46 -0
- {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/cli.py +0 -101
- lmnr/openllmetry_sdk/.python-version +0 -1
- lmnr/openllmetry_sdk/__init__.py +0 -72
- lmnr/openllmetry_sdk/config/__init__.py +0 -9
- lmnr/openllmetry_sdk/decorators/base.py +0 -185
- lmnr/openllmetry_sdk/instruments.py +0 -38
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- lmnr/openllmetry_sdk/utils/package_check.py +0 -7
- lmnr/openllmetry_sdk/version.py +0 -1
- lmnr/sdk/datasets.py +0 -55
- lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
- lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
- lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
- lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background sending for browser events.
|
|
3
|
+
|
|
4
|
+
This module provides background execution for HTTP requests that send browser events,
|
|
5
|
+
ensuring sends never block the main execution flow while guaranteeing completion at
|
|
6
|
+
program exit.
|
|
7
|
+
|
|
8
|
+
## Background Event Loop Architecture
|
|
9
|
+
Uses a dedicated event loop running in a separate thread to handle async HTTP requests.
|
|
10
|
+
This architecture provides:
|
|
11
|
+
|
|
12
|
+
1. **Non-blocking execution**: Sends happen in the background, never blocking the main
|
|
13
|
+
thread or Playwright's event loop, allowing browser automation to continue smoothly.
|
|
14
|
+
|
|
15
|
+
2. **Guaranteed completion**: When the program exits, all pending async sends are
|
|
16
|
+
awaited and complete successfully, even if they're slow. No events are dropped.
|
|
17
|
+
|
|
18
|
+
3. **Lifecycle independence**: The background loop runs independently of Playwright's
|
|
19
|
+
event loop, so it survives when Playwright shuts down its internal loop before
|
|
20
|
+
program exit.
|
|
21
|
+
|
|
22
|
+
The pattern uses `asyncio.run_coroutine_threadsafe()` to submit async coroutines
|
|
23
|
+
from any thread (sync or async) to our background loop, maintaining pure async code
|
|
24
|
+
while achieving cross-thread execution.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import atexit
|
|
29
|
+
import threading
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from lmnr.sdk.log import get_default_logger
|
|
33
|
+
|
|
34
|
+
logger = get_default_logger(__name__)
|
|
35
|
+
|
|
36
|
+
# Timeout for waiting for each async send operation at exit
|
|
37
|
+
ASYNC_SEND_TIMEOUT_SECONDS = 30
|
|
38
|
+
|
|
39
|
+
# Timeout for background loop creation
|
|
40
|
+
LOOP_CREATION_TIMEOUT_SECONDS = 5
|
|
41
|
+
|
|
42
|
+
# Timeout for thread join during cleanup
|
|
43
|
+
THREAD_JOIN_TIMEOUT_SECONDS = 5
|
|
44
|
+
|
|
45
|
+
# ==============================================================================
|
|
46
|
+
# Background event loop for async sends
|
|
47
|
+
# ==============================================================================
|
|
48
|
+
|
|
49
|
+
# Background event loop state
|
|
50
|
+
_background_loop = None
|
|
51
|
+
_background_loop_thread = None
|
|
52
|
+
_background_loop_lock = threading.Lock()
|
|
53
|
+
_background_loop_ready = threading.Event()
|
|
54
|
+
_pending_async_futures: set[asyncio.Future[Any]] = set()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_background_loop() -> asyncio.AbstractEventLoop:
|
|
58
|
+
"""
|
|
59
|
+
Get or create the background event loop for async sends.
|
|
60
|
+
|
|
61
|
+
Creates a dedicated event loop running in a daemon thread on first call.
|
|
62
|
+
Subsequent calls return the same loop. Thread-safe.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The background event loop running in a separate thread.
|
|
66
|
+
"""
|
|
67
|
+
global _background_loop, _background_loop_thread
|
|
68
|
+
|
|
69
|
+
with _background_loop_lock:
|
|
70
|
+
if _background_loop is None:
|
|
71
|
+
# Create a new event loop in a background thread
|
|
72
|
+
def run_loop():
|
|
73
|
+
global _background_loop
|
|
74
|
+
_background_loop = asyncio.new_event_loop()
|
|
75
|
+
asyncio.set_event_loop(_background_loop)
|
|
76
|
+
_background_loop_ready.set()
|
|
77
|
+
_background_loop.run_forever()
|
|
78
|
+
|
|
79
|
+
_background_loop_thread = threading.Thread(
|
|
80
|
+
target=run_loop, daemon=True, name="lmnr-async-sends"
|
|
81
|
+
)
|
|
82
|
+
_background_loop_thread.start()
|
|
83
|
+
|
|
84
|
+
# Register cleanup handler
|
|
85
|
+
atexit.register(_cleanup_background_loop)
|
|
86
|
+
|
|
87
|
+
# Wait for loop to be created (outside the lock to avoid blocking other threads)
|
|
88
|
+
if not _background_loop_ready.wait(timeout=LOOP_CREATION_TIMEOUT_SECONDS):
|
|
89
|
+
raise RuntimeError("Background loop creation timed out")
|
|
90
|
+
|
|
91
|
+
return _background_loop
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def track_async_send(future: asyncio.Future) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Track an async send future for cleanup at exit.
|
|
97
|
+
|
|
98
|
+
The future is automatically removed from tracking when it completes,
|
|
99
|
+
preventing memory leaks.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
future: The future returned by asyncio.run_coroutine_threadsafe()
|
|
103
|
+
"""
|
|
104
|
+
with _background_loop_lock:
|
|
105
|
+
_pending_async_futures.add(future)
|
|
106
|
+
|
|
107
|
+
def remove_on_done(f):
|
|
108
|
+
"""Remove the future from tracking when it completes."""
|
|
109
|
+
with _background_loop_lock:
|
|
110
|
+
_pending_async_futures.discard(f)
|
|
111
|
+
|
|
112
|
+
future.add_done_callback(remove_on_done)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cleanup_background_loop():
|
|
116
|
+
"""
|
|
117
|
+
Shutdown the background event loop and wait for all pending sends to complete.
|
|
118
|
+
|
|
119
|
+
Called automatically at program exit via atexit. Waits for each pending send
|
|
120
|
+
to complete with a timeout, then stops the background loop gracefully.
|
|
121
|
+
"""
|
|
122
|
+
global _background_loop
|
|
123
|
+
|
|
124
|
+
# Create a snapshot of pending futures to avoid holding the lock during waits
|
|
125
|
+
with _background_loop_lock:
|
|
126
|
+
futures_to_wait = list(_pending_async_futures)
|
|
127
|
+
|
|
128
|
+
pending_count = len(futures_to_wait)
|
|
129
|
+
|
|
130
|
+
if pending_count > 0:
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Finishing sending {pending_count} browser events... "
|
|
133
|
+
"Ctrl+C to cancel (may result in incomplete session recording)."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Wait for all pending futures to complete
|
|
137
|
+
for future in futures_to_wait:
|
|
138
|
+
try:
|
|
139
|
+
future.result(timeout=ASYNC_SEND_TIMEOUT_SECONDS)
|
|
140
|
+
except TimeoutError:
|
|
141
|
+
logger.debug("Timeout waiting for async send to complete")
|
|
142
|
+
except KeyboardInterrupt:
|
|
143
|
+
logger.debug("Interrupted, cancelling pending async sends")
|
|
144
|
+
for f in futures_to_wait:
|
|
145
|
+
f.cancel()
|
|
146
|
+
raise
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.debug(f"Error in async send: {e}")
|
|
149
|
+
|
|
150
|
+
# Stop the background loop
|
|
151
|
+
if _background_loop is not None and not _background_loop.is_closed():
|
|
152
|
+
try:
|
|
153
|
+
_background_loop.call_soon_threadsafe(_background_loop.stop)
|
|
154
|
+
# Wait for thread to finish
|
|
155
|
+
if _background_loop_thread is not None:
|
|
156
|
+
_background_loop_thread.join(timeout=THREAD_JOIN_TIMEOUT_SECONDS)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.debug(f"Error stopping background loop: {e}")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
|
5
|
+
from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
|
|
6
|
+
from lmnr.version import __version__
|
|
7
|
+
from lmnr.sdk.browser.cdp_utils import (
|
|
8
|
+
is_recorder_present,
|
|
9
|
+
start_recording_events,
|
|
10
|
+
take_full_snapshot,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
14
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
15
|
+
from opentelemetry.trace import get_tracer, Tracer
|
|
16
|
+
from typing import Collection
|
|
17
|
+
from wrapt import wrap_function_wrapper
|
|
18
|
+
|
|
19
|
+
# Stable versions, e.g. 0.6.0, satisfy this condition too
|
|
20
|
+
_instruments = ("browser-use >= 0.6.0rc1",)
|
|
21
|
+
|
|
22
|
+
WRAPPED_METHODS = [
|
|
23
|
+
{
|
|
24
|
+
"package": "browser_use.browser.session",
|
|
25
|
+
"object": "BrowserSession",
|
|
26
|
+
"method": "get_or_create_cdp_session",
|
|
27
|
+
"action": "inject_session_recorder",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"package": "browser_use.browser.session",
|
|
31
|
+
"object": "BrowserSession",
|
|
32
|
+
"method": "on_SwitchTabEvent",
|
|
33
|
+
"action": "take_full_snapshot",
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def process_wrapped_result(result, instance, client, to_wrap):
|
|
39
|
+
if to_wrap.get("action") == "inject_session_recorder":
|
|
40
|
+
is_registered = await is_recorder_present(result)
|
|
41
|
+
if not is_registered:
|
|
42
|
+
await start_recording_events(result, str(uuid.uuid4()), client)
|
|
43
|
+
|
|
44
|
+
if to_wrap.get("action") == "take_full_snapshot":
|
|
45
|
+
target_id = result
|
|
46
|
+
if target_id:
|
|
47
|
+
cdp_session = await instance.get_or_create_cdp_session(target_id)
|
|
48
|
+
await take_full_snapshot(cdp_session)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@with_tracer_and_client_wrapper
|
|
52
|
+
async def _wrap(
|
|
53
|
+
tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
|
|
54
|
+
):
|
|
55
|
+
result = await wrapped(*args, **kwargs)
|
|
56
|
+
asyncio.create_task(process_wrapped_result(result, instance, client, to_wrap))
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BrowserUseInstrumentor(BaseInstrumentor):
|
|
62
|
+
def __init__(self, async_client: AsyncLaminarClient):
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.async_client = async_client
|
|
65
|
+
|
|
66
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
67
|
+
return _instruments
|
|
68
|
+
|
|
69
|
+
def _instrument(self, **kwargs):
|
|
70
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
71
|
+
tracer = get_tracer(__name__, __version__, tracer_provider)
|
|
72
|
+
|
|
73
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
74
|
+
wrap_package = wrapped_method.get("package")
|
|
75
|
+
wrap_object = wrapped_method.get("object")
|
|
76
|
+
wrap_method = wrapped_method.get("method")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
wrap_function_wrapper(
|
|
80
|
+
wrap_package,
|
|
81
|
+
f"{wrap_object}.{wrap_method}",
|
|
82
|
+
_wrap(
|
|
83
|
+
tracer,
|
|
84
|
+
self.async_client,
|
|
85
|
+
wrapped_method,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
except (ModuleNotFoundError, ImportError):
|
|
89
|
+
pass # that's ok, we're not instrumenting everything
|
|
90
|
+
|
|
91
|
+
def _uninstrument(self, **kwargs):
|
|
92
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
93
|
+
wrap_package = wrapped_method.get("package")
|
|
94
|
+
wrap_object = wrapped_method.get("object")
|
|
95
|
+
wrap_method = wrapped_method.get("method")
|
|
96
|
+
|
|
97
|
+
unwrap(
|
|
98
|
+
f"{wrap_package}.{wrap_object}" if wrap_object else wrap_package,
|
|
99
|
+
wrap_method,
|
|
100
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from lmnr import Laminar
|
|
2
|
+
from lmnr.sdk.browser.utils import with_tracer_wrapper
|
|
3
|
+
from lmnr.sdk.utils import get_input_from_func_args, json_dumps
|
|
4
|
+
from lmnr.version import __version__
|
|
5
|
+
|
|
6
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
7
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
8
|
+
from opentelemetry.trace import get_tracer, Tracer
|
|
9
|
+
from typing import Collection
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
import pydantic
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from browser_use import AgentHistoryList
|
|
15
|
+
except ImportError as e:
|
|
16
|
+
raise ImportError(
|
|
17
|
+
f"Attempted to import {__file__}, but it is designed "
|
|
18
|
+
"to patch Browser Use < 0.5.0, which is not installed. Use `pip install browser-use` "
|
|
19
|
+
"to install Browser Use or remove this import."
|
|
20
|
+
) from e
|
|
21
|
+
|
|
22
|
+
_instruments = ("browser-use < 0.5.0",)
|
|
23
|
+
|
|
24
|
+
WRAPPED_METHODS = [
|
|
25
|
+
{
|
|
26
|
+
"package": "browser_use.agent.service",
|
|
27
|
+
"object": "Agent",
|
|
28
|
+
"method": "run",
|
|
29
|
+
"span_name": "agent.run",
|
|
30
|
+
"ignore_input": True,
|
|
31
|
+
"ignore_output": True,
|
|
32
|
+
"span_type": "DEFAULT",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"package": "browser_use.agent.service",
|
|
36
|
+
"object": "Agent",
|
|
37
|
+
"method": "step",
|
|
38
|
+
"span_name": "agent.step",
|
|
39
|
+
"ignore_input": True,
|
|
40
|
+
"ignore_output": True,
|
|
41
|
+
"span_type": "DEFAULT",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"package": "browser_use.controller.service",
|
|
45
|
+
"object": "Controller",
|
|
46
|
+
"method": "act",
|
|
47
|
+
"span_name": "controller.act",
|
|
48
|
+
"ignore_input": True,
|
|
49
|
+
"ignore_output": True,
|
|
50
|
+
"span_type": "DEFAULT",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"package": "browser_use.controller.registry.service",
|
|
54
|
+
"object": "Registry",
|
|
55
|
+
"method": "execute_action",
|
|
56
|
+
"ignore_input": True,
|
|
57
|
+
"ignore_output": True,
|
|
58
|
+
"span_type": "TOOL",
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@with_tracer_wrapper
|
|
64
|
+
async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
65
|
+
span_name = to_wrap.get("span_name")
|
|
66
|
+
attributes = {
|
|
67
|
+
"lmnr.span.type": to_wrap.get("span_type"),
|
|
68
|
+
}
|
|
69
|
+
if to_wrap.get("method") == "execute_action":
|
|
70
|
+
span_name = args[0] if len(args) > 0 else kwargs.get("action_name", "action")
|
|
71
|
+
attributes["lmnr.span.input"] = json_dumps(
|
|
72
|
+
{
|
|
73
|
+
"action": span_name,
|
|
74
|
+
"params": args[1] if len(args) > 1 else kwargs.get("params", {}),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
if not to_wrap.get("ignore_input"):
|
|
79
|
+
inp_dict = get_input_from_func_args(wrapped, True, args, kwargs)
|
|
80
|
+
# Add task to the `agent.run` span input
|
|
81
|
+
if to_wrap.get("method") == "run" and hasattr(instance, "task"):
|
|
82
|
+
inp_dict["task"] = instance.task
|
|
83
|
+
attributes["lmnr.span.input"] = json_dumps(inp_dict)
|
|
84
|
+
if to_wrap.get("method") == "step" and to_wrap.get("object") == "Agent":
|
|
85
|
+
# Add step number to the `agent.step` span name
|
|
86
|
+
step_info = kwargs.get("step_info", args[0] if len(args) > 0 else None)
|
|
87
|
+
if step_info and hasattr(step_info, "step_number"):
|
|
88
|
+
span_name = f"agent.step.{step_info.step_number}"
|
|
89
|
+
|
|
90
|
+
with Laminar.start_as_current_span(span_name) as span:
|
|
91
|
+
result = await wrapped(*args, **kwargs)
|
|
92
|
+
if not to_wrap.get("ignore_output"):
|
|
93
|
+
to_serialize = result
|
|
94
|
+
if isinstance(result, AgentHistoryList):
|
|
95
|
+
to_serialize = result.final_result()
|
|
96
|
+
serialized = (
|
|
97
|
+
to_serialize.model_dump_json()
|
|
98
|
+
if isinstance(to_serialize, pydantic.BaseModel)
|
|
99
|
+
else json_dumps(to_serialize)
|
|
100
|
+
)
|
|
101
|
+
span.set_attribute("lmnr.span.output", serialized)
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class BrowserUseLegacyInstrumentor(BaseInstrumentor):
|
|
106
|
+
def __init__(self):
|
|
107
|
+
super().__init__()
|
|
108
|
+
|
|
109
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
110
|
+
return _instruments
|
|
111
|
+
|
|
112
|
+
def _instrument(self, **kwargs):
|
|
113
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
114
|
+
tracer = get_tracer(__name__, __version__, tracer_provider)
|
|
115
|
+
|
|
116
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
117
|
+
wrap_package = wrapped_method.get("package")
|
|
118
|
+
wrap_object = wrapped_method.get("object")
|
|
119
|
+
wrap_method = wrapped_method.get("method")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
wrap_function_wrapper(
|
|
123
|
+
wrap_package,
|
|
124
|
+
f"{wrap_object}.{wrap_method}",
|
|
125
|
+
_wrap(
|
|
126
|
+
tracer,
|
|
127
|
+
wrapped_method,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
except ModuleNotFoundError:
|
|
131
|
+
pass # that's ok, we're not instrumenting everything
|
|
132
|
+
|
|
133
|
+
def _uninstrument(self, **kwargs):
|
|
134
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
135
|
+
wrap_package = wrapped_method.get("package")
|
|
136
|
+
wrap_object = wrapped_method.get("object")
|
|
137
|
+
wrap_method = wrapped_method.get("method")
|
|
138
|
+
|
|
139
|
+
unwrap(
|
|
140
|
+
f"{wrap_package}.{wrap_object}" if wrap_object else wrap_package,
|
|
141
|
+
wrap_method,
|
|
142
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Collection
|
|
2
|
+
|
|
3
|
+
from lmnr import Laminar
|
|
4
|
+
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
|
5
|
+
from lmnr.sdk.log import get_default_logger
|
|
6
|
+
|
|
7
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
8
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
9
|
+
from opentelemetry.trace import NonRecordingSpan, get_current_span
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_instruments = ("bubus >= 1.3.0",)
|
|
14
|
+
event_id_to_span_context = {}
|
|
15
|
+
logger = get_default_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def wrap_dispatch(wrapped, instance, args, kwargs):
|
|
19
|
+
event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
|
|
20
|
+
if event and hasattr(event, "event_id"):
|
|
21
|
+
event_id = event.event_id
|
|
22
|
+
if event_id:
|
|
23
|
+
span = get_current_span(get_current_context())
|
|
24
|
+
event_id_to_span_context[event_id] = span.get_span_context()
|
|
25
|
+
return wrapped(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def wrap_process_event(wrapped, instance, args, kwargs):
|
|
29
|
+
event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
|
|
30
|
+
span_context = None
|
|
31
|
+
if event and hasattr(event, "event_id"):
|
|
32
|
+
event_id = event.event_id
|
|
33
|
+
if event_id:
|
|
34
|
+
span_context = event_id_to_span_context.get(event_id)
|
|
35
|
+
if not span_context:
|
|
36
|
+
return await wrapped(*args, **kwargs)
|
|
37
|
+
if not Laminar.is_initialized():
|
|
38
|
+
return await wrapped(*args, **kwargs)
|
|
39
|
+
with Laminar.use_span(NonRecordingSpan(span_context)):
|
|
40
|
+
return await wrapped(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BubusInstrumentor(BaseInstrumentor):
|
|
44
|
+
def __init__(self):
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
48
|
+
return _instruments
|
|
49
|
+
|
|
50
|
+
def _instrument(self, **kwargs):
|
|
51
|
+
try:
|
|
52
|
+
wrap_function_wrapper("bubus.service", "EventBus.dispatch", wrap_dispatch)
|
|
53
|
+
except (ModuleNotFoundError, ImportError):
|
|
54
|
+
pass
|
|
55
|
+
try:
|
|
56
|
+
wrap_function_wrapper(
|
|
57
|
+
"bubus.service", "EventBus.process_event", wrap_process_event
|
|
58
|
+
)
|
|
59
|
+
except (ModuleNotFoundError, ImportError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def _uninstrument(self, **kwargs):
|
|
63
|
+
try:
|
|
64
|
+
unwrap("bubus.service", "EventBus.dispatch")
|
|
65
|
+
except (ModuleNotFoundError, ImportError):
|
|
66
|
+
pass
|
|
67
|
+
try:
|
|
68
|
+
unwrap("bubus.service", "EventBus.process_event")
|
|
69
|
+
except (ModuleNotFoundError, ImportError):
|
|
70
|
+
pass
|
|
71
|
+
event_id_to_span_context.clear()
|