lmnr 0.6.16__py3-none-any.whl → 0.7.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmnr/__init__.py +6 -15
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/{cli.py → cli/evals.py} +20 -102
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +9 -2
- lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
- lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
- lmnr/opentelemetry_lib/litellm/utils.py +82 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
- lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
- lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
- lmnr/opentelemetry_lib/tracing/processor.py +128 -30
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +12 -12
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +18 -44
- lmnr/sdk/browser/playwright_otel.py +104 -187
- lmnr/sdk/browser/pw_utils.py +249 -210
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +1 -1
- lmnr/sdk/client/asynchronous/async_client.py +47 -15
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +83 -17
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +47 -15
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +103 -23
- lmnr/sdk/evaluations.py +122 -33
- lmnr/sdk/laminar.py +816 -333
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +124 -143
- lmnr/sdk/utils.py +115 -2
- lmnr/version.py +1 -1
- {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
- lmnr/sdk/client/synchronous/resources/agent.py +0 -323
- lmnr/sdk/datasets.py +0 -60
- lmnr-0.6.16.dist-info/LICENSE +0 -75
- lmnr-0.6.16.dist-info/RECORD +0 -61
- lmnr-0.6.16.dist-info/WHEEL +0 -4
- lmnr-0.6.16.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from lmnr import Laminar
|
|
6
|
+
from lmnr.opentelemetry_lib.tracing import get_current_context
|
|
7
|
+
from lmnr.opentelemetry_lib.tracing.attributes import SPAN_IDS_PATH, SPAN_PATH
|
|
8
|
+
from lmnr.sdk.log import get_default_logger
|
|
9
|
+
from lmnr.sdk.utils import get_input_from_func_args, is_method, json_dumps
|
|
10
|
+
|
|
11
|
+
from opentelemetry import trace
|
|
12
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
13
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
14
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
15
|
+
from opentelemetry.trace import Status, StatusCode
|
|
16
|
+
from typing import Any, Collection
|
|
17
|
+
from typing_extensions import TypedDict
|
|
18
|
+
from wrapt import FunctionWrapper, wrap_function_wrapper
|
|
19
|
+
|
|
20
|
+
from .proxy import start_proxy, release_proxy, get_cc_proxy_base_url, set_trace_to_proxy
|
|
21
|
+
|
|
22
|
+
logger = get_default_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpanContextPayload(TypedDict):
|
|
26
|
+
trace_id: str
|
|
27
|
+
span_id: str
|
|
28
|
+
project_api_key: str
|
|
29
|
+
span_ids_path: list[str]
|
|
30
|
+
span_path: list[str]
|
|
31
|
+
laminar_url: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_instruments = ("claude-agent-sdk >= 0.1.0",)
|
|
35
|
+
|
|
36
|
+
WRAPPED_METHODS = [
|
|
37
|
+
{
|
|
38
|
+
"package": "claude_agent_sdk.client",
|
|
39
|
+
"object": "ClaudeSDKClient",
|
|
40
|
+
"method": "connect",
|
|
41
|
+
"class_name": "ClaudeSDKClient",
|
|
42
|
+
"is_async": True,
|
|
43
|
+
"is_streaming": False,
|
|
44
|
+
# start proxy on connection
|
|
45
|
+
"is_start_proxy": True,
|
|
46
|
+
"is_publish_span_context": True, # TODO: is there a need to publish span context here?
|
|
47
|
+
"is_release_proxy": False,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"package": "claude_agent_sdk.client",
|
|
51
|
+
"object": "ClaudeSDKClient",
|
|
52
|
+
"method": "query",
|
|
53
|
+
"class_name": "ClaudeSDKClient",
|
|
54
|
+
"is_async": True,
|
|
55
|
+
"is_streaming": False,
|
|
56
|
+
# only publish span context here as start/close managed by connect/disconnect
|
|
57
|
+
"is_start_proxy": False,
|
|
58
|
+
"is_publish_span_context": True,
|
|
59
|
+
"is_release_proxy": False,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"package": "claude_agent_sdk.client",
|
|
63
|
+
"object": "ClaudeSDKClient",
|
|
64
|
+
"method": "receive_messages",
|
|
65
|
+
"class_name": "ClaudeSDKClient",
|
|
66
|
+
"is_async": True,
|
|
67
|
+
"is_streaming": True,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"package": "claude_agent_sdk.client",
|
|
71
|
+
"object": "ClaudeSDKClient",
|
|
72
|
+
"method": "receive_response",
|
|
73
|
+
"class_name": "ClaudeSDKClient",
|
|
74
|
+
"is_async": True,
|
|
75
|
+
"is_streaming": True,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"package": "claude_agent_sdk.client",
|
|
79
|
+
"object": "ClaudeSDKClient",
|
|
80
|
+
"method": "interrupt",
|
|
81
|
+
"class_name": "ClaudeSDKClient",
|
|
82
|
+
"is_async": True,
|
|
83
|
+
"is_streaming": False,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"package": "claude_agent_sdk.client",
|
|
87
|
+
"object": "ClaudeSDKClient",
|
|
88
|
+
"method": "disconnect",
|
|
89
|
+
"class_name": "ClaudeSDKClient",
|
|
90
|
+
"is_async": True,
|
|
91
|
+
"is_streaming": False,
|
|
92
|
+
# close proxy on a connection drop
|
|
93
|
+
"is_start_proxy": False,
|
|
94
|
+
"is_publish_span_context": False,
|
|
95
|
+
"is_release_proxy": True,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
# No "object" and "class_name" fields for module-level functions
|
|
99
|
+
"package": "claude_agent_sdk",
|
|
100
|
+
"method": "query",
|
|
101
|
+
"is_async": True,
|
|
102
|
+
"is_streaming": True,
|
|
103
|
+
# start, send span to, and release proxy here as it is a module-level function doing all on its own
|
|
104
|
+
"is_start_proxy": True,
|
|
105
|
+
"is_publish_span_context": True,
|
|
106
|
+
"is_release_proxy": True,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
# No "object" and "class_name" fields for module-level functions
|
|
110
|
+
"package": "claude_agent_sdk",
|
|
111
|
+
"method": "create_sdk_mcp_server",
|
|
112
|
+
"is_async": False,
|
|
113
|
+
"is_streaming": False,
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
_MODULE_FUNCTION_ORIGINALS: dict[tuple[str, str], Any] = {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _with_wrapper(func):
|
|
121
|
+
"""Helper for providing tracer for wrapper functions. Includes metric collectors."""
|
|
122
|
+
|
|
123
|
+
def wrapper(
|
|
124
|
+
to_wrap,
|
|
125
|
+
):
|
|
126
|
+
def wrapper(wrapped, instance, args, kwargs):
|
|
127
|
+
return func(
|
|
128
|
+
to_wrap,
|
|
129
|
+
wrapped,
|
|
130
|
+
instance,
|
|
131
|
+
args,
|
|
132
|
+
kwargs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return wrapper
|
|
136
|
+
|
|
137
|
+
return wrapper
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _replace_function_aliases(original, wrapped):
|
|
141
|
+
for module in list(sys.modules.values()):
|
|
142
|
+
module_dict = getattr(module, "__dict__", None)
|
|
143
|
+
if not module_dict:
|
|
144
|
+
continue
|
|
145
|
+
for attr, value in list(module_dict.items()):
|
|
146
|
+
if value is original:
|
|
147
|
+
setattr(module, attr, wrapped)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _wrap_module_function(module_name: str, function_name: str, wrapper):
|
|
151
|
+
try:
|
|
152
|
+
module = sys.modules.get(module_name) or importlib.import_module(module_name)
|
|
153
|
+
except ModuleNotFoundError:
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
original = getattr(module, function_name)
|
|
158
|
+
except AttributeError:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
key = (module_name, function_name)
|
|
162
|
+
if key not in _MODULE_FUNCTION_ORIGINALS:
|
|
163
|
+
_MODULE_FUNCTION_ORIGINALS[key] = original
|
|
164
|
+
|
|
165
|
+
wrapped_function = FunctionWrapper(original, wrapper)
|
|
166
|
+
setattr(module, function_name, wrapped_function)
|
|
167
|
+
_replace_function_aliases(original, wrapped_function)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _unwrap_module_function(module_name: str, function_name: str):
|
|
171
|
+
key = (module_name, function_name)
|
|
172
|
+
original = _MODULE_FUNCTION_ORIGINALS.get(key)
|
|
173
|
+
if not original:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
module = sys.modules.get(module_name)
|
|
177
|
+
if not module:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
current = getattr(module, function_name, None)
|
|
181
|
+
setattr(module, function_name, original)
|
|
182
|
+
if current is not None:
|
|
183
|
+
_replace_function_aliases(current, original)
|
|
184
|
+
del _MODULE_FUNCTION_ORIGINALS[key]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _span_name(to_wrap: dict[str, str]) -> str:
|
|
188
|
+
class_name = to_wrap.get("class_name")
|
|
189
|
+
method = to_wrap.get("method")
|
|
190
|
+
return f"{class_name}.{method}" if class_name else method
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _record_input(span, wrapped, args, kwargs):
|
|
194
|
+
try:
|
|
195
|
+
span.set_attribute(
|
|
196
|
+
"lmnr.span.input",
|
|
197
|
+
json_dumps(
|
|
198
|
+
get_input_from_func_args(
|
|
199
|
+
wrapped,
|
|
200
|
+
is_method=is_method(wrapped),
|
|
201
|
+
func_args=list(args),
|
|
202
|
+
func_kwargs=kwargs,
|
|
203
|
+
)
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _record_output(span, to_wrap, value):
|
|
211
|
+
try:
|
|
212
|
+
span.set_attribute("lmnr.span.output", json_dumps(value))
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _get_span_context_payload() -> dict[str, str] | None:
|
|
218
|
+
current_span: ReadableSpan = trace.get_current_span(context=get_current_context())
|
|
219
|
+
if current_span is trace.INVALID_SPAN:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
span_context = current_span.get_span_context()
|
|
223
|
+
if span_context is None or not span_context.is_valid:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
span_ids_path = []
|
|
227
|
+
span_path = []
|
|
228
|
+
if hasattr(current_span, "attributes"):
|
|
229
|
+
readable_span: ReadableSpan = current_span
|
|
230
|
+
span_ids_path = list(readable_span.attributes.get(SPAN_IDS_PATH, tuple()))
|
|
231
|
+
span_path = list(readable_span.attributes.get(SPAN_PATH, tuple()))
|
|
232
|
+
|
|
233
|
+
project_api_key = Laminar.get_project_api_key()
|
|
234
|
+
laminar_url = Laminar.get_base_http_url()
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"trace_id": f"{span_context.trace_id:032x}",
|
|
238
|
+
"span_id": f"{span_context.span_id:016x}",
|
|
239
|
+
"project_api_key": project_api_key or "",
|
|
240
|
+
"span_ids_path": span_ids_path,
|
|
241
|
+
"span_path": span_path,
|
|
242
|
+
"laminar_url": laminar_url,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _publish_span_context() -> None:
|
|
247
|
+
payload = _get_span_context_payload()
|
|
248
|
+
if not payload:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
set_trace_to_proxy(
|
|
252
|
+
trace_id=payload["trace_id"],
|
|
253
|
+
span_id=payload["span_id"],
|
|
254
|
+
project_api_key=payload["project_api_key"],
|
|
255
|
+
span_ids_path=payload["span_ids_path"],
|
|
256
|
+
span_path=payload["span_path"],
|
|
257
|
+
laminar_url=payload["laminar_url"],
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@_with_wrapper
|
|
262
|
+
def _wrap_sync(to_wrap, wrapped, instance, args, kwargs):
|
|
263
|
+
with Laminar.start_as_current_span(
|
|
264
|
+
_span_name(to_wrap),
|
|
265
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
|
266
|
+
) as span:
|
|
267
|
+
_record_input(span, wrapped, args, kwargs)
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
result = wrapped(*args, **kwargs)
|
|
271
|
+
except Exception as e: # pylint: disable=broad-except
|
|
272
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
273
|
+
span.record_exception(e)
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
_record_output(span, to_wrap, result)
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@_with_wrapper
|
|
281
|
+
async def _wrap_async(to_wrap, wrapped, instance, args, kwargs):
|
|
282
|
+
with Laminar.start_as_current_span(
|
|
283
|
+
_span_name(to_wrap),
|
|
284
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
|
285
|
+
) as span:
|
|
286
|
+
_record_input(span, wrapped, args, kwargs)
|
|
287
|
+
|
|
288
|
+
original_base_url = None
|
|
289
|
+
if to_wrap.get("is_start_proxy"):
|
|
290
|
+
original_base_url = os.environ.get("ANTHROPIC_BASE_URL")
|
|
291
|
+
start_proxy()
|
|
292
|
+
|
|
293
|
+
if to_wrap.get("is_publish_span_context"):
|
|
294
|
+
proxy_base_url = get_cc_proxy_base_url()
|
|
295
|
+
if proxy_base_url:
|
|
296
|
+
_publish_span_context()
|
|
297
|
+
else:
|
|
298
|
+
logger.debug(
|
|
299
|
+
"No claude proxy server found. Skipping span context publication."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
result = await wrapped(*args, **kwargs)
|
|
304
|
+
except Exception as e: # pylint: disable=broad-except
|
|
305
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
306
|
+
span.record_exception(e)
|
|
307
|
+
raise
|
|
308
|
+
finally:
|
|
309
|
+
if original_base_url is not None:
|
|
310
|
+
if original_base_url:
|
|
311
|
+
os.environ["ANTHROPIC_BASE_URL"] = original_base_url
|
|
312
|
+
else:
|
|
313
|
+
os.environ.pop("ANTHROPIC_BASE_URL", None)
|
|
314
|
+
if to_wrap.get("is_release_proxy"):
|
|
315
|
+
release_proxy()
|
|
316
|
+
|
|
317
|
+
_record_output(span, to_wrap, result)
|
|
318
|
+
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@_with_wrapper
|
|
323
|
+
def _wrap_async_gen(to_wrap, wrapped, instance, args, kwargs):
|
|
324
|
+
async def generator():
|
|
325
|
+
span = Laminar.start_span(
|
|
326
|
+
_span_name(to_wrap),
|
|
327
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
|
328
|
+
)
|
|
329
|
+
collected = []
|
|
330
|
+
async_iter = None
|
|
331
|
+
|
|
332
|
+
original_base_url = None
|
|
333
|
+
if to_wrap.get("is_start_proxy"):
|
|
334
|
+
original_base_url = os.environ.get("ANTHROPIC_BASE_URL")
|
|
335
|
+
start_proxy()
|
|
336
|
+
|
|
337
|
+
if to_wrap.get("is_publish_span_context"):
|
|
338
|
+
with Laminar.use_span(span):
|
|
339
|
+
proxy_base_url = get_cc_proxy_base_url()
|
|
340
|
+
if proxy_base_url:
|
|
341
|
+
_publish_span_context()
|
|
342
|
+
else:
|
|
343
|
+
logger.debug(
|
|
344
|
+
"No claude proxy server found. Skipping span context publication."
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
with Laminar.use_span(span):
|
|
349
|
+
_record_input(span, wrapped, args, kwargs)
|
|
350
|
+
async_source = wrapped(*args, **kwargs)
|
|
351
|
+
async_iter = (
|
|
352
|
+
async_source.__aiter__()
|
|
353
|
+
if hasattr(async_source, "__aiter__")
|
|
354
|
+
else async_source
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
while True:
|
|
358
|
+
try:
|
|
359
|
+
with Laminar.use_span(
|
|
360
|
+
span, record_exception=False, set_status_on_exception=False
|
|
361
|
+
):
|
|
362
|
+
item = await async_iter.__anext__()
|
|
363
|
+
collected.append(item)
|
|
364
|
+
except StopAsyncIteration:
|
|
365
|
+
break
|
|
366
|
+
yield item
|
|
367
|
+
except Exception as e: # pylint: disable=broad-except
|
|
368
|
+
with Laminar.use_span(span):
|
|
369
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
370
|
+
span.record_exception(e)
|
|
371
|
+
raise
|
|
372
|
+
finally:
|
|
373
|
+
if original_base_url is not None:
|
|
374
|
+
if original_base_url:
|
|
375
|
+
os.environ["ANTHROPIC_BASE_URL"] = original_base_url
|
|
376
|
+
else:
|
|
377
|
+
os.environ.pop("ANTHROPIC_BASE_URL", None)
|
|
378
|
+
if async_iter and hasattr(async_iter, "aclose"):
|
|
379
|
+
try:
|
|
380
|
+
with Laminar.use_span(span):
|
|
381
|
+
await async_iter.aclose()
|
|
382
|
+
except Exception: # pylint: disable=broad-except
|
|
383
|
+
pass
|
|
384
|
+
with Laminar.use_span(span):
|
|
385
|
+
_record_output(span, to_wrap, collected)
|
|
386
|
+
span.end()
|
|
387
|
+
|
|
388
|
+
if to_wrap.get("is_release_proxy"):
|
|
389
|
+
release_proxy()
|
|
390
|
+
|
|
391
|
+
return generator()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class ClaudeAgentInstrumentor(BaseInstrumentor):
|
|
395
|
+
def __init__(self):
|
|
396
|
+
super().__init__()
|
|
397
|
+
|
|
398
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
399
|
+
return _instruments
|
|
400
|
+
|
|
401
|
+
def _instrument(self, **kwargs):
|
|
402
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
403
|
+
wrap_package = wrapped_method.get("package")
|
|
404
|
+
wrap_object = wrapped_method.get("object")
|
|
405
|
+
wrap_method = wrapped_method.get("method")
|
|
406
|
+
is_streaming = wrapped_method.get("is_streaming", False)
|
|
407
|
+
is_async = wrapped_method.get("is_async", False)
|
|
408
|
+
|
|
409
|
+
if is_streaming:
|
|
410
|
+
wrapper_factory = _wrap_async_gen
|
|
411
|
+
elif is_async:
|
|
412
|
+
wrapper_factory = _wrap_async
|
|
413
|
+
else:
|
|
414
|
+
wrapper_factory = _wrap_sync
|
|
415
|
+
|
|
416
|
+
wrapper = wrapper_factory(wrapped_method)
|
|
417
|
+
|
|
418
|
+
if wrap_object:
|
|
419
|
+
target = f"{wrap_object}.{wrap_method}"
|
|
420
|
+
try:
|
|
421
|
+
wrap_function_wrapper(
|
|
422
|
+
wrap_package,
|
|
423
|
+
target,
|
|
424
|
+
wrapper,
|
|
425
|
+
)
|
|
426
|
+
except (ModuleNotFoundError, AttributeError):
|
|
427
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
428
|
+
else:
|
|
429
|
+
try:
|
|
430
|
+
_wrap_module_function(
|
|
431
|
+
wrap_package,
|
|
432
|
+
wrap_method,
|
|
433
|
+
wrapper,
|
|
434
|
+
)
|
|
435
|
+
except (ModuleNotFoundError, AttributeError):
|
|
436
|
+
pass # that's ok
|
|
437
|
+
|
|
438
|
+
def _uninstrument(self, **kwargs):
|
|
439
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
440
|
+
wrap_package = wrapped_method.get("package")
|
|
441
|
+
wrap_object = wrapped_method.get("object")
|
|
442
|
+
wrap_method = wrapped_method.get("method")
|
|
443
|
+
|
|
444
|
+
if wrap_object:
|
|
445
|
+
module_path = f"{wrap_package}.{wrap_object}"
|
|
446
|
+
try:
|
|
447
|
+
unwrap(module_path, wrap_method)
|
|
448
|
+
except (ModuleNotFoundError, AttributeError):
|
|
449
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
450
|
+
else:
|
|
451
|
+
_unwrap_module_function(wrap_package, wrap_method)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from lmnr.sdk.log import get_default_logger
|
|
11
|
+
|
|
12
|
+
from lmnr_claude_code_proxy import run_server, set_current_trace, stop_server
|
|
13
|
+
|
|
14
|
+
logger = get_default_logger(__name__)
|
|
15
|
+
|
|
16
|
+
DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
|
|
17
|
+
DEFAULT_CC_PROXY_PORT = 45667
|
|
18
|
+
CC_PROXY_PORT_ATTEMPTS = 5
|
|
19
|
+
|
|
20
|
+
_CC_PROXY_LOCK = threading.Lock()
|
|
21
|
+
_CC_PROXY_PORT: int | None = None
|
|
22
|
+
_CC_PROXY_BASE_URL: str | None = None
|
|
23
|
+
_CC_PROXY_TARGET_URL: str | None = None
|
|
24
|
+
_CC_PROXY_SHUTDOWN_REGISTERED = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_available_port(start_port: int, attempts: int) -> Optional[int]:
|
|
28
|
+
for offset in range(attempts):
|
|
29
|
+
candidate = start_port + offset
|
|
30
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
31
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
32
|
+
try:
|
|
33
|
+
sock.bind(("127.0.0.1", candidate))
|
|
34
|
+
except OSError:
|
|
35
|
+
continue
|
|
36
|
+
return candidate
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wait_for_port(port: int, timeout: float = 5.0) -> bool:
|
|
41
|
+
deadline = time.monotonic() + timeout
|
|
42
|
+
while time.monotonic() < deadline:
|
|
43
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
44
|
+
sock.settimeout(0.2)
|
|
45
|
+
try:
|
|
46
|
+
sock.connect(("127.0.0.1", port))
|
|
47
|
+
return True
|
|
48
|
+
except OSError:
|
|
49
|
+
time.sleep(0.1)
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stop_cc_proxy_locked():
|
|
54
|
+
global _CC_PROXY_PORT, _CC_PROXY_BASE_URL
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
stop_server()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.debug("Unable to stop cc-proxy: %s", e)
|
|
60
|
+
|
|
61
|
+
if _CC_PROXY_TARGET_URL:
|
|
62
|
+
os.environ["ANTHROPIC_BASE_URL"] = _CC_PROXY_TARGET_URL
|
|
63
|
+
|
|
64
|
+
_CC_PROXY_PORT = None
|
|
65
|
+
_CC_PROXY_BASE_URL = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _stop_cc_proxy():
|
|
69
|
+
with _CC_PROXY_LOCK:
|
|
70
|
+
_stop_cc_proxy_locked()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _register_proxy_shutdown():
|
|
74
|
+
global _CC_PROXY_SHUTDOWN_REGISTERED
|
|
75
|
+
if not _CC_PROXY_SHUTDOWN_REGISTERED:
|
|
76
|
+
atexit.register(_stop_cc_proxy)
|
|
77
|
+
_CC_PROXY_SHUTDOWN_REGISTERED = True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_cc_proxy_base_url() -> str | None:
|
|
81
|
+
return _CC_PROXY_BASE_URL
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def start_proxy() -> Optional[str]:
|
|
85
|
+
with _CC_PROXY_LOCK:
|
|
86
|
+
global _CC_PROXY_PORT, _CC_PROXY_BASE_URL, _CC_PROXY_TARGET_URL
|
|
87
|
+
|
|
88
|
+
port = _find_available_port(DEFAULT_CC_PROXY_PORT, CC_PROXY_PORT_ATTEMPTS)
|
|
89
|
+
if port is None:
|
|
90
|
+
logger.warning("Unable to allocate port for cc-proxy.")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
target_url = (
|
|
94
|
+
_CC_PROXY_TARGET_URL
|
|
95
|
+
or os.environ.get("ANTHROPIC_ORIGINAL_BASE_URL")
|
|
96
|
+
or os.environ.get("ANTHROPIC_BASE_URL")
|
|
97
|
+
or DEFAULT_ANTHROPIC_BASE_URL
|
|
98
|
+
)
|
|
99
|
+
_CC_PROXY_TARGET_URL = target_url
|
|
100
|
+
os.environ.setdefault("ANTHROPIC_ORIGINAL_BASE_URL", target_url)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
run_server(target_url, port=port)
|
|
104
|
+
except OSError as exc: # pragma: no cover
|
|
105
|
+
logger.warning("Unable to start cc-proxy: %s", exc)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if not _wait_for_port(port):
|
|
109
|
+
logger.warning("cc-proxy failed to start on port %s", port)
|
|
110
|
+
stop_server()
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
proxy_base_url = f"http://127.0.0.1:{port}"
|
|
114
|
+
_CC_PROXY_PORT = port
|
|
115
|
+
_CC_PROXY_BASE_URL = proxy_base_url
|
|
116
|
+
os.environ["ANTHROPIC_BASE_URL"] = proxy_base_url
|
|
117
|
+
_register_proxy_shutdown()
|
|
118
|
+
|
|
119
|
+
logger.info("Started claude proxy server on: " + str(proxy_base_url))
|
|
120
|
+
return proxy_base_url
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def release_proxy() -> None:
|
|
124
|
+
with _CC_PROXY_LOCK:
|
|
125
|
+
_stop_cc_proxy_locked()
|
|
126
|
+
logger.debug("Released claude proxy server")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def set_trace_to_proxy(
|
|
130
|
+
trace_id: str,
|
|
131
|
+
span_id: str,
|
|
132
|
+
project_api_key: str,
|
|
133
|
+
span_path: list[str] = [],
|
|
134
|
+
span_ids_path: list[str] = [],
|
|
135
|
+
laminar_url: str = "https://api.lmnr.ai",
|
|
136
|
+
):
|
|
137
|
+
set_current_trace(
|
|
138
|
+
trace_id=trace_id,
|
|
139
|
+
span_id=span_id,
|
|
140
|
+
project_api_key=project_api_key,
|
|
141
|
+
span_path=span_path,
|
|
142
|
+
span_ids_path=span_ids_path,
|
|
143
|
+
laminar_url=laminar_url,
|
|
144
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""OpenTelemetry CUA instrumentation"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, AsyncGenerator, Collection
|
|
5
|
+
|
|
6
|
+
from lmnr import Laminar
|
|
7
|
+
from lmnr.sdk.utils import json_dumps
|
|
8
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
9
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
10
|
+
|
|
11
|
+
from opentelemetry.trace import Span
|
|
12
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
13
|
+
from wrapt import wrap_function_wrapper
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_instruments = ("cua-agent >= 0.4.0",)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _wrap_run(
|
|
21
|
+
wrapped,
|
|
22
|
+
instance,
|
|
23
|
+
args,
|
|
24
|
+
kwargs,
|
|
25
|
+
):
|
|
26
|
+
parent_span = Laminar.start_span("ComputerAgent.run")
|
|
27
|
+
instance._lmnr_parent_span = parent_span
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
result: AsyncGenerator[dict[str, Any], None] = wrapped(*args, **kwargs)
|
|
31
|
+
return _abuild_from_streaming_response(parent_span, result)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
if parent_span.is_recording():
|
|
34
|
+
parent_span.set_status(Status(StatusCode.ERROR))
|
|
35
|
+
parent_span.record_exception(e)
|
|
36
|
+
parent_span.end()
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def _abuild_from_streaming_response(
|
|
41
|
+
parent_span: Span, response: AsyncGenerator[dict[str, Any], None]
|
|
42
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
43
|
+
with Laminar.use_span(parent_span, end_on_exit=True):
|
|
44
|
+
response_iter = aiter(response)
|
|
45
|
+
while True:
|
|
46
|
+
step = None
|
|
47
|
+
step_span = Laminar.start_span("ComputerAgent.step")
|
|
48
|
+
with Laminar.use_span(step_span):
|
|
49
|
+
try:
|
|
50
|
+
step = await anext(response_iter)
|
|
51
|
+
step_span.set_attribute("lmnr.span.output", json_dumps(step))
|
|
52
|
+
try:
|
|
53
|
+
# When processing tool calls, each output item is processed separately,
|
|
54
|
+
# if the output is message, agent.step returns an empty array
|
|
55
|
+
# https://github.com/trycua/cua/blob/17d670962970a1d1774daaec029ebf92f1f9235e/libs/python/agent/agent/agent.py#L459
|
|
56
|
+
if len(step.get("output", [])) == 0:
|
|
57
|
+
continue
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
if step_span.is_recording():
|
|
61
|
+
step_span.end()
|
|
62
|
+
except StopAsyncIteration:
|
|
63
|
+
# don't end on purpose, there is no iteration step here.
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if step is not None:
|
|
67
|
+
yield step
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CuaAgentInstrumentor(BaseInstrumentor):
|
|
71
|
+
def __init__(self):
|
|
72
|
+
super().__init__()
|
|
73
|
+
|
|
74
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
75
|
+
return _instruments
|
|
76
|
+
|
|
77
|
+
def _instrument(self, **kwargs):
|
|
78
|
+
wrap_package = "agent.agent"
|
|
79
|
+
wrap_object = "ComputerAgent"
|
|
80
|
+
wrap_method = "run"
|
|
81
|
+
try:
|
|
82
|
+
wrap_function_wrapper(
|
|
83
|
+
wrap_package,
|
|
84
|
+
f"{wrap_object}.{wrap_method}",
|
|
85
|
+
_wrap_run,
|
|
86
|
+
)
|
|
87
|
+
except ModuleNotFoundError:
|
|
88
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
89
|
+
|
|
90
|
+
def _uninstrument(self, **kwargs):
|
|
91
|
+
wrap_package = "agent.agent"
|
|
92
|
+
wrap_object = "ComputerAgent"
|
|
93
|
+
wrap_method = "run"
|
|
94
|
+
try:
|
|
95
|
+
unwrap(
|
|
96
|
+
f"{wrap_package}.{wrap_object}",
|
|
97
|
+
wrap_method,
|
|
98
|
+
)
|
|
99
|
+
except ModuleNotFoundError:
|
|
100
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|