lmnr 0.6.16__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 (113) hide show
  1. lmnr/__init__.py +6 -15
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/{cli.py → cli/evals.py} +20 -102
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +9 -2
  7. lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
  8. lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
  9. lmnr/opentelemetry_lib/litellm/utils.py +82 -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 +191 -129
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  56. lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
  57. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
  58. lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
  59. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  60. lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
  61. lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
  62. lmnr/opentelemetry_lib/tracing/processor.py +128 -30
  63. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  64. lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
  65. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  66. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  67. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  68. lmnr/sdk/browser/background_send_events.py +158 -0
  69. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  70. lmnr/sdk/browser/browser_use_otel.py +12 -12
  71. lmnr/sdk/browser/bubus_otel.py +71 -0
  72. lmnr/sdk/browser/cdp_utils.py +518 -0
  73. lmnr/sdk/browser/inject_script.js +514 -0
  74. lmnr/sdk/browser/patchright_otel.py +18 -44
  75. lmnr/sdk/browser/playwright_otel.py +104 -187
  76. lmnr/sdk/browser/pw_utils.py +249 -210
  77. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  78. lmnr/sdk/browser/utils.py +1 -1
  79. lmnr/sdk/client/asynchronous/async_client.py +47 -15
  80. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
  81. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  82. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  83. lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
  84. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  85. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  86. lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
  87. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  88. lmnr/sdk/client/synchronous/resources/evals.py +83 -17
  89. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  90. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  91. lmnr/sdk/client/synchronous/sync_client.py +47 -15
  92. lmnr/sdk/datasets/__init__.py +94 -0
  93. lmnr/sdk/datasets/file_utils.py +91 -0
  94. lmnr/sdk/decorators.py +103 -23
  95. lmnr/sdk/evaluations.py +122 -33
  96. lmnr/sdk/laminar.py +816 -333
  97. lmnr/sdk/log.py +7 -2
  98. lmnr/sdk/types.py +124 -143
  99. lmnr/sdk/utils.py +115 -2
  100. lmnr/version.py +1 -1
  101. {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
  102. lmnr-0.7.26.dist-info/RECORD +116 -0
  103. lmnr-0.7.26.dist-info/WHEEL +4 -0
  104. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  105. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  106. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  107. lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
  108. lmnr/sdk/client/synchronous/resources/agent.py +0 -323
  109. lmnr/sdk/datasets.py +0 -60
  110. lmnr-0.6.16.dist-info/LICENSE +0 -75
  111. lmnr-0.6.16.dist-info/RECORD +0 -61
  112. lmnr-0.6.16.dist-info/WHEEL +0 -4
  113. lmnr-0.6.16.dist-info/entry_points.txt +0 -3
@@ -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")
@@ -1,6 +1,5 @@
1
- from lmnr.opentelemetry_lib.decorators import json_dumps
2
1
  from lmnr.sdk.browser.utils import with_tracer_wrapper
3
- from lmnr.sdk.utils import get_input_from_func_args
2
+ from lmnr.sdk.utils import get_input_from_func_args, json_dumps
4
3
  from lmnr.version import __version__
5
4
 
6
5
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -25,14 +24,14 @@ WRAPPED_METHODS = [
25
24
  {
26
25
  "package": "skyvern.library.skyvern",
27
26
  "object": "Skyvern", # Class name
28
- "method": "run_task", # Method name
27
+ "method": "run_task", # Method name
29
28
  "span_name": "Skyvern.run_task",
30
29
  "span_type": "DEFAULT",
31
30
  },
32
31
  {
33
32
  "package": "skyvern.webeye.scraper.scraper",
34
33
  # No "object" field for module-level functions
35
- "method": "get_interactable_element_tree", # Function name
34
+ "method": "get_interactable_element_tree", # Function name
36
35
  "span_name": "get_interactable_element_tree",
37
36
  "span_type": "DEFAULT",
38
37
  },
@@ -43,31 +42,31 @@ WRAPPED_METHODS = [
43
42
  "span_name": "ForgeAgent.execute_step",
44
43
  "span_type": "DEFAULT",
45
44
  },
46
- {
47
- "package": "skyvern.services.task_v2_service",
48
- "method": "initialize_task_v2",
49
- "span_name": "initialize_task_v2",
50
- "span_type": "DEFAULT",
45
+ {
46
+ "package": "skyvern.services.task_v2_service",
47
+ "method": "initialize_task_v2",
48
+ "span_name": "initialize_task_v2",
49
+ "span_type": "DEFAULT",
51
50
  },
52
- {
53
- "package": "skyvern.services.task_v2_service",
54
- "method": "run_task_v2_helper",
55
- "span_name": "run_task_v2_helper",
56
- "span_type": "DEFAULT",
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",
57
56
  },
58
- {
57
+ {
59
58
  "package": "skyvern.forge.sdk.workflow.models.block",
60
59
  "object": "Block",
61
- "method": "_generate_workflow_run_block_description",
62
- "span_name": "Block._generate_workflow_run_block_description",
63
- "span_type": "DEFAULT",
64
- },
65
- {
66
- "package": "skyvern.webeye.actions.handler",
67
- "method": "extract_information_for_navigation_goal",
68
- "span_name": "extract_information_for_navigation_goal",
69
- "span_type": "DEFAULT",
70
- },
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
+ },
71
70
  ]
72
71
 
73
72
 
@@ -77,36 +76,36 @@ async def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
77
76
  attributes = {
78
77
  "lmnr.span.type": to_wrap.get("span_type"),
79
78
  }
80
-
79
+
81
80
  attributes["lmnr.span.input"] = json_dumps(
82
81
  get_input_from_func_args(wrapped, True, args, kwargs)
83
82
  )
84
-
83
+
85
84
  with tracer.start_as_current_span(span_name, attributes=attributes) as span:
86
- try:
87
- result = await wrapped(*args, **kwargs)
88
-
89
- to_serialize = result
90
- serialized = (
91
- to_serialize.model_dump_json()
92
- if isinstance(to_serialize, pydantic.BaseModel)
93
- else json_dumps(to_serialize)
94
- )
95
- span.set_attribute("lmnr.span.output", serialized)
96
- return result
97
-
98
- except Exception as e:
99
- span.record_exception(e)
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)
100
99
  raise
101
100
 
102
101
 
103
- def instrument_llm_handler(tracer: Tracer):
104
- from skyvern.forge import app
105
-
106
- # Store the original handler
107
- original_handler = app.LLM_API_HANDLER
108
-
109
- async def wrapped_llm_handler(*args, **kwargs):
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):
110
109
 
111
110
  prompt_name = kwargs.get("prompt_name", "")
112
111
 
@@ -115,13 +114,13 @@ def instrument_llm_handler(tracer: Tracer):
115
114
  else:
116
115
  span_name = "app.LLM_API_HANDLER"
117
116
 
118
- attributes = {
119
- "lmnr.span.type": "DEFAULT",
120
- }
117
+ attributes = {
118
+ "lmnr.span.type": "DEFAULT",
119
+ }
121
120
 
122
- with tracer.start_as_current_span(span_name, attributes=attributes) as span:
123
- try:
124
- result = await original_handler(*args, **kwargs)
121
+ with tracer.start_as_current_span(span_name, attributes=attributes) as span:
122
+ try:
123
+ result = await original_handler(*args, **kwargs)
125
124
 
126
125
  to_serialize = result
127
126
  serialized = (
@@ -129,13 +128,13 @@ def instrument_llm_handler(tracer: Tracer):
129
128
  if isinstance(to_serialize, pydantic.BaseModel)
130
129
  else json_dumps(to_serialize)
131
130
  )
132
- span.set_attribute("lmnr.span.output", serialized)
133
- return result
134
- except Exception as e:
135
- span.record_exception(e)
131
+ span.set_attribute("lmnr.span.output", serialized)
132
+ return result
133
+ except Exception as e:
134
+ span.record_exception(e)
136
135
  raise
137
-
138
- # Replace the global handler
136
+
137
+ # Replace the global handler
139
138
  app.LLM_API_HANDLER = wrapped_llm_handler
140
139
 
141
140
 
@@ -190,4 +189,3 @@ class SkyvernInstrumentor(BaseInstrumentor):
190
189
  module_path = wrap_package
191
190
 
192
191
  unwrap(module_path, wrap_method)
193
-