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/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