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.
Files changed (37) hide show
  1. lmnr/__init__.py +30 -0
  2. lmnr/openllmetry_sdk/__init__.py +4 -16
  3. lmnr/openllmetry_sdk/tracing/attributes.py +0 -1
  4. lmnr/openllmetry_sdk/tracing/tracing.py +30 -10
  5. lmnr/sdk/browser/browser_use_otel.py +4 -4
  6. lmnr/sdk/browser/playwright_otel.py +299 -228
  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 +220 -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 +215 -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 +59 -35
  27. lmnr/sdk/laminar.py +34 -174
  28. lmnr/sdk/types.py +124 -23
  29. lmnr/sdk/utils.py +10 -0
  30. lmnr/version.py +6 -6
  31. {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/METADATA +88 -38
  32. lmnr-0.5.1.dist-info/RECORD +55 -0
  33. {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/WHEEL +1 -1
  34. lmnr/sdk/client.py +0 -313
  35. lmnr-0.4.66.dist-info/RECORD +0 -39
  36. {lmnr-0.4.66.dist-info → lmnr-0.5.1.dist-info}/LICENSE +0 -0
  37. {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 = 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
@@ -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
- if url.endswith("/"):
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 3000
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
- sys.stderr.write(f"\nError: {error}\n")
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 = [] # Add this line to track 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 LaminarClient.init_eval(
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 LaminarClient.shutdown_async()
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 LaminarClient.save_eval_datapoints(
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
- LaminarClient.save_eval_datapoints(eval_id, [datapoint], self.group_name)
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, schedules the evaluation as a task in the
376
- event loop and returns an awaitable handle.
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 loop.run_until_complete(evaluation.run())
483
+ return evaluation.run()
460
484
  else:
461
485
  return asyncio.run(evaluation.run())