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.
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/PKG-INFO +1 -1
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/pyproject.toml +1 -1
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/analytics.py +141 -6
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/models.py +12 -13
- raindrop_ai-0.0.32/raindrop/version.py +1 -0
- raindrop_ai-0.0.30/raindrop/version.py +0 -1
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/README.md +0 -0
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/__init__.py +0 -0
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/interaction.py +0 -0
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/redact.py +0 -0
- {raindrop_ai-0.0.30 → raindrop_ai-0.0.32}/raindrop/well-known-names.json +0 -0
|
@@ -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
|
|
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(
|
|
20
|
-
if not
|
|
19
|
+
def _require_value(self):
|
|
20
|
+
if not self.value:
|
|
21
21
|
raise ValueError("value must be non-empty.")
|
|
22
|
-
return
|
|
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(
|
|
33
|
-
if not (
|
|
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
|
|
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(
|
|
97
|
+
def _check_comment_in_properties(self):
|
|
98
98
|
# Check properties safely after potential initialization
|
|
99
|
-
|
|
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
|
|
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(
|
|
116
|
+
def _check_after_in_properties(self):
|
|
118
117
|
# Check properties safely after potential initialization
|
|
119
|
-
props =
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|