lmnr 0.6.11__py3-none-any.whl → 0.6.13__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 CHANGED
@@ -13,6 +13,7 @@ from .sdk.types import (
13
13
  )
14
14
  from .sdk.decorators import observe
15
15
  from .sdk.types import LaminarSpanContext
16
+ from .opentelemetry_lib.litellm import LaminarLiteLLMCallback
16
17
  from .opentelemetry_lib.tracing.attributes import Attributes
17
18
  from .opentelemetry_lib.tracing.instruments import Instruments
18
19
  from .opentelemetry_lib.tracing.processor import LaminarSpanProcessor
@@ -30,6 +31,7 @@ __all__ = [
30
31
  "Laminar",
31
32
  "LaminarClient",
32
33
  "LaminarDataset",
34
+ "LaminarLiteLLMCallback",
33
35
  "LaminarSpanContext",
34
36
  "LaminarSpanProcessor",
35
37
  "RunAgentResponseChunk",
@@ -7,7 +7,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME
7
7
  from lmnr.opentelemetry_lib.tracing.instruments import Instruments
8
8
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
9
9
 
10
- MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 # 1MB
10
+ MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 * 10 # 10MB
11
11
 
12
12
 
13
13
  class TracerManager:
@@ -0,0 +1,371 @@
1
+ """LiteLLM callback logger for Laminar"""
2
+
3
+ import json
4
+ from datetime import datetime
5
+
6
+ from opentelemetry.trace import SpanKind, Status, StatusCode, Tracer
7
+ from lmnr.opentelemetry_lib.litellm.utils import model_as_dict, set_span_attribute
8
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
9
+
10
+ from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
11
+ from lmnr.sdk.log import get_default_logger
12
+
13
+ logger = get_default_logger(__name__)
14
+
15
+ SUPPORTED_CALL_TYPES = ["completion", "acompletion"]
16
+
17
+ # Try to import the necessary LiteLLM components and gracefully handle ImportError
18
+ try:
19
+ if not is_package_installed("litellm"):
20
+ raise ImportError("LiteLLM is not installed")
21
+
22
+ from litellm.integrations.custom_batch_logger import CustomBatchLogger
23
+
24
+ class LaminarLiteLLMCallback(CustomBatchLogger):
25
+ """Custom LiteLLM logger that sends logs to Laminar via OpenTelemetry spans
26
+
27
+ Usage:
28
+ import litellm
29
+ from lmnr import Laminar, LaminarLiteLLMCallback
30
+
31
+ # make sure this comes first
32
+ Laminar.initialize()
33
+
34
+ # Add the logger to LiteLLM callbacks
35
+ litellm.callbacks = [LaminarLiteLLMCallback()]
36
+ """
37
+
38
+ def __init__(self, **kwargs):
39
+ super().__init__(**kwargs)
40
+ if not hasattr(TracerWrapper, "instance") or TracerWrapper.instance is None:
41
+ raise ValueError("Laminar must be initialized before LiteLLM callback")
42
+
43
+ def _get_tracer(self) -> Tracer:
44
+ if not hasattr(TracerWrapper, "instance") or TracerWrapper.instance is None:
45
+ raise ValueError("Laminar must be initialized before LiteLLM callback")
46
+ return TracerWrapper().get_tracer()
47
+
48
+ def log_success_event(
49
+ self, kwargs, response_obj, start_time: datetime, end_time: datetime
50
+ ):
51
+ if kwargs.get("call_type") not in SUPPORTED_CALL_TYPES:
52
+ return
53
+ try:
54
+ self._create_span(
55
+ kwargs, response_obj, start_time, end_time, is_success=True
56
+ )
57
+ except Exception as e:
58
+ logger.error(f"Error in log_success_event: {e}")
59
+
60
+ def log_failure_event(
61
+ self, kwargs, response_obj, start_time: datetime, end_time: datetime
62
+ ):
63
+ if kwargs.get("call_type") not in SUPPORTED_CALL_TYPES:
64
+ return
65
+ try:
66
+ self._create_span(
67
+ kwargs, response_obj, start_time, end_time, is_success=False
68
+ )
69
+ except Exception as e:
70
+ logger.error(f"Error in log_failure_event: {e}")
71
+
72
+ async def async_log_success_event(
73
+ self, kwargs, response_obj, start_time: datetime, end_time: datetime
74
+ ):
75
+ self.log_success_event(kwargs, response_obj, start_time, end_time)
76
+
77
+ async def async_log_failure_event(
78
+ self, kwargs, response_obj, start_time: datetime, end_time: datetime
79
+ ):
80
+ self.log_failure_event(kwargs, response_obj, start_time, end_time)
81
+
82
+ def _create_span(
83
+ self,
84
+ kwargs,
85
+ response_obj,
86
+ start_time: datetime,
87
+ end_time: datetime,
88
+ is_success: bool,
89
+ ):
90
+ """Create an OpenTelemetry span for the LiteLLM call"""
91
+ span_name = "litellm.completion"
92
+ try:
93
+ tracer = self._get_tracer()
94
+ except Exception as e:
95
+ logger.error(f"Error getting tracer: {e}")
96
+ return
97
+ span = tracer.start_span(
98
+ span_name,
99
+ kind=SpanKind.CLIENT,
100
+ start_time=int(start_time.timestamp() * 1e9),
101
+ attributes={
102
+ "lmnr.internal.provider": "litellm",
103
+ },
104
+ )
105
+ try:
106
+ model = kwargs.get("model", "unknown")
107
+ if kwargs.get("custom_llm_provider"):
108
+ set_span_attribute(
109
+ span, "gen_ai.system", kwargs["custom_llm_provider"]
110
+ )
111
+
112
+ messages = kwargs.get("messages", [])
113
+ self._process_input_messages(span, messages)
114
+
115
+ tools = kwargs.get("tools", [])
116
+ self._process_request_tool_definitions(span, tools)
117
+
118
+ set_span_attribute(span, "gen_ai.request.model", model)
119
+
120
+ # Add more attributes from kwargs
121
+ if "temperature" in kwargs:
122
+ set_span_attribute(
123
+ span, "gen_ai.request.temperature", kwargs["temperature"]
124
+ )
125
+ if "max_tokens" in kwargs:
126
+ set_span_attribute(
127
+ span, "gen_ai.request.max_tokens", kwargs["max_tokens"]
128
+ )
129
+ if "top_p" in kwargs:
130
+ set_span_attribute(span, "gen_ai.request.top_p", kwargs["top_p"])
131
+
132
+ if is_success:
133
+ span.set_status(Status(StatusCode.OK))
134
+ if kwargs.get("complete_streaming_response"):
135
+ self._process_success_response(
136
+ span,
137
+ kwargs.get("complete_streaming_response"),
138
+ )
139
+ else:
140
+ self._process_success_response(span, response_obj)
141
+ else:
142
+ span.set_status(Status(StatusCode.ERROR))
143
+ if isinstance(response_obj, Exception):
144
+ span.record_exception(response_obj)
145
+
146
+ except Exception as e:
147
+ span.record_exception(e)
148
+ logger.error(f"Error in Laminar LiteLLM instrumentation: {e}")
149
+ finally:
150
+ span.end(int(end_time.timestamp() * 1e9))
151
+
152
+ def _process_input_messages(self, span, messages):
153
+ """Process and set message attributes on the span"""
154
+ if not isinstance(messages, list):
155
+ return
156
+
157
+ for i, message in enumerate(messages):
158
+ message_dict = model_as_dict(message)
159
+ role = message_dict.get("role", "unknown")
160
+ set_span_attribute(span, f"gen_ai.prompt.{i}.role", role)
161
+
162
+ tool_calls = message_dict.get("tool_calls", [])
163
+ self._process_tool_calls(span, tool_calls, i, is_response=False)
164
+
165
+ content = message_dict.get("content", "")
166
+ if content is None:
167
+ continue
168
+ if isinstance(content, str):
169
+ set_span_attribute(span, f"gen_ai.prompt.{i}.content", content)
170
+ elif isinstance(content, list):
171
+ set_span_attribute(
172
+ span, f"gen_ai.prompt.{i}.content", json.dumps(content)
173
+ )
174
+ else:
175
+ set_span_attribute(
176
+ span,
177
+ f"gen_ai.prompt.{i}.content",
178
+ json.dumps(model_as_dict(content)),
179
+ )
180
+ if role == "tool":
181
+ set_span_attribute(
182
+ span,
183
+ f"gen_ai.prompt.{i}.tool_call_id",
184
+ message_dict.get("tool_call_id"),
185
+ )
186
+
187
+ def _process_request_tool_definitions(self, span, tools):
188
+ """Process and set tool definitions attributes on the span"""
189
+ if not isinstance(tools, list):
190
+ return
191
+
192
+ for i, tool in enumerate(tools):
193
+ tool_dict = model_as_dict(tool)
194
+ if tool_dict.get("type") != "function":
195
+ # TODO: parse other tool types
196
+ continue
197
+
198
+ function_dict = tool_dict.get("function", {})
199
+ function_name = function_dict.get("name", "")
200
+ function_description = function_dict.get("description", "")
201
+ function_parameters = function_dict.get("parameters", {})
202
+ set_span_attribute(
203
+ span,
204
+ f"llm.request.functions.{i}.name",
205
+ function_name,
206
+ )
207
+ set_span_attribute(
208
+ span,
209
+ f"llm.request.functions.{i}.description",
210
+ function_description,
211
+ )
212
+ set_span_attribute(
213
+ span,
214
+ f"llm.request.functions.{i}.parameters",
215
+ json.dumps(function_parameters),
216
+ )
217
+
218
+ def _process_response_usage(self, span, usage):
219
+ """Process and set usage attributes on the span"""
220
+ usage_dict = model_as_dict(usage)
221
+ if (
222
+ not usage_dict.get("prompt_tokens")
223
+ and not usage_dict.get("completion_tokens")
224
+ and not usage_dict.get("total_tokens")
225
+ ):
226
+ return
227
+
228
+ set_span_attribute(
229
+ span, "gen_ai.usage.input_tokens", usage_dict.get("prompt_tokens")
230
+ )
231
+ set_span_attribute(
232
+ span, "gen_ai.usage.output_tokens", usage_dict.get("completion_tokens")
233
+ )
234
+ set_span_attribute(
235
+ span, "llm.usage.total_tokens", usage_dict.get("total_tokens")
236
+ )
237
+
238
+ if usage_dict.get("prompt_tokens_details"):
239
+ details = usage_dict.get("prompt_tokens_details", {})
240
+ details = model_as_dict(details)
241
+ if details.get("cached_tokens"):
242
+ set_span_attribute(
243
+ span,
244
+ "gen_ai.usage.cache_read_input_tokens",
245
+ details.get("cached_tokens"),
246
+ )
247
+ # TODO: add audio/image/text token details
248
+ # TODO: add completion tokens details (reasoning tokens)
249
+
250
+ def _process_tool_calls(self, span, tool_calls, choice_index, is_response=True):
251
+ """Process and set tool call attributes on the span"""
252
+ attr_prefix = "completion" if is_response else "prompt"
253
+ if not isinstance(tool_calls, list):
254
+ return
255
+
256
+ for j, tool_call in enumerate(tool_calls):
257
+ tool_call_dict = model_as_dict(tool_call)
258
+
259
+ tool_name = tool_call_dict.get(
260
+ "name", tool_call_dict.get("function", {}).get("name", "")
261
+ )
262
+ set_span_attribute(
263
+ span,
264
+ f"gen_ai.{attr_prefix}.{choice_index}.tool_calls.{j}.name",
265
+ tool_name,
266
+ )
267
+
268
+ call_id = tool_call_dict.get("id", "")
269
+ set_span_attribute(
270
+ span,
271
+ f"gen_ai.{attr_prefix}.{choice_index}.tool_calls.{j}.id",
272
+ call_id,
273
+ )
274
+
275
+ tool_arguments = tool_call_dict.get(
276
+ "arguments", tool_call_dict.get("function", {}).get("arguments", "")
277
+ )
278
+ if isinstance(tool_arguments, str):
279
+ set_span_attribute(
280
+ span,
281
+ f"gen_ai.{attr_prefix}.{choice_index}.tool_calls.{j}.arguments",
282
+ tool_arguments,
283
+ )
284
+ else:
285
+ set_span_attribute(
286
+ span,
287
+ f"gen_ai.{attr_prefix}.{choice_index}.tool_calls.{j}.arguments",
288
+ json.dumps(model_as_dict(tool_arguments)),
289
+ )
290
+
291
+ def _process_response_choices(self, span, choices):
292
+ """Process and set choice attributes on the span"""
293
+ if not isinstance(choices, list):
294
+ return
295
+
296
+ for i, choice in enumerate(choices):
297
+ choice_dict = model_as_dict(choice)
298
+ message = choice_dict.get("message", choice_dict)
299
+
300
+ role = message.get("role", "unknown")
301
+ set_span_attribute(span, f"gen_ai.completion.{i}.role", role)
302
+
303
+ tool_calls = message.get("tool_calls", [])
304
+ self._process_tool_calls(span, tool_calls, i, is_response=True)
305
+
306
+ content = message.get("content", "")
307
+ if content is None:
308
+ continue
309
+ if isinstance(content, str):
310
+ set_span_attribute(span, f"gen_ai.completion.{i}.content", content)
311
+ elif isinstance(content, list):
312
+ set_span_attribute(
313
+ span, f"gen_ai.completion.{i}.content", json.dumps(content)
314
+ )
315
+ else:
316
+ set_span_attribute(
317
+ span,
318
+ f"gen_ai.completion.{i}.content",
319
+ json.dumps(model_as_dict(content)),
320
+ )
321
+
322
+ def _process_success_response(self, span, response_obj):
323
+ """Process successful response attributes"""
324
+ response_dict = model_as_dict(response_obj)
325
+ set_span_attribute(span, "gen_ai.response.id", response_dict.get("id"))
326
+ set_span_attribute(
327
+ span, "gen_ai.response.model", response_dict.get("model")
328
+ )
329
+
330
+ if response_dict.get("usage"):
331
+ self._process_response_usage(span, response_dict.get("usage"))
332
+
333
+ if response_dict.get("cache_creation_input_tokens"):
334
+ set_span_attribute(
335
+ span,
336
+ "gen_ai.usage.cache_creation_input_tokens",
337
+ response_dict.get("cache_creation_input_tokens"),
338
+ )
339
+ if response_dict.get("cache_read_input_tokens"):
340
+ set_span_attribute(
341
+ span,
342
+ "gen_ai.usage.cache_read_input_tokens",
343
+ response_dict.get("cache_read_input_tokens"),
344
+ )
345
+
346
+ if response_dict.get("choices"):
347
+ self._process_response_choices(span, response_dict.get("choices"))
348
+
349
+ except ImportError as e:
350
+ logger.debug(f"LiteLLM callback unavailable: {e}")
351
+
352
+ # Create a no-op logger when LiteLLM is not available
353
+ class LaminarLiteLLMCallback:
354
+ """No-op logger when LiteLLM is not available"""
355
+
356
+ def __init__(self, **kwargs):
357
+ logger.warning(
358
+ "LiteLLM is not installed. Install with: pip install litellm"
359
+ )
360
+
361
+ def log_success_event(self, *args, **kwargs):
362
+ pass
363
+
364
+ def log_failure_event(self, *args, **kwargs):
365
+ pass
366
+
367
+ async def async_log_success_event(self, *args, **kwargs):
368
+ pass
369
+
370
+ async def async_log_failure_event(self, *args, **kwargs):
371
+ pass
@@ -0,0 +1,18 @@
1
+ from pydantic import BaseModel
2
+ from opentelemetry.sdk.trace import Span
3
+ from opentelemetry.util.types import AttributeValue
4
+
5
+
6
+ def model_as_dict(model: BaseModel | dict) -> dict:
7
+ if isinstance(model, BaseModel) and hasattr(model, "model_dump"):
8
+ return model.model_dump()
9
+ elif isinstance(model, dict):
10
+ return model
11
+ else:
12
+ return dict(model)
13
+
14
+
15
+ def set_span_attribute(span: Span, key: str, value: AttributeValue | None):
16
+ if value is None or value == "":
17
+ return
18
+ span.set_attribute(key, value)
@@ -12,7 +12,6 @@ from .config import (
12
12
  Config,
13
13
  )
14
14
  from .utils import (
15
- ProcessedContentPart,
16
15
  dont_throw,
17
16
  get_content,
18
17
  role_from_content_union,
@@ -130,27 +129,30 @@ def _set_request_attributes(span, args, kwargs):
130
129
  )
131
130
 
132
131
  tools: list[types.FunctionDeclaration] = []
133
- if kwargs.get("tools"):
134
- for tool in kwargs.get("tools"):
132
+ arg_tools = config_dict.get("tools", kwargs.get("tools"))
133
+ if arg_tools:
134
+ for tool in arg_tools:
135
135
  if isinstance(tool, types.Tool):
136
136
  tools += tool.function_declarations or []
137
137
  elif isinstance(tool, Callable):
138
138
  tools.append(types.FunctionDeclaration.from_callable(tool))
139
+
139
140
  for tool_num, tool in enumerate(tools):
141
+ tool_dict = to_dict(tool)
140
142
  set_span_attribute(
141
143
  span,
142
144
  f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{tool_num}.name",
143
- to_dict(tool).get("name"),
145
+ tool_dict.get("name"),
144
146
  )
145
147
  set_span_attribute(
146
148
  span,
147
149
  f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{tool_num}.description",
148
- to_dict(tool).get("description"),
150
+ tool_dict.get("description"),
149
151
  )
150
152
  set_span_attribute(
151
153
  span,
152
154
  f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{tool_num}.parameters",
153
- to_dict(tool).get("parameters"),
155
+ json.dumps(tool_dict.get("parameters")),
154
156
  )
155
157
 
156
158
  if should_send_prompts():
@@ -162,7 +164,9 @@ def _set_request_attributes(span, args, kwargs):
162
164
  set_span_attribute(
163
165
  span,
164
166
  f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.content",
165
- (get_content(process_content_union(system_instruction)) or {}).get("text", ""),
167
+ (get_content(process_content_union(system_instruction)) or {}).get(
168
+ "text", ""
169
+ ),
166
170
  )
167
171
  set_span_attribute(
168
172
  span, f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.role", "system"
@@ -174,6 +178,7 @@ def _set_request_attributes(span, args, kwargs):
174
178
  for content in contents:
175
179
  processed_content = process_content_union(content)
176
180
  content_str = get_content(processed_content)
181
+
177
182
  set_span_attribute(
178
183
  span,
179
184
  f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.content",
@@ -188,26 +193,35 @@ def _set_request_attributes(span, args, kwargs):
188
193
  if isinstance(processed_content, list)
189
194
  else [processed_content]
190
195
  )
191
- for j, block in enumerate(blocks):
196
+ tool_call_index = 0
197
+ for block in blocks:
192
198
  block_dict = to_dict(block)
199
+
193
200
  if not block_dict.get("function_call"):
194
201
  continue
195
202
  function_call = to_dict(block_dict.get("function_call", {}))
203
+
196
204
  set_span_attribute(
197
205
  span,
198
- f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{j}.name",
206
+ f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{tool_call_index}.name",
199
207
  function_call.get("name"),
200
208
  )
201
209
  set_span_attribute(
202
210
  span,
203
- f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{j}.id",
204
- function_call.get("id"),
211
+ f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{tool_call_index}.id",
212
+ (
213
+ function_call.get("id")
214
+ if function_call.get("id") is not None
215
+ else function_call.get("name")
216
+ ), # google genai doesn't support tool call ids
205
217
  )
206
218
  set_span_attribute(
207
219
  span,
208
- f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{j}.arguments",
220
+ f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{tool_call_index}.arguments",
209
221
  json.dumps(function_call.get("arguments")),
210
222
  )
223
+ tool_call_index += 1
224
+
211
225
  set_span_attribute(
212
226
  span,
213
227
  f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.role",
@@ -258,21 +272,8 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
258
272
  candidates_list = candidates if isinstance(candidates, list) else [candidates]
259
273
  for i, candidate in enumerate(candidates_list):
260
274
  processed_content = process_content_union(candidate.content)
261
- if isinstance(processed_content, list):
262
- if all(
263
- isinstance(item, dict) and item.get("type") == "text"
264
- for item in processed_content
265
- ):
266
- content_str = processed_content[0]["text"]
267
- elif all(
268
- isinstance(item, ProcessedContentPart) and item.content
269
- for item in processed_content
270
- ):
271
- content_str = processed_content[0].content
272
- else:
273
- content_str = get_content(processed_content)
274
- else:
275
- content_str = get_content(processed_content)
275
+ content_str = get_content(processed_content)
276
+
276
277
  set_span_attribute(
277
278
  span, f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.role", "model"
278
279
  )
@@ -290,26 +291,33 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
290
291
  if isinstance(processed_content, list)
291
292
  else [processed_content]
292
293
  )
293
- for j, block in enumerate(blocks):
294
+
295
+ tool_call_index = 0
296
+ for block in blocks:
294
297
  block_dict = to_dict(block)
295
298
  if not block_dict.get("function_call"):
296
299
  continue
297
300
  function_call = to_dict(block_dict.get("function_call", {}))
298
301
  set_span_attribute(
299
302
  span,
300
- f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{j}.name",
303
+ f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{tool_call_index}.name",
301
304
  function_call.get("name"),
302
305
  )
303
306
  set_span_attribute(
304
307
  span,
305
- f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{j}.id",
306
- function_call.get("id"),
308
+ f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{tool_call_index}.id",
309
+ (
310
+ function_call.get("id")
311
+ if function_call.get("id") is not None
312
+ else function_call.get("name")
313
+ ), # google genai doesn't support tool call ids
307
314
  )
308
315
  set_span_attribute(
309
316
  span,
310
- f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{j}.arguments",
317
+ f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{tool_call_index}.arguments",
311
318
  json.dumps(function_call.get("arguments")),
312
319
  )
320
+ tool_call_index += 1
313
321
 
314
322
 
315
323
  @dont_throw
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import logging
2
3
  import traceback
3
4
 
@@ -74,7 +75,8 @@ def to_dict(obj: BaseModel | pydantic.BaseModel | dict) -> dict[str, Any]:
74
75
  return obj
75
76
  else:
76
77
  return dict(obj)
77
- except Exception:
78
+ except Exception as e:
79
+ logging.error(f"Error converting to dict: {obj}, error: {e}")
78
80
  return dict(obj)
79
81
 
80
82
 
@@ -96,7 +98,7 @@ def get_content(
96
98
  else:
97
99
  return None
98
100
  elif isinstance(content, list):
99
- return [get_content(item) or "" for item in content if item is not None]
101
+ return [get_content(item) for item in content]
100
102
  elif isinstance(content, str):
101
103
  return {
102
104
  "type": "text",
@@ -226,11 +228,15 @@ def _process_image_item(
226
228
  content_index: int,
227
229
  ) -> ProcessedContentPart | dict | None:
228
230
  # Convert to openai format, so backends can handle it
231
+ data = blob.get("data")
232
+ encoded_data = (
233
+ base64.b64encode(data).decode("utf-8") if isinstance(data, bytes) else data
234
+ )
229
235
  return (
230
236
  ProcessedContentPart(
231
237
  image_url=ImageUrl(
232
238
  image_url=ImageUrlInner(
233
- url=f"data:image/{blob.get('mime_type').split('/')[1]};base64,{blob.get('data')}",
239
+ url=f"data:image/{blob.get('mime_type').split('/')[1]};base64,{encoded_data}",
234
240
  )
235
241
  )
236
242
  )
lmnr/sdk/evaluations.py CHANGED
@@ -56,7 +56,11 @@ def get_average_scores(results: list[EvaluationResultDatapoint]) -> dict[str, Nu
56
56
 
57
57
  average_scores = {}
58
58
  for key, values in per_score_values.items():
59
- average_scores[key] = sum(values) / len(values)
59
+ scores = [v for v in values if v is not None]
60
+
61
+ # If there are no scores, we don't want to include the key in the average scores
62
+ if len(scores) > 0:
63
+ average_scores[key] = sum(scores) / len(scores)
60
64
 
61
65
  return average_scores
62
66
 
@@ -97,8 +101,7 @@ class Evaluation:
97
101
  self,
98
102
  data: EvaluationDataset | list[Datapoint | dict],
99
103
  executor: Any,
100
- evaluators: dict[str, EvaluatorFunction],
101
- human_evaluators: list[HumanEvaluator] = [],
104
+ evaluators: dict[str, EvaluatorFunction | HumanEvaluator],
102
105
  name: str | None = None,
103
106
  group_name: str | None = None,
104
107
  concurrency_limit: int = DEFAULT_BATCH_SIZE,
@@ -123,17 +126,15 @@ class Evaluation:
123
126
  executor (Callable[..., Any]): The executor function.\
124
127
  Takes the data point + any additional arguments and returns\
125
128
  the output to evaluate.
126
- evaluators (dict[str, Callable[..., Any]]): Evaluator functions and\
127
- names. Each evaluator function takes the output of the executor\
128
- _and_ the target data, and returns a score. The score can be a\
129
- single number or a dict of string keys and number values.\
130
- If the score is a single number, it will be named after the\
131
- evaluator function. Evaluator function names must contain only\
132
- letters, digits, hyphens, underscores, or spaces.
133
- human_evaluators (list[HumanEvaluator], optional):\
134
- [Beta] List of instances of HumanEvaluator. For now, human\
135
- evaluator only holds the queue name.
136
- Defaults to an empty list.
129
+ evaluators (dict[str, Callable[..., Any] | HumanEvaluator]): Evaluator\
130
+ functions and HumanEvaluator instances with names. Each evaluator\
131
+ function takes the output of the executor _and_ the target data,\
132
+ and returns a score. The score can be a single number or a dict\
133
+ of string keys and number values. If the score is a single number,\
134
+ it will be named after the evaluator function.\
135
+ HumanEvaluator instances create empty spans for manual evaluation.\
136
+ Evaluator names must contain only letters, digits, hyphens,\
137
+ underscores, or spaces.
137
138
  name (str | None, optional): Optional name of the evaluation.\
138
139
  Used to identify the evaluation in the group.\
139
140
  If not provided, a random name will be generated.
@@ -194,7 +195,6 @@ class Evaluation:
194
195
  self.concurrency_limit = concurrency_limit
195
196
  self.batch_size = concurrency_limit
196
197
  self._logger = get_default_logger(self.__class__.__name__)
197
- self.human_evaluators = human_evaluators
198
198
  self.upload_tasks = []
199
199
  self.base_http_url = f"{base_url}:{http_port or 443}"
200
200
 
@@ -345,24 +345,40 @@ class Evaluation:
345
345
  # Iterate over evaluators
346
346
  scores: dict[str, Numeric] = {}
347
347
  for evaluator_name, evaluator in self.evaluators.items():
348
- with L.start_as_current_span(
349
- evaluator_name, input={"output": output, "target": target}
350
- ) as evaluator_span:
351
- evaluator_span.set_attribute(SPAN_TYPE, SpanType.EVALUATOR.value)
352
- if is_async(evaluator):
353
- value = await evaluator(output, target)
354
- else:
355
- loop = asyncio.get_event_loop()
356
- value = await loop.run_in_executor(
357
- None, evaluator, output, target
358
- )
359
- L.set_span_output(value)
360
-
361
- # If evaluator returns a single number, use evaluator name as key
362
- if isinstance(value, NumericTypes):
363
- scores[evaluator_name] = value
348
+ # Check if evaluator is a HumanEvaluator instance
349
+ if isinstance(evaluator, HumanEvaluator):
350
+ # Create an empty span for human evaluators
351
+ with L.start_as_current_span(
352
+ evaluator_name,
353
+ input={"output": output, "target": target}
354
+ ) as human_evaluator_span:
355
+ human_evaluator_span.set_attribute(SPAN_TYPE, SpanType.HUMAN_EVALUATOR.value)
356
+ # Human evaluators don't execute automatically, just create the span
357
+ L.set_span_output(None)
358
+
359
+ # We don't want to save the score for human evaluators
360
+ scores[evaluator_name] = None
364
361
  else:
365
- scores.update(value)
362
+ # Regular evaluator function
363
+ with L.start_as_current_span(
364
+ evaluator_name,
365
+ input={"output": output, "target": target}
366
+ ) as evaluator_span:
367
+ evaluator_span.set_attribute(SPAN_TYPE, SpanType.EVALUATOR.value)
368
+ if is_async(evaluator):
369
+ value = await evaluator(output, target)
370
+ else:
371
+ loop = asyncio.get_event_loop()
372
+ value = await loop.run_in_executor(
373
+ None, evaluator, output, target
374
+ )
375
+ L.set_span_output(value)
376
+
377
+ # If evaluator returns a single number, use evaluator name as key
378
+ if isinstance(value, NumericTypes):
379
+ scores[evaluator_name] = value
380
+ else:
381
+ scores.update(value)
366
382
 
367
383
  trace_id = uuid.UUID(int=evaluation_span.get_span_context().trace_id)
368
384
 
@@ -373,10 +389,6 @@ class Evaluation:
373
389
  executor_output=output,
374
390
  scores=scores,
375
391
  trace_id=trace_id,
376
- # For now add all human evaluators to all result datapoints
377
- # In the future, we will add ways to specify which human evaluators
378
- # to add to which result datapoints, e.g. sample some randomly
379
- human_evaluators=self.human_evaluators,
380
392
  executor_span_id=executor_span_id,
381
393
  index=index,
382
394
  metadata=datapoint.metadata,
@@ -394,8 +406,7 @@ class Evaluation:
394
406
  def evaluate(
395
407
  data: EvaluationDataset | list[Datapoint | dict],
396
408
  executor: ExecutorFunction,
397
- evaluators: dict[str, EvaluatorFunction],
398
- human_evaluators: list[HumanEvaluator] = [],
409
+ evaluators: dict[str, EvaluatorFunction | HumanEvaluator],
399
410
  name: str | None = None,
400
411
  group_name: str | None = None,
401
412
  concurrency_limit: int = DEFAULT_BATCH_SIZE,
@@ -424,18 +435,15 @@ def evaluate(
424
435
  executor (Callable[..., Any]): The executor function.\
425
436
  Takes the data point + any additional arguments\
426
437
  and returns the output to evaluate.
427
- evaluators (List[Callable[..., Any]]):
428
- evaluators (dict[str, Callable[..., Any]]): Evaluator functions and\
429
- names. Each evaluator function takes the output of the executor\
430
- _and_ the target data, and returns a score. The score can be a\
431
- single number or a dict of string keys and number values.\
432
- If the score is a single number, it will be named after the\
433
- evaluator function. Evaluator function names must contain only\
434
- letters, digits, hyphens, underscores, or spaces.
435
- human_evaluators (list[HumanEvaluator], optional):\
436
- [Beta] List of instances of HumanEvaluator. For now, human\
437
- evaluator only holds the queue name.
438
- Defaults to an empty list.
438
+ evaluators (dict[str, Callable[..., Any] | HumanEvaluator]): Evaluator\
439
+ functions and HumanEvaluator instances with names. Each evaluator\
440
+ function takes the output of the executor _and_ the target data,\
441
+ and returns a score. The score can be a single number or a dict\
442
+ of string keys and number values. If the score is a single number,\
443
+ it will be named after the evaluator function.\
444
+ HumanEvaluator instances create empty spans for manual evaluation.\
445
+ Evaluator function names must contain only letters, digits, hyphens,\
446
+ underscores, or spaces.
439
447
  name (str | None, optional): Optional name of the evaluation.\
440
448
  Used to identify the evaluation in the group. If not provided, a\
441
449
  random name will be generated.
@@ -470,7 +478,6 @@ def evaluate(
470
478
  executor=executor,
471
479
  evaluators=evaluators,
472
480
  group_name=group_name,
473
- human_evaluators=human_evaluators,
474
481
  name=name,
475
482
  concurrency_limit=concurrency_limit,
476
483
  project_api_key=project_api_key,
lmnr/sdk/types.py CHANGED
@@ -9,7 +9,7 @@ import uuid
9
9
 
10
10
  from enum import Enum
11
11
  from opentelemetry.trace import SpanContext, TraceFlags
12
- from typing import Any, Awaitable, Callable, Literal
12
+ from typing import Any, Awaitable, Callable, Literal, Optional
13
13
 
14
14
  from .utils import serialize
15
15
 
@@ -50,7 +50,7 @@ EvaluatorFunction = Callable[
50
50
 
51
51
 
52
52
  class HumanEvaluator(pydantic.BaseModel):
53
- queueName: str
53
+ pass
54
54
 
55
55
 
56
56
  class InitEvaluationResponse(pydantic.BaseModel):
@@ -94,8 +94,7 @@ class EvaluationResultDatapoint(pydantic.BaseModel):
94
94
  data: EvaluationDatapointData
95
95
  target: EvaluationDatapointTarget
96
96
  executor_output: ExecutorFunctionReturnType
97
- scores: dict[str, Numeric]
98
- human_evaluators: list[HumanEvaluator] = pydantic.Field(default_factory=list)
97
+ scores: dict[str, Optional[Numeric]]
99
98
  trace_id: uuid.UUID
100
99
  executor_span_id: uuid.UUID
101
100
  metadata: EvaluationDatapointMetadata = pydantic.Field(default=None)
@@ -112,14 +111,6 @@ class EvaluationResultDatapoint(pydantic.BaseModel):
112
111
  "executorOutput": str(serialize(self.executor_output))[:100],
113
112
  "scores": self.scores,
114
113
  "traceId": str(self.trace_id),
115
- "humanEvaluators": [
116
- (
117
- v.model_dump()
118
- if isinstance(v, pydantic.BaseModel)
119
- else serialize(v)
120
- )
121
- for v in self.human_evaluators
122
- ],
123
114
  "executorSpanId": str(self.executor_span_id),
124
115
  "index": self.index,
125
116
  "metadata": (
@@ -136,6 +127,7 @@ class SpanType(Enum):
136
127
  PIPELINE = "PIPELINE" # must not be set manually
137
128
  EXECUTOR = "EXECUTOR"
138
129
  EVALUATOR = "EVALUATOR"
130
+ HUMAN_EVALUATOR = "HUMAN_EVALUATOR"
139
131
  EVALUATION = "EVALUATION"
140
132
 
141
133
 
lmnr/version.py CHANGED
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.6.11"
6
+ __version__ = "0.6.13"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lmnr
3
- Version: 0.6.11
3
+ Version: 0.6.13
4
4
  Summary: Python SDK for Laminar
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -46,61 +46,61 @@ Requires-Dist: httpx (>=0.25.0)
46
46
  Requires-Dist: opentelemetry-api (>=1.33.0)
47
47
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.33.0)
48
48
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.33.0)
49
- Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.40.8) ; extra == "alephalpha"
50
- Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.40.8) ; extra == "all"
51
- Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.40.8) ; extra == "all"
52
- Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.40.8) ; extra == "anthropic"
53
- Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.40.8) ; extra == "all"
54
- Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.40.8) ; extra == "bedrock"
55
- Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.40.8) ; extra == "all"
56
- Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.40.8) ; extra == "chromadb"
57
- Requires-Dist: opentelemetry-instrumentation-cohere (>=0.40.8) ; extra == "all"
58
- Requires-Dist: opentelemetry-instrumentation-cohere (>=0.40.8) ; extra == "cohere"
59
- Requires-Dist: opentelemetry-instrumentation-crewai (>=0.40.8) ; extra == "all"
60
- Requires-Dist: opentelemetry-instrumentation-crewai (>=0.40.8) ; extra == "crewai"
61
- Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.40.8) ; extra == "all"
62
- Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.40.8) ; extra == "google-generativeai"
63
- Requires-Dist: opentelemetry-instrumentation-groq (>=0.40.8) ; extra == "all"
64
- Requires-Dist: opentelemetry-instrumentation-groq (>=0.40.8) ; extra == "groq"
65
- Requires-Dist: opentelemetry-instrumentation-haystack (>=0.40.8) ; extra == "all"
66
- Requires-Dist: opentelemetry-instrumentation-haystack (>=0.40.8) ; extra == "haystack"
67
- Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.40.8) ; extra == "all"
68
- Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.40.8) ; extra == "lancedb"
69
- Requires-Dist: opentelemetry-instrumentation-langchain (>=0.40.8) ; extra == "all"
70
- Requires-Dist: opentelemetry-instrumentation-langchain (>=0.40.8) ; extra == "langchain"
71
- Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.40.8) ; extra == "all"
72
- Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.40.8) ; extra == "llamaindex"
73
- Requires-Dist: opentelemetry-instrumentation-marqo (>=0.40.8) ; extra == "all"
74
- Requires-Dist: opentelemetry-instrumentation-marqo (>=0.40.8) ; extra == "marqo"
75
- Requires-Dist: opentelemetry-instrumentation-mcp (>=0.40.8) ; extra == "all"
76
- Requires-Dist: opentelemetry-instrumentation-mcp (>=0.40.8) ; extra == "mcp"
77
- Requires-Dist: opentelemetry-instrumentation-milvus (>=0.40.8) ; extra == "all"
78
- Requires-Dist: opentelemetry-instrumentation-milvus (>=0.40.8) ; extra == "milvus"
79
- Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.40.8) ; extra == "all"
80
- Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.40.8) ; extra == "mistralai"
81
- Requires-Dist: opentelemetry-instrumentation-ollama (>=0.40.8) ; extra == "all"
82
- Requires-Dist: opentelemetry-instrumentation-ollama (>=0.40.8) ; extra == "ollama"
83
- Requires-Dist: opentelemetry-instrumentation-openai (>=0.40.8) ; extra == "all"
84
- Requires-Dist: opentelemetry-instrumentation-openai (>=0.40.8) ; extra == "openai"
85
- Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.40.8) ; extra == "all"
86
- Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.40.8) ; extra == "pinecone"
87
- Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.40.8) ; extra == "all"
88
- Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.40.8) ; extra == "qdrant"
89
- Requires-Dist: opentelemetry-instrumentation-replicate (>=0.40.8) ; extra == "all"
90
- Requires-Dist: opentelemetry-instrumentation-replicate (>=0.40.8) ; extra == "replicate"
91
- Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.40.8) ; extra == "all"
92
- Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.40.8) ; extra == "sagemaker"
49
+ Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.40.12) ; extra == "alephalpha"
50
+ Requires-Dist: opentelemetry-instrumentation-alephalpha (>=0.40.12) ; extra == "all"
51
+ Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.40.12) ; extra == "all"
52
+ Requires-Dist: opentelemetry-instrumentation-anthropic (>=0.40.12) ; extra == "anthropic"
53
+ Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.40.12) ; extra == "all"
54
+ Requires-Dist: opentelemetry-instrumentation-bedrock (>=0.40.12) ; extra == "bedrock"
55
+ Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.40.12) ; extra == "all"
56
+ Requires-Dist: opentelemetry-instrumentation-chromadb (>=0.40.12) ; extra == "chromadb"
57
+ Requires-Dist: opentelemetry-instrumentation-cohere (>=0.40.12) ; extra == "all"
58
+ Requires-Dist: opentelemetry-instrumentation-cohere (>=0.40.12) ; extra == "cohere"
59
+ Requires-Dist: opentelemetry-instrumentation-crewai (>=0.40.12) ; extra == "all"
60
+ Requires-Dist: opentelemetry-instrumentation-crewai (>=0.40.12) ; extra == "crewai"
61
+ Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.40.12) ; extra == "all"
62
+ Requires-Dist: opentelemetry-instrumentation-google-generativeai (>=0.40.12) ; extra == "google-generativeai"
63
+ Requires-Dist: opentelemetry-instrumentation-groq (>=0.40.12) ; extra == "all"
64
+ Requires-Dist: opentelemetry-instrumentation-groq (>=0.40.12) ; extra == "groq"
65
+ Requires-Dist: opentelemetry-instrumentation-haystack (>=0.40.12) ; extra == "all"
66
+ Requires-Dist: opentelemetry-instrumentation-haystack (>=0.40.12) ; extra == "haystack"
67
+ Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.40.12) ; extra == "all"
68
+ Requires-Dist: opentelemetry-instrumentation-lancedb (>=0.40.12) ; extra == "lancedb"
69
+ Requires-Dist: opentelemetry-instrumentation-langchain (>=0.40.12) ; extra == "all"
70
+ Requires-Dist: opentelemetry-instrumentation-langchain (>=0.40.12) ; extra == "langchain"
71
+ Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.40.12) ; extra == "all"
72
+ Requires-Dist: opentelemetry-instrumentation-llamaindex (>=0.40.12) ; extra == "llamaindex"
73
+ Requires-Dist: opentelemetry-instrumentation-marqo (>=0.40.12) ; extra == "all"
74
+ Requires-Dist: opentelemetry-instrumentation-marqo (>=0.40.12) ; extra == "marqo"
75
+ Requires-Dist: opentelemetry-instrumentation-mcp (>=0.40.12) ; extra == "all"
76
+ Requires-Dist: opentelemetry-instrumentation-mcp (>=0.40.12) ; extra == "mcp"
77
+ Requires-Dist: opentelemetry-instrumentation-milvus (>=0.40.12) ; extra == "all"
78
+ Requires-Dist: opentelemetry-instrumentation-milvus (>=0.40.12) ; extra == "milvus"
79
+ Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.40.12) ; extra == "all"
80
+ Requires-Dist: opentelemetry-instrumentation-mistralai (>=0.40.12) ; extra == "mistralai"
81
+ Requires-Dist: opentelemetry-instrumentation-ollama (>=0.40.12) ; extra == "all"
82
+ Requires-Dist: opentelemetry-instrumentation-ollama (>=0.40.12) ; extra == "ollama"
83
+ Requires-Dist: opentelemetry-instrumentation-openai (>=0.40.12) ; extra == "all"
84
+ Requires-Dist: opentelemetry-instrumentation-openai (>=0.40.12) ; extra == "openai"
85
+ Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.40.12) ; extra == "all"
86
+ Requires-Dist: opentelemetry-instrumentation-pinecone (>=0.40.12) ; extra == "pinecone"
87
+ Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.40.12) ; extra == "all"
88
+ Requires-Dist: opentelemetry-instrumentation-qdrant (>=0.40.12) ; extra == "qdrant"
89
+ Requires-Dist: opentelemetry-instrumentation-replicate (>=0.40.12) ; extra == "all"
90
+ Requires-Dist: opentelemetry-instrumentation-replicate (>=0.40.12) ; extra == "replicate"
91
+ Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.40.12) ; extra == "all"
92
+ Requires-Dist: opentelemetry-instrumentation-sagemaker (>=0.40.12) ; extra == "sagemaker"
93
93
  Requires-Dist: opentelemetry-instrumentation-threading (>=0.54b0)
94
- Requires-Dist: opentelemetry-instrumentation-together (>=0.40.8) ; extra == "all"
95
- Requires-Dist: opentelemetry-instrumentation-together (>=0.40.8) ; extra == "together"
96
- Requires-Dist: opentelemetry-instrumentation-transformers (>=0.40.8) ; extra == "all"
97
- Requires-Dist: opentelemetry-instrumentation-transformers (>=0.40.8) ; extra == "transformers"
98
- Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.40.8) ; extra == "all"
99
- Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.40.8) ; extra == "vertexai"
100
- Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.40.8) ; extra == "all"
101
- Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.40.8) ; extra == "watsonx"
102
- Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.40.8) ; extra == "all"
103
- Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.40.8) ; extra == "weaviate"
94
+ Requires-Dist: opentelemetry-instrumentation-together (>=0.40.12) ; extra == "all"
95
+ Requires-Dist: opentelemetry-instrumentation-together (>=0.40.12) ; extra == "together"
96
+ Requires-Dist: opentelemetry-instrumentation-transformers (>=0.40.12) ; extra == "all"
97
+ Requires-Dist: opentelemetry-instrumentation-transformers (>=0.40.12) ; extra == "transformers"
98
+ Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.40.12) ; extra == "all"
99
+ Requires-Dist: opentelemetry-instrumentation-vertexai (>=0.40.12) ; extra == "vertexai"
100
+ Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.40.12) ; extra == "all"
101
+ Requires-Dist: opentelemetry-instrumentation-watsonx (>=0.40.12) ; extra == "watsonx"
102
+ Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.40.12) ; extra == "all"
103
+ Requires-Dist: opentelemetry-instrumentation-weaviate (>=0.40.12) ; extra == "weaviate"
104
104
  Requires-Dist: opentelemetry-sdk (>=1.33.0)
105
105
  Requires-Dist: opentelemetry-semantic-conventions (>=0.54b0)
106
106
  Requires-Dist: opentelemetry-semantic-conventions-ai (>=0.4.8)
@@ -1,11 +1,13 @@
1
- lmnr/__init__.py,sha256=eJ-gIHEk8KV-BeaU8c9spQww_T2G5_OMu4F8JEzngvA,1281
1
+ lmnr/__init__.py,sha256=gRg8Pb-0XVzC-XmiLlHI9Br5yAJXwT6srPFxXIOaZ1o,1373
2
2
  lmnr/cli.py,sha256=uHgLUfN_6eINtUlcQdOtODf2tI9AiwmlhojQF4UMB5Y,6047
3
3
  lmnr/opentelemetry_lib/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
4
- lmnr/opentelemetry_lib/__init__.py,sha256=MHT91gFyPte8OHiy18rQuImy4Bee1DXFSR4CH6-B-Wc,2154
4
+ lmnr/opentelemetry_lib/__init__.py,sha256=aWKsqRXUhVhu2BS555nO2JhZSsK8bTUylAVwWybquGE,2160
5
5
  lmnr/opentelemetry_lib/decorators/__init__.py,sha256=45HVoYnHC1Y9D_VSkioDbqD3gm4RPC5sKoztomBI5j8,8496
6
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=6Fvkc_zZEX1lk8g6ZGFrADLNOL055pkMdO-hEef8qBY,18525
6
+ lmnr/opentelemetry_lib/litellm/__init__.py,sha256=wjo46It5GdhmxPCaiA8kaKyaz3VsuRDRYUCCstvK0-Y,14858
7
+ lmnr/opentelemetry_lib/litellm/utils.py,sha256=2ozwVT-C3HIDEJ8Rekx7QYXouvNMqtEteCOHVRUgGic,539
8
+ lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=mltiTDVCCyMuhQNuoLHvblg9O5X0ncG6xN3f1opSeQU,18613
7
9
  lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py,sha256=25zevJ7g3MtJP_5gju3jBH7-wg7SbDkktysuUO29ksI,245
8
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py,sha256=ICQENOiICTKodjZVHhq3H5RIRY5bbuWp_KmzkDNgDRM,7471
10
+ lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py,sha256=8SSBliRoJtiZME5RDEwt90CI2BadKPHQrtV4p6bDy_0,7669
9
11
  lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py,sha256=Jyv9koZdGA4-oTaB7ATB7DaX7aNOY-3YOGL4wX0c7PM,3107
10
12
  lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py,sha256=nf9sJZXnnts4gYZortEiDvwYjYqYJZTAT0zutuP_R6Y,1512
11
13
  lmnr/opentelemetry_lib/tracing/__init__.py,sha256=27QogAe-aHyrVr6b56-DduUm0KEE24K1aV2e1nouTNg,6007
@@ -45,14 +47,14 @@ lmnr/sdk/client/synchronous/sync_client.py,sha256=IIzj-mAwHHoRuUX9KkJtrzTGi5UOyg
45
47
  lmnr/sdk/datasets.py,sha256=P9hRxfl7-I6qhLFFGgU-r_I7RJfLtF6sL56g5fKIbAA,1708
46
48
  lmnr/sdk/decorators.py,sha256=1uu9xxBYgblFqlhQqH17cZYq7babAmB1lEtvBgTsP0E,4468
47
49
  lmnr/sdk/eval_control.py,sha256=KROUrDhcZTrptRZ-hxvr60_o_Gt_8u045jb4cBXcuoY,184
48
- lmnr/sdk/evaluations.py,sha256=i5c9wi9ZWV-L7vYbHEnLQC2V34n3tasPRowJAnSr-qQ,21022
50
+ lmnr/sdk/evaluations.py,sha256=fMUDueAgGv9fyTuX7n0DsS8lOzrbZNMgPorA037tgDU,21458
49
51
  lmnr/sdk/laminar.py,sha256=oOVco_c9ZstT71HsquGsgbtFumXd2Ejz0rl_qpmMlTU,33996
50
52
  lmnr/sdk/log.py,sha256=nt_YMmPw1IRbGy0b7q4rTtP4Yo3pQfNxqJPXK3nDSNQ,2213
51
- lmnr/sdk/types.py,sha256=5tEX7yoemb9wYyXLy4aqdazudO5I8dglU5A-IegDhsQ,12653
53
+ lmnr/sdk/types.py,sha256=ZQp5SeYJNZsK3KrbSeXPY_xn6mGjW5mSw_i0Rd_Oa4k,12328
52
54
  lmnr/sdk/utils.py,sha256=yrcHIhoADf9lWH9qJWZMmkRWYvd0DuxPSLP3mY6YFw0,4327
53
- lmnr/version.py,sha256=qnwKwsPBqcM4aZx6FBz8GRvhfVyvAhaXzQqQSfXM-k0,1322
54
- lmnr-0.6.11.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
55
- lmnr-0.6.11.dist-info/METADATA,sha256=DpYsYjFiUQII2I71j7YJv6F10rgBqGsHCfG_lLQQfhQ,15132
56
- lmnr-0.6.11.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
57
- lmnr-0.6.11.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
58
- lmnr-0.6.11.dist-info/RECORD,,
55
+ lmnr/version.py,sha256=9gMGnzWiCqbv_PzYX6c-jQFz17zO1xyIONO32wE9xfY,1322
56
+ lmnr-0.6.13.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
57
+ lmnr-0.6.13.dist-info/METADATA,sha256=95yHwJlTi4CNL1ucZDk9ge2flO712qjpxikJ0jCkmiI,15186
58
+ lmnr-0.6.13.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
59
+ lmnr-0.6.13.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
60
+ lmnr-0.6.13.dist-info/RECORD,,
File without changes
File without changes