lmnr 0.4.3__py3-none-any.whl → 0.4.5__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/sdk/evaluations.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import Union
2
2
 
3
+ from .types import EvaluatorFunction, ExecutorFunction, EvaluationDatapoint
3
4
  from .utils import is_async
4
- from .types import EvaluatorFunction, ExecutorFunction, EvaluationDatapoint, Numeric
5
5
  from .laminar import Laminar as L
6
6
  import asyncio
7
7
 
@@ -95,7 +95,7 @@ class Evaluation:
95
95
  self.batch_size = batch_size
96
96
  L.initialize(project_api_key=project_api_key, base_url=base_url)
97
97
 
98
- async def run(self):
98
+ def run(self):
99
99
  """Runs the evaluation.
100
100
 
101
101
  Creates a new evaluation if no evaluation with such name exists, or
@@ -103,7 +103,23 @@ class Evaluation:
103
103
  batches of `self.batch_size`. The executor
104
104
  function is called on each data point to get the output,
105
105
  and then evaluate it by each evaluator function.
106
+
107
+ Usage:
108
+ ```python
109
+ # in a synchronous context:
110
+ e.run()
111
+ # in an asynchronous context:
112
+ await e.run()
113
+ ```
114
+
106
115
  """
116
+ loop = asyncio.get_event_loop()
117
+ if loop.is_running():
118
+ return loop.create_task(self._run())
119
+ else:
120
+ return loop.run_until_complete(self._run())
121
+
122
+ async def _run(self):
107
123
  response = L.create_evaluation(self.name)
108
124
 
109
125
  # Process batches sequentially
@@ -133,7 +149,7 @@ class Evaluation:
133
149
  async def _evaluate_datapoint(self, datapoint):
134
150
  output = (
135
151
  await self.executor(datapoint.data)
136
- if asyncio.iscoroutinefunction(self.executor)
152
+ if is_async(self.executor)
137
153
  else self.executor(datapoint.data)
138
154
  )
139
155
  target = datapoint.target
@@ -144,7 +160,7 @@ class Evaluation:
144
160
  evaluator = self.evaluators[evaluator_name]
145
161
  value = (
146
162
  await evaluator(output, target)
147
- if asyncio.iscoroutinefunction(evaluator)
163
+ if is_async(evaluator)
148
164
  else evaluator(output, target)
149
165
  )
150
166
 
lmnr/sdk/laminar.py CHANGED
@@ -11,7 +11,7 @@ from traceloop.sdk import Traceloop
11
11
  from traceloop.sdk.tracing import get_tracer
12
12
 
13
13
  from pydantic.alias_generators import to_snake
14
- from typing import Any, Optional, Tuple, Union
14
+ from typing import Any, Optional, Union
15
15
 
16
16
  import copy
17
17
  import datetime
@@ -201,15 +201,21 @@ class Laminar:
201
201
  def event(
202
202
  cls,
203
203
  name: str,
204
- value: AttributeValue,
204
+ value: Optional[AttributeValue] = None,
205
205
  timestamp: Optional[Union[datetime.datetime, int]] = None,
206
206
  ):
207
- """Associate an event with the current span
207
+ """Associate an event with the current span. If event with such name never
208
+ existed, Laminar will create a new event and infer its type from the value.
209
+ If the event already exists, Laminar will append the value to the event
210
+ if and only if the value is of a matching type. Otherwise, the event won't
211
+ be recorded Supported types are string, numeric, and boolean. If the value
212
+ is `None`, event is considered a boolean tag with the value of `True`.
208
213
 
209
214
  Args:
210
215
  name (str): event name
211
- value (AttributeValue): event value. Must be a primitive type
212
- or a sequence of values of the same primitive type
216
+ value (Optional[AttributeValue]): event value. Must be a primitive type.
217
+ Boolean true is assumed in the backend if value is None.
218
+ Defaults to None.
213
219
  timestamp (Optional[Union[datetime.datetime, int]], optional):
214
220
  If int, must be epoch nanoseconds. If not
215
221
  specified, relies on the underlying OpenTelemetry
@@ -220,8 +226,9 @@ class Laminar:
220
226
 
221
227
  event = {
222
228
  "lmnr.event.type": "default",
223
- "lmnr.event.value": value,
224
229
  }
230
+ if value is not None:
231
+ event["lmnr.event.value"] = value
225
232
 
226
233
  current_span = get_current_span()
227
234
  if current_span == INVALID_SPAN:
@@ -282,7 +289,7 @@ class Laminar:
282
289
  cls,
283
290
  name: str,
284
291
  input: Any = None,
285
- ) -> Tuple[Span, object]:
292
+ ) -> Span:
286
293
  """Start a new span with the given name. Useful for manual
287
294
  instrumentation.
288
295
 
@@ -297,33 +304,34 @@ class Laminar:
297
304
  that must be passed to `end_span` to end the span.
298
305
 
299
306
  """
300
- with get_tracer() as tracer:
301
- span = tracer.start_span(name)
302
- ctx = set_span_in_context(span)
303
- token = context.attach(ctx)
304
- span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, name)
305
- if input is not None:
306
- span.set_attribute(
307
- SpanAttributes.TRACELOOP_ENTITY_INPUT, json.dumps({"input": input})
308
- )
309
- return (span, token)
307
+ tracer = get_tracer().__enter__()
308
+ span = tracer.start_span(name)
309
+ # apparently, detaching from this context is not mandatory.
310
+ # According to traceloop, and the github issue in opentelemetry,
311
+ # the context is collected by the garbage collector.
312
+ # https://github.com/open-telemetry/opentelemetry-python/issues/2606#issuecomment-2106320379
313
+ context.attach(set_span_in_context(span))
314
+
315
+ if input is not None:
316
+ span.set_attribute(
317
+ SpanAttributes.TRACELOOP_ENTITY_INPUT, json.dumps({"input": input})
318
+ )
319
+
320
+ return span
310
321
 
311
322
  @classmethod
312
- def end_span(cls, span: Span, token: object, output: Any = None):
313
- """End the span started with `start_span`
323
+ def set_span_output(cls, span: Span, output: Any = None):
324
+ """Set the output of the span. Useful for manual instrumentation.
314
325
 
315
326
  Args:
316
- span (Span): span returned by `start_span`
317
- token (object): context token returned by `start_span`
327
+ span (Span): the span to set the output for
318
328
  output (Any, optional): output of the span. Will be sent as an
319
329
  attribute, so must be json serializable. Defaults to None.
320
330
  """
321
331
  if output is not None:
322
332
  span.set_attribute(
323
- SpanAttributes.TRACELOOP_ENTITY_OUTPUT, json.dumps({"output": output})
333
+ SpanAttributes.TRACELOOP_ENTITY_OUTPUT, json.dumps({output})
324
334
  )
325
- span.end()
326
- context.detach(token)
327
335
 
328
336
  @classmethod
329
337
  def set_session(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -89,6 +89,44 @@ def poem_writer(topic="turbulence"):
89
89
  print(poem_writer(topic="laminar flow"))
90
90
  ```
91
91
 
92
+ ### Manual instrumentation
93
+
94
+ Our manual instrumentation is a very thin wrapper around OpenTelemetry's
95
+ `trace.start_span`. Our wrapper sets the span into the active context.
96
+ You don't have to explicitly pass the spans around, it is enough to
97
+ just call `L.start_span`, and OpenTelemetry will handle the context management
98
+
99
+ ```python
100
+ from lmnr import observe, Laminar as L
101
+ L.initialize(project_api_key="<LMNR_PROJECT_API_KEY>")
102
+
103
+ def poem_writer(topic="turbulence"):
104
+
105
+ span = L.start_span("poem_writer", topic) # start a span
106
+
107
+ prompt = f"write a poem about {topic}"
108
+
109
+ # OpenAI calls are still automatically instrumented with OpenLLMetry
110
+ response = client.chat.completions.create(
111
+ model="gpt-4o",
112
+ messages=[
113
+ {"role": "system", "content": "You are a helpful assistant."},
114
+ {"role": "user", "content": prompt},
115
+ ],
116
+ )
117
+ poem = response.choices[0].message.content
118
+ # while within the span, you can attach laminar events to it
119
+ L.event("event_name", "event_value")
120
+
121
+ L.set_span_output(span, poem) # set an output
122
+
123
+ # IMPORTANT: don't forget to end all the spans (usually in `finally` blocks)
124
+ # Otherwise, the trace may not be sent/displayed correctly
125
+ span.end()
126
+
127
+ return poem
128
+ ```
129
+
92
130
 
93
131
  ## Sending events
94
132
 
@@ -1,13 +1,13 @@
1
1
  lmnr/__init__.py,sha256=wQwnHl662Xcz7GdSofFsEjmAK0nxioYA2Yq6Q78m4ps,194
2
2
  lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  lmnr/sdk/decorators.py,sha256=Xs6n0TGX9LZ9i1hE_UZz4LEyd_ZAfpVGfNQh_rKwOuA,2493
4
- lmnr/sdk/evaluations.py,sha256=jdXWRJyr17iAWva8Vyvn8yT-rZAoQ8Xz0QQ_zzfPwvY,6044
5
- lmnr/sdk/laminar.py,sha256=pMgWugFvzqYaEf1TRIjfFl_yo17GdOGGagnPBEA8nHQ,16520
4
+ lmnr/sdk/evaluations.py,sha256=Z0j2HyXgrwlGyiT_Ql7W3e_ZWjOlNlIj9RWAKjEgkkE,6366
5
+ lmnr/sdk/laminar.py,sha256=LOttLBrQoAlduqLYL6sODYHzqRrt44mDNgnnE2it5CE,17114
6
6
  lmnr/sdk/log.py,sha256=EgAMY77Zn1bv1imCqrmflD3imoAJ2yveOkIcrIP3e98,1170
7
7
  lmnr/sdk/types.py,sha256=gDwRSWR9A1__FGtQhVaFc6PUYQuIhubo5tpfYAajTQQ,4055
8
8
  lmnr/sdk/utils.py,sha256=ZsGJ86tq8lIbvOhSb1gJWH5K3GylO_lgX68FN6rG2nM,3358
9
- lmnr-0.4.3.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
10
- lmnr-0.4.3.dist-info/METADATA,sha256=woC24QpLILvRBtLdwCktKJydeiPXEOH8LBb7CSAnw4I,7025
11
- lmnr-0.4.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
- lmnr-0.4.3.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
13
- lmnr-0.4.3.dist-info/RECORD,,
9
+ lmnr-0.4.5.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
10
+ lmnr-0.4.5.dist-info/METADATA,sha256=tmHMhn3OT6gvymqbZk1aDp5_LH-dHiOMc2m_Lpt8ptU,8292
11
+ lmnr-0.4.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
+ lmnr-0.4.5.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
13
+ lmnr-0.4.5.dist-info/RECORD,,
File without changes
File without changes