lmnr 0.3.7__py3-none-any.whl → 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lmnr/__init__.py +3 -6
- lmnr/sdk/decorators.py +55 -267
- lmnr/sdk/evaluations.py +163 -0
- lmnr/sdk/laminar.py +447 -0
- lmnr/sdk/log.py +39 -0
- lmnr/sdk/types.py +55 -3
- lmnr/sdk/utils.py +10 -11
- lmnr-0.4.1.dist-info/METADATA +214 -0
- lmnr-0.4.1.dist-info/RECORD +13 -0
- lmnr/sdk/client.py +0 -161
- lmnr/sdk/collector.py +0 -177
- lmnr/sdk/constants.py +0 -1
- lmnr/sdk/context.py +0 -483
- lmnr/sdk/interface.py +0 -316
- lmnr/sdk/providers/__init__.py +0 -2
- lmnr/sdk/providers/base.py +0 -28
- lmnr/sdk/providers/fallback.py +0 -154
- lmnr/sdk/providers/openai.py +0 -159
- lmnr/sdk/providers/utils.py +0 -33
- lmnr/sdk/tracing_types.py +0 -210
- lmnr/semantic_conventions/__init__.py +0 -0
- lmnr/semantic_conventions/gen_ai_spans.py +0 -48
- lmnr-0.3.7.dist-info/METADATA +0 -266
- lmnr-0.3.7.dist-info/RECORD +0 -23
- {lmnr-0.3.7.dist-info → lmnr-0.4.1.dist-info}/LICENSE +0 -0
- {lmnr-0.3.7.dist-info → lmnr-0.4.1.dist-info}/WHEEL +0 -0
- {lmnr-0.3.7.dist-info → lmnr-0.4.1.dist-info}/entry_points.txt +0 -0
lmnr/sdk/context.py
DELETED
@@ -1,483 +0,0 @@
|
|
1
|
-
from .collector import ThreadManager
|
2
|
-
from .client import Laminar
|
3
|
-
from .providers import Provider
|
4
|
-
from .providers.fallback import FallbackProvider
|
5
|
-
from .tracing_types import EvaluateEvent, Event, Span, Trace
|
6
|
-
from .types import PipelineRunResponse
|
7
|
-
from .utils import PROVIDER_NAME_TO_OBJECT, is_async_iterator, is_iterator
|
8
|
-
|
9
|
-
from contextvars import ContextVar
|
10
|
-
from typing import Any, AsyncGenerator, Generator, Literal, Optional, Union
|
11
|
-
import atexit
|
12
|
-
import datetime
|
13
|
-
import dotenv
|
14
|
-
import inspect
|
15
|
-
import logging
|
16
|
-
import os
|
17
|
-
import pydantic
|
18
|
-
import uuid
|
19
|
-
|
20
|
-
|
21
|
-
_lmnr_stack_context: ContextVar[list[Union[Span, Trace]]] = ContextVar(
|
22
|
-
"lmnr_stack_context", default=[]
|
23
|
-
)
|
24
|
-
_root_trace_id_context: ContextVar[Optional[str]] = ContextVar(
|
25
|
-
"root_trace_id_context", default=None
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
class LaminarContextManager:
|
30
|
-
_log = logging.getLogger("laminar.context_manager")
|
31
|
-
|
32
|
-
def __init__(
|
33
|
-
self,
|
34
|
-
project_api_key: str = None,
|
35
|
-
threads: int = 1,
|
36
|
-
max_task_queue_size: int = 1000,
|
37
|
-
env: dict[str, str] = {},
|
38
|
-
):
|
39
|
-
self.project_api_key = project_api_key or os.environ.get("LMNR_PROJECT_API_KEY")
|
40
|
-
if not self.project_api_key:
|
41
|
-
dotenv_path = dotenv.find_dotenv(usecwd=True)
|
42
|
-
self.project_api_key = dotenv.get_key(
|
43
|
-
dotenv_path=dotenv_path, key_to_get="LMNR_PROJECT_API_KEY"
|
44
|
-
)
|
45
|
-
self.laminar = Laminar(project_api_key=self.project_api_key)
|
46
|
-
self.thread_manager = ThreadManager(
|
47
|
-
client=self.laminar,
|
48
|
-
max_task_queue_size=max_task_queue_size,
|
49
|
-
threads=threads,
|
50
|
-
)
|
51
|
-
self.env = env
|
52
|
-
# atexit executes functions last in first out, so we want to make sure
|
53
|
-
# that we finalize the trace before thread manager is closed, so the updated
|
54
|
-
# trace is sent to the server
|
55
|
-
atexit.register(self._force_finalize_trace)
|
56
|
-
|
57
|
-
def observe_start(
|
58
|
-
self,
|
59
|
-
# span attributes
|
60
|
-
name: str,
|
61
|
-
input: Optional[Any] = None,
|
62
|
-
metadata: Optional[dict[str, Any]] = None,
|
63
|
-
attributes: Optional[dict[str, Any]] = None,
|
64
|
-
span_type: Literal["DEFAULT", "LLM"] = "DEFAULT",
|
65
|
-
# trace attributes
|
66
|
-
user_id: Optional[str] = None,
|
67
|
-
session_id: Optional[str] = None,
|
68
|
-
release: Optional[str] = None,
|
69
|
-
) -> Span:
|
70
|
-
trace_id = _root_trace_id_context.get()
|
71
|
-
if not trace_id:
|
72
|
-
session_id = session_id or str(uuid.uuid4())
|
73
|
-
trace_id = uuid.uuid4()
|
74
|
-
trace = self.update_trace(
|
75
|
-
id=trace_id,
|
76
|
-
user_id=user_id,
|
77
|
-
session_id=session_id,
|
78
|
-
release=release,
|
79
|
-
)
|
80
|
-
_root_trace_id_context.set(trace.id)
|
81
|
-
_lmnr_stack_context.set([trace])
|
82
|
-
|
83
|
-
parent = _lmnr_stack_context.get()[-1] if _lmnr_stack_context.get() else None
|
84
|
-
parent_span_id = parent.id if isinstance(parent, Span) else None
|
85
|
-
span = self.create_span(
|
86
|
-
name=name,
|
87
|
-
trace_id=trace_id,
|
88
|
-
input=input,
|
89
|
-
metadata=metadata,
|
90
|
-
attributes=attributes,
|
91
|
-
parent_span_id=parent_span_id,
|
92
|
-
span_type=span_type,
|
93
|
-
)
|
94
|
-
stack = _lmnr_stack_context.get()
|
95
|
-
_lmnr_stack_context.set(stack + [span])
|
96
|
-
return span
|
97
|
-
|
98
|
-
def observe_end(
|
99
|
-
self,
|
100
|
-
span: Span,
|
101
|
-
provider_name: str = None,
|
102
|
-
result: Optional[Any] = None,
|
103
|
-
metadata: Optional[dict[str, Any]] = None,
|
104
|
-
attributes: Optional[dict[str, Any]] = None,
|
105
|
-
error: Optional[Exception] = None,
|
106
|
-
) -> Any:
|
107
|
-
stack = _lmnr_stack_context.get()
|
108
|
-
if not stack:
|
109
|
-
return
|
110
|
-
provider = PROVIDER_NAME_TO_OBJECT.get(provider_name, FallbackProvider())
|
111
|
-
new_stack = stack[:-1]
|
112
|
-
_lmnr_stack_context.set(new_stack)
|
113
|
-
|
114
|
-
if len(new_stack) == 1 and isinstance(stack[0], Trace):
|
115
|
-
trace = stack[0]
|
116
|
-
self.update_trace(
|
117
|
-
id=trace.id,
|
118
|
-
user_id=trace.userId,
|
119
|
-
session_id=trace.sessionId,
|
120
|
-
release=trace.release,
|
121
|
-
metadata=metadata,
|
122
|
-
)
|
123
|
-
_root_trace_id_context.set(None)
|
124
|
-
_lmnr_stack_context.set([])
|
125
|
-
|
126
|
-
if error is not None:
|
127
|
-
self.update_current_trace(success=False)
|
128
|
-
|
129
|
-
if inspect.isgenerator(result) or is_iterator(result):
|
130
|
-
return self._collect_generator_result(
|
131
|
-
provider=provider,
|
132
|
-
generator=result,
|
133
|
-
span=span,
|
134
|
-
metadata=metadata,
|
135
|
-
attributes=attributes,
|
136
|
-
)
|
137
|
-
elif inspect.isasyncgen(result) or is_async_iterator(result):
|
138
|
-
return self._collect_async_generator_result(
|
139
|
-
provider=provider,
|
140
|
-
generator=result,
|
141
|
-
span=span,
|
142
|
-
metadata=metadata,
|
143
|
-
attributes=attributes,
|
144
|
-
)
|
145
|
-
if span.spanType == "LLM" and error is None:
|
146
|
-
attributes = self._extract_llm_attributes_from_response(
|
147
|
-
provider=provider, response=result
|
148
|
-
)
|
149
|
-
|
150
|
-
return self._finalize_span(
|
151
|
-
span,
|
152
|
-
provider=provider,
|
153
|
-
result=error or result,
|
154
|
-
metadata=metadata,
|
155
|
-
attributes=attributes,
|
156
|
-
)
|
157
|
-
|
158
|
-
def update_current_span(
|
159
|
-
self,
|
160
|
-
metadata: Optional[dict[str, Any]] = None,
|
161
|
-
attributes: Optional[dict[str, Any]] = None,
|
162
|
-
evaluate_events: list[EvaluateEvent] = None,
|
163
|
-
events: list[Event] = None,
|
164
|
-
override: bool = False,
|
165
|
-
):
|
166
|
-
stack = _lmnr_stack_context.get()
|
167
|
-
if not stack:
|
168
|
-
return
|
169
|
-
span = stack[-1]
|
170
|
-
new_metadata = (
|
171
|
-
metadata if override else {**(span.metadata or {}), **(metadata or {})}
|
172
|
-
)
|
173
|
-
new_evaluate_events = (
|
174
|
-
evaluate_events
|
175
|
-
if override
|
176
|
-
else span.evaluateEvents + (evaluate_events or [])
|
177
|
-
)
|
178
|
-
new_events = events if override else span.events + (events or [])
|
179
|
-
new_attributes = (
|
180
|
-
attributes
|
181
|
-
if override
|
182
|
-
else {**(span.attributes or {}), **(attributes or {})}
|
183
|
-
)
|
184
|
-
self.update_span(
|
185
|
-
span=span,
|
186
|
-
metadata=new_metadata,
|
187
|
-
evaluate_events=new_evaluate_events,
|
188
|
-
events=new_events,
|
189
|
-
attributes=new_attributes,
|
190
|
-
)
|
191
|
-
|
192
|
-
def update_current_trace(
|
193
|
-
self,
|
194
|
-
user_id: Optional[str] = None,
|
195
|
-
session_id: Optional[str] = None,
|
196
|
-
release: Optional[str] = None,
|
197
|
-
metadata: Optional[dict[str, Any]] = None,
|
198
|
-
success: bool = True,
|
199
|
-
):
|
200
|
-
existing_trace = (
|
201
|
-
_lmnr_stack_context.get()[0] if _lmnr_stack_context.get() else None
|
202
|
-
)
|
203
|
-
if not existing_trace:
|
204
|
-
return
|
205
|
-
self.update_trace(
|
206
|
-
id=existing_trace.id,
|
207
|
-
user_id=user_id or existing_trace.userId,
|
208
|
-
session_id=session_id or existing_trace.sessionId,
|
209
|
-
release=release or existing_trace.release,
|
210
|
-
metadata=metadata or existing_trace.metadata,
|
211
|
-
success=success if success is not None else existing_trace.success,
|
212
|
-
)
|
213
|
-
|
214
|
-
def update_trace(
|
215
|
-
self,
|
216
|
-
id: uuid.UUID,
|
217
|
-
user_id: Optional[str] = None,
|
218
|
-
session_id: Optional[str] = None,
|
219
|
-
release: Optional[str] = None,
|
220
|
-
metadata: Optional[dict[str, Any]] = None,
|
221
|
-
success: bool = True,
|
222
|
-
) -> Trace:
|
223
|
-
trace = Trace(
|
224
|
-
id=id,
|
225
|
-
user_id=user_id,
|
226
|
-
session_id=session_id,
|
227
|
-
release=release,
|
228
|
-
metadata=metadata,
|
229
|
-
success=success,
|
230
|
-
)
|
231
|
-
self._add_observation(trace)
|
232
|
-
return trace
|
233
|
-
|
234
|
-
def create_span(
|
235
|
-
self,
|
236
|
-
name: str,
|
237
|
-
trace_id: uuid.UUID,
|
238
|
-
start_time: Optional[datetime.datetime] = None,
|
239
|
-
span_type: Literal["DEFAULT", "LLM"] = "DEFAULT",
|
240
|
-
id: Optional[uuid.UUID] = None,
|
241
|
-
parent_span_id: Optional[uuid.UUID] = None,
|
242
|
-
input: Optional[Any] = None,
|
243
|
-
metadata: Optional[dict[str, Any]] = None,
|
244
|
-
attributes: Optional[dict[str, Any]] = None,
|
245
|
-
evaluate_events: Optional[list[EvaluateEvent]] = None,
|
246
|
-
events: Optional[list[Event]] = None,
|
247
|
-
) -> Span:
|
248
|
-
"""Internal method to create a span object. Use `ObservationContext.span` instead."""
|
249
|
-
span = Span(
|
250
|
-
name=name,
|
251
|
-
trace_id=trace_id,
|
252
|
-
start_time=start_time or datetime.datetime.now(datetime.timezone.utc),
|
253
|
-
id=id,
|
254
|
-
parent_span_id=parent_span_id,
|
255
|
-
input=input,
|
256
|
-
metadata=metadata,
|
257
|
-
attributes=attributes,
|
258
|
-
span_type=span_type,
|
259
|
-
evaluate_events=evaluate_events or [],
|
260
|
-
events=events or [],
|
261
|
-
)
|
262
|
-
return span
|
263
|
-
|
264
|
-
def update_span(
|
265
|
-
self,
|
266
|
-
span: Span,
|
267
|
-
finalize: bool = False,
|
268
|
-
input: Optional[Any] = None,
|
269
|
-
end_time: Optional[datetime.datetime] = None,
|
270
|
-
output: Optional[Any] = None,
|
271
|
-
metadata: Optional[dict[str, Any]] = None,
|
272
|
-
attributes: Optional[dict[str, Any]] = None,
|
273
|
-
evaluate_events: Optional[list[EvaluateEvent]] = None,
|
274
|
-
events: Optional[list[Event]] = None,
|
275
|
-
override: bool = False,
|
276
|
-
) -> Span:
|
277
|
-
"""Internal method to update a span object. Use `SpanContext.update()` instead."""
|
278
|
-
span.update(
|
279
|
-
input=input or span.input,
|
280
|
-
output=output or span.output,
|
281
|
-
end_time=end_time,
|
282
|
-
metadata=metadata,
|
283
|
-
attributes=attributes,
|
284
|
-
evaluate_events=evaluate_events,
|
285
|
-
events=events,
|
286
|
-
override=override,
|
287
|
-
)
|
288
|
-
if finalize:
|
289
|
-
self._add_observation(span)
|
290
|
-
return span
|
291
|
-
|
292
|
-
def event(
|
293
|
-
self,
|
294
|
-
name: str,
|
295
|
-
value: Optional[Union[str, int, float, bool]] = None,
|
296
|
-
timestamp: Optional[datetime.datetime] = None,
|
297
|
-
):
|
298
|
-
stack = _lmnr_stack_context.get()
|
299
|
-
if not stack or not isinstance(stack[-1], Span):
|
300
|
-
self._log.warning(
|
301
|
-
f"No active span to add check event. Ignoring event. {name}"
|
302
|
-
)
|
303
|
-
return
|
304
|
-
|
305
|
-
span = stack[-1]
|
306
|
-
event = Event(
|
307
|
-
name=name,
|
308
|
-
span_id=span.id,
|
309
|
-
timestamp=timestamp,
|
310
|
-
value=value,
|
311
|
-
)
|
312
|
-
span.add_event(event)
|
313
|
-
_lmnr_stack_context.set(stack)
|
314
|
-
|
315
|
-
def evaluate_event(self, name: str, evaluator: str, data: dict):
|
316
|
-
stack = _lmnr_stack_context.get()
|
317
|
-
if not stack or not isinstance(stack[-1], Span):
|
318
|
-
self._log.warning(
|
319
|
-
f"No active span to add check event. Ignoring event. {name}"
|
320
|
-
)
|
321
|
-
return
|
322
|
-
stack[-1].evaluateEvents.append(
|
323
|
-
EvaluateEvent(
|
324
|
-
name=name,
|
325
|
-
evaluator=evaluator,
|
326
|
-
data=data,
|
327
|
-
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
328
|
-
env=self.env,
|
329
|
-
)
|
330
|
-
)
|
331
|
-
_lmnr_stack_context.set(stack)
|
332
|
-
|
333
|
-
def run_pipeline(
|
334
|
-
self,
|
335
|
-
pipeline: str,
|
336
|
-
inputs: dict[str, Any],
|
337
|
-
env: dict[str, str] = {},
|
338
|
-
metadata: dict[str, str] = {},
|
339
|
-
) -> PipelineRunResponse:
|
340
|
-
span = _lmnr_stack_context.get()[-1] if _lmnr_stack_context.get() else None
|
341
|
-
span_id = span.id if isinstance(span, Span) else None
|
342
|
-
trace = _lmnr_stack_context.get()[0] if _lmnr_stack_context.get() else None
|
343
|
-
trace_id = trace.id if isinstance(trace, Trace) else None
|
344
|
-
return self.laminar.run(
|
345
|
-
pipeline=pipeline,
|
346
|
-
inputs=inputs,
|
347
|
-
env=env,
|
348
|
-
metadata=metadata,
|
349
|
-
parent_span_id=span_id,
|
350
|
-
trace_id=trace_id,
|
351
|
-
)
|
352
|
-
|
353
|
-
def set_env(self, env: dict[str, str]):
|
354
|
-
self.env = env
|
355
|
-
|
356
|
-
def _force_finalize_trace(self):
|
357
|
-
# TODO: flush in progress spans as error?
|
358
|
-
pass
|
359
|
-
|
360
|
-
def _add_observation(self, observation: Union[Span, Trace]) -> bool:
|
361
|
-
return self.thread_manager.add_task(observation)
|
362
|
-
|
363
|
-
def _extract_llm_attributes_from_response(
|
364
|
-
self,
|
365
|
-
provider: Provider,
|
366
|
-
response: Union[str, dict[str, Any], pydantic.BaseModel],
|
367
|
-
) -> dict[str, Any]:
|
368
|
-
return provider.extract_llm_attributes_from_response(response)
|
369
|
-
|
370
|
-
def _stream_list_to_dict(
|
371
|
-
self, provider: Provider, response: list[Any]
|
372
|
-
) -> dict[str, Any]:
|
373
|
-
return provider.stream_list_to_dict(response)
|
374
|
-
|
375
|
-
def _extract_llm_output(
|
376
|
-
self,
|
377
|
-
provider: Provider,
|
378
|
-
result: Union[dict[str, Any], pydantic.BaseModel],
|
379
|
-
) -> str:
|
380
|
-
return provider.extract_llm_output(result)
|
381
|
-
|
382
|
-
def _finalize_span(
|
383
|
-
self,
|
384
|
-
span: Span,
|
385
|
-
provider: Provider = None,
|
386
|
-
result: Optional[Any] = None,
|
387
|
-
metadata: Optional[dict[str, Any]] = None,
|
388
|
-
attributes: Optional[dict[str, Any]] = None,
|
389
|
-
) -> Any:
|
390
|
-
self.update_span(
|
391
|
-
span=span,
|
392
|
-
finalize=True,
|
393
|
-
output=(
|
394
|
-
result
|
395
|
-
if span.spanType != "LLM"
|
396
|
-
else self._extract_llm_output(provider, result)
|
397
|
-
),
|
398
|
-
metadata=metadata,
|
399
|
-
attributes=attributes,
|
400
|
-
)
|
401
|
-
return result
|
402
|
-
|
403
|
-
def _collect_generator_result(
|
404
|
-
self,
|
405
|
-
generator: Generator,
|
406
|
-
span: Span,
|
407
|
-
provider: Provider = None,
|
408
|
-
metadata: Optional[dict[str, Any]] = None,
|
409
|
-
attributes: Optional[dict[str, Any]] = None,
|
410
|
-
) -> Generator:
|
411
|
-
items = []
|
412
|
-
try:
|
413
|
-
for item in generator:
|
414
|
-
items.append(item)
|
415
|
-
yield item
|
416
|
-
|
417
|
-
finally:
|
418
|
-
output = items
|
419
|
-
if all(isinstance(item, str) for item in items):
|
420
|
-
output = "".join(items)
|
421
|
-
if span.spanType == "LLM":
|
422
|
-
collected = self._stream_list_to_dict(
|
423
|
-
provider=provider, response=output
|
424
|
-
)
|
425
|
-
attributes = self._extract_llm_attributes_from_response(
|
426
|
-
provider=provider, response=collected
|
427
|
-
)
|
428
|
-
self._finalize_span(
|
429
|
-
span=span,
|
430
|
-
provider=provider,
|
431
|
-
result=collected,
|
432
|
-
metadata=metadata,
|
433
|
-
attributes=attributes,
|
434
|
-
)
|
435
|
-
|
436
|
-
async def _collect_async_generator_result(
|
437
|
-
self,
|
438
|
-
generator: AsyncGenerator,
|
439
|
-
span: Span,
|
440
|
-
provider: Provider = None,
|
441
|
-
metadata: Optional[dict[str, Any]] = None,
|
442
|
-
attributes: Optional[dict[str, Any]] = None,
|
443
|
-
) -> AsyncGenerator:
|
444
|
-
items = []
|
445
|
-
try:
|
446
|
-
async for item in generator:
|
447
|
-
items.append(item)
|
448
|
-
yield item
|
449
|
-
|
450
|
-
finally:
|
451
|
-
output = items
|
452
|
-
if all(isinstance(item, str) for item in items):
|
453
|
-
output = "".join(items)
|
454
|
-
if span.spanType == "LLM":
|
455
|
-
collected = self._stream_list_to_dict(
|
456
|
-
provider=provider, response=output
|
457
|
-
)
|
458
|
-
attributes = self._extract_llm_attributes_from_response(
|
459
|
-
provider=provider, response=collected
|
460
|
-
)
|
461
|
-
self._finalize_span(
|
462
|
-
span=span,
|
463
|
-
provider=provider,
|
464
|
-
result=collected,
|
465
|
-
metadata=metadata,
|
466
|
-
attributes=attributes,
|
467
|
-
)
|
468
|
-
|
469
|
-
|
470
|
-
# TODO: add lock for thread safety
|
471
|
-
class LaminarSingleton:
|
472
|
-
_instance = None
|
473
|
-
_l: Optional[LaminarContextManager] = None
|
474
|
-
|
475
|
-
def __new__(cls):
|
476
|
-
if not cls._instance:
|
477
|
-
cls._instance = super(LaminarSingleton, cls).__new__(cls)
|
478
|
-
return cls._instance
|
479
|
-
|
480
|
-
def get(cls, *args, **kwargs) -> LaminarContextManager:
|
481
|
-
if not cls._l:
|
482
|
-
cls._l = LaminarContextManager(*args, **kwargs)
|
483
|
-
return cls._l
|