lmnr 0.4.66__py3-none-any.whl → 0.5.1__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 -16
- lmnr/openllmetry_sdk/tracing/attributes.py +0 -1
- lmnr/openllmetry_sdk/tracing/tracing.py +30 -10
- lmnr/sdk/browser/browser_use_otel.py +4 -4
- lmnr/sdk/browser/playwright_otel.py +299 -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 +220 -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 +215 -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 +59 -35
- lmnr/sdk/laminar.py +34 -174
- lmnr/sdk/types.py +124 -23
- lmnr/sdk/utils.py +10 -0
- lmnr/version.py +6 -6
- {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/METADATA +88 -38
- lmnr-0.5.1.dist-info/RECORD +55 -0
- {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/WHEEL +1 -1
- lmnr/sdk/client.py +0 -313
- lmnr-0.4.66.dist-info/RECORD +0 -39
- {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/LICENSE +0 -0
- {lmnr-0.4.66.dist-info → lmnr-0.5.1.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
|
@@ -34,15 +35,13 @@ MAX_EXPORT_BATCH_SIZE = 64
|
|
34
35
|
def get_evaluation_url(
|
35
36
|
project_id: str, evaluation_id: str, base_url: Optional[str] = None
|
36
37
|
):
|
37
|
-
if not base_url:
|
38
|
+
if not base_url or base_url == "https://api.lmnr.ai":
|
38
39
|
base_url = "https://www.lmnr.ai"
|
39
40
|
|
40
41
|
url = base_url
|
41
|
-
|
42
|
-
url = url[:-1]
|
42
|
+
url = re.sub(r"\/$", "", url)
|
43
43
|
if url.endswith("localhost") or url.endswith("127.0.0.1"):
|
44
|
-
# We best effort assume that the frontend is running on port
|
45
|
-
# TODO: expose the frontend port?
|
44
|
+
# We best effort assume that the frontend is running on port 5667
|
46
45
|
url = url + ":5667"
|
47
46
|
return f"{url}/project/{project_id}/evaluations/{evaluation_id}"
|
48
47
|
|
@@ -78,7 +77,7 @@ class EvaluationReporter:
|
|
78
77
|
|
79
78
|
def stopWithError(self, error: Exception):
|
80
79
|
self.cli_progress.close()
|
81
|
-
|
80
|
+
raise error
|
82
81
|
|
83
82
|
def stop(
|
84
83
|
self, average_scores: dict[str, Numeric], project_id: str, evaluation_id: str
|
@@ -175,6 +174,8 @@ class Evaluation:
|
|
175
174
|
"underscores, or spaces."
|
176
175
|
)
|
177
176
|
|
177
|
+
base_url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
|
178
|
+
|
178
179
|
self.is_finished = False
|
179
180
|
self.reporter = EvaluationReporter(base_url)
|
180
181
|
if isinstance(data, list):
|
@@ -192,7 +193,27 @@ class Evaluation:
|
|
192
193
|
self.batch_size = concurrency_limit
|
193
194
|
self._logger = get_default_logger(self.__class__.__name__)
|
194
195
|
self.human_evaluators = human_evaluators
|
195
|
-
self.upload_tasks = []
|
196
|
+
self.upload_tasks = []
|
197
|
+
self.base_http_url = f"{base_url}:{http_port or 443}"
|
198
|
+
|
199
|
+
api_key = project_api_key
|
200
|
+
if not api_key:
|
201
|
+
dotenv_path = dotenv.find_dotenv(usecwd=True)
|
202
|
+
api_key = dotenv.get_key(
|
203
|
+
dotenv_path=dotenv_path, key_to_get="LMNR_PROJECT_API_KEY"
|
204
|
+
)
|
205
|
+
if not api_key:
|
206
|
+
raise ValueError(
|
207
|
+
"Please initialize the Laminar object with"
|
208
|
+
" your project API key or set the LMNR_PROJECT_API_KEY"
|
209
|
+
" environment variable in your environment or .env file"
|
210
|
+
)
|
211
|
+
self.project_api_key = api_key
|
212
|
+
|
213
|
+
self.client = AsyncLaminarClient(
|
214
|
+
base_url=self.base_http_url,
|
215
|
+
project_api_key=self.project_api_key,
|
216
|
+
)
|
196
217
|
L.initialize(
|
197
218
|
project_api_key=project_api_key,
|
198
219
|
base_url=base_url,
|
@@ -209,9 +230,16 @@ class Evaluation:
|
|
209
230
|
return await self._run()
|
210
231
|
|
211
232
|
async def _run(self) -> None:
|
233
|
+
if isinstance(self.data, LaminarDataset):
|
234
|
+
self.data.set_client(
|
235
|
+
LaminarClient(
|
236
|
+
self.base_http_url,
|
237
|
+
self.project_api_key,
|
238
|
+
)
|
239
|
+
)
|
212
240
|
self.reporter.start(len(self.data))
|
213
241
|
try:
|
214
|
-
evaluation = await
|
242
|
+
evaluation = await self.client._evals.init(
|
215
243
|
name=self.name, group_name=self.group_name
|
216
244
|
)
|
217
245
|
result_datapoints = await self._evaluate_in_batches(evaluation.id)
|
@@ -226,12 +254,19 @@ class Evaluation:
|
|
226
254
|
except Exception as e:
|
227
255
|
self.reporter.stopWithError(e)
|
228
256
|
self.is_finished = True
|
257
|
+
await self._shutdown()
|
229
258
|
return
|
230
259
|
|
231
260
|
average_scores = get_average_scores(result_datapoints)
|
232
261
|
self.reporter.stop(average_scores, evaluation.projectId, evaluation.id)
|
233
262
|
self.is_finished = True
|
234
|
-
await
|
263
|
+
await self._shutdown()
|
264
|
+
|
265
|
+
async def _shutdown(self):
|
266
|
+
L.shutdown()
|
267
|
+
await self.client.close()
|
268
|
+
if isinstance(self.data, LaminarDataset) and self.data.client:
|
269
|
+
self.data.client.close()
|
235
270
|
|
236
271
|
async def _evaluate_in_batches(
|
237
272
|
self, eval_id: uuid.UUID
|
@@ -285,7 +320,7 @@ class Evaluation:
|
|
285
320
|
executor_span_id=executor_span_id,
|
286
321
|
)
|
287
322
|
# First, create datapoint with trace_id so that we can show the dp in the UI
|
288
|
-
await
|
323
|
+
await self.client._evals.save_datapoints(
|
289
324
|
eval_id, [partial_datapoint], self.group_name
|
290
325
|
)
|
291
326
|
executor_span.set_attribute(SPAN_TYPE, SpanType.EXECUTOR.value)
|
@@ -342,7 +377,7 @@ class Evaluation:
|
|
342
377
|
|
343
378
|
# Create background upload task without awaiting it
|
344
379
|
upload_task = asyncio.create_task(
|
345
|
-
|
380
|
+
self.client._evals.save_datapoints(eval_id, [datapoint], self.group_name)
|
346
381
|
)
|
347
382
|
self.upload_tasks.append(upload_task)
|
348
383
|
|
@@ -355,7 +390,6 @@ def evaluate(
|
|
355
390
|
evaluators: dict[str, EvaluatorFunction],
|
356
391
|
human_evaluators: list[HumanEvaluator] = [],
|
357
392
|
name: Optional[str] = None,
|
358
|
-
group_id: Optional[str] = None, # Deprecated
|
359
393
|
group_name: Optional[str] = None,
|
360
394
|
concurrency_limit: int = DEFAULT_BATCH_SIZE,
|
361
395
|
project_api_key: Optional[str] = None,
|
@@ -372,8 +406,8 @@ def evaluate(
|
|
372
406
|
|
373
407
|
If there is no event loop, creates it and runs the evaluation until
|
374
408
|
completion.
|
375
|
-
If there is an event loop,
|
376
|
-
|
409
|
+
If there is an event loop, returns an awaitable handle immediately. IMPORTANT:
|
410
|
+
You must await the call to `evaluate`.
|
377
411
|
|
378
412
|
Parameters:
|
379
413
|
data (Union[list[EvaluationDatapoint|dict]], EvaluationDataset]):\
|
@@ -399,11 +433,6 @@ def evaluate(
|
|
399
433
|
Used to identify the evaluation in the group. If not provided, a\
|
400
434
|
random name will be generated.
|
401
435
|
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
436
|
group_name (Optional[str], optional): An identifier to group evaluations.\
|
408
437
|
Only evaluations within the same group_name can be visually compared.\
|
409
438
|
If not provided, set to "default".
|
@@ -429,11 +458,6 @@ def evaluate(
|
|
429
458
|
trace_export_timeout_seconds (Optional[int], optional): The timeout for\
|
430
459
|
trace export on OpenTelemetry exporter. Defaults to None.
|
431
460
|
"""
|
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
461
|
evaluation = Evaluation(
|
438
462
|
data=data,
|
439
463
|
executor=executor,
|
@@ -456,6 +480,6 @@ def evaluate(
|
|
456
480
|
else:
|
457
481
|
loop = asyncio.get_event_loop()
|
458
482
|
if loop.is_running():
|
459
|
-
return
|
483
|
+
return evaluation.run()
|
460
484
|
else:
|
461
485
|
return asyncio.run(evaluation.run())
|