lmnr 0.4.10__tar.gz → 0.4.12__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.4.10 → lmnr-0.4.12}/PKG-INFO +130 -119
- lmnr-0.4.12/README.md +235 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/pyproject.toml +5 -7
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/__init__.py +1 -1
- lmnr-0.4.12/src/lmnr/cli.py +39 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/decorators.py +7 -9
- lmnr-0.4.12/src/lmnr/sdk/evaluations.py +347 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/laminar.py +81 -44
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/types.py +60 -21
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/utils.py +4 -5
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/__init__.py +3 -42
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/config/__init__.py +0 -4
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/decorators/base.py +16 -9
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/instruments.py +9 -4
- lmnr-0.4.12/src/lmnr/traceloop_sdk/tracing/attributes.py +8 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tracing/tracing.py +37 -205
- lmnr-0.4.10/README.md +0 -225
- lmnr-0.4.10/src/lmnr/sdk/evaluations.py +0 -178
- lmnr-0.4.10/src/lmnr/traceloop_sdk/metrics/__init__.py +0 -0
- lmnr-0.4.10/src/lmnr/traceloop_sdk/metrics/metrics.py +0 -176
- lmnr-0.4.10/src/lmnr/traceloop_sdk/tracing/manual.py +0 -57
- {lmnr-0.4.10 → lmnr-0.4.12}/LICENSE +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/__init__.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/sdk/log.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/.flake8 +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/.python-version +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/decorators/__init__.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/__init__.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_and_external_association_properties.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_association_properties.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_manual/test_manual_report.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_manual/test_resource_attributes.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_privacy_no_prompts/test_simple_workflow.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_prompt_management/test_prompt_management.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_sdk_initialization/test_resource_attributes.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_tasks/test_task_io_serialization_with_langchain.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_workflows/test_simple_aworkflow.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_workflows/test_simple_workflow.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/cassettes/test_workflows/test_streaming_workflow.yaml +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/conftest.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_association_properties.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_manual.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_nested_tasks.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_privacy_no_prompts.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_sdk_initialization.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_tasks.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tests/test_workflows.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tracing/__init__.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tracing/content_allow_list.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/tracing/context_manager.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/utils/__init__.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/utils/in_memory_span_exporter.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/utils/json_encoder.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/utils/package_check.py +0 -0
- {lmnr-0.4.10 → lmnr-0.4.12}/src/lmnr/traceloop_sdk/version.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lmnr
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.12
|
4
4
|
Summary: Python SDK for Laminar AI
|
5
5
|
License: Apache-2.0
|
6
6
|
Author: lmnr.ai
|
@@ -11,9 +11,9 @@ Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Requires-Dist: argparse (>=1.0,<2.0)
|
14
15
|
Requires-Dist: asyncio (>=3.0,<4.0)
|
15
16
|
Requires-Dist: backoff (>=2.0,<3.0)
|
16
|
-
Requires-Dist: colorama (>=0.4,<0.5)
|
17
17
|
Requires-Dist: deprecated (>=1.0,<2.0)
|
18
18
|
Requires-Dist: jinja2 (>=3.0,<4.0)
|
19
19
|
Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0)
|
@@ -54,111 +54,123 @@ Requires-Dist: pydantic (>=2.7,<3.0)
|
|
54
54
|
Requires-Dist: python-dotenv (>=1.0,<2.0)
|
55
55
|
Requires-Dist: requests (>=2.0,<3.0)
|
56
56
|
Requires-Dist: tenacity (>=8.0,<9.0)
|
57
|
+
Requires-Dist: tqdm (>=4.0,<5.0)
|
57
58
|
Description-Content-Type: text/markdown
|
58
59
|
|
59
60
|
# Laminar Python
|
60
61
|
|
61
|
-
|
62
|
+
Python SDK for [Laminar](https://www.lmnr.ai).
|
63
|
+
|
64
|
+
[Laminar](https://www.lmnr.ai) is an open-source platform for engineering LLM products. Trace, evaluate, annotate, and analyze LLM data. Bring LLM applications to production with confidence.
|
65
|
+
|
66
|
+
Check our [open-source repo](https://github.com/lmnr-ai/lmnr) and don't forget to star it ⭐
|
62
67
|
|
63
68
|
<a href="https://pypi.org/project/lmnr/">  </a>
|
64
69
|

|
65
70
|

|
66
71
|
|
67
72
|
|
68
|
-
|
69
73
|
## Quickstart
|
70
|
-
```sh
|
71
|
-
python3 -m venv .myenv
|
72
|
-
source .myenv/bin/activate # or use your favorite env management tool
|
73
74
|
|
75
|
+
First, install the package:
|
76
|
+
|
77
|
+
```sh
|
74
78
|
pip install lmnr
|
75
79
|
```
|
76
80
|
|
77
|
-
And
|
81
|
+
And then in the code
|
78
82
|
|
79
83
|
```python
|
80
|
-
from lmnr import Laminar as L
|
84
|
+
from lmnr import Laminar as L
|
81
85
|
|
82
|
-
L.initialize(project_api_key="<
|
86
|
+
L.initialize(project_api_key="<PROJECT_API_KEY>")
|
83
87
|
```
|
84
88
|
|
85
|
-
|
86
|
-
calls with OpenTelemetry-compatible instrumentation
|
87
|
-
|
88
|
-
You can pass an empty set as `instruments=set()` to disable any kind of automatic instrumentation.
|
89
|
-
Also if you want to automatically instrument all supported libraries, then pass `instruments=None` or don't pass `instruments` at all.
|
89
|
+
This will automatically instrument most of the LLM, Vector DB, and related
|
90
|
+
calls with OpenTelemetry-compatible instrumentation.
|
90
91
|
|
91
|
-
|
92
|
-
by TraceLoop. Also, we are grateful to Traceloop for implementing autoinstrumentations for many libraries.
|
93
|
-
|
94
|
-
### Project API key
|
95
|
-
|
96
|
-
Get the key from the settings page of your Laminar project ([Learn more](https://docs.lmnr.ai/api-reference/introduction#authentication)).
|
97
|
-
You can either pass it to `.initialize()` or set it to `.env` at the root of your package with the key `LMNR_PROJECT_API_KEY`.
|
92
|
+
Note that you need to only initialize Laminar once in your application.
|
98
93
|
|
99
94
|
## Instrumentation
|
100
95
|
|
101
|
-
|
102
|
-
or to trace other functions.
|
96
|
+
### Manual instrumentation
|
103
97
|
|
104
|
-
|
98
|
+
To instrument any function in your code, we provide a simple `@observe()` decorator.
|
99
|
+
This can be useful if you want to trace a request handler or a function which combines multiple LLM calls.
|
105
100
|
|
106
101
|
```python
|
107
102
|
import os
|
108
103
|
from openai import OpenAI
|
104
|
+
from lmnr import Laminar as L, Instruments
|
109
105
|
|
110
|
-
|
111
|
-
from lmnr import observe, Laminar as L, Instruments
|
112
|
-
L.initialize(project_api_key="<LMNR_PROJECT_API_KEY>", instruments={Instruments.OPENAI})
|
106
|
+
L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"])
|
113
107
|
|
114
108
|
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
115
109
|
|
116
|
-
|
117
|
-
def poem_writer(topic="turbulence"):
|
110
|
+
def poem_writer(topic: str):
|
118
111
|
prompt = f"write a poem about {topic}"
|
112
|
+
messages = [
|
113
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
114
|
+
{"role": "user", "content": prompt},
|
115
|
+
]
|
116
|
+
|
117
|
+
# OpenAI calls are still automatically instrumented
|
119
118
|
response = client.chat.completions.create(
|
120
119
|
model="gpt-4o",
|
121
|
-
messages=
|
122
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
123
|
-
{"role": "user", "content": prompt},
|
124
|
-
],
|
120
|
+
messages=messages,
|
125
121
|
)
|
126
122
|
poem = response.choices[0].message.content
|
123
|
+
|
127
124
|
return poem
|
128
125
|
|
129
|
-
|
126
|
+
@observe()
|
127
|
+
def generate_poems():
|
128
|
+
poem1 = poem_writer(topic="laminar flow")
|
129
|
+
L.event("is_poem_generated", True)
|
130
|
+
poem2 = poem_writer(topic="turbulence")
|
131
|
+
L.event("is_poem_generated", True)
|
132
|
+
poems = f"{poem1}\n\n---\n\n{poem2}"
|
133
|
+
return poems
|
130
134
|
```
|
131
135
|
|
132
|
-
|
133
|
-
|
134
|
-
Also, you can `Laminar.start_as_current_span` if you want to record a chunk of your code.
|
136
|
+
Also, you can use `Laminar.start_as_current_span` if you want to record a chunk of your code using `with` statement.
|
135
137
|
|
136
138
|
```python
|
137
|
-
|
138
|
-
L.
|
139
|
+
def handle_user_request(topic: str):
|
140
|
+
with L.start_as_current_span(name="poem_writer", input=topic):
|
141
|
+
...
|
142
|
+
|
143
|
+
poem = poem_writer(topic=topic)
|
144
|
+
|
145
|
+
...
|
146
|
+
|
147
|
+
# while within the span, you can attach laminar events to it
|
148
|
+
L.event("is_poem_generated", True)
|
139
149
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
144
|
-
{"role": "user", "content": prompt},
|
145
|
-
]
|
150
|
+
# Use set_span_output to record the output of the span
|
151
|
+
L.set_span_output(poem)
|
152
|
+
```
|
146
153
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
poem = response.choices[0].message.content
|
154
|
-
# while within the span, you can attach laminar events to it
|
155
|
-
L.event("event_name", "event_value")
|
154
|
+
### Automatic instrumentation
|
155
|
+
|
156
|
+
Laminar allows you to automatically instrument majority of the most popular LLM, Vector DB, database, requests, and other libraries.
|
157
|
+
|
158
|
+
If you want to automatically instrument a default set of libraries, then simply do NOT pass `instruments` argument to `.initialize()`.
|
159
|
+
See the full list of available instrumentations in the [enum](/src/lmnr/traceloop_sdk/instruments.py).
|
156
160
|
|
157
|
-
|
161
|
+
If you want to automatically instrument only specific LLM, Vector DB, or other
|
162
|
+
calls with OpenTelemetry-compatible instrumentation, then pass the appropriate instruments to `.initialize()`.
|
163
|
+
For example, if you want to only instrument OpenAI and Anthropic, then do the following:
|
164
|
+
|
165
|
+
```python
|
166
|
+
from lmnr import Laminar as L, Instruments
|
158
167
|
|
159
|
-
|
168
|
+
L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"], instruments={Instruments.OPENAI, Instruments.ANTHROPIC})
|
160
169
|
```
|
161
170
|
|
171
|
+
If you want to fully disable any kind of autoinstrumentation, pass an empty set as `instruments=set()` to `.initialize()`.
|
172
|
+
|
173
|
+
Autoinstrumentations are provided by Traceloop's [OpenLLMetry](https://github.com/traceloop/openllmetry).
|
162
174
|
|
163
175
|
## Sending events
|
164
176
|
|
@@ -186,6 +198,67 @@ L.event("topic alignment", topic in poem)
|
|
186
198
|
L.evaluate_event("excessive_wordiness", "check_wordy", {"text_input": poem})
|
187
199
|
```
|
188
200
|
|
201
|
+
## Evaluations
|
202
|
+
|
203
|
+
### Quickstart
|
204
|
+
|
205
|
+
Install the package:
|
206
|
+
|
207
|
+
```sh
|
208
|
+
pip install lmnr
|
209
|
+
```
|
210
|
+
|
211
|
+
Create a file named `my_first_eval.py` with the following code:
|
212
|
+
|
213
|
+
```python
|
214
|
+
from lmnr import evaluate
|
215
|
+
|
216
|
+
def write_poem(data):
|
217
|
+
return f"This is a good poem about {data['topic']}"
|
218
|
+
|
219
|
+
def contains_poem(output, target):
|
220
|
+
return 1 if output in target['poem'] else 0
|
221
|
+
|
222
|
+
# Evaluation data
|
223
|
+
data = [
|
224
|
+
{"data": {"topic": "flowers"}, "target": {"poem": "This is a good poem about flowers"}},
|
225
|
+
{"data": {"topic": "cars"}, "target": {"poem": "I like cars"}},
|
226
|
+
]
|
227
|
+
|
228
|
+
evaluate(
|
229
|
+
data=data,
|
230
|
+
executor=write_poem,
|
231
|
+
evaluators={
|
232
|
+
"containsPoem": contains_poem
|
233
|
+
}
|
234
|
+
)
|
235
|
+
```
|
236
|
+
|
237
|
+
Run the following commands:
|
238
|
+
|
239
|
+
```sh
|
240
|
+
export LMNR_PROJECT_API_KEY=<YOUR_PROJECT_API_KEY> # get from Laminar project settings
|
241
|
+
lmnr eval my_first_eval.py # run in the virtual environment where lmnr is installed
|
242
|
+
```
|
243
|
+
|
244
|
+
Visit the URL printed in the console to see the results.
|
245
|
+
|
246
|
+
### Overview
|
247
|
+
|
248
|
+
Bring rigor to the development of your LLM applications with evaluations.
|
249
|
+
|
250
|
+
You can run evaluations locally by providing executor (part of the logic used in your application) and evaluators (numeric scoring functions) to `evaluate` function.
|
251
|
+
|
252
|
+
`evaluate` takes in the following parameters:
|
253
|
+
- `data` – an array of `EvaluationDatapoint` objects, where each `EvaluationDatapoint` has two keys: `target` and `data`, each containing a key-value object. Alternatively, you can pass in dictionaries, and we will instantiate `EvaluationDatapoint`s with pydantic if possible
|
254
|
+
- `executor` – the logic you want to evaluate. This function must take `data` as the first argument, and produce any output. It can be both a function or an `async` function.
|
255
|
+
- `evaluators` – Dictionary which maps evaluator names to evaluators. Functions that take output of executor as the first argument, `target` as the second argument and produce a numeric scores. Each function can produce either a single number or `dict[str, int|float]` of scores. Each evaluator can be both a function or an `async` function.
|
256
|
+
- `name` – optional name for the evaluation. Automatically generated if not provided.
|
257
|
+
|
258
|
+
\* If you already have the outputs of executors you want to evaluate, you can specify the executor as an identity function, that takes in `data` and returns only needed value(s) from it.
|
259
|
+
|
260
|
+
[Read docs](https://docs.lmnr.ai/evaluations/introduction) to learn more about evaluations.
|
261
|
+
|
189
262
|
## Laminar pipelines as prompt chain managers
|
190
263
|
|
191
264
|
You can create Laminar pipelines in the UI and manage chains of LLM calls there.
|
@@ -220,65 +293,3 @@ PipelineRunResponse(
|
|
220
293
|
)
|
221
294
|
```
|
222
295
|
|
223
|
-
## Running offline evaluations on your data
|
224
|
-
|
225
|
-
You can evaluate your code with your own data and send it to Laminar using the `Evaluation` class.
|
226
|
-
|
227
|
-
Evaluation takes in the following parameters:
|
228
|
-
- `name` – the name of your evaluation. If no such evaluation exists in the project, it will be created. Otherwise, data will be pushed to the existing evaluation
|
229
|
-
- `data` – an array of `EvaluationDatapoint` objects, where each `EvaluationDatapoint` has two keys: `target` and `data`, each containing a key-value object. Alternatively, you can pass in dictionaries, and we will instantiate `EvaluationDatapoint`s with pydantic if possible
|
230
|
-
- `executor` – the logic you want to evaluate. This function must take `data` as the first argument, and produce any output. *
|
231
|
-
- `evaluators` – evaluaton logic. List of functions that take output of executor as the first argument, `target` as the second argument and produce a numeric scores. Each function can produce either a single number or `dict[str, int|float]` of scores.
|
232
|
-
|
233
|
-
\* If you already have the outputs of executors you want to evaluate, you can specify the executor as an identity function, that takes in `data` and returns only needed value(s) from it.
|
234
|
-
|
235
|
-
### Example
|
236
|
-
|
237
|
-
```python
|
238
|
-
from openai import AsyncOpenAI
|
239
|
-
import asyncio
|
240
|
-
import os
|
241
|
-
|
242
|
-
openai_client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
243
|
-
|
244
|
-
async def get_capital(data):
|
245
|
-
country = data["country"]
|
246
|
-
response = await openai_client.chat.completions.create(
|
247
|
-
model="gpt-4o-mini",
|
248
|
-
messages=[
|
249
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
250
|
-
{
|
251
|
-
"role": "user",
|
252
|
-
"content": f"What is the capital of {country}? Just name the "
|
253
|
-
"city and nothing else",
|
254
|
-
},
|
255
|
-
],
|
256
|
-
)
|
257
|
-
return response.choices[0].message.content.strip()
|
258
|
-
|
259
|
-
|
260
|
-
# Evaluation data
|
261
|
-
data = [
|
262
|
-
{"data": {"country": "Canada"}, "target": {"capital": "Ottawa"}},
|
263
|
-
{"data": {"country": "Germany"}, "target": {"capital": "Berlin"}},
|
264
|
-
{"data": {"country": "Tanzania"}, "target": {"capital": "Dodoma"}},
|
265
|
-
]
|
266
|
-
|
267
|
-
|
268
|
-
def evaluator_A(output, target):
|
269
|
-
return 1 if output == target["capital"] else 0
|
270
|
-
|
271
|
-
|
272
|
-
# Create an Evaluation instance
|
273
|
-
e = Evaluation(
|
274
|
-
name="py-evaluation-async",
|
275
|
-
data=data,
|
276
|
-
executor=get_capital,
|
277
|
-
evaluators=[evaluator_A],
|
278
|
-
project_api_key=os.environ["LMNR_PROJECT_API_KEY"],
|
279
|
-
)
|
280
|
-
|
281
|
-
# Run the evaluation
|
282
|
-
asyncio.run(e.run())
|
283
|
-
```
|
284
|
-
|
lmnr-0.4.12/README.md
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
# Laminar Python
|
2
|
+
|
3
|
+
Python SDK for [Laminar](https://www.lmnr.ai).
|
4
|
+
|
5
|
+
[Laminar](https://www.lmnr.ai) is an open-source platform for engineering LLM products. Trace, evaluate, annotate, and analyze LLM data. Bring LLM applications to production with confidence.
|
6
|
+
|
7
|
+
Check our [open-source repo](https://github.com/lmnr-ai/lmnr) and don't forget to star it ⭐
|
8
|
+
|
9
|
+
<a href="https://pypi.org/project/lmnr/">  </a>
|
10
|
+

|
11
|
+

|
12
|
+
|
13
|
+
|
14
|
+
## Quickstart
|
15
|
+
|
16
|
+
First, install the package:
|
17
|
+
|
18
|
+
```sh
|
19
|
+
pip install lmnr
|
20
|
+
```
|
21
|
+
|
22
|
+
And then in the code
|
23
|
+
|
24
|
+
```python
|
25
|
+
from lmnr import Laminar as L
|
26
|
+
|
27
|
+
L.initialize(project_api_key="<PROJECT_API_KEY>")
|
28
|
+
```
|
29
|
+
|
30
|
+
This will automatically instrument most of the LLM, Vector DB, and related
|
31
|
+
calls with OpenTelemetry-compatible instrumentation.
|
32
|
+
|
33
|
+
Note that you need to only initialize Laminar once in your application.
|
34
|
+
|
35
|
+
## Instrumentation
|
36
|
+
|
37
|
+
### Manual instrumentation
|
38
|
+
|
39
|
+
To instrument any function in your code, we provide a simple `@observe()` decorator.
|
40
|
+
This can be useful if you want to trace a request handler or a function which combines multiple LLM calls.
|
41
|
+
|
42
|
+
```python
|
43
|
+
import os
|
44
|
+
from openai import OpenAI
|
45
|
+
from lmnr import Laminar as L, Instruments
|
46
|
+
|
47
|
+
L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"])
|
48
|
+
|
49
|
+
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
50
|
+
|
51
|
+
def poem_writer(topic: str):
|
52
|
+
prompt = f"write a poem about {topic}"
|
53
|
+
messages = [
|
54
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
55
|
+
{"role": "user", "content": prompt},
|
56
|
+
]
|
57
|
+
|
58
|
+
# OpenAI calls are still automatically instrumented
|
59
|
+
response = client.chat.completions.create(
|
60
|
+
model="gpt-4o",
|
61
|
+
messages=messages,
|
62
|
+
)
|
63
|
+
poem = response.choices[0].message.content
|
64
|
+
|
65
|
+
return poem
|
66
|
+
|
67
|
+
@observe()
|
68
|
+
def generate_poems():
|
69
|
+
poem1 = poem_writer(topic="laminar flow")
|
70
|
+
L.event("is_poem_generated", True)
|
71
|
+
poem2 = poem_writer(topic="turbulence")
|
72
|
+
L.event("is_poem_generated", True)
|
73
|
+
poems = f"{poem1}\n\n---\n\n{poem2}"
|
74
|
+
return poems
|
75
|
+
```
|
76
|
+
|
77
|
+
Also, you can use `Laminar.start_as_current_span` if you want to record a chunk of your code using `with` statement.
|
78
|
+
|
79
|
+
```python
|
80
|
+
def handle_user_request(topic: str):
|
81
|
+
with L.start_as_current_span(name="poem_writer", input=topic):
|
82
|
+
...
|
83
|
+
|
84
|
+
poem = poem_writer(topic=topic)
|
85
|
+
|
86
|
+
...
|
87
|
+
|
88
|
+
# while within the span, you can attach laminar events to it
|
89
|
+
L.event("is_poem_generated", True)
|
90
|
+
|
91
|
+
# Use set_span_output to record the output of the span
|
92
|
+
L.set_span_output(poem)
|
93
|
+
```
|
94
|
+
|
95
|
+
### Automatic instrumentation
|
96
|
+
|
97
|
+
Laminar allows you to automatically instrument majority of the most popular LLM, Vector DB, database, requests, and other libraries.
|
98
|
+
|
99
|
+
If you want to automatically instrument a default set of libraries, then simply do NOT pass `instruments` argument to `.initialize()`.
|
100
|
+
See the full list of available instrumentations in the [enum](/src/lmnr/traceloop_sdk/instruments.py).
|
101
|
+
|
102
|
+
If you want to automatically instrument only specific LLM, Vector DB, or other
|
103
|
+
calls with OpenTelemetry-compatible instrumentation, then pass the appropriate instruments to `.initialize()`.
|
104
|
+
For example, if you want to only instrument OpenAI and Anthropic, then do the following:
|
105
|
+
|
106
|
+
```python
|
107
|
+
from lmnr import Laminar as L, Instruments
|
108
|
+
|
109
|
+
L.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"], instruments={Instruments.OPENAI, Instruments.ANTHROPIC})
|
110
|
+
```
|
111
|
+
|
112
|
+
If you want to fully disable any kind of autoinstrumentation, pass an empty set as `instruments=set()` to `.initialize()`.
|
113
|
+
|
114
|
+
Autoinstrumentations are provided by Traceloop's [OpenLLMetry](https://github.com/traceloop/openllmetry).
|
115
|
+
|
116
|
+
## Sending events
|
117
|
+
|
118
|
+
You can send events in two ways:
|
119
|
+
- `.event(name, value)` – for a pre-defined event with one of possible values.
|
120
|
+
- `.evaluate_event(name, evaluator, data)` – for an event that is evaluated by evaluator pipeline based on the data.
|
121
|
+
|
122
|
+
Note that to run an evaluate event, you need to crate an evaluator pipeline and create a target version for it.
|
123
|
+
|
124
|
+
Read our [docs](https://docs.lmnr.ai) to learn more about event types and how they are created and evaluated.
|
125
|
+
|
126
|
+
### Example
|
127
|
+
|
128
|
+
```python
|
129
|
+
from lmnr import Laminar as L
|
130
|
+
# ...
|
131
|
+
poem = response.choices[0].message.content
|
132
|
+
|
133
|
+
# this will register True or False value with Laminar
|
134
|
+
L.event("topic alignment", topic in poem)
|
135
|
+
|
136
|
+
# this will run the pipeline `check_wordy` with `poem` set as the value
|
137
|
+
# of `text_input` node, and write the result as an event with name
|
138
|
+
# "excessive_wordiness"
|
139
|
+
L.evaluate_event("excessive_wordiness", "check_wordy", {"text_input": poem})
|
140
|
+
```
|
141
|
+
|
142
|
+
## Evaluations
|
143
|
+
|
144
|
+
### Quickstart
|
145
|
+
|
146
|
+
Install the package:
|
147
|
+
|
148
|
+
```sh
|
149
|
+
pip install lmnr
|
150
|
+
```
|
151
|
+
|
152
|
+
Create a file named `my_first_eval.py` with the following code:
|
153
|
+
|
154
|
+
```python
|
155
|
+
from lmnr import evaluate
|
156
|
+
|
157
|
+
def write_poem(data):
|
158
|
+
return f"This is a good poem about {data['topic']}"
|
159
|
+
|
160
|
+
def contains_poem(output, target):
|
161
|
+
return 1 if output in target['poem'] else 0
|
162
|
+
|
163
|
+
# Evaluation data
|
164
|
+
data = [
|
165
|
+
{"data": {"topic": "flowers"}, "target": {"poem": "This is a good poem about flowers"}},
|
166
|
+
{"data": {"topic": "cars"}, "target": {"poem": "I like cars"}},
|
167
|
+
]
|
168
|
+
|
169
|
+
evaluate(
|
170
|
+
data=data,
|
171
|
+
executor=write_poem,
|
172
|
+
evaluators={
|
173
|
+
"containsPoem": contains_poem
|
174
|
+
}
|
175
|
+
)
|
176
|
+
```
|
177
|
+
|
178
|
+
Run the following commands:
|
179
|
+
|
180
|
+
```sh
|
181
|
+
export LMNR_PROJECT_API_KEY=<YOUR_PROJECT_API_KEY> # get from Laminar project settings
|
182
|
+
lmnr eval my_first_eval.py # run in the virtual environment where lmnr is installed
|
183
|
+
```
|
184
|
+
|
185
|
+
Visit the URL printed in the console to see the results.
|
186
|
+
|
187
|
+
### Overview
|
188
|
+
|
189
|
+
Bring rigor to the development of your LLM applications with evaluations.
|
190
|
+
|
191
|
+
You can run evaluations locally by providing executor (part of the logic used in your application) and evaluators (numeric scoring functions) to `evaluate` function.
|
192
|
+
|
193
|
+
`evaluate` takes in the following parameters:
|
194
|
+
- `data` – an array of `EvaluationDatapoint` objects, where each `EvaluationDatapoint` has two keys: `target` and `data`, each containing a key-value object. Alternatively, you can pass in dictionaries, and we will instantiate `EvaluationDatapoint`s with pydantic if possible
|
195
|
+
- `executor` – the logic you want to evaluate. This function must take `data` as the first argument, and produce any output. It can be both a function or an `async` function.
|
196
|
+
- `evaluators` – Dictionary which maps evaluator names to evaluators. Functions that take output of executor as the first argument, `target` as the second argument and produce a numeric scores. Each function can produce either a single number or `dict[str, int|float]` of scores. Each evaluator can be both a function or an `async` function.
|
197
|
+
- `name` – optional name for the evaluation. Automatically generated if not provided.
|
198
|
+
|
199
|
+
\* If you already have the outputs of executors you want to evaluate, you can specify the executor as an identity function, that takes in `data` and returns only needed value(s) from it.
|
200
|
+
|
201
|
+
[Read docs](https://docs.lmnr.ai/evaluations/introduction) to learn more about evaluations.
|
202
|
+
|
203
|
+
## Laminar pipelines as prompt chain managers
|
204
|
+
|
205
|
+
You can create Laminar pipelines in the UI and manage chains of LLM calls there.
|
206
|
+
|
207
|
+
After you are ready to use your pipeline in your code, deploy it in Laminar by selecting the target version for the pipeline.
|
208
|
+
|
209
|
+
Once your pipeline target is set, you can call it from Python in just a few lines.
|
210
|
+
|
211
|
+
Example use:
|
212
|
+
|
213
|
+
```python
|
214
|
+
from lmnr import Laminar as L
|
215
|
+
|
216
|
+
L.initialize('<YOUR_PROJECT_API_KEY>', instruments=set())
|
217
|
+
|
218
|
+
result = l.run(
|
219
|
+
pipeline = 'my_pipeline_name',
|
220
|
+
inputs = {'input_node_name': 'some_value'},
|
221
|
+
# all environment variables
|
222
|
+
env = {'OPENAI_API_KEY': 'sk-some-key'},
|
223
|
+
)
|
224
|
+
```
|
225
|
+
|
226
|
+
Resulting in:
|
227
|
+
|
228
|
+
```python
|
229
|
+
>>> result
|
230
|
+
PipelineRunResponse(
|
231
|
+
outputs={'output': {'value': [ChatMessage(role='user', content='hello')]}},
|
232
|
+
# useful to locate your trace
|
233
|
+
run_id='53b012d5-5759-48a6-a9c5-0011610e3669'
|
234
|
+
)
|
235
|
+
```
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "lmnr"
|
3
|
-
version = "0.4.
|
3
|
+
version = "0.4.12"
|
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.4.
|
14
|
+
version = "0.4.12"
|
15
15
|
description = "Python SDK for Laminar AI"
|
16
16
|
authors = ["lmnr.ai"]
|
17
17
|
readme = "README.md"
|
@@ -33,7 +33,6 @@ opentelemetry-instrumentation-sqlalchemy = "^0.48b0"
|
|
33
33
|
opentelemetry-instrumentation-urllib3 = "^0.48b0"
|
34
34
|
opentelemetry-instrumentation-threading = "^0.48b0"
|
35
35
|
opentelemetry-semantic-conventions-ai = "0.4.1"
|
36
|
-
colorama = "^0.4"
|
37
36
|
tenacity = "~=8.0"
|
38
37
|
jinja2 = "~=3.0"
|
39
38
|
deprecated = "~=1.0"
|
@@ -62,6 +61,8 @@ opentelemetry-instrumentation-weaviate = "^0.30.0"
|
|
62
61
|
opentelemetry-instrumentation-alephalpha = "^0.30.0"
|
63
62
|
opentelemetry-instrumentation-marqo = "^0.30.0"
|
64
63
|
opentelemetry-instrumentation-groq = "^0.30.0"
|
64
|
+
tqdm = "~=4.0"
|
65
|
+
argparse = "~=1.0"
|
65
66
|
|
66
67
|
[tool.poetry.group.dev.dependencies]
|
67
68
|
autopep8 = "^2.2.0"
|
@@ -83,11 +84,8 @@ langchain-openai = "^0.1.15"
|
|
83
84
|
requires = ["poetry-core"]
|
84
85
|
build-backend = "poetry.core.masonry.api"
|
85
86
|
|
86
|
-
[project.entry-points.console_scripts]
|
87
|
-
lmnr = "lmnr.cli.cli:cli"
|
88
|
-
|
89
87
|
[tool.poetry.scripts]
|
90
|
-
lmnr = "lmnr.cli
|
88
|
+
lmnr = "lmnr.cli:cli"
|
91
89
|
|
92
90
|
[project.optional-dependencies]
|
93
91
|
test = ["pytest"]
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from argparse import ArgumentParser
|
2
|
+
import asyncio
|
3
|
+
import importlib
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
|
7
|
+
from lmnr.sdk.evaluations import set_global_evaluation
|
8
|
+
|
9
|
+
|
10
|
+
# TODO: Refactor this code
|
11
|
+
async def run_evaluation(args):
|
12
|
+
sys.path.insert(0, os.getcwd())
|
13
|
+
|
14
|
+
with set_global_evaluation(True):
|
15
|
+
file = os.path.abspath(args.file)
|
16
|
+
|
17
|
+
spec = importlib.util.spec_from_file_location("run_eval", file)
|
18
|
+
mod = importlib.util.module_from_spec(spec)
|
19
|
+
spec.loader.exec_module(mod)
|
20
|
+
|
21
|
+
from lmnr.sdk.evaluations import _evaluation
|
22
|
+
evaluation = _evaluation
|
23
|
+
await evaluation.run()
|
24
|
+
|
25
|
+
|
26
|
+
def cli():
|
27
|
+
parser = ArgumentParser(
|
28
|
+
prog="lmnr",
|
29
|
+
description="CLI for Laminar",
|
30
|
+
)
|
31
|
+
|
32
|
+
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
|
33
|
+
|
34
|
+
parser_eval = subparsers.add_parser("eval", description="Run an evaluation")
|
35
|
+
parser_eval.add_argument("file", help="A file containing the evaluation to run")
|
36
|
+
parser_eval.set_defaults(func=run_evaluation)
|
37
|
+
|
38
|
+
parsed = parser.parse_args()
|
39
|
+
asyncio.run(parsed.func(parsed))
|