lmnr 0.3.0b1__tar.gz → 0.3.2__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.
- {lmnr-0.3.0b1 → lmnr-0.3.2}/PKG-INFO +94 -22
- {lmnr-0.3.0b1 → lmnr-0.3.2}/README.md +93 -20
- {lmnr-0.3.0b1 → lmnr-0.3.2}/pyproject.toml +5 -3
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/__init__.py +3 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/client.py +5 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/context.py +28 -20
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/decorators.py +7 -3
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/interface.py +35 -66
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/providers/fallback.py +31 -8
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/providers/openai.py +29 -10
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/tracing_types.py +3 -6
- lmnr-0.3.2/src/lmnr/semantic_conventions/__init__.py +0 -0
- lmnr-0.3.2/src/lmnr/semantic_conventions/gen_ai_spans.py +48 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/LICENSE +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/__init__.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/collector.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/constants.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/providers/__init__.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/providers/base.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/providers/utils.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/types.py +0 -0
- {lmnr-0.3.0b1 → lmnr-0.3.2}/src/lmnr/sdk/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lmnr
|
3
|
-
Version: 0.3.
|
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
|
-
|
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.
|
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
|
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=
|
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,
|
132
|
-
llm_span.
|
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="
|
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
|
-
##
|
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
|
-
-
|
145
|
-
-
|
146
|
-
-
|
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
|
-
|
@@ -10,6 +10,32 @@ source .myenv/bin/activate # or use your favorite env management tool
|
|
10
10
|
pip install lmnr
|
11
11
|
```
|
12
12
|
|
13
|
+
Create .env file at the root and add `LMNR_PROJECT_API_KEY` value to it.
|
14
|
+
|
15
|
+
Read more [here](https://docs.lmnr.ai/api-reference/introduction#authentication) on how to get `LMNR_PROJECT_API_KEY`.
|
16
|
+
|
17
|
+
## Sending events
|
18
|
+
|
19
|
+
You can send events in two ways:
|
20
|
+
- `.event(name, value)` – for a pre-defined event with one of possible values.
|
21
|
+
- `.evaluate_event(name, data)` – for an event that our agent checks for and assigns a value from possible values.
|
22
|
+
|
23
|
+
There are 3 types of events:
|
24
|
+
- SCORE - this is an integer score where you specify inclusive minimum and maximum.
|
25
|
+
- CLASS - this is a classifier with one of the possible values.
|
26
|
+
- TAG - this event has no value and can be assigned to a span.
|
27
|
+
|
28
|
+
Important notes:
|
29
|
+
- If event name does not match anything pre-defined in the UI, the event won't be saved.
|
30
|
+
- If event value (when sent with `.event()`) is not in the domain, the event won't be saved.
|
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.
|
13
39
|
|
14
40
|
## Decorator instrumentation example
|
15
41
|
|
@@ -44,10 +70,11 @@ def poem_writer(topic="turbulence"):
|
|
44
70
|
poem = response.choices[0].message.content
|
45
71
|
|
46
72
|
if topic in poem:
|
47
|
-
|
73
|
+
# send an event with a pre-defined name
|
74
|
+
lmnr_context.event("topic_alignment", "good")
|
48
75
|
|
49
76
|
# to trigger an automatic check for a possible event do:
|
50
|
-
lmnr_context.
|
77
|
+
lmnr_context.evaluate_event("excessive_wordiness", poem)
|
51
78
|
|
52
79
|
return poem
|
53
80
|
|
@@ -70,11 +97,11 @@ For manual instrumetation you will need to import the following:
|
|
70
97
|
Both `TraceContext` and `SpanContext` expose the following interfaces:
|
71
98
|
- `span(name: str, **kwargs)` - create a child span within the current context. Returns `SpanContext`
|
72
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
|
73
|
-
- `end(**kwargs)` – update the current span, and terminate it
|
74
100
|
|
75
101
|
In addition, `SpanContext` allows you to:
|
76
|
-
- `event(name: str, value: str | int
|
102
|
+
- `event(name: str, value: str | int)` - emit a custom event at any point
|
77
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
|
78
105
|
|
79
106
|
Example:
|
80
107
|
|
@@ -82,11 +109,12 @@ Example:
|
|
82
109
|
import os
|
83
110
|
from openai import OpenAI
|
84
111
|
|
85
|
-
from lmnr import trace, TraceContext, SpanContext
|
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
|
86
114
|
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
87
115
|
|
88
116
|
def poem_writer(t: TraceContext, topic = "turbulence"):
|
89
|
-
span: SpanContext = t.span(name="poem_writer", input=
|
117
|
+
span: SpanContext = t.span(name="poem_writer", input=topic)
|
90
118
|
|
91
119
|
prompt = f"write a poem about {topic}"
|
92
120
|
messages = [
|
@@ -105,24 +133,73 @@ def poem_writer(t: TraceContext, topic = "turbulence"):
|
|
105
133
|
)
|
106
134
|
poem = response.choices[0].message.content
|
107
135
|
if topic in poem:
|
108
|
-
llm_span.event("topic_alignment") # send an event with a pre-defined name
|
109
|
-
|
110
|
-
# note that you can register possible events here as well,
|
111
|
-
llm_span.
|
136
|
+
llm_span.event("topic_alignment", "good") # send an event with a pre-defined name
|
137
|
+
|
138
|
+
# note that you can register possible events here as well,
|
139
|
+
# not only `llm_span.evaluate_event()`
|
140
|
+
llm_span.end(
|
141
|
+
output=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
|
+
}
|
150
|
+
)
|
112
151
|
span.end(output=poem)
|
113
152
|
return poem
|
114
153
|
|
115
154
|
|
116
|
-
t: TraceContext = trace(user_id="
|
155
|
+
t: TraceContext = trace(user_id="user123", session_id="session123", release="release")
|
117
156
|
main(t, topic="laminar flow")
|
118
|
-
t.end(success=True)
|
119
157
|
```
|
120
158
|
|
121
|
-
##
|
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:
|
122
190
|
|
123
|
-
-
|
124
|
-
-
|
125
|
-
-
|
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.
|
126
203
|
|
127
204
|
## Making Laminar pipeline calls
|
128
205
|
|
@@ -158,7 +235,3 @@ PipelineRunResponse(
|
|
158
235
|
run_id='53b012d5-5759-48a6-a9c5-0011610e3669'
|
159
236
|
)
|
160
237
|
```
|
161
|
-
|
162
|
-
## PROJECT_API_KEY
|
163
|
-
|
164
|
-
Read more [here](https://docs.lmnr.ai/api-reference/introduction#authentication) on how to get `PROJECT_API_KEY`.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "lmnr"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.2"
|
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.
|
14
|
+
version = "0.3.2"
|
15
15
|
description = "Python SDK for Laminar AI"
|
16
16
|
authors = ["lmnr.ai"]
|
17
17
|
readme = "README.md"
|
@@ -19,7 +19,6 @@ license = "Apache-2.0"
|
|
19
19
|
|
20
20
|
[tool.poetry.dependencies]
|
21
21
|
python = "^3.9"
|
22
|
-
black = "^24.4.2"
|
23
22
|
pydantic = "^2.7.4"
|
24
23
|
requests = "^2.32.3"
|
25
24
|
python-dotenv = "^1.0.1"
|
@@ -27,6 +26,9 @@ python-dotenv = "^1.0.1"
|
|
27
26
|
openai = "^1.41.1"
|
28
27
|
backoff = "^2.2.1"
|
29
28
|
|
29
|
+
[tool.poetry.group.dev.dependencies]
|
30
|
+
black = "^24.8.0"
|
31
|
+
|
30
32
|
[build-system]
|
31
33
|
requires = ["poetry-core"]
|
32
34
|
build-backend = "poetry.core.masonry.api"
|
@@ -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 *
|
@@ -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,
|
@@ -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
|
-
|
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
|
-
|
176
|
-
|
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.
|
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=
|
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(
|
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
|
-
|
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(
|
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[
|
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[
|
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
|
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
|
142
|
-
evaluate_events=evaluate_events
|
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
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
216
|
-
|
217
|
-
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
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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,
|
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
|
File without changes
|
File without changes
|