lmnr 0.7.6__tar.gz → 0.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {lmnr-0.7.6 → lmnr-0.7.7}/PKG-INFO +1 -1
  2. {lmnr-0.7.6 → lmnr-0.7.7}/pyproject.toml +1 -1
  3. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/decorators/__init__.py +1 -2
  4. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/litellm/__init__.py +9 -5
  5. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +2 -0
  6. lmnr-0.7.7/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +389 -0
  7. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +9 -0
  8. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/instruments.py +2 -0
  9. lmnr-0.7.7/src/lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  10. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/playwright_otel.py +32 -0
  11. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/utils.py +3 -3
  12. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/version.py +1 -1
  13. {lmnr-0.7.6 → lmnr-0.7.7}/README.md +0 -0
  14. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/__init__.py +0 -0
  15. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/cli.py +0 -0
  16. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/.flake8 +0 -0
  17. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/__init__.py +0 -0
  18. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/litellm/utils.py +0 -0
  19. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +0 -0
  20. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +0 -0
  21. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +0 -0
  22. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +0 -0
  23. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +0 -0
  24. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +0 -0
  25. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +0 -0
  26. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +0 -0
  27. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +0 -0
  28. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +0 -0
  29. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +0 -0
  30. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +0 -0
  31. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +0 -0
  32. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +0 -0
  33. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +0 -0
  34. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +0 -0
  35. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +0 -0
  36. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +0 -0
  37. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +0 -0
  38. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +0 -0
  39. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +0 -0
  40. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +0 -0
  41. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +0 -0
  42. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +0 -0
  43. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +0 -0
  44. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +0 -0
  45. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +0 -0
  46. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +0 -0
  47. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +0 -0
  48. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +0 -0
  49. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +0 -0
  50. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +0 -0
  51. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -0
  52. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +0 -0
  53. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +0 -0
  54. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +0 -0
  55. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +0 -0
  56. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +0 -0
  57. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +0 -0
  58. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/__init__.py +0 -0
  59. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/attributes.py +0 -0
  60. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/context.py +0 -0
  61. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/exporter.py +0 -0
  62. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/processor.py +0 -0
  63. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/tracing/tracer.py +0 -0
  64. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/utils/__init__.py +0 -0
  65. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/utils/json_encoder.py +0 -0
  66. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/opentelemetry_lib/utils/package_check.py +0 -0
  67. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/py.typed +0 -0
  68. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/__init__.py +0 -0
  69. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/__init__.py +0 -0
  70. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/browser_use_cdp_otel.py +0 -0
  71. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/browser_use_otel.py +0 -0
  72. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/cdp_utils.py +0 -0
  73. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/patchright_otel.py +0 -0
  74. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/pw_utils.py +0 -0
  75. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/recorder/record.umd.min.cjs +0 -0
  76. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/browser/utils.py +0 -0
  77. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/async_client.py +0 -0
  78. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/__init__.py +0 -0
  79. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/agent.py +0 -0
  80. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/base.py +0 -0
  81. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/browser_events.py +0 -0
  82. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/evals.py +0 -0
  83. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/evaluators.py +0 -0
  84. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/asynchronous/resources/tags.py +0 -0
  85. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/__init__.py +0 -0
  86. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/agent.py +0 -0
  87. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/base.py +0 -0
  88. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/browser_events.py +0 -0
  89. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/evals.py +0 -0
  90. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/evaluators.py +0 -0
  91. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/resources/tags.py +0 -0
  92. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/client/synchronous/sync_client.py +0 -0
  93. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/datasets.py +0 -0
  94. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/decorators.py +0 -0
  95. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/eval_control.py +0 -0
  96. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/evaluations.py +0 -0
  97. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/laminar.py +0 -0
  98. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/log.py +0 -0
  99. {lmnr-0.7.6 → lmnr-0.7.7}/src/lmnr/sdk/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmnr
3
- Version: 0.7.6
3
+ Version: 0.7.7
4
4
  Summary: Python SDK for Laminar
5
5
  Author: lmnr.ai
6
6
  Author-email: lmnr.ai <founders@lmnr.ai>
@@ -6,7 +6,7 @@
6
6
 
7
7
  [project]
8
8
  name = "lmnr"
9
- version = "0.7.6"
9
+ version = "0.7.7"
10
10
  description = "Python SDK for Laminar"
11
11
  authors = [
12
12
  { name = "lmnr.ai", email = "founders@lmnr.ai" }
@@ -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
- logging.warning("Failed to serialize data to JSON, type: %s", type(data))
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 get_event_attributes_from_context
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")
@@ -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
 
@@ -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
  """
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.7.6"
6
+ __version__ = "0.7.7"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes