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 @@
1
+ __version__ = "0.40.14"
@@ -0,0 +1,388 @@
1
+ """OpenTelemetry OpenHands AI instrumentation"""
2
+
3
+ import sys
4
+ from typing import Collection
5
+
6
+ from lmnr.opentelemetry_lib.tracing.attributes import (
7
+ ASSOCIATION_PROPERTIES,
8
+ SESSION_ID,
9
+ USER_ID,
10
+ )
11
+ from lmnr.opentelemetry_lib.utils.wrappers import _with_tracer_wrapper
12
+ from lmnr.sdk.log import get_default_logger
13
+ from lmnr.sdk.utils import get_input_from_func_args, json_dumps
14
+ from lmnr import Laminar
15
+ from lmnr.version import __version__
16
+
17
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
18
+ from opentelemetry.instrumentation.utils import unwrap
19
+ from opentelemetry.trace import get_tracer, Tracer
20
+ from wrapt import wrap_function_wrapper
21
+
22
+ logger = get_default_logger(__name__)
23
+
24
+ _instruments = ("openhands-ai >= 0.9.0", "openhands-aci >= 0.1.0")
25
+ parent_spans = {}
26
+
27
+
28
+ def is_message_action(event) -> bool:
29
+ """Check if event has action attribute equal to 'message'."""
30
+ return event and hasattr(event, "action") and event.action == "message"
31
+
32
+
33
+ def is_user_message(event) -> bool:
34
+ """Check if event is a message action from user source."""
35
+ return (
36
+ is_message_action(event) and hasattr(event, "source") and event.source == "user"
37
+ )
38
+
39
+
40
+ def is_agent_message(event) -> bool:
41
+ """Check if event is a message action from agent source."""
42
+ return (
43
+ is_message_action(event)
44
+ and hasattr(event, "source")
45
+ and event.source == "agent"
46
+ )
47
+
48
+
49
+ def is_agent_state_changed_to(event, state: str) -> bool:
50
+ """Check if event is an agent_state_changed observation with specific state."""
51
+ return (
52
+ event
53
+ and hasattr(event, "observation")
54
+ and event.observation == "agent_state_changed"
55
+ and hasattr(event, "agent_state")
56
+ and event.agent_state == state
57
+ )
58
+
59
+
60
+ def get_handle_action_action(event) -> str:
61
+ """Get the action of the handle_action event."""
62
+ if event and hasattr(event, "action"):
63
+ try:
64
+ return event.action.value
65
+ except Exception:
66
+ return event.action
67
+ return None
68
+
69
+
70
+ WRAPPED_METHODS = [
71
+ {
72
+ "package": "openhands.agenthub.codeact_agent.codeact_agent",
73
+ "object": "CodeActAgent",
74
+ "methods": [
75
+ {"method": "step"},
76
+ {"method": "response_to_actions"},
77
+ ],
78
+ },
79
+ {
80
+ "package": "openhands.controller.agent_controller",
81
+ "object": "AgentController",
82
+ "methods": [
83
+ {"method": "_step", "async": True},
84
+ {
85
+ "method": "_handle_action",
86
+ "async": True,
87
+ "span_type": "TOOL",
88
+ },
89
+ {"method": "_handle_observation", "async": True},
90
+ {"method": "_handle_message_action", "async": True},
91
+ {"method": "on_event"},
92
+ {"method": "save_state"},
93
+ {"method": "get_trajectory"},
94
+ {"method": "start_delegate"},
95
+ {"method": "end_delegate"},
96
+ {"method": "_is_stuck"},
97
+ ],
98
+ },
99
+ ]
100
+
101
+
102
+ @_with_tracer_wrapper
103
+ def _wrap_on_event(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
104
+ """Wrapper for on_event."""
105
+ controller_id = instance.id
106
+ user_id = instance.user_id
107
+ event = kwargs.get("event", args[0] if len(args) > 0 else None)
108
+ start_event = False
109
+ finish_event = False
110
+ user_message = ""
111
+ agent_message = ""
112
+ span_name = to_wrap.get("span_name")
113
+ span_type = to_wrap.get("span_type", "DEFAULT")
114
+ if event and hasattr(event, "action") and event.action == "system":
115
+ return wrapped(*args, **kwargs)
116
+
117
+ event_type = None
118
+ subtype = None
119
+ if event and hasattr(event, "action"):
120
+ event_type = "action"
121
+ try:
122
+ subtype = event.action.value
123
+ except Exception:
124
+ subtype = event.action
125
+ elif event and hasattr(event, "observation"):
126
+ event_type = "observation"
127
+ try:
128
+ subtype = event.observation.value
129
+ except Exception:
130
+ subtype = event.observation
131
+ if event_type and subtype:
132
+ span_name = f"event.{event_type}.{subtype}"
133
+ span_type = "EVENT"
134
+
135
+ # start trace on user message
136
+ if is_user_message(event):
137
+ user_message = event.content if hasattr(event, "content") else ""
138
+ start_event = True
139
+ # end trace on agent state change to finished or error
140
+ if is_agent_state_changed_to(event, "stopped") or is_agent_state_changed_to(
141
+ event, "awaiting_user_input"
142
+ ):
143
+ finish_event = True
144
+
145
+ if is_agent_state_changed_to(event, "user_rejected"):
146
+ agent_message = "<user_rejected>"
147
+
148
+ if is_agent_message(event):
149
+ agent_message = event.content if hasattr(event, "content") else ""
150
+
151
+ if start_event:
152
+ if controller_id in parent_spans:
153
+ logger.debug(
154
+ "Received a message, but already have a span for this trace. Resetting span."
155
+ )
156
+ parent_spans[controller_id].end()
157
+ del parent_spans[controller_id]
158
+ parent_span = Laminar.start_span("conversation.turn", span_type="DEFAULT")
159
+ if user_id:
160
+ parent_span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
161
+ if user_message:
162
+ parent_span.set_attribute("lmnr.span.input", user_message)
163
+ parent_span.set_attribute(
164
+ f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", controller_id
165
+ )
166
+ parent_spans[controller_id] = parent_span
167
+
168
+ if controller_id in parent_spans:
169
+ with Laminar.use_span(parent_spans[controller_id]):
170
+ result = _wrap_sync_method_inner(
171
+ tracer,
172
+ {**to_wrap, "span_name": span_name, "span_type": span_type},
173
+ wrapped,
174
+ instance,
175
+ args,
176
+ kwargs,
177
+ )
178
+ if agent_message:
179
+ parent_spans[controller_id].set_attribute(
180
+ "lmnr.span.output", agent_message
181
+ )
182
+ if finish_event:
183
+ parent_spans[controller_id].end()
184
+ del parent_spans[controller_id]
185
+ return result
186
+
187
+ return wrapped(*args, **kwargs)
188
+
189
+
190
+ @_with_tracer_wrapper
191
+ async def _wrap_handle_action(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
192
+ """Wrapper for on_event."""
193
+ event = kwargs.get("event", args[0] if len(args) > 0 else None)
194
+ if event and hasattr(event, "action"):
195
+ if event.action == "system":
196
+ return await wrapped(*args, **kwargs)
197
+ action_name = get_handle_action_action(event)
198
+ if action_name and action_name != "message":
199
+ to_wrap["span_name"] = f"action.{action_name}"
200
+ controller_id = instance.id
201
+ if controller_id not in parent_spans:
202
+ return await wrapped(*args, **kwargs)
203
+ return await _wrap_async_method_inner(
204
+ tracer, to_wrap, wrapped, instance, args, kwargs
205
+ )
206
+
207
+
208
+ def _wrap_sync_method_inner(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
209
+ """Wrapper for synchronous methods."""
210
+ span_name = to_wrap.get("span_name")
211
+
212
+ with Laminar.start_as_current_span(
213
+ span_name,
214
+ span_type=to_wrap.get("span_type", "DEFAULT"),
215
+ input=json_dumps(
216
+ get_input_from_func_args(
217
+ wrapped, to_wrap.get("object") is not None, args, kwargs
218
+ )
219
+ ),
220
+ ) as span:
221
+ try:
222
+ result = wrapped(*args, **kwargs)
223
+
224
+ # Capture output
225
+ if not to_wrap.get("ignore_output"):
226
+ span.set_attribute("lmnr.span.output", json_dumps(result))
227
+ return result
228
+
229
+ except Exception as e:
230
+ span.record_exception(e)
231
+ raise
232
+
233
+
234
+ @_with_tracer_wrapper
235
+ def _wrap_sync_method(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
236
+ instance_id = None
237
+ if to_wrap.get("object") == "AgentController":
238
+ instance_id = instance.id
239
+ if to_wrap.get("object") == "ActionExecutionClient" and hasattr(instance, "sid"):
240
+ instance_id = instance.sid
241
+ if instance_id is not None and instance_id not in parent_spans:
242
+ return wrapped(*args, **kwargs)
243
+ return _wrap_sync_method_inner(tracer, to_wrap, wrapped, instance, args, kwargs)
244
+
245
+
246
+ async def _wrap_async_method_inner(
247
+ tracer: Tracer, to_wrap, wrapped, instance, args, kwargs
248
+ ):
249
+ """Wrapper for asynchronous methods."""
250
+ span_name = to_wrap.get("span_name")
251
+ instance_id = None
252
+ if to_wrap.get("object") == "AgentController":
253
+ instance_id = instance.id
254
+ if to_wrap.get("object") == "ActionExecutionClient" and hasattr(instance, "sid"):
255
+ instance_id = instance.sid
256
+ if instance_id is not None and instance_id not in parent_spans:
257
+ return await wrapped(*args, **kwargs)
258
+
259
+ with Laminar.start_as_current_span(
260
+ span_name,
261
+ span_type=to_wrap.get("span_type", "DEFAULT"),
262
+ input=json_dumps(
263
+ get_input_from_func_args(
264
+ wrapped, to_wrap.get("object") is not None, args, kwargs
265
+ )
266
+ ),
267
+ ) as span:
268
+ try:
269
+ result = await wrapped(*args, **kwargs)
270
+
271
+ # Capture output
272
+ if not to_wrap.get("ignore_output"):
273
+ span.set_attribute("lmnr.span.output", json_dumps(result))
274
+ return result
275
+
276
+ except Exception as e:
277
+ span.record_exception(e)
278
+ raise
279
+
280
+
281
+ @_with_tracer_wrapper
282
+ async def _wrap_async_method(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
283
+ """Wrapper for asynchronous methods."""
284
+ return await _wrap_async_method_inner(
285
+ tracer, to_wrap, wrapped, instance, args, kwargs
286
+ )
287
+
288
+
289
+ class OpenHandsInstrumentor(BaseInstrumentor):
290
+ """An instrumentor for OpenHands AI."""
291
+
292
+ def __init__(self):
293
+ super().__init__()
294
+
295
+ def instrumentation_dependencies(self) -> Collection[str]:
296
+ return _instruments
297
+
298
+ def _instrument(self, **kwargs):
299
+ """Instrument OpenHands AI methods."""
300
+ tracer_provider = kwargs.get("tracer_provider")
301
+ tracer = get_tracer(__name__, __version__, tracer_provider)
302
+
303
+ for wrapped_config in WRAPPED_METHODS:
304
+ wrap_package = wrapped_config.get("package")
305
+
306
+ wrap_object = wrapped_config.get("object")
307
+ methods = wrapped_config.get("methods", [])
308
+
309
+ for method_config in methods:
310
+
311
+ wrap_method = method_config.get("method")
312
+ async_wrap = method_config.get("async", False)
313
+ windows_only = method_config.get("windows_only", False)
314
+ if windows_only and sys.platform != "win32":
315
+ continue
316
+
317
+ # Create the method configuration for the wrapper
318
+ method_wrapper_config = {
319
+ "package": wrap_package,
320
+ "object": wrap_object,
321
+ "method": wrap_method,
322
+ "span_name": method_config.get(
323
+ "span_name",
324
+ f"{wrap_object}.{wrap_method}" if wrap_object else wrap_method,
325
+ ),
326
+ "span_type": method_config.get("span_type", "DEFAULT"),
327
+ "async": async_wrap,
328
+ }
329
+
330
+ # Determine the target for wrapping
331
+ if wrap_object:
332
+ target = f"{wrap_object}.{wrap_method}"
333
+ else:
334
+ target = wrap_method
335
+
336
+ if wrap_object == "AgentController" and wrap_method == "on_event":
337
+ wrap_function_wrapper(
338
+ wrap_package,
339
+ target,
340
+ _wrap_on_event(tracer, method_wrapper_config),
341
+ )
342
+ continue
343
+ if wrap_object == "AgentController" and wrap_method == "_handle_action":
344
+ wrap_function_wrapper(
345
+ wrap_package,
346
+ target,
347
+ _wrap_handle_action(tracer, method_wrapper_config),
348
+ )
349
+ continue
350
+
351
+ try:
352
+ if async_wrap:
353
+ wrap_function_wrapper(
354
+ wrap_package,
355
+ target,
356
+ _wrap_async_method(tracer, method_wrapper_config),
357
+ )
358
+ else:
359
+ wrap_function_wrapper(
360
+ wrap_package,
361
+ target,
362
+ _wrap_sync_method(tracer, method_wrapper_config),
363
+ )
364
+ except (ModuleNotFoundError, AttributeError) as e:
365
+ logger.debug(f"Could not instrument {wrap_package}.{target}: {e}")
366
+
367
+ def _uninstrument(self, **kwargs):
368
+ """Remove OpenHands AI instrumentation."""
369
+ for wrapped_config in WRAPPED_METHODS:
370
+ wrap_package = wrapped_config.get("package")
371
+ wrap_object = wrapped_config.get("object")
372
+ methods = wrapped_config.get("methods", [])
373
+
374
+ for method_config in methods:
375
+ wrap_method = method_config.get("method")
376
+
377
+ # Determine the module path for unwrapping
378
+ if wrap_object:
379
+ module_path = f"{wrap_package}.{wrap_object}"
380
+ else:
381
+ module_path = wrap_package
382
+
383
+ try:
384
+ unwrap(module_path, wrap_method)
385
+ except (AttributeError, ValueError) as e:
386
+ logger.debug(
387
+ f"Could not uninstrument {module_path}.{wrap_method}: {e}"
388
+ )
@@ -0,0 +1,69 @@
1
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
2
+ from opentelemetry.instrumentation.utils import unwrap
3
+ from opentelemetry.trace import TraceFlags, SpanContext
4
+ from typing import Collection
5
+ from wrapt import wrap_function_wrapper
6
+ import logging
7
+
8
+
9
+ def _wrap_span_context(fn, instance, args, kwargs):
10
+ """
11
+ DataDog does something to the OpenTelemetry Contexts, so that when any code
12
+ tries to access the current active span, it returns a non-recording span.
13
+
14
+ There is nothing wrong about that per se, but they create their
15
+ NonRecordingSpan from an invalid SpanContext, because they don't
16
+ wrap the trace flags int/bitmap into a TraceFlags object.
17
+
18
+ It is an easy to miss bug, because `TraceFlags.SAMPLED` looks like an
19
+ instance of `TraceFlags`, but is actually just an integer 1, and the
20
+ proper way to create it is actually
21
+ `TraceFlags(TraceFlags.SAMPLED)` or `TraceFlags(0x1)`.
22
+
23
+ This is a problem because the trace flags are used to determine if a span
24
+ is sampled or not. If the trace flags are not wrapped, then the check
25
+ for sampling will fail, causing any span creation to fail, and sometimes
26
+ breaking the entire application.
27
+
28
+ Issue: https://github.com/DataDog/dd-trace-py/issues/12585
29
+ PR: https://github.com/DataDog/dd-trace-py/pull/12596
30
+ The PR only fixed the issue in one place, but it is still there in other places.
31
+ https://github.com/DataDog/dd-trace-py/pull/12596#issuecomment-2718239507
32
+
33
+ https://github.com/DataDog/dd-trace-py/blob/a8419a40fe9e73e0a84c4cab53094c384480a5a6/ddtrace/internal/opentelemetry/context.py#L83
34
+
35
+ We patch the `get_span_context` method to return a valid SpanContext.
36
+ """
37
+ res = fn(*args, **kwargs)
38
+
39
+ new_span_context = SpanContext(
40
+ trace_id=res.trace_id,
41
+ span_id=res.span_id,
42
+ is_remote=res.is_remote,
43
+ trace_state=res.trace_state,
44
+ trace_flags=TraceFlags(res.trace_flags),
45
+ )
46
+
47
+ return new_span_context
48
+
49
+
50
+ class OpentelemetryInstrumentor(BaseInstrumentor):
51
+ def __init__(self):
52
+ super().__init__()
53
+
54
+ def instrumentation_dependencies(self) -> Collection[str]:
55
+ return ("opentelemetry-api>=1.0.0",)
56
+
57
+ def _instrument(self, **kwargs):
58
+ try:
59
+ wrap_function_wrapper(
60
+ "opentelemetry.trace.span",
61
+ "NonRecordingSpan.get_span_context",
62
+ _wrap_span_context,
63
+ )
64
+
65
+ except Exception as e:
66
+ logging.debug(f"Error wrapping SpanContext: {e}")
67
+
68
+ def _uninstrument(self, **kwargs):
69
+ unwrap("opentelemetry.trace.span", "NonRecordingSpan.get_span_context")
@@ -0,0 +1,191 @@
1
+ from lmnr.sdk.browser.utils import with_tracer_wrapper
2
+ from lmnr.sdk.utils import get_input_from_func_args, json_dumps
3
+ from lmnr.version import __version__
4
+
5
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
6
+ from opentelemetry.instrumentation.utils import unwrap
7
+ from opentelemetry.trace import get_tracer, Tracer
8
+ from typing import Collection
9
+ from wrapt import wrap_function_wrapper
10
+ import pydantic
11
+
12
+ try:
13
+ from skyvern import Skyvern
14
+ except ImportError as e:
15
+ raise ImportError(
16
+ f"Attempted to import {__file__}, but it is designed "
17
+ "to patch Skyvern, which is not installed. Use `pip install skyvern` "
18
+ "to install Skyvern or remove this import."
19
+ ) from e
20
+
21
+ _instruments = ("skyvern >= 0.1.0",)
22
+
23
+ WRAPPED_METHODS = [
24
+ {
25
+ "package": "skyvern.library.skyvern",
26
+ "object": "Skyvern", # Class name
27
+ "method": "run_task", # Method name
28
+ "span_name": "Skyvern.run_task",
29
+ "span_type": "DEFAULT",
30
+ },
31
+ {
32
+ "package": "skyvern.webeye.scraper.scraper",
33
+ # No "object" field for module-level functions
34
+ "method": "get_interactable_element_tree", # Function name
35
+ "span_name": "get_interactable_element_tree",
36
+ "span_type": "DEFAULT",
37
+ },
38
+ {
39
+ "package": "skyvern.forge.agent",
40
+ "object": "ForgeAgent",
41
+ "method": "execute_step",
42
+ "span_name": "ForgeAgent.execute_step",
43
+ "span_type": "DEFAULT",
44
+ },
45
+ {
46
+ "package": "skyvern.services.task_v2_service",
47
+ "method": "initialize_task_v2",
48
+ "span_name": "initialize_task_v2",
49
+ "span_type": "DEFAULT",
50
+ },
51
+ {
52
+ "package": "skyvern.services.task_v2_service",
53
+ "method": "run_task_v2_helper",
54
+ "span_name": "run_task_v2_helper",
55
+ "span_type": "DEFAULT",
56
+ },
57
+ {
58
+ "package": "skyvern.forge.sdk.workflow.models.block",
59
+ "object": "Block",
60
+ "method": "_generate_workflow_run_block_description",
61
+ "span_name": "Block._generate_workflow_run_block_description",
62
+ "span_type": "DEFAULT",
63
+ },
64
+ {
65
+ "package": "skyvern.webeye.actions.handler",
66
+ "method": "extract_information_for_navigation_goal",
67
+ "span_name": "extract_information_for_navigation_goal",
68
+ "span_type": "DEFAULT",
69
+ },
70
+ ]
71
+
72
+
73
+ @with_tracer_wrapper
74
+ async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
75
+ span_name = to_wrap.get("span_name")
76
+ attributes = {
77
+ "lmnr.span.type": to_wrap.get("span_type"),
78
+ }
79
+
80
+ attributes["lmnr.span.input"] = json_dumps(
81
+ get_input_from_func_args(wrapped, True, args, kwargs)
82
+ )
83
+
84
+ with tracer.start_as_current_span(span_name, attributes=attributes) as span:
85
+ try:
86
+ result = await wrapped(*args, **kwargs)
87
+
88
+ to_serialize = result
89
+ serialized = (
90
+ to_serialize.model_dump_json()
91
+ if isinstance(to_serialize, pydantic.BaseModel)
92
+ else json_dumps(to_serialize)
93
+ )
94
+ span.set_attribute("lmnr.span.output", serialized)
95
+ return result
96
+
97
+ except Exception as e:
98
+ span.record_exception(e)
99
+ raise
100
+
101
+
102
+ def instrument_llm_handler(tracer: Tracer):
103
+ from skyvern.forge import app
104
+
105
+ # Store the original handler
106
+ original_handler = app.LLM_API_HANDLER
107
+
108
+ async def wrapped_llm_handler(*args, **kwargs):
109
+
110
+ prompt_name = kwargs.get("prompt_name", "")
111
+
112
+ if prompt_name:
113
+ span_name = f"{prompt_name}"
114
+ else:
115
+ span_name = "app.LLM_API_HANDLER"
116
+
117
+ attributes = {
118
+ "lmnr.span.type": "DEFAULT",
119
+ }
120
+
121
+ with tracer.start_as_current_span(span_name, attributes=attributes) as span:
122
+ try:
123
+ result = await original_handler(*args, **kwargs)
124
+
125
+ to_serialize = result
126
+ serialized = (
127
+ to_serialize.model_dump_json()
128
+ if isinstance(to_serialize, pydantic.BaseModel)
129
+ else json_dumps(to_serialize)
130
+ )
131
+ span.set_attribute("lmnr.span.output", serialized)
132
+ return result
133
+ except Exception as e:
134
+ span.record_exception(e)
135
+ raise
136
+
137
+ # Replace the global handler
138
+ app.LLM_API_HANDLER = wrapped_llm_handler
139
+
140
+
141
+ class SkyvernInstrumentor(BaseInstrumentor):
142
+ def __init__(self):
143
+ super().__init__()
144
+
145
+ def instrumentation_dependencies(self) -> Collection[str]:
146
+ return _instruments
147
+
148
+ def _instrument(self, **kwargs):
149
+
150
+ tracer_provider = kwargs.get("tracer_provider")
151
+ tracer = get_tracer(__name__, __version__, tracer_provider)
152
+
153
+ instrument_llm_handler(tracer)
154
+
155
+ for wrapped_method in WRAPPED_METHODS:
156
+ wrap_package = wrapped_method.get("package")
157
+ wrap_object = wrapped_method.get("object")
158
+ wrap_method = wrapped_method.get("method")
159
+
160
+ # For class methods: "Class.method", for module functions: just "function_name"
161
+ if wrap_object:
162
+ target = f"{wrap_object}.{wrap_method}"
163
+ else:
164
+ target = wrap_method
165
+
166
+ try:
167
+ wrap_function_wrapper(
168
+ wrap_package,
169
+ target,
170
+ _wrap(
171
+ tracer,
172
+ wrapped_method,
173
+ ),
174
+ )
175
+ except ModuleNotFoundError:
176
+ pass # that's ok, we're not instrumenting everything
177
+
178
+ def _uninstrument(self, **kwargs):
179
+
180
+ for wrapped_method in WRAPPED_METHODS:
181
+ wrap_package = wrapped_method.get("package")
182
+ wrap_object = wrapped_method.get("object")
183
+ wrap_method = wrapped_method.get("method")
184
+
185
+ # For class methods: "package.Class", for module functions: just "package"
186
+ if wrap_object:
187
+ module_path = f"{wrap_package}.{wrap_object}"
188
+ else:
189
+ module_path = wrap_package
190
+
191
+ unwrap(module_path, wrap_method)