lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmnr/__init__.py +32 -11
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/cli/evals.py +111 -0
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +70 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
- lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
- lmnr/opentelemetry_lib/litellm/utils.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
- lmnr/opentelemetry_lib/tracing/processor.py +193 -0
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +18 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/__init__.py +0 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +142 -0
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +151 -0
- lmnr/sdk/browser/playwright_otel.py +322 -0
- lmnr/sdk/browser/pw_utils.py +363 -0
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +70 -0
- lmnr/sdk/client/asynchronous/async_client.py +180 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +263 -0
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/sync_client.py +191 -0
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +163 -26
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +403 -191
- lmnr/sdk/laminar.py +1080 -549
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +246 -134
- lmnr/sdk/utils.py +151 -7
- lmnr/version.py +46 -0
- {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/cli.py +0 -101
- lmnr/openllmetry_sdk/.python-version +0 -1
- lmnr/openllmetry_sdk/__init__.py +0 -72
- lmnr/openllmetry_sdk/config/__init__.py +0 -9
- lmnr/openllmetry_sdk/decorators/base.py +0 -185
- lmnr/openllmetry_sdk/instruments.py +0 -38
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- lmnr/openllmetry_sdk/utils/package_check.py +0 -7
- lmnr/openllmetry_sdk/version.py +0 -1
- lmnr/sdk/datasets.py +0 -55
- lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
- lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
- lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
- lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
|
@@ -0,0 +1 @@
|
|
|
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)
|