lmnr 0.7.6__py3-none-any.whl → 0.7.8__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/opentelemetry_lib/decorators/__init__.py +1 -2
- lmnr/opentelemetry_lib/litellm/__init__.py +9 -5
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +9 -6
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +2 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +389 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +9 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +2 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/playwright_otel.py +32 -0
- lmnr/sdk/utils.py +3 -3
- lmnr/version.py +1 -1
- {lmnr-0.7.6.dist-info → lmnr-0.7.8.dist-info}/METADATA +1 -1
- {lmnr-0.7.6.dist-info → lmnr-0.7.8.dist-info}/RECORD +15 -13
- {lmnr-0.7.6.dist-info → lmnr-0.7.8.dist-info}/WHEEL +0 -0
- {lmnr-0.7.6.dist-info → lmnr-0.7.8.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,4 @@
|
|
1
1
|
from functools import wraps
|
2
|
-
import logging
|
3
2
|
import pydantic
|
4
3
|
import orjson
|
5
4
|
import types
|
@@ -60,7 +59,7 @@ def json_dumps(data: dict) -> str:
|
|
60
59
|
).decode("utf-8")
|
61
60
|
except Exception:
|
62
61
|
# Log the exception and return a placeholder if serialization completely fails
|
63
|
-
|
62
|
+
logger.info("Failed to serialize data to JSON, type: %s", type(data))
|
64
63
|
return "{}" # Return an empty JSON object as a fallback
|
65
64
|
|
66
65
|
|
@@ -7,14 +7,13 @@ from opentelemetry.trace import SpanKind, Status, StatusCode, Tracer
|
|
7
7
|
from lmnr.opentelemetry_lib.litellm.utils import model_as_dict, set_span_attribute
|
8
8
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
9
9
|
|
10
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
10
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
11
|
+
get_current_context,
|
12
|
+
get_event_attributes_from_context,
|
13
|
+
)
|
11
14
|
from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
|
12
15
|
from lmnr.sdk.log import get_default_logger
|
13
16
|
|
14
|
-
from lmnr.opentelemetry_lib.opentelemetry.instrumentation.openai import (
|
15
|
-
OpenAIInstrumentor,
|
16
|
-
)
|
17
|
-
|
18
17
|
logger = get_default_logger(__name__)
|
19
18
|
|
20
19
|
SUPPORTED_CALL_TYPES = ["completion", "acompletion"]
|
@@ -46,6 +45,10 @@ try:
|
|
46
45
|
raise ValueError("Laminar must be initialized before LiteLLM callback")
|
47
46
|
|
48
47
|
if is_package_installed("openai"):
|
48
|
+
from lmnr.opentelemetry_lib.opentelemetry.instrumentation.openai import (
|
49
|
+
OpenAIInstrumentor,
|
50
|
+
)
|
51
|
+
|
49
52
|
openai_instrumentor = OpenAIInstrumentor()
|
50
53
|
if (
|
51
54
|
openai_instrumentor
|
@@ -117,6 +120,7 @@ try:
|
|
117
120
|
attributes={
|
118
121
|
"lmnr.internal.provider": "litellm",
|
119
122
|
},
|
123
|
+
context=get_current_context(),
|
120
124
|
)
|
121
125
|
try:
|
122
126
|
model = kwargs.get("model", "unknown")
|
@@ -375,6 +375,7 @@ def _build_from_streaming_response(
|
|
375
375
|
# total token count in every chunk is greater by prompt token count than it should be,
|
376
376
|
# thus this awkward logic here
|
377
377
|
if aggregated_usage_metadata.get("prompt_token_count") is None:
|
378
|
+
# or 0, not .get(key, 0), because sometimes the value is explicitly None
|
378
379
|
aggregated_usage_metadata["prompt_token_count"] = (
|
379
380
|
usage_dict.get("prompt_token_count") or 0
|
380
381
|
)
|
@@ -417,14 +418,15 @@ async def _abuild_from_streaming_response(
|
|
417
418
|
aggregated_usage_metadata = defaultdict(int)
|
418
419
|
model_version = None
|
419
420
|
async for chunk in response:
|
421
|
+
if chunk.model_version:
|
422
|
+
model_version = chunk.model_version
|
423
|
+
|
420
424
|
if chunk.candidates:
|
421
425
|
# Currently gemini throws an error if you pass more than one candidate
|
422
426
|
# with streaming
|
423
427
|
if chunk.candidates and len(chunk.candidates) > 0:
|
424
428
|
final_parts += chunk.candidates[0].content.parts or []
|
425
429
|
role = chunk.candidates[0].content.role or role
|
426
|
-
if chunk.model_version:
|
427
|
-
model_version = chunk.model_version
|
428
430
|
if chunk.usage_metadata:
|
429
431
|
usage_dict = to_dict(chunk.usage_metadata)
|
430
432
|
# prompt token count is sent in every chunk
|
@@ -432,11 +434,12 @@ async def _abuild_from_streaming_response(
|
|
432
434
|
# total token count in every chunk is greater by prompt token count than it should be,
|
433
435
|
# thus this awkward logic here
|
434
436
|
if aggregated_usage_metadata.get("prompt_token_count") is None:
|
435
|
-
|
436
|
-
|
437
|
+
# or 0, not .get(key, 0), because sometimes the value is explicitly None
|
438
|
+
aggregated_usage_metadata["prompt_token_count"] = (
|
439
|
+
usage_dict.get("prompt_token_count") or 0
|
437
440
|
)
|
438
|
-
aggregated_usage_metadata["total_token_count"] =
|
439
|
-
"total_token_count"
|
441
|
+
aggregated_usage_metadata["total_token_count"] = (
|
442
|
+
usage_dict.get("total_token_count") or 0
|
440
443
|
)
|
441
444
|
aggregated_usage_metadata["candidates_token_count"] += (
|
442
445
|
usage_dict.get("candidates_token_count") or 0
|
@@ -12,6 +12,7 @@ from .shared.config import Config
|
|
12
12
|
|
13
13
|
import openai
|
14
14
|
|
15
|
+
|
15
16
|
_OPENAI_VERSION = version("openai")
|
16
17
|
|
17
18
|
LMNR_TRACE_CONTENT = "LMNR_TRACE_CONTENT"
|
@@ -22,6 +23,7 @@ def is_openai_v1():
|
|
22
23
|
|
23
24
|
|
24
25
|
def is_azure_openai(instance):
|
26
|
+
|
25
27
|
return is_openai_v1() and isinstance(
|
26
28
|
instance._client, (openai.AsyncAzureOpenAI, openai.AzureOpenAI)
|
27
29
|
)
|
@@ -0,0 +1,389 @@
|
|
1
|
+
"""OpenTelemetry OpenHands AI instrumentation"""
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from typing import Collection
|
5
|
+
|
6
|
+
from lmnr.opentelemetry_lib.decorators import json_dumps
|
7
|
+
from lmnr.opentelemetry_lib.tracing.attributes import (
|
8
|
+
ASSOCIATION_PROPERTIES,
|
9
|
+
SESSION_ID,
|
10
|
+
USER_ID,
|
11
|
+
)
|
12
|
+
from lmnr.opentelemetry_lib.utils.wrappers import _with_tracer_wrapper
|
13
|
+
from lmnr.sdk.log import get_default_logger
|
14
|
+
from lmnr.sdk.utils import get_input_from_func_args
|
15
|
+
from lmnr import Laminar
|
16
|
+
from lmnr.version import __version__
|
17
|
+
|
18
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
19
|
+
from opentelemetry.instrumentation.utils import unwrap
|
20
|
+
from opentelemetry.trace import get_tracer, Tracer
|
21
|
+
from wrapt import wrap_function_wrapper
|
22
|
+
|
23
|
+
logger = get_default_logger(__name__)
|
24
|
+
|
25
|
+
_instruments = ("openhands-ai >= 0.9.0", "openhands-aci >= 0.1.0")
|
26
|
+
parent_spans = {}
|
27
|
+
|
28
|
+
|
29
|
+
def is_message_action(event) -> bool:
|
30
|
+
"""Check if event has action attribute equal to 'message'."""
|
31
|
+
return event and hasattr(event, "action") and event.action == "message"
|
32
|
+
|
33
|
+
|
34
|
+
def is_user_message(event) -> bool:
|
35
|
+
"""Check if event is a message action from user source."""
|
36
|
+
return (
|
37
|
+
is_message_action(event) and hasattr(event, "source") and event.source == "user"
|
38
|
+
)
|
39
|
+
|
40
|
+
|
41
|
+
def is_agent_message(event) -> bool:
|
42
|
+
"""Check if event is a message action from agent source."""
|
43
|
+
return (
|
44
|
+
is_message_action(event)
|
45
|
+
and hasattr(event, "source")
|
46
|
+
and event.source == "agent"
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
def is_agent_state_changed_to(event, state: str) -> bool:
|
51
|
+
"""Check if event is an agent_state_changed observation with specific state."""
|
52
|
+
return (
|
53
|
+
event
|
54
|
+
and hasattr(event, "observation")
|
55
|
+
and event.observation == "agent_state_changed"
|
56
|
+
and hasattr(event, "agent_state")
|
57
|
+
and event.agent_state == state
|
58
|
+
)
|
59
|
+
|
60
|
+
|
61
|
+
def get_handle_action_action(event) -> str:
|
62
|
+
"""Get the action of the handle_action event."""
|
63
|
+
if event and hasattr(event, "action"):
|
64
|
+
try:
|
65
|
+
return event.action.value
|
66
|
+
except Exception:
|
67
|
+
return event.action
|
68
|
+
return None
|
69
|
+
|
70
|
+
|
71
|
+
WRAPPED_METHODS = [
|
72
|
+
{
|
73
|
+
"package": "openhands.agenthub.codeact_agent.codeact_agent",
|
74
|
+
"object": "CodeActAgent",
|
75
|
+
"methods": [
|
76
|
+
{"method": "step"},
|
77
|
+
{"method": "response_to_actions"},
|
78
|
+
],
|
79
|
+
},
|
80
|
+
{
|
81
|
+
"package": "openhands.controller.agent_controller",
|
82
|
+
"object": "AgentController",
|
83
|
+
"methods": [
|
84
|
+
{"method": "_step", "async": True},
|
85
|
+
{
|
86
|
+
"method": "_handle_action",
|
87
|
+
"async": True,
|
88
|
+
"span_type": "TOOL",
|
89
|
+
},
|
90
|
+
{"method": "_handle_observation", "async": True},
|
91
|
+
{"method": "_handle_message_action", "async": True},
|
92
|
+
{"method": "on_event"},
|
93
|
+
{"method": "save_state"},
|
94
|
+
{"method": "get_trajectory"},
|
95
|
+
{"method": "start_delegate"},
|
96
|
+
{"method": "end_delegate"},
|
97
|
+
{"method": "_is_stuck"},
|
98
|
+
],
|
99
|
+
},
|
100
|
+
]
|
101
|
+
|
102
|
+
|
103
|
+
@_with_tracer_wrapper
|
104
|
+
def _wrap_on_event(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
105
|
+
"""Wrapper for on_event."""
|
106
|
+
controller_id = instance.id
|
107
|
+
user_id = instance.user_id
|
108
|
+
event = kwargs.get("event", args[0] if len(args) > 0 else None)
|
109
|
+
start_event = False
|
110
|
+
finish_event = False
|
111
|
+
user_message = ""
|
112
|
+
agent_message = ""
|
113
|
+
span_name = to_wrap.get("span_name")
|
114
|
+
span_type = to_wrap.get("span_type", "DEFAULT")
|
115
|
+
if event and hasattr(event, "action") and event.action == "system":
|
116
|
+
return wrapped(*args, **kwargs)
|
117
|
+
|
118
|
+
event_type = None
|
119
|
+
subtype = None
|
120
|
+
if event and hasattr(event, "action"):
|
121
|
+
event_type = "action"
|
122
|
+
try:
|
123
|
+
subtype = event.action.value
|
124
|
+
except Exception:
|
125
|
+
subtype = event.action
|
126
|
+
elif event and hasattr(event, "observation"):
|
127
|
+
event_type = "observation"
|
128
|
+
try:
|
129
|
+
subtype = event.observation.value
|
130
|
+
except Exception:
|
131
|
+
subtype = event.observation
|
132
|
+
if event_type and subtype:
|
133
|
+
span_name = f"event.{event_type}.{subtype}"
|
134
|
+
span_type = "EVENT"
|
135
|
+
|
136
|
+
# start trace on user message
|
137
|
+
if is_user_message(event):
|
138
|
+
user_message = event.content if hasattr(event, "content") else ""
|
139
|
+
start_event = True
|
140
|
+
# end trace on agent state change to finished or error
|
141
|
+
if is_agent_state_changed_to(event, "stopped") or is_agent_state_changed_to(
|
142
|
+
event, "awaiting_user_input"
|
143
|
+
):
|
144
|
+
finish_event = True
|
145
|
+
|
146
|
+
if is_agent_state_changed_to(event, "user_rejected"):
|
147
|
+
agent_message = "<user_rejected>"
|
148
|
+
|
149
|
+
if is_agent_message(event):
|
150
|
+
agent_message = event.content if hasattr(event, "content") else ""
|
151
|
+
|
152
|
+
if start_event:
|
153
|
+
if controller_id in parent_spans:
|
154
|
+
logger.debug(
|
155
|
+
"Received a message, but already have a span for this trace. Resetting span."
|
156
|
+
)
|
157
|
+
parent_spans[controller_id].end()
|
158
|
+
del parent_spans[controller_id]
|
159
|
+
parent_span = Laminar.start_span("conversation.turn", span_type="DEFAULT")
|
160
|
+
if user_id:
|
161
|
+
parent_span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
|
162
|
+
if user_message:
|
163
|
+
parent_span.set_attribute("lmnr.span.input", user_message)
|
164
|
+
parent_span.set_attribute(
|
165
|
+
f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", controller_id
|
166
|
+
)
|
167
|
+
parent_spans[controller_id] = parent_span
|
168
|
+
|
169
|
+
if controller_id in parent_spans:
|
170
|
+
with Laminar.use_span(parent_spans[controller_id]):
|
171
|
+
result = _wrap_sync_method_inner(
|
172
|
+
tracer,
|
173
|
+
{**to_wrap, "span_name": span_name, "span_type": span_type},
|
174
|
+
wrapped,
|
175
|
+
instance,
|
176
|
+
args,
|
177
|
+
kwargs,
|
178
|
+
)
|
179
|
+
if agent_message:
|
180
|
+
parent_spans[controller_id].set_attribute(
|
181
|
+
"lmnr.span.output", agent_message
|
182
|
+
)
|
183
|
+
if finish_event:
|
184
|
+
parent_spans[controller_id].end()
|
185
|
+
del parent_spans[controller_id]
|
186
|
+
return result
|
187
|
+
|
188
|
+
return wrapped(*args, **kwargs)
|
189
|
+
|
190
|
+
|
191
|
+
@_with_tracer_wrapper
|
192
|
+
async def _wrap_handle_action(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
193
|
+
"""Wrapper for on_event."""
|
194
|
+
event = kwargs.get("event", args[0] if len(args) > 0 else None)
|
195
|
+
if event and hasattr(event, "action"):
|
196
|
+
if event.action == "system":
|
197
|
+
return await wrapped(*args, **kwargs)
|
198
|
+
action_name = get_handle_action_action(event)
|
199
|
+
if action_name and action_name != "message":
|
200
|
+
to_wrap["span_name"] = f"action.{action_name}"
|
201
|
+
controller_id = instance.id
|
202
|
+
if controller_id not in parent_spans:
|
203
|
+
return await wrapped(*args, **kwargs)
|
204
|
+
return await _wrap_async_method_inner(
|
205
|
+
tracer, to_wrap, wrapped, instance, args, kwargs
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
def _wrap_sync_method_inner(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
210
|
+
"""Wrapper for synchronous methods."""
|
211
|
+
span_name = to_wrap.get("span_name")
|
212
|
+
|
213
|
+
with Laminar.start_as_current_span(
|
214
|
+
span_name,
|
215
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
216
|
+
input=json_dumps(
|
217
|
+
get_input_from_func_args(
|
218
|
+
wrapped, to_wrap.get("object") is not None, args, kwargs
|
219
|
+
)
|
220
|
+
),
|
221
|
+
) as span:
|
222
|
+
try:
|
223
|
+
result = wrapped(*args, **kwargs)
|
224
|
+
|
225
|
+
# Capture output
|
226
|
+
if not to_wrap.get("ignore_output"):
|
227
|
+
span.set_attribute("lmnr.span.output", json_dumps(result))
|
228
|
+
return result
|
229
|
+
|
230
|
+
except Exception as e:
|
231
|
+
span.record_exception(e)
|
232
|
+
raise
|
233
|
+
|
234
|
+
|
235
|
+
@_with_tracer_wrapper
|
236
|
+
def _wrap_sync_method(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
237
|
+
instance_id = None
|
238
|
+
if to_wrap.get("object") == "AgentController":
|
239
|
+
instance_id = instance.id
|
240
|
+
if to_wrap.get("object") == "ActionExecutionClient" and hasattr(instance, "sid"):
|
241
|
+
instance_id = instance.sid
|
242
|
+
if instance_id is not None and instance_id not in parent_spans:
|
243
|
+
return wrapped(*args, **kwargs)
|
244
|
+
return _wrap_sync_method_inner(tracer, to_wrap, wrapped, instance, args, kwargs)
|
245
|
+
|
246
|
+
|
247
|
+
async def _wrap_async_method_inner(
|
248
|
+
tracer: Tracer, to_wrap, wrapped, instance, args, kwargs
|
249
|
+
):
|
250
|
+
"""Wrapper for asynchronous methods."""
|
251
|
+
span_name = to_wrap.get("span_name")
|
252
|
+
instance_id = None
|
253
|
+
if to_wrap.get("object") == "AgentController":
|
254
|
+
instance_id = instance.id
|
255
|
+
if to_wrap.get("object") == "ActionExecutionClient" and hasattr(instance, "sid"):
|
256
|
+
instance_id = instance.sid
|
257
|
+
if instance_id is not None and instance_id not in parent_spans:
|
258
|
+
return await wrapped(*args, **kwargs)
|
259
|
+
|
260
|
+
with Laminar.start_as_current_span(
|
261
|
+
span_name,
|
262
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
263
|
+
input=json_dumps(
|
264
|
+
get_input_from_func_args(
|
265
|
+
wrapped, to_wrap.get("object") is not None, args, kwargs
|
266
|
+
)
|
267
|
+
),
|
268
|
+
) as span:
|
269
|
+
try:
|
270
|
+
result = await wrapped(*args, **kwargs)
|
271
|
+
|
272
|
+
# Capture output
|
273
|
+
if not to_wrap.get("ignore_output"):
|
274
|
+
span.set_attribute("lmnr.span.output", json_dumps(result))
|
275
|
+
return result
|
276
|
+
|
277
|
+
except Exception as e:
|
278
|
+
span.record_exception(e)
|
279
|
+
raise
|
280
|
+
|
281
|
+
|
282
|
+
@_with_tracer_wrapper
|
283
|
+
async def _wrap_async_method(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
284
|
+
"""Wrapper for asynchronous methods."""
|
285
|
+
return await _wrap_async_method_inner(
|
286
|
+
tracer, to_wrap, wrapped, instance, args, kwargs
|
287
|
+
)
|
288
|
+
|
289
|
+
|
290
|
+
class OpenHandsInstrumentor(BaseInstrumentor):
|
291
|
+
"""An instrumentor for OpenHands AI."""
|
292
|
+
|
293
|
+
def __init__(self):
|
294
|
+
super().__init__()
|
295
|
+
|
296
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
297
|
+
return _instruments
|
298
|
+
|
299
|
+
def _instrument(self, **kwargs):
|
300
|
+
"""Instrument OpenHands AI methods."""
|
301
|
+
tracer_provider = kwargs.get("tracer_provider")
|
302
|
+
tracer = get_tracer(__name__, __version__, tracer_provider)
|
303
|
+
|
304
|
+
for wrapped_config in WRAPPED_METHODS:
|
305
|
+
wrap_package = wrapped_config.get("package")
|
306
|
+
|
307
|
+
wrap_object = wrapped_config.get("object")
|
308
|
+
methods = wrapped_config.get("methods", [])
|
309
|
+
|
310
|
+
for method_config in methods:
|
311
|
+
|
312
|
+
wrap_method = method_config.get("method")
|
313
|
+
async_wrap = method_config.get("async", False)
|
314
|
+
windows_only = method_config.get("windows_only", False)
|
315
|
+
if windows_only and sys.platform != "win32":
|
316
|
+
continue
|
317
|
+
|
318
|
+
# Create the method configuration for the wrapper
|
319
|
+
method_wrapper_config = {
|
320
|
+
"package": wrap_package,
|
321
|
+
"object": wrap_object,
|
322
|
+
"method": wrap_method,
|
323
|
+
"span_name": method_config.get(
|
324
|
+
"span_name",
|
325
|
+
f"{wrap_object}.{wrap_method}" if wrap_object else wrap_method,
|
326
|
+
),
|
327
|
+
"span_type": method_config.get("span_type", "DEFAULT"),
|
328
|
+
"async": async_wrap,
|
329
|
+
}
|
330
|
+
|
331
|
+
# Determine the target for wrapping
|
332
|
+
if wrap_object:
|
333
|
+
target = f"{wrap_object}.{wrap_method}"
|
334
|
+
else:
|
335
|
+
target = wrap_method
|
336
|
+
|
337
|
+
if wrap_object == "AgentController" and wrap_method == "on_event":
|
338
|
+
wrap_function_wrapper(
|
339
|
+
wrap_package,
|
340
|
+
target,
|
341
|
+
_wrap_on_event(tracer, method_wrapper_config),
|
342
|
+
)
|
343
|
+
continue
|
344
|
+
if wrap_object == "AgentController" and wrap_method == "_handle_action":
|
345
|
+
wrap_function_wrapper(
|
346
|
+
wrap_package,
|
347
|
+
target,
|
348
|
+
_wrap_handle_action(tracer, method_wrapper_config),
|
349
|
+
)
|
350
|
+
continue
|
351
|
+
|
352
|
+
try:
|
353
|
+
if async_wrap:
|
354
|
+
wrap_function_wrapper(
|
355
|
+
wrap_package,
|
356
|
+
target,
|
357
|
+
_wrap_async_method(tracer, method_wrapper_config),
|
358
|
+
)
|
359
|
+
else:
|
360
|
+
wrap_function_wrapper(
|
361
|
+
wrap_package,
|
362
|
+
target,
|
363
|
+
_wrap_sync_method(tracer, method_wrapper_config),
|
364
|
+
)
|
365
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
366
|
+
logger.debug(f"Could not instrument {wrap_package}.{target}: {e}")
|
367
|
+
|
368
|
+
def _uninstrument(self, **kwargs):
|
369
|
+
"""Remove OpenHands AI instrumentation."""
|
370
|
+
for wrapped_config in WRAPPED_METHODS:
|
371
|
+
wrap_package = wrapped_config.get("package")
|
372
|
+
wrap_object = wrapped_config.get("object")
|
373
|
+
methods = wrapped_config.get("methods", [])
|
374
|
+
|
375
|
+
for method_config in methods:
|
376
|
+
wrap_method = method_config.get("method")
|
377
|
+
|
378
|
+
# Determine the module path for unwrapping
|
379
|
+
if wrap_object:
|
380
|
+
module_path = f"{wrap_package}.{wrap_object}"
|
381
|
+
else:
|
382
|
+
module_path = wrap_package
|
383
|
+
|
384
|
+
try:
|
385
|
+
unwrap(module_path, wrap_method)
|
386
|
+
except (AttributeError, ValueError) as e:
|
387
|
+
logger.debug(
|
388
|
+
f"Could not uninstrument {module_path}.{wrap_method}: {e}"
|
389
|
+
)
|
@@ -268,6 +268,15 @@ class OpenAIInstrumentorInitializer(InstrumentorInitializer):
|
|
268
268
|
)
|
269
269
|
|
270
270
|
|
271
|
+
class OpenHandsAIInstrumentorInitializer(InstrumentorInitializer):
|
272
|
+
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
273
|
+
if not is_package_installed("openhands-ai"):
|
274
|
+
return None
|
275
|
+
from ..opentelemetry.instrumentation.openhands_ai import OpenHandsInstrumentor
|
276
|
+
|
277
|
+
return OpenHandsInstrumentor()
|
278
|
+
|
279
|
+
|
271
280
|
class OpenTelemetryInstrumentorInitializer(InstrumentorInitializer):
|
272
281
|
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
273
282
|
from ..opentelemetry.instrumentation.opentelemetry import (
|
@@ -33,6 +33,7 @@ class Instruments(Enum):
|
|
33
33
|
MISTRAL = "mistral"
|
34
34
|
OLLAMA = "ollama"
|
35
35
|
OPENAI = "openai"
|
36
|
+
OPENHANDS = "openhands"
|
36
37
|
# Patch OpenTelemetry to fix DataDog's broken Span context
|
37
38
|
# See lmnr.opentelemetry_lib.opentelemetry.instrumentation.opentelemetry
|
38
39
|
# for more details.
|
@@ -75,6 +76,7 @@ INSTRUMENTATION_INITIALIZERS: dict[
|
|
75
76
|
Instruments.MISTRAL: initializers.MistralInstrumentorInitializer(),
|
76
77
|
Instruments.OLLAMA: initializers.OllamaInstrumentorInitializer(),
|
77
78
|
Instruments.OPENAI: initializers.OpenAIInstrumentorInitializer(),
|
79
|
+
Instruments.OPENHANDS: initializers.OpenHandsAIInstrumentorInitializer(),
|
78
80
|
Instruments.OPENTELEMETRY: initializers.OpenTelemetryInstrumentorInitializer(),
|
79
81
|
Instruments.PATCHRIGHT: initializers.PatchrightInstrumentorInitializer(),
|
80
82
|
Instruments.PINECONE: initializers.PineconeInstrumentorInitializer(),
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# TODO: Remove the same thing from openai, anthropic, etc, and use this instead
|
2
|
+
|
3
|
+
|
4
|
+
def _with_tracer_wrapper(func):
|
5
|
+
def _with_tracer(tracer, to_wrap):
|
6
|
+
def wrapper(wrapped, instance, args, kwargs):
|
7
|
+
return func(tracer, to_wrap, wrapped, instance, args, kwargs)
|
8
|
+
|
9
|
+
return wrapper
|
10
|
+
|
11
|
+
return _with_tracer
|
@@ -154,6 +154,26 @@ async def _wrap_bring_to_front_async(
|
|
154
154
|
await take_full_snapshot_async(instance)
|
155
155
|
|
156
156
|
|
157
|
+
@with_tracer_and_client_wrapper
|
158
|
+
def _wrap_browser_new_page_sync(
|
159
|
+
tracer: Tracer, client: LaminarClient, to_wrap, wrapped, instance, args, kwargs
|
160
|
+
):
|
161
|
+
page = wrapped(*args, **kwargs)
|
162
|
+
session_id = str(uuid.uuid4().hex)
|
163
|
+
start_recording_events_sync(page, session_id, client)
|
164
|
+
return page
|
165
|
+
|
166
|
+
|
167
|
+
@with_tracer_and_client_wrapper
|
168
|
+
async def _wrap_browser_new_page_async(
|
169
|
+
tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
|
170
|
+
):
|
171
|
+
page = await wrapped(*args, **kwargs)
|
172
|
+
session_id = str(uuid.uuid4().hex)
|
173
|
+
await start_recording_events_async(page, session_id, client)
|
174
|
+
return page
|
175
|
+
|
176
|
+
|
157
177
|
WRAPPED_METHODS = [
|
158
178
|
{
|
159
179
|
"package": "playwright.sync_api",
|
@@ -191,6 +211,12 @@ WRAPPED_METHODS = [
|
|
191
211
|
"method": "bring_to_front",
|
192
212
|
"wrapper": _wrap_bring_to_front_sync,
|
193
213
|
},
|
214
|
+
{
|
215
|
+
"package": "playwright.sync_api",
|
216
|
+
"object": "Browser",
|
217
|
+
"method": "new_page",
|
218
|
+
"wrapper": _wrap_browser_new_page_sync,
|
219
|
+
},
|
194
220
|
]
|
195
221
|
|
196
222
|
WRAPPED_METHODS_ASYNC = [
|
@@ -230,6 +256,12 @@ WRAPPED_METHODS_ASYNC = [
|
|
230
256
|
"method": "bring_to_front",
|
231
257
|
"wrapper": _wrap_bring_to_front_async,
|
232
258
|
},
|
259
|
+
{
|
260
|
+
"package": "playwright.async_api",
|
261
|
+
"object": "Browser",
|
262
|
+
"method": "new_page",
|
263
|
+
"wrapper": _wrap_browser_new_page_async,
|
264
|
+
},
|
233
265
|
]
|
234
266
|
|
235
267
|
|
lmnr/sdk/utils.py
CHANGED
@@ -132,13 +132,13 @@ def is_otel_attribute_value_type(value: typing.Any) -> bool:
|
|
132
132
|
|
133
133
|
def format_id(id_value: str | int | uuid.UUID) -> str:
|
134
134
|
"""Format trace/span/evaluation ID to a UUID string, or return valid UUID strings as-is.
|
135
|
-
|
135
|
+
|
136
136
|
Args:
|
137
137
|
id_value: The ID in various formats (UUID, int, or valid UUID string)
|
138
|
-
|
138
|
+
|
139
139
|
Returns:
|
140
140
|
str: UUID string representation
|
141
|
-
|
141
|
+
|
142
142
|
Raises:
|
143
143
|
ValueError: If id_value cannot be converted to a valid UUID
|
144
144
|
"""
|
lmnr/version.py
CHANGED
@@ -2,8 +2,8 @@ lmnr/__init__.py,sha256=8be7b56ab62735fd54ca90a0642784c6153ed1d6e0f12734619ca061
|
|
2
2
|
lmnr/cli.py,sha256=b8780b51f37fe9e20db5495c41d3ad3837f6b48f408b09a58688d017850c0796,6047
|
3
3
|
lmnr/opentelemetry_lib/.flake8,sha256=6c2c6e0e51b1dd8439e501ca3e21899277076a787da868d0254ba37056b79405,150
|
4
4
|
lmnr/opentelemetry_lib/__init__.py,sha256=1350e8d12ea2f422751ab3a80d7d32d10c27ad8e4c2989407771dc6e544d9c65,2350
|
5
|
-
lmnr/opentelemetry_lib/decorators/__init__.py,sha256=
|
6
|
-
lmnr/opentelemetry_lib/litellm/__init__.py,sha256=
|
5
|
+
lmnr/opentelemetry_lib/decorators/__init__.py,sha256=b9bab9acd44c7d4d98660a8de1f4e42b9387170c62e3239fba174a8461255ead,11795
|
6
|
+
lmnr/opentelemetry_lib/litellm/__init__.py,sha256=d67d18581b42d7e5d9c724bd201044fb7a04d79e7162f5a47007c0a1f413141a,15852
|
7
7
|
lmnr/opentelemetry_lib/litellm/utils.py,sha256=da8cf0553f82dc7203109f117a4c7b4185e8baf34caad12d7823875515201a27,539
|
8
8
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py,sha256=2604189b7598edb5404ddbcd0775bdf2dc506dd5e6319eef4e4724e39c420301,23276
|
9
9
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py,sha256=972919b821b9b7e5dc7cd191ba7e78b30b6efa5d63514e8cb301996d6386392c,369
|
@@ -13,7 +13,7 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py,sha
|
|
13
13
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py,sha256=7ca9f49e4d9a3bac292d13a8ee9827fdfb8a46d13ebdcbbfbac9c5584d11eaf3,13441
|
14
14
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py,sha256=0044f02da8b99322fdbf3f8f6663f04ff5d1295ddae92a635fd16eb685d5fbb6,5386
|
15
15
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py,sha256=5aacde4ca55ef50ed07a239ad8a86889e0621b1cc72be19bd93be7c9e20910a9,23
|
16
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=
|
16
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=1eab6773a3c89c207fe0b508555062872cbf37674183a0eb4ed7e20785cef870,20701
|
17
17
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py,sha256=db9cdebc9ee0dccb493ffe608eede3047efec20ed26c3924b72b2e50edbd92c2,245
|
18
18
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py,sha256=857a6bc52f8bfd4da72786173615d31faaf3f9378f8f6150ffe8f6f9c4bb78f9,685
|
19
19
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py,sha256=f1248196246826d899304e510c4c2df74088d8169d28f1d0aed578a7a6c3cbfd,7669
|
@@ -35,27 +35,29 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wr
|
|
35
35
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py,sha256=9c96455b5ca2064dd3a9fb570d78b14ebbdf3d02f8e33255ee9e301c31336c9e,3043
|
36
36
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py,sha256=3c27c21b1aeb02bc19a91fb8c05717ae1c10ab4b01300c664aba42e0f50cb5a3,876
|
37
37
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py,sha256=9650a0e4ad2d3bfb2a072590da189bcf4f807aca070945af26a9f9b99d779b77,2021
|
38
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py,sha256=
|
38
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py,sha256=6b7ebc420e76c266f723248463c09175f536e10166015db12075e5e05f1c0f51,4651
|
39
39
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py,sha256=7f43421e052bd8f64d5d5b03170a3b7187c2ce038362fa15b5d1d0c43bc1a40d,6143
|
40
40
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py,sha256=7bdbf691ac89efb42ade686b7dbe69bd139a84f48482a013b7b68d3baa5b9c98,13527
|
41
41
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py,sha256=558036c734559b3526647c1b18cfb986699e8fb322855af72ea054c2e458f721,10404
|
42
42
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py,sha256=4809cde003e5892822828b373aa3e43a8adbaee4ff443f198401003f43c15e8a,4366
|
43
43
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py,sha256=b3853c60c58a36ba64de184211ac2d112bb8b53c4af62f0fc716fb87d168fd4b,24790
|
44
44
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py,sha256=4f39aaa913f3e49b0c174bc23028687d00bfaffc745bd3fe241e0ae6b442bed1,24
|
45
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py,sha256=8f5f73528e1e0ced28938cbedf5b9774b408e178ae42191284f4630d0aa99beb,13648
|
45
46
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py,sha256=1f86cdf738e2f68586b0a4569bb1e40edddd85c529f511ef49945ddb7b61fab5,2648
|
46
47
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py,sha256=764e4fe979fb08d7821419a3cc5c3ae89a6664b626ef928259f8f175c939eaea,6334
|
47
48
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py,sha256=90aa8558467d7e469fe1a6c75372c113da403557715f03b522b2fab94b287c40,6320
|
48
49
|
lmnr/opentelemetry_lib/tracing/__init__.py,sha256=b96aee7590af1853fffc4c3d8ce9127a67e1ce589f695a99aabe6b37d70b0e48,10203
|
49
|
-
lmnr/opentelemetry_lib/tracing/_instrument_initializers.py,sha256=
|
50
|
+
lmnr/opentelemetry_lib/tracing/_instrument_initializers.py,sha256=f66dbe208fc1bd4075c0725561f6151a4c7622e6ecfa961b7ad6674d18dfb97f,15180
|
50
51
|
lmnr/opentelemetry_lib/tracing/attributes.py,sha256=a879e337ff4e8569a4454544d303ccbc3b04bd42e1cdb765eb563aeaa08f731d,1653
|
51
52
|
lmnr/opentelemetry_lib/tracing/context.py,sha256=83f842be0fc29a96647cbf005c39ea761b0fb5913c4102f965411f47906a6135,4103
|
52
53
|
lmnr/opentelemetry_lib/tracing/exporter.py,sha256=6af8e61fd873e8f5db315d9b9f1edbf46b860ba7e50140f0bdcc6864c6d35a03,2082
|
53
|
-
lmnr/opentelemetry_lib/tracing/instruments.py,sha256=
|
54
|
+
lmnr/opentelemetry_lib/tracing/instruments.py,sha256=950c351bbc05a7665e6c0eb0ed1bafc1b4b2a037ed1086786ec8496802ecef2e,5618
|
54
55
|
lmnr/opentelemetry_lib/tracing/processor.py,sha256=cbc70f138e70c878ef57b02a2c46ef48dd7f694a522623a82dff1623b73d1e1c,3353
|
55
56
|
lmnr/opentelemetry_lib/tracing/tracer.py,sha256=33769a9a97385f5697eb0e0a6b1813a57ed956c7a8379d7ac2523e700e7dd528,1362
|
56
57
|
lmnr/opentelemetry_lib/utils/__init__.py,sha256=a4d85fd06def4dde5c728734de2d4c5c36eb89c49a8aa09b8b50cb5a149e90af,604
|
57
58
|
lmnr/opentelemetry_lib/utils/json_encoder.py,sha256=74ae9bfdac6bef42182fb56ff9bbb8c27b6f0c3bb29eda2ab0769d76a5fb3f9f,463
|
58
59
|
lmnr/opentelemetry_lib/utils/package_check.py,sha256=f8274186c96815c996a25fae06bf913f0bb7c835507739949f37c03bbe5d9ca9,527
|
60
|
+
lmnr/opentelemetry_lib/utils/wrappers.py,sha256=f7b1134809f2c408976a71e661fee5cb1aad172f4467846994205a60bf994e9f,330
|
59
61
|
lmnr/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
60
62
|
lmnr/sdk/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
61
63
|
lmnr/sdk/browser/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
@@ -63,7 +65,7 @@ lmnr/sdk/browser/browser_use_cdp_otel.py,sha256=aa14e3ea7bd0980a4b1fc2d9a5d40d38
|
|
63
65
|
lmnr/sdk/browser/browser_use_otel.py,sha256=e5549878c07bad451efef9f460ce52202284cff50075bce700ca61749102c5eb,5065
|
64
66
|
lmnr/sdk/browser/cdp_utils.py,sha256=3f43e4b9958e752b1b9aa0a3ae6321ef9e1cb66b3f4ca07990584b2c60c17a97,26103
|
65
67
|
lmnr/sdk/browser/patchright_otel.py,sha256=9d22ab1f28f1eddbcfd0032a14fe306bfe00bfc7f11128cb99836c4dd15fb7c8,4800
|
66
|
-
lmnr/sdk/browser/playwright_otel.py,sha256=
|
68
|
+
lmnr/sdk/browser/playwright_otel.py,sha256=859d220d856c8fe7104863efca0c6a3ed5464d778675e07d7f79c48f73d5e838,10416
|
67
69
|
lmnr/sdk/browser/pw_utils.py,sha256=a75769eb977d8e56c38a0eefad09b87550b872f8d4df186b36a8c4d4af2bffaf,29021
|
68
70
|
lmnr/sdk/browser/recorder/record.umd.min.cjs,sha256=f09c09052c2fc474efb0405e63d8d26ed2184b994513ce8aee04efdac8be155d,181235
|
69
71
|
lmnr/sdk/browser/utils.py,sha256=4a668776d2938108d25fbcecd61c8e1710a4da3e56230d5fefca5964dd09e3c1,2371
|
@@ -90,9 +92,9 @@ lmnr/sdk/evaluations.py,sha256=895d0b1af77395bddfbf69388679eb03f8f4d2ef03facab27
|
|
90
92
|
lmnr/sdk/laminar.py,sha256=24d680407ce694f1a7ec0e9c0524eae3deb7d638ad5caff3a591ddf7963ad480,37533
|
91
93
|
lmnr/sdk/log.py,sha256=9edfd83263f0d4845b1b2d1beeae2b4ed3f8628de941f371a893d72b79c348d4,2213
|
92
94
|
lmnr/sdk/types.py,sha256=f8a8368e225c4d2f82df54d92f029065afb60c3eff494c77c6e574963ed524ff,13454
|
93
|
-
lmnr/sdk/utils.py,sha256=
|
94
|
-
lmnr/version.py,sha256=
|
95
|
-
lmnr-0.7.
|
96
|
-
lmnr-0.7.
|
97
|
-
lmnr-0.7.
|
98
|
-
lmnr-0.7.
|
95
|
+
lmnr/sdk/utils.py,sha256=0c5a81c305dcd3922f4b31c4f42cf83719c03888725838395adae167de92db76,5019
|
96
|
+
lmnr/version.py,sha256=fafd83a5d984558d181b537237ee4ba4e9ccbad408b74e243065597b77580152,1321
|
97
|
+
lmnr-0.7.8.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
|
98
|
+
lmnr-0.7.8.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
|
99
|
+
lmnr-0.7.8.dist-info/METADATA,sha256=272987c3279723e7609210aef23cf45f706ecd6750fc45d6a0e08d409302acf0,14196
|
100
|
+
lmnr-0.7.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|