lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmnr/__init__.py +32 -11
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/cli/evals.py +111 -0
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +70 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
- lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
- lmnr/opentelemetry_lib/litellm/utils.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
- lmnr/opentelemetry_lib/tracing/processor.py +193 -0
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +18 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/__init__.py +0 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +142 -0
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +151 -0
- lmnr/sdk/browser/playwright_otel.py +322 -0
- lmnr/sdk/browser/pw_utils.py +363 -0
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +70 -0
- lmnr/sdk/client/asynchronous/async_client.py +180 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +263 -0
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/sync_client.py +191 -0
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +163 -26
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +403 -191
- lmnr/sdk/laminar.py +1080 -549
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +246 -134
- lmnr/sdk/utils.py +151 -7
- lmnr/version.py +46 -0
- {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/cli.py +0 -101
- lmnr/openllmetry_sdk/.python-version +0 -1
- lmnr/openllmetry_sdk/__init__.py +0 -72
- lmnr/openllmetry_sdk/config/__init__.py +0 -9
- lmnr/openllmetry_sdk/decorators/base.py +0 -185
- lmnr/openllmetry_sdk/instruments.py +0 -38
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- lmnr/openllmetry_sdk/utils/package_check.py +0 -7
- lmnr/openllmetry_sdk/version.py +0 -1
- lmnr/sdk/datasets.py +0 -55
- lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
- lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
- lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
- lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .utils import (
|
|
4
|
+
dont_throw,
|
|
5
|
+
model_as_dict,
|
|
6
|
+
set_span_attribute,
|
|
7
|
+
should_send_prompts,
|
|
8
|
+
)
|
|
9
|
+
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
|
|
10
|
+
GEN_AI_RESPONSE_ID,
|
|
11
|
+
)
|
|
12
|
+
from opentelemetry.semconv_ai import (
|
|
13
|
+
SpanAttributes,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CONTENT_FILTER_KEY = "content_filter_results"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dont_throw
|
|
20
|
+
def set_input_attributes(span, kwargs):
|
|
21
|
+
if not span.is_recording():
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
if should_send_prompts():
|
|
25
|
+
if kwargs.get("prompt") is not None:
|
|
26
|
+
set_span_attribute(
|
|
27
|
+
span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
elif kwargs.get("messages") is not None:
|
|
31
|
+
for i, message in enumerate(kwargs.get("messages")):
|
|
32
|
+
set_span_attribute(
|
|
33
|
+
span,
|
|
34
|
+
f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
|
|
35
|
+
_dump_content(message.get("content")),
|
|
36
|
+
)
|
|
37
|
+
set_span_attribute(
|
|
38
|
+
span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message.get("role")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dont_throw
|
|
43
|
+
def set_model_input_attributes(span, kwargs):
|
|
44
|
+
if not span.is_recording():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model"))
|
|
48
|
+
set_span_attribute(
|
|
49
|
+
span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens_to_sample")
|
|
50
|
+
)
|
|
51
|
+
set_span_attribute(
|
|
52
|
+
span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
|
|
53
|
+
)
|
|
54
|
+
set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
|
|
55
|
+
set_span_attribute(
|
|
56
|
+
span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
|
|
57
|
+
)
|
|
58
|
+
set_span_attribute(
|
|
59
|
+
span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
|
|
60
|
+
)
|
|
61
|
+
set_span_attribute(
|
|
62
|
+
span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_streaming_response_attributes(
|
|
67
|
+
span, accumulated_content, finish_reason=None, usage=None
|
|
68
|
+
):
|
|
69
|
+
"""Set span attributes for accumulated streaming response."""
|
|
70
|
+
if not span.is_recording() or not should_send_prompts():
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.0"
|
|
74
|
+
set_span_attribute(span, f"{prefix}.role", "assistant")
|
|
75
|
+
set_span_attribute(span, f"{prefix}.content", accumulated_content)
|
|
76
|
+
if finish_reason:
|
|
77
|
+
set_span_attribute(span, f"{prefix}.finish_reason", finish_reason)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_model_streaming_response_attributes(span, usage):
|
|
81
|
+
if not span.is_recording():
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if usage:
|
|
85
|
+
set_span_attribute(
|
|
86
|
+
span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, usage.completion_tokens
|
|
87
|
+
)
|
|
88
|
+
set_span_attribute(
|
|
89
|
+
span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage.prompt_tokens
|
|
90
|
+
)
|
|
91
|
+
set_span_attribute(
|
|
92
|
+
span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.total_tokens
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dont_throw
|
|
97
|
+
def set_model_response_attributes(span, response, token_histogram):
|
|
98
|
+
if not span.is_recording():
|
|
99
|
+
return
|
|
100
|
+
response = model_as_dict(response)
|
|
101
|
+
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
|
|
102
|
+
set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
|
|
103
|
+
|
|
104
|
+
usage = response.get("usage") or {}
|
|
105
|
+
prompt_tokens = usage.get("prompt_tokens")
|
|
106
|
+
completion_tokens = usage.get("completion_tokens")
|
|
107
|
+
if usage:
|
|
108
|
+
set_span_attribute(
|
|
109
|
+
span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.get("total_tokens")
|
|
110
|
+
)
|
|
111
|
+
set_span_attribute(
|
|
112
|
+
span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
|
|
113
|
+
)
|
|
114
|
+
set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, prompt_tokens)
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
isinstance(prompt_tokens, int)
|
|
118
|
+
and prompt_tokens >= 0
|
|
119
|
+
and token_histogram is not None
|
|
120
|
+
):
|
|
121
|
+
token_histogram.record(
|
|
122
|
+
prompt_tokens,
|
|
123
|
+
attributes={
|
|
124
|
+
SpanAttributes.LLM_TOKEN_TYPE: "input",
|
|
125
|
+
SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (
|
|
130
|
+
isinstance(completion_tokens, int)
|
|
131
|
+
and completion_tokens >= 0
|
|
132
|
+
and token_histogram is not None
|
|
133
|
+
):
|
|
134
|
+
token_histogram.record(
|
|
135
|
+
completion_tokens,
|
|
136
|
+
attributes={
|
|
137
|
+
SpanAttributes.LLM_TOKEN_TYPE: "output",
|
|
138
|
+
SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def set_response_attributes(span, response):
|
|
144
|
+
if not span.is_recording():
|
|
145
|
+
return
|
|
146
|
+
choices = model_as_dict(response).get("choices")
|
|
147
|
+
if should_send_prompts() and choices:
|
|
148
|
+
_set_completions(span, choices)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _set_completions(span, choices):
|
|
152
|
+
if choices is None or not should_send_prompts():
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
for choice in choices:
|
|
156
|
+
index = choice.get("index")
|
|
157
|
+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
|
|
158
|
+
set_span_attribute(span, f"{prefix}.finish_reason", choice.get("finish_reason"))
|
|
159
|
+
|
|
160
|
+
if choice.get("content_filter_results"):
|
|
161
|
+
set_span_attribute(
|
|
162
|
+
span,
|
|
163
|
+
f"{prefix}.{CONTENT_FILTER_KEY}",
|
|
164
|
+
json.dumps(choice.get("content_filter_results")),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if choice.get("finish_reason") == "content_filter":
|
|
168
|
+
set_span_attribute(span, f"{prefix}.role", "assistant")
|
|
169
|
+
set_span_attribute(span, f"{prefix}.content", "FILTERED")
|
|
170
|
+
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
message = choice.get("message")
|
|
174
|
+
if not message:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
set_span_attribute(span, f"{prefix}.role", message.get("role"))
|
|
178
|
+
set_span_attribute(span, f"{prefix}.content", message.get("content"))
|
|
179
|
+
|
|
180
|
+
function_call = message.get("function_call")
|
|
181
|
+
if function_call:
|
|
182
|
+
set_span_attribute(
|
|
183
|
+
span, f"{prefix}.tool_calls.0.name", function_call.get("name")
|
|
184
|
+
)
|
|
185
|
+
set_span_attribute(
|
|
186
|
+
span,
|
|
187
|
+
f"{prefix}.tool_calls.0.arguments",
|
|
188
|
+
function_call.get("arguments"),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
tool_calls = message.get("tool_calls")
|
|
192
|
+
if tool_calls:
|
|
193
|
+
for i, tool_call in enumerate(tool_calls):
|
|
194
|
+
function = tool_call.get("function")
|
|
195
|
+
set_span_attribute(
|
|
196
|
+
span,
|
|
197
|
+
f"{prefix}.tool_calls.{i}.id",
|
|
198
|
+
tool_call.get("id"),
|
|
199
|
+
)
|
|
200
|
+
set_span_attribute(
|
|
201
|
+
span,
|
|
202
|
+
f"{prefix}.tool_calls.{i}.name",
|
|
203
|
+
function.get("name"),
|
|
204
|
+
)
|
|
205
|
+
set_span_attribute(
|
|
206
|
+
span,
|
|
207
|
+
f"{prefix}.tool_calls.{i}.arguments",
|
|
208
|
+
function.get("arguments"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _dump_content(content):
|
|
213
|
+
if isinstance(content, str):
|
|
214
|
+
return content
|
|
215
|
+
json_serializable = []
|
|
216
|
+
for item in content:
|
|
217
|
+
if item.get("type") == "text":
|
|
218
|
+
json_serializable.append({"type": "text", "text": item.get("text")})
|
|
219
|
+
elif image_url := item.get("image_url"):
|
|
220
|
+
json_serializable.append(
|
|
221
|
+
{
|
|
222
|
+
"type": "image_url",
|
|
223
|
+
"image_url": {
|
|
224
|
+
"url": image_url.get("url"),
|
|
225
|
+
"detail": image_url.get("detail"),
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
return json.dumps(json_serializable)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
from opentelemetry import context as context_api
|
|
7
|
+
from .config import Config
|
|
8
|
+
from opentelemetry.semconv_ai import SpanAttributes
|
|
9
|
+
|
|
10
|
+
GEN_AI_SYSTEM = "gen_ai.system"
|
|
11
|
+
GEN_AI_SYSTEM_GROQ = "groq"
|
|
12
|
+
|
|
13
|
+
_PYDANTIC_VERSION = version("pydantic")
|
|
14
|
+
|
|
15
|
+
LMNR_TRACE_CONTENT = "LMNR_TRACE_CONTENT"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_span_attribute(span, name, value):
|
|
19
|
+
if value is not None and value != "":
|
|
20
|
+
span.set_attribute(name, value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def should_send_prompts():
|
|
24
|
+
return (
|
|
25
|
+
os.getenv(LMNR_TRACE_CONTENT) or "true"
|
|
26
|
+
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dont_throw(func):
|
|
30
|
+
"""
|
|
31
|
+
A decorator that wraps the passed in function and logs exceptions instead of throwing them.
|
|
32
|
+
|
|
33
|
+
@param func: The function to wrap
|
|
34
|
+
@return: The wrapper function
|
|
35
|
+
"""
|
|
36
|
+
# Obtain a logger specific to the function's module
|
|
37
|
+
logger = logging.getLogger(func.__module__)
|
|
38
|
+
|
|
39
|
+
def wrapper(*args, **kwargs):
|
|
40
|
+
try:
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.debug(
|
|
44
|
+
"OpenLLMetry failed to trace in %s, error: %s",
|
|
45
|
+
func.__name__,
|
|
46
|
+
traceback.format_exc(),
|
|
47
|
+
)
|
|
48
|
+
if Config.exception_logger:
|
|
49
|
+
Config.exception_logger(e)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dont_throw
|
|
55
|
+
def shared_metrics_attributes(response):
|
|
56
|
+
response_dict = model_as_dict(response)
|
|
57
|
+
|
|
58
|
+
common_attributes = Config.get_common_metrics_attributes()
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
**common_attributes,
|
|
62
|
+
GEN_AI_SYSTEM: GEN_AI_SYSTEM_GROQ,
|
|
63
|
+
SpanAttributes.LLM_RESPONSE_MODEL: response_dict.get("model"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dont_throw
|
|
68
|
+
def error_metrics_attributes(exception):
|
|
69
|
+
return {
|
|
70
|
+
GEN_AI_SYSTEM: GEN_AI_SYSTEM_GROQ,
|
|
71
|
+
"error.type": exception.__class__.__name__,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def model_as_dict(model):
|
|
76
|
+
if _PYDANTIC_VERSION < "2.0.0":
|
|
77
|
+
return model.dict()
|
|
78
|
+
if hasattr(model, "model_dump"):
|
|
79
|
+
return model.model_dump()
|
|
80
|
+
elif hasattr(model, "parse"): # Raw API response
|
|
81
|
+
return model_as_dict(model.parse())
|
|
82
|
+
else:
|
|
83
|
+
return model
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def should_emit_events() -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Checks if the instrumentation isn't using the legacy attributes
|
|
89
|
+
and if the event logger is not None.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
return not Config.use_legacy_attributes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.41.0"
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""OpenTelemetry Kernel instrumentation"""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Collection
|
|
5
|
+
|
|
6
|
+
from lmnr.opentelemetry_lib.opentelemetry.instrumentation.kernel.utils import (
|
|
7
|
+
process_tool_output_formatter,
|
|
8
|
+
screenshot_tool_output_formatter,
|
|
9
|
+
)
|
|
10
|
+
from lmnr.sdk.decorators import observe
|
|
11
|
+
from lmnr.sdk.utils import get_input_from_func_args, is_async, json_dumps
|
|
12
|
+
from lmnr import Laminar
|
|
13
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
14
|
+
from opentelemetry.instrumentation.utils import unwrap
|
|
15
|
+
|
|
16
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
17
|
+
from wrapt import wrap_function_wrapper
|
|
18
|
+
|
|
19
|
+
_instruments = ("kernel >= 0.2.0",)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
WRAPPED_METHODS = [
|
|
23
|
+
{
|
|
24
|
+
"package": "kernel.resources.browsers",
|
|
25
|
+
"object": "BrowsersResource",
|
|
26
|
+
"method": "create",
|
|
27
|
+
"class_name": "Browser",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"package": "kernel.resources.browsers",
|
|
31
|
+
"object": "BrowsersResource",
|
|
32
|
+
"method": "retrieve",
|
|
33
|
+
"class_name": "Browser",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"package": "kernel.resources.browsers",
|
|
37
|
+
"object": "BrowsersResource",
|
|
38
|
+
"method": "list",
|
|
39
|
+
"class_name": "Browser",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"package": "kernel.resources.browsers",
|
|
43
|
+
"object": "BrowsersResource",
|
|
44
|
+
"method": "delete",
|
|
45
|
+
"class_name": "Browser",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"package": "kernel.resources.browsers",
|
|
49
|
+
"object": "BrowsersResource",
|
|
50
|
+
"method": "delete_by_id",
|
|
51
|
+
"class_name": "Browser",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"package": "kernel.resources.browsers",
|
|
55
|
+
"object": "BrowsersResource",
|
|
56
|
+
"method": "load_extensions",
|
|
57
|
+
"class_name": "Browser",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"package": "kernel.resources.browsers.computer",
|
|
61
|
+
"object": "ComputerResource",
|
|
62
|
+
"method": "capture_screenshot",
|
|
63
|
+
"class_name": "Computer",
|
|
64
|
+
"span_type": "TOOL",
|
|
65
|
+
"output_formatter": screenshot_tool_output_formatter,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"package": "kernel.resources.browsers.computer",
|
|
69
|
+
"object": "ComputerResource",
|
|
70
|
+
"method": "click_mouse",
|
|
71
|
+
"class_name": "Computer",
|
|
72
|
+
"span_type": "TOOL",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"package": "kernel.resources.browsers.computer",
|
|
76
|
+
"object": "ComputerResource",
|
|
77
|
+
"method": "drag_mouse",
|
|
78
|
+
"class_name": "Computer",
|
|
79
|
+
"span_type": "TOOL",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"package": "kernel.resources.browsers.computer",
|
|
83
|
+
"object": "ComputerResource",
|
|
84
|
+
"method": "move_mouse",
|
|
85
|
+
"class_name": "Computer",
|
|
86
|
+
"span_type": "TOOL",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"package": "kernel.resources.browsers.computer",
|
|
90
|
+
"object": "ComputerResource",
|
|
91
|
+
"method": "press_key",
|
|
92
|
+
"class_name": "Computer",
|
|
93
|
+
"span_type": "TOOL",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"package": "kernel.resources.browsers.computer",
|
|
97
|
+
"object": "ComputerResource",
|
|
98
|
+
"method": "scroll",
|
|
99
|
+
"class_name": "Computer",
|
|
100
|
+
"span_type": "TOOL",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"package": "kernel.resources.browsers.computer",
|
|
104
|
+
"object": "ComputerResource",
|
|
105
|
+
"method": "type_text",
|
|
106
|
+
"class_name": "Computer",
|
|
107
|
+
"span_type": "TOOL",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"package": "kernel.resources.browsers.playwright",
|
|
111
|
+
"object": "PlaywrightResource",
|
|
112
|
+
"method": "execute",
|
|
113
|
+
"class_name": "Playwright",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"package": "kernel.resources.browsers.process",
|
|
117
|
+
"object": "ProcessResource",
|
|
118
|
+
"method": "exec",
|
|
119
|
+
"class_name": "Process",
|
|
120
|
+
"span_type": "TOOL",
|
|
121
|
+
"output_formatter": process_tool_output_formatter,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"package": "kernel.resources.browsers.process",
|
|
125
|
+
"object": "ProcessResource",
|
|
126
|
+
"method": "kill",
|
|
127
|
+
"class_name": "Process",
|
|
128
|
+
"span_type": "TOOL",
|
|
129
|
+
"output_formatter": process_tool_output_formatter,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"package": "kernel.resources.browsers.process",
|
|
133
|
+
"object": "ProcessResource",
|
|
134
|
+
"method": "spawn",
|
|
135
|
+
"class_name": "Process",
|
|
136
|
+
"span_type": "TOOL",
|
|
137
|
+
"output_formatter": process_tool_output_formatter,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"package": "kernel.resources.browsers.process",
|
|
141
|
+
"object": "ProcessResource",
|
|
142
|
+
"method": "status",
|
|
143
|
+
"class_name": "Process",
|
|
144
|
+
"span_type": "TOOL",
|
|
145
|
+
"output_formatter": process_tool_output_formatter,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"package": "kernel.resources.browsers.process",
|
|
149
|
+
"object": "ProcessResource",
|
|
150
|
+
"method": "stdin",
|
|
151
|
+
"class_name": "Process",
|
|
152
|
+
"span_type": "TOOL",
|
|
153
|
+
"output_formatter": process_tool_output_formatter,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"package": "kernel.resources.browsers.process",
|
|
157
|
+
"object": "ProcessResource",
|
|
158
|
+
"method": "stdout_stream",
|
|
159
|
+
"class_name": "Process",
|
|
160
|
+
"span_type": "TOOL",
|
|
161
|
+
"output_formatter": process_tool_output_formatter,
|
|
162
|
+
},
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _with_wrapper(func):
|
|
167
|
+
"""Helper for providing tracer for wrapper functions. Includes metric collectors."""
|
|
168
|
+
|
|
169
|
+
def wrapper(
|
|
170
|
+
to_wrap,
|
|
171
|
+
):
|
|
172
|
+
def wrapper(wrapped, instance, args, kwargs):
|
|
173
|
+
return func(
|
|
174
|
+
to_wrap,
|
|
175
|
+
wrapped,
|
|
176
|
+
instance,
|
|
177
|
+
args,
|
|
178
|
+
kwargs,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return wrapper
|
|
182
|
+
|
|
183
|
+
return wrapper
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@_with_wrapper
|
|
187
|
+
def _wrap(
|
|
188
|
+
to_wrap,
|
|
189
|
+
wrapped,
|
|
190
|
+
instance,
|
|
191
|
+
args,
|
|
192
|
+
kwargs,
|
|
193
|
+
):
|
|
194
|
+
with Laminar.start_as_current_span(
|
|
195
|
+
f"{to_wrap.get('class_name')}.{to_wrap.get('method')}",
|
|
196
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
|
197
|
+
) as span:
|
|
198
|
+
input_kv = get_input_from_func_args(wrapped, True, args, kwargs)
|
|
199
|
+
if "id" in input_kv:
|
|
200
|
+
input_kv["session_id"] = input_kv.get("id")
|
|
201
|
+
input_kv.pop("id")
|
|
202
|
+
span.set_attribute(
|
|
203
|
+
"lmnr.span.input",
|
|
204
|
+
json_dumps(input_kv),
|
|
205
|
+
)
|
|
206
|
+
try:
|
|
207
|
+
result = wrapped(*args, **kwargs)
|
|
208
|
+
except Exception as e: # pylint: disable=broad-except
|
|
209
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
210
|
+
span.record_exception(e)
|
|
211
|
+
raise
|
|
212
|
+
output_formatter = to_wrap.get("output_formatter") or (lambda x: json_dumps(x))
|
|
213
|
+
span.set_attribute("lmnr.span.output", output_formatter(result))
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@_with_wrapper
|
|
218
|
+
async def _wrap_async(
|
|
219
|
+
to_wrap,
|
|
220
|
+
wrapped,
|
|
221
|
+
instance,
|
|
222
|
+
args,
|
|
223
|
+
kwargs,
|
|
224
|
+
):
|
|
225
|
+
with Laminar.start_as_current_span(
|
|
226
|
+
f"{to_wrap.get('class_name')}.{to_wrap.get('method')}",
|
|
227
|
+
span_type=to_wrap.get("span_type", "DEFAULT"),
|
|
228
|
+
) as span:
|
|
229
|
+
input_kv = get_input_from_func_args(wrapped, True, args, kwargs)
|
|
230
|
+
if "id" in input_kv:
|
|
231
|
+
input_kv["session_id"] = input_kv.get("id")
|
|
232
|
+
input_kv.pop("id")
|
|
233
|
+
span.set_attribute(
|
|
234
|
+
"lmnr.span.input",
|
|
235
|
+
json_dumps(input_kv),
|
|
236
|
+
)
|
|
237
|
+
try:
|
|
238
|
+
result = await wrapped(*args, **kwargs)
|
|
239
|
+
except Exception as e: # pylint: disable=broad-except
|
|
240
|
+
span.set_status(Status(StatusCode.ERROR))
|
|
241
|
+
span.record_exception(e)
|
|
242
|
+
raise
|
|
243
|
+
output_formatter = to_wrap.get("output_formatter") or (lambda x: json_dumps(x))
|
|
244
|
+
span.set_attribute("lmnr.span.output", output_formatter(result))
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@_with_wrapper
|
|
249
|
+
def _wrap_app_action(
|
|
250
|
+
to_wrap,
|
|
251
|
+
wrapped,
|
|
252
|
+
instance,
|
|
253
|
+
args,
|
|
254
|
+
kwargs,
|
|
255
|
+
):
|
|
256
|
+
"""
|
|
257
|
+
Wraps app.action() decorator factory to add tracing to action handlers.
|
|
258
|
+
|
|
259
|
+
wrapped: the original `action` method
|
|
260
|
+
args: (name,) - the action name
|
|
261
|
+
kwargs: potentially {'name': ...}
|
|
262
|
+
|
|
263
|
+
Returns a decorator that wraps handlers with tracing before registering them.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
# Call the original action method to get the decorator
|
|
267
|
+
original_decorator = wrapped(*args, **kwargs)
|
|
268
|
+
|
|
269
|
+
# Get the action name from args
|
|
270
|
+
action_name = args[0] if args else kwargs.get("name", "unknown")
|
|
271
|
+
|
|
272
|
+
# Create a wrapper for the decorator that intercepts the handler
|
|
273
|
+
def tracing_decorator(handler):
|
|
274
|
+
# Apply the observe decorator to add tracing
|
|
275
|
+
observed_handler = observe(
|
|
276
|
+
name=f"action.{action_name}",
|
|
277
|
+
span_type="DEFAULT",
|
|
278
|
+
)(handler)
|
|
279
|
+
|
|
280
|
+
# Create an additional wrapper to add post-execution logic
|
|
281
|
+
if is_async(handler):
|
|
282
|
+
|
|
283
|
+
@functools.wraps(handler)
|
|
284
|
+
async def async_wrapper_with_flush(*handler_args, **handler_kwargs):
|
|
285
|
+
# Execute the observed handler (tracing happens here)
|
|
286
|
+
result = await observed_handler(*handler_args, **handler_kwargs)
|
|
287
|
+
|
|
288
|
+
Laminar.flush()
|
|
289
|
+
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
# Register the wrapper with the original decorator
|
|
293
|
+
return original_decorator(async_wrapper_with_flush)
|
|
294
|
+
else:
|
|
295
|
+
|
|
296
|
+
@functools.wraps(handler)
|
|
297
|
+
def sync_wrapper_with_flush(*handler_args, **handler_kwargs):
|
|
298
|
+
# Execute the observed handler (tracing happens here)
|
|
299
|
+
result = observed_handler(*handler_args, **handler_kwargs)
|
|
300
|
+
|
|
301
|
+
Laminar.flush()
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
# Register the wrapper with the original decorator
|
|
306
|
+
return original_decorator(sync_wrapper_with_flush)
|
|
307
|
+
|
|
308
|
+
return tracing_decorator
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class KernelInstrumentor(BaseInstrumentor):
|
|
312
|
+
def __init__(self):
|
|
313
|
+
super().__init__()
|
|
314
|
+
|
|
315
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
316
|
+
return _instruments
|
|
317
|
+
|
|
318
|
+
def _instrument(self, **kwargs):
|
|
319
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
320
|
+
wrap_package = wrapped_method.get("package")
|
|
321
|
+
wrap_object = wrapped_method.get("object")
|
|
322
|
+
wrap_method = wrapped_method.get("method")
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
wrap_function_wrapper(
|
|
326
|
+
wrap_package,
|
|
327
|
+
f"{wrap_object}.{wrap_method}",
|
|
328
|
+
_wrap(wrapped_method),
|
|
329
|
+
)
|
|
330
|
+
except (ModuleNotFoundError, AttributeError):
|
|
331
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
332
|
+
|
|
333
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
334
|
+
wrap_package = wrapped_method.get("package")
|
|
335
|
+
wrap_object = f"Async{wrapped_method.get('object')}"
|
|
336
|
+
wrap_method = wrapped_method.get("method")
|
|
337
|
+
try:
|
|
338
|
+
wrap_function_wrapper(
|
|
339
|
+
wrap_package,
|
|
340
|
+
f"{wrap_object}.{wrap_method}",
|
|
341
|
+
_wrap_async(wrapped_method),
|
|
342
|
+
)
|
|
343
|
+
except (ModuleNotFoundError, AttributeError):
|
|
344
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
wrap_function_wrapper(
|
|
348
|
+
"kernel.app_framework",
|
|
349
|
+
"KernelApp.action",
|
|
350
|
+
_wrap_app_action({}),
|
|
351
|
+
)
|
|
352
|
+
except (ModuleNotFoundError, AttributeError):
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
def _uninstrument(self, **kwargs):
|
|
356
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
357
|
+
wrap_package = wrapped_method.get("package")
|
|
358
|
+
wrap_object = wrapped_method.get("object")
|
|
359
|
+
try:
|
|
360
|
+
unwrap(
|
|
361
|
+
f"{wrap_package}.{wrap_object}",
|
|
362
|
+
wrapped_method.get("method"),
|
|
363
|
+
)
|
|
364
|
+
except (ModuleNotFoundError, AttributeError):
|
|
365
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
366
|
+
|
|
367
|
+
for wrapped_method in WRAPPED_METHODS:
|
|
368
|
+
wrap_package = wrapped_method.get("package")
|
|
369
|
+
wrap_object = f"Async{wrapped_method.get('object')}"
|
|
370
|
+
try:
|
|
371
|
+
unwrap(
|
|
372
|
+
f"{wrap_package}.{wrap_object}",
|
|
373
|
+
wrapped_method.get("method"),
|
|
374
|
+
)
|
|
375
|
+
except (ModuleNotFoundError, AttributeError):
|
|
376
|
+
pass # that's ok, we don't want to fail if some methods do not exist
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
unwrap("kernel.app_framework.KernelApp", "action")
|
|
380
|
+
except (ModuleNotFoundError, AttributeError):
|
|
381
|
+
pass
|