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
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(name: str, level: int = logging.INFO, propagate: bool = False):
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
- console_log_handler.setFormatter(VerboseColorfulFormatter())
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 aiohttp
1
+ from __future__ import annotations # For "Self" | str | ... type hint
2
+
3
+ import json
4
+ import logging
2
5
  import datetime
3
- from enum import Enum
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 .utils import serialize
9
-
10
-
11
- class ChatMessage(pydantic.BaseModel):
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
- class ConditionedValue(pydantic.BaseModel):
17
- condition: str
18
- value: "NodeInput"
16
+ DEFAULT_DATAPOINT_MAX_DATA_LENGTH = 16_000_000 # 16MB
19
17
 
20
18
 
21
- Numeric = Union[int, float]
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 = Optional[Any] # must be JSON-serializable
111
- EvaluationDatapointMetadata = Optional[Any] # must be JSON-serializable
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(pydantic.BaseModel):
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 = pydantic.Field(default=None)
120
- metadata: EvaluationDatapointMetadata = pydantic.Field(default=None)
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 = Union[Numeric, dict[str, Numeric]]
49
+ EvaluatorFunctionReturnType = Numeric | dict[str, Numeric]
125
50
 
126
51
  ExecutorFunction = Callable[
127
52
  [EvaluationDatapointData, Any],
128
- Union[ExecutorFunctionReturnType, Awaitable[ExecutorFunctionReturnType]],
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
- Union[EvaluatorFunctionReturnType, Awaitable[EvaluatorFunctionReturnType]],
62
+ EvaluatorFunctionReturnType | Awaitable[EvaluatorFunctionReturnType],
138
63
  ]
139
64
 
140
65
 
141
- class HumanEvaluator(pydantic.BaseModel):
142
- queueName: str
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
- class CreateEvaluationResponse(pydantic.BaseModel):
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 EvaluationResultDatapoint(pydantic.BaseModel):
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
- "data": serialize(self.data),
170
- "target": serialize(self.target),
171
- "executorOutput": serialize(self.executor_output),
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(pydantic.BaseModel):
213
+ class GetDatapointsResponse(BaseModel):
204
214
  items: list[Datapoint]
205
- totalCount: int
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 TracingLevel(Enum):
209
- OFF = 0
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 asyncio.iscoroutinefunction(func):
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
- CO_COROUTINE = inspect.CO_COROUTINE
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 = func_kwargs.copy()
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