lmnr 0.4.37__py3-none-any.whl → 0.4.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmnr/__init__.py +1 -0
- lmnr/cli.py +25 -11
- lmnr/openllmetry_sdk/decorators/base.py +28 -2
- lmnr/openllmetry_sdk/tracing/attributes.py +1 -0
- lmnr/openllmetry_sdk/tracing/tracing.py +84 -33
- lmnr/openllmetry_sdk/utils/package_check.py +1 -0
- lmnr/sdk/datasets.py +2 -4
- lmnr/sdk/decorators.py +1 -9
- lmnr/sdk/eval_control.py +4 -0
- lmnr/sdk/evaluations.py +11 -28
- lmnr/sdk/laminar.py +172 -75
- lmnr/sdk/types.py +39 -4
- {lmnr-0.4.37.dist-info → lmnr-0.4.45.dist-info}/METADATA +93 -62
- {lmnr-0.4.37.dist-info → lmnr-0.4.45.dist-info}/RECORD +17 -16
- {lmnr-0.4.37.dist-info → lmnr-0.4.45.dist-info}/LICENSE +0 -0
- {lmnr-0.4.37.dist-info → lmnr-0.4.45.dist-info}/WHEEL +0 -0
- {lmnr-0.4.37.dist-info → lmnr-0.4.45.dist-info}/entry_points.txt +0 -0
lmnr/sdk/laminar.py
CHANGED
@@ -16,8 +16,10 @@ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExport
|
|
16
16
|
from opentelemetry.util.types import AttributeValue
|
17
17
|
|
18
18
|
from pydantic.alias_generators import to_snake
|
19
|
-
from typing import Any, Literal, Optional, Set, Union
|
19
|
+
from typing import Any, Awaitable, Literal, Optional, Set, Union
|
20
20
|
|
21
|
+
import aiohttp
|
22
|
+
import asyncio
|
21
23
|
import copy
|
22
24
|
import datetime
|
23
25
|
import dotenv
|
@@ -25,8 +27,8 @@ import json
|
|
25
27
|
import logging
|
26
28
|
import os
|
27
29
|
import random
|
28
|
-
import re
|
29
30
|
import requests
|
31
|
+
import re
|
30
32
|
import urllib.parse
|
31
33
|
import uuid
|
32
34
|
|
@@ -36,7 +38,6 @@ from lmnr.openllmetry_sdk.tracing.attributes import (
|
|
36
38
|
SPAN_OUTPUT,
|
37
39
|
SPAN_PATH,
|
38
40
|
TRACE_TYPE,
|
39
|
-
USER_ID,
|
40
41
|
)
|
41
42
|
from lmnr.openllmetry_sdk.tracing.tracing import (
|
42
43
|
get_span_path,
|
@@ -55,7 +56,10 @@ from .types import (
|
|
55
56
|
PipelineRunResponse,
|
56
57
|
NodeInput,
|
57
58
|
PipelineRunRequest,
|
59
|
+
SemanticSearchRequest,
|
60
|
+
SemanticSearchResponse,
|
58
61
|
TraceType,
|
62
|
+
TracingLevel,
|
59
63
|
)
|
60
64
|
|
61
65
|
|
@@ -65,7 +69,6 @@ class Laminar:
|
|
65
69
|
__project_api_key: Optional[str] = None
|
66
70
|
__env: dict[str, str] = {}
|
67
71
|
__initialized: bool = False
|
68
|
-
__http_session: Optional[requests.Session] = None
|
69
72
|
|
70
73
|
@classmethod
|
71
74
|
def initialize(
|
@@ -130,7 +133,6 @@ class Laminar:
|
|
130
133
|
cls.__env = env
|
131
134
|
cls.__initialized = True
|
132
135
|
cls._initialize_logger()
|
133
|
-
cls.__http_session = requests.Session()
|
134
136
|
Traceloop.init(
|
135
137
|
exporter=OTLPSpanExporter(
|
136
138
|
endpoint=cls.__base_grpc_url,
|
@@ -165,8 +167,9 @@ class Laminar:
|
|
165
167
|
metadata: dict[str, str] = {},
|
166
168
|
parent_span_id: Optional[uuid.UUID] = None,
|
167
169
|
trace_id: Optional[uuid.UUID] = None,
|
168
|
-
) -> PipelineRunResponse:
|
169
|
-
"""Runs the pipeline with the given inputs
|
170
|
+
) -> Union[PipelineRunResponse, Awaitable[PipelineRunResponse]]:
|
171
|
+
"""Runs the pipeline with the given inputs. If called from an async
|
172
|
+
function, must be awaited.
|
170
173
|
|
171
174
|
Args:
|
172
175
|
pipeline (str): name of the Laminar pipeline.\
|
@@ -216,34 +219,47 @@ class Laminar:
|
|
216
219
|
parent_span_id=parent_span_id,
|
217
220
|
trace_id=trace_id,
|
218
221
|
)
|
222
|
+
loop = asyncio.get_event_loop()
|
223
|
+
if loop.is_running():
|
224
|
+
return cls.__run(request)
|
225
|
+
else:
|
226
|
+
return asyncio.run(cls.__run(request))
|
219
227
|
except Exception as e:
|
220
228
|
raise ValueError(f"Invalid request: {e}")
|
221
229
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
230
|
+
@classmethod
|
231
|
+
def semantic_search(
|
232
|
+
cls,
|
233
|
+
query: str,
|
234
|
+
dataset_id: uuid.UUID,
|
235
|
+
limit: Optional[int] = None,
|
236
|
+
threshold: Optional[float] = None,
|
237
|
+
) -> SemanticSearchResponse:
|
238
|
+
"""Perform a semantic search on a dataset. If called from an async
|
239
|
+
function, must be awaited.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
query (str): query string to search by
|
243
|
+
dataset_id (uuid.UUID): id of the dataset to search in
|
244
|
+
limit (Optional[int], optional): maximum number of results to\
|
245
|
+
return. Defaults to None.
|
246
|
+
threshold (Optional[float], optional): minimum score for a result\
|
247
|
+
to be returned. Defaults to None.
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
SemanticSearchResponse: response object containing the search results sorted by score in descending order
|
251
|
+
"""
|
252
|
+
request = SemanticSearchRequest(
|
253
|
+
query=query,
|
254
|
+
dataset_id=dataset_id,
|
255
|
+
limit=limit,
|
256
|
+
threshold=threshold,
|
234
257
|
)
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
for key in keys:
|
241
|
-
value = resp_json[key]
|
242
|
-
del resp_json[key]
|
243
|
-
resp_json[to_snake(key)] = value
|
244
|
-
return PipelineRunResponse(**resp_json)
|
245
|
-
except Exception:
|
246
|
-
raise PipelineRunError(response)
|
258
|
+
loop = asyncio.get_event_loop()
|
259
|
+
if loop.is_running():
|
260
|
+
return cls.__semantic_search(request)
|
261
|
+
else:
|
262
|
+
return asyncio.run(cls.__semantic_search(request))
|
247
263
|
|
248
264
|
@classmethod
|
249
265
|
def event(
|
@@ -330,6 +346,10 @@ class Laminar:
|
|
330
346
|
span. Defaults to None.
|
331
347
|
"""
|
332
348
|
|
349
|
+
if not cls.is_initialized():
|
350
|
+
yield
|
351
|
+
return
|
352
|
+
|
333
353
|
with get_tracer() as tracer:
|
334
354
|
span_path = get_span_path(name)
|
335
355
|
ctx = set_value("span_path", span_path, context)
|
@@ -539,6 +559,39 @@ class Laminar:
|
|
539
559
|
if output is not None and span != trace.INVALID_SPAN:
|
540
560
|
span.set_attribute(SPAN_OUTPUT, json_dumps(output))
|
541
561
|
|
562
|
+
@classmethod
|
563
|
+
@contextmanager
|
564
|
+
def set_tracing_level(self, level: TracingLevel):
|
565
|
+
"""Set the tracing level for the current span and the context
|
566
|
+
(i.e. any children spans created from the current span in the current
|
567
|
+
thread).
|
568
|
+
|
569
|
+
Tracing level can be one of:
|
570
|
+
- `TracingLevel.ALL`: Enable tracing for the current span and all
|
571
|
+
children spans.
|
572
|
+
- `TracingLevel.META_ONLY`: Enable tracing for the current span and all
|
573
|
+
children spans, but only record metadata, e.g. tokens, costs.
|
574
|
+
- `TracingLevel.OFF`: Disable recording any spans.
|
575
|
+
|
576
|
+
Example:
|
577
|
+
```python
|
578
|
+
from lmnr import Laminar, TracingLevel
|
579
|
+
|
580
|
+
with Laminar.set_tracing_level(TracingLevel.META_ONLY):
|
581
|
+
openai_client.chat.completions.create()
|
582
|
+
```
|
583
|
+
"""
|
584
|
+
if level == TracingLevel.ALL:
|
585
|
+
yield
|
586
|
+
else:
|
587
|
+
level = "meta_only" if level == TracingLevel.META_ONLY else "off"
|
588
|
+
update_association_properties({"tracing_level": level})
|
589
|
+
yield
|
590
|
+
try:
|
591
|
+
remove_association_properties({"tracing_level": level})
|
592
|
+
except Exception:
|
593
|
+
pass
|
594
|
+
|
542
595
|
@classmethod
|
543
596
|
def set_span_attributes(
|
544
597
|
cls,
|
@@ -588,7 +641,6 @@ class Laminar:
|
|
588
641
|
def set_session(
|
589
642
|
cls,
|
590
643
|
session_id: Optional[str] = None,
|
591
|
-
user_id: Optional[str] = None,
|
592
644
|
):
|
593
645
|
"""Set the session and user id for the current span and the context
|
594
646
|
(i.e. any children spans created from the current span in the current
|
@@ -599,29 +651,18 @@ class Laminar:
|
|
599
651
|
Useful to debug and group long-running\
|
600
652
|
sessions/conversations.
|
601
653
|
Defaults to None.
|
602
|
-
user_id (Optional[str], optional). Deprecated.\
|
603
|
-
Use `Laminar.set_metadata` instead.\
|
604
|
-
Custom user id.\
|
605
|
-
Useful for grouping spans or traces by user.\
|
606
|
-
Defaults to None.
|
607
654
|
"""
|
608
655
|
association_properties = {}
|
609
656
|
if session_id is not None:
|
610
657
|
association_properties[SESSION_ID] = session_id
|
611
|
-
if user_id is not None:
|
612
|
-
cls.__logger.warning(
|
613
|
-
"User ID in set_session is deprecated and will be removed soon. "
|
614
|
-
"Please use `Laminar.set_metadata` instead."
|
615
|
-
)
|
616
|
-
association_properties["metadata." + USER_ID] = user_id
|
617
658
|
update_association_properties(association_properties)
|
618
659
|
|
619
660
|
@classmethod
|
620
|
-
def set_metadata(cls, metadata: dict[str,
|
661
|
+
def set_metadata(cls, metadata: dict[str, str]):
|
621
662
|
"""Set the metadata for the current trace.
|
622
663
|
|
623
664
|
Args:
|
624
|
-
metadata (dict[str,
|
665
|
+
metadata (dict[str, str]): Metadata to set for the trace. Willl be\
|
625
666
|
sent as attributes, so must be json serializable.
|
626
667
|
"""
|
627
668
|
props = {f"metadata.{k}": json_dumps(v) for k, v in metadata.items()}
|
@@ -636,20 +677,6 @@ class Laminar:
|
|
636
677
|
props.pop(k)
|
637
678
|
set_association_properties(props)
|
638
679
|
|
639
|
-
@classmethod
|
640
|
-
def _set_trace_type(
|
641
|
-
cls,
|
642
|
-
trace_type: TraceType,
|
643
|
-
):
|
644
|
-
"""Set the trace_type for the current span and the context
|
645
|
-
Args:
|
646
|
-
trace_type (TraceType): Type of the trace
|
647
|
-
"""
|
648
|
-
association_properties = {
|
649
|
-
TRACE_TYPE: trace_type.value,
|
650
|
-
}
|
651
|
-
update_association_properties(association_properties)
|
652
|
-
|
653
680
|
@classmethod
|
654
681
|
def clear_session(cls):
|
655
682
|
"""Clear the session and user id from the context"""
|
@@ -659,30 +686,33 @@ class Laminar:
|
|
659
686
|
set_association_properties(props)
|
660
687
|
|
661
688
|
@classmethod
|
662
|
-
def create_evaluation(
|
689
|
+
async def create_evaluation(
|
663
690
|
cls,
|
664
691
|
data: list[EvaluationResultDatapoint],
|
665
692
|
group_id: Optional[str] = None,
|
666
693
|
name: Optional[str] = None,
|
667
694
|
) -> CreateEvaluationResponse:
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
{
|
695
|
+
async with aiohttp.ClientSession() as session:
|
696
|
+
async with session.post(
|
697
|
+
cls.__base_http_url + "/v1/evaluations",
|
698
|
+
json={
|
672
699
|
"groupId": group_id,
|
673
700
|
"name": name,
|
674
701
|
"points": [datapoint.to_dict() for datapoint in data],
|
675
|
-
}
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
702
|
+
},
|
703
|
+
headers=cls._headers(),
|
704
|
+
) as response:
|
705
|
+
if response.status != 200:
|
706
|
+
try:
|
707
|
+
resp_json = await response.json()
|
708
|
+
raise ValueError(
|
709
|
+
f"Error creating evaluation {json.dumps(resp_json)}"
|
710
|
+
)
|
711
|
+
except aiohttp.ClientError:
|
712
|
+
text = await response.text()
|
713
|
+
raise ValueError(f"Error creating evaluation {text}")
|
714
|
+
resp_json = await response.json()
|
715
|
+
return CreateEvaluationResponse.model_validate(resp_json)
|
686
716
|
|
687
717
|
@classmethod
|
688
718
|
def get_datapoints(
|
@@ -691,6 +721,10 @@ class Laminar:
|
|
691
721
|
offset: int,
|
692
722
|
limit: int,
|
693
723
|
) -> GetDatapointsResponse:
|
724
|
+
# TODO: Use aiohttp. Currently, this function is called from within
|
725
|
+
# `LaminarDataset.__len__`, which is sync, but can be called from
|
726
|
+
# both sync and async. Python does not make it easy to mix things this
|
727
|
+
# way, so we should probably refactor `LaminarDataset`.
|
694
728
|
params = {"name": dataset_name, "offset": offset, "limit": limit}
|
695
729
|
url = (
|
696
730
|
cls.__base_http_url
|
@@ -717,3 +751,66 @@ class Laminar:
|
|
717
751
|
"Authorization": "Bearer " + cls.__project_api_key,
|
718
752
|
"Content-Type": "application/json",
|
719
753
|
}
|
754
|
+
|
755
|
+
@classmethod
|
756
|
+
def _set_trace_type(
|
757
|
+
cls,
|
758
|
+
trace_type: TraceType,
|
759
|
+
):
|
760
|
+
"""Set the trace_type for the current span and the context
|
761
|
+
Args:
|
762
|
+
trace_type (TraceType): Type of the trace
|
763
|
+
"""
|
764
|
+
association_properties = {
|
765
|
+
TRACE_TYPE: trace_type.value,
|
766
|
+
}
|
767
|
+
update_association_properties(association_properties)
|
768
|
+
|
769
|
+
@classmethod
|
770
|
+
async def __run(
|
771
|
+
cls,
|
772
|
+
request: PipelineRunRequest,
|
773
|
+
) -> PipelineRunResponse:
|
774
|
+
async with aiohttp.ClientSession() as session:
|
775
|
+
async with session.post(
|
776
|
+
cls.__base_http_url + "/v1/pipeline/run",
|
777
|
+
data=json.dumps(request.to_dict()),
|
778
|
+
headers=cls._headers(),
|
779
|
+
) as response:
|
780
|
+
if response.status != 200:
|
781
|
+
raise PipelineRunError(response)
|
782
|
+
try:
|
783
|
+
resp_json = await response.json()
|
784
|
+
keys = list(resp_json.keys())
|
785
|
+
for key in keys:
|
786
|
+
value = resp_json[key]
|
787
|
+
del resp_json[key]
|
788
|
+
resp_json[to_snake(key)] = value
|
789
|
+
return PipelineRunResponse(**resp_json)
|
790
|
+
except Exception:
|
791
|
+
raise PipelineRunError(response)
|
792
|
+
|
793
|
+
@classmethod
|
794
|
+
async def __semantic_search(
|
795
|
+
cls,
|
796
|
+
request: SemanticSearchRequest,
|
797
|
+
) -> SemanticSearchResponse:
|
798
|
+
async with aiohttp.ClientSession() as session:
|
799
|
+
async with session.post(
|
800
|
+
cls.__base_http_url + "/v1/semantic-search",
|
801
|
+
data=json.dumps(request.to_dict()),
|
802
|
+
headers=cls._headers(),
|
803
|
+
) as response:
|
804
|
+
if response.status != 200:
|
805
|
+
raise ValueError(
|
806
|
+
f"Error performing semantic search: [{response.status}] {response.text}"
|
807
|
+
)
|
808
|
+
try:
|
809
|
+
resp_json = await response.json()
|
810
|
+
for result in resp_json["results"]:
|
811
|
+
result["dataset_id"] = uuid.UUID(result["datasetId"])
|
812
|
+
return SemanticSearchResponse(**resp_json)
|
813
|
+
except Exception as e:
|
814
|
+
raise ValueError(
|
815
|
+
f"Error parsing semantic search response: status={response.status} error={e}"
|
816
|
+
)
|
lmnr/sdk/types.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
+
import aiohttp
|
1
2
|
import datetime
|
2
3
|
from enum import Enum
|
3
4
|
import pydantic
|
4
|
-
import requests
|
5
5
|
from typing import Any, Awaitable, Callable, Optional, Union
|
6
6
|
import uuid
|
7
7
|
|
@@ -55,11 +55,40 @@ class PipelineRunResponse(pydantic.BaseModel):
|
|
55
55
|
run_id: str
|
56
56
|
|
57
57
|
|
58
|
+
class SemanticSearchRequest(pydantic.BaseModel):
|
59
|
+
query: str
|
60
|
+
dataset_id: uuid.UUID
|
61
|
+
limit: Optional[int] = pydantic.Field(default=None)
|
62
|
+
threshold: Optional[float] = pydantic.Field(default=None, ge=0.0, le=1.0)
|
63
|
+
|
64
|
+
def to_dict(self):
|
65
|
+
res = {
|
66
|
+
"query": self.query,
|
67
|
+
"datasetId": str(self.dataset_id),
|
68
|
+
}
|
69
|
+
if self.limit is not None:
|
70
|
+
res["limit"] = self.limit
|
71
|
+
if self.threshold is not None:
|
72
|
+
res["threshold"] = self.threshold
|
73
|
+
return res
|
74
|
+
|
75
|
+
|
76
|
+
class SemanticSearchResult(pydantic.BaseModel):
|
77
|
+
dataset_id: uuid.UUID
|
78
|
+
score: float
|
79
|
+
data: dict[str, Any]
|
80
|
+
content: str
|
81
|
+
|
82
|
+
|
83
|
+
class SemanticSearchResponse(pydantic.BaseModel):
|
84
|
+
results: list[SemanticSearchResult]
|
85
|
+
|
86
|
+
|
58
87
|
class PipelineRunError(Exception):
|
59
88
|
error_code: str
|
60
89
|
error_message: str
|
61
90
|
|
62
|
-
def __init__(self, response:
|
91
|
+
def __init__(self, response: aiohttp.ClientResponse):
|
63
92
|
try:
|
64
93
|
resp_json = response.json()
|
65
94
|
self.error_code = resp_json["error_code"]
|
@@ -95,7 +124,7 @@ ExecutorFunctionReturnType = Any
|
|
95
124
|
EvaluatorFunctionReturnType = Union[Numeric, dict[str, Numeric]]
|
96
125
|
|
97
126
|
ExecutorFunction = Callable[
|
98
|
-
[EvaluationDatapointData, Any
|
127
|
+
[EvaluationDatapointData, Any],
|
99
128
|
Union[ExecutorFunctionReturnType, Awaitable[ExecutorFunctionReturnType]],
|
100
129
|
]
|
101
130
|
|
@@ -104,7 +133,7 @@ ExecutorFunction = Callable[
|
|
104
133
|
# record of string keys and number values. The latter is useful for evaluating
|
105
134
|
# multiple criteria in one go instead of running multiple evaluators.
|
106
135
|
EvaluatorFunction = Callable[
|
107
|
-
[ExecutorFunctionReturnType, Any
|
136
|
+
[ExecutorFunctionReturnType, Any],
|
108
137
|
Union[EvaluatorFunctionReturnType, Awaitable[EvaluatorFunctionReturnType]],
|
109
138
|
]
|
110
139
|
|
@@ -174,3 +203,9 @@ class TraceType(Enum):
|
|
174
203
|
class GetDatapointsResponse(pydantic.BaseModel):
|
175
204
|
items: list[Datapoint]
|
176
205
|
totalCount: int
|
206
|
+
|
207
|
+
|
208
|
+
class TracingLevel(Enum):
|
209
|
+
OFF = 0
|
210
|
+
META_ONLY = 1
|
211
|
+
ALL = 2
|