Metadata-Version: 2.1
Name: raindrop-ai
Version: 0.0.52
Summary: Raindrop AI (Python SDK)
License: MIT
Author: Raindrop AI
Author-email: sdk@raindrop.ai
Requires-Python: >=3.10, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*, !=3.9.*
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: httpx (>=0.24,<1)
Requires-Dist: opentelemetry-sdk (>=1.36.0)
Requires-Dist: protobuf (>=5.29.6,<7)
Requires-Dist: pydantic (>=2.09,<3)
Requires-Dist: requests (>=2.32.3,<3.0.0)
Requires-Dist: traceloop-sdk (>=0.46.0)
Description-Content-Type: text/markdown

# Raindrop Python SDK

The official Python SDK for [Raindrop AI](https://raindrop.ai) — track AI events, collect user signals, and instrument LLM applications with OpenTelemetry-based tracing.

## Installation

```bash
pip install raindrop-ai
```

**Requires Python 3.10+**

## Quick Start

```python
import raindrop.analytics as raindrop

raindrop.init(api_key="your-api-key", tracing_enabled=True)

# Track an AI event
raindrop.track_ai(
    user_id="user-123",
    event="chat-completion",
    model="gpt-4",
    input="What is the weather?",
    output="It's sunny and 72°F.",
    convo_id="conv-456",
)
```

## Payload size limits

As of `0.0.52`, text fields (ai input/output, tool span I/O, LLM span
content) are capped at **1,000,000 characters per field by default** and
truncated with a `...[truncated by raindrop]` marker. Fields up to 1M chars
— i.e. everything the ingest API accepts today — round-trip unchanged. The
cap is enforced before (or during) serialization, so oversized payloads cost
the cap — not the payload — on your calling thread, and a single capped
ASCII field still fits under the 1 MB event-level ingest limit. Tune it via:

```python
raindrop.init(api_key="...", max_text_field_chars=250_000)
```

A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is still honored
for span content. All outbound HTTP carries finite timeouts, and the atexit
shutdown flush runs under a 10s deadline so a dead network can never wedge
your process exit.

## Interactions

Use `begin()` and `finish()` for multi-step AI workflows:

```python
interaction = raindrop.begin(
    user_id="user-123",
    event="agent-run",
    input="Search for weather data",
    convo_id="conv-456",
)

# Update incrementally
interaction.set_property("region", "us-east")
interaction.add_attachments([
    raindrop.Attachment(type="code", value="print('hello')", language="python")
])

# Complete the interaction
interaction.finish(output="Found weather data for NYC")
```

### Resuming Interactions

Access the current interaction from nested functions:

```python
@raindrop.tool("sentiment_analyzer")
def analyze_sentiment(text: str):
    interaction = raindrop.resume_interaction()
    interaction.set_property("sentiment", "positive")
    return {"sentiment": "positive"}
```

## Decorators

Instrument functions with automatic span creation:

```python
@raindrop.interaction("my_workflow")
def run_workflow():
    ...

@raindrop.task("process_data")
def process():
    ...

@raindrop.tool("search")
def search(query: str):
    ...
```

## Spans

### Context Managers

```python
with raindrop.task_span("process_data"):
    result = do_processing()

with raindrop.tool_span("web_search"):
    results = search(query)
```

### Manual Spans

For async or distributed operations where you need explicit control:

```python
span = raindrop.start_span(kind="tool", name="async_search")
span.record_input({"query": "weather"})

# ... later, when the result arrives
span.record_output({"result": "sunny"})
span.end()
```

## Retroactive Tool Logging

Log tool calls after they complete, without wrapping them in spans:

```python
interaction = raindrop.begin(user_id="user-123", event="agent-run")

interaction.track_tool(
    name="web_search",
    input={"query": "weather in NYC"},
    output={"results": ["Sunny, 72°F"]},
    duration_ms=150,
)

interaction.track_tool(
    name="database_query",
    input={"query": "SELECT * FROM users"},
    duration_ms=50,
    error=ConnectionError("Connection timeout"),
)

interaction.finish(output="Done")
```

## Signals

Track user feedback on AI outputs:

```python
# Basic signal
raindrop.track_signal(event_id="evt-123", name="thumbs_up")

# Feedback with comment
raindrop.track_signal(
    event_id="evt-123",
    name="user_feedback",
    signal_type="feedback",
    comment="This answer was helpful",
    sentiment="POSITIVE",
)

# Edit signal
raindrop.track_signal(
    event_id="evt-123",
    name="user_edit",
    signal_type="edit",
    after="The corrected response text",
)
```

## User Identification

```python
raindrop.identify("user-123", traits={"plan": "pro", "company": "Acme"})
```

## PII Redaction

Enable automatic redaction of emails, phone numbers, credit cards, SSNs, and other PII from AI inputs and outputs:

```python
raindrop.set_redact_pii(True)
```

## Auto-Instrumentation

By default, Raindrop auto-instruments detected LLM libraries (OpenAI, Anthropic, Bedrock, etc.) via Traceloop. To disable:

```python
raindrop.init(api_key="your-key", tracing_enabled=True, auto_instrument=False)
```

Or selectively control which libraries are instrumented:

```python
from raindrop.analytics import Instruments

raindrop.init(
    api_key="your-key",
    tracing_enabled=True,
    instruments={Instruments.OPENAI},
)
```

> **Note:** When auto-instrumentation is enabled, the SDK automatically suppresses
> noisy warnings from instrumentors for providers you don't use (e.g. "Error
> initializing MistralAI instrumentor") and from OTel attribute type validation
> (e.g. provider SDKs using sentinel types like `Omit`). Enable
> `set_debug_logs(True)` to see these messages for troubleshooting.

## Buffering and Performance

All event-tracking calls (`track_ai`, `identify`, `track_signal`, and
`Interaction.set_input` / `set_properties` / `add_attachments` / `finish`) are
non-blocking from the caller's perspective. They append to an in-memory buffer
that a background daemon thread drains every second by POSTing to the Raindrop
API. The HTTP request never runs on the calling thread, so it is safe to call
these from a request hot path.

`shutdown()` is registered via `atexit` and drains any still-pending events
before the process exits. Call `flush()` explicitly if you need to force a
drain at a known point.

```python
# Tune the in-memory buffer size (default 10_000 events)
import raindrop.analytics as raindrop
raindrop.max_queue_size = 500
```

## Configuration

| Function | Description |
|---|---|
| `init(api_key, tracing_enabled=False, auto_instrument=True)` | Initialize the SDK |
| `set_debug_logs(True)` | Enable debug logging |
| `set_redact_pii(True)` | Enable PII redaction |
| `flush()` | Flush buffered events |
| `shutdown()` | Graceful shutdown (called automatically on exit) |

## Environment Variables

| Variable | Description |
|---|---|
| `TRACELOOP_TRACE_CONTENT` | Enable/disable content capture (default: `"true"`) |
| `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` | Max span attribute value length |

## Development

```bash
# Install dependencies
pip install poetry
poetry install

# Run tests
poetry run pytest

# Run with coverage
poetry run pytest --cov=raindrop

# Run specific test file
poetry run pytest tests/test_analytics.py -v
```

## License

MIT

