lmnr 0.6.7__py3-none-any.whl → 0.6.9__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.
@@ -0,0 +1,119 @@
1
+ """OpenTelemetry Langgraph instrumentation"""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Collection
6
+
7
+ from .utils import (
8
+ with_tracer_wrapper,
9
+ )
10
+
11
+ from langchain_core.runnables.graph import Graph
12
+ from opentelemetry.trace import Tracer
13
+ from wrapt import wrap_function_wrapper
14
+ from opentelemetry.trace import get_tracer, get_current_span
15
+
16
+ from lmnr.opentelemetry_lib.tracing.context_properties import (
17
+ update_association_properties,
18
+ )
19
+
20
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
21
+ from opentelemetry.instrumentation.utils import unwrap
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _instruments = ("langgraph >= 0.1.0",)
27
+
28
+
29
+ @with_tracer_wrapper
30
+ def wrap_pregel_stream(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
31
+ graph: Graph = instance.get_graph()
32
+ nodes = [
33
+ {
34
+ "id": node.id,
35
+ "name": node.name,
36
+ "metadata": node.metadata,
37
+ }
38
+ for node in graph.nodes.values()
39
+ ]
40
+ edges = [
41
+ {
42
+ "source": edge.source,
43
+ "target": edge.target,
44
+ "conditional": edge.conditional,
45
+ }
46
+ for edge in graph.edges
47
+ ]
48
+ update_association_properties(
49
+ {
50
+ "langgraph.edges": json.dumps(edges),
51
+ "langgraph.nodes": json.dumps(nodes),
52
+ },
53
+ )
54
+ return wrapped(*args, **kwargs)
55
+
56
+
57
+ @with_tracer_wrapper
58
+ async def async_wrap_pregel_stream(
59
+ tracer: Tracer, to_wrap, wrapped, instance, args, kwargs
60
+ ):
61
+ graph: Graph = await instance.aget_graph()
62
+ nodes = [
63
+ {
64
+ "id": node.id,
65
+ "name": node.name,
66
+ "metadata": node.metadata,
67
+ }
68
+ for node in graph.nodes.values()
69
+ ]
70
+ edges = [
71
+ {
72
+ "source": edge.source,
73
+ "target": edge.target,
74
+ "conditional": edge.conditional,
75
+ }
76
+ for edge in graph.edges
77
+ ]
78
+ update_association_properties(
79
+ {
80
+ "langgraph.edges": json.dumps(edges),
81
+ "langgraph.nodes": json.dumps(nodes),
82
+ },
83
+ )
84
+ return await wrapped(*args, **kwargs)
85
+
86
+
87
+ class LanggraphInstrumentor(BaseInstrumentor):
88
+ """An instrumentor for Langgraph."""
89
+
90
+ def __init__(self):
91
+ super().__init__()
92
+
93
+ def instrumentation_dependencies(self) -> Collection[str]:
94
+ return _instruments
95
+
96
+ def _instrument(self, **kwargs):
97
+ tracer_provider = kwargs.get("tracer_provider")
98
+ tracer = get_tracer(__name__, "0.0.1a0", tracer_provider)
99
+
100
+ wrap_function_wrapper(
101
+ module="langgraph.pregel",
102
+ name="Pregel.stream",
103
+ wrapper=wrap_pregel_stream(tracer, "Pregel.stream"),
104
+ )
105
+ wrap_function_wrapper(
106
+ module="langgraph.pregel",
107
+ name="Pregel.astream",
108
+ wrapper=async_wrap_pregel_stream(tracer, "Pregel.astream"),
109
+ )
110
+
111
+ def _uninstrument(self, **kwargs):
112
+ unwrap(
113
+ module="langgraph.pregel",
114
+ name="Pregel.stream",
115
+ )
116
+ unwrap(
117
+ module="langgraph.pregel",
118
+ name="Pregel.astream",
119
+ )
@@ -0,0 +1,60 @@
1
+ import logging
2
+ import traceback
3
+
4
+ import pydantic
5
+ from opentelemetry.trace import Span
6
+ from typing import Any
7
+
8
+
9
+ def set_span_attribute(span: Span, name: str, value: str):
10
+ if value is not None:
11
+ if value != "":
12
+ span.set_attribute(name, value)
13
+ return
14
+
15
+
16
+ def dont_throw(func):
17
+ """
18
+ A decorator that wraps the passed in function and logs exceptions instead of throwing them.
19
+
20
+ @param func: The function to wrap
21
+ @return: The wrapper function
22
+ """
23
+ # Obtain a logger specific to the function's module
24
+ logger = logging.getLogger(func.__module__)
25
+
26
+ def wrapper(*args, **kwargs):
27
+ try:
28
+ return func(*args, **kwargs)
29
+ except Exception:
30
+ logger.debug(
31
+ "Laminar failed to trace in %s, error: %s",
32
+ func.__name__,
33
+ traceback.format_exc(),
34
+ )
35
+
36
+ return wrapper
37
+
38
+
39
+ def to_dict(obj: pydantic.BaseModel | dict) -> dict[str, Any]:
40
+ try:
41
+ if isinstance(obj, pydantic.BaseModel):
42
+ return obj.model_dump()
43
+ elif isinstance(obj, dict):
44
+ return obj
45
+ else:
46
+ return dict(obj)
47
+ except Exception:
48
+ return dict(obj)
49
+
50
+
51
+ def with_tracer_wrapper(func):
52
+ """Helper for providing tracer for wrapper functions."""
53
+
54
+ def _with_tracer(tracer, to_wrap):
55
+ def wrapper(wrapped, instance, args, kwargs):
56
+ return func(tracer, to_wrap, wrapped, instance, args, kwargs)
57
+
58
+ return wrapper
59
+
60
+ return _with_tracer
@@ -171,6 +171,18 @@ class LangchainInstrumentorInitializer(InstrumentorInitializer):
171
171
  return LangchainInstrumentor()
172
172
 
173
173
 
174
+ class LanggraphInstrumentorInitializer(InstrumentorInitializer):
175
+ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
176
+ if not is_package_installed("langgraph"):
177
+ return None
178
+ if not is_package_installed("langchain-core"):
179
+ return None
180
+
181
+ from ..opentelemetry.instrumentation.langgraph import LanggraphInstrumentor
182
+
183
+ return LanggraphInstrumentor()
184
+
185
+
174
186
  class LlamaIndexInstrumentorInitializer(InstrumentorInitializer):
175
187
  def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
176
188
  if not (
@@ -6,10 +6,12 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
6
6
  )
7
7
 
8
8
  from opentelemetry.context import Context, attach, set_value, get_value
9
- from opentelemetry.trace import Span
9
+ from opentelemetry.sdk.trace import Span
10
10
  from opentelemetry import trace
11
11
 
12
12
 
13
+ # TODO: delete this once deprecated Laminar.with_labels is removed. The logic
14
+ # should be moved into Laminar.set_tracing_level
13
15
  def set_association_properties(properties: dict) -> None:
14
16
  attach(set_value("association_properties", properties))
15
17
 
@@ -17,10 +19,13 @@ def set_association_properties(properties: dict) -> None:
17
19
  _set_association_properties_attributes(span, properties)
18
20
 
19
21
 
22
+ # TODO: delete this once deprecated Laminar.with_labels is removed
20
23
  def get_association_properties(context: Context | None = None) -> dict:
21
24
  return get_value("association_properties", context) or {}
22
25
 
23
26
 
27
+ # TODO: delete this once deprecated Laminar.with_labels is removed. The logic
28
+ # should be moved into Laminar.set_tracing_level
24
29
  def update_association_properties(
25
30
  properties: dict,
26
31
  set_on_current_span: bool = True,
@@ -37,6 +42,7 @@ def update_association_properties(
37
42
  _set_association_properties_attributes(span, properties)
38
43
 
39
44
 
45
+ # TODO: this logic should be moved into Laminar.set_tracing_level
40
46
  def remove_association_properties(properties: dict) -> None:
41
47
  props: dict = copy.copy(get_value("association_properties") or {})
42
48
  for k in properties.keys():
@@ -45,8 +51,15 @@ def remove_association_properties(properties: dict) -> None:
45
51
 
46
52
 
47
53
  def _set_association_properties_attributes(span: Span, properties: dict) -> None:
54
+ if not span.is_recording():
55
+ return
48
56
  for key, value in properties.items():
49
57
  if key == TRACING_LEVEL:
50
58
  span.set_attribute(f"lmnr.internal.{TRACING_LEVEL}", value)
51
59
  continue
60
+ if (
61
+ key in ["langgraph.edges", "langgraph.nodes"]
62
+ and span.name != "LangGraph.workflow"
63
+ ):
64
+ continue
52
65
  span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{key}", value)
@@ -26,6 +26,7 @@ class Instruments(Enum):
26
26
  HAYSTACK = "haystack"
27
27
  LANCEDB = "lancedb"
28
28
  LANGCHAIN = "langchain"
29
+ LANGGRAPH = "langgraph"
29
30
  LLAMA_INDEX = "llama_index"
30
31
  MARQO = "marqo"
31
32
  MCP = "mcp"
@@ -62,6 +63,7 @@ INSTRUMENTATION_INITIALIZERS: dict[
62
63
  Instruments.HAYSTACK: initializers.HaystackInstrumentorInitializer(),
63
64
  Instruments.LANCEDB: initializers.LanceDBInstrumentorInitializer(),
64
65
  Instruments.LANGCHAIN: initializers.LangchainInstrumentorInitializer(),
66
+ Instruments.LANGGRAPH: initializers.LanggraphInstrumentorInitializer(),
65
67
  Instruments.LLAMA_INDEX: initializers.LlamaIndexInstrumentorInitializer(),
66
68
  Instruments.MARQO: initializers.MarqoInstrumentorInitializer(),
67
69
  Instruments.MCP: initializers.MCPInstrumentorInitializer(),
@@ -6,7 +6,7 @@ from opentelemetry.sdk.trace.export import (
6
6
  BatchSpanProcessor,
7
7
  SimpleSpanProcessor,
8
8
  )
9
- from opentelemetry.trace import Span
9
+ from opentelemetry.sdk.trace import Span
10
10
  from opentelemetry.context import Context, get_value, get_current, set_value
11
11
 
12
12
  from lmnr.opentelemetry_lib.tracing.attributes import (
lmnr/sdk/decorators.py CHANGED
@@ -9,9 +9,11 @@ from typing import Any, Callable, Literal, TypeVar, cast
9
9
  from typing_extensions import ParamSpec
10
10
 
11
11
  from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID
12
+ from lmnr.sdk.log import get_default_logger
12
13
 
13
14
  from .utils import is_async
14
15
 
16
+ logger = get_default_logger(__name__)
15
17
 
16
18
  P = ParamSpec("P")
17
19
  R = TypeVar("R")
@@ -27,6 +29,7 @@ def observe(
27
29
  span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
28
30
  ignore_inputs: list[str] | None = None,
29
31
  metadata: dict[str, Any] | None = None,
32
+ tags: list[str] | None = None,
30
33
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
31
34
  """The main decorator entrypoint for Laminar. This is used to wrap
32
35
  functions and methods to create spans.
@@ -52,6 +55,8 @@ def observe(
52
55
  this argument. Defaults to None.
53
56
  metadata (dict[str, Any] | None, optional): Metadata to associate with\
54
57
  the trace. Must be JSON serializable. Defaults to None.
58
+ tags (list[str] | None, optional): Tags to associate with the trace.
59
+ Defaults to None.
55
60
  Raises:
56
61
  Exception: re-raises the exception if the wrapped function raises an\
57
62
  exception
@@ -79,6 +84,13 @@ def observe(
79
84
  for k, v in metadata.items()
80
85
  }
81
86
  )
87
+ if tags is not None:
88
+ if not isinstance(tags, list) or not all(
89
+ isinstance(tag, str) for tag in tags
90
+ ):
91
+ logger.warning("Tags must be a list of strings. Tags will be ignored.")
92
+ else:
93
+ association_properties["tags"] = tags
82
94
  result = (
83
95
  aentity_method(
84
96
  name=name,
lmnr/sdk/evaluations.py CHANGED
@@ -233,10 +233,10 @@ class Evaluation:
233
233
  export_timeout_seconds=trace_export_timeout_seconds,
234
234
  )
235
235
 
236
- async def run(self) -> Awaitable[None]:
236
+ async def run(self) -> Awaitable[dict[str, int | float]]:
237
237
  return await self._run()
238
238
 
239
- async def _run(self) -> None:
239
+ async def _run(self) -> dict[str, int | float]:
240
240
  if isinstance(self.data, LaminarDataset):
241
241
  self.data.set_client(
242
242
  LaminarClient(
@@ -261,11 +261,12 @@ class Evaluation:
261
261
  except Exception as e:
262
262
  self.reporter.stopWithError(e)
263
263
  await self._shutdown()
264
- return
264
+ return {}
265
265
 
266
266
  average_scores = get_average_scores(result_datapoints)
267
267
  self.reporter.stop(average_scores, evaluation.projectId, evaluation.id)
268
268
  await self._shutdown()
269
+ return average_scores
269
270
 
270
271
  async def _shutdown(self):
271
272
  # We use flush() instead of shutdown() because multiple evaluations
lmnr/sdk/laminar.py CHANGED
@@ -1,10 +1,13 @@
1
1
  from contextlib import contextmanager
2
2
  from contextvars import Context
3
+ import warnings
4
+ from typing_extensions import deprecated
3
5
  from lmnr.opentelemetry_lib import TracerManager
4
6
  from lmnr.opentelemetry_lib.tracing.instruments import Instruments
5
7
  from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
6
8
  from lmnr.opentelemetry_lib.tracing.attributes import (
7
9
  ASSOCIATION_PROPERTIES,
10
+ USER_ID,
8
11
  Attributes,
9
12
  SPAN_TYPE,
10
13
  )
@@ -18,7 +21,6 @@ from opentelemetry.util.types import AttributeValue
18
21
 
19
22
  from typing import Any, Literal
20
23
 
21
- import copy
22
24
  import datetime
23
25
  import logging
24
26
  import os
@@ -37,7 +39,7 @@ from lmnr.opentelemetry_lib.tracing.context_properties import (
37
39
  set_association_properties,
38
40
  update_association_properties,
39
41
  )
40
- from lmnr.sdk.utils import from_env
42
+ from lmnr.sdk.utils import from_env, is_otel_attribute_value_type
41
43
 
42
44
  from .log import VerboseColorfulFormatter
43
45
 
@@ -224,6 +226,7 @@ class Laminar:
224
226
  context: Context | None = None,
225
227
  labels: list[str] | None = None,
226
228
  parent_span_context: LaminarSpanContext | None = None,
229
+ tags: list[str] | None = None,
227
230
  ):
228
231
  """Start a new span as the current span. Useful for manual
229
232
  instrumentation. If `span_type` is set to `"LLM"`, you should report
@@ -256,8 +259,10 @@ class Laminar:
256
259
  `Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
257
260
  `Laminar.get_span_context_str` for more information.
258
261
  Defaults to None.
259
- labels (list[str] | None, optional): labels to set for the\
260
- span. Defaults to None.
262
+ labels (list[str] | None, optional): [DEPRECATED] Use tags\
263
+ instead. Labels to set for the span. Defaults to None.
264
+ tags (list[str] | None, optional): tags to set for the span.
265
+ Defaults to None.
261
266
  """
262
267
 
263
268
  if not cls.is_initialized():
@@ -283,18 +288,32 @@ class Laminar:
283
288
  label_props = {}
284
289
  try:
285
290
  if labels:
291
+ warnings.warn(
292
+ "`Laminar.start_as_current_span` `labels` is deprecated. Use `tags` instead.",
293
+ DeprecationWarning,
294
+ )
286
295
  label_props = {f"{ASSOCIATION_PROPERTIES}.labels": labels}
287
296
  except Exception:
288
297
  cls.__logger.warning(
289
298
  f"`start_as_current_span` Could not set labels: {labels}. "
290
299
  "They will be propagated to the next span."
291
300
  )
301
+ tag_props = {}
302
+ if tags:
303
+ if isinstance(tags, list) and all(isinstance(tag, str) for tag in tags):
304
+ tag_props = {f"{ASSOCIATION_PROPERTIES}.tags": tags}
305
+ else:
306
+ cls.__logger.warning(
307
+ f"`start_as_current_span` Could not set tags: {tags}. Tags must be a list of strings. "
308
+ "Tags will be ignored."
309
+ )
292
310
  with tracer.start_as_current_span(
293
311
  name,
294
312
  context=ctx,
295
313
  attributes={
296
314
  SPAN_TYPE: span_type,
297
315
  **(label_props),
316
+ **(tag_props),
298
317
  },
299
318
  ) as span:
300
319
  if input is not None:
@@ -319,6 +338,10 @@ class Laminar:
319
338
 
320
339
  @classmethod
321
340
  @contextmanager
341
+ @deprecated(
342
+ "Use `Laminar.set_span_tags` or the `tags` argument of "
343
+ "`Laminar.start_as_current_span` or `Laminar.start_span` instead"
344
+ )
322
345
  def with_labels(cls, labels: list[str], context: Context | None = None):
323
346
  """Set labels for spans within this `with` context. This is useful for
324
347
  adding labels to the spans created in the auto-instrumentations.
@@ -334,6 +357,11 @@ class Laminar:
334
357
  openai_client.chat.completions.create()
335
358
  ```
336
359
  """
360
+ warnings.warn(
361
+ "`Laminar.with_labels` is deprecated. Use `Laminar.set_span_tags` or the `tags` argument of "
362
+ "`Laminar.start_as_current_span` or `Laminar.start_span` instead",
363
+ DeprecationWarning,
364
+ )
337
365
  if not cls.is_initialized():
338
366
  yield
339
367
  return
@@ -365,6 +393,7 @@ class Laminar:
365
393
  context: Context | None = None,
366
394
  parent_span_context: LaminarSpanContext | None = None,
367
395
  labels: dict[str, str] | None = None,
396
+ tags: list[str] | None = None,
368
397
  ):
369
398
  """Start a new span. Useful for manual instrumentation.
370
399
  If `span_type` is set to `"LLM"`, you should report usage and response
@@ -416,8 +445,10 @@ class Laminar:
416
445
  `Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
417
446
  `Laminar.get_span_context_str` for more information.
418
447
  Defaults to None.
419
- labels (dict[str, str] | None, optional): labels to set for the\
420
- span. Defaults to None.
448
+ tags (list[str] | None, optional): tags to set for the span.
449
+ Defaults to None.
450
+ labels (dict[str, str] | None, optional): [DEPRECATED] Use tags\
451
+ instead. Labels to set for the span. Defaults to None.
421
452
  """
422
453
  if not cls.is_initialized():
423
454
  return trace.NonRecordingSpan(
@@ -440,6 +471,10 @@ class Laminar:
440
471
  label_props = {}
441
472
  try:
442
473
  if labels:
474
+ warnings.warn(
475
+ "`Laminar.start_span` `labels` is deprecated. Use `tags` instead.",
476
+ DeprecationWarning,
477
+ )
443
478
  label_props = {
444
479
  f"{ASSOCIATION_PROPERTIES}.labels": json_dumps(labels)
445
480
  }
@@ -448,12 +483,22 @@ class Laminar:
448
483
  f"`start_span` Could not set labels: {labels}. They will be "
449
484
  "propagated to the next span."
450
485
  )
486
+ tag_props = {}
487
+ if tags:
488
+ if isinstance(tags, list) and all(isinstance(tag, str) for tag in tags):
489
+ tag_props = {f"{ASSOCIATION_PROPERTIES}.tags": tags}
490
+ else:
491
+ cls.__logger.warning(
492
+ f"`start_span` Could not set tags: {tags}. Tags must be a list of strings. "
493
+ + "Tags will be ignored."
494
+ )
451
495
  span = tracer.start_span(
452
496
  name,
453
497
  context=ctx,
454
498
  attributes={
455
499
  SPAN_TYPE: span_type,
456
500
  **(label_props),
501
+ **(tag_props),
457
502
  },
458
503
  )
459
504
  if input is not None:
@@ -562,7 +607,6 @@ class Laminar:
562
607
  cls.__logger.warning(
563
608
  f"Attribute {key} is not a valid Laminar attribute."
564
609
  )
565
- continue
566
610
  if not isinstance(value, (str, int, float, bool)):
567
611
  span.set_attribute(key.value, json_dumps(value))
568
612
  else:
@@ -649,11 +693,30 @@ class Laminar:
649
693
  TracerManager.shutdown()
650
694
 
651
695
  @classmethod
696
+ def set_span_tags(cls, tags: list[str]):
697
+ """Set the tags for the current span.
698
+
699
+ Args:
700
+ tags (list[str]): Tags to set for the span.
701
+ """
702
+ span = trace.get_current_span()
703
+ if span == trace.INVALID_SPAN:
704
+ cls.__logger.warning("No active span to set tags on")
705
+ return
706
+ if not isinstance(tags, list) or not all(isinstance(tag, str) for tag in tags):
707
+ cls.__logger.warning(
708
+ "Tags must be a list of strings. Tags will be ignored."
709
+ )
710
+ return
711
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", tags)
712
+
713
+ @classmethod
714
+ @deprecated("Use `Laminar.set_trace_session_id` instead")
652
715
  def set_session(
653
716
  cls,
654
717
  session_id: str | None = None,
655
718
  ):
656
- """Set the session and user id for the current span and the context
719
+ """Set the session id for the current span and the context
657
720
  (i.e. any children spans created from the current span in the current
658
721
  thread).
659
722
 
@@ -663,12 +726,53 @@ class Laminar:
663
726
  sessions/conversations.
664
727
  Defaults to None.
665
728
  """
729
+ warnings.warn(
730
+ "`Laminar.set_session` is deprecated. Use `Laminar.set_trace_session_id` instead",
731
+ DeprecationWarning,
732
+ )
666
733
  association_properties = {}
667
734
  if session_id is not None:
668
735
  association_properties[SESSION_ID] = session_id
669
- update_association_properties(association_properties)
736
+ # update_association_properties(association_properties)
737
+ span = trace.get_current_span()
738
+ if span == trace.INVALID_SPAN:
739
+ cls.__logger.warning("No active span to set session id on")
740
+ return
741
+ if session_id is not None:
742
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
743
+
744
+ @classmethod
745
+ def set_trace_session_id(cls, session_id: str | None = None):
746
+ """Set the session id for the current trace.
747
+ Overrides any existing session id.
748
+
749
+ Args:
750
+ session_id (str | None, optional): Custom session id. Defaults to None.
751
+ """
752
+ span = trace.get_current_span()
753
+ if span == trace.INVALID_SPAN:
754
+ cls.__logger.warning("No active span to set session id on")
755
+ return
756
+ if session_id is not None:
757
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
670
758
 
671
759
  @classmethod
760
+ def set_trace_user_id(cls, user_id: str | None = None):
761
+ """Set the user id for the current trace.
762
+ Overrides any existing user id.
763
+
764
+ Args:
765
+ user_id (str | None, optional): Custom user id. Defaults to None.
766
+ """
767
+ span = trace.get_current_span()
768
+ if span == trace.INVALID_SPAN:
769
+ cls.__logger.warning("No active span to set user id on")
770
+ return
771
+ if user_id is not None:
772
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
773
+
774
+ @classmethod
775
+ @deprecated("Use `Laminar.set_trace_metadata` instead")
672
776
  def set_metadata(cls, metadata: dict[str, str]):
673
777
  """Set the metadata for the current trace.
674
778
 
@@ -676,25 +780,37 @@ class Laminar:
676
780
  metadata (dict[str, str]): Metadata to set for the trace. Will be\
677
781
  sent as attributes, so must be json serializable.
678
782
  """
783
+ warnings.warn(
784
+ "`Laminar.set_metadata` is deprecated. Use `Laminar.set_trace_metadata` instead",
785
+ DeprecationWarning,
786
+ )
679
787
  props = {f"metadata.{k}": json_dumps(v) for k, v in metadata.items()}
680
- update_association_properties(props)
788
+ # update_association_properties(props)
789
+ span = trace.get_current_span()
790
+ if span == trace.INVALID_SPAN:
791
+ cls.__logger.warning("No active span to set metadata on")
792
+ return
793
+ for key, value in props.items():
794
+ span.set_attribute(key, value)
681
795
 
682
796
  @classmethod
683
- def clear_metadata(cls):
684
- """Clear the metadata from the context"""
685
- props: dict = copy.copy(context_api.get_value("association_properties"))
686
- metadata_keys = [k for k in props.keys() if k.startswith("metadata.")]
687
- for k in metadata_keys:
688
- props.pop(k)
689
- set_association_properties(props)
797
+ def set_trace_metadata(cls, metadata: dict[str, AttributeValue]):
798
+ """Set the metadata for the current trace.
690
799
 
691
- @classmethod
692
- def clear_session(cls):
693
- """Clear the session and user id from the context"""
694
- props: dict = copy.copy(context_api.get_value("association_properties"))
695
- props.pop("session_id", None)
696
- props.pop("user_id", None)
697
- set_association_properties(props)
800
+ Args:
801
+ metadata (dict[str, AttributeValue]): Metadata to set for the trace.
802
+ """
803
+ span = trace.get_current_span()
804
+ if span == trace.INVALID_SPAN:
805
+ cls.__logger.warning("No active span to set metadata on")
806
+ return
807
+ for key, value in metadata.items():
808
+ if is_otel_attribute_value_type(value):
809
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.metadata.{key}", value)
810
+ else:
811
+ span.set_attribute(
812
+ f"{ASSOCIATION_PROPERTIES}.metadata.{key}", json_dumps(value)
813
+ )
698
814
 
699
815
  @classmethod
700
816
  def get_base_http_url(cls):
@@ -735,7 +851,8 @@ class Laminar:
735
851
  Args:
736
852
  trace_type (TraceType): Type of the trace
737
853
  """
738
- association_properties = {
739
- TRACE_TYPE: trace_type.value,
740
- }
741
- update_association_properties(association_properties)
854
+ span = trace.get_current_span()
855
+ if span == trace.INVALID_SPAN:
856
+ cls.__logger.warning("No active span to set trace type on")
857
+ return
858
+ span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", trace_type.value)
lmnr/sdk/types.py CHANGED
@@ -81,7 +81,7 @@ class PartialEvaluationDatapoint(pydantic.BaseModel):
81
81
  "traceId": str(self.trace_id),
82
82
  "executorSpanId": str(self.executor_span_id),
83
83
  "metadata": (
84
- serialize(self.metadata) if self.metadata is not None else None
84
+ serialize(self.metadata) if self.metadata is not None else {}
85
85
  ),
86
86
  }
87
87
  except Exception as e:
@@ -123,7 +123,7 @@ class EvaluationResultDatapoint(pydantic.BaseModel):
123
123
  "executorSpanId": str(self.executor_span_id),
124
124
  "index": self.index,
125
125
  "metadata": (
126
- serialize(self.metadata) if self.metadata is not None else None
126
+ serialize(self.metadata) if self.metadata is not None else {}
127
127
  ),
128
128
  }
129
129
  except Exception as e: