lmnr 0.4.65__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.
Files changed (37) hide show
  1. lmnr/__init__.py +30 -0
  2. lmnr/openllmetry_sdk/__init__.py +4 -15
  3. lmnr/openllmetry_sdk/tracing/attributes.py +0 -1
  4. lmnr/openllmetry_sdk/tracing/tracing.py +24 -9
  5. lmnr/sdk/browser/browser_use_otel.py +11 -12
  6. lmnr/sdk/browser/playwright_otel.py +214 -229
  7. lmnr/sdk/browser/pw_utils.py +289 -0
  8. lmnr/sdk/browser/utils.py +18 -53
  9. lmnr/sdk/client/asynchronous/async_client.py +157 -0
  10. lmnr/sdk/client/asynchronous/resources/__init__.py +13 -0
  11. lmnr/sdk/client/asynchronous/resources/agent.py +215 -0
  12. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  13. lmnr/sdk/client/asynchronous/resources/browser_events.py +40 -0
  14. lmnr/sdk/client/asynchronous/resources/evals.py +64 -0
  15. lmnr/sdk/client/asynchronous/resources/pipeline.py +89 -0
  16. lmnr/sdk/client/asynchronous/resources/semantic_search.py +60 -0
  17. lmnr/sdk/client/synchronous/resources/__init__.py +7 -0
  18. lmnr/sdk/client/synchronous/resources/agent.py +209 -0
  19. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  20. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  21. lmnr/sdk/client/synchronous/resources/evals.py +102 -0
  22. lmnr/sdk/client/synchronous/resources/pipeline.py +89 -0
  23. lmnr/sdk/client/synchronous/resources/semantic_search.py +60 -0
  24. lmnr/sdk/client/synchronous/sync_client.py +170 -0
  25. lmnr/sdk/datasets.py +7 -2
  26. lmnr/sdk/evaluations.py +53 -27
  27. lmnr/sdk/laminar.py +22 -175
  28. lmnr/sdk/types.py +121 -23
  29. lmnr/sdk/utils.py +10 -0
  30. lmnr/version.py +6 -6
  31. {lmnr-0.4.65.dist-info → lmnr-0.5.0.dist-info}/METADATA +88 -38
  32. lmnr-0.5.0.dist-info/RECORD +55 -0
  33. lmnr/sdk/client.py +0 -313
  34. lmnr-0.4.65.dist-info/RECORD +0 -39
  35. {lmnr-0.4.65.dist-info → lmnr-0.5.0.dist-info}/LICENSE +0 -0
  36. {lmnr-0.4.65.dist-info → lmnr-0.5.0.dist-info}/WHEEL +0 -0
  37. {lmnr-0.4.65.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 = LaminarClient.get_datapoints(self.name, self._offset, self._fetch_size)
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 ..openllmetry_sdk.instruments import Instruments
9
- from ..openllmetry_sdk.tracing.attributes import SPAN_TYPE
8
+ from lmnr.openllmetry_sdk.instruments import Instruments
9
+ from lmnr.openllmetry_sdk.tracing.attributes import SPAN_TYPE
10
10
 
11
- from .client import LaminarClient
12
- from .datasets import EvaluationDataset
13
- from .eval_control import EVALUATION_INSTANCE, PREPARE_ONLY
14
- from .laminar import Laminar as L
15
- from .log import get_default_logger
16
- from .types import (
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
- sys.stderr.write(f"\nError: {error}\n")
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 = [] # Add this line to track 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 LaminarClient.init_eval(
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 LaminarClient.shutdown_async()
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 LaminarClient.save_eval_datapoints(
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
- LaminarClient.save_eval_datapoints(eval_id, [datapoint], self.group_name)
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,