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.
Files changed (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /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()