lmnr 0.4.66__py3-none-any.whl → 0.5.0__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 +30 -0
- lmnr/openllmetry_sdk/__init__.py +4 -15
- lmnr/openllmetry_sdk/tracing/attributes.py +0 -1
- lmnr/openllmetry_sdk/tracing/tracing.py +24 -9
- lmnr/sdk/browser/browser_use_otel.py +4 -4
- lmnr/sdk/browser/playwright_otel.py +213 -228
- lmnr/sdk/browser/pw_utils.py +289 -0
- lmnr/sdk/browser/utils.py +18 -53
- lmnr/sdk/client/asynchronous/async_client.py +157 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +13 -0
- lmnr/sdk/client/asynchronous/resources/agent.py +215 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +64 -0
- lmnr/sdk/client/asynchronous/resources/pipeline.py +89 -0
- lmnr/sdk/client/asynchronous/resources/semantic_search.py +60 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +7 -0
- lmnr/sdk/client/synchronous/resources/agent.py +209 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/evals.py +102 -0
- lmnr/sdk/client/synchronous/resources/pipeline.py +89 -0
- lmnr/sdk/client/synchronous/resources/semantic_search.py +60 -0
- lmnr/sdk/client/synchronous/sync_client.py +170 -0
- lmnr/sdk/datasets.py +7 -2
- lmnr/sdk/evaluations.py +53 -27
- lmnr/sdk/laminar.py +22 -175
- lmnr/sdk/types.py +121 -23
- lmnr/sdk/utils.py +10 -0
- lmnr/version.py +6 -6
- {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/METADATA +88 -38
- lmnr-0.5.0.dist-info/RECORD +55 -0
- lmnr/sdk/client.py +0 -313
- lmnr-0.4.66.dist-info/RECORD +0 -39
- {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/LICENSE +0 -0
- {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/WHEEL +0 -0
- {lmnr-0.4.66.dist-info → lmnr-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
"""
|
2
|
+
Laminar HTTP client. Used to send data to/from the Laminar API.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
import re
|
7
|
+
from typing import Optional, TypeVar
|
8
|
+
from types import TracebackType
|
9
|
+
|
10
|
+
from lmnr.sdk.client.synchronous.resources import (
|
11
|
+
Agent,
|
12
|
+
BrowserEvents,
|
13
|
+
Evals,
|
14
|
+
Pipeline,
|
15
|
+
SemanticSearch,
|
16
|
+
)
|
17
|
+
from lmnr.sdk.utils import from_env
|
18
|
+
|
19
|
+
_T = TypeVar("_T", bound="LaminarClient")
|
20
|
+
|
21
|
+
|
22
|
+
class LaminarClient:
|
23
|
+
__base_url: str
|
24
|
+
__project_api_key: str
|
25
|
+
__client: httpx.Client = None
|
26
|
+
|
27
|
+
# Resource properties
|
28
|
+
__pipeline: Optional[Pipeline] = None
|
29
|
+
__semantic_search: Optional[SemanticSearch] = None
|
30
|
+
__agent: Optional[Agent] = None
|
31
|
+
__evals: Optional[Evals] = None
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
base_url: Optional[str] = None,
|
36
|
+
project_api_key: Optional[str] = None,
|
37
|
+
port: Optional[int] = None,
|
38
|
+
timeout: int = 3600,
|
39
|
+
):
|
40
|
+
"""Initializer for the Laminar HTTP client.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
base_url (str): base URL of the Laminar API.
|
44
|
+
project_api_key (str): Laminar project API key
|
45
|
+
port (Optional[int], optional): port of the Laminar API HTTP server.\
|
46
|
+
Overrides any port in the base URL.
|
47
|
+
Defaults to None. If none is provided, the default port (443) will
|
48
|
+
be used.
|
49
|
+
timeout (int, optional): global timeout seconds for the HTTP client.\
|
50
|
+
Applied to all httpx operations, i.e. connect, read, get_from_pool, etc.
|
51
|
+
Defaults to 3600.
|
52
|
+
"""
|
53
|
+
# If port is already in the base URL, use it as is
|
54
|
+
base_url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
|
55
|
+
if match := re.search(r":(\d{1,5})$", base_url):
|
56
|
+
base_url = base_url[: -len(match.group(0))]
|
57
|
+
if port is None:
|
58
|
+
port = int(match.group(1))
|
59
|
+
|
60
|
+
base_url = base_url.rstrip("/")
|
61
|
+
self.__base_url = f"{base_url}:{port or 443}"
|
62
|
+
self.__project_api_key = project_api_key or from_env("LMNR_PROJECT_API_KEY")
|
63
|
+
if not self.__project_api_key:
|
64
|
+
raise ValueError(
|
65
|
+
"Project API key is not set. Please set the LMNR_PROJECT_API_KEY environment "
|
66
|
+
"variable or pass project_api_key to the initializer."
|
67
|
+
)
|
68
|
+
self.__client = httpx.Client(
|
69
|
+
headers=self._headers(),
|
70
|
+
timeout=timeout,
|
71
|
+
)
|
72
|
+
|
73
|
+
# Initialize resource objects
|
74
|
+
self.__pipeline = Pipeline(
|
75
|
+
self.__client, self.__base_url, self.__project_api_key
|
76
|
+
)
|
77
|
+
self.__semantic_search = SemanticSearch(
|
78
|
+
self.__client, self.__base_url, self.__project_api_key
|
79
|
+
)
|
80
|
+
self.__agent = Agent(self.__client, self.__base_url, self.__project_api_key)
|
81
|
+
self.__evals = Evals(self.__client, self.__base_url, self.__project_api_key)
|
82
|
+
self.__browser_events = BrowserEvents(
|
83
|
+
self.__client, self.__base_url, self.__project_api_key
|
84
|
+
)
|
85
|
+
|
86
|
+
@property
|
87
|
+
def pipeline(self) -> Pipeline:
|
88
|
+
"""Get the Pipeline resource.
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
Pipeline: The Pipeline resource instance.
|
92
|
+
"""
|
93
|
+
return self.__pipeline
|
94
|
+
|
95
|
+
@property
|
96
|
+
def semantic_search(self) -> SemanticSearch:
|
97
|
+
"""Get the SemanticSearch resource.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
SemanticSearch: The SemanticSearch resource instance.
|
101
|
+
"""
|
102
|
+
return self.__semantic_search
|
103
|
+
|
104
|
+
@property
|
105
|
+
def agent(self) -> Agent:
|
106
|
+
"""Get the Agent resource.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Agent: The Agent resource instance.
|
110
|
+
"""
|
111
|
+
return self.__agent
|
112
|
+
|
113
|
+
@property
|
114
|
+
def _evals(self) -> Evals:
|
115
|
+
"""Get the Evals resource.
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Evals: The Evals resource instance.
|
119
|
+
"""
|
120
|
+
return self.__evals
|
121
|
+
|
122
|
+
@property
|
123
|
+
def _browser_events(self) -> BrowserEvents:
|
124
|
+
"""Get the BrowserEvents resource.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
BrowserEvents: The BrowserEvents resource instance.
|
128
|
+
"""
|
129
|
+
return self.__browser_events
|
130
|
+
|
131
|
+
def shutdown(self):
|
132
|
+
"""Shutdown the client by closing underlying connections."""
|
133
|
+
self.__client.close()
|
134
|
+
|
135
|
+
def is_closed(self) -> bool:
|
136
|
+
"""Check if the client is closed.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
bool: True if the client is closed, False otherwise.
|
140
|
+
"""
|
141
|
+
return self.__client.is_closed
|
142
|
+
|
143
|
+
def close(self) -> None:
|
144
|
+
"""Close the underlying HTTPX client.
|
145
|
+
|
146
|
+
The client will *not* be usable after this.
|
147
|
+
"""
|
148
|
+
# If an error is thrown while constructing a client, self._client
|
149
|
+
# may not be present
|
150
|
+
if hasattr(self, "_client"):
|
151
|
+
self.__client.close()
|
152
|
+
|
153
|
+
def __enter__(self: _T) -> _T:
|
154
|
+
return self
|
155
|
+
|
156
|
+
def __exit__(
|
157
|
+
self,
|
158
|
+
exc_type: Optional[type[BaseException]],
|
159
|
+
exc: Optional[BaseException],
|
160
|
+
exc_tb: Optional[TracebackType],
|
161
|
+
) -> None:
|
162
|
+
self.close()
|
163
|
+
|
164
|
+
def _headers(self) -> dict[str, str]:
|
165
|
+
assert self.__project_api_key is not None, "Project API key is not set"
|
166
|
+
return {
|
167
|
+
"Authorization": "Bearer " + self.__project_api_key,
|
168
|
+
"Content-Type": "application/json",
|
169
|
+
"Accept": "application/json",
|
170
|
+
}
|
lmnr/sdk/datasets.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
|
3
|
-
from .client import LaminarClient
|
3
|
+
from .client.synchronous.sync_client import LaminarClient
|
4
4
|
from .log import get_default_logger
|
5
5
|
from .types import Datapoint
|
6
6
|
|
@@ -38,7 +38,9 @@ class LaminarDataset(EvaluationDataset):
|
|
38
38
|
f"dataset {self.name}. Fetching batch from {self._offset} to "
|
39
39
|
+ f"{self._offset + self._fetch_size}"
|
40
40
|
)
|
41
|
-
resp =
|
41
|
+
resp = self.client._evals.get_datapoints(
|
42
|
+
self.name, self._offset, self._fetch_size
|
43
|
+
)
|
42
44
|
self._fetched_items += resp.items
|
43
45
|
self._offset = len(self._fetched_items)
|
44
46
|
if self._len is None:
|
@@ -53,3 +55,6 @@ class LaminarDataset(EvaluationDataset):
|
|
53
55
|
if idx >= len(self._fetched_items):
|
54
56
|
self._fetch_batch()
|
55
57
|
return self._fetched_items[idx]
|
58
|
+
|
59
|
+
def set_client(self, client: LaminarClient):
|
60
|
+
self.client = client
|
lmnr/sdk/evaluations.py
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
import asyncio
|
2
2
|
import re
|
3
|
-
import sys
|
4
3
|
import uuid
|
4
|
+
import dotenv
|
5
5
|
from tqdm import tqdm
|
6
6
|
from typing import Any, Awaitable, Optional, Set, Union
|
7
7
|
|
8
|
-
from
|
9
|
-
from
|
8
|
+
from lmnr.openllmetry_sdk.instruments import Instruments
|
9
|
+
from lmnr.openllmetry_sdk.tracing.attributes import SPAN_TYPE
|
10
10
|
|
11
|
-
from .client import
|
12
|
-
from .
|
13
|
-
from .
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
from .
|
11
|
+
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
12
|
+
from lmnr.sdk.client.synchronous.sync_client import LaminarClient
|
13
|
+
from lmnr.sdk.datasets import EvaluationDataset, LaminarDataset
|
14
|
+
from lmnr.sdk.eval_control import EVALUATION_INSTANCE, PREPARE_ONLY
|
15
|
+
from lmnr.sdk.laminar import Laminar as L
|
16
|
+
from lmnr.sdk.log import get_default_logger
|
17
|
+
from lmnr.sdk.types import (
|
17
18
|
Datapoint,
|
18
19
|
EvaluationResultDatapoint,
|
19
20
|
EvaluatorFunction,
|
@@ -25,7 +26,7 @@ from .types import (
|
|
25
26
|
SpanType,
|
26
27
|
TraceType,
|
27
28
|
)
|
28
|
-
from .utils import is_async
|
29
|
+
from lmnr.sdk.utils import from_env, is_async
|
29
30
|
|
30
31
|
DEFAULT_BATCH_SIZE = 5
|
31
32
|
MAX_EXPORT_BATCH_SIZE = 64
|
@@ -78,7 +79,7 @@ class EvaluationReporter:
|
|
78
79
|
|
79
80
|
def stopWithError(self, error: Exception):
|
80
81
|
self.cli_progress.close()
|
81
|
-
|
82
|
+
raise error
|
82
83
|
|
83
84
|
def stop(
|
84
85
|
self, average_scores: dict[str, Numeric], project_id: str, evaluation_id: str
|
@@ -175,6 +176,8 @@ class Evaluation:
|
|
175
176
|
"underscores, or spaces."
|
176
177
|
)
|
177
178
|
|
179
|
+
base_url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
|
180
|
+
|
178
181
|
self.is_finished = False
|
179
182
|
self.reporter = EvaluationReporter(base_url)
|
180
183
|
if isinstance(data, list):
|
@@ -192,7 +195,27 @@ class Evaluation:
|
|
192
195
|
self.batch_size = concurrency_limit
|
193
196
|
self._logger = get_default_logger(self.__class__.__name__)
|
194
197
|
self.human_evaluators = human_evaluators
|
195
|
-
self.upload_tasks = []
|
198
|
+
self.upload_tasks = []
|
199
|
+
self.base_http_url = f"{base_url}:{http_port or 443}"
|
200
|
+
|
201
|
+
api_key = project_api_key
|
202
|
+
if not api_key:
|
203
|
+
dotenv_path = dotenv.find_dotenv(usecwd=True)
|
204
|
+
api_key = dotenv.get_key(
|
205
|
+
dotenv_path=dotenv_path, key_to_get="LMNR_PROJECT_API_KEY"
|
206
|
+
)
|
207
|
+
if not api_key:
|
208
|
+
raise ValueError(
|
209
|
+
"Please initialize the Laminar object with"
|
210
|
+
" your project API key or set the LMNR_PROJECT_API_KEY"
|
211
|
+
" environment variable in your environment or .env file"
|
212
|
+
)
|
213
|
+
self.project_api_key = api_key
|
214
|
+
|
215
|
+
self.client = AsyncLaminarClient(
|
216
|
+
base_url=self.base_http_url,
|
217
|
+
project_api_key=self.project_api_key,
|
218
|
+
)
|
196
219
|
L.initialize(
|
197
220
|
project_api_key=project_api_key,
|
198
221
|
base_url=base_url,
|
@@ -209,9 +232,16 @@ class Evaluation:
|
|
209
232
|
return await self._run()
|
210
233
|
|
211
234
|
async def _run(self) -> None:
|
235
|
+
if isinstance(self.data, LaminarDataset):
|
236
|
+
self.data.set_client(
|
237
|
+
LaminarClient(
|
238
|
+
self.base_http_url,
|
239
|
+
self.project_api_key,
|
240
|
+
)
|
241
|
+
)
|
212
242
|
self.reporter.start(len(self.data))
|
213
243
|
try:
|
214
|
-
evaluation = await
|
244
|
+
evaluation = await self.client._evals.init(
|
215
245
|
name=self.name, group_name=self.group_name
|
216
246
|
)
|
217
247
|
result_datapoints = await self._evaluate_in_batches(evaluation.id)
|
@@ -226,12 +256,19 @@ class Evaluation:
|
|
226
256
|
except Exception as e:
|
227
257
|
self.reporter.stopWithError(e)
|
228
258
|
self.is_finished = True
|
259
|
+
await self._shutdown()
|
229
260
|
return
|
230
261
|
|
231
262
|
average_scores = get_average_scores(result_datapoints)
|
232
263
|
self.reporter.stop(average_scores, evaluation.projectId, evaluation.id)
|
233
264
|
self.is_finished = True
|
234
|
-
await
|
265
|
+
await self._shutdown()
|
266
|
+
|
267
|
+
async def _shutdown(self):
|
268
|
+
L.shutdown()
|
269
|
+
await self.client.close()
|
270
|
+
if isinstance(self.data, LaminarDataset) and self.data.client:
|
271
|
+
self.data.client.close()
|
235
272
|
|
236
273
|
async def _evaluate_in_batches(
|
237
274
|
self, eval_id: uuid.UUID
|
@@ -285,7 +322,7 @@ class Evaluation:
|
|
285
322
|
executor_span_id=executor_span_id,
|
286
323
|
)
|
287
324
|
# First, create datapoint with trace_id so that we can show the dp in the UI
|
288
|
-
await
|
325
|
+
await self.client._evals.save_datapoints(
|
289
326
|
eval_id, [partial_datapoint], self.group_name
|
290
327
|
)
|
291
328
|
executor_span.set_attribute(SPAN_TYPE, SpanType.EXECUTOR.value)
|
@@ -342,7 +379,7 @@ class Evaluation:
|
|
342
379
|
|
343
380
|
# Create background upload task without awaiting it
|
344
381
|
upload_task = asyncio.create_task(
|
345
|
-
|
382
|
+
self.client._evals.save_datapoints(eval_id, [datapoint], self.group_name)
|
346
383
|
)
|
347
384
|
self.upload_tasks.append(upload_task)
|
348
385
|
|
@@ -355,7 +392,6 @@ def evaluate(
|
|
355
392
|
evaluators: dict[str, EvaluatorFunction],
|
356
393
|
human_evaluators: list[HumanEvaluator] = [],
|
357
394
|
name: Optional[str] = None,
|
358
|
-
group_id: Optional[str] = None, # Deprecated
|
359
395
|
group_name: Optional[str] = None,
|
360
396
|
concurrency_limit: int = DEFAULT_BATCH_SIZE,
|
361
397
|
project_api_key: Optional[str] = None,
|
@@ -399,11 +435,6 @@ def evaluate(
|
|
399
435
|
Used to identify the evaluation in the group. If not provided, a\
|
400
436
|
random name will be generated.
|
401
437
|
Defaults to None.
|
402
|
-
group_id (Optional[str], optional): [DEPRECATED] Use group_name instead.
|
403
|
-
An identifier to group evaluations.\
|
404
|
-
Only evaluations within the same group_id can be\
|
405
|
-
visually compared. If not provided, set to "default".
|
406
|
-
Defaults to None
|
407
438
|
group_name (Optional[str], optional): An identifier to group evaluations.\
|
408
439
|
Only evaluations within the same group_name can be visually compared.\
|
409
440
|
If not provided, set to "default".
|
@@ -429,11 +460,6 @@ def evaluate(
|
|
429
460
|
trace_export_timeout_seconds (Optional[int], optional): The timeout for\
|
430
461
|
trace export on OpenTelemetry exporter. Defaults to None.
|
431
462
|
"""
|
432
|
-
if group_id:
|
433
|
-
raise DeprecationWarning("group_id is deprecated. Use group_name instead.")
|
434
|
-
|
435
|
-
group_name = group_name or group_id
|
436
|
-
|
437
463
|
evaluation = Evaluation(
|
438
464
|
data=data,
|
439
465
|
executor=executor,
|