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.
- lmnr/__init__.py +6 -15
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/{cli.py → cli/evals.py} +20 -102
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +9 -2
- lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
- lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
- lmnr/opentelemetry_lib/litellm/utils.py +82 -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 +191 -129
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
- 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 +16 -16
- 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 +59 -61
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
- lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
- lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
- lmnr/opentelemetry_lib/tracing/processor.py +128 -30
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -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 +12 -12
- 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 +18 -44
- lmnr/sdk/browser/playwright_otel.py +104 -187
- lmnr/sdk/browser/pw_utils.py +249 -210
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +1 -1
- lmnr/sdk/client/asynchronous/async_client.py +47 -15
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +83 -17
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +47 -15
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +103 -23
- lmnr/sdk/evaluations.py +122 -33
- lmnr/sdk/laminar.py +816 -333
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +124 -143
- lmnr/sdk/utils.py +115 -2
- lmnr/version.py +1 -1
- {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
- 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/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
- lmnr/sdk/client/synchronous/resources/agent.py +0 -323
- lmnr/sdk/datasets.py +0 -60
- lmnr-0.6.16.dist-info/LICENSE +0 -75
- lmnr-0.6.16.dist-info/RECORD +0 -61
- lmnr-0.6.16.dist-info/WHEEL +0 -4
- 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",
|
|
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",
|
|
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
|
-
|