raindrop-ai 0.0.35__py3-none-any.whl → 0.0.37__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.
raindrop/analytics.py CHANGED
@@ -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
 
raindrop/interaction.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from __future__ import annotations
2
+ import json
3
+ import time
2
4
  from typing import (
3
5
  Any,
4
6
  Dict,
@@ -9,6 +11,7 @@ from typing import (
9
11
  Union,
10
12
  Iterator,
11
13
  )
14
+ from datetime import datetime, timezone
12
15
  from uuid import uuid4
13
16
  from dataclasses import dataclass
14
17
 
@@ -107,6 +110,146 @@ class Interaction:
107
110
  convo_id=self._convo_id,
108
111
  )
109
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
+
110
253
  # convenience
111
254
  @property
112
255
  def id(self) -> str:
raindrop/version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.0.35"
1
+ VERSION = "0.0.37"
@@ -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
@@ -0,0 +1,10 @@
1
+ raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ raindrop/analytics.py,sha256=dL64bp5Sk1gq5PYcKAdQBNlmrn-5aW-LXTa0C3CApKA,30335
3
+ raindrop/interaction.py,sha256=qMTGN-nMDeYmujLhBqIUPq4pKDQx6HX0fqVwB2aq0eA,8639
4
+ raindrop/models.py,sha256=9lOOUQ2FF11RPkntuLZwN3e54pa9HtR8lGvCbzlWOPM,5198
5
+ raindrop/redact.py,sha256=rMNUoI90KxOY3d_zcHAr0TFD2yQ_CDgpDz-1XJLVmHs,7658
6
+ raindrop/version.py,sha256=bPu3tNWPJ9tg3HaQ8cbDkapv1vcKPLFyWLaaRG7eW2Q,19
7
+ raindrop/well-known-names.json,sha256=9giJF6u6W1R0APW-Pf1dvNUU32OXQEoQ9CBQXSnA3ks,144403
8
+ raindrop_ai-0.0.37.dist-info/METADATA,sha256=BO2HCUOURX8Rlhyu4L_XCon7XwS9ln1EP3qEfk6topY,1346
9
+ raindrop_ai-0.0.37.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
+ raindrop_ai-0.0.37.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- raindrop/analytics.py,sha256=uS4D8_r9VEzIVJgPDBpT7Nkr2i1PS9Yj_QCWgqGEBNM,28507
3
- raindrop/interaction.py,sha256=Kw8HNaDSHVIi1p-2HK5SsYGI4Xg1zQFml4dX2EhIT7w,3123
4
- raindrop/models.py,sha256=9lOOUQ2FF11RPkntuLZwN3e54pa9HtR8lGvCbzlWOPM,5198
5
- raindrop/redact.py,sha256=rMNUoI90KxOY3d_zcHAr0TFD2yQ_CDgpDz-1XJLVmHs,7658
6
- raindrop/version.py,sha256=XbHQAtEbxGWuqWT-2KR0HWozWyaEyD9WkdIKQJfT_OA,19
7
- raindrop/well-known-names.json,sha256=9giJF6u6W1R0APW-Pf1dvNUU32OXQEoQ9CBQXSnA3ks,144403
8
- raindrop_ai-0.0.35.dist-info/METADATA,sha256=0mJdipZfyB_rnDfBZjsnINUjiKqiJBSyUEMokqy6Bpg,1313
9
- raindrop_ai-0.0.35.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
- raindrop_ai-0.0.35.dist-info/RECORD,,