lmnr 0.4.10__py3-none-any.whl → 0.4.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lmnr
3
- Version: 0.4.10
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
- OpenTelemetry log sender for [Laminar](https://github.com/lmnr-ai/lmnr) for Python code.
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/"> ![PyPI - Version](https://img.shields.io/pypi/v/lmnr?label=lmnr&logo=pypi&logoColor=3775A9) </a>
64
69
  ![PyPI - Downloads](https://img.shields.io/pypi/dm/lmnr)
65
70
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/lmnr)
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 the in your main Python file
81
+ And then in the code
78
82
 
79
83
  ```python
80
- from lmnr import Laminar as L, Instruments
84
+ from lmnr import Laminar as L
81
85
 
82
- L.initialize(project_api_key="<LMNR_PROJECT_API_KEY>", instruments={Instruments.OPENAI, Instruments.ANTHROPIC})
86
+ L.initialize(project_api_key="<PROJECT_API_KEY>")
83
87
  ```
84
88
 
85
- If you want to automatically instrument particular LLM, Vector DB, and related
86
- calls with OpenTelemetry-compatible instrumentation, then pass the appropriate instruments to `.initialize()`.
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
- Our code is based on the [OpenLLMetry](https://github.com/traceloop/openllmetry), open-source package
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
- In addition to automatic instrumentation, we provide a simple `@observe()` decorator, if you want more fine-grained tracing
102
- or to trace other functions.
96
+ ### Manual instrumentation
103
97
 
104
- ### Example
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
- @observe() # annotate all functions you want to trace
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
- print(poem_writer(topic="laminar flow"))
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
- ### Manual instrumentation
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
- from lmnr import observe, Laminar as L, Instruments
138
- L.initialize(project_api_key="<LMNR_PROJECT_API_KEY>", instruments={Instruments.OPENAI})
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
- def poem_writer(topic="turbulence"):
141
- prompt = f"write a poem about {topic}"
142
- messages = [
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
- with L.start_as_current_span(name="poem_writer", input=messages):
148
- # OpenAI calls are still automatically instrumented with OpenLLMetry
149
- response = client.chat.completions.create(
150
- model="gpt-4o",
151
- messages=messages,
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
- L.set_span_output(poem) # set an output
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
- return poem
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
-
@@ -1,20 +1,19 @@
1
- lmnr/__init__.py,sha256=bA1f7JsEdSdU93HTz3SQLSanq-UgZGvb5I2OE0CWGR8,233
1
+ lmnr/__init__.py,sha256=5Ks8UIicCzCBgwSz0MOX3I7jVruPMUO3SmxIwUoODzQ,231
2
+ lmnr/cli.py,sha256=Ptvm5dsNLKUY5lwnN8XkT5GtCYjzpRNi2WvefknB3OQ,1079
2
3
  lmnr/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- lmnr/sdk/decorators.py,sha256=W46diLcINe0HAhxktrjbfQnaIfklSb0AydBHHxiko9U,2314
4
- lmnr/sdk/evaluations.py,sha256=EaRcwbdXxj4w2yzak1xFv-YhDuxRVentQcJ-CypBoH0,6307
5
- lmnr/sdk/laminar.py,sha256=M8HdP6ZYJHdngUVrGj4GMZxz_EZyx3woHm-UpfWmIvs,18439
4
+ lmnr/sdk/decorators.py,sha256=ii7Bqp6flaanIFSK6M1_ZZV-izp4o3hkR1MmY7wnFQQ,2227
5
+ lmnr/sdk/evaluations.py,sha256=M3LlgYphMEJ-MdZ-UN6jzpWk1G-HHrTmzTQicZoEgwk,13590
6
+ lmnr/sdk/laminar.py,sha256=3LqzqhsSOHxz11_lxAdvqy_awtOnTdPTeYxYEZ3F4Go,19407
6
7
  lmnr/sdk/log.py,sha256=EgAMY77Zn1bv1imCqrmflD3imoAJ2yveOkIcrIP3e98,1170
7
- lmnr/sdk/types.py,sha256=w7BJsoEPHiNps62cQt3Hd6tEZ7ZFCKRTPzcwdD6rNak,4050
8
- lmnr/sdk/utils.py,sha256=ZsGJ86tq8lIbvOhSb1gJWH5K3GylO_lgX68FN6rG2nM,3358
8
+ lmnr/sdk/types.py,sha256=QB89q6WeN715x15ukoRVufXk6FSP_1pGn8QsUSIJG5U,5062
9
+ lmnr/sdk/utils.py,sha256=s81p6uJehgJSaLWy3sR5fTpEDH7vzn3i_UujUHChl6M,3346
9
10
  lmnr/traceloop_sdk/.flake8,sha256=bCxuDlGx3YQ55QHKPiGJkncHanh9qGjQJUujcFa3lAU,150
10
11
  lmnr/traceloop_sdk/.python-version,sha256=9OLQBQVbD4zE4cJsPePhnAfV_snrPSoqEQw-PXgPMOs,6
11
- lmnr/traceloop_sdk/__init__.py,sha256=J-zVw6j0DmceVvJVZXAFcCzN_scz9hB3X17NQgPMgOg,4420
12
- lmnr/traceloop_sdk/config/__init__.py,sha256=EGN3ixOt_ORbMxqaQdLaC14kmO-gyG4mnGJ2GfN-R-E,364
12
+ lmnr/traceloop_sdk/__init__.py,sha256=hp3q1OsFaGgaQCEanJrL38BJN32hWqCNVCSjYpndEsY,2957
13
+ lmnr/traceloop_sdk/config/__init__.py,sha256=DliMGp2NjYAqRFLKpWQPUKjGMHRO8QsVfazBA1qENQ8,248
13
14
  lmnr/traceloop_sdk/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- lmnr/traceloop_sdk/decorators/base.py,sha256=wcqXF0iVQgRXMyWTcJ5QvL_6q2y_gttwsX8dllmAtWM,4891
15
- lmnr/traceloop_sdk/instruments.py,sha256=G5EFAbpc20WD3M6xK6rlbj-Yy_r_f1m3gidY6UXzSRQ,701
16
- lmnr/traceloop_sdk/metrics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- lmnr/traceloop_sdk/metrics/metrics.py,sha256=AlQ2a2os1WcZbfBd155u_UzBbPrbuPia6O_HbojV9Wc,5055
15
+ lmnr/traceloop_sdk/decorators/base.py,sha256=5YCzAErlhv1bMDO1C9LBlLWYk3bwku0RLjGLR-TkR4c,5128
16
+ lmnr/traceloop_sdk/instruments.py,sha256=oMvIASueW3GeChpjIdH-DD9aFBVB8OtHZ0HawppTrlI,942
18
17
  lmnr/traceloop_sdk/tests/__init__.py,sha256=RYnG0-8zbXL0-2Ste1mEBf5sN4d_rQjGTCgPBuaZC74,20
19
18
  lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_and_external_association_properties.yaml,sha256=26g0wRA0juicHg_XrhcE8H4vhs1lawDs0o0aLFn-I7w,3103
20
19
  lmnr/traceloop_sdk/tests/cassettes/test_association_properties/test_langchain_association_properties.yaml,sha256=FNlSWlYCsWc3w7UPZzfGjDnxS3gAOhL-kpsu4BTxsDE,3061
@@ -36,17 +35,17 @@ lmnr/traceloop_sdk/tests/test_sdk_initialization.py,sha256=fRaf6lrxFzJIN94P1Tav_
36
35
  lmnr/traceloop_sdk/tests/test_tasks.py,sha256=xlEx8BKp4yG83SCjK5WkPGfyC33JSrx4h8VyjVwGbgw,906
37
36
  lmnr/traceloop_sdk/tests/test_workflows.py,sha256=RVcfY3WAFIDZC15-aSua21aoQyYeWE7KypDyUsm-2EM,9372
38
37
  lmnr/traceloop_sdk/tracing/__init__.py,sha256=Ckq7zCM26VdJVB5tIZv0GTPyMZKyfso_KWD5yPHaqdo,66
38
+ lmnr/traceloop_sdk/tracing/attributes.py,sha256=Rvglt_2IeZzKJ-mumrp9qAtTwHza34CrNgv4CvYihk0,221
39
39
  lmnr/traceloop_sdk/tracing/content_allow_list.py,sha256=3feztm6PBWNelc8pAZUcQyEGyeSpNiVKjOaDk65l2ps,846
40
40
  lmnr/traceloop_sdk/tracing/context_manager.py,sha256=csVlB6kDmbgSPsROHwnddvGGblx55v6lJMRj0wsSMQM,304
41
- lmnr/traceloop_sdk/tracing/manual.py,sha256=RPwEreHHdzmw7g15u4G21GqhHOvRp7d72ylQNLG1jRM,1841
42
- lmnr/traceloop_sdk/tracing/tracing.py,sha256=VFrf5D6CC3DquLy_19_5I_L_w1kO2X61KvPW0XD26-k,42347
41
+ lmnr/traceloop_sdk/tracing/tracing.py,sha256=8plGdX6nErrPERgYXQDQRyBTtVgv2Ies46ph-msLLQE,35443
43
42
  lmnr/traceloop_sdk/utils/__init__.py,sha256=pNhf0G3vTd5ccoc03i1MXDbricSaiqCbi1DLWhSekK8,604
44
43
  lmnr/traceloop_sdk/utils/in_memory_span_exporter.py,sha256=H_4TRaThMO1H6vUQ0OpQvzJk_fZH0OOsRAM1iZQXsR8,2112
45
44
  lmnr/traceloop_sdk/utils/json_encoder.py,sha256=dK6b_axr70IYL7Vv-bu4wntvDDuyntoqsHaddqX7P58,463
46
45
  lmnr/traceloop_sdk/utils/package_check.py,sha256=TZSngzJOpFhfUZLXIs38cpMxQiZSmp0D-sCrIyhz7BA,251
47
46
  lmnr/traceloop_sdk/version.py,sha256=OlatFEFA4ttqSSIiV8jdE-sq3KG5zu2hnC4B4mzWF3s,23
48
- lmnr-0.4.10.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
49
- lmnr-0.4.10.dist-info/METADATA,sha256=VPXsfYwAy1uTv_qucCqgAmyGyZMngr0HuysSSdpX8Jw,10999
50
- lmnr-0.4.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
51
- lmnr-0.4.10.dist-info/entry_points.txt,sha256=Qg7ZRax4k-rcQsZ26XRYQ8YFSBiyY2PNxYfq4a6PYXI,41
52
- lmnr-0.4.10.dist-info/RECORD,,
47
+ lmnr-0.4.12.dist-info/LICENSE,sha256=67b_wJHVV1CBaWkrKFWU1wyqTPSdzH77Ls-59631COg,10411
48
+ lmnr-0.4.12.dist-info/METADATA,sha256=jlbEUKz0NWRyf5lqRWTGlN88v6beI_lUnoTtcY91q4o,11233
49
+ lmnr-0.4.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
+ lmnr-0.4.12.dist-info/entry_points.txt,sha256=K1jE20ww4jzHNZLnsfWBvU3YKDGBgbOiYG5Y7ivQcq4,37
51
+ lmnr-0.4.12.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ lmnr=lmnr.cli:cli
3
+
File without changes
@@ -1,176 +0,0 @@
1
- from collections.abc import Sequence
2
- from typing import Dict
3
-
4
- from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
5
- OTLPMetricExporter as GRPCExporter,
6
- )
7
- from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
8
- OTLPMetricExporter as HTTPExporter,
9
- )
10
- from opentelemetry.semconv_ai import Meters
11
- from opentelemetry.sdk.metrics import MeterProvider
12
- from opentelemetry.sdk.metrics.export import (
13
- PeriodicExportingMetricReader,
14
- MetricExporter,
15
- )
16
- from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
17
- from opentelemetry.sdk.resources import Resource
18
-
19
- from opentelemetry import metrics
20
-
21
-
22
- class MetricsWrapper(object):
23
- resource_attributes: dict = {}
24
- endpoint: str = None
25
- # if it needs headers?
26
- headers: Dict[str, str] = {}
27
- __metrics_exporter: MetricExporter = None
28
- __metrics_provider: MeterProvider = None
29
-
30
- def __new__(cls, exporter: MetricExporter = None) -> "MetricsWrapper":
31
- if not hasattr(cls, "instance"):
32
- obj = cls.instance = super(MetricsWrapper, cls).__new__(cls)
33
- if not MetricsWrapper.endpoint:
34
- return obj
35
-
36
- obj.__metrics_exporter = (
37
- exporter
38
- if exporter
39
- else init_metrics_exporter(
40
- MetricsWrapper.endpoint, MetricsWrapper.headers
41
- )
42
- )
43
-
44
- obj.__metrics_provider = init_metrics_provider(
45
- obj.__metrics_exporter, MetricsWrapper.resource_attributes
46
- )
47
-
48
- return cls.instance
49
-
50
- @staticmethod
51
- def set_static_params(
52
- resource_attributes: dict,
53
- endpoint: str,
54
- headers: Dict[str, str],
55
- ) -> None:
56
- MetricsWrapper.resource_attributes = resource_attributes
57
- MetricsWrapper.endpoint = endpoint
58
- MetricsWrapper.headers = headers
59
-
60
-
61
- def init_metrics_exporter(endpoint: str, headers: Dict[str, str]) -> MetricExporter:
62
- if "http" in endpoint.lower() or "https" in endpoint.lower():
63
- return HTTPExporter(endpoint=f"{endpoint}/v1/metrics", headers=headers)
64
- else:
65
- return GRPCExporter(endpoint=endpoint, headers=headers)
66
-
67
-
68
- def init_metrics_provider(
69
- exporter: MetricExporter, resource_attributes: dict = None
70
- ) -> MeterProvider:
71
- resource = (
72
- Resource.create(resource_attributes)
73
- if resource_attributes
74
- else Resource.create()
75
- )
76
- reader = PeriodicExportingMetricReader(exporter)
77
- provider = MeterProvider(
78
- metric_readers=[reader],
79
- resource=resource,
80
- views=metric_views(),
81
- )
82
-
83
- metrics.set_meter_provider(provider)
84
- return provider
85
-
86
-
87
- def metric_views() -> Sequence[View]:
88
- return [
89
- View(
90
- instrument_name=Meters.LLM_TOKEN_USAGE,
91
- aggregation=ExplicitBucketHistogramAggregation(
92
- [
93
- 0.01,
94
- 0.02,
95
- 0.04,
96
- 0.08,
97
- 0.16,
98
- 0.32,
99
- 0.64,
100
- 1.28,
101
- 2.56,
102
- 5.12,
103
- 10.24,
104
- 20.48,
105
- 40.96,
106
- 81.92,
107
- ]
108
- ),
109
- ),
110
- View(
111
- instrument_name=Meters.LLM_OPERATION_DURATION,
112
- aggregation=ExplicitBucketHistogramAggregation(
113
- [
114
- 1,
115
- 4,
116
- 16,
117
- 64,
118
- 256,
119
- 1024,
120
- 4096,
121
- 16384,
122
- 65536,
123
- 262144,
124
- 1048576,
125
- 4194304,
126
- 16777216,
127
- 67108864,
128
- ]
129
- ),
130
- ),
131
- View(
132
- instrument_name=Meters.PINECONE_DB_QUERY_DURATION,
133
- aggregation=ExplicitBucketHistogramAggregation(
134
- [
135
- 0.01,
136
- 0.02,
137
- 0.04,
138
- 0.08,
139
- 0.16,
140
- 0.32,
141
- 0.64,
142
- 1.28,
143
- 2.56,
144
- 5.12,
145
- 10.24,
146
- 20.48,
147
- 40.96,
148
- 81.92,
149
- ]
150
- ),
151
- ),
152
- View(
153
- instrument_name=Meters.PINECONE_DB_QUERY_SCORES,
154
- aggregation=ExplicitBucketHistogramAggregation(
155
- [
156
- -1,
157
- -0.875,
158
- -0.75,
159
- -0.625,
160
- -0.5,
161
- -0.375,
162
- -0.25,
163
- -0.125,
164
- 0,
165
- 0.125,
166
- 0.25,
167
- 0.375,
168
- 0.5,
169
- 0.625,
170
- 0.75,
171
- 0.875,
172
- 1,
173
- ]
174
- ),
175
- ),
176
- ]
@@ -1,57 +0,0 @@
1
- from contextlib import contextmanager
2
- from opentelemetry.semconv_ai import SpanAttributes
3
- from opentelemetry.trace import Span
4
- from pydantic import BaseModel
5
- from lmnr.traceloop_sdk.tracing.context_manager import get_tracer
6
-
7
-
8
- class LLMMessage(BaseModel):
9
- role: str
10
- content: str
11
-
12
-
13
- class LLMUsage(BaseModel):
14
- prompt_tokens: int
15
- completion_tokens: int
16
- total_tokens: int
17
-
18
-
19
- class LLMSpan:
20
- _span: Span = None
21
-
22
- def __init__(self, span: Span):
23
- self._span = span
24
- pass
25
-
26
- def report_request(self, model: str, messages: list[LLMMessage]):
27
- self._span.set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model)
28
- for idx, message in enumerate(messages):
29
- self._span.set_attribute(
30
- f"{SpanAttributes.LLM_PROMPTS}.{idx}.role", message.role
31
- )
32
- self._span.set_attribute(
33
- f"{SpanAttributes.LLM_PROMPTS}.{idx}.content", message.content
34
- )
35
-
36
- def report_response(self, model: str, completions: list[str]):
37
- self._span.set_attribute(SpanAttributes.LLM_RESPONSE_MODEL, model)
38
- for idx, completion in enumerate(completions):
39
- self._span.set_attribute(
40
- f"{SpanAttributes.LLM_COMPLETIONS}.{idx}.role", "assistant"
41
- )
42
- self._span.set_attribute(
43
- f"{SpanAttributes.LLM_COMPLETIONS}.{idx}", completion
44
- )
45
-
46
-
47
- @contextmanager
48
- def track_llm_call(vendor: str, type: str):
49
- with get_tracer() as tracer:
50
- with tracer.start_as_current_span(name=f"{vendor}.{type}") as span:
51
- span.set_attribute(SpanAttributes.LLM_SYSTEM, vendor)
52
- span.set_attribute(SpanAttributes.LLM_REQUEST_TYPE, type)
53
- llm_span = LLMSpan(span)
54
- try:
55
- yield llm_span
56
- finally:
57
- span.end()
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- lmnr=lmnr.cli.cli:cli
3
-
File without changes
File without changes