raindrop-ai 0.0.28__tar.gz → 0.0.30__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.28 → raindrop_ai-0.0.30}/PKG-INFO +21 -1
- raindrop_ai-0.0.30/README.md +43 -0
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/pyproject.toml +16 -1
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/analytics.py +190 -5
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/interaction.py +25 -5
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/models.py +36 -14
- raindrop_ai-0.0.30/raindrop/version.py +1 -0
- raindrop_ai-0.0.28/README.md +0 -24
- raindrop_ai-0.0.28/raindrop/version.py +0 -1
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/__init__.py +0 -0
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/redact.py +0 -0
- {raindrop_ai-0.0.28 → raindrop_ai-0.0.30}/raindrop/well-known-names.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: raindrop-ai
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
4
4
|
Summary: Raindrop AI (Python SDK)
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Raindrop AI
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: pydantic (>=2.09,<3)
|
|
15
15
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
16
|
+
Requires-Dist: traceloop-sdk (==0.45.6)
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
18
19
|
# Raindrop Python SDK
|
|
@@ -31,6 +32,25 @@ poetry install
|
|
|
31
32
|
|
|
32
33
|
## Run tests
|
|
33
34
|
|
|
35
|
+
### Using pytest (recommended)
|
|
36
|
+
```bash
|
|
37
|
+
# Run all tests
|
|
38
|
+
poetry run pytest
|
|
39
|
+
|
|
40
|
+
# Run tests with verbose output
|
|
41
|
+
poetry run pytest -v
|
|
42
|
+
|
|
43
|
+
# Run specific test file
|
|
44
|
+
poetry run pytest tests/test_trace_attributes.py
|
|
45
|
+
|
|
46
|
+
# Run specific test
|
|
47
|
+
poetry run pytest tests/test_trace_attributes.py::TestTraceAttributes::test_user_id_attribute_direct
|
|
48
|
+
|
|
49
|
+
# Run tests with coverage
|
|
50
|
+
poetry run pytest --cov=raindrop
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Using green (legacy)
|
|
34
54
|
```bash
|
|
35
55
|
poetry run green -vv
|
|
36
56
|
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Raindrop Python SDK
|
|
2
|
+
|
|
3
|
+
## Installation dependencies
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install poetry
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
poetry install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## Run tests
|
|
16
|
+
|
|
17
|
+
### Using pytest (recommended)
|
|
18
|
+
```bash
|
|
19
|
+
# Run all tests
|
|
20
|
+
poetry run pytest
|
|
21
|
+
|
|
22
|
+
# Run tests with verbose output
|
|
23
|
+
poetry run pytest -v
|
|
24
|
+
|
|
25
|
+
# Run specific test file
|
|
26
|
+
poetry run pytest tests/test_trace_attributes.py
|
|
27
|
+
|
|
28
|
+
# Run specific test
|
|
29
|
+
poetry run pytest tests/test_trace_attributes.py::TestTraceAttributes::test_user_id_attribute_direct
|
|
30
|
+
|
|
31
|
+
# Run tests with coverage
|
|
32
|
+
poetry run pytest --cov=raindrop
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Using green (legacy)
|
|
36
|
+
```bash
|
|
37
|
+
poetry run green -vv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "raindrop-ai"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.30"
|
|
4
4
|
description = "Raindrop AI (Python SDK)"
|
|
5
5
|
authors = ["Raindrop AI <sdk@raindrop.ai>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -11,10 +11,25 @@ packages = [{include = "raindrop"}]
|
|
|
11
11
|
python = ">=3.10,<3.12.1 || >3.12.1,<4.0"
|
|
12
12
|
pydantic = ">=2.09,<3"
|
|
13
13
|
requests = "^2.32.3"
|
|
14
|
+
traceloop-sdk = "0.45.6"
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
[tool.poetry.group.dev.dependencies]
|
|
17
18
|
green = "^4.0.2"
|
|
19
|
+
pytest = "^8.4.1"
|
|
20
|
+
pytest-cov = "^6.2.1"
|
|
21
|
+
openai = "^1.100.1"
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
python_files = ["test_*.py"]
|
|
26
|
+
python_classes = ["Test*"]
|
|
27
|
+
python_functions = ["test_*"]
|
|
28
|
+
addopts = ["-v", "--tb=short"]
|
|
29
|
+
filterwarnings = [
|
|
30
|
+
"ignore::DeprecationWarning",
|
|
31
|
+
"ignore::PendingDeprecationWarning",
|
|
32
|
+
]
|
|
18
33
|
|
|
19
34
|
[build-system]
|
|
20
35
|
requires = ["poetry-core"]
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import time
|
|
3
3
|
import threading
|
|
4
|
-
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Callable, Union, List, Dict, Optional, Literal, Any
|
|
5
7
|
import requests
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
import logging
|
|
@@ -23,6 +25,39 @@ from raindrop.models import (
|
|
|
23
25
|
)
|
|
24
26
|
from raindrop.interaction import Interaction
|
|
25
27
|
from raindrop.redact import perform_pii_redaction
|
|
28
|
+
import weakref
|
|
29
|
+
import urllib.parse
|
|
30
|
+
|
|
31
|
+
from traceloop.sdk import Traceloop
|
|
32
|
+
from traceloop.sdk.tracing.tracing import TracerWrapper
|
|
33
|
+
from opentelemetry.trace import get_current_span
|
|
34
|
+
from traceloop.sdk.decorators import (
|
|
35
|
+
task as tlp_task,
|
|
36
|
+
workflow as tlp_workflow,
|
|
37
|
+
TraceloopSpanKindValues,
|
|
38
|
+
F,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Configuration functions
|
|
43
|
+
"set_debug_logs",
|
|
44
|
+
"set_redact_pii",
|
|
45
|
+
"init",
|
|
46
|
+
|
|
47
|
+
"identify",
|
|
48
|
+
"track_ai",
|
|
49
|
+
"track_signal",
|
|
50
|
+
"begin",
|
|
51
|
+
"resume_interaction",
|
|
52
|
+
|
|
53
|
+
"interaction",
|
|
54
|
+
"task",
|
|
55
|
+
"tool",
|
|
56
|
+
"set_span_properties",
|
|
57
|
+
|
|
58
|
+
"flush",
|
|
59
|
+
"shutdown",
|
|
60
|
+
]
|
|
26
61
|
|
|
27
62
|
|
|
28
63
|
# Configure logging
|
|
@@ -40,6 +75,7 @@ buffer = []
|
|
|
40
75
|
flush_lock = threading.Lock()
|
|
41
76
|
debug_logs = False
|
|
42
77
|
redact_pii = False
|
|
78
|
+
_tracing_enabled = False
|
|
43
79
|
flush_thread = None
|
|
44
80
|
shutdown_event = threading.Event()
|
|
45
81
|
max_ingest_size_bytes = 1 * 1024 * 1024 # 1 MB
|
|
@@ -236,6 +272,11 @@ def shutdown():
|
|
|
236
272
|
if flush_thread:
|
|
237
273
|
flush_thread.join(timeout=10)
|
|
238
274
|
flush() # Final flush to ensure all events are sent
|
|
275
|
+
if _tracing_enabled:
|
|
276
|
+
try:
|
|
277
|
+
TracerWrapper().flush()
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.debug(f"Could not flush TracerWrapper during shutdown: {e}")
|
|
239
280
|
|
|
240
281
|
|
|
241
282
|
def _check_write_key():
|
|
@@ -365,6 +406,14 @@ def track_signal(
|
|
|
365
406
|
save_to_buffer({"type": "signals/track", "data": data})
|
|
366
407
|
|
|
367
408
|
|
|
409
|
+
INTERACTION_TRACE_ID_REGISTRY: weakref.WeakValueDictionary[int, Interaction] = (
|
|
410
|
+
weakref.WeakValueDictionary()
|
|
411
|
+
)
|
|
412
|
+
INTERACTION_EVENT_ID_REGISTRY: weakref.WeakValueDictionary[str, Interaction] = (
|
|
413
|
+
weakref.WeakValueDictionary()
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
368
417
|
def begin(
|
|
369
418
|
user_id: str,
|
|
370
419
|
event: str,
|
|
@@ -387,6 +436,10 @@ def begin(
|
|
|
387
436
|
# Combine properties with initial_fields, giving precedence to initial_fields if keys clash
|
|
388
437
|
final_properties = (properties or {}).copy()
|
|
389
438
|
|
|
439
|
+
current_trace_id = _safe_current_trace_id()
|
|
440
|
+
if current_trace_id is not None:
|
|
441
|
+
final_properties["trace_id"] = f"{current_trace_id:032x}"
|
|
442
|
+
|
|
390
443
|
partial_event = PartialTrackAIEvent(
|
|
391
444
|
event_id=eid,
|
|
392
445
|
user_id=user_id,
|
|
@@ -397,13 +450,145 @@ def begin(
|
|
|
397
450
|
attachments=attachments,
|
|
398
451
|
)
|
|
399
452
|
|
|
453
|
+
span_attributes = {
|
|
454
|
+
"user_id": user_id,
|
|
455
|
+
"convo_id": convo_id,
|
|
456
|
+
"event": event,
|
|
457
|
+
"event_id": eid,
|
|
458
|
+
}
|
|
459
|
+
if _tracing_enabled:
|
|
460
|
+
Traceloop.set_association_properties(
|
|
461
|
+
{k: v for k, v in span_attributes.items() if v is not None}
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
interaction = Interaction(eid)
|
|
465
|
+
INTERACTION_EVENT_ID_REGISTRY[eid] = interaction
|
|
466
|
+
if current_trace_id is not None and current_trace_id != 0:
|
|
467
|
+
INTERACTION_TRACE_ID_REGISTRY[current_trace_id] = interaction
|
|
468
|
+
|
|
400
469
|
_track_ai_partial(partial_event)
|
|
401
|
-
return
|
|
470
|
+
return interaction
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@contextmanager
|
|
474
|
+
def _temp_env(key: str, value: str):
|
|
475
|
+
"""Temporarily sets an environment variable. Hacky helper to deal with traceloop's BS"""
|
|
476
|
+
orig = os.environ.get(key)
|
|
477
|
+
os.environ[key] = value
|
|
478
|
+
try:
|
|
479
|
+
yield
|
|
480
|
+
finally:
|
|
481
|
+
if orig is None:
|
|
482
|
+
del os.environ[key]
|
|
483
|
+
else:
|
|
484
|
+
os.environ[key] = orig
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def init(
|
|
488
|
+
api_key: str,
|
|
489
|
+
tracing_enabled: bool = False,
|
|
490
|
+
**traceloop_kwargs,
|
|
491
|
+
):
|
|
492
|
+
"""Initialize Raindrop with Traceloop integration."""
|
|
493
|
+
global write_key
|
|
494
|
+
write_key = api_key
|
|
495
|
+
|
|
496
|
+
global _tracing_enabled
|
|
497
|
+
_tracing_enabled = tracing_enabled
|
|
498
|
+
|
|
499
|
+
if not _tracing_enabled:
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
parsed_url = urllib.parse.urlparse(api_url)
|
|
503
|
+
api_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
504
|
+
|
|
505
|
+
with _temp_env("TRACELOOP_METRICS_ENABLED", "false"):
|
|
506
|
+
Traceloop.init(
|
|
507
|
+
api_endpoint=api_endpoint,
|
|
508
|
+
api_key=api_key,
|
|
509
|
+
telemetry_enabled=False,
|
|
510
|
+
**traceloop_kwargs,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _safe_current_trace_id() -> int | None:
|
|
515
|
+
"""Return current trace id or None if unavailable."""
|
|
516
|
+
try:
|
|
517
|
+
trace_id = get_current_span().get_span_context().trace_id
|
|
518
|
+
except Exception:
|
|
519
|
+
return None
|
|
520
|
+
return trace_id if trace_id else None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def interaction(
|
|
524
|
+
name: Optional[str] = None,
|
|
525
|
+
version: Optional[int] = None,
|
|
526
|
+
method_name: Optional[str] = None,
|
|
527
|
+
) -> Callable[[F], F]:
|
|
528
|
+
return tlp_workflow(
|
|
529
|
+
name=name,
|
|
530
|
+
version=version,
|
|
531
|
+
method_name=method_name,
|
|
532
|
+
tlp_span_kind=TraceloopSpanKindValues.WORKFLOW,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def task(
|
|
537
|
+
name: Optional[str] = None,
|
|
538
|
+
version: Optional[int] = None,
|
|
539
|
+
method_name: Optional[str] = None,
|
|
540
|
+
tlp_span_kind: Optional[TraceloopSpanKindValues] = TraceloopSpanKindValues.TASK,
|
|
541
|
+
) -> Callable[[F], F]:
|
|
542
|
+
return tlp_task(
|
|
543
|
+
name=name,
|
|
544
|
+
version=version,
|
|
545
|
+
method_name=method_name,
|
|
546
|
+
tlp_span_kind=tlp_span_kind,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def tool(
|
|
551
|
+
name: Optional[str] = None,
|
|
552
|
+
version: Optional[int] = None,
|
|
553
|
+
method_name: Optional[str] = None,
|
|
554
|
+
) -> Callable[[F], F]:
|
|
555
|
+
return tlp_task(
|
|
556
|
+
name=name,
|
|
557
|
+
version=version,
|
|
558
|
+
method_name=method_name,
|
|
559
|
+
tlp_span_kind=TraceloopSpanKindValues.TOOL,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def set_span_properties(properties: Dict[str, Any]) -> None:
|
|
564
|
+
"""
|
|
565
|
+
Set association properties on the current span for tracing.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
properties: Dictionary of properties to associate with the current span
|
|
569
|
+
"""
|
|
570
|
+
if not _tracing_enabled:
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
Traceloop.set_association_properties(properties)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def resume_interaction(event_id: str | None = None) -> Interaction:
|
|
577
|
+
"""Return an Interaction associated with the current trace or given event_id."""
|
|
578
|
+
|
|
579
|
+
if event_id is not None:
|
|
580
|
+
if (interaction := INTERACTION_EVENT_ID_REGISTRY.get(event_id)) is not None:
|
|
581
|
+
return interaction
|
|
582
|
+
return Interaction(event_id)
|
|
402
583
|
|
|
584
|
+
if (trace_id := _safe_current_trace_id()) is not None:
|
|
585
|
+
if (interaction := INTERACTION_TRACE_ID_REGISTRY.get(trace_id)) is not None:
|
|
586
|
+
return interaction
|
|
403
587
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
588
|
+
# Fallback: create a fresh Interaction when no identifiers are available
|
|
589
|
+
# TODO: Return No-Op interaction if event_id is None
|
|
590
|
+
logger.debug("No interaction found, creating a new one")
|
|
591
|
+
return Interaction()
|
|
407
592
|
|
|
408
593
|
|
|
409
594
|
def _track_ai_partial(event: PartialTrackAIEvent) -> None:
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Dict,
|
|
5
|
+
List,
|
|
6
|
+
Optional,
|
|
7
|
+
Union,
|
|
8
|
+
Iterator,
|
|
9
|
+
)
|
|
3
10
|
from uuid import uuid4
|
|
11
|
+
from dataclasses import dataclass
|
|
4
12
|
|
|
5
13
|
from .models import Attachment, PartialTrackAIEvent
|
|
6
14
|
from . import analytics as _core
|
|
15
|
+
from opentelemetry import context as context_api
|
|
7
16
|
|
|
8
17
|
|
|
9
18
|
class Interaction:
|
|
@@ -11,12 +20,19 @@ class Interaction:
|
|
|
11
20
|
Thin helper returned by analytics.begin().
|
|
12
21
|
Each mutator just relays a partial update back to Analytics.
|
|
13
22
|
"""
|
|
14
|
-
__slots__ = ("_event_id", "_analytics")
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
__slots__ = (
|
|
25
|
+
"_event_id",
|
|
26
|
+
"_analytics",
|
|
27
|
+
"__weakref__",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
event_id: Optional[str] = None,
|
|
33
|
+
):
|
|
17
34
|
self._event_id = event_id or str(uuid4())
|
|
18
35
|
self._analytics = _core
|
|
19
|
-
|
|
20
36
|
|
|
21
37
|
# -- mutators ----------------------------------------------------------- #
|
|
22
38
|
def set_input(self, text: str) -> None:
|
|
@@ -34,7 +50,11 @@ class Interaction:
|
|
|
34
50
|
PartialTrackAIEvent(event_id=self._event_id, properties=props)
|
|
35
51
|
)
|
|
36
52
|
|
|
53
|
+
def set_property(self, key: str, value: Any) -> None:
|
|
54
|
+
self.set_properties({key: value})
|
|
55
|
+
|
|
37
56
|
def finish(self, *, output: str | None = None, **extra) -> None:
|
|
57
|
+
|
|
38
58
|
payload = PartialTrackAIEvent(
|
|
39
59
|
event_id=self._event_id,
|
|
40
60
|
ai_data={"output": output} if output is not None else None,
|
|
@@ -46,4 +66,4 @@ class Interaction:
|
|
|
46
66
|
# convenience
|
|
47
67
|
@property
|
|
48
68
|
def id(self) -> str:
|
|
49
|
-
return self._event_id
|
|
69
|
+
return self._event_id
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, ValidationError, model_validator, field_validator
|
|
2
2
|
from typing import Any, Optional, Dict, Literal, List, Union
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class _Base(BaseModel):
|
|
6
8
|
model_config = dict(extra="forbid", validate_default=True)
|
|
@@ -8,10 +10,10 @@ class _Base(BaseModel):
|
|
|
8
10
|
|
|
9
11
|
class Attachment(BaseModel):
|
|
10
12
|
type: Literal["code", "text", "image", "iframe"]
|
|
11
|
-
value: str
|
|
12
|
-
name: Optional[str] = None
|
|
13
|
+
value: str # URL, raw code, etc.
|
|
14
|
+
name: Optional[str] = None # e.g. "Generated SQL"
|
|
13
15
|
role: Optional[Literal["input", "output", "context"]] = None
|
|
14
|
-
language: Optional[str] = None
|
|
16
|
+
language: Optional[str] = None # for code snippets
|
|
15
17
|
|
|
16
18
|
@model_validator(mode="after")
|
|
17
19
|
def _require_value(cls, values):
|
|
@@ -19,6 +21,7 @@ class Attachment(BaseModel):
|
|
|
19
21
|
raise ValueError("value must be non-empty.")
|
|
20
22
|
return values
|
|
21
23
|
|
|
24
|
+
|
|
22
25
|
class AIData(_Base):
|
|
23
26
|
model: Optional[str]
|
|
24
27
|
input: Optional[str]
|
|
@@ -30,7 +33,8 @@ class AIData(_Base):
|
|
|
30
33
|
if not (values.input or values.output):
|
|
31
34
|
raise ValueError("Either 'input' or 'output' must be non-empty.")
|
|
32
35
|
return values
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
class TrackAIEvent(_Base):
|
|
35
39
|
event_id: Optional[str] = None
|
|
36
40
|
user_id: str
|
|
@@ -38,7 +42,9 @@ class TrackAIEvent(_Base):
|
|
|
38
42
|
ai_data: AIData
|
|
39
43
|
properties: Dict[str, Any] = Field(default_factory=dict)
|
|
40
44
|
timestamp: datetime = Field(
|
|
41
|
-
default_factory=lambda: datetime.now(timezone.utc)
|
|
45
|
+
default_factory=lambda: datetime.now(timezone.utc)
|
|
46
|
+
.replace(microsecond=0)
|
|
47
|
+
.isoformat()
|
|
42
48
|
)
|
|
43
49
|
attachments: Optional[List[Attachment]] = None
|
|
44
50
|
|
|
@@ -55,8 +61,10 @@ class TrackAIEvent(_Base):
|
|
|
55
61
|
|
|
56
62
|
# --- Signal Tracking Models --- #
|
|
57
63
|
|
|
64
|
+
|
|
58
65
|
class BaseSignal(_Base):
|
|
59
66
|
"""Base model for signal events, containing common fields."""
|
|
67
|
+
|
|
60
68
|
event_id: str
|
|
61
69
|
signal_name: str
|
|
62
70
|
timestamp: datetime = Field(
|
|
@@ -73,41 +81,52 @@ class BaseSignal(_Base):
|
|
|
73
81
|
raise ValueError(f"'{info.field_name}' must be a non-empty string.")
|
|
74
82
|
return v
|
|
75
83
|
|
|
84
|
+
|
|
76
85
|
class DefaultSignal(BaseSignal):
|
|
77
86
|
"""Model for default signal events."""
|
|
87
|
+
|
|
78
88
|
signal_type: Literal["default"] = "default"
|
|
79
89
|
|
|
90
|
+
|
|
80
91
|
class FeedbackSignal(BaseSignal):
|
|
81
92
|
"""Model for feedback signal events, requiring a comment."""
|
|
93
|
+
|
|
82
94
|
signal_type: Literal["feedback"]
|
|
83
95
|
|
|
84
96
|
@model_validator(mode="after")
|
|
85
97
|
def _check_comment_in_properties(cls, values):
|
|
86
98
|
# Check properties safely after potential initialization
|
|
87
99
|
# Use getattr to safely access properties, returning None if not present
|
|
88
|
-
props = getattr(values,
|
|
100
|
+
props = getattr(values, "properties", None)
|
|
89
101
|
if not isinstance(props, dict):
|
|
90
|
-
|
|
102
|
+
raise ValueError("'properties' must be a dictionary for feedback signals.")
|
|
91
103
|
comment = props.get("comment")
|
|
92
104
|
if not comment or not isinstance(comment, str) or not comment.strip():
|
|
93
|
-
raise ValueError(
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"'properties' must contain a non-empty string 'comment' for feedback signals."
|
|
107
|
+
)
|
|
94
108
|
return values
|
|
95
109
|
|
|
110
|
+
|
|
96
111
|
class EditSignal(BaseSignal):
|
|
97
112
|
"""Model for edit signal events, requiring after content."""
|
|
113
|
+
|
|
98
114
|
signal_type: Literal["edit"]
|
|
99
115
|
|
|
100
116
|
@model_validator(mode="after")
|
|
101
117
|
def _check_after_in_properties(cls, values):
|
|
102
118
|
# Check properties safely after potential initialization
|
|
103
|
-
props = getattr(values,
|
|
119
|
+
props = getattr(values, "properties", None)
|
|
104
120
|
if not isinstance(props, dict):
|
|
105
|
-
|
|
121
|
+
raise ValueError("'properties' must be a dictionary for edit signals.")
|
|
106
122
|
after = props.get("after")
|
|
107
123
|
if not after or not isinstance(after, str) or not after.strip():
|
|
108
|
-
|
|
124
|
+
raise ValueError(
|
|
125
|
+
"'properties' must contain a non-empty string 'after' for edit signals."
|
|
126
|
+
)
|
|
109
127
|
return values
|
|
110
128
|
|
|
129
|
+
|
|
111
130
|
# Discriminated Union for Signal Events
|
|
112
131
|
# Pydantic will automatically use the 'signal_type' field to determine which model to use.
|
|
113
132
|
SignalEvent = Union[DefaultSignal, FeedbackSignal, EditSignal]
|
|
@@ -117,18 +136,21 @@ SignalEvent = Union[DefaultSignal, FeedbackSignal, EditSignal]
|
|
|
117
136
|
|
|
118
137
|
class PartialAIData(_Base):
|
|
119
138
|
"""Looser version for incremental updates."""
|
|
139
|
+
|
|
120
140
|
model: Optional[str] = None
|
|
121
141
|
input: Optional[str] = None
|
|
122
142
|
output: Optional[str] = None
|
|
123
|
-
convo_id: Optional[str] = None
|
|
143
|
+
convo_id: Optional[str] = None
|
|
144
|
+
|
|
124
145
|
|
|
125
146
|
class PartialTrackAIEvent(_Base):
|
|
126
147
|
"""Accepts *any subset* of TrackAIEvent fields."""
|
|
127
|
-
|
|
148
|
+
|
|
149
|
+
event_id: str # always required for merge-key
|
|
128
150
|
user_id: Optional[str] = None
|
|
129
151
|
event: Optional[str] = None
|
|
130
152
|
ai_data: Optional[PartialAIData] = None
|
|
131
153
|
timestamp: Optional[datetime] = None
|
|
132
154
|
properties: Optional[Dict[str, Any]] = None
|
|
133
155
|
attachments: Optional[List[Attachment]] = None
|
|
134
|
-
is_pending: Optional[bool] = True
|
|
156
|
+
is_pending: Optional[bool] = True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.0.30"
|
raindrop_ai-0.0.28/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.0.28"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|