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.
Files changed (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /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