raindrop-ai 0.0.26__tar.gz → 0.0.27__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.26 → raindrop_ai-0.0.27}/PKG-INFO +1 -1
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/pyproject.toml +1 -1
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/analytics.py +89 -35
- raindrop_ai-0.0.27/raindrop/version.py +1 -0
- raindrop_ai-0.0.26/raindrop/version.py +0 -1
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/README.md +0 -0
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/__init__.py +0 -0
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/interaction.py +0 -0
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/models.py +0 -0
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/redact.py +0 -0
- {raindrop_ai-0.0.26 → raindrop_ai-0.0.27}/raindrop/well-known-names.json +0 -0
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
339
|
+
else: # signal_type == "default"
|
|
305
340
|
if comment is not None:
|
|
306
|
-
logger.warning(
|
|
341
|
+
logger.warning(
|
|
342
|
+
"'comment' argument provided for non-feedback signal type; ignored."
|
|
343
|
+
)
|
|
307
344
|
if after is not None:
|
|
308
|
-
logger.warning(
|
|
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=
|
|
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
|
-
#
|
|
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
|
|
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 =
|
|
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] =
|
|
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(
|
|
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
|
-
|
|
481
|
+
|
|
482
|
+
atexit.register(shutdown)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.0.27"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.0.26"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|