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/sdk/log.py
CHANGED
|
@@ -62,11 +62,16 @@ class VerboseFormatter(CustomFormatter):
|
|
|
62
62
|
return formatter.format(record)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def get_default_logger(
|
|
65
|
+
def get_default_logger(
|
|
66
|
+
name: str, level: int = logging.INFO, propagate: bool = False, verbose: bool = True
|
|
67
|
+
) -> logging.Logger:
|
|
66
68
|
logger = logging.getLogger(name)
|
|
67
69
|
logger.setLevel(level)
|
|
68
70
|
console_log_handler = logging.StreamHandler()
|
|
69
|
-
|
|
71
|
+
if verbose:
|
|
72
|
+
console_log_handler.setFormatter(VerboseColorfulFormatter())
|
|
73
|
+
else:
|
|
74
|
+
console_log_handler.setFormatter(ColorfulFormatter())
|
|
70
75
|
logger.addHandler(console_log_handler)
|
|
71
76
|
logger.propagate = propagate
|
|
72
77
|
return logger
|
lmnr/sdk/types.py
CHANGED
|
@@ -1,131 +1,56 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations # For "Self" | str | ... type hint
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
2
5
|
import datetime
|
|
3
|
-
from
|
|
4
|
-
import pydantic
|
|
5
|
-
from typing import Any, Awaitable, Callable, Optional, Union
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
6
7
|
import uuid
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
role: str
|
|
13
|
-
content: str
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from opentelemetry.trace import SpanContext, TraceFlags
|
|
11
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
12
|
+
from typing_extensions import TypedDict # compatibility with python < 3.12
|
|
14
13
|
|
|
14
|
+
from .utils import serialize, json_dumps
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
condition: str
|
|
18
|
-
value: "NodeInput"
|
|
16
|
+
DEFAULT_DATAPOINT_MAX_DATA_LENGTH = 16_000_000 # 16MB
|
|
19
17
|
|
|
20
18
|
|
|
21
|
-
Numeric =
|
|
19
|
+
Numeric = int | float
|
|
22
20
|
NumericTypes = (int, float) # for use with isinstance
|
|
23
21
|
|
|
24
|
-
NodeInput = Union[str, list[ChatMessage], ConditionedValue, Numeric, bool]
|
|
25
|
-
PipelineOutput = Union[NodeInput]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class PipelineRunRequest(pydantic.BaseModel):
|
|
29
|
-
inputs: dict[str, NodeInput]
|
|
30
|
-
pipeline: str
|
|
31
|
-
env: dict[str, str] = pydantic.Field(default_factory=dict)
|
|
32
|
-
metadata: dict[str, str] = pydantic.Field(default_factory=dict)
|
|
33
|
-
stream: bool = pydantic.Field(default=False)
|
|
34
|
-
parent_span_id: Optional[uuid.UUID] = pydantic.Field(default=None)
|
|
35
|
-
trace_id: Optional[uuid.UUID] = pydantic.Field(default=None)
|
|
36
|
-
|
|
37
|
-
# uuid is not serializable by default, so we need to convert it to a string
|
|
38
|
-
def to_dict(self):
|
|
39
|
-
return {
|
|
40
|
-
"inputs": {
|
|
41
|
-
k: v.model_dump() if isinstance(v, pydantic.BaseModel) else serialize(v)
|
|
42
|
-
for k, v in self.inputs.items()
|
|
43
|
-
},
|
|
44
|
-
"pipeline": self.pipeline,
|
|
45
|
-
"env": self.env,
|
|
46
|
-
"metadata": self.metadata,
|
|
47
|
-
"stream": self.stream,
|
|
48
|
-
"parentSpanId": str(self.parent_span_id) if self.parent_span_id else None,
|
|
49
|
-
"traceId": str(self.trace_id) if self.trace_id else None,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class PipelineRunResponse(pydantic.BaseModel):
|
|
54
|
-
outputs: dict[str, dict[str, PipelineOutput]]
|
|
55
|
-
run_id: str
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class SemanticSearchRequest(pydantic.BaseModel):
|
|
59
|
-
query: str
|
|
60
|
-
dataset_id: uuid.UUID
|
|
61
|
-
limit: Optional[int] = pydantic.Field(default=None)
|
|
62
|
-
threshold: Optional[float] = pydantic.Field(default=None, ge=0.0, le=1.0)
|
|
63
|
-
|
|
64
|
-
def to_dict(self):
|
|
65
|
-
res = {
|
|
66
|
-
"query": self.query,
|
|
67
|
-
"datasetId": str(self.dataset_id),
|
|
68
|
-
}
|
|
69
|
-
if self.limit is not None:
|
|
70
|
-
res["limit"] = self.limit
|
|
71
|
-
if self.threshold is not None:
|
|
72
|
-
res["threshold"] = self.threshold
|
|
73
|
-
return res
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class SemanticSearchResult(pydantic.BaseModel):
|
|
77
|
-
dataset_id: uuid.UUID
|
|
78
|
-
score: float
|
|
79
|
-
data: dict[str, Any]
|
|
80
|
-
content: str
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class SemanticSearchResponse(pydantic.BaseModel):
|
|
84
|
-
results: list[SemanticSearchResult]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class PipelineRunError(Exception):
|
|
88
|
-
error_code: str
|
|
89
|
-
error_message: str
|
|
90
|
-
|
|
91
|
-
def __init__(self, response: aiohttp.ClientResponse):
|
|
92
|
-
try:
|
|
93
|
-
resp_json = response.json()
|
|
94
|
-
self.error_code = resp_json["error_code"]
|
|
95
|
-
self.error_message = resp_json["error_message"]
|
|
96
|
-
super().__init__(self.error_message)
|
|
97
|
-
except Exception:
|
|
98
|
-
super().__init__(response.text)
|
|
99
|
-
|
|
100
|
-
def __str__(self) -> str:
|
|
101
|
-
try:
|
|
102
|
-
return str(
|
|
103
|
-
{"error_code": self.error_code, "error_message": self.error_message}
|
|
104
|
-
)
|
|
105
|
-
except Exception:
|
|
106
|
-
return super().__str__()
|
|
107
|
-
|
|
108
|
-
|
|
109
22
|
EvaluationDatapointData = Any # non-null, must be JSON-serializable
|
|
110
|
-
EvaluationDatapointTarget =
|
|
111
|
-
EvaluationDatapointMetadata =
|
|
23
|
+
EvaluationDatapointTarget = Any | None # must be JSON-serializable
|
|
24
|
+
EvaluationDatapointMetadata = Any | None # must be JSON-serializable
|
|
112
25
|
|
|
113
26
|
|
|
114
27
|
# EvaluationDatapoint is a single data point in the evaluation
|
|
115
|
-
class Datapoint(
|
|
28
|
+
class Datapoint(BaseModel):
|
|
116
29
|
# input to the executor function.
|
|
117
30
|
data: EvaluationDatapointData
|
|
118
31
|
# input to the evaluator function (alongside the executor output).
|
|
119
|
-
target: EvaluationDatapointTarget =
|
|
120
|
-
metadata: EvaluationDatapointMetadata =
|
|
32
|
+
target: EvaluationDatapointTarget = Field(default_factory=dict)
|
|
33
|
+
metadata: EvaluationDatapointMetadata = Field(default_factory=dict)
|
|
34
|
+
id: uuid.UUID | None = Field(default=None)
|
|
35
|
+
created_at: datetime.datetime | None = Field(default=None, alias="createdAt")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Dataset(BaseModel):
|
|
39
|
+
id: uuid.UUID = Field()
|
|
40
|
+
name: str = Field()
|
|
41
|
+
created_at: datetime.datetime = Field(alias="createdAt")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PushDatapointsResponse(BaseModel):
|
|
45
|
+
dataset_id: uuid.UUID = Field(alias="datasetId")
|
|
121
46
|
|
|
122
47
|
|
|
123
48
|
ExecutorFunctionReturnType = Any
|
|
124
|
-
EvaluatorFunctionReturnType =
|
|
49
|
+
EvaluatorFunctionReturnType = Numeric | dict[str, Numeric]
|
|
125
50
|
|
|
126
51
|
ExecutorFunction = Callable[
|
|
127
52
|
[EvaluationDatapointData, Any],
|
|
128
|
-
|
|
53
|
+
ExecutorFunctionReturnType | Awaitable[ExecutorFunctionReturnType],
|
|
129
54
|
]
|
|
130
55
|
|
|
131
56
|
# EvaluatorFunction is a function that takes the output of the executor and the
|
|
@@ -134,18 +59,20 @@ ExecutorFunction = Callable[
|
|
|
134
59
|
# multiple criteria in one go instead of running multiple evaluators.
|
|
135
60
|
EvaluatorFunction = Callable[
|
|
136
61
|
[ExecutorFunctionReturnType, Any],
|
|
137
|
-
|
|
62
|
+
EvaluatorFunctionReturnType | Awaitable[EvaluatorFunctionReturnType],
|
|
138
63
|
]
|
|
139
64
|
|
|
140
65
|
|
|
141
|
-
class
|
|
142
|
-
|
|
66
|
+
class HumanEvaluatorOptionsEntry(TypedDict):
|
|
67
|
+
label: str
|
|
68
|
+
value: float
|
|
143
69
|
|
|
144
|
-
def __init__(self, queue_name: str):
|
|
145
|
-
super().__init__(queueName=queue_name)
|
|
146
70
|
|
|
71
|
+
class HumanEvaluator(BaseModel):
|
|
72
|
+
options: list[HumanEvaluatorOptionsEntry] = Field(default_factory=list)
|
|
147
73
|
|
|
148
|
-
|
|
74
|
+
|
|
75
|
+
class InitEvaluationResponse(BaseModel):
|
|
149
76
|
id: uuid.UUID
|
|
150
77
|
createdAt: datetime.datetime
|
|
151
78
|
groupId: str
|
|
@@ -153,33 +80,116 @@ class CreateEvaluationResponse(pydantic.BaseModel):
|
|
|
153
80
|
projectId: uuid.UUID
|
|
154
81
|
|
|
155
82
|
|
|
156
|
-
class
|
|
83
|
+
class EvaluationDatapointDatasetLink(BaseModel):
|
|
84
|
+
dataset_id: uuid.UUID
|
|
85
|
+
datapoint_id: uuid.UUID
|
|
86
|
+
created_at: datetime.datetime
|
|
87
|
+
|
|
88
|
+
def to_dict(self):
|
|
89
|
+
return {
|
|
90
|
+
"datasetId": str(self.dataset_id),
|
|
91
|
+
"datapointId": str(self.datapoint_id),
|
|
92
|
+
"createdAt": self.created_at.isoformat(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PartialEvaluationDatapoint(BaseModel):
|
|
97
|
+
id: uuid.UUID
|
|
98
|
+
data: EvaluationDatapointData
|
|
99
|
+
target: EvaluationDatapointTarget
|
|
100
|
+
index: int
|
|
101
|
+
trace_id: uuid.UUID
|
|
102
|
+
executor_span_id: uuid.UUID
|
|
103
|
+
metadata: EvaluationDatapointMetadata = Field(default=None)
|
|
104
|
+
dataset_link: EvaluationDatapointDatasetLink | None = Field(default=None)
|
|
105
|
+
|
|
106
|
+
# uuid is not serializable by default, so we need to convert it to a string
|
|
107
|
+
def to_dict(self, max_data_length: int = DEFAULT_DATAPOINT_MAX_DATA_LENGTH):
|
|
108
|
+
serialized_data = serialize(self.data)
|
|
109
|
+
serialized_target = serialize(self.target)
|
|
110
|
+
str_data = json_dumps(serialized_data)
|
|
111
|
+
str_target = json_dumps(serialized_target)
|
|
112
|
+
try:
|
|
113
|
+
return {
|
|
114
|
+
"id": str(self.id),
|
|
115
|
+
"data": (
|
|
116
|
+
str_data[:max_data_length]
|
|
117
|
+
if len(str_data) > max_data_length
|
|
118
|
+
else serialized_data
|
|
119
|
+
),
|
|
120
|
+
"target": (
|
|
121
|
+
str_target[:max_data_length]
|
|
122
|
+
if len(str_target) > max_data_length
|
|
123
|
+
else serialized_target
|
|
124
|
+
),
|
|
125
|
+
"index": self.index,
|
|
126
|
+
"traceId": str(self.trace_id),
|
|
127
|
+
"executorSpanId": str(self.executor_span_id),
|
|
128
|
+
"metadata": (
|
|
129
|
+
serialize(self.metadata) if self.metadata is not None else {}
|
|
130
|
+
),
|
|
131
|
+
"datasetLink": (
|
|
132
|
+
self.dataset_link.to_dict()
|
|
133
|
+
if self.dataset_link is not None
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise ValueError(f"Error serializing PartialEvaluationDatapoint: {e}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class EvaluationResultDatapoint(BaseModel):
|
|
142
|
+
id: uuid.UUID
|
|
143
|
+
index: int
|
|
157
144
|
data: EvaluationDatapointData
|
|
158
145
|
target: EvaluationDatapointTarget
|
|
159
146
|
executor_output: ExecutorFunctionReturnType
|
|
160
|
-
scores: dict[str, Numeric]
|
|
161
|
-
human_evaluators: list[HumanEvaluator] = pydantic.Field(default_factory=list)
|
|
147
|
+
scores: dict[str, Optional[Numeric]]
|
|
162
148
|
trace_id: uuid.UUID
|
|
163
149
|
executor_span_id: uuid.UUID
|
|
150
|
+
metadata: EvaluationDatapointMetadata = Field(default=None)
|
|
151
|
+
dataset_link: EvaluationDatapointDatasetLink | None = Field(default=None)
|
|
164
152
|
|
|
165
153
|
# uuid is not serializable by default, so we need to convert it to a string
|
|
166
|
-
def to_dict(self):
|
|
154
|
+
def to_dict(self, max_data_length: int = DEFAULT_DATAPOINT_MAX_DATA_LENGTH):
|
|
167
155
|
try:
|
|
156
|
+
serialized_data = serialize(self.data)
|
|
157
|
+
serialized_target = serialize(self.target)
|
|
158
|
+
serialized_executor_output = serialize(self.executor_output)
|
|
159
|
+
str_data = json.dumps(serialized_data)
|
|
160
|
+
str_target = json.dumps(serialized_target)
|
|
161
|
+
str_executor_output = json.dumps(serialized_executor_output)
|
|
168
162
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
163
|
+
# preserve only preview of the data, target and executor output
|
|
164
|
+
# (full data is in trace)
|
|
165
|
+
"id": str(self.id),
|
|
166
|
+
"data": (
|
|
167
|
+
str_data[:max_data_length]
|
|
168
|
+
if len(str_data) > max_data_length
|
|
169
|
+
else serialized_data
|
|
170
|
+
),
|
|
171
|
+
"target": (
|
|
172
|
+
str_target[:max_data_length]
|
|
173
|
+
if len(str_target) > max_data_length
|
|
174
|
+
else serialized_target
|
|
175
|
+
),
|
|
176
|
+
"executorOutput": (
|
|
177
|
+
str_executor_output[:max_data_length]
|
|
178
|
+
if len(str_executor_output) > max_data_length
|
|
179
|
+
else serialized_executor_output
|
|
180
|
+
),
|
|
172
181
|
"scores": self.scores,
|
|
173
182
|
"traceId": str(self.trace_id),
|
|
174
|
-
"humanEvaluators": [
|
|
175
|
-
(
|
|
176
|
-
v.model_dump()
|
|
177
|
-
if isinstance(v, pydantic.BaseModel)
|
|
178
|
-
else serialize(v)
|
|
179
|
-
)
|
|
180
|
-
for v in self.human_evaluators
|
|
181
|
-
],
|
|
182
183
|
"executorSpanId": str(self.executor_span_id),
|
|
184
|
+
"index": self.index,
|
|
185
|
+
"metadata": (
|
|
186
|
+
serialize(self.metadata) if self.metadata is not None else {}
|
|
187
|
+
),
|
|
188
|
+
"datasetLink": (
|
|
189
|
+
self.dataset_link.to_dict()
|
|
190
|
+
if self.dataset_link is not None
|
|
191
|
+
else None
|
|
192
|
+
),
|
|
183
193
|
}
|
|
184
194
|
except Exception as e:
|
|
185
195
|
raise ValueError(f"Error serializing EvaluationResultDatapoint: {e}")
|
|
@@ -191,21 +201,123 @@ class SpanType(Enum):
|
|
|
191
201
|
PIPELINE = "PIPELINE" # must not be set manually
|
|
192
202
|
EXECUTOR = "EXECUTOR"
|
|
193
203
|
EVALUATOR = "EVALUATOR"
|
|
204
|
+
HUMAN_EVALUATOR = "HUMAN_EVALUATOR"
|
|
194
205
|
EVALUATION = "EVALUATION"
|
|
195
206
|
|
|
196
207
|
|
|
197
208
|
class TraceType(Enum):
|
|
198
209
|
DEFAULT = "DEFAULT"
|
|
199
|
-
EVENT = "EVENT" # deprecated
|
|
200
210
|
EVALUATION = "EVALUATION"
|
|
201
211
|
|
|
202
212
|
|
|
203
|
-
class GetDatapointsResponse(
|
|
213
|
+
class GetDatapointsResponse(BaseModel):
|
|
204
214
|
items: list[Datapoint]
|
|
205
|
-
|
|
215
|
+
total_count: int = Field(alias="totalCount")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class LaminarSpanContext(BaseModel):
|
|
219
|
+
"""
|
|
220
|
+
A span context that can be used to continue a trace across services. This
|
|
221
|
+
is a slightly modified version of the OpenTelemetry span context. For
|
|
222
|
+
usage examples, see `Laminar.serialize_span_context`,
|
|
223
|
+
`Laminar.get_span_context`, and `Laminar.deserialize_laminar_span_context`.
|
|
224
|
+
|
|
225
|
+
The difference between this and the OpenTelemetry span context is that
|
|
226
|
+
the `trace_id` and `span_id` are stored as UUIDs instead of integers for
|
|
227
|
+
easier debugging, and the separate trace flags are not currently stored.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
trace_id: uuid.UUID
|
|
231
|
+
span_id: uuid.UUID
|
|
232
|
+
is_remote: bool = Field(default=False)
|
|
233
|
+
span_path: list[str] = Field(default=[])
|
|
234
|
+
span_ids_path: list[str] = Field(default=[]) # stringified UUIDs
|
|
235
|
+
user_id: str | None = Field(default=None)
|
|
236
|
+
session_id: str | None = Field(default=None)
|
|
237
|
+
trace_type: TraceType | None = Field(default=None)
|
|
238
|
+
metadata: dict[str, Any] | None = Field(default=None)
|
|
239
|
+
|
|
240
|
+
def __str__(self) -> str:
|
|
241
|
+
return self.model_dump_json()
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def try_to_otel_span_context(
|
|
245
|
+
cls,
|
|
246
|
+
span_context: "LaminarSpanContext" | dict[str, Any] | str | SpanContext,
|
|
247
|
+
logger: logging.Logger | None = None,
|
|
248
|
+
) -> SpanContext:
|
|
249
|
+
if logger is None:
|
|
250
|
+
logger = logging.getLogger(__name__)
|
|
251
|
+
|
|
252
|
+
if isinstance(span_context, LaminarSpanContext):
|
|
253
|
+
return SpanContext(
|
|
254
|
+
trace_id=span_context.trace_id.int,
|
|
255
|
+
span_id=span_context.span_id.int,
|
|
256
|
+
is_remote=span_context.is_remote,
|
|
257
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
258
|
+
)
|
|
259
|
+
elif isinstance(span_context, SpanContext) or (
|
|
260
|
+
isinstance(getattr(span_context, "trace_id", None), int)
|
|
261
|
+
and isinstance(getattr(span_context, "span_id", None), int)
|
|
262
|
+
):
|
|
263
|
+
logger.warning(
|
|
264
|
+
"span_context provided"
|
|
265
|
+
" is likely a raw OpenTelemetry span context. Will try to use it. "
|
|
266
|
+
"Please use `LaminarSpanContext` instead."
|
|
267
|
+
)
|
|
268
|
+
return span_context
|
|
269
|
+
elif isinstance(span_context, (dict, str)):
|
|
270
|
+
try:
|
|
271
|
+
laminar_span_context = cls.deserialize(span_context)
|
|
272
|
+
return SpanContext(
|
|
273
|
+
trace_id=laminar_span_context.trace_id.int,
|
|
274
|
+
span_id=laminar_span_context.span_id.int,
|
|
275
|
+
is_remote=laminar_span_context.is_remote,
|
|
276
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
277
|
+
)
|
|
278
|
+
except Exception:
|
|
279
|
+
raise ValueError("Invalid span_context provided")
|
|
280
|
+
else:
|
|
281
|
+
raise ValueError("Invalid span_context provided")
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def deserialize(cls, data: dict[str, Any] | str) -> "LaminarSpanContext":
|
|
285
|
+
if isinstance(data, dict):
|
|
286
|
+
# Convert camelCase to snake_case for known fields
|
|
287
|
+
converted_data = {
|
|
288
|
+
"trace_id": data.get("trace_id") or data.get("traceId"),
|
|
289
|
+
"span_id": data.get("span_id") or data.get("spanId"),
|
|
290
|
+
"is_remote": data.get("is_remote") or data.get("isRemote", False),
|
|
291
|
+
"span_path": data.get("span_path") or data.get("spanPath", []),
|
|
292
|
+
"span_ids_path": data.get("span_ids_path")
|
|
293
|
+
or data.get("spanIdsPath", []),
|
|
294
|
+
"user_id": data.get("user_id") or data.get("userId"),
|
|
295
|
+
"session_id": data.get("session_id") or data.get("sessionId"),
|
|
296
|
+
"trace_type": data.get("trace_type") or data.get("traceType"),
|
|
297
|
+
"metadata": data.get("metadata") or data.get("metadata", {}),
|
|
298
|
+
}
|
|
299
|
+
return cls.model_validate(converted_data)
|
|
300
|
+
elif isinstance(data, str):
|
|
301
|
+
return cls.deserialize(json.loads(data))
|
|
302
|
+
else:
|
|
303
|
+
raise ValueError("Invalid span_context provided")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class ModelProvider(str, Enum):
|
|
307
|
+
ANTHROPIC = "anthropic"
|
|
308
|
+
BEDROCK = "bedrock"
|
|
309
|
+
OPENAI = "openai"
|
|
310
|
+
GEMINI = "gemini"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class MaskInputOptions(TypedDict):
|
|
314
|
+
textarea: bool | None
|
|
315
|
+
text: bool | None
|
|
316
|
+
number: bool | None
|
|
317
|
+
select: bool | None
|
|
318
|
+
email: bool | None
|
|
319
|
+
tel: bool | None
|
|
206
320
|
|
|
207
321
|
|
|
208
|
-
class
|
|
209
|
-
|
|
210
|
-
META_ONLY = 1
|
|
211
|
-
ALL = 2
|
|
322
|
+
class SessionRecordingOptions(TypedDict):
|
|
323
|
+
mask_input_options: MaskInputOptions | None
|
lmnr/sdk/utils.py
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import datetime
|
|
3
2
|
import dataclasses
|
|
3
|
+
import dotenv
|
|
4
4
|
import enum
|
|
5
5
|
import inspect
|
|
6
|
+
import os
|
|
7
|
+
import orjson
|
|
6
8
|
import pydantic
|
|
7
9
|
import queue
|
|
8
10
|
import typing
|
|
9
11
|
import uuid
|
|
10
12
|
|
|
13
|
+
from lmnr.sdk.log import get_default_logger
|
|
14
|
+
|
|
15
|
+
logger = get_default_logger(__name__)
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
def is_method(func: typing.Callable) -> bool:
|
|
13
19
|
# inspect.ismethod is True for bound methods only, but in the decorator,
|
|
@@ -31,14 +37,13 @@ def is_async(func: typing.Callable) -> bool:
|
|
|
31
37
|
return False
|
|
32
38
|
|
|
33
39
|
# Check if the function is asynchronous
|
|
34
|
-
if
|
|
40
|
+
if inspect.iscoroutinefunction(func):
|
|
35
41
|
return True
|
|
36
42
|
|
|
37
43
|
# Fallback: check if the function's code object contains 'async'.
|
|
38
|
-
# This is for cases when a decorator did not properly use
|
|
44
|
+
# This is for cases when a decorator (not ours) did not properly use
|
|
39
45
|
# `functools.wraps` or `functools.update_wrapper`
|
|
40
|
-
|
|
41
|
-
return (func.__code__.co_flags & CO_COROUTINE) != 0
|
|
46
|
+
return (func.__code__.co_flags & inspect.CO_COROUTINE) != 0
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
def is_async_iterator(o: typing.Any) -> bool:
|
|
@@ -49,7 +54,7 @@ def is_iterator(o: typing.Any) -> bool:
|
|
|
49
54
|
return hasattr(o, "__iter__") and hasattr(o, "__next__")
|
|
50
55
|
|
|
51
56
|
|
|
52
|
-
def serialize(obj: typing.Any) -> dict[str, typing.Any]:
|
|
57
|
+
def serialize(obj: typing.Any) -> str | dict[str, typing.Any]:
|
|
53
58
|
def serialize_inner(o: typing.Any):
|
|
54
59
|
if isinstance(o, (datetime.datetime, datetime.date)):
|
|
55
60
|
return o.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
|
|
@@ -86,14 +91,153 @@ def get_input_from_func_args(
|
|
|
86
91
|
is_method: bool = False,
|
|
87
92
|
func_args: list[typing.Any] = [],
|
|
88
93
|
func_kwargs: dict[str, typing.Any] = {},
|
|
94
|
+
ignore_inputs: list[str] | None = None,
|
|
89
95
|
) -> dict[str, typing.Any]:
|
|
90
96
|
# Remove implicitly passed "self" or "cls" argument for
|
|
91
97
|
# instance or class methods
|
|
92
|
-
res =
|
|
98
|
+
res = {
|
|
99
|
+
k: v
|
|
100
|
+
for k, v in func_kwargs.items()
|
|
101
|
+
if not (ignore_inputs and k in ignore_inputs)
|
|
102
|
+
}
|
|
93
103
|
for i, k in enumerate(inspect.signature(func).parameters.keys()):
|
|
94
104
|
if is_method and k in ["self", "cls"]:
|
|
95
105
|
continue
|
|
106
|
+
if ignore_inputs and k in ignore_inputs:
|
|
107
|
+
continue
|
|
96
108
|
# If param has default value, then it's not present in func args
|
|
97
109
|
if i < len(func_args):
|
|
98
110
|
res[k] = func_args[i]
|
|
99
111
|
return res
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def from_env(key: str) -> str | None:
|
|
115
|
+
if val := os.getenv(key):
|
|
116
|
+
return val
|
|
117
|
+
dotenv_path = dotenv.find_dotenv(usecwd=True)
|
|
118
|
+
# use DotEnv directly so we can set verbose to False
|
|
119
|
+
return dotenv.main.DotEnv(dotenv_path, verbose=False, encoding="utf-8").get(key)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_otel_attribute_value_type(value: typing.Any) -> bool:
|
|
123
|
+
def is_primitive_type(value: typing.Any) -> bool:
|
|
124
|
+
return isinstance(value, (int, float, str, bool))
|
|
125
|
+
|
|
126
|
+
if is_primitive_type(value):
|
|
127
|
+
return True
|
|
128
|
+
elif isinstance(value, typing.Sequence):
|
|
129
|
+
if len(value) > 0:
|
|
130
|
+
return is_primitive_type(value[0]) and all(
|
|
131
|
+
isinstance(v, type(value[0])) for v in value
|
|
132
|
+
)
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_otel_env_var(var_name: str) -> str | None:
|
|
138
|
+
"""Get OTEL environment variable with priority order.
|
|
139
|
+
|
|
140
|
+
Checks in order:
|
|
141
|
+
1. OTEL_EXPORTER_OTLP_TRACES_{var_name}
|
|
142
|
+
2. OTEL_EXPORTER_OTLP_{var_name}
|
|
143
|
+
3. OTEL_{var_name}
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
var_name: The variable name (e.g., 'ENDPOINT', 'HEADERS', 'TIMEOUT')
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
str | None: The environment variable value or None if not found
|
|
150
|
+
"""
|
|
151
|
+
candidates = [
|
|
152
|
+
f"OTEL_EXPORTER_OTLP_TRACES_{var_name}",
|
|
153
|
+
f"OTEL_EXPORTER_OTLP_{var_name}",
|
|
154
|
+
f"OTEL_{var_name}",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
for candidate in candidates:
|
|
158
|
+
if value := from_env(candidate):
|
|
159
|
+
return value
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_otel_headers(headers_str: str | None) -> dict[str, str]:
|
|
164
|
+
"""Parse OTEL headers string into dictionary.
|
|
165
|
+
|
|
166
|
+
Format: key1=value1,key2=value2
|
|
167
|
+
Values are URL-decoded.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
headers_str: Headers string in OTEL format
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
dict[str, str]: Parsed headers dictionary
|
|
174
|
+
"""
|
|
175
|
+
if not headers_str:
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
headers = {}
|
|
179
|
+
for pair in headers_str.split(","):
|
|
180
|
+
if "=" in pair:
|
|
181
|
+
key, value = pair.split("=", 1)
|
|
182
|
+
import urllib.parse
|
|
183
|
+
|
|
184
|
+
headers[key.strip()] = urllib.parse.unquote(value.strip())
|
|
185
|
+
return headers
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_id(id_value: str | int | uuid.UUID) -> str:
|
|
189
|
+
"""Format trace/span/evaluation ID to a UUID string, or return valid UUID strings as-is.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
id_value: The ID in various formats (UUID, int, or valid UUID string)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: UUID string representation
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: If id_value cannot be converted to a valid UUID
|
|
199
|
+
"""
|
|
200
|
+
if isinstance(id_value, uuid.UUID):
|
|
201
|
+
return str(id_value)
|
|
202
|
+
elif isinstance(id_value, int):
|
|
203
|
+
return str(uuid.UUID(int=id_value))
|
|
204
|
+
elif isinstance(id_value, str):
|
|
205
|
+
uuid.UUID(id_value)
|
|
206
|
+
return id_value
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(f"Invalid ID type: {type(id_value)}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
DEFAULT_PLACEHOLDER = {}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def default_json(o):
|
|
215
|
+
if isinstance(o, pydantic.BaseModel):
|
|
216
|
+
return o.model_dump()
|
|
217
|
+
|
|
218
|
+
# Handle various sequence types, but not strings or bytes
|
|
219
|
+
if isinstance(o, (list, tuple, set, frozenset)):
|
|
220
|
+
return list(o)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return str(o)
|
|
224
|
+
except Exception:
|
|
225
|
+
logger.debug("Failed to serialize data to JSON, inner type: %s", type(o))
|
|
226
|
+
pass
|
|
227
|
+
return DEFAULT_PLACEHOLDER
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def json_dumps(data: dict) -> str:
|
|
231
|
+
try:
|
|
232
|
+
return orjson.dumps(
|
|
233
|
+
data,
|
|
234
|
+
default=default_json,
|
|
235
|
+
option=orjson.OPT_SERIALIZE_DATACLASS
|
|
236
|
+
| orjson.OPT_SERIALIZE_UUID
|
|
237
|
+
| orjson.OPT_UTC_Z
|
|
238
|
+
| orjson.OPT_NON_STR_KEYS,
|
|
239
|
+
).decode("utf-8")
|
|
240
|
+
except Exception:
|
|
241
|
+
# Log the exception and return a placeholder if serialization completely fails
|
|
242
|
+
logger.info("Failed to serialize data to JSON, type: %s", type(data))
|
|
243
|
+
return "{}" # Return an empty JSON object as a fallback
|