lmnr 0.3.0b1__py3-none-any.whl → 0.3.2__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 CHANGED
@@ -1,4 +1,7 @@
1
1
  from .sdk.client import Laminar
2
2
  from .sdk.decorators import observe, lmnr_context, wrap_llm_call
3
3
  from .sdk.interface import trace, TraceContext, SpanContext
4
+ from .sdk.tracing_types import EvaluateEvent
4
5
  from .sdk.types import ChatMessage, PipelineRunError, PipelineRunResponse, NodeInput
6
+
7
+ from .semantic_conventions import *
lmnr/sdk/client.py CHANGED
@@ -38,6 +38,11 @@ class Laminar:
38
38
  self.project_api_key = dotenv.get_key(
39
39
  dotenv_path=dotenv_path, key_to_get="LMNR_PROJECT_API_KEY"
40
40
  )
41
+ if not self.project_api_key:
42
+ raise ValueError(
43
+ "Please initialize the Laminar object with your project API key or set "
44
+ "the LMNR_PROJECT_API_KEY environment variable in your environment or .env file"
45
+ )
41
46
 
42
47
  def run(
43
48
  self,
lmnr/sdk/context.py CHANGED
@@ -75,7 +75,6 @@ class LaminarContextManager:
75
75
  user_id=user_id,
76
76
  session_id=session_id,
77
77
  release=release,
78
- start_time=datetime.datetime.now(datetime.timezone.utc),
79
78
  )
80
79
  _root_trace_id_context.set(trace.id)
81
80
  _lmnr_stack_context.set([trace])
@@ -116,8 +115,6 @@ class LaminarContextManager:
116
115
  trace = stack[0]
117
116
  self.update_trace(
118
117
  id=trace.id,
119
- start_time=trace.startTime,
120
- end_time=datetime.datetime.now(datetime.timezone.utc),
121
118
  user_id=trace.userId,
122
119
  session_id=trace.sessionId,
123
120
  release=trace.release,
@@ -127,9 +124,7 @@ class LaminarContextManager:
127
124
  _lmnr_stack_context.set([])
128
125
 
129
126
  if error is not None:
130
- self.update_current_trace(
131
- success=False, end_time=datetime.datetime.now(datetime.timezone.utc)
132
- )
127
+ self.update_current_trace(success=False)
133
128
 
134
129
  if inspect.isgenerator(result) or is_iterator(result):
135
130
  return self._collect_generator_result(
@@ -162,7 +157,8 @@ class LaminarContextManager:
162
157
  def update_current_span(
163
158
  self,
164
159
  metadata: Optional[dict[str, Any]] = None,
165
- check_event_names: list[str] = None,
160
+ attributes: Optional[dict[str, Any]] = None,
161
+ evaluate_events: list[EvaluateEvent] = None,
166
162
  override: bool = False,
167
163
  ):
168
164
  stack = _lmnr_stack_context.get()
@@ -172,15 +168,21 @@ class LaminarContextManager:
172
168
  new_metadata = (
173
169
  metadata if override else {**(span.metadata or {}), **(metadata or {})}
174
170
  )
175
- new_check_event_names = (
176
- check_event_names
171
+ new_evaluate_events = (
172
+ evaluate_events
173
+ if override
174
+ else span.evaluateEvents + (evaluate_events or [])
175
+ )
176
+ new_attributes = (
177
+ attributes
177
178
  if override
178
- else span.evaluateEvents + (check_event_names or [])
179
+ else {**(span.attributes or {}), **(attributes or {})}
179
180
  )
180
181
  self.update_span(
181
182
  span=span,
182
183
  metadata=new_metadata,
183
- evaluate_events=new_check_event_names,
184
+ evaluate_events=new_evaluate_events,
185
+ attributes=new_attributes,
184
186
  )
185
187
 
186
188
  def update_current_trace(
@@ -190,7 +192,6 @@ class LaminarContextManager:
190
192
  release: Optional[str] = None,
191
193
  metadata: Optional[dict[str, Any]] = None,
192
194
  success: bool = True,
193
- end_time: Optional[datetime.datetime] = None,
194
195
  ):
195
196
  existing_trace = (
196
197
  _lmnr_stack_context.get()[0] if _lmnr_stack_context.get() else None
@@ -199,8 +200,6 @@ class LaminarContextManager:
199
200
  return
200
201
  self.update_trace(
201
202
  id=existing_trace.id,
202
- start_time=existing_trace.startTime,
203
- end_time=end_time,
204
203
  user_id=user_id or existing_trace.userId,
205
204
  session_id=session_id or existing_trace.sessionId,
206
205
  release=release or existing_trace.release,
@@ -211,8 +210,6 @@ class LaminarContextManager:
211
210
  def update_trace(
212
211
  self,
213
212
  id: uuid.UUID,
214
- start_time: Optional[datetime.datetime] = None,
215
- end_time: Optional[datetime.datetime] = None,
216
213
  user_id: Optional[str] = None,
217
214
  session_id: Optional[str] = None,
218
215
  release: Optional[str] = None,
@@ -220,8 +217,6 @@ class LaminarContextManager:
220
217
  success: bool = True,
221
218
  ) -> Trace:
222
219
  trace = Trace(
223
- start_time=start_time,
224
- end_time=end_time,
225
220
  id=id,
226
221
  user_id=user_id,
227
222
  session_id=session_id,
@@ -245,6 +240,7 @@ class LaminarContextManager:
245
240
  attributes: Optional[dict[str, Any]] = None,
246
241
  check_event_names: list[str] = None,
247
242
  ) -> Span:
243
+ """Internal method to create a span object. Use `ObservationContext.span` instead."""
248
244
  span = Span(
249
245
  name=name,
250
246
  trace_id=trace_id,
@@ -263,18 +259,23 @@ class LaminarContextManager:
263
259
  self,
264
260
  span: Span,
265
261
  finalize: bool = False,
262
+ input: Optional[Any] = None,
266
263
  end_time: Optional[datetime.datetime] = None,
267
264
  output: Optional[Any] = None,
268
265
  metadata: Optional[dict[str, Any]] = None,
269
266
  attributes: Optional[dict[str, Any]] = None,
270
267
  evaluate_events: Optional[list[EvaluateEvent]] = None,
268
+ override: bool = False,
271
269
  ) -> Span:
270
+ """Internal method to update a span object. Use `SpanContext.update()` instead."""
272
271
  span.update(
272
+ input=input,
273
273
  end_time=end_time,
274
274
  output=output,
275
275
  metadata=metadata,
276
276
  attributes=attributes,
277
277
  evaluate_events=evaluate_events,
278
+ override=override,
278
279
  )
279
280
  if finalize:
280
281
  self._add_observation(span)
@@ -305,7 +306,13 @@ class LaminarContextManager:
305
306
  f"No active span to add check event. Ignoring event. {name}"
306
307
  )
307
308
  return
308
- stack[-1].evaluateEvents.append(EvaluateEvent(name=name, data=data))
309
+ stack[-1].evaluateEvents.append(
310
+ EvaluateEvent(
311
+ name=name,
312
+ data=data,
313
+ timestamp=datetime.datetime.now(datetime.timezone.utc),
314
+ )
315
+ )
309
316
 
310
317
  def run_pipeline(
311
318
  self,
@@ -328,7 +335,8 @@ class LaminarContextManager:
328
335
  )
329
336
 
330
337
  def _force_finalize_trace(self):
331
- self.update_current_trace(end_time=datetime.datetime.now(datetime.timezone.utc))
338
+ # TODO: flush in progress spans as error?
339
+ pass
332
340
 
333
341
  def _add_observation(self, observation: Union[Span, Trace]) -> bool:
334
342
  return self.thread_manager.add_task(observation)
lmnr/sdk/decorators.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Callable, Literal, Optional, Union
5
5
 
6
6
  from .context import LaminarSingleton
7
7
  from .providers.fallback import FallbackProvider
8
+ from ..semantic_conventions.gen_ai_spans import PROVIDER
8
9
  from .types import NodeInput, PipelineRunResponse
9
10
  from .utils import (
10
11
  PROVIDER_NAME_TO_OBJECT,
@@ -103,6 +104,7 @@ class LaminarDecorator:
103
104
  def update_current_span(
104
105
  self,
105
106
  metadata: Optional[dict[str, Any]] = None,
107
+ attributes: Optional[dict[str, Any]] = None,
106
108
  override: bool = False,
107
109
  ):
108
110
  """Update the current span with any optional metadata.
@@ -112,7 +114,9 @@ class LaminarDecorator:
112
114
  override (bool, optional): Whether to override the existing metadata. If False, metadata is merged with the existing metadata. Defaults to False.
113
115
  """
114
116
  laminar = LaminarSingleton().get()
115
- laminar.update_current_span(metadata=metadata, override=override)
117
+ laminar.update_current_span(
118
+ metadata=metadata, attributes=attributes, override=override
119
+ )
116
120
 
117
121
  def update_current_trace(
118
122
  self,
@@ -232,7 +236,7 @@ def wrap_llm_call(func: Callable, name: str = None, provider: str = None) -> Cal
232
236
  if provider_module
233
237
  else {}
234
238
  )
235
- attributes["provider"] = provider_name
239
+ attributes[PROVIDER] = provider_name
236
240
  span = laminar.observe_start(
237
241
  name=name, span_type="LLM", input=inp, attributes=attributes
238
242
  )
@@ -255,7 +259,7 @@ def wrap_llm_call(func: Callable, name: str = None, provider: str = None) -> Cal
255
259
  if provider_module
256
260
  else {}
257
261
  )
258
- attributes["provider"] = provider_name
262
+ attributes[PROVIDER] = provider_name
259
263
  span = laminar.observe_start(
260
264
  name=name, span_type="LLM", input=inp, attributes=attributes
261
265
  )
lmnr/sdk/interface.py CHANGED
@@ -24,9 +24,6 @@ class ObservationContext:
24
24
  def _get_parent(self) -> "ObservationContext":
25
25
  raise NotImplementedError
26
26
 
27
- def end(self, *args, **kwargs):
28
- raise NotImplementedError
29
-
30
27
  def update(self, *args, **kwargs):
31
28
  raise NotImplementedError
32
29
 
@@ -50,7 +47,7 @@ class ObservationContext:
50
47
  Returns:
51
48
  SpanContext: The new span context
52
49
  """
53
- parent = self._get_parent()
50
+ parent = self
54
51
  parent_span_id = (
55
52
  parent.observation.id if isinstance(parent.observation, Span) else None
56
53
  )
@@ -87,16 +84,20 @@ class SpanContext(ObservationContext):
87
84
 
88
85
  def end(
89
86
  self,
87
+ input: Optional[Any] = None,
90
88
  output: Optional[Any] = None,
91
89
  metadata: Optional[dict[str, Any]] = None,
90
+ attributes: Optional[dict[str, Any]] = None,
92
91
  evaluate_events: Optional[list[EvaluateEvent]] = None,
93
92
  override: bool = False,
94
93
  ) -> "SpanContext":
95
94
  """End the span with the given output and optional metadata and evaluate events.
96
95
 
97
96
  Args:
97
+ input (Optional[Any], optional): Inputs to the span. Defaults to None.
98
98
  output (Optional[Any], optional): output of the span. Defaults to None.
99
99
  metadata (Optional[dict[str, Any]], optional): any additional metadata to the span. Defaults to None.
100
+ attributes (Optional[dict[str, Any]], optional): pre-defined attributes (see semantic-convention). Defaults to None.
100
101
  check_event_names (Optional[list[EvaluateEvent]], optional): List of events to evaluate for and tag. Defaults to None.
101
102
  override (bool, optional): override existing metadata fully. If False, metadata is merged. Defaults to False.
102
103
 
@@ -111,25 +112,31 @@ class SpanContext(ObservationContext):
111
112
  )
112
113
  self._get_parent()._children.pop(self.observation.id)
113
114
  return self._update(
115
+ input=input,
114
116
  output=output,
115
117
  metadata=metadata,
116
118
  evaluate_events=evaluate_events,
119
+ attributes=attributes,
117
120
  override=override,
118
121
  finalize=True,
119
122
  )
120
123
 
121
124
  def update(
122
125
  self,
126
+ input: Optional[Any] = None,
123
127
  output: Optional[Any] = None,
124
128
  metadata: Optional[dict[str, Any]] = None,
129
+ attributes: Optional[dict[str, Any]] = None,
125
130
  evaluate_events: Optional[list[EvaluateEvent]] = None,
126
131
  override: bool = False,
127
132
  ) -> "SpanContext":
128
133
  """Update the current span with (optionally) the given output and optional metadata and evaluate events, but don't end it.
129
134
 
130
135
  Args:
136
+ input (Optional[Any], optional): Inputs to the span. Defaults to None.
131
137
  output (Optional[Any], optional): output of the span. Defaults to None.
132
138
  metadata (Optional[dict[str, Any]], optional): any additional metadata to the span. Defaults to None.
139
+ attributes (Optional[dict[str, Any]], optional): pre-defined attributes (see semantic-convention). Defaults to None.
133
140
  check_event_names (Optional[list[EvaluateEvent]], optional): List of events to evaluate for and tag. Defaults to None.
134
141
  override (bool, optional): override existing metadata fully. If False, metadata is merged. Defaults to False.
135
142
 
@@ -137,9 +144,11 @@ class SpanContext(ObservationContext):
137
144
  SpanContext: the finished span context
138
145
  """
139
146
  return self._update(
147
+ input=input or self.observation.input,
140
148
  output=output or self.observation.output,
141
- metadata=metadata or self.observation.metadata,
142
- evaluate_events=evaluate_events or self.observation.evaluateEvents,
149
+ metadata=metadata,
150
+ evaluate_events=evaluate_events,
151
+ attributes=attributes,
143
152
  override=override,
144
153
  finalize=False,
145
154
  )
@@ -182,40 +191,39 @@ class SpanContext(ObservationContext):
182
191
  Returns:
183
192
  SpanContext: the updated span context
184
193
  """
185
- existing_evaluate_events = self.observation.evaluateEvents
186
- output = self.observation.output
187
194
  self._update(
188
- output=output,
189
- evaluate_events=existing_evaluate_events
190
- + [EvaluateEvent(name=name, data=data)],
195
+ input=self.observation.input,
196
+ output=self.observation.output,
197
+ evaluate_events=[
198
+ EvaluateEvent(
199
+ name=name,
200
+ data=data,
201
+ timestamp=datetime.datetime.now(datetime.timezone.utc),
202
+ )
203
+ ],
191
204
  override=False,
192
205
  )
193
206
 
194
207
  def _update(
195
208
  self,
209
+ input: Optional[Any] = None,
196
210
  output: Optional[Any] = None,
197
211
  metadata: Optional[dict[str, Any]] = None,
212
+ attributes: Optional[dict[str, Any]] = None,
198
213
  evaluate_events: Optional[list[EvaluateEvent]] = None,
199
214
  override: bool = False,
200
215
  finalize: bool = False,
201
216
  ) -> "SpanContext":
202
- new_metadata = (
203
- metadata
204
- if override
205
- else {**(self.observation.metadata or {}), **(metadata or {})}
206
- )
207
- new_evaluate_events = (
208
- evaluate_events
209
- if override
210
- else self.observation.evaluateEvents + (evaluate_events or [])
211
- )
212
217
  self.observation = laminar.update_span(
218
+ input=input,
219
+ output=output,
213
220
  span=self.observation,
214
221
  end_time=datetime.datetime.now(datetime.timezone.utc),
215
- output=output,
216
- metadata=new_metadata,
217
- evaluate_events=new_evaluate_events,
222
+ metadata=metadata,
223
+ attributes=attributes,
224
+ evaluate_events=evaluate_events,
218
225
  finalize=finalize,
226
+ override=override,
219
227
  )
220
228
  return self
221
229
 
@@ -253,42 +261,6 @@ class TraceContext(ObservationContext):
253
261
  success=success if success is not None else self.observation.success,
254
262
  )
255
263
 
256
- def end(
257
- self,
258
- user_id: Optional[str] = None,
259
- session_id: Optional[str] = None,
260
- release: Optional[str] = None,
261
- metadata: Optional[dict[str, Any]] = None,
262
- success: bool = True,
263
- ) -> "TraceContext":
264
- """End the current trace with the given metadata and success status.
265
-
266
- Args:
267
- user_id (Optional[str], optional): Custom user_id of your user. Useful for grouping and further analytics. Defaults to None.
268
- session_id (Optional[str], optional): Custom session_id for your session. Random UUID is generated on Laminar side, if not specified.
269
- Defaults to None.
270
- release (Optional[str], optional): _description_. Release of your application. Useful for grouping and further analytics. Defaults to None.
271
- metadata (Optional[dict[str, Any]], optional): any additional metadata to the trace. Defaults to None.
272
- success (bool, optional): whether this trace ran successfully. Defaults to True.
273
-
274
- Returns:
275
- TraceContext: context of the ended trace
276
- """
277
- if self._children:
278
- self._log.warning(
279
- "Ending trace id: %s, but it has children that have not been finalized. Children: %s",
280
- self.observation.id,
281
- [child.observation.name for child in self._children.values()],
282
- )
283
- return self._update(
284
- user_id=user_id or self.observation.userId,
285
- session_id=session_id or self.observation.sessionId,
286
- release=release or self.observation.release,
287
- metadata=metadata or self.observation.metadata,
288
- success=success if success is not None else self.observation.success,
289
- end_time=datetime.datetime.now(datetime.timezone.utc),
290
- )
291
-
292
264
  def _update(
293
265
  self,
294
266
  user_id: Optional[str] = None,
@@ -301,12 +273,10 @@ class TraceContext(ObservationContext):
301
273
  self.observation = laminar.update_trace(
302
274
  id=self.observation.id,
303
275
  user_id=user_id,
304
- start_time=self.observation.startTime,
305
276
  session_id=session_id,
306
277
  release=release,
307
278
  metadata=metadata,
308
279
  success=success,
309
- end_time=end_time,
310
280
  )
311
281
  return self
312
282
 
@@ -320,9 +290,9 @@ def trace(
320
290
 
321
291
  Args:
322
292
  user_id (Optional[str], optional): Custom user_id of your user. Useful for grouping and further analytics. Defaults to None.
323
- session_id (Optional[str], optional): Custom session_id for your session. Random UUID is generated on Laminar side, if not specified.
324
- Defaults to None.
325
- release (Optional[str], optional): _description_. Release of your application. Useful for grouping and further analytics. Defaults to None.
293
+ session_id (Optional[str], optional): Custom session_id for your session. Random UUID is generated on Laminar side, if not specified.
294
+ Defaults to None.
295
+ release (Optional[str], optional): _description_. Release of your application. Useful for grouping and further analytics. Defaults to None.
326
296
 
327
297
  Returns:
328
298
  TraceContext: the pointer to the trace context. Use `.span()` to create a new span within this context.
@@ -334,6 +304,5 @@ def trace(
334
304
  user_id=user_id,
335
305
  session_id=session_id,
336
306
  release=release,
337
- start_time=datetime.datetime.now(datetime.timezone.utc),
338
307
  )
339
308
  return TraceContext(trace, None)
@@ -1,3 +1,19 @@
1
+ from ...semantic_conventions.gen_ai_spans import (
2
+ FINISH_REASONS,
3
+ FREQUENCY_PENALTY,
4
+ INPUT_TOKEN_COUNT,
5
+ MAX_TOKENS,
6
+ OUTPUT_TOKEN_COUNT,
7
+ PRESENCE_PENALTY,
8
+ REQUEST_MODEL,
9
+ RESPONSE_MODEL,
10
+ STOP_SEQUENCES,
11
+ STREAM,
12
+ TEMPERATURE,
13
+ TOP_K,
14
+ TOP_P,
15
+ TOTAL_TOKEN_COUNT,
16
+ )
1
17
  from .base import Provider
2
18
  from .utils import parse_or_dump_to_dict
3
19
 
@@ -85,11 +101,12 @@ class FallbackProvider(Provider):
85
101
  decisions.append(None)
86
102
 
87
103
  return {
88
- "response_model": obj.get("model"),
89
- "input_token_count": obj.get("usage", {}).get("prompt_tokens"),
90
- "output_token_count": obj.get("usage", {}).get("completion_tokens"),
91
- "total_token_count": obj.get("usage", {}).get("total_tokens"),
92
- "decision": self._from_singleton_list(decisions),
104
+ RESPONSE_MODEL: obj.get("model"),
105
+ INPUT_TOKEN_COUNT: obj.get("usage", {}).get("prompt_tokens"),
106
+ OUTPUT_TOKEN_COUNT: obj.get("usage", {}).get("completion_tokens"),
107
+ TOTAL_TOKEN_COUNT: obj.get("usage", {}).get("total_tokens"),
108
+ FINISH_REASONS: obj.get("finish_reason"),
109
+ # "decision": self._from_singleton_list(decisions),
93
110
  }
94
111
 
95
112
  def extract_llm_output(
@@ -107,9 +124,15 @@ class FallbackProvider(Provider):
107
124
  self, func_args: list[Any], func_kwargs: dict[str, Any]
108
125
  ) -> dict[str, Any]:
109
126
  return {
110
- "request_model": func_kwargs.get("model"),
111
- "temperature": func_kwargs.get("temperature"),
112
- "stream": func_kwargs.get("stream", False),
127
+ REQUEST_MODEL: func_kwargs.get("model"),
128
+ TEMPERATURE: func_kwargs.get("temperature"),
129
+ TOP_P: func_kwargs.get("top_p"),
130
+ TOP_K: func_kwargs.get("top_k"),
131
+ FREQUENCY_PENALTY: func_kwargs.get("frequency_penalty"),
132
+ PRESENCE_PENALTY: func_kwargs.get("presence_penalty"),
133
+ STOP_SEQUENCES: func_kwargs.get("stop"),
134
+ MAX_TOKENS: func_kwargs.get("max_tokens"),
135
+ STREAM: func_kwargs.get("stream", False),
113
136
  }
114
137
 
115
138
  def _message_to_key_and_output(
@@ -1,4 +1,19 @@
1
1
  from .base import Provider
2
+ from ...semantic_conventions.gen_ai_spans import (
3
+ FINISH_REASONS,
4
+ FREQUENCY_PENALTY,
5
+ INPUT_TOKEN_COUNT,
6
+ MAX_TOKENS,
7
+ OUTPUT_TOKEN_COUNT,
8
+ PRESENCE_PENALTY,
9
+ REQUEST_MODEL,
10
+ RESPONSE_MODEL,
11
+ STOP_SEQUENCES,
12
+ STREAM,
13
+ TEMPERATURE,
14
+ TOP_P,
15
+ TOTAL_TOKEN_COUNT,
16
+ )
2
17
  from .utils import parse_or_dump_to_dict
3
18
 
4
19
  from collections import defaultdict
@@ -92,12 +107,12 @@ class OpenAI(Provider):
92
107
  decisions.append(None)
93
108
 
94
109
  return {
95
- "response_model": obj.get("model"),
96
- "input_token_count": obj.get("usage", {}).get("prompt_tokens"),
97
- "output_token_count": obj.get("usage", {}).get("completion_tokens"),
98
- "total_token_count": obj.get("usage", {}).get("total_tokens"),
99
- "finish_reason": obj.get("finish_reason"),
100
- "decision": self._from_singleton_list(decisions),
110
+ RESPONSE_MODEL: obj.get("model"),
111
+ INPUT_TOKEN_COUNT: obj.get("usage", {}).get("prompt_tokens"),
112
+ OUTPUT_TOKEN_COUNT: obj.get("usage", {}).get("completion_tokens"),
113
+ TOTAL_TOKEN_COUNT: obj.get("usage", {}).get("total_tokens"),
114
+ FINISH_REASONS: obj.get("finish_reason"),
115
+ # "decision": self._from_singleton_list(decisions),
101
116
  }
102
117
 
103
118
  def extract_llm_output(
@@ -115,10 +130,14 @@ class OpenAI(Provider):
115
130
  self, func_args: list[Any], func_kwargs: dict[str, Any]
116
131
  ) -> dict[str, Any]:
117
132
  return {
118
- "request_model": func_kwargs.get("model"),
119
- "temperature": func_kwargs.get("temperature"),
120
- "top_p": func_kwargs.get("top_p"),
121
- "stream": func_kwargs.get("stream", False),
133
+ REQUEST_MODEL: func_kwargs.get("model"),
134
+ TEMPERATURE: func_kwargs.get("temperature"),
135
+ TOP_P: func_kwargs.get("top_p"),
136
+ FREQUENCY_PENALTY: func_kwargs.get("frequency_penalty"),
137
+ PRESENCE_PENALTY: func_kwargs.get("presence_penalty"),
138
+ STOP_SEQUENCES: func_kwargs.get("stop"),
139
+ MAX_TOKENS: func_kwargs.get("max_tokens"),
140
+ STREAM: func_kwargs.get("stream", False),
122
141
  }
123
142
 
124
143
  def _message_to_key_and_output(
lmnr/sdk/tracing_types.py CHANGED
@@ -10,6 +10,7 @@ from .utils import to_dict
10
10
  class EvaluateEvent(pydantic.BaseModel):
11
11
  name: str
12
12
  data: str
13
+ timestamp: Optional[datetime.datetime] = None
13
14
 
14
15
 
15
16
  class Span(pydantic.BaseModel):
@@ -62,6 +63,7 @@ class Span(pydantic.BaseModel):
62
63
  def update(
63
64
  self,
64
65
  end_time: Optional[datetime.datetime],
66
+ input: Optional[Any] = None,
65
67
  output: Optional[Any] = None,
66
68
  metadata: Optional[dict[str, Any]] = None,
67
69
  attributes: Optional[dict[str, Any]] = None,
@@ -69,6 +71,7 @@ class Span(pydantic.BaseModel):
69
71
  override: bool = False,
70
72
  ):
71
73
  self.endTime = end_time or datetime.datetime.now(datetime.timezone.utc)
74
+ self.input = input
72
75
  self.output = output
73
76
  new_metadata = (
74
77
  metadata if override else {**(self.metadata or {}), **(metadata or {})}
@@ -111,8 +114,6 @@ class Trace(pydantic.BaseModel):
111
114
  id: uuid.UUID
112
115
  version: str = CURRENT_TRACING_VERSION
113
116
  success: bool = True
114
- startTime: Optional[datetime.datetime] = None
115
- endTime: Optional[datetime.datetime] = None
116
117
  userId: Optional[str] = None # provided by user or null
117
118
  sessionId: Optional[str] = None # provided by user or uuid()
118
119
  release: Optional[str] = None
@@ -121,8 +122,6 @@ class Trace(pydantic.BaseModel):
121
122
  def __init__(
122
123
  self,
123
124
  success: bool = True,
124
- start_time: Optional[datetime.datetime] = None,
125
- end_time: Optional[datetime.datetime] = None,
126
125
  id: Optional[uuid.UUID] = None,
127
126
  user_id: Optional[str] = None,
128
127
  session_id: Optional[str] = None,
@@ -132,9 +131,7 @@ class Trace(pydantic.BaseModel):
132
131
  id_ = id or uuid.uuid4()
133
132
  super().__init__(
134
133
  id=id_,
135
- startTime=start_time,
136
134
  success=success,
137
- endTime=end_time,
138
135
  userId=user_id,
139
136
  sessionId=session_id,
140
137
  release=release,
File without changes
@@ -0,0 +1,48 @@
1
+ # source: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-spans.md
2
+ # last updated: 2024-08-26
3
+
4
+ REQUEST_MODEL: str = "gen_ai.request.model"
5
+ RESPONSE_MODEL: str = "gen_ai.response.model"
6
+ PROVIDER: str = "gen_ai.system"
7
+ INPUT_TOKEN_COUNT: str = "gen_ai.usage.input_tokens"
8
+ OUTPUT_TOKEN_COUNT: str = "gen_ai.usage.output_tokens"
9
+ TOTAL_TOKEN_COUNT: str = "gen_ai.usage.total_tokens" # custom, not in the spec
10
+ # https://github.com/openlit/openlit/blob/main/sdk/python/src/openlit/semcov/__init__.py
11
+ COST: str = "gen_ai.usage.cost"
12
+
13
+ OPERATION: str = "gen_ai.operation.name"
14
+
15
+ FREQUENCY_PENALTY: str = "gen_ai.request.frequency_penalty"
16
+ TEMPERATURE: str = "gen_ai.request.temperature"
17
+ MAX_TOKENS: str = "gen_ai.request.max_tokens"
18
+ PRESENCE_PENALTY: str = "gen_ai.request.presence_penalty"
19
+ STOP_SEQUENCES: str = "gen_ai.request.stop_sequences"
20
+ TEMPERATURE: str = "gen_ai.request.temperature"
21
+ TOP_P: str = "gen_ai.request.top_p"
22
+ TOP_K: str = "gen_ai.request.top_k"
23
+
24
+ # https://github.com/openlit/openlit/blob/main/sdk/python/src/openlit/semcov/__init__.py
25
+ STREAM: str = "gen_ai.request.is_stream"
26
+
27
+ FINISH_REASONS = "gen_ai.response.finish_reasons"
28
+
29
+ __all__ = [
30
+ "REQUEST_MODEL",
31
+ "RESPONSE_MODEL",
32
+ "PROVIDER",
33
+ "INPUT_TOKEN_COUNT",
34
+ "OUTPUT_TOKEN_COUNT",
35
+ "TOTAL_TOKEN_COUNT",
36
+ "COST",
37
+ "OPERATION",
38
+ "FREQUENCY_PENALTY",
39
+ "TEMPERATURE",
40
+ "MAX_TOKENS",
41
+ "PRESENCE_PENALTY",
42
+ "STOP_SEQUENCES",
43
+ "TEMPERATURE",
44
+ "TOP_P",
45
+ "TOP_K",
46
+ "STREAM",
47
+ "FINISH_REASONS",
48
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.3.0b1
3
+ Version: 0.3.2
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: backoff (>=2.2.1,<3.0.0)
15
- Requires-Dist: black (>=24.4.2,<25.0.0)
16
15
  Requires-Dist: openai (>=1.41.1,<2.0.0)
17
16
  Requires-Dist: pydantic (>=2.7.4,<3.0.0)
18
17
  Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
@@ -31,6 +30,32 @@ source .myenv/bin/activate # or use your favorite env management tool
31
30
  pip install lmnr
32
31
  ```
33
32
 
33
+ Create .env file at the root and add `LMNR_PROJECT_API_KEY` value to it.
34
+
35
+ Read more [here](https://docs.lmnr.ai/api-reference/introduction#authentication) on how to get `LMNR_PROJECT_API_KEY`.
36
+
37
+ ## Sending events
38
+
39
+ You can send events in two ways:
40
+ - `.event(name, value)` – for a pre-defined event with one of possible values.
41
+ - `.evaluate_event(name, data)` – for an event that our agent checks for and assigns a value from possible values.
42
+
43
+ There are 3 types of events:
44
+ - SCORE - this is an integer score where you specify inclusive minimum and maximum.
45
+ - CLASS - this is a classifier with one of the possible values.
46
+ - TAG - this event has no value and can be assigned to a span.
47
+
48
+ Important notes:
49
+ - If event name does not match anything pre-defined in the UI, the event won't be saved.
50
+ - If event value (when sent with `.event()`) is not in the domain, the event won't be saved.
51
+
52
+ ## Instrumentation
53
+
54
+ We provide two ways to instrument your python code:
55
+ - With `@observe()` decorators and `wrap_llm_call` helpers
56
+ - Manually
57
+
58
+ It is important to not mix the two styles of instrumentation, this can lead to unpredictable results.
34
59
 
35
60
  ## Decorator instrumentation example
36
61
 
@@ -65,10 +90,11 @@ def poem_writer(topic="turbulence"):
65
90
  poem = response.choices[0].message.content
66
91
 
67
92
  if topic in poem:
68
- lmnr_context.event("topic_alignment") # send an event with a pre-defined name
93
+ # send an event with a pre-defined name
94
+ lmnr_context.event("topic_alignment", "good")
69
95
 
70
96
  # to trigger an automatic check for a possible event do:
71
- lmnr_context.check_span_event("excessive_wordiness")
97
+ lmnr_context.evaluate_event("excessive_wordiness", poem)
72
98
 
73
99
  return poem
74
100
 
@@ -91,11 +117,11 @@ For manual instrumetation you will need to import the following:
91
117
  Both `TraceContext` and `SpanContext` expose the following interfaces:
92
118
  - `span(name: str, **kwargs)` - create a child span within the current context. Returns `SpanContext`
93
119
  - `update(**kwargs)` - update the current trace or span and return it. Returns `TraceContext` or `SpanContext`. Useful when some metadata becomes known later during the program execution
94
- - `end(**kwargs)` – update the current span, and terminate it
95
120
 
96
121
  In addition, `SpanContext` allows you to:
97
- - `event(name: str, value: str | int = None)` - emit a custom event at any point
122
+ - `event(name: str, value: str | int)` - emit a custom event at any point
98
123
  - `evaluate_event(name: str, data: str)` - register a possible event for automatic checking by Laminar.
124
+ - `end(**kwargs)` – update the current span, and terminate it
99
125
 
100
126
  Example:
101
127
 
@@ -103,11 +129,12 @@ Example:
103
129
  import os
104
130
  from openai import OpenAI
105
131
 
106
- from lmnr import trace, TraceContext, SpanContext
132
+ from lmnr import trace, TraceContext, SpanContext, EvaluateEvent
133
+ from lmnr.semantic_conventions.gen_ai_spans import INPUT_TOKEN_COUNT, OUTPUT_TOKEN_COUNT, RESPONSE_MODEL, PROVIDER, STREAM
107
134
  client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
108
135
 
109
136
  def poem_writer(t: TraceContext, topic = "turbulence"):
110
- span: SpanContext = t.span(name="poem_writer", input=None)
137
+ span: SpanContext = t.span(name="poem_writer", input=topic)
111
138
 
112
139
  prompt = f"write a poem about {topic}"
113
140
  messages = [
@@ -126,24 +153,73 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
126
153
  )
127
154
  poem = response.choices[0].message.content
128
155
  if topic in poem:
129
- llm_span.event("topic_alignment") # send an event with a pre-defined name
130
-
131
- # note that you can register possible events here as well, not only `llm_span.check_span_event()`
132
- llm_span.end(output=poem, check_event_names=["excessive_wordiness"])
156
+ llm_span.event("topic_alignment", "good") # send an event with a pre-defined name
157
+
158
+ # note that you can register possible events here as well,
159
+ # not only `llm_span.evaluate_event()`
160
+ llm_span.end(
161
+ output=poem,
162
+ evaluate_events=[EvaluateEvent(name="excessive_wordines", data=poem)],
163
+ attributes={
164
+ INPUT_TOKEN_COUNT: response.usage.prompt_tokens,
165
+ OUTPUT_TOKEN_COUNT: response.usage.completion_tokens,
166
+ RESPONSE_MODEL: response.model,
167
+ PROVIDER: 'openai',
168
+ STREAM: False
169
+ }
170
+ )
133
171
  span.end(output=poem)
134
172
  return poem
135
173
 
136
174
 
137
- t: TraceContext = trace(user_id="user", session_id="session", release="release")
175
+ t: TraceContext = trace(user_id="user123", session_id="session123", release="release")
138
176
  main(t, topic="laminar flow")
139
- t.end(success=True)
140
177
  ```
141
178
 
142
- ## Features
179
+ ## Manual attributes
180
+
181
+ You can specify span attributes when creating/updating/ending spans.
182
+
183
+ If you use [decorator instrumentation](#decorator-instrumentation-example), `wrap_llm_call` handles all of this for you.
184
+
185
+ Example usage:
186
+
187
+ ```python
188
+ from lmnr.semantic_conventions.gen_ai_spans import REQUEST_MODEL
189
+
190
+ # span_type = LLM is important for correct attribute semantics
191
+ llm_span = span.span(name="OpenAI completion", input=messages, span_type="LLM")
192
+ llm_span.update(
193
+ attributes={REQUEST_MODEL: "gpt-4o-mini"}
194
+ )
195
+ response = client.chat.completions.create(
196
+ model="gpt-4o-mini",
197
+ messages=[
198
+ {"role": "system", "content": "You are a helpful assistant."},
199
+ {"role": "user", "content": "Hello. What is the capital of France?"},
200
+ ],
201
+ )
202
+ ```
203
+
204
+ Semantics:
205
+
206
+ Check for available semantic conventions in `lmnr.semantic_conventions.gen_ai_spans`.
207
+
208
+ You can specify the cost with `COST`. Otherwise, the cost will be calculated
209
+ on the Laminar servers, given the following are specified:
143
210
 
144
- - Make Laminar endpoint calls from your Python code
145
- - Make Laminar endpoint calls that can run your own functions as tools
146
- - CLI to generate code from pipelines you build on Laminar or execute your own functions while you test your flows in workshop
211
+ - span_type is `"LLM"`
212
+ - Model provider: `PROVIDER`, e.g. 'openai', 'anthropic'
213
+ - Output tokens: `OUTPUT_TOKEN_COUNT`
214
+ - Input tokens: `INPUT_TOKEN_COUNT`*
215
+ - Model. We look at `RESPONSE_MODEL` first, and then, if it is not present, we take the value of `REQUEST_MODEL`
216
+
217
+ \* Also, for the case when `PROVIDER` is `"openai"`, the `STREAM` is set to `True`, and `INPUT_TOKEN_COUNT` is not set, we will calculate
218
+ the number of input tokens, and the cost on the server using [tiktoken](https://github.com/zurawiki/tiktoken-rs) and
219
+ use it in cost calculation.
220
+ This is done because OpenAI does not stream the usage back
221
+ when streaming is enabled. Output token count is (approximately) equal to the number of streaming
222
+ events sent by OpenAI, but there is no way to calculate the input token count, other than re-tokenizing.
147
223
 
148
224
  ## Making Laminar pipeline calls
149
225
 
@@ -180,7 +256,3 @@ PipelineRunResponse(
180
256
  )
181
257
  ```
182
258
 
183
- ## PROJECT_API_KEY
184
-
185
- Read more [here](https://docs.lmnr.ai/api-reference/introduction#authentication) on how to get `PROJECT_API_KEY`.
186
-
@@ -0,0 +1,23 @@
1
+ lmnr/__init__.py,sha256=vUiBEqNVi-dWlFKqnyxt0387t1kzVXrwFp7ShhDJyXY,324
2
+ lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ lmnr/sdk/client.py,sha256=e6cIvJq38a6XAU8FGWYtNXVAPlEoZEyKS7hC3M_6EkU,5749
4
+ lmnr/sdk/collector.py,sha256=6LRmPhOcmGplUDWm_sJh0dVrLTHknd_kmq7POGuAvoQ,5338
5
+ lmnr/sdk/constants.py,sha256=USCfwuUqRx6_0xC8WUxqGj766dInqQkWJcf8U5vPK7s,34
6
+ lmnr/sdk/context.py,sha256=0fLckE56hQxBrPWCsqVCwpTlj6V1nfYJFiiI3FChG_c,15356
7
+ lmnr/sdk/decorators.py,sha256=njl1w76SuKJBpCMD-g_LGLSuDCrRS8WCQ4FIgrelhmY,12011
8
+ lmnr/sdk/interface.py,sha256=ShVtyH2HpKDJU0FLMB-87Zd9tPH0kCP5meKtfz8a3X4,12229
9
+ lmnr/sdk/providers/__init__.py,sha256=wNCgQnt9-bnTNXLQWdPgyKhqA1ajiaEd1Rr2KPOpazM,54
10
+ lmnr/sdk/providers/base.py,sha256=xc6iA8yY_VK6tbzswt-3naZ53aAXtOLur9j8eimC_ZA,1054
11
+ lmnr/sdk/providers/fallback.py,sha256=9-srLJgDK5CWD8DIdnxo6jbSsnwDvdHC-vA06BfEkqQ,5431
12
+ lmnr/sdk/providers/openai.py,sha256=9X2VWz4_EqQBEA1UEww2EKdqods6IzOEmaO6dnhY-Lw,5725
13
+ lmnr/sdk/providers/utils.py,sha256=ROt82VrvezExYOxionAynD3dp6oX5JoPW6F1ayTm7q8,946
14
+ lmnr/sdk/tracing_types.py,sha256=N2aA0gtfayawmiLaotEs_8k5sYIN7uvi9oka54ubHGc,6241
15
+ lmnr/sdk/types.py,sha256=hVxOsa3oCQQ-8aS_WkOtErg4nHJRkBVySfYlTgDlDyk,2084
16
+ lmnr/sdk/utils.py,sha256=1yhXtdGmVXfnc8SOQiTH_zAZGbZrzO8oaFd7q5nE7eY,3349
17
+ lmnr/semantic_conventions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ lmnr/semantic_conventions/gen_ai_spans.py,sha256=3s-2J5v3t5LcMKwK2DefPn56XpxN5oMEYtb9Mf9D_gA,1541
19
+ lmnr-0.3.2.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
20
+ lmnr-0.3.2.dist-info/METADATA,sha256=3NxSt1RIDS1Y06y5uPJauKB7Zlrz8KpgigRs33_ip4o,9663
21
+ lmnr-0.3.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
22
+ lmnr-0.3.2.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
23
+ lmnr-0.3.2.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- lmnr/__init__.py,sha256=U3sQyxCHM9ojzfo05XYxM0T_Bh1StZFSp5K82NjATxc,242
2
- lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- lmnr/sdk/client.py,sha256=6mp4sQF1IEESPYe6ABFgchMBQBKr2AT7eqP-mIC5cEA,5482
4
- lmnr/sdk/collector.py,sha256=6LRmPhOcmGplUDWm_sJh0dVrLTHknd_kmq7POGuAvoQ,5338
5
- lmnr/sdk/constants.py,sha256=USCfwuUqRx6_0xC8WUxqGj766dInqQkWJcf8U5vPK7s,34
6
- lmnr/sdk/context.py,sha256=jfu2HGyZEJYSDf-LQAmmK8MKFnNhYfR66k_baQWx99s,15271
7
- lmnr/sdk/decorators.py,sha256=B2wdhs45-ZM0niotZBOx5FSSCu_vGQ32pntG9o3fKKU,11860
8
- lmnr/sdk/interface.py,sha256=BucPNopp_Xvb1Tvn6We4ETvqqQiWtwjbCksAtt4qmvU,13717
9
- lmnr/sdk/providers/__init__.py,sha256=wNCgQnt9-bnTNXLQWdPgyKhqA1ajiaEd1Rr2KPOpazM,54
10
- lmnr/sdk/providers/base.py,sha256=xc6iA8yY_VK6tbzswt-3naZ53aAXtOLur9j8eimC_ZA,1054
11
- lmnr/sdk/providers/fallback.py,sha256=DXnxBX1vxusGSUC76d0AjouR4NSoajQMdMeG37TRf4k,4741
12
- lmnr/sdk/providers/openai.py,sha256=EygnBniKlcic6eIOfS5zORpytLqUYZxnDRB5Z4MnXZY,5193
13
- lmnr/sdk/providers/utils.py,sha256=ROt82VrvezExYOxionAynD3dp6oX5JoPW6F1ayTm7q8,946
14
- lmnr/sdk/tracing_types.py,sha256=RvVb8yCLjCu9DT59OX_tvUxaOTCtE6fcsDH4nMddzHA,6399
15
- lmnr/sdk/types.py,sha256=hVxOsa3oCQQ-8aS_WkOtErg4nHJRkBVySfYlTgDlDyk,2084
16
- lmnr/sdk/utils.py,sha256=1yhXtdGmVXfnc8SOQiTH_zAZGbZrzO8oaFd7q5nE7eY,3349
17
- lmnr-0.3.0b1.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
18
- lmnr-0.3.0b1.dist-info/METADATA,sha256=U5UBpCkOSbDzsoQw4b4GkJxGKgMlxv8qoVRLzAVupiw,6700
19
- lmnr-0.3.0b1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
- lmnr-0.3.0b1.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
21
- lmnr-0.3.0b1.dist-info/RECORD,,
File without changes
File without changes