lmnr 0.7.16__py3-none-any.whl → 0.7.18__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.
Potentially problematic release.
This version of lmnr might be problematic. Click here for more details.
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +24 -11
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +50 -0
- lmnr/sdk/laminar.py +93 -7
- lmnr/version.py +1 -1
- {lmnr-0.7.16.dist-info → lmnr-0.7.18.dist-info}/METADATA +2 -1
- {lmnr-0.7.16.dist-info → lmnr-0.7.18.dist-info}/RECORD +8 -8
- {lmnr-0.7.16.dist-info → lmnr-0.7.18.dist-info}/WHEEL +0 -0
- {lmnr-0.7.16.dist-info → lmnr-0.7.18.dist-info}/entry_points.txt +0 -0
|
@@ -21,6 +21,7 @@ from .schema_utils import SchemaJSONEncoder, process_schema
|
|
|
21
21
|
from .utils import (
|
|
22
22
|
dont_throw,
|
|
23
23
|
get_content,
|
|
24
|
+
merge_text_parts,
|
|
24
25
|
process_content_union,
|
|
25
26
|
process_stream_chunk,
|
|
26
27
|
role_from_content_union,
|
|
@@ -205,15 +206,17 @@ def _set_request_attributes(span, args, kwargs):
|
|
|
205
206
|
contents = [contents]
|
|
206
207
|
for content in contents:
|
|
207
208
|
processed_content = process_content_union(content)
|
|
208
|
-
|
|
209
|
+
content_payload = get_content(processed_content)
|
|
210
|
+
if isinstance(content_payload, dict):
|
|
211
|
+
content_payload = [content_payload]
|
|
209
212
|
|
|
210
213
|
set_span_attribute(
|
|
211
214
|
span,
|
|
212
215
|
f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.content",
|
|
213
216
|
(
|
|
214
|
-
|
|
215
|
-
if isinstance(
|
|
216
|
-
else json_dumps(
|
|
217
|
+
content_payload
|
|
218
|
+
if isinstance(content_payload, str)
|
|
219
|
+
else json_dumps(content_payload)
|
|
217
220
|
),
|
|
218
221
|
)
|
|
219
222
|
blocks = (
|
|
@@ -317,20 +320,22 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
317
320
|
for candidate in candidates_list:
|
|
318
321
|
has_content = False
|
|
319
322
|
processed_content = process_content_union(candidate.content)
|
|
320
|
-
|
|
323
|
+
content_payload = get_content(processed_content)
|
|
324
|
+
if isinstance(content_payload, dict):
|
|
325
|
+
content_payload = [content_payload]
|
|
321
326
|
|
|
322
327
|
set_span_attribute(
|
|
323
328
|
span, f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.role", "model"
|
|
324
329
|
)
|
|
325
|
-
if
|
|
330
|
+
if content_payload:
|
|
326
331
|
has_content = True
|
|
327
332
|
set_span_attribute(
|
|
328
333
|
span,
|
|
329
334
|
f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.content",
|
|
330
335
|
(
|
|
331
|
-
|
|
332
|
-
if isinstance(
|
|
333
|
-
else json_dumps(
|
|
336
|
+
content_payload
|
|
337
|
+
if isinstance(content_payload, str)
|
|
338
|
+
else json_dumps(content_payload)
|
|
334
339
|
),
|
|
335
340
|
)
|
|
336
341
|
blocks = (
|
|
@@ -379,6 +384,10 @@ def _build_from_streaming_response(
|
|
|
379
384
|
aggregated_usage_metadata = defaultdict(int)
|
|
380
385
|
model_version = None
|
|
381
386
|
for chunk in response:
|
|
387
|
+
try:
|
|
388
|
+
span.add_event("llm.content.completion.chunk")
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
382
391
|
# Important: do all processing in a separate sync function, that is
|
|
383
392
|
# wrapped in @dont_throw. If we did it here, the @dont_throw on top of
|
|
384
393
|
# this function would not be able to catch the errors, as they are
|
|
@@ -403,7 +412,7 @@ def _build_from_streaming_response(
|
|
|
403
412
|
candidates=[
|
|
404
413
|
{
|
|
405
414
|
"content": {
|
|
406
|
-
"parts": final_parts,
|
|
415
|
+
"parts": merge_text_parts(final_parts),
|
|
407
416
|
"role": role,
|
|
408
417
|
},
|
|
409
418
|
}
|
|
@@ -429,6 +438,10 @@ async def _abuild_from_streaming_response(
|
|
|
429
438
|
aggregated_usage_metadata = defaultdict(int)
|
|
430
439
|
model_version = None
|
|
431
440
|
async for chunk in response:
|
|
441
|
+
try:
|
|
442
|
+
span.add_event("llm.content.completion.chunk")
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
432
445
|
# Important: do all processing in a separate sync function, that is
|
|
433
446
|
# wrapped in @dont_throw. If we did it here, the @dont_throw on top of
|
|
434
447
|
# this function would not be able to catch the errors, as they are
|
|
@@ -453,7 +466,7 @@ async def _abuild_from_streaming_response(
|
|
|
453
466
|
candidates=[
|
|
454
467
|
{
|
|
455
468
|
"content": {
|
|
456
|
-
"parts": final_parts,
|
|
469
|
+
"parts": merge_text_parts(final_parts),
|
|
457
470
|
"role": role,
|
|
458
471
|
},
|
|
459
472
|
}
|
|
@@ -40,6 +40,56 @@ class ProcessChunkResult(TypedDict):
|
|
|
40
40
|
model_version: str | None
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def merge_text_parts(
|
|
44
|
+
parts: list[types.PartDict | types.File | types.Part | str],
|
|
45
|
+
) -> list[types.Part]:
|
|
46
|
+
if not parts:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
merged_parts: list[types.Part] = []
|
|
50
|
+
accumulated_text = ""
|
|
51
|
+
|
|
52
|
+
for part in parts:
|
|
53
|
+
# Handle string input - treat as text
|
|
54
|
+
if isinstance(part, str):
|
|
55
|
+
accumulated_text += part
|
|
56
|
+
# Handle File objects - they are not text, so don't merge
|
|
57
|
+
elif isinstance(part, types.File):
|
|
58
|
+
# Flush any accumulated text first
|
|
59
|
+
if accumulated_text:
|
|
60
|
+
merged_parts.append(types.Part(text=accumulated_text))
|
|
61
|
+
accumulated_text = ""
|
|
62
|
+
# Add the File as-is (wrapped in a Part if needed)
|
|
63
|
+
# Note: File objects should be passed through as-is in the original part
|
|
64
|
+
merged_parts.append(part)
|
|
65
|
+
# Handle Part and PartDict (dicts)
|
|
66
|
+
else:
|
|
67
|
+
part_dict = to_dict(part)
|
|
68
|
+
|
|
69
|
+
# Check if this is a text part
|
|
70
|
+
if part_dict.get("text") is not None:
|
|
71
|
+
accumulated_text += part_dict.get("text")
|
|
72
|
+
else:
|
|
73
|
+
# Non-text part (inline_data, function_call, etc.)
|
|
74
|
+
# Flush any accumulated text first
|
|
75
|
+
if accumulated_text:
|
|
76
|
+
merged_parts.append(types.Part(text=accumulated_text))
|
|
77
|
+
accumulated_text = ""
|
|
78
|
+
|
|
79
|
+
# Add the non-text part as-is
|
|
80
|
+
if isinstance(part, types.Part):
|
|
81
|
+
merged_parts.append(part)
|
|
82
|
+
elif isinstance(part, dict):
|
|
83
|
+
# Convert dict to Part object
|
|
84
|
+
merged_parts.append(types.Part(**part_dict))
|
|
85
|
+
|
|
86
|
+
# Don't forget to add any remaining accumulated text
|
|
87
|
+
if accumulated_text:
|
|
88
|
+
merged_parts.append(types.Part(text=accumulated_text))
|
|
89
|
+
|
|
90
|
+
return merged_parts
|
|
91
|
+
|
|
92
|
+
|
|
43
93
|
def set_span_attribute(span: Span, name: str, value: Any):
|
|
44
94
|
if value is not None and value != "":
|
|
45
95
|
span.set_attribute(name, value)
|
lmnr/sdk/laminar.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
|
-
from contextvars import Context
|
|
2
|
+
from contextvars import Context, Token
|
|
3
3
|
import warnings
|
|
4
4
|
from lmnr.opentelemetry_lib import TracerManager
|
|
5
5
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper, get_current_context
|
|
@@ -434,17 +434,17 @@ class Laminar:
|
|
|
434
434
|
with Laminar.use_span(span):
|
|
435
435
|
with Laminar.start_as_current_span("foo_inner"):
|
|
436
436
|
some_function()
|
|
437
|
-
|
|
437
|
+
|
|
438
438
|
def bar():
|
|
439
439
|
with Laminar.use_span(span):
|
|
440
440
|
openai_client.chat.completions.create()
|
|
441
|
-
|
|
441
|
+
|
|
442
442
|
span = Laminar.start_span("outer")
|
|
443
443
|
foo(span)
|
|
444
444
|
bar(span)
|
|
445
445
|
# IMPORTANT: End the span manually
|
|
446
446
|
span.end()
|
|
447
|
-
|
|
447
|
+
|
|
448
448
|
# Results in:
|
|
449
449
|
# | outer
|
|
450
450
|
# | | foo
|
|
@@ -642,6 +642,92 @@ class Laminar:
|
|
|
642
642
|
if end_on_exit:
|
|
643
643
|
span.end()
|
|
644
644
|
|
|
645
|
+
@classmethod
|
|
646
|
+
def start_active_span(
|
|
647
|
+
cls,
|
|
648
|
+
name: str,
|
|
649
|
+
input: Any = None,
|
|
650
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
651
|
+
context: Context | None = None,
|
|
652
|
+
parent_span_context: LaminarSpanContext | None = None,
|
|
653
|
+
tags: list[str] | None = None,
|
|
654
|
+
) -> tuple[Span, Token[Context] | None]:
|
|
655
|
+
"""Start a new span. Useful for manual instrumentation.
|
|
656
|
+
If `span_type` is set to `"LLM"`, you should report usage and response
|
|
657
|
+
attributes manually. See `Laminar.set_span_attributes` for more
|
|
658
|
+
information. Returns the span and a context token that can be used to
|
|
659
|
+
detach the context.
|
|
660
|
+
|
|
661
|
+
Usage example:
|
|
662
|
+
```python
|
|
663
|
+
from src.lmnr import Laminar
|
|
664
|
+
def foo():
|
|
665
|
+
with Laminar.start_active_span("foo_inner"):
|
|
666
|
+
some_function()
|
|
667
|
+
|
|
668
|
+
def bar():
|
|
669
|
+
openai_client.chat.completions.create()
|
|
670
|
+
|
|
671
|
+
span, ctx_token = Laminar.start_active_span("outer")
|
|
672
|
+
foo()
|
|
673
|
+
bar()
|
|
674
|
+
# IMPORTANT: End the span manually
|
|
675
|
+
Laminar.end_active_span(span, ctx_token)
|
|
676
|
+
|
|
677
|
+
# Results in:
|
|
678
|
+
# | outer
|
|
679
|
+
# | | foo
|
|
680
|
+
# | | | foo_inner
|
|
681
|
+
# | | bar
|
|
682
|
+
# | | | openai.chat
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
name (str): name of the span
|
|
687
|
+
input (Any, optional): input to the span. Will be sent as an\
|
|
688
|
+
attribute, so must be json serializable. Defaults to None.
|
|
689
|
+
span_type (Literal["DEFAULT", "LLM", "TOOL"], optional):\
|
|
690
|
+
type of the span. If you use `"LLM"`, you should report usage\
|
|
691
|
+
and response attributes manually. Defaults to "DEFAULT".
|
|
692
|
+
context (Context | None, optional): raw OpenTelemetry context\
|
|
693
|
+
to attach the span to. Defaults to None.
|
|
694
|
+
parent_span_context (LaminarSpanContext | None, optional): parent\
|
|
695
|
+
span context to use for the span. Useful for continuing traces\
|
|
696
|
+
across services. If parent_span_context is a\
|
|
697
|
+
raw OpenTelemetry span context, or if it is a dictionary or string\
|
|
698
|
+
obtained from `Laminar.get_laminar_span_context_dict()` or\
|
|
699
|
+
`Laminar.get_laminar_span_context_str()` respectively, it will be\
|
|
700
|
+
converted to a `LaminarSpanContext` if possible. See also\
|
|
701
|
+
`Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
|
|
702
|
+
`Laminar.get_span_context_str` for more information.
|
|
703
|
+
Defaults to None.
|
|
704
|
+
tags (list[str] | None, optional): tags to set for the span.
|
|
705
|
+
Defaults to None.
|
|
706
|
+
"""
|
|
707
|
+
span = cls.start_span(
|
|
708
|
+
name, input, span_type, context, parent_span_context, tags
|
|
709
|
+
)
|
|
710
|
+
if not cls.is_initialized():
|
|
711
|
+
return span, None
|
|
712
|
+
wrapper = TracerWrapper()
|
|
713
|
+
context = wrapper.push_span_context(span)
|
|
714
|
+
context_token = context_api.attach(context)
|
|
715
|
+
return span, context_token
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def end_active_span(cls, span: Span, ctx_token: Token[Context]):
|
|
719
|
+
"""End an active span."""
|
|
720
|
+
span.end()
|
|
721
|
+
if not cls.is_initialized():
|
|
722
|
+
return
|
|
723
|
+
wrapper = TracerWrapper()
|
|
724
|
+
try:
|
|
725
|
+
wrapper.pop_span_context()
|
|
726
|
+
if ctx_token is not None:
|
|
727
|
+
context_api.detach(ctx_token)
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
730
|
+
|
|
645
731
|
@classmethod
|
|
646
732
|
def set_span_output(cls, output: Any = None):
|
|
647
733
|
"""Set the output of the current span. Useful for manual
|
|
@@ -671,12 +757,12 @@ class Laminar:
|
|
|
671
757
|
instrumentation.
|
|
672
758
|
Example:
|
|
673
759
|
```python
|
|
674
|
-
with
|
|
760
|
+
with Laminar.start_as_current_span(
|
|
675
761
|
name="my_span_name", input=input["messages"], span_type="LLM"
|
|
676
762
|
):
|
|
677
763
|
response = await my_custom_call_to_openai(input)
|
|
678
|
-
|
|
679
|
-
|
|
764
|
+
Laminar.set_span_output(response["choices"][0]["message"]["content"])
|
|
765
|
+
Laminar.set_span_attributes({
|
|
680
766
|
Attributes.PROVIDER: 'openai',
|
|
681
767
|
Attributes.REQUEST_MODEL: input["model"],
|
|
682
768
|
Attributes.RESPONSE_MODEL: response["model"],
|
lmnr/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lmnr
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.18
|
|
4
4
|
Summary: Python SDK for Laminar
|
|
5
5
|
Author: lmnr.ai
|
|
6
6
|
Author-email: lmnr.ai <founders@lmnr.ai>
|
|
@@ -26,6 +26,7 @@ Requires-Dist: grpcio>=1
|
|
|
26
26
|
Requires-Dist: httpx>=0.24.0
|
|
27
27
|
Requires-Dist: orjson>=3.0.0
|
|
28
28
|
Requires-Dist: packaging>=22.0
|
|
29
|
+
Requires-Dist: opentelemetry-instrumentation-threading>=0.57b0
|
|
29
30
|
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.47.1 ; extra == 'alephalpha'
|
|
30
31
|
Requires-Dist: opentelemetry-instrumentation-alephalpha>=0.47.1 ; extra == 'all'
|
|
31
32
|
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.47.1 ; extra == 'all'
|
|
@@ -16,10 +16,10 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py,sha256
|
|
|
16
16
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py,sha256=48391d935883506fe1dc4f6ace6011ecaed76a8f82f8026ccb553b2180afdb8c,3455
|
|
17
17
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py,sha256=61d2681e99c3084d1bcc27f7ca551f44a70126df6c5f23320c1e9c1654e05c42,15037
|
|
18
18
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py,sha256=19090d4d9a0511645f66112ebe6f05a9993905b11d8ae3060dab2dcc4c1a5fb2,329
|
|
19
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=
|
|
19
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py,sha256=df9f06eda1f16f1ab99dbfdfae20ff55da1fb7842b53d51179b4fccdf9b0ac8f,20691
|
|
20
20
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py,sha256=db9cdebc9ee0dccb493ffe608eede3047efec20ed26c3924b72b2e50edbd92c2,245
|
|
21
21
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py,sha256=b10619e76e5893f8b891f92531d29dcf6651e8f9a7dcbf81c3f35341ce311f6e,753
|
|
22
|
-
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py,sha256=
|
|
22
|
+
lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py,sha256=ac662208d336cb9f5eb086389cd77d73e1a5a8bcc22ce8fffccd12a828b4e5cd,11092
|
|
23
23
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py,sha256=1e98467711405e4ff8ccd0b53c002e7a676c581616ef015e8b6606bd7057478b,14986
|
|
24
24
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py,sha256=29d557d9dee56354e89634bdc3f4795f346ee67bbfec56184b4fb394e45a7e03,203
|
|
25
25
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py,sha256=1f07d78bf360832951c708fcb3737718e50d39ce05beb8adbf57e818b4873703,4481
|
|
@@ -93,12 +93,12 @@ lmnr/sdk/datasets.py,sha256=3fd851c5f97bf88eaa84b1451a053eaff23b4497cbb45eac2f9e
|
|
|
93
93
|
lmnr/sdk/decorators.py,sha256=c709b76a814e019c919fd811591850787a2f266b7b6f46123f66ddd92e1092d5,6920
|
|
94
94
|
lmnr/sdk/eval_control.py,sha256=291394ac385c653ae9b5167e871bebeb4fe8fc6b7ff2ed38e636f87015dcba86,184
|
|
95
95
|
lmnr/sdk/evaluations.py,sha256=7e55cbca77fa32cb64cb77aed8076a1994258a5b652c7f1d45231928e4aefe26,23885
|
|
96
|
-
lmnr/sdk/laminar.py,sha256=
|
|
96
|
+
lmnr/sdk/laminar.py,sha256=5fcb699577b19d60b0cf5c494eb366420d69d0ba17ac2cde979f0648ac38486e,41772
|
|
97
97
|
lmnr/sdk/log.py,sha256=9edfd83263f0d4845b1b2d1beeae2b4ed3f8628de941f371a893d72b79c348d4,2213
|
|
98
98
|
lmnr/sdk/types.py,sha256=d8061ca90dd582b408a893ebbbeb1586e8750ed30433ef4f6d63423a078511b0,14574
|
|
99
99
|
lmnr/sdk/utils.py,sha256=4114559ba6ae57fcba2de2bfaa09339688ce5752c36f028a7b55e51eae624947,6307
|
|
100
|
-
lmnr/version.py,sha256=
|
|
101
|
-
lmnr-0.7.
|
|
102
|
-
lmnr-0.7.
|
|
103
|
-
lmnr-0.7.
|
|
104
|
-
lmnr-0.7.
|
|
100
|
+
lmnr/version.py,sha256=98f78bacb066c12efdabeed4190a9b7a2386c093d36e8d1ce91276954fcab806,1322
|
|
101
|
+
lmnr-0.7.18.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
|
|
102
|
+
lmnr-0.7.18.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
|
|
103
|
+
lmnr-0.7.18.dist-info/METADATA,sha256=b69c7b58f2b88f8839580b89bf55a7e732f92e60f88588dfe68f406579899be3,14258
|
|
104
|
+
lmnr-0.7.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|