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
lmnr/cli/evals.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from argparse import Namespace
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from lmnr.sdk.evaluations import Evaluation
|
|
11
|
+
from lmnr.sdk.eval_control import PREPARE_ONLY, EVALUATION_INSTANCES
|
|
12
|
+
from lmnr.sdk.log import get_default_logger
|
|
13
|
+
|
|
14
|
+
LOG = get_default_logger(__name__)
|
|
15
|
+
EVAL_DIR = "evals"
|
|
16
|
+
DEFAULT_DATASET_PULL_BATCH_SIZE = 100
|
|
17
|
+
DEFAULT_DATASET_PUSH_BATCH_SIZE = 100
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log_evaluation_instance_not_found() -> None:
|
|
21
|
+
LOG.warning(
|
|
22
|
+
"Evaluation instance not found. "
|
|
23
|
+
"`evaluate` must be called at the top level of the file, "
|
|
24
|
+
"not inside a function when running evaluations from the CLI."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def run_evaluation(args: Namespace) -> None:
|
|
29
|
+
sys.path.append(os.getcwd())
|
|
30
|
+
|
|
31
|
+
if len(args.file) == 0:
|
|
32
|
+
files = [
|
|
33
|
+
os.path.join(EVAL_DIR, f)
|
|
34
|
+
for f in os.listdir(EVAL_DIR)
|
|
35
|
+
if re.match(r".*_eval\.py$", f) or re.match(r"eval_.*\.py$", f)
|
|
36
|
+
]
|
|
37
|
+
if len(files) == 0:
|
|
38
|
+
LOG.error("No evaluation files found in `evals` directory")
|
|
39
|
+
LOG.info(
|
|
40
|
+
"Eval files must be located in the `evals` directory and must be named *_eval.py or eval_*.py"
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
files.sort()
|
|
44
|
+
LOG.info(f"Located {len(files)} evaluation files in {EVAL_DIR}")
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
files = []
|
|
48
|
+
for pattern in args.file:
|
|
49
|
+
matches = glob.glob(pattern)
|
|
50
|
+
if matches:
|
|
51
|
+
files.extend(matches)
|
|
52
|
+
else:
|
|
53
|
+
# If no matches found, treat as literal filename
|
|
54
|
+
files.append(pattern)
|
|
55
|
+
|
|
56
|
+
prep_token = PREPARE_ONLY.set(True)
|
|
57
|
+
scores = []
|
|
58
|
+
try:
|
|
59
|
+
for file in files:
|
|
60
|
+
# Reset EVALUATION_INSTANCES before loading each file
|
|
61
|
+
EVALUATION_INSTANCES.set([])
|
|
62
|
+
|
|
63
|
+
LOG.info(f"Running evaluation from {file}")
|
|
64
|
+
file = os.path.abspath(file)
|
|
65
|
+
name = "user_module" + file
|
|
66
|
+
|
|
67
|
+
spec = importlib.util.spec_from_file_location(name, file)
|
|
68
|
+
if spec is None or spec.loader is None:
|
|
69
|
+
LOG.error(f"Could not load module specification from {file}")
|
|
70
|
+
if args.continue_on_error:
|
|
71
|
+
continue
|
|
72
|
+
return
|
|
73
|
+
mod = importlib.util.module_from_spec(spec)
|
|
74
|
+
sys.modules[name] = mod
|
|
75
|
+
|
|
76
|
+
spec.loader.exec_module(mod)
|
|
77
|
+
evaluations = []
|
|
78
|
+
try:
|
|
79
|
+
evaluations: list[Evaluation] | None = EVALUATION_INSTANCES.get()
|
|
80
|
+
if evaluations is None:
|
|
81
|
+
raise LookupError()
|
|
82
|
+
# may be raised by `get()` or manually by us above
|
|
83
|
+
except LookupError:
|
|
84
|
+
log_evaluation_instance_not_found()
|
|
85
|
+
if args.continue_on_error:
|
|
86
|
+
continue
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
LOG.info(f"Loaded {len(evaluations)} evaluations from {file}")
|
|
90
|
+
|
|
91
|
+
for evaluation in evaluations:
|
|
92
|
+
try:
|
|
93
|
+
eval_result = await evaluation.run()
|
|
94
|
+
scores.append(
|
|
95
|
+
{
|
|
96
|
+
"file": file,
|
|
97
|
+
"scores": eval_result["average_scores"],
|
|
98
|
+
"evaluation_id": str(eval_result["evaluation_id"]),
|
|
99
|
+
"url": eval_result["url"],
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
LOG.error(f"Error running evaluation: {e}")
|
|
104
|
+
if not args.continue_on_error:
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
if args.output_file:
|
|
108
|
+
with open(args.output_file, "w") as f:
|
|
109
|
+
json.dump(scores, f, indent=2)
|
|
110
|
+
finally:
|
|
111
|
+
PREPARE_ONLY.reset(prep_token)
|
lmnr/cli/rules.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import urllib.request
|
|
3
|
+
import urllib.error
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from lmnr.sdk.log import get_default_logger
|
|
8
|
+
|
|
9
|
+
LOG = get_default_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_cursor_rules() -> None:
|
|
13
|
+
"""Download laminar.mdc file from a hardcoded public URL and save it to .cursor/rules/laminar.mdc"""
|
|
14
|
+
# Hardcoded URL for the laminar.mdc file
|
|
15
|
+
url = "https://raw.githubusercontent.com/lmnr-ai/lmnr/dev/rules/laminar.mdc"
|
|
16
|
+
|
|
17
|
+
# Create .cursor/rules directory if it doesn't exist
|
|
18
|
+
rules_dir = Path(".cursor/rules")
|
|
19
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
|
|
21
|
+
# Define the target file path
|
|
22
|
+
target_file = rules_dir / "laminar.mdc"
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
LOG.info(f"Downloading laminar.mdc from {url}")
|
|
26
|
+
|
|
27
|
+
# Download the file
|
|
28
|
+
with urllib.request.urlopen(url) as response:
|
|
29
|
+
content = response.read()
|
|
30
|
+
|
|
31
|
+
# Write the content to the target file (this will overwrite if it exists)
|
|
32
|
+
with open(target_file, "wb") as f:
|
|
33
|
+
f.write(content)
|
|
34
|
+
|
|
35
|
+
LOG.info(f"Successfully downloaded laminar.mdc to {target_file}")
|
|
36
|
+
|
|
37
|
+
except urllib.error.URLError as e:
|
|
38
|
+
LOG.error(f"Failed to download file from {url}: {e}")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
LOG.error(f"Unexpected error: {e}")
|
|
42
|
+
sys.exit(1)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
5
|
+
from opentelemetry.sdk.resources import SERVICE_NAME
|
|
6
|
+
|
|
7
|
+
from lmnr.opentelemetry_lib.tracing.instruments import Instruments
|
|
8
|
+
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
|
9
|
+
from lmnr.sdk.types import SessionRecordingOptions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TracerManager:
|
|
13
|
+
__tracer_wrapper: TracerWrapper
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def init(
|
|
17
|
+
app_name: str | None = sys.argv[0],
|
|
18
|
+
disable_batch=False,
|
|
19
|
+
exporter: SpanExporter | None = None,
|
|
20
|
+
resource_attributes: dict = {},
|
|
21
|
+
instruments: set[Instruments] | None = None,
|
|
22
|
+
block_instruments: set[Instruments] | None = None,
|
|
23
|
+
base_url: str = "https://api.lmnr.ai",
|
|
24
|
+
port: int = 8443,
|
|
25
|
+
http_port: int = 443,
|
|
26
|
+
project_api_key: str | None = None,
|
|
27
|
+
max_export_batch_size: int | None = None,
|
|
28
|
+
force_http: bool = False,
|
|
29
|
+
timeout_seconds: int = 30,
|
|
30
|
+
set_global_tracer_provider: bool = True,
|
|
31
|
+
otel_logger_level: int = logging.ERROR,
|
|
32
|
+
session_recording_options: SessionRecordingOptions | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
enable_content_tracing = True
|
|
35
|
+
|
|
36
|
+
# Tracer init
|
|
37
|
+
resource_attributes.update({SERVICE_NAME: app_name})
|
|
38
|
+
TracerWrapper.set_static_params(resource_attributes, enable_content_tracing)
|
|
39
|
+
TracerManager.__tracer_wrapper = TracerWrapper(
|
|
40
|
+
disable_batch=disable_batch,
|
|
41
|
+
exporter=exporter,
|
|
42
|
+
instruments=instruments,
|
|
43
|
+
block_instruments=block_instruments,
|
|
44
|
+
base_url=base_url,
|
|
45
|
+
port=port,
|
|
46
|
+
http_port=http_port,
|
|
47
|
+
project_api_key=project_api_key,
|
|
48
|
+
max_export_batch_size=max_export_batch_size,
|
|
49
|
+
force_http=force_http,
|
|
50
|
+
timeout_seconds=timeout_seconds,
|
|
51
|
+
set_global_tracer_provider=set_global_tracer_provider,
|
|
52
|
+
otel_logger_level=otel_logger_level,
|
|
53
|
+
session_recording_options=session_recording_options,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def flush() -> bool:
|
|
58
|
+
if not hasattr(TracerManager, "_TracerManager__tracer_wrapper"):
|
|
59
|
+
return False
|
|
60
|
+
return TracerManager.__tracer_wrapper.flush()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def shutdown():
|
|
64
|
+
TracerManager.__tracer_wrapper.shutdown()
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def force_reinit_processor():
|
|
68
|
+
if not hasattr(TracerManager, "_TracerManager__tracer_wrapper"):
|
|
69
|
+
return False
|
|
70
|
+
return TracerManager.__tracer_wrapper.force_reinit_processor()
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
import types
|
|
3
|
+
from typing import Any, AsyncGenerator, Callable, Generator, Literal, TypeVar
|
|
4
|
+
|
|
5
|
+
from opentelemetry import context as context_api
|
|
6
|
+
from opentelemetry.trace import Span, Status, StatusCode
|
|
7
|
+
|
|
8
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
|
9
|
+
CONTEXT_METADATA_KEY,
|
|
10
|
+
attach_context,
|
|
11
|
+
detach_context,
|
|
12
|
+
get_event_attributes_from_context,
|
|
13
|
+
)
|
|
14
|
+
from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
|
|
15
|
+
from lmnr.opentelemetry_lib.tracing.utils import set_association_props_in_context
|
|
16
|
+
from lmnr.sdk.utils import get_input_from_func_args, is_method
|
|
17
|
+
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
|
|
18
|
+
from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
19
|
+
ASSOCIATION_PROPERTIES,
|
|
20
|
+
METADATA,
|
|
21
|
+
SPAN_TYPE,
|
|
22
|
+
)
|
|
23
|
+
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
|
24
|
+
from lmnr.sdk.log import get_default_logger
|
|
25
|
+
from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
|
|
26
|
+
|
|
27
|
+
logger = get_default_logger(__name__)
|
|
28
|
+
|
|
29
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _setup_span(
|
|
33
|
+
span_name: str,
|
|
34
|
+
span_type: str,
|
|
35
|
+
association_properties: dict[str, Any] | None,
|
|
36
|
+
preserve_global_context: bool = False,
|
|
37
|
+
metadata: dict[str, Any] | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""Set up a span with the given name, type, and association properties."""
|
|
40
|
+
with get_tracer_with_context() as (tracer, isolated_context):
|
|
41
|
+
# Create span in isolated context
|
|
42
|
+
span = tracer.start_span(
|
|
43
|
+
span_name,
|
|
44
|
+
context=isolated_context if not preserve_global_context else None,
|
|
45
|
+
attributes={SPAN_TYPE: span_type},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
ctx_metadata = context_api.get_value(CONTEXT_METADATA_KEY, isolated_context)
|
|
49
|
+
merged_metadata = {
|
|
50
|
+
**(ctx_metadata or {}),
|
|
51
|
+
**(metadata or {}),
|
|
52
|
+
}
|
|
53
|
+
for key, value in merged_metadata.items():
|
|
54
|
+
span.set_attribute(
|
|
55
|
+
f"{ASSOCIATION_PROPERTIES}.{METADATA}.{key}",
|
|
56
|
+
(value if is_otel_attribute_value_type(value) else json_dumps(value)),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if association_properties is not None:
|
|
60
|
+
for key, value in association_properties.items():
|
|
61
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
|
|
62
|
+
|
|
63
|
+
return span
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _process_input(
|
|
67
|
+
span: Span,
|
|
68
|
+
fn: Callable,
|
|
69
|
+
args: tuple,
|
|
70
|
+
kwargs: dict,
|
|
71
|
+
ignore_input: bool,
|
|
72
|
+
ignore_inputs: list[str] | None,
|
|
73
|
+
input_formatter: Callable[..., str] | None,
|
|
74
|
+
):
|
|
75
|
+
"""Process and set input attributes on the span."""
|
|
76
|
+
if ignore_input:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
if input_formatter is not None:
|
|
81
|
+
inp = input_formatter(*args, **kwargs)
|
|
82
|
+
else:
|
|
83
|
+
inp = get_input_from_func_args(
|
|
84
|
+
fn,
|
|
85
|
+
is_method=is_method(fn),
|
|
86
|
+
func_args=args,
|
|
87
|
+
func_kwargs=kwargs,
|
|
88
|
+
ignore_inputs=ignore_inputs,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not isinstance(span, LaminarSpan):
|
|
92
|
+
span = LaminarSpan(span)
|
|
93
|
+
span.set_input(inp)
|
|
94
|
+
except Exception:
|
|
95
|
+
msg = "Failed to process input, ignoring"
|
|
96
|
+
if input_formatter is not None:
|
|
97
|
+
# Only warn the user if they provided an input formatter
|
|
98
|
+
# because it's their responsibility to make sure it works.
|
|
99
|
+
logger.warning(msg, exc_info=True)
|
|
100
|
+
else:
|
|
101
|
+
logger.debug(msg, exc_info=True)
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _process_output(
|
|
106
|
+
span: Span,
|
|
107
|
+
result: Any,
|
|
108
|
+
ignore_output: bool,
|
|
109
|
+
output_formatter: Callable[..., str] | None,
|
|
110
|
+
):
|
|
111
|
+
"""Process and set output attributes on the span."""
|
|
112
|
+
if ignore_output:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if output_formatter is not None:
|
|
117
|
+
output = output_formatter(result)
|
|
118
|
+
else:
|
|
119
|
+
output = result
|
|
120
|
+
|
|
121
|
+
if not isinstance(span, LaminarSpan):
|
|
122
|
+
span = LaminarSpan(span)
|
|
123
|
+
span.set_output(output)
|
|
124
|
+
except Exception:
|
|
125
|
+
msg = "Failed to process output, ignoring"
|
|
126
|
+
if output_formatter is not None:
|
|
127
|
+
# Only warn the user if they provided an output formatter
|
|
128
|
+
# because it's their responsibility to make sure it works.
|
|
129
|
+
logger.warning(msg, exc_info=True)
|
|
130
|
+
else:
|
|
131
|
+
logger.debug(msg, exc_info=True)
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cleanup_span(span: Span, wrapper: TracerWrapper):
|
|
136
|
+
"""Clean up span and context."""
|
|
137
|
+
span.end()
|
|
138
|
+
wrapper.pop_span_context()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def observe_base(
|
|
142
|
+
*,
|
|
143
|
+
name: str | None = None,
|
|
144
|
+
ignore_input: bool = False,
|
|
145
|
+
ignore_inputs: list[str] | None = None,
|
|
146
|
+
ignore_output: bool = False,
|
|
147
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
149
|
+
association_properties: dict[str, Any] | None = None,
|
|
150
|
+
input_formatter: Callable[..., str] | None = None,
|
|
151
|
+
output_formatter: Callable[..., str] | None = None,
|
|
152
|
+
preserve_global_context: bool = False,
|
|
153
|
+
) -> Callable[[F], F]:
|
|
154
|
+
def decorate(fn: F) -> F:
|
|
155
|
+
@wraps(fn)
|
|
156
|
+
def wrap(*args, **kwargs):
|
|
157
|
+
if not TracerWrapper.verify_initialized():
|
|
158
|
+
return fn(*args, **kwargs)
|
|
159
|
+
|
|
160
|
+
span_name = name or fn.__name__
|
|
161
|
+
wrapper = TracerWrapper()
|
|
162
|
+
|
|
163
|
+
span = _setup_span(
|
|
164
|
+
span_name,
|
|
165
|
+
span_type,
|
|
166
|
+
association_properties,
|
|
167
|
+
preserve_global_context,
|
|
168
|
+
metadata,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Set association props in context before push_span_context
|
|
172
|
+
# so child spans inherit them
|
|
173
|
+
assoc_props_token = set_association_props_in_context(span)
|
|
174
|
+
if assoc_props_token and isinstance(span, LaminarSpan):
|
|
175
|
+
span._lmnr_assoc_props_token = assoc_props_token
|
|
176
|
+
|
|
177
|
+
new_context = wrapper.push_span_context(span)
|
|
178
|
+
# Some auto-instrumentations are not under our control, so they
|
|
179
|
+
# don't have access to our isolated context. We attach the context
|
|
180
|
+
# to the OTEL global context, so that spans know their parent
|
|
181
|
+
# span and trace_id.
|
|
182
|
+
ctx_token = context_api.attach(new_context)
|
|
183
|
+
# update our isolated context too
|
|
184
|
+
isolated_ctx_token = attach_context(new_context)
|
|
185
|
+
|
|
186
|
+
_process_input(
|
|
187
|
+
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
res = fn(*args, **kwargs)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
_process_exception(span, e)
|
|
194
|
+
_cleanup_span(span, wrapper)
|
|
195
|
+
raise
|
|
196
|
+
finally:
|
|
197
|
+
# Always restore global context
|
|
198
|
+
context_api.detach(ctx_token)
|
|
199
|
+
detach_context(isolated_ctx_token)
|
|
200
|
+
# span will be ended in the generator
|
|
201
|
+
if isinstance(res, types.GeneratorType):
|
|
202
|
+
return _handle_generator(span, wrapper, res)
|
|
203
|
+
if isinstance(res, types.AsyncGeneratorType):
|
|
204
|
+
# async def foo() -> AsyncGenerator[int, None]:
|
|
205
|
+
# is not considered async in a classical sense in Python,
|
|
206
|
+
# so we handle this inside the sync wrapper.
|
|
207
|
+
# In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
|
|
208
|
+
# Flags are listed from LSB here:
|
|
209
|
+
# https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
|
|
210
|
+
# See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
|
|
211
|
+
return _ahandle_generator(span, wrapper, res)
|
|
212
|
+
|
|
213
|
+
_process_output(span, res, ignore_output, output_formatter)
|
|
214
|
+
_cleanup_span(span, wrapper)
|
|
215
|
+
return res
|
|
216
|
+
|
|
217
|
+
return wrap
|
|
218
|
+
|
|
219
|
+
return decorate
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Async Decorators
|
|
223
|
+
def async_observe_base(
|
|
224
|
+
*,
|
|
225
|
+
name: str | None = None,
|
|
226
|
+
ignore_input: bool = False,
|
|
227
|
+
ignore_inputs: list[str] | None = None,
|
|
228
|
+
ignore_output: bool = False,
|
|
229
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
230
|
+
metadata: dict[str, Any] | None = None,
|
|
231
|
+
association_properties: dict[str, Any] | None = None,
|
|
232
|
+
input_formatter: Callable[..., str] | None = None,
|
|
233
|
+
output_formatter: Callable[..., str] | None = None,
|
|
234
|
+
preserve_global_context: bool = False,
|
|
235
|
+
) -> Callable[[F], F]:
|
|
236
|
+
def decorate(fn: F) -> F:
|
|
237
|
+
@wraps(fn)
|
|
238
|
+
async def wrap(*args, **kwargs):
|
|
239
|
+
if not TracerWrapper.verify_initialized():
|
|
240
|
+
return await fn(*args, **kwargs)
|
|
241
|
+
|
|
242
|
+
span_name = name or fn.__name__
|
|
243
|
+
wrapper = TracerWrapper()
|
|
244
|
+
|
|
245
|
+
span = _setup_span(
|
|
246
|
+
span_name,
|
|
247
|
+
span_type,
|
|
248
|
+
association_properties,
|
|
249
|
+
preserve_global_context,
|
|
250
|
+
metadata,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Set association props in context before push_span_context
|
|
254
|
+
# so child spans inherit them
|
|
255
|
+
assoc_props_token = set_association_props_in_context(span)
|
|
256
|
+
if assoc_props_token and isinstance(span, LaminarSpan):
|
|
257
|
+
span._lmnr_assoc_props_token = assoc_props_token
|
|
258
|
+
|
|
259
|
+
new_context = wrapper.push_span_context(span)
|
|
260
|
+
# Some auto-instrumentations are not under our control, so they
|
|
261
|
+
# don't have access to our isolated context. We attach the context
|
|
262
|
+
# to the OTEL global context, so that spans know their parent
|
|
263
|
+
# span and trace_id.
|
|
264
|
+
ctx_token = context_api.attach(new_context)
|
|
265
|
+
# update our isolated context too
|
|
266
|
+
isolated_ctx_token = attach_context(new_context)
|
|
267
|
+
|
|
268
|
+
_process_input(
|
|
269
|
+
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
res = await fn(*args, **kwargs)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
_process_exception(span, e)
|
|
276
|
+
_cleanup_span(span, wrapper)
|
|
277
|
+
raise e
|
|
278
|
+
finally:
|
|
279
|
+
# Always restore global context
|
|
280
|
+
context_api.detach(ctx_token)
|
|
281
|
+
detach_context(isolated_ctx_token)
|
|
282
|
+
|
|
283
|
+
# span will be ended in the generator
|
|
284
|
+
if isinstance(res, types.AsyncGeneratorType):
|
|
285
|
+
# probably unreachable, read the comment in the similar
|
|
286
|
+
# part of the sync wrapper.
|
|
287
|
+
return await _ahandle_generator(span, wrapper, res)
|
|
288
|
+
|
|
289
|
+
_process_output(span, res, ignore_output, output_formatter)
|
|
290
|
+
_cleanup_span(span, wrapper)
|
|
291
|
+
return res
|
|
292
|
+
|
|
293
|
+
return wrap
|
|
294
|
+
|
|
295
|
+
return decorate
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _handle_generator(
|
|
299
|
+
span: Span,
|
|
300
|
+
wrapper: TracerWrapper,
|
|
301
|
+
res: Generator,
|
|
302
|
+
ignore_output: bool = False,
|
|
303
|
+
output_formatter: Callable[..., str] | None = None,
|
|
304
|
+
):
|
|
305
|
+
results = []
|
|
306
|
+
try:
|
|
307
|
+
for part in res:
|
|
308
|
+
results.append(part)
|
|
309
|
+
yield part
|
|
310
|
+
finally:
|
|
311
|
+
_process_output(span, results, ignore_output, output_formatter)
|
|
312
|
+
_cleanup_span(span, wrapper)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _ahandle_generator(
|
|
316
|
+
span: Span,
|
|
317
|
+
wrapper: TracerWrapper,
|
|
318
|
+
res: AsyncGenerator,
|
|
319
|
+
ignore_output: bool = False,
|
|
320
|
+
output_formatter: Callable[..., str] | None = None,
|
|
321
|
+
):
|
|
322
|
+
results = []
|
|
323
|
+
try:
|
|
324
|
+
async for part in res:
|
|
325
|
+
results.append(part)
|
|
326
|
+
yield part
|
|
327
|
+
finally:
|
|
328
|
+
_process_output(span, results, ignore_output, output_formatter)
|
|
329
|
+
_cleanup_span(span, wrapper)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _process_exception(span: Span, e: Exception):
|
|
333
|
+
# Note that this `escaped` is sent as a StringValue("True"), not a boolean.
|
|
334
|
+
span.record_exception(
|
|
335
|
+
e, attributes=get_event_attributes_from_context(), escaped=True
|
|
336
|
+
)
|
|
337
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|