raindrop-ai 0.0.35__tar.gz → 0.0.37__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.35
3
+ Version: 0.0.37
4
4
  Summary: Raindrop AI (Python SDK)
5
5
  License: MIT
6
6
  Author: Raindrop AI
@@ -15,6 +15,7 @@ Requires-Dist: opentelemetry-sdk (>=1.39.0)
15
15
  Requires-Dist: pydantic (>=2.09,<3)
16
16
  Requires-Dist: requests (>=2.32.3,<3.0.0)
17
17
  Requires-Dist: traceloop-sdk (>=0.46.0)
18
+ Requires-Dist: urllib3 (>=2.6.0)
18
19
  Description-Content-Type: text/markdown
19
20
 
20
21
  # Raindrop Python SDK
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "raindrop-ai"
3
- version = "0.0.35"
3
+ version = "0.0.37"
4
4
  description = "Raindrop AI (Python SDK)"
5
5
  authors = ["Raindrop AI <sdk@raindrop.ai>"]
6
6
  license = "MIT"
@@ -13,6 +13,7 @@ pydantic = ">=2.09,<3"
13
13
  requests = "^2.32.3"
14
14
  traceloop-sdk = ">=0.46.0"
15
15
  opentelemetry-sdk = ">=1.39.0"
16
+ urllib3 = ">=2.6.0"
16
17
 
17
18
 
18
19
  [tool.poetry.group.dev.dependencies]
@@ -66,6 +66,7 @@ __all__ = [
66
66
  "start_span",
67
67
  "ManualSpan",
68
68
  "set_span_properties",
69
+ "set_llm_span_io",
69
70
  "flush",
70
71
  "shutdown",
71
72
  ]
@@ -347,6 +348,59 @@ def _should_send_prompts():
347
348
  ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
348
349
 
349
350
 
351
+ def set_llm_span_io(
352
+ input: Any = None,
353
+ output: Any = None,
354
+ ) -> None:
355
+ """
356
+ Set LLM input/output content on the current span.
357
+
358
+ Use this to add prompt/completion content to auto-instrumented spans
359
+ that don't capture content automatically (e.g., Bedrock with aioboto3).
360
+
361
+ Args:
362
+ input: The input/prompt content (messages, text, etc.)
363
+ output: The output/completion content (response text, message, etc.)
364
+
365
+ Example:
366
+ response = await bedrock_client.converse(modelId=model, messages=messages)
367
+ raindrop.set_llm_span_io(
368
+ input=messages,
369
+ output=response["output"]["message"]["content"]
370
+ )
371
+ """
372
+ if not _should_send_prompts():
373
+ return
374
+
375
+ span = get_current_span()
376
+ if not span or not span.is_recording():
377
+ logger.debug("[raindrop] set_llm_span_io called but no active span found")
378
+ return
379
+
380
+ try:
381
+ if input is not None:
382
+ input_str = (
383
+ json.dumps(input, cls=JSONEncoder)
384
+ if not isinstance(input, str)
385
+ else input
386
+ )
387
+ input_str = _truncate_json_if_needed(input_str)
388
+ span.set_attribute("gen_ai.prompt.0.role", "user")
389
+ span.set_attribute("gen_ai.prompt.0.content", input_str)
390
+
391
+ if output is not None:
392
+ output_str = (
393
+ json.dumps(output, cls=JSONEncoder)
394
+ if not isinstance(output, str)
395
+ else output
396
+ )
397
+ output_str = _truncate_json_if_needed(output_str)
398
+ span.set_attribute("gen_ai.completion.0.role", "assistant")
399
+ span.set_attribute("gen_ai.completion.0.content", output_str)
400
+ except Exception as e:
401
+ logger.debug(f"[raindrop] Failed to record LLM content: {e}")
402
+
403
+
350
404
  # Signal types - This is now defined in models.py
351
405
  # SignalType = Literal["default", "feedback", "edit"]
352
406
 
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import time
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ List,
8
+ Literal,
9
+ Optional,
10
+ TYPE_CHECKING,
11
+ Union,
12
+ Iterator,
13
+ )
14
+ from datetime import datetime, timezone
15
+ from uuid import uuid4
16
+ from dataclasses import dataclass
17
+
18
+ from .models import Attachment, PartialTrackAIEvent
19
+ from . import analytics as _core
20
+ from opentelemetry import context as context_api
21
+
22
+ if TYPE_CHECKING:
23
+ from .analytics import ManualSpan
24
+
25
+
26
+ class Interaction:
27
+ """
28
+ Thin helper returned by analytics.begin().
29
+ Each mutator just relays a partial update back to Analytics.
30
+ """
31
+
32
+ __slots__ = (
33
+ "_event_id",
34
+ "_user_id",
35
+ "_event",
36
+ "_convo_id",
37
+ "_analytics",
38
+ "__weakref__",
39
+ )
40
+
41
+ def __init__(
42
+ self,
43
+ event_id: Optional[str] = None,
44
+ user_id: Optional[str] = None,
45
+ event: Optional[str] = None,
46
+ convo_id: Optional[str] = None,
47
+ ):
48
+ self._event_id = event_id or str(uuid4())
49
+ self._user_id = user_id
50
+ self._event = event
51
+ self._convo_id = convo_id
52
+ self._analytics = _core
53
+
54
+ # -- mutators ----------------------------------------------------------- #
55
+ def set_input(self, text: str) -> None:
56
+ self._analytics._track_ai_partial(
57
+ PartialTrackAIEvent(event_id=self._event_id, ai_data={"input": text})
58
+ )
59
+
60
+ def add_attachments(self, attachments: List[Attachment]) -> None:
61
+ self._analytics._track_ai_partial(
62
+ PartialTrackAIEvent(event_id=self._event_id, attachments=attachments)
63
+ )
64
+
65
+ def set_properties(self, props: Dict[str, Any]) -> None:
66
+ self._analytics._track_ai_partial(
67
+ PartialTrackAIEvent(event_id=self._event_id, properties=props)
68
+ )
69
+
70
+ def set_property(self, key: str, value: Any) -> None:
71
+ self.set_properties({key: value})
72
+
73
+ def finish(self, *, output: str | None = None, **extra) -> None:
74
+
75
+ payload = PartialTrackAIEvent(
76
+ event_id=self._event_id,
77
+ ai_data={"output": output} if output is not None else None,
78
+ is_pending=False,
79
+ **extra,
80
+ )
81
+ self._analytics._track_ai_partial(payload)
82
+
83
+ def start_span(
84
+ self,
85
+ kind: Literal["task", "tool"],
86
+ name: str,
87
+ version: int | None = None,
88
+ ) -> "ManualSpan":
89
+ """
90
+ Create a manual span tied to this interaction.
91
+
92
+ The span automatically inherits association properties from this interaction
93
+ (event_id, user_id, event, convo_id) for proper tracing.
94
+
95
+ Args:
96
+ kind: Type of span - "task" or "tool"
97
+ name: Name of the span
98
+ version: Optional version number
99
+
100
+ Returns:
101
+ ManualSpan instance that must be explicitly ended with .end()
102
+ """
103
+ return self._analytics.start_span(
104
+ kind,
105
+ name,
106
+ version,
107
+ event_id=self._event_id,
108
+ user_id=self._user_id,
109
+ event=self._event,
110
+ convo_id=self._convo_id,
111
+ )
112
+
113
+ def track_tool(
114
+ self,
115
+ *,
116
+ name: str,
117
+ input: Any | None = None,
118
+ output: Any | None = None,
119
+ duration_ms: float | int | None = None,
120
+ start_time: datetime | int | float | None = None,
121
+ error: BaseException | str | None = None,
122
+ properties: Dict[str, Any] | None = None,
123
+ version: int | None = None,
124
+ ) -> None:
125
+ """
126
+ Retroactively log a tool span tied to this interaction.
127
+ """
128
+ if not _core._tracing_enabled or not _core.TracerWrapper.verify_initialized():
129
+ return
130
+
131
+ # Duration normalization
132
+ dur_ms = float(duration_ms) if duration_ms is not None else 0.0
133
+ if dur_ms < 0:
134
+ dur_ms = 0.0
135
+ duration_ns = int(round(dur_ms * 1_000_000))
136
+
137
+ # start_time normalization (epoch nanoseconds)
138
+ start_ns: int | None = None
139
+ if isinstance(start_time, datetime):
140
+ dt = start_time
141
+ if dt.tzinfo is None:
142
+ dt = dt.replace(tzinfo=timezone.utc)
143
+ else:
144
+ dt = dt.astimezone(timezone.utc)
145
+
146
+ epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
147
+ delta = dt - epoch
148
+ total_us = (
149
+ delta.days * 86400 + delta.seconds
150
+ ) * 1_000_000 + delta.microseconds
151
+ start_ns = total_us * 1_000
152
+ elif isinstance(start_time, (int, float)):
153
+ v = float(start_time)
154
+ # Heuristic: values smaller than ~1973 in ms are likely epoch-seconds
155
+ start_ns = (
156
+ int(round(v * 1_000_000_000))
157
+ if abs(v) < 1e11
158
+ else int(round(v * 1_000_000))
159
+ )
160
+
161
+ if start_ns is None:
162
+ start_ns = time.time_ns() - duration_ns
163
+
164
+ end_ns = start_ns + duration_ns
165
+
166
+ tlp_kind = _core.TraceloopSpanKindValues.TOOL
167
+ span_name = f"{name}.{tlp_kind.value}"
168
+
169
+ tracer = _core.trace.get_tracer("traceloop.tracer")
170
+ span = tracer.start_span(span_name, start_time=start_ns)
171
+
172
+ try:
173
+ span.set_attribute(_core.SpanAttributes.TRACELOOP_SPAN_KIND, tlp_kind.value)
174
+ span.set_attribute(_core.SpanAttributes.TRACELOOP_ENTITY_NAME, name)
175
+ if version is not None:
176
+ span.set_attribute(
177
+ _core.SpanAttributes.TRACELOOP_ENTITY_VERSION, version
178
+ )
179
+
180
+ association_props = {
181
+ "event_id": self._event_id,
182
+ "user_id": self._user_id,
183
+ "event": self._event,
184
+ "convo_id": self._convo_id,
185
+ }
186
+ for key, value in association_props.items():
187
+ if value is not None:
188
+ span.set_attribute(f"traceloop.association.properties.{key}", value)
189
+
190
+ if properties:
191
+ for key, value in properties.items():
192
+ if key in association_props:
193
+ continue
194
+ if value is None:
195
+ continue
196
+ if isinstance(value, (str, bool, int, float)):
197
+ attr_val: Any = value
198
+ else:
199
+ try:
200
+ attr_val = json.dumps(value, cls=_core.JSONEncoder)
201
+ except Exception:
202
+ attr_val = str(value)
203
+ span.set_attribute(
204
+ f"traceloop.association.properties.{key}", attr_val
205
+ )
206
+
207
+ if duration_ms is not None:
208
+ span.set_attribute("traceloop.entity.duration_ms", dur_ms)
209
+
210
+ if _core._should_send_prompts():
211
+ if input is not None:
212
+ try:
213
+ json_input = json.dumps(
214
+ {"args": [input]}, cls=_core.JSONEncoder
215
+ )
216
+ span.set_attribute(
217
+ _core.SpanAttributes.TRACELOOP_ENTITY_INPUT,
218
+ _core._truncate_json_if_needed(json_input),
219
+ )
220
+ except Exception as e:
221
+ _core.logger.debug(
222
+ f"[raindrop] Could not serialize input for span: {e}"
223
+ )
224
+
225
+ if output is not None:
226
+ try:
227
+ json_output = json.dumps(output, cls=_core.JSONEncoder)
228
+ span.set_attribute(
229
+ _core.SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
230
+ _core._truncate_json_if_needed(json_output),
231
+ )
232
+ except Exception as e:
233
+ _core.logger.debug(
234
+ f"[raindrop] Could not serialize output for span: {e}"
235
+ )
236
+
237
+ if error is not None:
238
+ exc = (
239
+ error if isinstance(error, BaseException) else Exception(str(error))
240
+ )
241
+ span.set_status(_core.Status(_core.StatusCode.ERROR, str(exc)))
242
+ span.record_exception(exc)
243
+ else:
244
+ span.set_status(_core.Status(_core.StatusCode.OK))
245
+ finally:
246
+ span.end(end_time=end_ns)
247
+
248
+ if _core.debug_logs:
249
+ _core.logger.debug(
250
+ f'[raindrop] track_tool: logged tool span "{name}" (duration_ms={duration_ms})'
251
+ )
252
+
253
+ # convenience
254
+ @property
255
+ def id(self) -> str:
256
+ return self._event_id
@@ -0,0 +1 @@
1
+ VERSION = "0.0.37"
@@ -1,113 +0,0 @@
1
- from __future__ import annotations
2
- from typing import (
3
- Any,
4
- Dict,
5
- List,
6
- Literal,
7
- Optional,
8
- TYPE_CHECKING,
9
- Union,
10
- Iterator,
11
- )
12
- from uuid import uuid4
13
- from dataclasses import dataclass
14
-
15
- from .models import Attachment, PartialTrackAIEvent
16
- from . import analytics as _core
17
- from opentelemetry import context as context_api
18
-
19
- if TYPE_CHECKING:
20
- from .analytics import ManualSpan
21
-
22
-
23
- class Interaction:
24
- """
25
- Thin helper returned by analytics.begin().
26
- Each mutator just relays a partial update back to Analytics.
27
- """
28
-
29
- __slots__ = (
30
- "_event_id",
31
- "_user_id",
32
- "_event",
33
- "_convo_id",
34
- "_analytics",
35
- "__weakref__",
36
- )
37
-
38
- def __init__(
39
- self,
40
- event_id: Optional[str] = None,
41
- user_id: Optional[str] = None,
42
- event: Optional[str] = None,
43
- convo_id: Optional[str] = None,
44
- ):
45
- self._event_id = event_id or str(uuid4())
46
- self._user_id = user_id
47
- self._event = event
48
- self._convo_id = convo_id
49
- self._analytics = _core
50
-
51
- # -- mutators ----------------------------------------------------------- #
52
- def set_input(self, text: str) -> None:
53
- self._analytics._track_ai_partial(
54
- PartialTrackAIEvent(event_id=self._event_id, ai_data={"input": text})
55
- )
56
-
57
- def add_attachments(self, attachments: List[Attachment]) -> None:
58
- self._analytics._track_ai_partial(
59
- PartialTrackAIEvent(event_id=self._event_id, attachments=attachments)
60
- )
61
-
62
- def set_properties(self, props: Dict[str, Any]) -> None:
63
- self._analytics._track_ai_partial(
64
- PartialTrackAIEvent(event_id=self._event_id, properties=props)
65
- )
66
-
67
- def set_property(self, key: str, value: Any) -> None:
68
- self.set_properties({key: value})
69
-
70
- def finish(self, *, output: str | None = None, **extra) -> None:
71
-
72
- payload = PartialTrackAIEvent(
73
- event_id=self._event_id,
74
- ai_data={"output": output} if output is not None else None,
75
- is_pending=False,
76
- **extra,
77
- )
78
- self._analytics._track_ai_partial(payload)
79
-
80
- def start_span(
81
- self,
82
- kind: Literal["task", "tool"],
83
- name: str,
84
- version: int | None = None,
85
- ) -> "ManualSpan":
86
- """
87
- Create a manual span tied to this interaction.
88
-
89
- The span automatically inherits association properties from this interaction
90
- (event_id, user_id, event, convo_id) for proper tracing.
91
-
92
- Args:
93
- kind: Type of span - "task" or "tool"
94
- name: Name of the span
95
- version: Optional version number
96
-
97
- Returns:
98
- ManualSpan instance that must be explicitly ended with .end()
99
- """
100
- return self._analytics.start_span(
101
- kind,
102
- name,
103
- version,
104
- event_id=self._event_id,
105
- user_id=self._user_id,
106
- event=self._event,
107
- convo_id=self._convo_id,
108
- )
109
-
110
- # convenience
111
- @property
112
- def id(self) -> str:
113
- return self._event_id
@@ -1 +0,0 @@
1
- VERSION = "0.0.35"
File without changes