raindrop-ai 0.0.19__py3-none-any.whl → 0.0.21__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 +182 -60
- raindrop/interaction.py +49 -0
- raindrop/models.py +133 -0
- raindrop_ai-0.0.21.dist-info/METADATA +41 -0
- raindrop_ai-0.0.21.dist-info/RECORD +8 -0
- {raindrop_ai-0.0.19.dist-info → raindrop_ai-0.0.21.dist-info}/WHEEL +1 -2
- raindrop_ai-0.0.19.dist-info/METADATA +0 -28
- raindrop_ai-0.0.19.dist-info/RECORD +0 -7
- raindrop_ai-0.0.19.dist-info/top_level.txt +0 -1
raindrop/analytics.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import time
|
|
3
3
|
import threading
|
|
4
|
-
from typing import Union, List, Dict, Optional, Literal
|
|
4
|
+
from typing import Union, List, Dict, Optional, Literal, Any
|
|
5
5
|
import requests
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
import logging
|
|
8
8
|
import json
|
|
9
9
|
import uuid
|
|
10
10
|
import atexit
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
from threading import Timer
|
|
11
13
|
from raindrop.version import VERSION
|
|
14
|
+
from raindrop.models import TrackAIEvent, Attachment, SignalEvent, DefaultSignal, FeedbackSignal, EditSignal, PartialTrackAIEvent, PartialAIData
|
|
15
|
+
from raindrop.interaction import Interaction
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
# Configure logging
|
|
@@ -27,6 +32,10 @@ flush_thread = None
|
|
|
27
32
|
shutdown_event = threading.Event()
|
|
28
33
|
max_ingest_size_bytes = 1 * 1024 * 1024 # 1 MB
|
|
29
34
|
|
|
35
|
+
_partial_buffers: dict[str, PartialTrackAIEvent] = {}
|
|
36
|
+
_partial_timers: dict[str, Timer] = {}
|
|
37
|
+
_PARTIAL_TIMEOUT = 2 # 2 seconds
|
|
38
|
+
|
|
30
39
|
def set_debug_logs(value: bool):
|
|
31
40
|
global debug_logs
|
|
32
41
|
debug_logs = value
|
|
@@ -125,58 +134,49 @@ def identify(user_id: str, traits: Dict[str, Union[str, int, bool, float]]) -> N
|
|
|
125
134
|
data = {"user_id": user_id, "traits": traits}
|
|
126
135
|
save_to_buffer({"type": "users/identify", "data": data})
|
|
127
136
|
|
|
128
|
-
def track(
|
|
129
|
-
user_id: str,
|
|
130
|
-
event: str,
|
|
131
|
-
properties: Optional[Dict[str, Union[str, int, bool, float]]] = None,
|
|
132
|
-
timestamp: Optional[str] = None,
|
|
133
|
-
) -> None:
|
|
134
|
-
if not _check_write_key():
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
data = {
|
|
138
|
-
"event_id": str(uuid.uuid4()),
|
|
139
|
-
"user_id": user_id,
|
|
140
|
-
"event": event,
|
|
141
|
-
"properties": properties,
|
|
142
|
-
"timestamp": timestamp if timestamp else _get_timestamp(),
|
|
143
|
-
}
|
|
144
|
-
data.setdefault("properties", {})["$context"] = _get_context()
|
|
145
|
-
|
|
146
|
-
save_to_buffer({"type": "events/track", "data": data})
|
|
147
137
|
|
|
148
138
|
def track_ai(
|
|
149
139
|
user_id: str,
|
|
150
140
|
event: str,
|
|
141
|
+
event_id: Optional[str] = None,
|
|
151
142
|
model: Optional[str] = None,
|
|
152
|
-
|
|
143
|
+
input: Optional[str] = None,
|
|
153
144
|
output: Optional[str] = None,
|
|
154
145
|
convo_id: Optional[str] = None,
|
|
155
146
|
properties: Optional[Dict[str, Union[str, int, bool, float]]] = None,
|
|
156
147
|
timestamp: Optional[str] = None,
|
|
157
|
-
|
|
148
|
+
attachments: Optional[List[Attachment]] = None,
|
|
149
|
+
) -> str:
|
|
158
150
|
if not _check_write_key():
|
|
159
151
|
return
|
|
152
|
+
|
|
153
|
+
event_id = event_id or str(uuid.uuid4())
|
|
160
154
|
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
try:
|
|
156
|
+
payload = TrackAIEvent(
|
|
157
|
+
event_id=event_id,
|
|
158
|
+
user_id=user_id,
|
|
159
|
+
event=event,
|
|
160
|
+
timestamp=timestamp or _get_timestamp(),
|
|
161
|
+
properties=properties or {},
|
|
162
|
+
ai_data=dict( # Pydantic will coerce to AIData
|
|
163
|
+
model=model,
|
|
164
|
+
input=input,
|
|
165
|
+
output=output,
|
|
166
|
+
convo_id=convo_id,
|
|
167
|
+
),
|
|
168
|
+
attachments=attachments,
|
|
169
|
+
)
|
|
170
|
+
except ValidationError as err:
|
|
171
|
+
logger.error(f"[raindrop] Invalid data passed to track_ai: {err}")
|
|
172
|
+
return None
|
|
163
173
|
|
|
164
|
-
event_id = str(uuid.uuid4())
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"timestamp": timestamp if timestamp else _get_timestamp(),
|
|
172
|
-
"ai_data": {
|
|
173
|
-
"model": model,
|
|
174
|
-
"input": user_input,
|
|
175
|
-
"output": output,
|
|
176
|
-
"convo_id": convo_id,
|
|
177
|
-
},
|
|
178
|
-
}
|
|
179
|
-
data.setdefault("properties", {})["$context"] = _get_context()
|
|
175
|
+
if payload.properties is None:
|
|
176
|
+
payload.properties = {}
|
|
177
|
+
payload.properties["$context"] = _get_context()
|
|
178
|
+
|
|
179
|
+
data = payload.model_dump()
|
|
180
180
|
|
|
181
181
|
size = _get_size(data)
|
|
182
182
|
if size > max_ingest_size_bytes:
|
|
@@ -191,6 +191,9 @@ def track_ai(
|
|
|
191
191
|
|
|
192
192
|
def shutdown():
|
|
193
193
|
logger.info("Shutting down raindrop analytics")
|
|
194
|
+
for eid in list(_partial_timers.keys()):
|
|
195
|
+
_flush_partial_event(eid)
|
|
196
|
+
|
|
194
197
|
shutdown_event.set()
|
|
195
198
|
if flush_thread:
|
|
196
199
|
flush_thread.join(timeout=10)
|
|
@@ -219,24 +222,25 @@ def _get_timestamp():
|
|
|
219
222
|
|
|
220
223
|
def _get_size(event: dict[str, any]) -> int:
|
|
221
224
|
try:
|
|
222
|
-
|
|
225
|
+
# Add default=str to handle types like datetime
|
|
226
|
+
data = json.dumps(event, default=str)
|
|
223
227
|
return len(data.encode('utf-8'))
|
|
224
228
|
except (TypeError, OverflowError) as e:
|
|
225
229
|
logger.error(f"Error serializing event for size calculation: {e}")
|
|
226
230
|
return 0
|
|
227
231
|
|
|
228
|
-
# Signal types
|
|
229
|
-
SignalType = Literal["default", "feedback", "edit"]
|
|
232
|
+
# Signal types - This is now defined in models.py
|
|
233
|
+
# SignalType = Literal["default", "feedback", "edit"]
|
|
230
234
|
|
|
231
235
|
def track_signal(
|
|
232
236
|
event_id: str,
|
|
233
237
|
name: str,
|
|
234
|
-
signal_type:
|
|
238
|
+
signal_type: Literal["default", "feedback", "edit"] = "default",
|
|
235
239
|
timestamp: Optional[str] = None,
|
|
236
|
-
properties: Optional[Dict[str,
|
|
240
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
237
241
|
attachment_id: Optional[str] = None,
|
|
238
242
|
comment: Optional[str] = None,
|
|
239
|
-
after: Optional[str] = None
|
|
243
|
+
after: Optional[str] = None
|
|
240
244
|
) -> None:
|
|
241
245
|
"""
|
|
242
246
|
Track a signal event.
|
|
@@ -245,31 +249,55 @@ def track_signal(
|
|
|
245
249
|
event_id: The ID of the event to attach the signal to
|
|
246
250
|
name: Name of the signal (e.g. "thumbs_up", "thumbs_down")
|
|
247
251
|
signal_type: Type of signal ("default", "feedback", or "edit")
|
|
248
|
-
timestamp: Optional timestamp for the signal
|
|
249
|
-
properties: Optional
|
|
252
|
+
timestamp: Optional timestamp for the signal (ISO 8601 format)
|
|
253
|
+
properties: Optional dictionary of additional properties.
|
|
250
254
|
attachment_id: Optional ID of an attachment
|
|
251
|
-
comment: Optional comment (only
|
|
252
|
-
after: Optional after content (only
|
|
255
|
+
comment: Optional comment string (required and used only if signal_type is 'feedback').
|
|
256
|
+
after: Optional after content string (required and used only if signal_type is 'edit').
|
|
253
257
|
"""
|
|
254
258
|
if not _check_write_key():
|
|
255
259
|
return
|
|
256
260
|
|
|
257
|
-
# Prepare
|
|
258
|
-
|
|
259
|
-
if comment is not None:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
# Prepare the final properties dictionary
|
|
262
|
+
final_properties = properties.copy() if properties else {}
|
|
263
|
+
if signal_type == "feedback" and comment is not None:
|
|
264
|
+
if "comment" in final_properties:
|
|
265
|
+
logger.warning("'comment' provided as both argument and in properties; argument value used.")
|
|
266
|
+
final_properties["comment"] = comment
|
|
267
|
+
elif signal_type == "edit" and after is not None:
|
|
268
|
+
if "after" in final_properties:
|
|
269
|
+
logger.warning("'after' provided as both argument and in properties; argument value used.")
|
|
270
|
+
final_properties["after"] = after
|
|
271
|
+
|
|
272
|
+
# Prepare base arguments for all signal types
|
|
273
|
+
base_args = {
|
|
265
274
|
"event_id": event_id,
|
|
266
275
|
"signal_name": name,
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"properties": signal_properties,
|
|
276
|
+
"timestamp": timestamp or _get_timestamp(),
|
|
277
|
+
"properties": final_properties,
|
|
270
278
|
"attachment_id": attachment_id,
|
|
271
279
|
}
|
|
272
280
|
|
|
281
|
+
try:
|
|
282
|
+
# Construct the specific signal model based on signal_type
|
|
283
|
+
if signal_type == "feedback":
|
|
284
|
+
payload = FeedbackSignal(**base_args, signal_type=signal_type)
|
|
285
|
+
elif signal_type == "edit":
|
|
286
|
+
payload = EditSignal(**base_args, signal_type=signal_type)
|
|
287
|
+
else: # signal_type == "default"
|
|
288
|
+
if comment is not None:
|
|
289
|
+
logger.warning("'comment' argument provided for non-feedback signal type; ignored.")
|
|
290
|
+
if after is not None:
|
|
291
|
+
logger.warning("'after' argument provided for non-edit signal type; ignored.")
|
|
292
|
+
payload = DefaultSignal(**base_args, signal_type=signal_type)
|
|
293
|
+
|
|
294
|
+
except ValidationError as err:
|
|
295
|
+
logger.error(f"[raindrop] Invalid data passed to track_signal: {err}")
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
# model_dump handles the timestamp correctly
|
|
299
|
+
data = payload.model_dump(mode='json')
|
|
300
|
+
|
|
273
301
|
size = _get_size(data)
|
|
274
302
|
if size > max_ingest_size_bytes:
|
|
275
303
|
logger.warning(
|
|
@@ -280,4 +308,98 @@ def track_signal(
|
|
|
280
308
|
|
|
281
309
|
save_to_buffer({"type": "signals/track", "data": data})
|
|
282
310
|
|
|
311
|
+
def begin(
|
|
312
|
+
user_id: str,
|
|
313
|
+
event: str,
|
|
314
|
+
event_id: str | None = None,
|
|
315
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
316
|
+
input: Optional[str] = None,
|
|
317
|
+
attachments: Optional[List[Attachment]] = None,
|
|
318
|
+
) -> Interaction:
|
|
319
|
+
"""
|
|
320
|
+
Starts (or resumes) an interaction and returns a helper object.
|
|
321
|
+
"""
|
|
322
|
+
eid = event_id or str(uuid.uuid4())
|
|
323
|
+
|
|
324
|
+
# Prepare ai_data if input is provided
|
|
325
|
+
ai_data_partial = None
|
|
326
|
+
if input:
|
|
327
|
+
ai_data_partial = PartialAIData(input=input)
|
|
328
|
+
|
|
329
|
+
# Combine properties with initial_fields, giving precedence to initial_fields if keys clash
|
|
330
|
+
final_properties = (properties or {}).copy()
|
|
331
|
+
|
|
332
|
+
partial_event = PartialTrackAIEvent(
|
|
333
|
+
event_id=eid,
|
|
334
|
+
user_id=user_id,
|
|
335
|
+
event=event,
|
|
336
|
+
ai_data=ai_data_partial,
|
|
337
|
+
properties=final_properties or None, # Pass None if empty, matching PartialTrackAIEvent defaults
|
|
338
|
+
attachments=attachments
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
_track_ai_partial(partial_event)
|
|
342
|
+
return Interaction(eid)
|
|
343
|
+
|
|
344
|
+
def resume_interaction(event_id: str) -> Interaction:
|
|
345
|
+
"""Return an Interaction wrapper for an existing eventId."""
|
|
346
|
+
return Interaction(event_id)
|
|
347
|
+
|
|
348
|
+
def _track_ai_partial(event: PartialTrackAIEvent) -> None:
|
|
349
|
+
"""
|
|
350
|
+
Merge the incoming patch into an in-memory doc and flush to backend:
|
|
351
|
+
• on `.finish()` (is_pending == False)
|
|
352
|
+
• or after 20 s of inactivity
|
|
353
|
+
"""
|
|
354
|
+
eid = event.event_id
|
|
355
|
+
|
|
356
|
+
# 1. merge
|
|
357
|
+
existing = _partial_buffers.get(eid, PartialTrackAIEvent(event_id=eid))
|
|
358
|
+
existing.is_pending = existing.is_pending if existing.is_pending is not None else True
|
|
359
|
+
merged_dict = existing.model_dump(exclude_none=True)
|
|
360
|
+
incoming = event.model_dump(exclude_none=True)
|
|
361
|
+
|
|
362
|
+
# deep merge ai_data / properties
|
|
363
|
+
def _deep(d: dict, u: dict):
|
|
364
|
+
for k, v in u.items():
|
|
365
|
+
d[k] = _deep(d.get(k, {}) if isinstance(v, dict) else v, v) if isinstance(v, dict) else v
|
|
366
|
+
return d
|
|
367
|
+
merged = _deep(merged_dict, incoming)
|
|
368
|
+
merged_obj = PartialTrackAIEvent(**merged)
|
|
369
|
+
|
|
370
|
+
_partial_buffers[eid] = merged_obj
|
|
371
|
+
|
|
372
|
+
# 2. timer handling
|
|
373
|
+
if t := _partial_timers.get(eid):
|
|
374
|
+
t.cancel()
|
|
375
|
+
if merged_obj.is_pending is False:
|
|
376
|
+
_flush_partial_event(eid)
|
|
377
|
+
else:
|
|
378
|
+
_partial_timers[eid] = Timer(_PARTIAL_TIMEOUT, _flush_partial_event, args=[eid])
|
|
379
|
+
_partial_timers[eid].daemon = True
|
|
380
|
+
_partial_timers[eid].start()
|
|
381
|
+
|
|
382
|
+
if debug_logs:
|
|
383
|
+
logger.debug(f"[raindrop] updated partial {eid}: {merged_obj.model_dump(exclude_none=True)}")
|
|
384
|
+
|
|
385
|
+
def _flush_partial_event(event_id: str) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Send the accumulated patch as a single object to `events/track_partial`.
|
|
388
|
+
"""
|
|
389
|
+
if t := _partial_timers.pop(event_id, None):
|
|
390
|
+
t.cancel()
|
|
391
|
+
|
|
392
|
+
evt = _partial_buffers.pop(event_id, None)
|
|
393
|
+
if not evt:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# convert to ordinary TrackAIEvent-ish dict before send
|
|
397
|
+
data = evt.model_dump(mode="json", exclude_none=True)
|
|
398
|
+
size = _get_size(data)
|
|
399
|
+
if size > max_ingest_size_bytes:
|
|
400
|
+
logger.warning(f"[raindrop] partial event {event_id} > 1 MB; skipping")
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
send_request("events/track_partial", data)
|
|
404
|
+
|
|
283
405
|
atexit.register(shutdown)
|
raindrop/interaction.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from .models import Attachment, PartialTrackAIEvent
|
|
6
|
+
from . import analytics as _core
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Interaction:
|
|
10
|
+
"""
|
|
11
|
+
Thin helper returned by analytics.begin().
|
|
12
|
+
Each mutator just relays a partial update back to Analytics.
|
|
13
|
+
"""
|
|
14
|
+
__slots__ = ("_event_id", "_analytics")
|
|
15
|
+
|
|
16
|
+
def __init__(self, event_id: Optional[str] = None):
|
|
17
|
+
self._event_id = event_id or str(uuid4())
|
|
18
|
+
self._analytics = _core
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# -- mutators ----------------------------------------------------------- #
|
|
22
|
+
def set_input(self, text: str) -> None:
|
|
23
|
+
self._analytics._track_ai_partial(
|
|
24
|
+
PartialTrackAIEvent(event_id=self._event_id, ai_data={"input": text})
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def add_attachments(self, attachments: List[Attachment]) -> None:
|
|
28
|
+
self._analytics._track_ai_partial(
|
|
29
|
+
PartialTrackAIEvent(event_id=self._event_id, attachments=attachments)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def set_properties(self, props: Dict[str, Any]) -> None:
|
|
33
|
+
self._analytics._track_ai_partial(
|
|
34
|
+
PartialTrackAIEvent(event_id=self._event_id, properties=props)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def finish(self, *, output: str | None = None, **extra) -> None:
|
|
38
|
+
payload = PartialTrackAIEvent(
|
|
39
|
+
event_id=self._event_id,
|
|
40
|
+
ai_data={"output": output} if output is not None else None,
|
|
41
|
+
is_pending=False,
|
|
42
|
+
**extra,
|
|
43
|
+
)
|
|
44
|
+
self._analytics._track_ai_partial(payload)
|
|
45
|
+
|
|
46
|
+
# convenience
|
|
47
|
+
@property
|
|
48
|
+
def id(self) -> str:
|
|
49
|
+
return self._event_id
|
raindrop/models.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, ValidationError, model_validator, field_validator
|
|
2
|
+
from typing import Any, Optional, Dict, Literal, List, Union
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
class _Base(BaseModel):
|
|
6
|
+
model_config = dict(extra="forbid", validate_default=True)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Attachment(BaseModel):
|
|
10
|
+
type: Literal["code", "text", "image", "iframe"]
|
|
11
|
+
value: str # URL, raw code, etc.
|
|
12
|
+
name: Optional[str] = None # e.g. "Generated SQL"
|
|
13
|
+
role: Optional[Literal["input", "output", "context"]] = None
|
|
14
|
+
language: Optional[str] = None # for code snippets
|
|
15
|
+
|
|
16
|
+
@model_validator(mode="after")
|
|
17
|
+
def _require_value(cls, values):
|
|
18
|
+
if not values.value:
|
|
19
|
+
raise ValueError("value must be non-empty.")
|
|
20
|
+
return values
|
|
21
|
+
|
|
22
|
+
class AIData(_Base):
|
|
23
|
+
model: Optional[str]
|
|
24
|
+
input: Optional[str]
|
|
25
|
+
output: Optional[str]
|
|
26
|
+
convo_id: Optional[str]
|
|
27
|
+
|
|
28
|
+
@model_validator(mode="after")
|
|
29
|
+
def _require_input_or_output(cls, values):
|
|
30
|
+
if not (values.input or values.output):
|
|
31
|
+
raise ValueError("Either 'input' or 'output' must be non-empty.")
|
|
32
|
+
return values
|
|
33
|
+
|
|
34
|
+
class TrackAIEvent(_Base):
|
|
35
|
+
event_id: Optional[str] = None
|
|
36
|
+
user_id: str
|
|
37
|
+
event: str
|
|
38
|
+
ai_data: AIData
|
|
39
|
+
properties: Dict[str, Any] = Field(default_factory=dict)
|
|
40
|
+
timestamp: datetime = Field(
|
|
41
|
+
default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
42
|
+
)
|
|
43
|
+
attachments: Optional[List[Attachment]] = None
|
|
44
|
+
|
|
45
|
+
# Ensure user_id and event are non-empty strings
|
|
46
|
+
@field_validator("user_id", "event")
|
|
47
|
+
def _non_empty(cls, v, info):
|
|
48
|
+
if v is None or (isinstance(v, str) and v.strip() == ""):
|
|
49
|
+
raise ValueError(f"'{info.field_name}' must be a non-empty string.")
|
|
50
|
+
return v
|
|
51
|
+
|
|
52
|
+
# No need to duplicate input/output check here; AIData already enforces it
|
|
53
|
+
# but keep method to return values unchanged so that pydantic doesn't complain about unused return
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --- Signal Tracking Models --- #
|
|
57
|
+
|
|
58
|
+
class BaseSignal(_Base):
|
|
59
|
+
"""Base model for signal events, containing common fields."""
|
|
60
|
+
event_id: str
|
|
61
|
+
signal_name: str
|
|
62
|
+
timestamp: datetime = Field(
|
|
63
|
+
# Return a datetime object; Pydantic's model_dump will handle serialization to string
|
|
64
|
+
default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0)
|
|
65
|
+
)
|
|
66
|
+
properties: Dict[str, Any] = Field(default_factory=dict)
|
|
67
|
+
attachment_id: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
@field_validator("event_id", "signal_name")
|
|
70
|
+
def _non_empty_strings(cls, v, info):
|
|
71
|
+
if not isinstance(v, str) or not v.strip():
|
|
72
|
+
raise ValueError(f"'{info.field_name}' must be a non-empty string.")
|
|
73
|
+
return v
|
|
74
|
+
|
|
75
|
+
class DefaultSignal(BaseSignal):
|
|
76
|
+
"""Model for default signal events."""
|
|
77
|
+
signal_type: Literal["default"] = "default"
|
|
78
|
+
|
|
79
|
+
class FeedbackSignal(BaseSignal):
|
|
80
|
+
"""Model for feedback signal events, requiring a comment."""
|
|
81
|
+
signal_type: Literal["feedback"]
|
|
82
|
+
|
|
83
|
+
@model_validator(mode="after")
|
|
84
|
+
def _check_comment_in_properties(cls, values):
|
|
85
|
+
# Check properties safely after potential initialization
|
|
86
|
+
# Use getattr to safely access properties, returning None if not present
|
|
87
|
+
props = getattr(values, 'properties', None)
|
|
88
|
+
if not isinstance(props, dict):
|
|
89
|
+
raise ValueError("'properties' must be a dictionary for feedback signals.")
|
|
90
|
+
comment = props.get("comment")
|
|
91
|
+
if not comment or not isinstance(comment, str) or not comment.strip():
|
|
92
|
+
raise ValueError("'properties' must contain a non-empty string 'comment' for feedback signals.")
|
|
93
|
+
return values
|
|
94
|
+
|
|
95
|
+
class EditSignal(BaseSignal):
|
|
96
|
+
"""Model for edit signal events, requiring after content."""
|
|
97
|
+
signal_type: Literal["edit"]
|
|
98
|
+
|
|
99
|
+
@model_validator(mode="after")
|
|
100
|
+
def _check_after_in_properties(cls, values):
|
|
101
|
+
# Check properties safely after potential initialization
|
|
102
|
+
props = getattr(values, 'properties', None)
|
|
103
|
+
if not isinstance(props, dict):
|
|
104
|
+
raise ValueError("'properties' must be a dictionary for edit signals.")
|
|
105
|
+
after = props.get("after")
|
|
106
|
+
if not after or not isinstance(after, str) or not after.strip():
|
|
107
|
+
raise ValueError("'properties' must contain a non-empty string 'after' for edit signals.")
|
|
108
|
+
return values
|
|
109
|
+
|
|
110
|
+
# Discriminated Union for Signal Events
|
|
111
|
+
# Pydantic will automatically use the 'signal_type' field to determine which model to use.
|
|
112
|
+
SignalEvent = Union[DefaultSignal, FeedbackSignal, EditSignal]
|
|
113
|
+
|
|
114
|
+
# --- End Signal Tracking Models --- #
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PartialAIData(_Base):
|
|
118
|
+
"""Looser version for incremental updates."""
|
|
119
|
+
model: Optional[str] = None
|
|
120
|
+
input: Optional[str] = None
|
|
121
|
+
output: Optional[str] = None
|
|
122
|
+
convo_id: Optional[str] = None
|
|
123
|
+
|
|
124
|
+
class PartialTrackAIEvent(_Base):
|
|
125
|
+
"""Accepts *any subset* of TrackAIEvent fields."""
|
|
126
|
+
event_id: str # always required for merge-key
|
|
127
|
+
user_id: Optional[str] = None
|
|
128
|
+
event: Optional[str] = None
|
|
129
|
+
ai_data: Optional[PartialAIData] = None
|
|
130
|
+
timestamp: Optional[datetime] = None
|
|
131
|
+
properties: Optional[Dict[str, Any]] = None
|
|
132
|
+
attachments: Optional[List[Attachment]] = None
|
|
133
|
+
is_pending: Optional[bool] = True
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: raindrop-ai
|
|
3
|
+
Version: 0.0.21
|
|
4
|
+
Summary: Raindrop AI (Python SDK)
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Raindrop AI
|
|
7
|
+
Author-email: sdk@raindrop.ai
|
|
8
|
+
Requires-Python: >=3.11, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*, !=3.9.*, !=3.10.*
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: pydantic (>=2.11,<3)
|
|
14
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Raindrop Python SDK
|
|
18
|
+
|
|
19
|
+
## Installation dependencies
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install poetry
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
poetry install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Run tests
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
poetry run green -vv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
raindrop/analytics.py,sha256=IgxyvcbMa92FO5GFL3AcBnK1qv36NwxCQSiFbIh-9XU,13410
|
|
3
|
+
raindrop/interaction.py,sha256=U8OapN5tZGSMVkn-dTLum5sIg_AgSYKX9lrtwgXJOuI,1613
|
|
4
|
+
raindrop/models.py,sha256=THs3VQXGfJAdfg99rEBlqUzDh_F1HBC2hMOQx49l6BQ,5229
|
|
5
|
+
raindrop/version.py,sha256=zKIDJIJluqVEGWC_VXIxFo3Hrk5Q7UGBBqP5uigDAN8,18
|
|
6
|
+
raindrop_ai-0.0.21.dist-info/METADATA,sha256=8aH4lRYs-zEXfj0XSmrwlD4LTj5G-3pp3ahdwHL-k8c,774
|
|
7
|
+
raindrop_ai-0.0.21.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
8
|
+
raindrop_ai-0.0.21.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: raindrop-ai
|
|
3
|
-
Version: 0.0.19
|
|
4
|
-
Summary: Raindrop AI (Python SDK)
|
|
5
|
-
Home-page: https://raindrop.ai
|
|
6
|
-
Author: Raindrop AI
|
|
7
|
-
Author-email: sdk@raindrop.ai
|
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
|
9
|
-
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: requests
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: requires-dist
|
|
26
|
-
Dynamic: summary
|
|
27
|
-
|
|
28
|
-
For questions, email us at sdk@raindrop.ai
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
raindrop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
raindrop/analytics.py,sha256=zrhvoAM65KSFTc9VWwBTGKhfMtB0H_7J-QmApRXpOKc,8585
|
|
3
|
-
raindrop/version.py,sha256=zKIDJIJluqVEGWC_VXIxFo3Hrk5Q7UGBBqP5uigDAN8,18
|
|
4
|
-
raindrop_ai-0.0.19.dist-info/METADATA,sha256=CXtCww_mqKXivz4eIhbwymtWo2Y-XM28tSUzBoWSzp4,881
|
|
5
|
-
raindrop_ai-0.0.19.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
6
|
-
raindrop_ai-0.0.19.dist-info/top_level.txt,sha256=yyf4RASIufJ0tUhUJ1-X3R05s9BuOU6dXCmv08UVpo8,9
|
|
7
|
-
raindrop_ai-0.0.19.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
raindrop
|