raindrop-ai 0.0.30__tar.gz → 0.0.32__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: raindrop-ai
3
- Version: 0.0.30
3
+ Version: 0.0.32
4
4
  Summary: Raindrop AI (Python SDK)
5
5
  License: MIT
6
6
  Author: Raindrop AI
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "raindrop-ai"
3
- version = "0.0.30"
3
+ version = "0.0.32"
4
4
  description = "Raindrop AI (Python SDK)"
5
5
  authors = ["Raindrop AI <sdk@raindrop.ai>"]
6
6
  license = "MIT"
@@ -29,8 +29,18 @@ import weakref
29
29
  import urllib.parse
30
30
 
31
31
  from traceloop.sdk import Traceloop
32
- from traceloop.sdk.tracing.tracing import TracerWrapper
32
+ from traceloop.sdk.tracing.tracing import (
33
+ TracerWrapper,
34
+ get_chained_entity_path,
35
+ set_entity_path,
36
+ )
33
37
  from opentelemetry.trace import get_current_span
38
+ from opentelemetry import trace
39
+ from opentelemetry import context as context_api
40
+ from opentelemetry.semconv_ai import SpanAttributes
41
+ from opentelemetry.trace.status import Status, StatusCode
42
+ from traceloop.sdk.utils.json_encoder import JSONEncoder
43
+ from traceloop.sdk.tracing.context_manager import get_tracer
34
44
  from traceloop.sdk.decorators import (
35
45
  task as tlp_task,
36
46
  workflow as tlp_workflow,
@@ -41,20 +51,19 @@ from traceloop.sdk.decorators import (
41
51
  __all__ = [
42
52
  # Configuration functions
43
53
  "set_debug_logs",
44
- "set_redact_pii",
54
+ "set_redact_pii",
45
55
  "init",
46
-
47
56
  "identify",
48
57
  "track_ai",
49
58
  "track_signal",
50
59
  "begin",
51
60
  "resume_interaction",
52
-
53
61
  "interaction",
54
- "task",
62
+ "task",
55
63
  "tool",
64
+ "task_span",
65
+ "tool_span",
56
66
  "set_span_properties",
57
-
58
67
  "flush",
59
68
  "shutdown",
60
69
  ]
@@ -314,6 +323,28 @@ def _get_size(event: dict[str, any]) -> int:
314
323
  return 0
315
324
 
316
325
 
326
+ def _truncate_json_if_needed(json_str: str) -> str:
327
+ """
328
+ Truncate JSON string if it exceeds OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
329
+ truncation may yield an invalid JSON string, which is expected for logging purposes.
330
+ """
331
+ limit_str = os.getenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT")
332
+ if limit_str:
333
+ try:
334
+ limit = int(limit_str)
335
+ if limit > 0 and len(json_str) > limit:
336
+ return json_str[:limit]
337
+ except ValueError:
338
+ pass
339
+ return json_str
340
+
341
+
342
+ def _should_send_prompts():
343
+ return (
344
+ os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
345
+ ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
346
+
347
+
317
348
  # Signal types - This is now defined in models.py
318
349
  # SignalType = Literal["default", "feedback", "edit"]
319
350
 
@@ -573,6 +604,110 @@ def set_span_properties(properties: Dict[str, Any]) -> None:
573
604
  Traceloop.set_association_properties(properties)
574
605
 
575
606
 
607
+ class TraceEntitySpan:
608
+ def __init__(self, span):
609
+ self._span = span
610
+
611
+ def record_input(self, data: Any) -> None:
612
+ if self._span and _should_send_prompts():
613
+ try:
614
+ json_input = json.dumps({"args": [data]}, cls=JSONEncoder)
615
+ truncated = _truncate_json_if_needed(json_input)
616
+ self._span.set_attribute(
617
+ SpanAttributes.TRACELOOP_ENTITY_INPUT, truncated
618
+ )
619
+ except TypeError as e:
620
+ logger.debug(f"[raindrop] Could not serialize input for span: {e}")
621
+
622
+ def record_output(self, data: Any) -> None:
623
+ if self._span and _should_send_prompts():
624
+ try:
625
+ json_output = json.dumps(data, cls=JSONEncoder)
626
+ truncated = _truncate_json_if_needed(json_output)
627
+ self._span.set_attribute(
628
+ SpanAttributes.TRACELOOP_ENTITY_OUTPUT, truncated
629
+ )
630
+ except TypeError as e:
631
+ logger.debug(f"[raindrop] Could not serialize output for span: {e}")
632
+
633
+ def set_properties(self, props: Dict[str, Any]) -> None:
634
+ if _tracing_enabled and props:
635
+ Traceloop.set_association_properties(props)
636
+
637
+
638
+ class _EntitySpanContext:
639
+ def __init__(self, kind: Literal["task", "tool"], name: str, version: int | None):
640
+ self._kind = kind
641
+ self._name = name
642
+ self._version = version
643
+ self._span = None
644
+ self._ctx_token = None
645
+ self._span_cm = None
646
+ self._helper = TraceEntitySpan(None)
647
+
648
+ # internal start/finish
649
+ def _start(self) -> None:
650
+ if not _tracing_enabled or not TracerWrapper.verify_initialized():
651
+ return
652
+ tlp_kind = (
653
+ TraceloopSpanKindValues.TASK
654
+ if self._kind == "task"
655
+ else TraceloopSpanKindValues.TOOL
656
+ )
657
+ span_name = f"{self._name}.{tlp_kind.value}"
658
+ with get_tracer() as tracer:
659
+ self._span_cm = tracer.start_as_current_span(span_name)
660
+ span = self._span_cm.__enter__()
661
+
662
+ if tlp_kind in [TraceloopSpanKindValues.TASK, TraceloopSpanKindValues.TOOL]:
663
+ entity_path = get_chained_entity_path(self._name)
664
+ set_entity_path(entity_path)
665
+
666
+ span.set_attribute(SpanAttributes.TRACELOOP_SPAN_KIND, tlp_kind.value)
667
+ span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_NAME, self._name)
668
+ if self._version is not None:
669
+ span.set_attribute(SpanAttributes.TRACELOOP_ENTITY_VERSION, self._version)
670
+
671
+ self._span = span
672
+ self._helper = TraceEntitySpan(span)
673
+
674
+ def _end(self, exc_type, exc, tb) -> bool:
675
+ if not self._span:
676
+ return False
677
+ try:
678
+ if exc is not None:
679
+ self._span.set_status(Status(StatusCode.ERROR, str(exc)))
680
+ self._span.record_exception(exc)
681
+ return False
682
+ finally:
683
+ if self._span_cm is not None:
684
+ self._span_cm.__exit__(exc_type, exc, tb)
685
+
686
+ # sync
687
+ def __enter__(self) -> TraceEntitySpan:
688
+ self._start()
689
+ return self._helper
690
+
691
+ def __exit__(self, exc_type, exc, tb) -> bool:
692
+ return self._end(exc_type, exc, tb)
693
+
694
+ # async
695
+ async def __aenter__(self) -> TraceEntitySpan:
696
+ self._start()
697
+ return self._helper
698
+
699
+ async def __aexit__(self, exc_type, exc, tb) -> bool:
700
+ return self._end(exc_type, exc, tb)
701
+
702
+
703
+ def task_span(name: str, version: int | None = None) -> _EntitySpanContext:
704
+ return _EntitySpanContext("task", name, version)
705
+
706
+
707
+ def tool_span(name: str, version: int | None = None) -> _EntitySpanContext:
708
+ return _EntitySpanContext("tool", name, version)
709
+
710
+
576
711
  def resume_interaction(event_id: str | None = None) -> Interaction:
577
712
  """Return an Interaction associated with the current trace or given event_id."""
578
713
 
@@ -16,10 +16,10 @@ class Attachment(BaseModel):
16
16
  language: Optional[str] = None # for code snippets
17
17
 
18
18
  @model_validator(mode="after")
19
- def _require_value(cls, values):
20
- if not values.value:
19
+ def _require_value(self):
20
+ if not self.value:
21
21
  raise ValueError("value must be non-empty.")
22
- return values
22
+ return self
23
23
 
24
24
 
25
25
  class AIData(_Base):
@@ -29,10 +29,10 @@ class AIData(_Base):
29
29
  convo_id: Optional[str]
30
30
 
31
31
  @model_validator(mode="after")
32
- def _require_input_or_output(cls, values):
33
- if not (values.input or values.output):
32
+ def _require_input_or_output(self):
33
+ if not (self.input or self.output):
34
34
  raise ValueError("Either 'input' or 'output' must be non-empty.")
35
- return values
35
+ return self
36
36
 
37
37
 
38
38
  class TrackAIEvent(_Base):
@@ -94,10 +94,9 @@ class FeedbackSignal(BaseSignal):
94
94
  signal_type: Literal["feedback"]
95
95
 
96
96
  @model_validator(mode="after")
97
- def _check_comment_in_properties(cls, values):
97
+ def _check_comment_in_properties(self):
98
98
  # Check properties safely after potential initialization
99
- # Use getattr to safely access properties, returning None if not present
100
- props = getattr(values, "properties", None)
99
+ props = self.properties
101
100
  if not isinstance(props, dict):
102
101
  raise ValueError("'properties' must be a dictionary for feedback signals.")
103
102
  comment = props.get("comment")
@@ -105,7 +104,7 @@ class FeedbackSignal(BaseSignal):
105
104
  raise ValueError(
106
105
  "'properties' must contain a non-empty string 'comment' for feedback signals."
107
106
  )
108
- return values
107
+ return self
109
108
 
110
109
 
111
110
  class EditSignal(BaseSignal):
@@ -114,9 +113,9 @@ class EditSignal(BaseSignal):
114
113
  signal_type: Literal["edit"]
115
114
 
116
115
  @model_validator(mode="after")
117
- def _check_after_in_properties(cls, values):
116
+ def _check_after_in_properties(self):
118
117
  # Check properties safely after potential initialization
119
- props = getattr(values, "properties", None)
118
+ props = self.properties
120
119
  if not isinstance(props, dict):
121
120
  raise ValueError("'properties' must be a dictionary for edit signals.")
122
121
  after = props.get("after")
@@ -124,7 +123,7 @@ class EditSignal(BaseSignal):
124
123
  raise ValueError(
125
124
  "'properties' must contain a non-empty string 'after' for edit signals."
126
125
  )
127
- return values
126
+ return self
128
127
 
129
128
 
130
129
  # Discriminated Union for Signal Events
@@ -0,0 +1 @@
1
+ VERSION = "0.0.32"
@@ -1 +0,0 @@
1
- VERSION = "0.0.30"
File without changes