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,94 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from lmnr.sdk.client.synchronous.sync_client import LaminarClient
|
|
7
|
+
from lmnr.sdk.datasets.file_utils import load_from_paths
|
|
8
|
+
from lmnr.sdk.log import get_default_logger
|
|
9
|
+
from lmnr.sdk.types import Datapoint
|
|
10
|
+
|
|
11
|
+
DEFAULT_FETCH_SIZE = 25
|
|
12
|
+
LOG = get_default_logger(__name__, verbose=False)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EvaluationDataset(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def __len__(self) -> int:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def __getitem__(self, idx) -> Datapoint:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def slice(self, start: int, end: int):
|
|
29
|
+
return [self[i] for i in range(max(start, 0), min(end, len(self)))]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LaminarDataset(EvaluationDataset):
|
|
33
|
+
client: LaminarClient
|
|
34
|
+
id: uuid.UUID | None = None
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
name: str | None = None,
|
|
39
|
+
id: uuid.UUID | None = None,
|
|
40
|
+
fetch_size: int = DEFAULT_FETCH_SIZE,
|
|
41
|
+
):
|
|
42
|
+
self.name = name
|
|
43
|
+
self.id = id
|
|
44
|
+
if name is None and id is None:
|
|
45
|
+
raise ValueError("Either name or id must be provided")
|
|
46
|
+
if name is not None and id is not None:
|
|
47
|
+
raise ValueError("Only one of name or id must be provided")
|
|
48
|
+
self._len = None
|
|
49
|
+
self._fetched_items = []
|
|
50
|
+
self._offset = 0
|
|
51
|
+
self._fetch_size = fetch_size
|
|
52
|
+
self._logger = get_default_logger(self.__class__.__name__)
|
|
53
|
+
|
|
54
|
+
def _fetch_batch(self):
|
|
55
|
+
self._logger.debug(
|
|
56
|
+
f"dataset name: {self.name}, id: {self.id}. Fetching batch from {self._offset} to "
|
|
57
|
+
+ f"{self._offset + self._fetch_size}"
|
|
58
|
+
)
|
|
59
|
+
identifier = {"id": self.id} if self.id is not None else {"name": self.name}
|
|
60
|
+
resp = self.client.datasets.pull(
|
|
61
|
+
**identifier,
|
|
62
|
+
offset=self._offset,
|
|
63
|
+
limit=self._fetch_size,
|
|
64
|
+
)
|
|
65
|
+
self._fetched_items += resp.items
|
|
66
|
+
self._offset = len(self._fetched_items)
|
|
67
|
+
if self._len is None:
|
|
68
|
+
self._len = resp.total_count
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
if self._len is None:
|
|
72
|
+
self._fetch_batch()
|
|
73
|
+
return self._len
|
|
74
|
+
|
|
75
|
+
def __getitem__(self, idx) -> Datapoint:
|
|
76
|
+
if idx >= len(self._fetched_items):
|
|
77
|
+
self._fetch_batch()
|
|
78
|
+
return self._fetched_items[idx]
|
|
79
|
+
|
|
80
|
+
def set_client(self, client: LaminarClient):
|
|
81
|
+
self.client = client
|
|
82
|
+
|
|
83
|
+
def push(self, paths: str | list[str], recursive: bool = False):
|
|
84
|
+
paths = [paths] if isinstance(paths, str) else paths
|
|
85
|
+
paths = [Path(path) for path in paths]
|
|
86
|
+
data = load_from_paths(paths, recursive)
|
|
87
|
+
if len(data) == 0:
|
|
88
|
+
LOG.warning("No data to push. Skipping")
|
|
89
|
+
return
|
|
90
|
+
identifier = {"id": self.id} if self.id is not None else {"name": self.name}
|
|
91
|
+
self.client.datasets.push(data, **identifier)
|
|
92
|
+
LOG.info(
|
|
93
|
+
f"Successfully pushed {len(data)} datapoints to dataset [{identifier}]"
|
|
94
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
import csv
|
|
4
|
+
import orjson
|
|
5
|
+
|
|
6
|
+
from lmnr.sdk.log import get_default_logger
|
|
7
|
+
|
|
8
|
+
LOG = get_default_logger(__name__, verbose=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_supported_file(file: Path) -> bool:
|
|
12
|
+
"""Check if a file is supported."""
|
|
13
|
+
return file.suffix in [".json", ".csv", ".jsonl"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _collect_files(paths: list[Path], recursive: bool = False) -> list[Path]:
|
|
17
|
+
"""
|
|
18
|
+
Collect all supported files from the given paths.
|
|
19
|
+
|
|
20
|
+
Handles both files and directories. If a path is a directory,
|
|
21
|
+
collects all supported files within it (recursively if specified).
|
|
22
|
+
"""
|
|
23
|
+
collected_files = []
|
|
24
|
+
|
|
25
|
+
for path in paths:
|
|
26
|
+
if path.is_file():
|
|
27
|
+
if _is_supported_file(path):
|
|
28
|
+
collected_files.append(path)
|
|
29
|
+
else:
|
|
30
|
+
LOG.warning(f"Skipping unsupported file type: {path}")
|
|
31
|
+
elif path.is_dir():
|
|
32
|
+
for item in path.iterdir():
|
|
33
|
+
if item.is_file() and _is_supported_file(item):
|
|
34
|
+
collected_files.append(item)
|
|
35
|
+
elif recursive and item.is_dir():
|
|
36
|
+
# Recursively collect files from subdirectories
|
|
37
|
+
collected_files.extend(_collect_files([item], recursive=True))
|
|
38
|
+
else:
|
|
39
|
+
LOG.warning(f"Path does not exist or is not accessible: {path}")
|
|
40
|
+
|
|
41
|
+
return collected_files
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_file(file: Path) -> list[dict[str, Any]]:
|
|
45
|
+
"""Read data from a single file and return as a list of dictionaries."""
|
|
46
|
+
if file.suffix == ".json":
|
|
47
|
+
result = orjson.loads(file.read_bytes())
|
|
48
|
+
if isinstance(result, list):
|
|
49
|
+
return result
|
|
50
|
+
else:
|
|
51
|
+
return [result]
|
|
52
|
+
elif file.suffix == ".csv":
|
|
53
|
+
return [dict(row) for row in csv.DictReader(file.read_text().splitlines())]
|
|
54
|
+
elif file.suffix == ".jsonl":
|
|
55
|
+
return [
|
|
56
|
+
orjson.loads(line) for line in file.read_text().splitlines() if line.strip()
|
|
57
|
+
]
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"Unsupported file type: {file.suffix}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_from_paths(paths: list[Path], recursive: bool = False) -> list[dict[str, Any]]:
|
|
63
|
+
"""
|
|
64
|
+
Load data from all files in the specified paths.
|
|
65
|
+
|
|
66
|
+
First collects all file paths, then reads each file's data.
|
|
67
|
+
"""
|
|
68
|
+
files = _collect_files(paths, recursive)
|
|
69
|
+
|
|
70
|
+
if not files:
|
|
71
|
+
LOG.warning("No supported files found in the specified paths")
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
LOG.info(f"Found {len(files)} file(s) to read")
|
|
75
|
+
|
|
76
|
+
result = []
|
|
77
|
+
for file in files:
|
|
78
|
+
try:
|
|
79
|
+
data = _read_file(file)
|
|
80
|
+
result.extend(data)
|
|
81
|
+
LOG.info(f"Read {len(data)} record(s) from {file}")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
LOG.error(f"Error reading file {file}: {e}")
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_paths(paths: list[str]) -> list[Path]:
|
|
90
|
+
"""Parse paths."""
|
|
91
|
+
return [Path(path) for path in paths]
|
lmnr/sdk/decorators.py
CHANGED
|
@@ -1,46 +1,132 @@
|
|
|
1
|
-
from lmnr.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from lmnr.opentelemetry_lib.decorators import (
|
|
2
|
+
observe_base,
|
|
3
|
+
async_observe_base,
|
|
4
4
|
)
|
|
5
5
|
from opentelemetry.trace import INVALID_SPAN, get_current_span
|
|
6
6
|
|
|
7
|
-
from typing import Callable,
|
|
7
|
+
from typing import Any, Callable, Coroutine, Literal, TypeVar, overload
|
|
8
8
|
from typing_extensions import ParamSpec
|
|
9
9
|
|
|
10
|
-
from lmnr.
|
|
11
|
-
from lmnr.
|
|
10
|
+
from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID
|
|
11
|
+
from lmnr.sdk.log import get_default_logger
|
|
12
|
+
from lmnr.sdk.types import TraceType
|
|
12
13
|
|
|
13
14
|
from .utils import is_async
|
|
14
15
|
|
|
16
|
+
logger = get_default_logger(__name__)
|
|
15
17
|
|
|
16
18
|
P = ParamSpec("P")
|
|
17
19
|
R = TypeVar("R")
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
# Overload for synchronous functions
|
|
23
|
+
@overload
|
|
20
24
|
def observe(
|
|
21
25
|
*,
|
|
22
|
-
name:
|
|
23
|
-
session_id:
|
|
24
|
-
|
|
26
|
+
name: str | None = None,
|
|
27
|
+
session_id: str | None = None,
|
|
28
|
+
user_id: str | None = None,
|
|
29
|
+
ignore_input: bool = False,
|
|
30
|
+
ignore_output: bool = False,
|
|
31
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
32
|
+
ignore_inputs: list[str] | None = None,
|
|
33
|
+
input_formatter: Callable[..., str] | None = None,
|
|
34
|
+
output_formatter: Callable[..., str] | None = None,
|
|
35
|
+
metadata: dict[str, Any] | None = None,
|
|
36
|
+
tags: list[str] | None = None,
|
|
37
|
+
preserve_global_context: bool = False,
|
|
38
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Overload for asynchronous functions
|
|
42
|
+
@overload
|
|
43
|
+
def observe(
|
|
44
|
+
*,
|
|
45
|
+
name: str | None = None,
|
|
46
|
+
session_id: str | None = None,
|
|
47
|
+
user_id: str | None = None,
|
|
48
|
+
ignore_input: bool = False,
|
|
49
|
+
ignore_output: bool = False,
|
|
50
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
51
|
+
ignore_inputs: list[str] | None = None,
|
|
52
|
+
input_formatter: Callable[..., str] | None = None,
|
|
53
|
+
output_formatter: Callable[..., str] | None = None,
|
|
54
|
+
metadata: dict[str, Any] | None = None,
|
|
55
|
+
tags: list[str] | None = None,
|
|
56
|
+
preserve_global_context: bool = False,
|
|
57
|
+
) -> Callable[
|
|
58
|
+
[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
|
|
59
|
+
]: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Implementation
|
|
63
|
+
def observe(
|
|
64
|
+
*,
|
|
65
|
+
name: str | None = None,
|
|
66
|
+
session_id: str | None = None,
|
|
67
|
+
user_id: str | None = None,
|
|
68
|
+
ignore_input: bool = False,
|
|
69
|
+
ignore_output: bool = False,
|
|
70
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
71
|
+
ignore_inputs: list[str] | None = None,
|
|
72
|
+
input_formatter: Callable[..., str] | None = None,
|
|
73
|
+
output_formatter: Callable[..., str] | None = None,
|
|
74
|
+
metadata: dict[str, Any] | None = None,
|
|
75
|
+
tags: list[str] | None = None,
|
|
76
|
+
preserve_global_context: bool = False,
|
|
77
|
+
):
|
|
78
|
+
# Return type is determined by overloads above
|
|
25
79
|
"""The main decorator entrypoint for Laminar. This is used to wrap
|
|
26
80
|
functions and methods to create spans.
|
|
27
81
|
|
|
28
82
|
Args:
|
|
29
|
-
name (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
83
|
+
name (str | None, optional): Name of the span. Function name is used if\
|
|
84
|
+
not specified. Defaults to None.
|
|
85
|
+
session_id (str | None, optional): Session ID to associate with the\
|
|
86
|
+
span and the following context. Defaults to None.
|
|
87
|
+
user_id (str | None, optional): User ID to associate with the span and\
|
|
88
|
+
the following context. This is different from ID of a Laminar user.
|
|
89
|
+
Defaults to None.
|
|
90
|
+
ignore_input (bool, optional): Whether to ignore ALL input of the\
|
|
91
|
+
wrapped function. Defaults to False.
|
|
92
|
+
ignore_output (bool, optional): Whether to ignore ALL output of the\
|
|
93
|
+
wrapped function. Defaults to False.
|
|
94
|
+
span_type (Literal["DEFAULT", "LLM", "TOOL"], optional): Type of the span.
|
|
95
|
+
Defaults to "DEFAULT".
|
|
96
|
+
ignore_inputs (list[str] | None, optional): List of input keys to\
|
|
97
|
+
ignore. For example, if the wrapped function takes three arguments\
|
|
98
|
+
def foo(a, b, `sensitive_data`), and you want to ignore the\
|
|
99
|
+
`sensitive_data` argument, you can pass ["sensitive_data"] to\
|
|
100
|
+
this argument. Defaults to None.
|
|
101
|
+
input_formatter (Callable[P, str] | None, optional): A custom function\
|
|
102
|
+
to format the input of the wrapped function. This function should\
|
|
103
|
+
accept the same parameters as the wrapped function and return a string.\
|
|
104
|
+
All function arguments are passed to this function. Ignored if\
|
|
105
|
+
`ignore_input` is True. Does not respect `ignore_inputs` argument.
|
|
106
|
+
Defaults to None.
|
|
107
|
+
output_formatter (Callable[[R], str] | None, optional): A custom function\
|
|
108
|
+
to format the output of the wrapped function. This function should\
|
|
109
|
+
accept a single parameter (the return value of the wrapped function)\
|
|
110
|
+
and return a string. Ignored if `ignore_output` is True.\
|
|
111
|
+
Defaults to None.
|
|
112
|
+
metadata (dict[str, Any] | None, optional): Metadata to associate with\
|
|
113
|
+
the trace. Must be JSON serializable. Defaults to None.
|
|
114
|
+
tags (list[str] | None, optional): Tags to associate with the trace.
|
|
115
|
+
Defaults to None.
|
|
116
|
+
preserve_global_context (bool, optional): Whether to preserve the global\
|
|
117
|
+
OpenTelemetry context. If set to True, Laminar spans will continue\
|
|
118
|
+
traces started in the global context. Defaults to False.
|
|
35
119
|
Raises:
|
|
36
|
-
Exception: re-raises the exception if the wrapped function raises
|
|
37
|
-
|
|
120
|
+
Exception: re-raises the exception if the wrapped function raises an\
|
|
121
|
+
exception
|
|
38
122
|
|
|
39
123
|
Returns:
|
|
40
124
|
R: Returns the result of the wrapped function
|
|
41
125
|
"""
|
|
42
126
|
|
|
43
|
-
def decorator(
|
|
127
|
+
def decorator(
|
|
128
|
+
func: Callable[P, R] | Callable[P, Coroutine[Any, Any, R]],
|
|
129
|
+
) -> Callable[P, R] | Callable[P, Coroutine[Any, Any, R]]:
|
|
44
130
|
current_span = get_current_span()
|
|
45
131
|
if current_span != INVALID_SPAN:
|
|
46
132
|
if session_id is not None:
|
|
@@ -48,11 +134,62 @@ def observe(
|
|
|
48
134
|
association_properties = {}
|
|
49
135
|
if session_id is not None:
|
|
50
136
|
association_properties["session_id"] = session_id
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
137
|
+
if user_id is not None:
|
|
138
|
+
association_properties["user_id"] = user_id
|
|
139
|
+
if span_type in ["EVALUATION", "EXECUTOR", "EVALUATOR"]:
|
|
140
|
+
association_properties["trace_type"] = TraceType.EVALUATION.value
|
|
141
|
+
if tags is not None:
|
|
142
|
+
if not isinstance(tags, list) or not all(
|
|
143
|
+
isinstance(tag, str) for tag in tags
|
|
144
|
+
):
|
|
145
|
+
logger.warning("Tags must be a list of strings. Tags will be ignored.")
|
|
146
|
+
else:
|
|
147
|
+
# list(set(tags)) to deduplicate tags
|
|
148
|
+
association_properties["tags"] = list(set(tags))
|
|
149
|
+
if input_formatter is not None and ignore_input:
|
|
150
|
+
logger.warning(
|
|
151
|
+
f"observe, function {func.__name__}: Input formatter"
|
|
152
|
+
" is ignored because `ignore_input` is True. Specify only one of"
|
|
153
|
+
" `ignore_input` or `input_formatter`."
|
|
154
|
+
)
|
|
155
|
+
if input_formatter is not None and ignore_inputs is not None:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"observe, function {func.__name__}: Both input formatter and"
|
|
158
|
+
" `ignore_inputs` are specified. Input formatter"
|
|
159
|
+
" will pass all arguments to the formatter regardless of"
|
|
160
|
+
" `ignore_inputs`."
|
|
161
|
+
)
|
|
162
|
+
if output_formatter is not None and ignore_output:
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"observe, function {func.__name__}: Output formatter"
|
|
165
|
+
" is ignored because `ignore_output` is True. Specify only one of"
|
|
166
|
+
" `ignore_output` or `output_formatter`."
|
|
167
|
+
)
|
|
168
|
+
if is_async(func):
|
|
169
|
+
return async_observe_base(
|
|
170
|
+
name=name,
|
|
171
|
+
ignore_input=ignore_input,
|
|
172
|
+
ignore_output=ignore_output,
|
|
173
|
+
span_type=span_type,
|
|
174
|
+
metadata=metadata,
|
|
175
|
+
ignore_inputs=ignore_inputs,
|
|
176
|
+
input_formatter=input_formatter,
|
|
177
|
+
output_formatter=output_formatter,
|
|
178
|
+
association_properties=association_properties,
|
|
179
|
+
preserve_global_context=preserve_global_context,
|
|
180
|
+
)(func)
|
|
181
|
+
else:
|
|
182
|
+
return observe_base(
|
|
183
|
+
name=name,
|
|
184
|
+
ignore_input=ignore_input,
|
|
185
|
+
ignore_output=ignore_output,
|
|
186
|
+
span_type=span_type,
|
|
187
|
+
metadata=metadata,
|
|
188
|
+
ignore_inputs=ignore_inputs,
|
|
189
|
+
input_formatter=input_formatter,
|
|
190
|
+
output_formatter=output_formatter,
|
|
191
|
+
association_properties=association_properties,
|
|
192
|
+
preserve_global_context=preserve_global_context,
|
|
193
|
+
)(func)
|
|
194
|
+
|
|
195
|
+
return decorator
|
lmnr/sdk/eval_control.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from contextvars import ContextVar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
PREPARE_ONLY: ContextVar[bool] = ContextVar("__lmnr_prepare_only", default=False)
|
|
5
|
+
EVALUATION_INSTANCES = ContextVar("__lmnr_evaluation_instances")
|