raindrop-ai 0.0.26__py3-none-any.whl → 0.0.27__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
@@ -11,14 +11,24 @@ import atexit
11
11
  from pydantic import ValidationError
12
12
  from threading import Timer
13
13
  from raindrop.version import VERSION
14
- from raindrop.models import TrackAIEvent, Attachment, SignalEvent, DefaultSignal, FeedbackSignal, EditSignal, PartialTrackAIEvent, PartialAIData
14
+ from raindrop.models import (
15
+ TrackAIEvent,
16
+ Attachment,
17
+ SignalEvent,
18
+ DefaultSignal,
19
+ FeedbackSignal,
20
+ EditSignal,
21
+ PartialTrackAIEvent,
22
+ PartialAIData,
23
+ )
15
24
  from raindrop.interaction import Interaction
16
25
  from raindrop.redact import perform_pii_redaction
17
26
 
18
27
 
19
-
20
28
  # Configure logging
21
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
29
+ logging.basicConfig(
30
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
31
+ )
22
32
  logger = logging.getLogger(__name__)
23
33
 
24
34
  write_key = None
@@ -36,7 +46,8 @@ max_ingest_size_bytes = 1 * 1024 * 1024 # 1 MB
36
46
 
37
47
  _partial_buffers: dict[str, PartialTrackAIEvent] = {}
38
48
  _partial_timers: dict[str, Timer] = {}
39
- _PARTIAL_TIMEOUT = 2 # 2 seconds
49
+ _PARTIAL_TIMEOUT = 2 # 2 seconds
50
+
40
51
 
41
52
  def set_debug_logs(value: bool):
42
53
  global debug_logs
@@ -46,6 +57,7 @@ def set_debug_logs(value: bool):
46
57
  else:
47
58
  logger.setLevel(logging.INFO)
48
59
 
60
+
49
61
  def set_redact_pii(value: bool):
50
62
  global redact_pii
51
63
  redact_pii = value
@@ -54,6 +66,7 @@ def set_redact_pii(value: bool):
54
66
  else:
55
67
  logger.info("PII redaction disabled")
56
68
 
69
+
57
70
  def start_flush_thread():
58
71
  logger.debug("Opening flush thread")
59
72
  global flush_thread
@@ -62,6 +75,7 @@ def start_flush_thread():
62
75
  flush_thread.daemon = True
63
76
  flush_thread.start()
64
77
 
78
+
65
79
  def flush_loop():
66
80
  while not shutdown_event.is_set():
67
81
  try:
@@ -70,6 +84,7 @@ def flush_loop():
70
84
  logger.error(f"Error in flush loop: {e}")
71
85
  time.sleep(upload_interval)
72
86
 
87
+
73
88
  def flush() -> None:
74
89
  global buffer
75
90
 
@@ -95,14 +110,17 @@ def flush() -> None:
95
110
 
96
111
  for endpoint, events_data in grouped_events.items():
97
112
  for i in range(0, len(events_data), upload_size):
98
- batch = events_data[i:i+upload_size]
113
+ batch = events_data[i : i + upload_size]
99
114
  logger.debug(f"Sending {len(batch)} events to {endpoint}")
100
115
  send_request(endpoint, batch)
101
116
 
102
117
  logger.debug("Flush complete")
103
118
 
104
- def send_request(endpoint: str, data_entries: List[Dict[str, Union[str, Dict]]]) -> None:
105
-
119
+
120
+ def send_request(
121
+ endpoint: str, data_entries: List[Dict[str, Union[str, Dict]]]
122
+ ) -> None:
123
+
106
124
  url = f"{api_url}{endpoint}"
107
125
  headers = {
108
126
  "Content-Type": "application/json",
@@ -117,15 +135,20 @@ def send_request(endpoint: str, data_entries: List[Dict[str, Union[str, Dict]]])
117
135
  logger.debug(f"Request successful: {response.status_code}")
118
136
  break
119
137
  except requests.exceptions.RequestException as e:
120
- logger.error(f"Error sending request (attempt {attempt + 1}/{max_retries}): {e}")
138
+ logger.error(
139
+ f"Error sending request (attempt {attempt + 1}/{max_retries}): {e}"
140
+ )
121
141
  if attempt == max_retries - 1:
122
142
  logger.error(f"Failed to send request after {max_retries} attempts")
123
143
 
144
+
124
145
  def save_to_buffer(event: Dict[str, Union[str, Dict]]) -> None:
125
146
  global buffer
126
147
 
127
148
  if len(buffer) >= max_queue_size * 0.8:
128
- logger.warning(f"Buffer is at {len(buffer) / max_queue_size * 100:.2f}% capacity")
149
+ logger.warning(
150
+ f"Buffer is at {len(buffer) / max_queue_size * 100:.2f}% capacity"
151
+ )
129
152
 
130
153
  if len(buffer) >= max_queue_size:
131
154
  logger.error("Buffer is full. Discarding event.")
@@ -138,6 +161,7 @@ def save_to_buffer(event: Dict[str, Union[str, Dict]]) -> None:
138
161
 
139
162
  start_flush_thread()
140
163
 
164
+
141
165
  def identify(user_id: str, traits: Dict[str, Union[str, int, bool, float]]) -> None:
142
166
  if not _check_write_key():
143
167
  return
@@ -159,7 +183,7 @@ def track_ai(
159
183
  ) -> str:
160
184
  if not _check_write_key():
161
185
  return
162
-
186
+
163
187
  event_id = event_id or str(uuid.uuid4())
164
188
 
165
189
  try:
@@ -169,7 +193,7 @@ def track_ai(
169
193
  event=event,
170
194
  timestamp=timestamp or _get_timestamp(),
171
195
  properties=properties or {},
172
- ai_data=dict( # Pydantic will coerce to AIData
196
+ ai_data=dict( # Pydantic will coerce to AIData
173
197
  model=model,
174
198
  input=input,
175
199
  output=output,
@@ -181,13 +205,12 @@ def track_ai(
181
205
  logger.error(f"[raindrop] Invalid data passed to track_ai: {err}")
182
206
  return None
183
207
 
184
-
185
208
  if payload.properties is None:
186
209
  payload.properties = {}
187
210
  payload.properties["$context"] = _get_context()
188
211
 
189
212
  data = payload.model_dump(mode="json")
190
-
213
+
191
214
  # Apply PII redaction if enabled
192
215
  if redact_pii:
193
216
  data = perform_pii_redaction(data)
@@ -203,22 +226,27 @@ def track_ai(
203
226
  save_to_buffer({"type": "events/track", "data": data})
204
227
  return event_id
205
228
 
229
+
206
230
  def shutdown():
207
231
  logger.info("Shutting down raindrop analytics")
208
232
  for eid in list(_partial_timers.keys()):
209
- _flush_partial_event(eid)
233
+ _flush_partial_event(eid)
210
234
 
211
235
  shutdown_event.set()
212
236
  if flush_thread:
213
237
  flush_thread.join(timeout=10)
214
238
  flush() # Final flush to ensure all events are sent
215
239
 
240
+
216
241
  def _check_write_key():
217
242
  if write_key is None:
218
- logger.warning("write_key is not set. Please set it before using raindrop analytics.")
243
+ logger.warning(
244
+ "write_key is not set. Please set it before using raindrop analytics."
245
+ )
219
246
  return False
220
247
  return True
221
248
 
249
+
222
250
  def _get_context():
223
251
  return {
224
252
  "library": {
@@ -230,6 +258,7 @@ def _get_context():
230
258
  },
231
259
  }
232
260
 
261
+
233
262
  def _get_timestamp():
234
263
  return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
235
264
 
@@ -238,14 +267,16 @@ def _get_size(event: dict[str, any]) -> int:
238
267
  try:
239
268
  # Add default=str to handle types like datetime
240
269
  data = json.dumps(event, default=str)
241
- return len(data.encode('utf-8'))
270
+ return len(data.encode("utf-8"))
242
271
  except (TypeError, OverflowError) as e:
243
272
  logger.error(f"Error serializing event for size calculation: {e}")
244
- return 0
273
+ return 0
274
+
245
275
 
246
276
  # Signal types - This is now defined in models.py
247
277
  # SignalType = Literal["default", "feedback", "edit"]
248
278
 
279
+
249
280
  def track_signal(
250
281
  event_id: str,
251
282
  name: str,
@@ -278,11 +309,15 @@ def track_signal(
278
309
  final_properties = properties.copy() if properties else {}
279
310
  if signal_type == "feedback" and comment is not None:
280
311
  if "comment" in final_properties:
281
- logger.warning("'comment' provided as both argument and in properties; argument value used.")
312
+ logger.warning(
313
+ "'comment' provided as both argument and in properties; argument value used."
314
+ )
282
315
  final_properties["comment"] = comment
283
316
  elif signal_type == "edit" and after is not None:
284
317
  if "after" in final_properties:
285
- logger.warning("'after' provided as both argument and in properties; argument value used.")
318
+ logger.warning(
319
+ "'after' provided as both argument and in properties; argument value used."
320
+ )
286
321
  final_properties["after"] = after
287
322
 
288
323
  # Prepare base arguments for all signal types
@@ -292,7 +327,7 @@ def track_signal(
292
327
  "timestamp": timestamp or _get_timestamp(),
293
328
  "properties": final_properties,
294
329
  "attachment_id": attachment_id,
295
- "sentiment": sentiment
330
+ "sentiment": sentiment,
296
331
  }
297
332
 
298
333
  try:
@@ -301,11 +336,15 @@ def track_signal(
301
336
  payload = FeedbackSignal(**base_args, signal_type=signal_type)
302
337
  elif signal_type == "edit":
303
338
  payload = EditSignal(**base_args, signal_type=signal_type)
304
- else: # signal_type == "default"
339
+ else: # signal_type == "default"
305
340
  if comment is not None:
306
- logger.warning("'comment' argument provided for non-feedback signal type; ignored.")
341
+ logger.warning(
342
+ "'comment' argument provided for non-feedback signal type; ignored."
343
+ )
307
344
  if after is not None:
308
- logger.warning("'after' argument provided for non-edit signal type; ignored.")
345
+ logger.warning(
346
+ "'after' argument provided for non-edit signal type; ignored."
347
+ )
309
348
  payload = DefaultSignal(**base_args, signal_type=signal_type)
310
349
 
311
350
  except ValidationError as err:
@@ -313,7 +352,7 @@ def track_signal(
313
352
  return None
314
353
 
315
354
  # model_dump handles the timestamp correctly
316
- data = payload.model_dump(mode='json')
355
+ data = payload.model_dump(mode="json")
317
356
 
318
357
  size = _get_size(data)
319
358
  if size > max_ingest_size_bytes:
@@ -325,6 +364,7 @@ def track_signal(
325
364
 
326
365
  save_to_buffer({"type": "signals/track", "data": data})
327
366
 
367
+
328
368
  def begin(
329
369
  user_id: str,
330
370
  event: str,
@@ -338,10 +378,10 @@ def begin(
338
378
  Starts (or resumes) an interaction and returns a helper object.
339
379
  """
340
380
  eid = event_id or str(uuid.uuid4())
341
-
342
- # Prepare ai_data if input is provided
381
+
382
+ # Instantiate ai_data if either input or convo_id is supplied so that convo_id isn't lost when input is set later
343
383
  ai_data_partial = None
344
- if input:
384
+ if input is not None or convo_id is not None:
345
385
  ai_data_partial = PartialAIData(input=input, convo_id=convo_id)
346
386
 
347
387
  # Combine properties with initial_fields, giving precedence to initial_fields if keys clash
@@ -352,17 +392,20 @@ def begin(
352
392
  user_id=user_id,
353
393
  event=event,
354
394
  ai_data=ai_data_partial,
355
- properties=final_properties or None, # Pass None if empty, matching PartialTrackAIEvent defaults
395
+ properties=final_properties
396
+ or None, # Pass None if empty, matching PartialTrackAIEvent defaults
356
397
  attachments=attachments,
357
398
  )
358
399
 
359
400
  _track_ai_partial(partial_event)
360
401
  return Interaction(eid)
361
402
 
403
+
362
404
  def resume_interaction(event_id: str) -> Interaction:
363
405
  """Return an Interaction wrapper for an existing eventId."""
364
406
  return Interaction(event_id)
365
407
 
408
+
366
409
  def _track_ai_partial(event: PartialTrackAIEvent) -> None:
367
410
  """
368
411
  Merge the incoming patch into an in-memory doc and flush to backend:
@@ -373,15 +416,22 @@ def _track_ai_partial(event: PartialTrackAIEvent) -> None:
373
416
 
374
417
  # 1. merge
375
418
  existing = _partial_buffers.get(eid, PartialTrackAIEvent(event_id=eid))
376
- existing.is_pending = existing.is_pending if existing.is_pending is not None else True
419
+ existing.is_pending = (
420
+ existing.is_pending if existing.is_pending is not None else True
421
+ )
377
422
  merged_dict = existing.model_dump(exclude_none=True)
378
423
  incoming = event.model_dump(exclude_none=True)
379
424
 
380
425
  # deep merge ai_data / properties
381
426
  def _deep(d: dict, u: dict):
382
427
  for k, v in u.items():
383
- d[k] = _deep(d.get(k, {}) if isinstance(v, dict) else v, v) if isinstance(v, dict) else v
428
+ d[k] = (
429
+ _deep(d.get(k, {}) if isinstance(v, dict) else v, v)
430
+ if isinstance(v, dict)
431
+ else v
432
+ )
384
433
  return d
434
+
385
435
  merged = _deep(merged_dict, incoming)
386
436
  merged_obj = PartialTrackAIEvent(**merged)
387
437
 
@@ -398,7 +448,10 @@ def _track_ai_partial(event: PartialTrackAIEvent) -> None:
398
448
  _partial_timers[eid].start()
399
449
 
400
450
  if debug_logs:
401
- logger.debug(f"[raindrop] updated partial {eid}: {merged_obj.model_dump(exclude_none=True)}")
451
+ logger.debug(
452
+ f"[raindrop] updated partial {eid}: {merged_obj.model_dump(exclude_none=True)}"
453
+ )
454
+
402
455
 
403
456
  def _flush_partial_event(event_id: str) -> None:
404
457
  """
@@ -413,16 +466,17 @@ def _flush_partial_event(event_id: str) -> None:
413
466
 
414
467
  # convert to ordinary TrackAIEvent-ish dict before send
415
468
  data = evt.model_dump(mode="json", exclude_none=True)
416
-
469
+
417
470
  # Apply PII redaction if enabled
418
471
  if redact_pii:
419
472
  data = perform_pii_redaction(data)
420
-
473
+
421
474
  size = _get_size(data)
422
475
  if size > max_ingest_size_bytes:
423
476
  logger.warning(f"[raindrop] partial event {event_id} > 1 MB; skipping")
424
477
  return
425
-
478
+
426
479
  send_request("events/track_partial", data)
427
480
 
428
- atexit.register(shutdown)
481
+
482
+ atexit.register(shutdown)
raindrop/version.py CHANGED
@@ -1 +1 @@
1
- VERSION = "0.0.26"
1
+ VERSION = "0.0.27"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: raindrop-ai
3
- Version: 0.0.26
3
+ Version: 0.0.27
4
4
  Summary: Raindrop AI (Python SDK)
5
5
  License: MIT
6
6
  Author: Raindrop AI
@@ -1,10 +1,10 @@
1
1
  raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- raindrop/analytics.py,sha256=XlwCh0n9zuktHCbEjqCWpci5juZvHPAtV-M56jrfxac,14153
2
+ raindrop/analytics.py,sha256=T7oUrbtZmB0CkC2Xlp9mm_ue7UvqmnTe8w5SMhr3kKc,14618
3
3
  raindrop/interaction.py,sha256=U8OapN5tZGSMVkn-dTLum5sIg_AgSYKX9lrtwgXJOuI,1613
4
4
  raindrop/models.py,sha256=8YKuyB34P7sfawPFL3jOt5RgYlNXzf9dEsj8_2rjZKU,5293
5
5
  raindrop/redact.py,sha256=rMNUoI90KxOY3d_zcHAr0TFD2yQ_CDgpDz-1XJLVmHs,7658
6
- raindrop/version.py,sha256=LPUM3wfkvVJsJ8CHEldvcNvmnVxuPYI2IwY5jSUgC7U,19
6
+ raindrop/version.py,sha256=n8NEARTC1SPwdkZnwdsRGBgkhM_5xwZKERo2I3Ak8-M,19
7
7
  raindrop/well-known-names.json,sha256=9giJF6u6W1R0APW-Pf1dvNUU32OXQEoQ9CBQXSnA3ks,144403
8
- raindrop_ai-0.0.26.dist-info/METADATA,sha256=ohdfwXzJXfkamC6WBGTjeCqUFMP5LSQnkP5U9qgEyxs,774
9
- raindrop_ai-0.0.26.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
- raindrop_ai-0.0.26.dist-info/RECORD,,
8
+ raindrop_ai-0.0.27.dist-info/METADATA,sha256=2j-yQFW80-RfUedor3HXUNU7e-c0HWpbYKlSYqFMPRk,774
9
+ raindrop_ai-0.0.27.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
+ raindrop_ai-0.0.27.dist-info/RECORD,,