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 +3 -2
- lmnr/sdk/evaluations.py +90 -58
- lmnr/sdk/laminar.py +32 -10
- lmnr/sdk/types.py +38 -5
- lmnr/sdk/utils.py +4 -5
- lmnr/traceloop_sdk/__init__.py +3 -29
- lmnr/traceloop_sdk/config/__init__.py +0 -4
- lmnr/traceloop_sdk/decorators/base.py +16 -9
- lmnr/traceloop_sdk/tracing/attributes.py +8 -0
- lmnr/traceloop_sdk/tracing/tracing.py +31 -142
- {lmnr-0.4.12b3.dist-info → lmnr-0.4.12b4.dist-info}/METADATA +17 -12
- {lmnr-0.4.12b3.dist-info → lmnr-0.4.12b4.dist-info}/RECORD +15 -17
- lmnr/traceloop_sdk/metrics/__init__.py +0 -0
- lmnr/traceloop_sdk/metrics/metrics.py +0 -176
- lmnr/traceloop_sdk/tracing/manual.py +0 -57
- {lmnr-0.4.12b3.dist-info → lmnr-0.4.12b4.dist-info}/LICENSE +0 -0
- {lmnr-0.4.12b3.dist-info → lmnr-0.4.12b4.dist-info}/WHEEL +0 -0
- {lmnr-0.4.12b3.dist-info → lmnr-0.4.12b4.dist-info}/entry_points.txt +0 -0
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
|
-
|
47
|
+
SESSION_ID, session_id
|
47
48
|
)
|
48
49
|
if user_id is not None:
|
49
50
|
current_span.set_attribute(
|
50
|
-
|
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
|
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(
|
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
|
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:
|
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 =
|
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
|
-
|
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(
|
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(
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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:
|
260
|
-
evaluators:
|
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.
|
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
|
-
|
360
|
-
json.dumps(
|
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[
|
407
|
+
association_properties[SESSION_ID] = session_id
|
400
408
|
if user_id is not None:
|
401
|
-
association_properties[
|
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.
|
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
|
2
|
+
from enum import Enum
|
3
3
|
import pydantic
|
4
|
-
import
|
4
|
+
import requests
|
5
5
|
from typing import Any, Awaitable, Callable, Literal, Optional, Union
|
6
|
+
import uuid
|
6
7
|
|
7
|
-
from .utils import
|
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
|
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
|
-
|
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
|
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
|
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)
|
97
|
+
if i < len(func_args):
|
99
98
|
res[k] = func_args[i]
|
100
99
|
return res
|
lmnr/traceloop_sdk/__init__.py
CHANGED
@@ -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
|
-
|
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
|
-
|
56
|
-
_json_dumps(
|
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
|
-
|
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
|
-
|
109
|
-
_json_dumps(
|
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
|
|
@@ -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 = "
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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(
|
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(
|
259
|
-
print(Fore.RESET)
|
223
|
+
print("Warning: redis library does not exist.")
|
260
224
|
|
261
225
|
else:
|
262
226
|
print(
|
263
|
-
|
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"{
|
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.
|
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
|
-
|
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.
|
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
|
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 =
|
313
|
-
name="
|
312
|
+
e = evaluate(
|
313
|
+
name="my-evaluation",
|
314
314
|
data=data,
|
315
315
|
executor=get_capital,
|
316
|
-
evaluators=
|
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=
|
5
|
-
lmnr/sdk/evaluations.py,sha256=
|
6
|
-
lmnr/sdk/laminar.py,sha256=
|
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=
|
9
|
-
lmnr/sdk/utils.py,sha256=
|
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
|
13
|
-
lmnr/traceloop_sdk/config/__init__.py,sha256=
|
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=
|
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/
|
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.
|
50
|
-
lmnr-0.4.
|
51
|
-
lmnr-0.4.
|
52
|
-
lmnr-0.4.
|
53
|
-
lmnr-0.4.
|
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()
|
File without changes
|
File without changes
|
File without changes
|