lmnr 0.4.12b3__py3-none-any.whl → 0.4.12b4__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/sdk/decorators.py CHANGED
@@ -6,6 +6,7 @@ from opentelemetry.trace import INVALID_SPAN, get_current_span
6
6
 
7
7
  from typing import Callable, Optional, cast
8
8
 
9
+ from lmnr.traceloop_sdk.tracing.attributes import SESSION_ID, USER_ID
9
10
  from lmnr.traceloop_sdk.tracing.tracing import update_association_properties
10
11
 
11
12
  from .utils import is_async
@@ -43,11 +44,11 @@ def observe(
43
44
  if current_span != INVALID_SPAN:
44
45
  if session_id is not None:
45
46
  current_span.set_attribute(
46
- "traceloop.association.properties.session_id", session_id
47
+ SESSION_ID, session_id
47
48
  )
48
49
  if user_id is not None:
49
50
  current_span.set_attribute(
50
- "traceloop.association.properties.user_id", user_id
51
+ USER_ID, user_id
51
52
  )
52
53
  association_properties = {}
53
54
  if session_id is not None:
lmnr/sdk/evaluations.py CHANGED
@@ -2,12 +2,26 @@ import asyncio
2
2
  import sys
3
3
  from abc import ABC, abstractmethod
4
4
  from contextlib import contextmanager
5
- from typing import Any, Awaitable, Optional, Union
5
+ from typing import Any, Awaitable, Optional, Set, Union
6
+ import uuid
6
7
 
7
8
  from tqdm import tqdm
8
9
 
10
+ from ..traceloop_sdk.instruments import Instruments
11
+ from ..traceloop_sdk.tracing.attributes import SPAN_TYPE
12
+
9
13
  from .laminar import Laminar as L
10
- from .types import CreateEvaluationResponse, Datapoint, EvaluationResultDatapoint, Numeric, NumericTypes
14
+ from .types import (
15
+ CreateEvaluationResponse,
16
+ Datapoint,
17
+ EvaluationResultDatapoint,
18
+ EvaluatorFunction,
19
+ ExecutorFunction,
20
+ Numeric,
21
+ NumericTypes,
22
+ SpanType,
23
+ TraceType,
24
+ )
11
25
  from .utils import is_async
12
26
 
13
27
  DEFAULT_BATCH_SIZE = 5
@@ -39,7 +53,11 @@ class EvaluationReporter:
39
53
  def start(self, name: str, project_id: str, id: str, length: int):
40
54
  print(f"Running evaluation {name}...\n")
41
55
  print(f"Check progress and results at {get_evaluation_url(project_id, id)}\n")
42
- self.cli_progress = tqdm(total=length, bar_format="{bar} {percentage:3.0f}% | ETA: {remaining}s | {n_fmt}/{total_fmt}", ncols=60)
56
+ self.cli_progress = tqdm(
57
+ total=length,
58
+ bar_format="{bar} {percentage:3.0f}% | ETA: {remaining}s | {n_fmt}/{total_fmt}",
59
+ ncols=60,
60
+ )
43
61
 
44
62
  def update(self, batch_length: int):
45
63
  self.cli_progress.update(batch_length)
@@ -51,7 +69,7 @@ class EvaluationReporter:
51
69
  def stop(self, average_scores: dict[str, Numeric]):
52
70
  self.cli_progress.close()
53
71
  print("\nAverage scores:")
54
- for (name, score) in average_scores.items():
72
+ for name, score in average_scores.items():
55
73
  print(f"{name}: {score}")
56
74
  print("\n")
57
75
 
@@ -78,12 +96,14 @@ class Evaluation:
78
96
  self,
79
97
  data: Union[EvaluationDataset, list[Union[Datapoint, dict]]],
80
98
  executor: Any,
81
- evaluators: list[Any],
99
+ evaluators: dict[str, EvaluatorFunction],
82
100
  name: Optional[str] = None,
83
101
  batch_size: int = DEFAULT_BATCH_SIZE,
84
102
  project_api_key: Optional[str] = None,
85
103
  base_url: Optional[str] = None,
86
104
  http_port: Optional[int] = None,
105
+ grpc_port: Optional[int] = None,
106
+ instruments: Optional[Set[Instruments]] = None,
87
107
  ):
88
108
  """
89
109
  Initializes an instance of the Evaluations class.
@@ -114,33 +134,18 @@ class Evaluation:
114
134
  Defaults to "https://api.lmnr.ai".
115
135
  http_port (Optional[int], optional): The port for the Laminar API HTTP service.
116
136
  Defaults to 443.
137
+ instruments (Optional[Set[Instruments]], optional): Set of modules to auto-instrument.
138
+ Defaults to None. If None, all available instruments will be used.
117
139
  """
118
140
 
119
141
  self.is_finished = False
120
142
  self.name = name
121
143
  self.reporter = EvaluationReporter()
122
144
  self.executor = executor
123
- self.evaluators = dict(
124
- zip(
125
- [
126
- (
127
- e.__name__
128
- if e.__name__ and e.__name__ != "<lambda>"
129
- else f"evaluator_{i+1}"
130
- )
131
- for i, e in enumerate(evaluators)
132
- ],
133
- evaluators,
134
- )
135
- )
136
- self.evaluator_names = list(self.evaluators.keys())
145
+ self.evaluators = evaluators
137
146
  if isinstance(data, list):
138
147
  self.data = [
139
- (
140
- Datapoint.model_validate(point)
141
- if isinstance(point, dict)
142
- else point
143
- )
148
+ (Datapoint.model_validate(point) if isinstance(point, dict) else point)
144
149
  for point in data
145
150
  ]
146
151
  else:
@@ -150,7 +155,8 @@ class Evaluation:
150
155
  project_api_key=project_api_key,
151
156
  base_url=base_url,
152
157
  http_port=http_port,
153
- instruments=set(),
158
+ grpc_port=grpc_port,
159
+ instruments=instruments,
154
160
  )
155
161
 
156
162
  def run(self) -> Union[None, Awaitable[None]]:
@@ -205,7 +211,7 @@ class Evaluation:
205
211
  async def evaluate_in_batches(self, evaluation: CreateEvaluationResponse):
206
212
  for i in range(0, len(self.data), self.batch_size):
207
213
  batch = (
208
- self.data[i: i + self.batch_size]
214
+ self.data[i : i + self.batch_size]
209
215
  if isinstance(self.data, list)
210
216
  else self.data.slice(i, i + self.batch_size)
211
217
  )
@@ -217,52 +223,72 @@ class Evaluation:
217
223
  finally:
218
224
  self.reporter.update(len(batch))
219
225
 
220
- async def _evaluate_batch(self, batch: list[Datapoint]) -> list[EvaluationResultDatapoint]:
226
+ async def _evaluate_batch(
227
+ self, batch: list[Datapoint]
228
+ ) -> list[EvaluationResultDatapoint]:
221
229
  batch_promises = [self._evaluate_datapoint(datapoint) for datapoint in batch]
222
230
  results = await asyncio.gather(*batch_promises)
223
231
  return results
224
232
 
225
- async def _evaluate_datapoint(self, datapoint) -> EvaluationResultDatapoint:
226
- output = (
227
- await self.executor(datapoint.data)
228
- if is_async(self.executor)
229
- else self.executor(datapoint.data)
230
- )
231
- target = datapoint.target
232
-
233
- # Iterate over evaluators
234
- scores: dict[str, Numeric] = {}
235
- for evaluator_name in self.evaluator_names:
236
- evaluator = self.evaluators[evaluator_name]
237
- value = (
238
- await evaluator(output, target)
239
- if is_async(evaluator)
240
- else evaluator(output, target)
233
+ async def _evaluate_datapoint(
234
+ self, datapoint: Datapoint
235
+ ) -> EvaluationResultDatapoint:
236
+ with L.start_as_current_span("evaluation") as evaluation_span:
237
+ L._set_trace_type(trace_type=TraceType.EVALUATION)
238
+ evaluation_span.set_attribute(SPAN_TYPE, SpanType.EVALUATION.value)
239
+ with L.start_as_current_span(
240
+ "executor", input={"data": datapoint.data}
241
+ ) as executor_span:
242
+ executor_span.set_attribute(SPAN_TYPE, SpanType.EXECUTOR.value)
243
+ output = (
244
+ await self.executor(datapoint.data)
245
+ if is_async(self.executor)
246
+ else self.executor(datapoint.data)
247
+ )
248
+ L.set_span_output(output)
249
+ target = datapoint.target
250
+
251
+ # Iterate over evaluators
252
+ scores: dict[str, Numeric] = {}
253
+ for evaluator_name, evaluator in self.evaluators.items():
254
+ with L.start_as_current_span(
255
+ "evaluator", input={"output": output, "target": target}
256
+ ) as evaluator_span:
257
+ evaluator_span.set_attribute(SPAN_TYPE, SpanType.EVALUATOR.value)
258
+ value = (
259
+ await evaluator(output, target)
260
+ if is_async(evaluator)
261
+ else evaluator(output, target)
262
+ )
263
+ L.set_span_output(value)
264
+
265
+ # If evaluator returns a single number, use evaluator name as key
266
+ if isinstance(value, NumericTypes):
267
+ scores[evaluator_name] = value
268
+ else:
269
+ scores.update(value)
270
+
271
+ trace_id = uuid.UUID(int=evaluation_span.get_span_context().trace_id)
272
+ return EvaluationResultDatapoint(
273
+ data=datapoint.data,
274
+ target=target,
275
+ executor_output=output,
276
+ scores=scores,
277
+ trace_id=trace_id,
241
278
  )
242
279
 
243
- # If evaluator returns a single number, use evaluator name as key
244
- if isinstance(value, NumericTypes):
245
- scores[evaluator_name] = value
246
- else:
247
- scores.update(value)
248
-
249
- return EvaluationResultDatapoint(
250
- data=datapoint.data,
251
- target=target,
252
- executorOutput=output,
253
- scores=scores,
254
- )
255
-
256
280
 
257
281
  def evaluate(
258
282
  data: Union[EvaluationDataset, list[Union[Datapoint, dict]]],
259
- executor: Any,
260
- evaluators: list[Any],
283
+ executor: ExecutorFunction,
284
+ evaluators: dict[str, EvaluatorFunction],
261
285
  name: Optional[str] = None,
262
286
  batch_size: int = DEFAULT_BATCH_SIZE,
263
287
  project_api_key: Optional[str] = None,
264
288
  base_url: Optional[str] = None,
265
289
  http_port: Optional[int] = None,
290
+ grpc_port: Optional[int] = None,
291
+ instruments: Optional[Set[Instruments]] = None,
266
292
  ) -> Optional[Awaitable[None]]:
267
293
  """
268
294
  If added to the file which is called through lmnr eval command, then simply registers the evaluation.
@@ -295,6 +321,10 @@ def evaluate(
295
321
  Defaults to "https://api.lmnr.ai".
296
322
  http_port (Optional[int], optional): The port for the Laminar API HTTP service.
297
323
  Defaults to 443.
324
+ grpc_port (Optional[int], optional): The port for the Laminar API gRPC service.
325
+ Defaults to 8443.
326
+ instruments (Optional[Set[Instruments]], optional): Set of modules to auto-instrument.
327
+ Defaults to None. If None, all available instruments will be used.
298
328
  """
299
329
 
300
330
  evaluation = Evaluation(
@@ -306,6 +336,8 @@ def evaluate(
306
336
  project_api_key=project_api_key,
307
337
  base_url=base_url,
308
338
  http_port=http_port,
339
+ grpc_port=grpc_port,
340
+ instruments=instruments,
309
341
  )
310
342
 
311
343
  global _evaluation
lmnr/sdk/laminar.py CHANGED
@@ -5,7 +5,6 @@ from opentelemetry.trace import (
5
5
  get_current_span,
6
6
  SpanKind,
7
7
  )
8
- from opentelemetry.semconv_ai import SpanAttributes
9
8
  from opentelemetry.util.types import AttributeValue
10
9
  from opentelemetry.context.context import Context
11
10
  from opentelemetry.util import types
@@ -26,7 +25,17 @@ import os
26
25
  import requests
27
26
  import uuid
28
27
 
29
- from lmnr.traceloop_sdk.tracing.tracing import set_association_properties, update_association_properties
28
+ from lmnr.traceloop_sdk.tracing.attributes import (
29
+ SESSION_ID,
30
+ SPAN_INPUT,
31
+ SPAN_OUTPUT,
32
+ TRACE_TYPE,
33
+ USER_ID,
34
+ )
35
+ from lmnr.traceloop_sdk.tracing.tracing import (
36
+ set_association_properties,
37
+ update_association_properties,
38
+ )
30
39
 
31
40
  from .log import VerboseColorfulFormatter
32
41
 
@@ -37,6 +46,7 @@ from .types import (
37
46
  PipelineRunResponse,
38
47
  NodeInput,
39
48
  PipelineRunRequest,
49
+ TraceType,
40
50
  UpdateEvaluationResponse,
41
51
  )
42
52
 
@@ -356,8 +366,8 @@ class Laminar:
356
366
  ) as span:
357
367
  if input is not None:
358
368
  span.set_attribute(
359
- SpanAttributes.TRACELOOP_ENTITY_INPUT,
360
- json.dumps({"input": input}),
369
+ SPAN_INPUT,
370
+ json.dumps(input),
361
371
  )
362
372
  yield span
363
373
 
@@ -371,9 +381,7 @@ class Laminar:
371
381
  """
372
382
  span = get_current_span()
373
383
  if output is not None and span != INVALID_SPAN:
374
- span.set_attribute(
375
- SpanAttributes.TRACELOOP_ENTITY_OUTPUT, json.dumps(output)
376
- )
384
+ span.set_attribute(SPAN_OUTPUT, json.dumps(output))
377
385
 
378
386
  @classmethod
379
387
  def set_session(
@@ -396,9 +404,23 @@ class Laminar:
396
404
  """
397
405
  association_properties = {}
398
406
  if session_id is not None:
399
- association_properties["session_id"] = session_id
407
+ association_properties[SESSION_ID] = session_id
400
408
  if user_id is not None:
401
- association_properties["user_id"] = user_id
409
+ association_properties[USER_ID] = user_id
410
+ update_association_properties(association_properties)
411
+
412
+ @classmethod
413
+ def _set_trace_type(
414
+ cls,
415
+ trace_type: TraceType,
416
+ ):
417
+ """Set the trace_type for the current span and the context
418
+ Args:
419
+ trace_type (TraceType): Type of the trace
420
+ """
421
+ association_properties = {
422
+ TRACE_TYPE: trace_type.value,
423
+ }
402
424
  update_association_properties(association_properties)
403
425
 
404
426
  @classmethod
@@ -430,7 +452,7 @@ class Laminar:
430
452
  ) -> requests.Response:
431
453
  body = {
432
454
  "evaluationId": str(evaluation_id),
433
- "points": [datapoint.model_dump() for datapoint in data],
455
+ "points": [datapoint.to_dict() for datapoint in data],
434
456
  }
435
457
  response = requests.post(
436
458
  cls.__base_http_url + "/v1/evaluation-datapoints",
lmnr/sdk/types.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import datetime
2
- import requests
2
+ from enum import Enum
3
3
  import pydantic
4
- import uuid
4
+ import requests
5
5
  from typing import Any, Awaitable, Callable, Literal, Optional, Union
6
+ import uuid
6
7
 
7
- from .utils import to_dict
8
+ from .utils import serialize
8
9
 
9
10
 
10
11
  class ChatMessage(pydantic.BaseModel):
@@ -37,7 +38,7 @@ class PipelineRunRequest(pydantic.BaseModel):
37
38
  def to_dict(self):
38
39
  return {
39
40
  "inputs": {
40
- k: v.model_dump() if isinstance(v, pydantic.BaseModel) else to_dict(v)
41
+ k: v.model_dump() if isinstance(v, pydantic.BaseModel) else serialize(v)
41
42
  for k, v in self.inputs.items()
42
43
  },
43
44
  "pipeline": self.pipeline,
@@ -125,5 +126,37 @@ UpdateEvaluationResponse = CreateEvaluationResponse
125
126
  class EvaluationResultDatapoint(pydantic.BaseModel):
126
127
  data: EvaluationDatapointData
127
128
  target: EvaluationDatapointTarget
128
- executorOutput: ExecutorFunctionReturnType
129
+ executor_output: ExecutorFunctionReturnType
129
130
  scores: dict[str, Numeric]
131
+ trace_id: uuid.UUID
132
+
133
+ # uuid is not serializable by default, so we need to convert it to a string
134
+ def to_dict(self):
135
+ return {
136
+ "data": {
137
+ k: v.model_dump() if isinstance(v, pydantic.BaseModel) else serialize(v)
138
+ for k, v in self.data.items()
139
+ },
140
+ "target": {
141
+ k: v.model_dump() if isinstance(v, pydantic.BaseModel) else serialize(v)
142
+ for k, v in self.target.items()
143
+ },
144
+ "executorOutput": serialize(self.executor_output),
145
+ "scores": self.scores,
146
+ "traceId": str(self.trace_id),
147
+ }
148
+
149
+
150
+ class SpanType(Enum):
151
+ DEFAULT = "DEFAULT"
152
+ LLM = "LLM"
153
+ PIPELINE = "PIPELINE" # must not be set manually
154
+ EXECUTOR = "EXECUTOR"
155
+ EVALUATOR = "EVALUATOR"
156
+ EVALUATION = "EVALUATION"
157
+
158
+
159
+ class TraceType(Enum):
160
+ DEFAULT = "DEFAULT"
161
+ EVENT = "EVENT" # must not be set manually
162
+ EVALUATION = "EVALUATION"
lmnr/sdk/utils.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import copy
3
2
  import datetime
4
3
  import dataclasses
5
4
  import enum
@@ -50,7 +49,7 @@ def is_iterator(o: typing.Any) -> bool:
50
49
  return hasattr(o, "__iter__") and hasattr(o, "__next__")
51
50
 
52
51
 
53
- def to_dict(obj: typing.Any) -> dict[str, typing.Any]:
52
+ def serialize(obj: typing.Any) -> dict[str, typing.Any]:
54
53
  def to_dict_inner(o: typing.Any):
55
54
  if isinstance(o, (datetime.datetime, datetime.date)):
56
55
  return o.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
@@ -59,7 +58,7 @@ def to_dict(obj: typing.Any) -> dict[str, typing.Any]:
59
58
  elif isinstance(o, (int, float, str, bool)):
60
59
  return o
61
60
  elif isinstance(o, uuid.UUID):
62
- return str(o) # same as in return, but explicit
61
+ return str(o) # same as in final return, but explicit
63
62
  elif isinstance(o, enum.Enum):
64
63
  return o.value
65
64
  elif dataclasses.is_dataclass(o):
@@ -90,11 +89,11 @@ def get_input_from_func_args(
90
89
  ) -> dict[str, typing.Any]:
91
90
  # Remove implicitly passed "self" or "cls" argument for
92
91
  # instance or class methods
93
- res = copy.deepcopy(func_kwargs)
92
+ res = func_kwargs.copy()
94
93
  for i, k in enumerate(inspect.signature(func).parameters.keys()):
95
94
  if is_method and k in ["self", "cls"]:
96
95
  continue
97
96
  # If param has default value, then it's not present in func args
98
- if len(func_args) > i:
97
+ if i < len(func_args):
99
98
  res[k] = func_args[i]
100
99
  return res
@@ -3,20 +3,16 @@ import sys
3
3
  from pathlib import Path
4
4
 
5
5
  from typing import Optional, Set
6
- from colorama import Fore
7
6
  from opentelemetry.sdk.trace import SpanProcessor
8
7
  from opentelemetry.sdk.trace.export import SpanExporter
9
- from opentelemetry.sdk.metrics.export import MetricExporter
10
8
  from opentelemetry.sdk.resources import SERVICE_NAME
11
9
  from opentelemetry.propagators.textmap import TextMapPropagator
12
10
  from opentelemetry.util.re import parse_env_headers
13
11
 
14
- from lmnr.traceloop_sdk.metrics.metrics import MetricsWrapper
15
12
  from lmnr.traceloop_sdk.instruments import Instruments
16
13
  from lmnr.traceloop_sdk.config import (
17
14
  is_content_tracing_enabled,
18
15
  is_tracing_enabled,
19
- is_metrics_enabled,
20
16
  )
21
17
  from lmnr.traceloop_sdk.tracing.tracing import TracerWrapper
22
18
  from typing import Dict
@@ -38,8 +34,6 @@ class Traceloop:
38
34
  headers: Dict[str, str] = {},
39
35
  disable_batch=False,
40
36
  exporter: Optional[SpanExporter] = None,
41
- metrics_exporter: Optional[MetricExporter] = None,
42
- metrics_headers: Optional[Dict[str, str]] = None,
43
37
  processor: Optional[SpanProcessor] = None,
44
38
  propagator: Optional[TextMapPropagator] = None,
45
39
  should_enrich_metrics: bool = True,
@@ -50,7 +44,7 @@ class Traceloop:
50
44
  api_key = os.getenv("TRACELOOP_API_KEY") or api_key
51
45
 
52
46
  if not is_tracing_enabled():
53
- print(Fore.YELLOW + "Tracing is disabled" + Fore.RESET)
47
+ # print(Fore.YELLOW + "Tracing is disabled" + Fore.RESET)
54
48
  return
55
49
 
56
50
  enable_content_tracing = is_content_tracing_enabled()
@@ -67,12 +61,10 @@ class Traceloop:
67
61
  and not api_key
68
62
  ):
69
63
  print(
70
- Fore.RED
71
- + "Error: Missing API key,"
64
+ "Error: Missing API key,"
72
65
  + " go to project settings to create one"
73
66
  )
74
67
  print("Set the LMNR_PROJECT_API_KEY environment variable to the key")
75
- print(Fore.RESET)
76
68
  return
77
69
 
78
70
  if api_key and not exporter and not processor and not headers:
@@ -80,7 +72,7 @@ class Traceloop:
80
72
  "Authorization": f"Bearer {api_key}",
81
73
  }
82
74
 
83
- print(Fore.RESET)
75
+ # print(Fore.RESET)
84
76
 
85
77
  # Tracer init
86
78
  resource_attributes.update({SERVICE_NAME: app_name})
@@ -95,21 +87,3 @@ class Traceloop:
95
87
  should_enrich_metrics=should_enrich_metrics,
96
88
  instruments=instruments,
97
89
  )
98
-
99
- if not metrics_exporter and exporter:
100
- return
101
-
102
- metrics_endpoint = os.getenv("TRACELOOP_METRICS_ENDPOINT") or api_endpoint
103
- metrics_headers = (
104
- os.getenv("TRACELOOP_METRICS_HEADERS") or metrics_headers or headers
105
- )
106
-
107
- if not is_metrics_enabled() or not metrics_exporter and exporter:
108
- print(Fore.YELLOW + "Metrics are disabled" + Fore.RESET)
109
- return
110
-
111
- MetricsWrapper.set_static_params(
112
- resource_attributes, metrics_endpoint, metrics_headers
113
- )
114
-
115
- Traceloop.__metrics_wrapper = MetricsWrapper(exporter=metrics_exporter)
@@ -7,7 +7,3 @@ def is_tracing_enabled() -> bool:
7
7
 
8
8
  def is_content_tracing_enabled() -> bool:
9
9
  return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true"
10
-
11
-
12
- def is_metrics_enabled() -> bool:
13
- return (os.getenv("TRACELOOP_METRICS_ENABLED") or "true").lower() == "true"
@@ -7,9 +7,10 @@ import warnings
7
7
 
8
8
  from opentelemetry import trace
9
9
  from opentelemetry import context as context_api
10
- from opentelemetry.semconv_ai import SpanAttributes
11
10
 
11
+ from lmnr.sdk.utils import get_input_from_func_args, is_method
12
12
  from lmnr.traceloop_sdk.tracing import get_tracer
13
+ from lmnr.traceloop_sdk.tracing.attributes import SPAN_INPUT, SPAN_OUTPUT
13
14
  from lmnr.traceloop_sdk.tracing.tracing import TracerWrapper
14
15
  from lmnr.traceloop_sdk.utils.json_encoder import JSONEncoder
15
16
 
@@ -52,8 +53,12 @@ def entity_method(
52
53
  try:
53
54
  if _should_send_prompts():
54
55
  span.set_attribute(
55
- SpanAttributes.TRACELOOP_ENTITY_INPUT,
56
- _json_dumps({"args": args, "kwargs": kwargs}),
56
+ SPAN_INPUT,
57
+ _json_dumps(
58
+ get_input_from_func_args(
59
+ fn, is_method(fn), args, kwargs
60
+ )
61
+ ),
57
62
  )
58
63
  except TypeError:
59
64
  pass
@@ -67,7 +72,7 @@ def entity_method(
67
72
  try:
68
73
  if _should_send_prompts():
69
74
  span.set_attribute(
70
- SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
75
+ SPAN_OUTPUT,
71
76
  _json_dumps(res),
72
77
  )
73
78
  except TypeError:
@@ -105,8 +110,12 @@ def aentity_method(
105
110
  try:
106
111
  if _should_send_prompts():
107
112
  span.set_attribute(
108
- SpanAttributes.TRACELOOP_ENTITY_INPUT,
109
- _json_dumps({"args": args, "kwargs": kwargs}),
113
+ SPAN_INPUT,
114
+ _json_dumps(
115
+ get_input_from_func_args(
116
+ fn, is_method(fn), args, kwargs
117
+ )
118
+ ),
110
119
  )
111
120
  except TypeError:
112
121
  pass
@@ -119,9 +128,7 @@ def aentity_method(
119
128
 
120
129
  try:
121
130
  if _should_send_prompts():
122
- span.set_attribute(
123
- SpanAttributes.TRACELOOP_ENTITY_OUTPUT, json.dumps(res)
124
- )
131
+ span.set_attribute(SPAN_OUTPUT, json.dumps(res))
125
132
  except TypeError:
126
133
  pass
127
134
 
@@ -0,0 +1,8 @@
1
+ SPAN_INPUT = "lmnr.span.input"
2
+ SPAN_OUTPUT = "lmnr.span.output"
3
+ SPAN_TYPE = "lmnr.span.type"
4
+
5
+ ASSOCIATION_PROPERTIES = "lmnr.association.properties"
6
+ SESSION_ID = "session_id"
7
+ USER_ID = "user_id"
8
+ TRACE_TYPE = "trace_type"
@@ -3,7 +3,6 @@ import logging
3
3
  import os
4
4
 
5
5
 
6
- from colorama import Fore
7
6
  from opentelemetry import trace
8
7
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
9
8
  OTLPSpanExporter as HTTPExporter,
@@ -24,17 +23,16 @@ from opentelemetry.trace import get_tracer_provider, ProxyTracerProvider
24
23
  from opentelemetry.context import get_value, attach, set_value
25
24
  from opentelemetry.instrumentation.threading import ThreadingInstrumentor
26
25
 
27
- from opentelemetry.semconv_ai import SpanAttributes
28
-
29
26
  # from lmnr.traceloop_sdk import Telemetry
30
27
  from lmnr.traceloop_sdk.instruments import Instruments
28
+ from lmnr.traceloop_sdk.tracing.attributes import ASSOCIATION_PROPERTIES
31
29
  from lmnr.traceloop_sdk.tracing.content_allow_list import ContentAllowList
32
30
  from lmnr.traceloop_sdk.utils import is_notebook
33
31
  from lmnr.traceloop_sdk.utils.package_check import is_package_installed
34
32
  from typing import Dict, Optional, Set
35
33
 
36
34
 
37
- TRACER_NAME = "traceloop.tracer"
35
+ TRACER_NAME = "lmnr.tracer"
38
36
  EXCLUDED_URLS = """
39
37
  iam.cloud.ibm.com,
40
38
  dataplatform.cloud.ibm.com,
@@ -44,7 +42,7 @@ EXCLUDED_URLS = """
44
42
  api.anthropic.com,
45
43
  api.cohere.ai,
46
44
  pinecone.io,
47
- traceloop.com,
45
+ api.lmnr.ai,
48
46
  posthog.com,
49
47
  sentry.io,
50
48
  bedrock-runtime,
@@ -130,138 +128,103 @@ class TracerWrapper(object):
130
128
  for instrument in instruments:
131
129
  if instrument == Instruments.OPENAI:
132
130
  if not init_openai_instrumentor(should_enrich_metrics):
133
- print(Fore.RED + "Warning: OpenAI library does not exist.")
134
- print(Fore.RESET)
131
+ print("Warning: OpenAI library does not exist.")
135
132
  elif instrument == Instruments.ANTHROPIC:
136
133
  if not init_anthropic_instrumentor(should_enrich_metrics):
137
134
  print(
138
- Fore.RED + "Warning: Anthropic library does not exist."
135
+ "Warning: Anthropic library does not exist."
139
136
  )
140
- print(Fore.RESET)
141
137
  elif instrument == Instruments.COHERE:
142
138
  if not init_cohere_instrumentor():
143
- print(Fore.RED + "Warning: Cohere library does not exist.")
144
- print(Fore.RESET)
139
+ print("Warning: Cohere library does not exist.")
145
140
  elif instrument == Instruments.PINECONE:
146
141
  if not init_pinecone_instrumentor():
147
142
  print(
148
- Fore.RED + "Warning: Pinecone library does not exist."
143
+ "Warning: Pinecone library does not exist."
149
144
  )
150
- print(Fore.RESET)
151
145
  elif instrument == Instruments.CHROMA:
152
146
  if not init_chroma_instrumentor():
153
- print(Fore.RED + "Warning: Chroma library does not exist.")
154
- print(Fore.RESET)
147
+ print("Warning: Chroma library does not exist.")
155
148
  elif instrument == Instruments.GOOGLE_GENERATIVEAI:
156
149
  if not init_google_generativeai_instrumentor():
157
- print(
158
- Fore.RED
159
- + "Warning: Google Generative AI library does not exist."
160
- )
161
- print(Fore.RESET)
150
+ print("Warning: Google Generative AI library does not exist.")
162
151
  elif instrument == Instruments.LANGCHAIN:
163
152
  if not init_langchain_instrumentor():
164
153
  print(
165
- Fore.RED + "Warning: LangChain library does not exist."
154
+ "Warning: LangChain library does not exist."
166
155
  )
167
- print(Fore.RESET)
168
156
  elif instrument == Instruments.MISTRAL:
169
157
  if not init_mistralai_instrumentor():
170
158
  print(
171
- Fore.RED + "Warning: MistralAI library does not exist."
159
+ "Warning: MistralAI library does not exist."
172
160
  )
173
- print(Fore.RESET)
174
161
  elif instrument == Instruments.OLLAMA:
175
162
  if not init_ollama_instrumentor():
176
- print(Fore.RED + "Warning: Ollama library does not exist.")
177
- print(Fore.RESET)
163
+ print("Warning: Ollama library does not exist.")
178
164
  elif instrument == Instruments.LLAMA_INDEX:
179
165
  if not init_llama_index_instrumentor():
180
166
  print(
181
- Fore.RED + "Warning: LlamaIndex library does not exist."
167
+ "Warning: LlamaIndex library does not exist."
182
168
  )
183
- print(Fore.RESET)
184
169
  elif instrument == Instruments.MILVUS:
185
170
  if not init_milvus_instrumentor():
186
- print(Fore.RED + "Warning: Milvus library does not exist.")
187
- print(Fore.RESET)
171
+ print("Warning: Milvus library does not exist.")
188
172
  elif instrument == Instruments.TRANSFORMERS:
189
173
  if not init_transformers_instrumentor():
190
- print(
191
- Fore.RED
192
- + "Warning: Transformers library does not exist."
193
- )
194
- print(Fore.RESET)
174
+ print("Warning: Transformers library does not exist.")
195
175
  elif instrument == Instruments.TOGETHER:
196
176
  if not init_together_instrumentor():
197
177
  print(
198
- Fore.RED + "Warning: TogetherAI library does not exist."
178
+ "Warning: TogetherAI library does not exist."
199
179
  )
200
- print(Fore.RESET)
201
180
  elif instrument == Instruments.REQUESTS:
202
181
  if not init_requests_instrumentor():
203
182
  print(
204
- Fore.RED + "Warning: Requests library does not exist."
183
+ "Warning: Requests library does not exist."
205
184
  )
206
- print(Fore.RESET)
207
185
  elif instrument == Instruments.URLLIB3:
208
186
  if not init_urllib3_instrumentor():
209
- print(Fore.RED + "Warning: urllib3 library does not exist.")
210
- print(Fore.RESET)
187
+ print("Warning: urllib3 library does not exist.")
211
188
  elif instrument == Instruments.PYMYSQL:
212
189
  if not init_pymysql_instrumentor():
213
- print(Fore.RED + "Warning: PyMySQL library does not exist.")
214
- print(Fore.RESET)
190
+ print("Warning: PyMySQL library does not exist.")
215
191
  elif instrument == Instruments.BEDROCK:
216
192
  if not init_bedrock_instrumentor(should_enrich_metrics):
217
- print(Fore.RED + "Warning: Bedrock library does not exist.")
218
- print(Fore.RESET)
193
+ print("Warning: Bedrock library does not exist.")
219
194
  elif instrument == Instruments.REPLICATE:
220
195
  if not init_replicate_instrumentor():
221
196
  print(
222
- Fore.RED + "Warning: Replicate library does not exist."
197
+ "Warning: Replicate library does not exist."
223
198
  )
224
- print(Fore.RESET)
225
199
  elif instrument == Instruments.VERTEXAI:
226
200
  if not init_vertexai_instrumentor():
227
201
  print(
228
- Fore.RED + "Warning: Vertex AI library does not exist."
202
+ "Warning: Vertex AI library does not exist."
229
203
  )
230
- print(Fore.RESET)
231
204
  elif instrument == Instruments.WATSONX:
232
205
  if not init_watsonx_instrumentor():
233
- print(Fore.RED + "Warning: Watsonx library does not exist.")
234
- print(Fore.RESET)
206
+ print("Warning: Watsonx library does not exist.")
235
207
  elif instrument == Instruments.WEAVIATE:
236
208
  if not init_weaviate_instrumentor():
237
209
  print(
238
- Fore.RED + "Warning: Weaviate library does not exist."
210
+ "Warning: Weaviate library does not exist."
239
211
  )
240
- print(Fore.RESET)
241
212
  elif instrument == Instruments.ALEPHALPHA:
242
213
  if not init_alephalpha_instrumentor():
243
- print(
244
- Fore.RED
245
- + "Warning: Aleph Alpha library does not exist."
246
- )
247
- print(Fore.RESET)
214
+ print("Warning: Aleph Alpha library does not exist.")
248
215
  elif instrument == Instruments.MARQO:
249
216
  if not init_marqo_instrumentor():
250
- print(Fore.RED + "Warning: marqo library does not exist.")
251
- print(Fore.RESET)
217
+ print("Warning: marqo library does not exist.")
252
218
  elif instrument == Instruments.LANCEDB:
253
219
  if not init_lancedb_instrumentor():
254
- print(Fore.RED + "Warning: LanceDB library does not exist.")
255
- print(Fore.RESET)
220
+ print("Warning: LanceDB library does not exist.")
256
221
  elif instrument == Instruments.REDIS:
257
222
  if not init_redis_instrumentor():
258
- print(Fore.RED + "Warning: redis library does not exist.")
259
- print(Fore.RESET)
223
+ print("Warning: redis library does not exist.")
260
224
 
261
225
  else:
262
226
  print(
263
- Fore.RED
264
- + "Warning: "
227
+ "Warning: "
265
228
  + instrument
266
229
  + " instrumentation does not exist."
267
230
  )
@@ -270,7 +233,6 @@ class TracerWrapper(object):
270
233
  + "from lmnr.traceloop_sdk.instruments import Instruments\n"
271
234
  + 'Traceloop.init(app_name="...", instruments=set([Instruments.OPENAI]))'
272
235
  )
273
- print(Fore.RESET)
274
236
 
275
237
  obj.__content_allow_list = ContentAllowList()
276
238
 
@@ -293,49 +255,6 @@ class TracerWrapper(object):
293
255
  else:
294
256
  attach(set_value("override_enable_content_tracing", False))
295
257
 
296
- if is_llm_span(span):
297
- managed_prompt = get_value("managed_prompt")
298
- if managed_prompt is not None:
299
- span.set_attribute(
300
- SpanAttributes.TRACELOOP_PROMPT_MANAGED, managed_prompt
301
- )
302
-
303
- prompt_key = get_value("prompt_key")
304
- if prompt_key is not None:
305
- span.set_attribute(SpanAttributes.TRACELOOP_PROMPT_KEY, prompt_key)
306
-
307
- prompt_version = get_value("prompt_version")
308
- if prompt_version is not None:
309
- span.set_attribute(
310
- SpanAttributes.TRACELOOP_PROMPT_VERSION, prompt_version
311
- )
312
-
313
- prompt_version_name = get_value("prompt_version_name")
314
- if prompt_version_name is not None:
315
- span.set_attribute(
316
- SpanAttributes.TRACELOOP_PROMPT_VERSION_NAME, prompt_version_name
317
- )
318
-
319
- prompt_version_hash = get_value("prompt_version_hash")
320
- if prompt_version_hash is not None:
321
- span.set_attribute(
322
- SpanAttributes.TRACELOOP_PROMPT_VERSION_HASH, prompt_version_hash
323
- )
324
-
325
- prompt_template = get_value("prompt_template")
326
- if prompt_template is not None:
327
- span.set_attribute(
328
- SpanAttributes.TRACELOOP_PROMPT_TEMPLATE, prompt_template
329
- )
330
-
331
- prompt_template_variables = get_value("prompt_template_variables")
332
- if prompt_template_variables is not None:
333
- for key, value in prompt_template_variables.items():
334
- span.set_attribute(
335
- f"{SpanAttributes.TRACELOOP_PROMPT_TEMPLATE_VARIABLES}.{key}",
336
- value,
337
- )
338
-
339
258
  # Call original on_start method if it exists in custom processor
340
259
  if self.__spans_processor_original_on_start:
341
260
  self.__spans_processor_original_on_start(span, parent_context)
@@ -360,11 +279,7 @@ class TracerWrapper(object):
360
279
  if (os.getenv("TRACELOOP_SUPPRESS_WARNINGS") or "false").lower() == "true":
361
280
  return False
362
281
 
363
- print(
364
- Fore.RED
365
- + "Warning: Traceloop not initialized, make sure you call Traceloop.init()"
366
- )
367
- print(Fore.RESET)
282
+ print("Warning: Laminar not initialized, make sure to initialize")
368
283
  return False
369
284
 
370
285
  def flush(self):
@@ -399,7 +314,7 @@ def update_association_properties(properties: dict) -> None:
399
314
  def _set_association_properties_attributes(span, properties: dict) -> None:
400
315
  for key, value in properties.items():
401
316
  span.set_attribute(
402
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.{key}", value
317
+ f"{ASSOCIATION_PROPERTIES}.{key}", value
403
318
  )
404
319
 
405
320
 
@@ -427,10 +342,6 @@ def set_external_prompt_tracing_context(
427
342
  attach(set_value("prompt_template_variables", variables))
428
343
 
429
344
 
430
- def is_llm_span(span) -> bool:
431
- return span.attributes.get(SpanAttributes.LLM_REQUEST_TYPE) is not None
432
-
433
-
434
345
  def init_spans_exporter(api_endpoint: str, headers: Dict[str, str]) -> SpanExporter:
435
346
  if "http" in api_endpoint.lower() or "https" in api_endpoint.lower():
436
347
  return HTTPExporter(endpoint=f"{api_endpoint}/v1/traces", headers=headers)
@@ -499,7 +410,6 @@ def init_openai_instrumentor(should_enrich_metrics: bool):
499
410
  # exception_logger=lambda e: Telemetry().log_exception(e),
500
411
  enrich_assistant=should_enrich_metrics,
501
412
  enrich_token_usage=should_enrich_metrics,
502
- get_common_metrics_attributes=metrics_common_attributes,
503
413
  )
504
414
  if not instrumentor.is_instrumented_by_opentelemetry:
505
415
  instrumentor.instrument()
@@ -520,7 +430,6 @@ def init_anthropic_instrumentor(should_enrich_metrics: bool):
520
430
  instrumentor = AnthropicInstrumentor(
521
431
  # exception_logger=lambda e: Telemetry().log_exception(e),
522
432
  enrich_token_usage=should_enrich_metrics,
523
- get_common_metrics_attributes=metrics_common_attributes,
524
433
  )
525
434
  if not instrumentor.is_instrumented_by_opentelemetry:
526
435
  instrumentor.instrument()
@@ -988,23 +897,3 @@ def init_groq_instrumentor():
988
897
  logging.error(f"Error initializing Groq instrumentor: {e}")
989
898
  # Telemetry().log_exception(e)
990
899
  return False
991
-
992
-
993
- def metrics_common_attributes():
994
- common_attributes = {}
995
- workflow_name = get_value("workflow_name")
996
- if workflow_name is not None:
997
- common_attributes[SpanAttributes.TRACELOOP_WORKFLOW_NAME] = workflow_name
998
-
999
- entity_name = get_value("entity_name")
1000
- if entity_name is not None:
1001
- common_attributes[SpanAttributes.TRACELOOP_ENTITY_NAME] = entity_name
1002
-
1003
- association_properties = get_value("association_properties")
1004
- if association_properties is not None:
1005
- for key, value in association_properties.items():
1006
- common_attributes[
1007
- f"{SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.{key}"
1008
- ] = value
1009
-
1010
- return common_attributes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.4.12b3
3
+ Version: 0.4.12b4
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: argparse (>=1.0,<2.0)
15
15
  Requires-Dist: asyncio (>=3.0,<4.0)
16
16
  Requires-Dist: backoff (>=2.0,<3.0)
17
- Requires-Dist: colorama (>=0.4,<0.5)
18
17
  Requires-Dist: deprecated (>=1.0,<2.0)
19
18
  Requires-Dist: jinja2 (>=3.0,<4.0)
20
19
  Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0)
@@ -197,7 +196,7 @@ L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"], instruments={In
197
196
 
198
197
  If you want to fully disable any kind of autoinstrumentation, pass an empty set as `instruments=set()` to `.initialize()`.
199
198
 
200
- Majority of the autoinstrumentations are provided by Traceloop's [OpenLLMetry](https://github.com/traceloop/openllmetry).
199
+ Autoinstrumentations are provided by Traceloop's [OpenLLMetry](https://github.com/traceloop/openllmetry).
201
200
 
202
201
  ## Sending events
203
202
 
@@ -267,13 +266,14 @@ Evaluation takes in the following parameters:
267
266
  - `name` – the name of your evaluation. If no such evaluation exists in the project, it will be created. Otherwise, data will be pushed to the existing evaluation
268
267
  - `data` – an array of `EvaluationDatapoint` objects, where each `EvaluationDatapoint` has two keys: `target` and `data`, each containing a key-value object. Alternatively, you can pass in dictionaries, and we will instantiate `EvaluationDatapoint`s with pydantic if possible
269
268
  - `executor` – the logic you want to evaluate. This function must take `data` as the first argument, and produce any output. *
270
- - `evaluators` – evaluaton logic. List of functions that take output of executor as the first argument, `target` as the second argument and produce a numeric scores. Each function can produce either a single number or `dict[str, int|float]` of scores.
269
+ - `evaluators` – evaluaton logic. Functions that take output of executor as the first argument, `target` as the second argument and produce a numeric scores. Pass a dict from evaluator name to a function. Each function can produce either a single number or `dict[str, int|float]` of scores.
271
270
 
272
271
  \* If you already have the outputs of executors you want to evaluate, you can specify the executor as an identity function, that takes in `data` and returns only needed value(s) from it.
273
272
 
274
- ### Example
273
+ ### Example code
275
274
 
276
275
  ```python
276
+ from lmnr import evaluate
277
277
  from openai import AsyncOpenAI
278
278
  import asyncio
279
279
  import os
@@ -304,20 +304,25 @@ data = [
304
304
  ]
305
305
 
306
306
 
307
- def evaluator_A(output, target):
307
+ def correctness(output, target):
308
308
  return 1 if output == target["capital"] else 0
309
309
 
310
310
 
311
311
  # Create an Evaluation instance
312
- e = Evaluation(
313
- name="py-evaluation-async",
312
+ e = evaluate(
313
+ name="my-evaluation",
314
314
  data=data,
315
315
  executor=get_capital,
316
- evaluators=[evaluator_A],
316
+ evaluators={"correctness": correctness},
317
317
  project_api_key=os.environ["LMNR_PROJECT_API_KEY"],
318
318
  )
319
-
320
- # Run the evaluation
321
- asyncio.run(e.run())
322
319
  ```
323
320
 
321
+ ### Running from CLI.
322
+
323
+ 1. Make sure `lmnr` is installed in a venv. CLI does not work with a global env
324
+ 1. Run `lmnr path/to/my/eval.py`
325
+
326
+ ### Running from code
327
+
328
+ Simply execute the function, e.g. `python3 path/to/my/eval.py`
@@ -1,21 +1,19 @@
1
1
  lmnr/__init__.py,sha256=5Ks8UIicCzCBgwSz0MOX3I7jVruPMUO3SmxIwUoODzQ,231
2
2
  lmnr/cli.py,sha256=Ptvm5dsNLKUY5lwnN8XkT5GtCYjzpRNi2WvefknB3OQ,1079
3
3
  lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- lmnr/sdk/decorators.py,sha256=O8S4PI6LUfdWPkbroigl5khtnkyhp24J8qzSdlvCs44,2227
5
- lmnr/sdk/evaluations.py,sha256=Ip0XJsdNEPcq4LhPaYF7DOS0y1sDXpPuDf7Vf2YUkJc,11940
6
- lmnr/sdk/laminar.py,sha256=vG9JkaonK8XBLSFVf4sKV50silbg7UVVLTbWFB3SANw,19011
4
+ lmnr/sdk/decorators.py,sha256=ii7Bqp6flaanIFSK6M1_ZZV-izp4o3hkR1MmY7wnFQQ,2227
5
+ lmnr/sdk/evaluations.py,sha256=kVf6cZAF53HMYIdmlaTV5YD0UdPsv_YCzvs1Mts9Zps,13587
6
+ lmnr/sdk/laminar.py,sha256=3LqzqhsSOHxz11_lxAdvqy_awtOnTdPTeYxYEZ3F4Go,19407
7
7
  lmnr/sdk/log.py,sha256=EgAMY77Zn1bv1imCqrmflD3imoAJ2yveOkIcrIP3e98,1170
8
- lmnr/sdk/types.py,sha256=nSN6Z4CDVFJ0o-vjzIT1qzwRiJ70pReGpqdNOcjXKic,4062
9
- lmnr/sdk/utils.py,sha256=ZsGJ86tq8lIbvOhSb1gJWH5K3GylO_lgX68FN6rG2nM,3358
8
+ lmnr/sdk/types.py,sha256=QB89q6WeN715x15ukoRVufXk6FSP_1pGn8QsUSIJG5U,5062
9
+ lmnr/sdk/utils.py,sha256=s81p6uJehgJSaLWy3sR5fTpEDH7vzn3i_UujUHChl6M,3346
10
10
  lmnr/traceloop_sdk/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
11
11
  lmnr/traceloop_sdk/.python-version,sha256=9OLQBQVbD4zE4cJsPePhnAfV_snrPSoqEQw-PXgPMOs,6
12
- lmnr/traceloop_sdk/__init__.py,sha256=-wa25NtU7BeTgqjT6rpVIEdaVP4aOjLgbYrc3B3-JaM,3932
13
- lmnr/traceloop_sdk/config/__init__.py,sha256=EGN3ixOt_ORbMxqaQdLaC14kmO-gyG4mnGJ2GfN-R-E,364
12
+ lmnr/traceloop_sdk/__init__.py,sha256=hp3q1OsFaGgaQCEanJrL38BJN32hWqCNVCSjYpndEsY,2957
13
+ lmnr/traceloop_sdk/config/__init__.py,sha256=DliMGp2NjYAqRFLKpWQPUKjGMHRO8QsVfazBA1qENQ8,248
14
14
  lmnr/traceloop_sdk/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- lmnr/traceloop_sdk/decorators/base.py,sha256=wcqXF0iVQgRXMyWTcJ5QvL_6q2y_gttwsX8dllmAtWM,4891
15
+ lmnr/traceloop_sdk/decorators/base.py,sha256=5YCzAErlhv1bMDO1C9LBlLWYk3bwku0RLjGLR-TkR4c,5128
16
16
  lmnr/traceloop_sdk/instruments.py,sha256=oMvIASueW3GeChpjIdH-DD9aFBVB8OtHZ0HawppTrlI,942
17
- lmnr/traceloop_sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- lmnr/traceloop_sdk/metrics/metrics.py,sha256=AlQ2a2os1WcZbfBd155u_UzBbPrbuPia6O_HbojV9Wc,5055
19
17
  lmnr/traceloop_sdk/tests/__init__.py,sha256=RYnG0-8zbXL0-2Ste1mEBf5sN4d_rQjGTCgPBuaZC74,20
20
18
  lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_and_external_association_properties.yaml,sha256=26g0wRA0juicHg_XrhcE8H4vhs1lawDs0o0aLFn-I7w,3103
21
19
  lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_association_properties.yaml,sha256=FNlSWlYCsWc3w7UPZzfGjDnxS3gAOhL-kpsu4BTxsDE,3061
@@ -37,17 +35,17 @@ lmnr/traceloop_sdk/tests/test_sdk_initialization.py,sha256=fRaf6lrxFzJIN94P1Tav_
37
35
  lmnr/traceloop_sdk/tests/test_tasks.py,sha256=xlEx8BKp4yG83SCjK5WkPGfyC33JSrx4h8VyjVwGbgw,906
38
36
  lmnr/traceloop_sdk/tests/test_workflows.py,sha256=RVcfY3WAFIDZC15-aSua21aoQyYeWE7KypDyUsm-2EM,9372
39
37
  lmnr/traceloop_sdk/tracing/__init__.py,sha256=Ckq7zCM26VdJVB5tIZv0GTPyMZKyfso_KWD5yPHaqdo,66
38
+ lmnr/traceloop_sdk/tracing/attributes.py,sha256=Rvglt_2IeZzKJ-mumrp9qAtTwHza34CrNgv4CvYihk0,221
40
39
  lmnr/traceloop_sdk/tracing/content_allow_list.py,sha256=3feztm6PBWNelc8pAZUcQyEGyeSpNiVKjOaDk65l2ps,846
41
40
  lmnr/traceloop_sdk/tracing/context_manager.py,sha256=csVlB6kDmbgSPsROHwnddvGGblx55v6lJMRj0wsSMQM,304
42
- lmnr/traceloop_sdk/tracing/manual.py,sha256=RPwEreHHdzmw7g15u4G21GqhHOvRp7d72ylQNLG1jRM,1841
43
- lmnr/traceloop_sdk/tracing/tracing.py,sha256=5e8AsiFKaIO6zqAbMfhw242glVsQUkxbNhTWP7QDqSg,40108
41
+ lmnr/traceloop_sdk/tracing/tracing.py,sha256=8plGdX6nErrPERgYXQDQRyBTtVgv2Ies46ph-msLLQE,35443
44
42
  lmnr/traceloop_sdk/utils/__init__.py,sha256=pNhf0G3vTd5ccoc03i1MXDbricSaiqCbi1DLWhSekK8,604
45
43
  lmnr/traceloop_sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
46
44
  lmnr/traceloop_sdk/utils/json_encoder.py,sha256=dK6b_axr70IYL7Vv-bu4wntvDDuyntoqsHaddqX7P58,463
47
45
  lmnr/traceloop_sdk/utils/package_check.py,sha256=TZSngzJOpFhfUZLXIs38cpMxQiZSmp0D-sCrIyhz7BA,251
48
46
  lmnr/traceloop_sdk/version.py,sha256=OlatFEFA4ttqSSIiV8jdE-sq3KG5zu2hnC4B4mzWF3s,23
49
- lmnr-0.4.12b3.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
50
- lmnr-0.4.12b3.dist-info/METADATA,sha256=VnqchbOU6e4xrMuioUj2uJPWDNWGkcz5nmOM-Fua0UI,11992
51
- lmnr-0.4.12b3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
52
- lmnr-0.4.12b3.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
53
- lmnr-0.4.12b3.dist-info/RECORD,,
47
+ lmnr-0.4.12b4.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
48
+ lmnr-0.4.12b4.dist-info/METADATA,sha256=u8pLIuLaw6if3D60lp5ekTKpVoJGxt3Z-gcjVKoDj7g,12196
49
+ lmnr-0.4.12b4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
+ lmnr-0.4.12b4.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
51
+ lmnr-0.4.12b4.dist-info/RECORD,,
File without changes
@@ -1,176 +0,0 @@
1
- from collections.abc import Sequence
2
- from typing import Dict
3
-
4
- from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
5
- OTLPMetricExporter as GRPCExporter,
6
- )
7
- from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
8
- OTLPMetricExporter as HTTPExporter,
9
- )
10
- from opentelemetry.semconv_ai import Meters
11
- from opentelemetry.sdk.metrics import MeterProvider
12
- from opentelemetry.sdk.metrics.export import (
13
- PeriodicExportingMetricReader,
14
- MetricExporter,
15
- )
16
- from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
17
- from opentelemetry.sdk.resources import Resource
18
-
19
- from opentelemetry import metrics
20
-
21
-
22
- class MetricsWrapper(object):
23
- resource_attributes: dict = {}
24
- endpoint: str = None
25
- # if it needs headers?
26
- headers: Dict[str, str] = {}
27
- __metrics_exporter: MetricExporter = None
28
- __metrics_provider: MeterProvider = None
29
-
30
- def __new__(cls, exporter: MetricExporter = None) -> "MetricsWrapper":
31
- if not hasattr(cls, "instance"):
32
- obj = cls.instance = super(MetricsWrapper, cls).__new__(cls)
33
- if not MetricsWrapper.endpoint:
34
- return obj
35
-
36
- obj.__metrics_exporter = (
37
- exporter
38
- if exporter
39
- else init_metrics_exporter(
40
- MetricsWrapper.endpoint, MetricsWrapper.headers
41
- )
42
- )
43
-
44
- obj.__metrics_provider = init_metrics_provider(
45
- obj.__metrics_exporter, MetricsWrapper.resource_attributes
46
- )
47
-
48
- return cls.instance
49
-
50
- @staticmethod
51
- def set_static_params(
52
- resource_attributes: dict,
53
- endpoint: str,
54
- headers: Dict[str, str],
55
- ) -> None:
56
- MetricsWrapper.resource_attributes = resource_attributes
57
- MetricsWrapper.endpoint = endpoint
58
- MetricsWrapper.headers = headers
59
-
60
-
61
- def init_metrics_exporter(endpoint: str, headers: Dict[str, str]) -> MetricExporter:
62
- if "http" in endpoint.lower() or "https" in endpoint.lower():
63
- return HTTPExporter(endpoint=f"{endpoint}/v1/metrics", headers=headers)
64
- else:
65
- return GRPCExporter(endpoint=endpoint, headers=headers)
66
-
67
-
68
- def init_metrics_provider(
69
- exporter: MetricExporter, resource_attributes: dict = None
70
- ) -> MeterProvider:
71
- resource = (
72
- Resource.create(resource_attributes)
73
- if resource_attributes
74
- else Resource.create()
75
- )
76
- reader = PeriodicExportingMetricReader(exporter)
77
- provider = MeterProvider(
78
- metric_readers=[reader],
79
- resource=resource,
80
- views=metric_views(),
81
- )
82
-
83
- metrics.set_meter_provider(provider)
84
- return provider
85
-
86
-
87
- def metric_views() -> Sequence[View]:
88
- return [
89
- View(
90
- instrument_name=Meters.LLM_TOKEN_USAGE,
91
- aggregation=ExplicitBucketHistogramAggregation(
92
- [
93
- 0.01,
94
- 0.02,
95
- 0.04,
96
- 0.08,
97
- 0.16,
98
- 0.32,
99
- 0.64,
100
- 1.28,
101
- 2.56,
102
- 5.12,
103
- 10.24,
104
- 20.48,
105
- 40.96,
106
- 81.92,
107
- ]
108
- ),
109
- ),
110
- View(
111
- instrument_name=Meters.LLM_OPERATION_DURATION,
112
- aggregation=ExplicitBucketHistogramAggregation(
113
- [
114
- 1,
115
- 4,
116
- 16,
117
- 64,
118
- 256,
119
- 1024,
120
- 4096,
121
- 16384,
122
- 65536,
123
- 262144,
124
- 1048576,
125
- 4194304,
126
- 16777216,
127
- 67108864,
128
- ]
129
- ),
130
- ),
131
- View(
132
- instrument_name=Meters.PINECONE_DB_QUERY_DURATION,
133
- aggregation=ExplicitBucketHistogramAggregation(
134
- [
135
- 0.01,
136
- 0.02,
137
- 0.04,
138
- 0.08,
139
- 0.16,
140
- 0.32,
141
- 0.64,
142
- 1.28,
143
- 2.56,
144
- 5.12,
145
- 10.24,
146
- 20.48,
147
- 40.96,
148
- 81.92,
149
- ]
150
- ),
151
- ),
152
- View(
153
- instrument_name=Meters.PINECONE_DB_QUERY_SCORES,
154
- aggregation=ExplicitBucketHistogramAggregation(
155
- [
156
- -1,
157
- -0.875,
158
- -0.75,
159
- -0.625,
160
- -0.5,
161
- -0.375,
162
- -0.25,
163
- -0.125,
164
- 0,
165
- 0.125,
166
- 0.25,
167
- 0.375,
168
- 0.5,
169
- 0.625,
170
- 0.75,
171
- 0.875,
172
- 1,
173
- ]
174
- ),
175
- ),
176
- ]
@@ -1,57 +0,0 @@
1
- from contextlib import contextmanager
2
- from opentelemetry.semconv_ai import SpanAttributes
3
- from opentelemetry.trace import Span
4
- from pydantic import BaseModel
5
- from lmnr.traceloop_sdk.tracing.context_manager import get_tracer
6
-
7
-
8
- class LLMMessage(BaseModel):
9
- role: str
10
- content: str
11
-
12
-
13
- class LLMUsage(BaseModel):
14
- prompt_tokens: int
15
- completion_tokens: int
16
- total_tokens: int
17
-
18
-
19
- class LLMSpan:
20
- _span: Span = None
21
-
22
- def __init__(self, span: Span):
23
- self._span = span
24
- pass
25
-
26
- def report_request(self, model: str, messages: list[LLMMessage]):
27
- self._span.set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model)
28
- for idx, message in enumerate(messages):
29
- self._span.set_attribute(
30
- f"{SpanAttributes.LLM_PROMPTS}.{idx}.role", message.role
31
- )
32
- self._span.set_attribute(
33
- f"{SpanAttributes.LLM_PROMPTS}.{idx}.content", message.content
34
- )
35
-
36
- def report_response(self, model: str, completions: list[str]):
37
- self._span.set_attribute(SpanAttributes.LLM_RESPONSE_MODEL, model)
38
- for idx, completion in enumerate(completions):
39
- self._span.set_attribute(
40
- f"{SpanAttributes.LLM_COMPLETIONS}.{idx}.role", "assistant"
41
- )
42
- self._span.set_attribute(
43
- f"{SpanAttributes.LLM_COMPLETIONS}.{idx}", completion
44
- )
45
-
46
-
47
- @contextmanager
48
- def track_llm_call(vendor: str, type: str):
49
- with get_tracer() as tracer:
50
- with tracer.start_as_current_span(name=f"{vendor}.{type}") as span:
51
- span.set_attribute(SpanAttributes.LLM_SYSTEM, vendor)
52
- span.set_attribute(SpanAttributes.LLM_REQUEST_TYPE, type)
53
- llm_span = LLMSpan(span)
54
- try:
55
- yield llm_span
56
- finally:
57
- span.end()