lmnr 0.3.1__tar.gz → 0.3.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for Laminar AI
5
5
  License: Apache-2.0
6
6
  Author: lmnr.ai
@@ -49,6 +49,14 @@ Important notes:
49
49
  - If event name does not match anything pre-defined in the UI, the event won't be saved.
50
50
  - If event value (when sent with `.event()`) is not in the domain, the event won't be saved.
51
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.
59
+
52
60
  ## Decorator instrumentation example
53
61
 
54
62
  For easy automatic instrumentation, we provide you two simple primitives:
@@ -109,11 +117,11 @@ For manual instrumetation you will need to import the following:
109
117
  Both `TraceContext` and `SpanContext` expose the following interfaces:
110
118
  - `span(name: str, **kwargs)` - create a child span within the current context. Returns `SpanContext`
111
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
112
- - `end(**kwargs)` – update the current span, and terminate it
113
120
 
114
121
  In addition, `SpanContext` allows you to:
115
122
  - `event(name: str, value: str | int)` - emit a custom event at any point
116
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
117
125
 
118
126
  Example:
119
127
 
@@ -122,6 +130,7 @@ import os
122
130
  from openai import OpenAI
123
131
 
124
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
125
134
  client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
126
135
 
127
136
  def poem_writer(t: TraceContext, topic = "turbulence"):
@@ -150,7 +159,14 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
150
159
  # not only `llm_span.evaluate_event()`
151
160
  llm_span.end(
152
161
  output=poem,
153
- evaluate_events=[EvaluateEvent(name="excessive_wordines", data=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
+ }
154
170
  )
155
171
  span.end(output=poem)
156
172
  return poem
@@ -158,14 +174,52 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
158
174
 
159
175
  t: TraceContext = trace(user_id="user123", session_id="session123", release="release")
160
176
  main(t, topic="laminar flow")
161
- t.end(success=True)
162
177
  ```
163
178
 
164
- ## 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:
165
210
 
166
- - Make Laminar endpoint calls from your Python code
167
- - Make Laminar endpoint calls that can run your own functions as tools
168
- - 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.
169
223
 
170
224
  ## Making Laminar pipeline calls
171
225
 
@@ -202,4 +256,3 @@ PipelineRunResponse(
202
256
  )
203
257
  ```
204
258
 
205
-
@@ -29,6 +29,14 @@ Important notes:
29
29
  - If event name does not match anything pre-defined in the UI, the event won't be saved.
30
30
  - If event value (when sent with `.event()`) is not in the domain, the event won't be saved.
31
31
 
32
+ ## Instrumentation
33
+
34
+ We provide two ways to instrument your python code:
35
+ - With `@observe()` decorators and `wrap_llm_call` helpers
36
+ - Manually
37
+
38
+ It is important to not mix the two styles of instrumentation, this can lead to unpredictable results.
39
+
32
40
  ## Decorator instrumentation example
33
41
 
34
42
  For easy automatic instrumentation, we provide you two simple primitives:
@@ -89,11 +97,11 @@ For manual instrumetation you will need to import the following:
89
97
  Both `TraceContext` and `SpanContext` expose the following interfaces:
90
98
  - `span(name: str, **kwargs)` - create a child span within the current context. Returns `SpanContext`
91
99
  - `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
92
- - `end(**kwargs)` – update the current span, and terminate it
93
100
 
94
101
  In addition, `SpanContext` allows you to:
95
102
  - `event(name: str, value: str | int)` - emit a custom event at any point
96
103
  - `evaluate_event(name: str, data: str)` - register a possible event for automatic checking by Laminar.
104
+ - `end(**kwargs)` – update the current span, and terminate it
97
105
 
98
106
  Example:
99
107
 
@@ -102,6 +110,7 @@ import os
102
110
  from openai import OpenAI
103
111
 
104
112
  from lmnr import trace, TraceContext, SpanContext, EvaluateEvent
113
+ from lmnr.semantic_conventions.gen_ai_spans import INPUT_TOKEN_COUNT, OUTPUT_TOKEN_COUNT, RESPONSE_MODEL, PROVIDER, STREAM
105
114
  client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
106
115
 
107
116
  def poem_writer(t: TraceContext, topic = "turbulence"):
@@ -130,7 +139,14 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
130
139
  # not only `llm_span.evaluate_event()`
131
140
  llm_span.end(
132
141
  output=poem,
133
- evaluate_events=[EvaluateEvent(name="excessive_wordines", data=poem)]
142
+ evaluate_events=[EvaluateEvent(name="excessive_wordines", data=poem)],
143
+ attributes={
144
+ INPUT_TOKEN_COUNT: response.usage.prompt_tokens,
145
+ OUTPUT_TOKEN_COUNT: response.usage.completion_tokens,
146
+ RESPONSE_MODEL: response.model,
147
+ PROVIDER: 'openai',
148
+ STREAM: False
149
+ }
134
150
  )
135
151
  span.end(output=poem)
136
152
  return poem
@@ -138,14 +154,52 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
138
154
 
139
155
  t: TraceContext = trace(user_id="user123", session_id="session123", release="release")
140
156
  main(t, topic="laminar flow")
141
- t.end(success=True)
142
157
  ```
143
158
 
144
- ## Features
159
+ ## Manual attributes
160
+
161
+ You can specify span attributes when creating/updating/ending spans.
162
+
163
+ If you use [decorator instrumentation](#decorator-instrumentation-example), `wrap_llm_call` handles all of this for you.
164
+
165
+ Example usage:
166
+
167
+ ```python
168
+ from lmnr.semantic_conventions.gen_ai_spans import REQUEST_MODEL
169
+
170
+ # span_type = LLM is important for correct attribute semantics
171
+ llm_span = span.span(name="OpenAI completion", input=messages, span_type="LLM")
172
+ llm_span.update(
173
+ attributes={REQUEST_MODEL: "gpt-4o-mini"}
174
+ )
175
+ response = client.chat.completions.create(
176
+ model="gpt-4o-mini",
177
+ messages=[
178
+ {"role": "system", "content": "You are a helpful assistant."},
179
+ {"role": "user", "content": "Hello. What is the capital of France?"},
180
+ ],
181
+ )
182
+ ```
183
+
184
+ Semantics:
185
+
186
+ Check for available semantic conventions in `lmnr.semantic_conventions.gen_ai_spans`.
187
+
188
+ You can specify the cost with `COST`. Otherwise, the cost will be calculated
189
+ on the Laminar servers, given the following are specified:
145
190
 
146
- - Make Laminar endpoint calls from your Python code
147
- - Make Laminar endpoint calls that can run your own functions as tools
148
- - CLI to generate code from pipelines you build on Laminar or execute your own functions while you test your flows in workshop
191
+ - span_type is `"LLM"`
192
+ - Model provider: `PROVIDER`, e.g. 'openai', 'anthropic'
193
+ - Output tokens: `OUTPUT_TOKEN_COUNT`
194
+ - Input tokens: `INPUT_TOKEN_COUNT`*
195
+ - Model. We look at `RESPONSE_MODEL` first, and then, if it is not present, we take the value of `REQUEST_MODEL`
196
+
197
+ \* Also, for the case when `PROVIDER` is `"openai"`, the `STREAM` is set to `True`, and `INPUT_TOKEN_COUNT` is not set, we will calculate
198
+ the number of input tokens, and the cost on the server using [tiktoken](https://github.com/zurawiki/tiktoken-rs) and
199
+ use it in cost calculation.
200
+ This is done because OpenAI does not stream the usage back
201
+ when streaming is enabled. Output token count is (approximately) equal to the number of streaming
202
+ events sent by OpenAI, but there is no way to calculate the input token count, other than re-tokenizing.
149
203
 
150
204
  ## Making Laminar pipeline calls
151
205
 
@@ -181,4 +235,3 @@ PipelineRunResponse(
181
235
  run_id='53b012d5-5759-48a6-a9c5-0011610e3669'
182
236
  )
183
237
  ```
184
-
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lmnr"
3
- version = "0.3.1"
3
+ version = "0.3.3"
4
4
  description = "Python SDK for Laminar AI"
5
5
  authors = [
6
6
  { name = "lmnr.ai", email = "founders@lmnr.ai" }
@@ -11,7 +11,7 @@ license = "Apache-2.0"
11
11
 
12
12
  [tool.poetry]
13
13
  name = "lmnr"
14
- version = "0.3.1"
14
+ version = "0.3.3"
15
15
  description = "Python SDK for Laminar AI"
16
16
  authors = ["lmnr.ai"]
17
17
  readme = "README.md"
@@ -3,3 +3,5 @@ from .sdk.decorators import observe, lmnr_context, wrap_llm_call
3
3
  from .sdk.interface import trace, TraceContext, SpanContext
4
4
  from .sdk.tracing_types import EvaluateEvent
5
5
  from .sdk.types import ChatMessage, PipelineRunError, PipelineRunResponse, NodeInput
6
+
7
+ from .semantic_conventions import *
@@ -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)
@@ -283,7 +284,7 @@ class LaminarContextManager:
283
284
  def event(
284
285
  self,
285
286
  name: str,
286
- value: Optional[Union[str, int]] = None,
287
+ value: Optional[Union[str, int, float, bool]] = None,
287
288
  timestamp: Optional[datetime.datetime] = None,
288
289
  ):
289
290
  span = _lmnr_stack_context.get()[-1] if _lmnr_stack_context.get() else None
@@ -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)
@@ -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,
@@ -138,14 +142,14 @@ class LaminarDecorator:
138
142
  def event(
139
143
  self,
140
144
  name: str,
141
- value: Optional[Union[str, int]] = None,
145
+ value: Optional[Union[str, int, float, bool]] = None,
142
146
  timestamp: Optional[datetime.datetime] = None,
143
147
  ):
144
148
  """Associate an event with the current span
145
149
 
146
150
  Args:
147
151
  name (str): name of the event. Must be predefined in the Laminar events page.
148
- value (Optional[Union[str, int]], optional): value of the event. Must match range definition in Laminar events page. Defaults to None.
152
+ value (Optional[Union[str, int, float, bool]], optional): value of the event. Must match range definition in Laminar events page. Defaults to None.
149
153
  timestamp (Optional[datetime.datetime], optional): If you need custom timestamp. If not specified, current time is used. Defaults to None.
150
154
  """
151
155
  laminar = LaminarSingleton().get()
@@ -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
  )
@@ -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
  )
@@ -147,14 +156,14 @@ class SpanContext(ObservationContext):
147
156
  def event(
148
157
  self,
149
158
  name: str,
150
- value: Optional[Union[str, int]] = None,
159
+ value: Optional[Union[str, int, float, bool]] = None,
151
160
  timestamp: Optional[datetime.datetime] = None,
152
161
  ) -> "SpanContext":
153
162
  """Associate an event with the current span
154
163
 
155
164
  Args:
156
165
  name (str): name of the event. Must be predefined in the Laminar events page.
157
- value (Optional[Union[str, int]], optional): value of the event. Must match range definition in Laminar events page. Defaults to None.
166
+ value (Optional[Union[str, int, float, bool]], optional): value of the event. Must match range definition in Laminar events page. Defaults to None.
158
167
  timestamp (Optional[datetime.datetime], optional): If you need custom timestamp. If not specified, current time is used. Defaults to None.
159
168
 
160
169
  Returns:
@@ -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(
@@ -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,
@@ -163,14 +160,14 @@ class Event(pydantic.BaseModel):
163
160
  templateName: str
164
161
  timestamp: datetime.datetime
165
162
  spanId: uuid.UUID
166
- value: Optional[Union[int, str]] = None
163
+ value: Optional[Union[int, str, float, bool]] = None
167
164
 
168
165
  def __init__(
169
166
  self,
170
167
  name: str,
171
168
  span_id: uuid.UUID,
172
169
  timestamp: Optional[datetime.datetime] = None,
173
- value: Optional[Union[int, str]] = None,
170
+ value: Optional[Union[int, str, float, bool]] = None,
174
171
  ):
175
172
  super().__init__(
176
173
  id=uuid.uuid4(),
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
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes