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.

@@ -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
- content_str = get_content(processed_content)
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
- content_str
215
- if isinstance(content_str, str)
216
- else json_dumps(content_str)
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
- content_str = get_content(processed_content)
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 content_str:
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
- content_str
332
- if isinstance(content_str, str)
333
- else json_dumps(content_str)
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 L.start_as_current_span(
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
- L.set_span_output(response["choices"][0]["message"]["content"])
679
- L.set_span_attributes({
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
@@ -3,7 +3,7 @@ import httpx
3
3
  from packaging import version
4
4
 
5
5
 
6
- __version__ = "0.7.16"
6
+ __version__ = "0.7.18"
7
7
  PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
8
8
 
9
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmnr
3
- Version: 0.7.16
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=a0037e5ed736e3124c7cd0af7eff6d2656a622d4b0a16785e2d2fb7d760f85a0,20163
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=2e1dc91b04757f7f6c960e3f1c58bb9f7e735f0e1dcb811a08833faf18766d3b,9242
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=24adfd64da01d7fd69ba9437cf9860a5c64aa6baab1bb92d8ba143db1be12e96,38313
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=cc6cc51d32104d432712f37c6215b60901c85269bf239b623a9d3d72dcd220f8,1322
101
- lmnr-0.7.16.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
102
- lmnr-0.7.16.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
103
- lmnr-0.7.16.dist-info/METADATA,sha256=0405208ecd989926a7b75dd4fedbab4fbd010c126fca736e09673ea955efeed6,14195
104
- lmnr-0.7.16.dist-info/RECORD,,
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