Metadata-Version: 2.5
Name: raindrop-strands
Version: 0.0.14
Summary: Raindrop integration for Strands Agents
Project-URL: Homepage, https://raindrop.ai
Author-email: Raindrop AI <sdk@raindrop.ai>
License-Expression: MIT
License-File: LICENSE
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: raindrop-ai>=0.0.70
Requires-Dist: strands-agents>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: requests>=2.20; extra == 'dev'
Description-Content-Type: text/markdown

# raindrop-strands

Raindrop integration for [Strands Agents](https://strandsagents.com) (Python). Automatically captures agent invocations, model calls, tool usage, and token metrics via the Strands hook system.

## Installation

```bash
pip install raindrop-strands strands-agents
```

`strands-agents` is a required dependency.

## Quick Start

```python
import os
from strands import Agent
from raindrop_strands import RaindropStrands

raindrop = RaindropStrands(
    api_key=os.environ.get("RAINDROP_API_KEY"),
    user_id="user_123",
    convo_id="session_456",
)

agent = Agent(
    model="us.amazon.nova-lite-v1:0",
    system_prompt="You are a helpful assistant.",
)

raindrop.handler.register_hooks(agent)

result = agent("What is the capital of France?")
print(result)

raindrop.flush()
```

Omitting `api_key` disables telemetry shipping (a warning is emitted) but does not crash your application.

## Debug Mode

Enable verbose logging to troubleshoot telemetry issues:

```python
raindrop = RaindropStrands(
    api_key=os.environ.get("RAINDROP_API_KEY"),
    debug=True,
)
```

## Configuration

```python
raindrop = RaindropStrands(
    api_key="rk_...",              # Optional: Raindrop API key
    user_id="user_123",            # Required: events without a user id are not sent
    convo_id="session_456",        # Optional: conversation/session ID
    project_id="support-prod",     # Optional: route events to a specific project (slug)
    tracing_enabled=True,          # Optional: enable OTEL-based tracing (default: True)
    bypass_otel_for_tools=True,    # Optional: bypass OTEL for tool spans (default: True)
    debug=False,                   # Optional: enable debug logging (default: False)
)
```

## Projects

Route events to a specific [project](https://docs.raindrop.ai/platform/projects) by passing its slug as `project_id`:

```python
raindrop = RaindropStrands(
    api_key="rk_...",
    project_id="support-prod",
)
```

`project_id` sets the `X-Raindrop-Project-Id` header on every event. Omit it (or pass `"default"`) to use your org's default **Production** project, which is the existing behavior. The same option is accepted by the `create_raindrop_strands(...)` factory. Invalid slugs are ignored with a warning and no header is sent.

## Factory Function

A `create_raindrop_strands()` factory function is also available for convenience:

```python
from raindrop_strands import create_raindrop_strands

raindrop = create_raindrop_strands(api_key="rk_...")
agent = Agent(model="us.amazon.nova-lite-v1:0")
raindrop.handler.register_hooks(agent)
result = agent("Hello!")
raindrop.flush()
```

## Identifying Users

```python
raindrop.identify(
    user_id="user_123",
    traits={"plan": "pro", "email": "user@example.com"},
)
```

## Tracking Signals

Track user feedback or other signals on AI responses:

```python
raindrop.track_signal(
    event_id="evt_...",
    name="thumbs_up",
    signal_type="feedback",
    sentiment="POSITIVE",
)
```

## Per-call properties and identity

An agent invocation has no channel for your own event data, so wrap the call in
`event_context`. Its `properties` merge into every event captured inside the
block, and `user_id` / `convo_id` replace the wrapper's own values for those
events:

```python
from raindrop_strands import event_context

with event_context(user_id="user-123", convo_id="conv-9", properties={"plan": "pro"}):
    agent("What is the capital of France?")
```

## Updating the last event

`last_event_id` is the `event_id` of the most recent event this wrapper
captured. Use it to attach a signal, or reopen the event with
`resume_interaction()` (defaults to `last_event_id`) and update it through the
core SDK's `Interaction`:

```python
interaction = raindrop.resume_interaction()
if interaction is not None:
    interaction.set_properties({"reviewed": True})
    interaction.finish()
```

**Key clashes.** If one of your properties has the same key as one the wrapper
derives itself, your value wins, whichever update carries it (a debug-level log
line names the key). The wrapper's own keys are `ai.*` (for example
`ai.usage.prompt_tokens`), `error`/`error.*` and `strands.*`; avoid them unless
you mean to replace the wrapper's value.

Property-only updates need `raindrop-ai` 0.0.76 or newer; older core
versions drop an update that carries no input or output; the wrapper logs one warning when it resumes an event on such a core.

`last_event_id` is per wrapper instance; under concurrent calls it is whichever
event was captured last, so pass `event_id=` when you need to correlate
concurrent calls.

## Flush & Shutdown

Always flush before your process exits to ensure all data is sent:

```python
raindrop.flush()       # flush pending data
raindrop.shutdown()    # flush + release resources
```

## What Gets Captured

- **Agent invocations**: input prompt, output text, model name
- **Token usage**: prompt tokens, completion tokens, and cached tokens (from Bedrock/Anthropic `cacheReadInputTokens` / `cacheCreationInputTokens`)
- **Tool call spans**: individual tool spans tracked via `interaction.track_tool()` with name, input, output, duration, and error
- **Finish reason**: `stop_reason` or `finish_reason` from model responses (e.g., `end_turn`, `tool_use`)
- **Errors**: error type and message captured in event properties
- **Async support**: preserved via Strands' hook system

## Tool catalog capture (`ai.prompt.tools`)

Every model span of an instrumented agent records the agent's registered tool specs (`agent.tool_registry.get_all_tool_specs()`, the same list the SDK hands the model on each call) as `ai.prompt.tools`. Capture is **exact**: Strands reads that registry right after the `BeforeModelCallEvent` hooks run and passes it to `Model.stream(tool_specs=...)` verbatim, and the handler binds the same list in `on_before_model_call` and releases it in `on_after_model_call` (and at the end of the invocation as a safety net), so the model span opened in between — the framework's own `chat` span carrying `gen_ai.request.model`, or a span from an OpenLLMetry instrumentor when library auto-instrumentation is enabled — carries the list. Each tool is one JSON document in an OTLP string array: `{"type":"function","name","description","inputSchema"}` (the JSON Schema under `inputSchema.json` is recorded as given — properties, enums, `required` and per-field descriptions intact). An agent whose registry cannot be read records no attribute; an agent with no tools records `[]`. Strands' structured-output forced mode (which sends a single synthetic tool instead of the registry) is not reflected.

Pass `tools=` to `RaindropStrands(...)` / `create_raindrop_strands(...)` to record a different catalog instead: it replaces the registry list for every model call of every agent registered with that handler (never merges), and `tools=[]` records an empty catalog. The hook handler has no per-call metadata channel, so there is no per-call override. Tool definitions are prompt content and follow the same switch as prompts: with `TRACELOOP_TRACE_CONTENT=false` they are never recorded. Capture requires `raindrop-ai>=0.0.70` (older cores log at debug and record nothing).

## API

### `RaindropStrands(api_key, user_id, convo_id, project_id, tracing_enabled, bypass_otel_for_tools, disable_auto_instrument, debug, tools)`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `api_key` | `str \| None` | `None` | Raindrop API key (`rk_...`). Omit to disable telemetry |
| `user_id` | `str \| None` | `None` | Associate all events with a user |
| `convo_id` | `str \| None` | `None` | Group events into a conversation |
| `project_id` | `str \| None` | `None` | Route events to a specific [project](https://docs.raindrop.ai/platform/projects) (slug); omit for the default **Production** project |
| `tracing_enabled` | `bool` | `True` | Enable OTEL-based tracing |
| `bypass_otel_for_tools` | `bool` | `True` | Bypass OTEL for tool spans |
| `disable_auto_instrument` | `bool` | `True` | Library auto-instrumentation is opt-in (see below) |
| `debug` | `bool` | `False` | Enable debug logging |
| `tools` | `Sequence[Mapping] \| None` | `None` | Override for the `ai.prompt.tools` catalog (see above); `[]` records an empty catalog |

### Library auto-instrumentation is opt-in

As of `0.0.3`, `disable_auto_instrument` defaults to `True`: the
integration no longer lets Traceloop monkey-patch every LLM client library
it recognizes in your process (including the botocore machinery Strands' default Bedrock provider drives). The
hook handler captures input/output, token usage, model name, and tool calls
directly from Strands hook events, so no library patching is needed for full
dashboards.

If you specifically want LLM-call-level spans from library instrumentation
and have verified compatibility in your environment, opt back in with
`disable_auto_instrument=False`.

**Properties:**

- `handler` — `RaindropEventHandler` instance to register on agents

**Methods:**

- `flush()` — flush pending telemetry
- `shutdown()` — flush and release resources
- `identify(user_id, traits)` — identify a user with optional traits
- `track_signal(event_id, name, ...)` — track a signal event
- `event_context(user_id=None, convo_id=None, properties=None)` — module function: per-call properties and identity for the events captured inside the block
- `last_event_id` — `event_id` of the most recent invocation this wrapper captured, or `None`
- `resume_interaction(event_id=None)` — reopen an event (default: `last_event_id`) as a core `Interaction` to update it; `None` when there is nothing to resume

## Application Git metadata

`RaindropStrands(...)` and `create_raindrop_strands(...)` accept the keyword-only `app_git` option. It defaults to `True`: explicit Raindrop Git environment or deployment context is applied immediately, and the base SDK may perform one bounded background local-Git lookup from the process working directory. Event capture, flush, and shutdown never wait for that lookup. Pass `False` to disable enrichment, or pass an `AppGitOptions` mapping with `commit_sha`, `commit_dirty`, `branch`, `source_directory`, `detect_branch`, and/or `auto_detect`. Automatic branch discovery remains opt-in through `detect_branch=True` (or `RAINDROP_GIT_DETECT_BRANCH=true`).

For an ordinary in-process application, the process working directory is treated as the application-under-test checkout. A remote, coding, workflow, or observer process must not rely on its own checkout: pass `app_git=False`, provide explicit revision values, or set `source_directory` to the actual application checkout. Canonical per-operation properties remain authoritative. When supplying `client=`, configure `app_git` while constructing that `Raindrop` client; the supplied client is authoritative and the wrapper's `app_git` argument does not reconfigure it.

Release order is deliberate: first publish the base SDK feature, then publish the wrapper feature release with its minimum dependency coordinated to that base release. The existing `raindrop-ai` lower bound remains compatible, but application Git metadata is unavailable on an older core and must not be claimed complete until the base is upgraded. Until coordination assigns a released version, the wrapper checks for an explicit base `app_git` parameter and omits the option when unsupported. Explicit non-default configuration is debug-logged and omitted. Unsupported `app_git` is determined by signature inspection before construction, not by retrying initialization after a `TypeError`; Git configuration adds no initialization attempts and does not change any existing framework-specific initialization fallback.

## User id

Every event needs a real user id: set `user_id=` on the wrapper or per call with `event_context(user_id=...)`. Without one (missing, blank, or a placeholder such as `"unknown"` or `"anonymous"`), the event is **not sent** and the wrapper logs one warning; it never raises. Updating an event you resumed with `resume_interaction(event_id=...)` needs no user id, since ingest keeps the one it stored.

## Model spans

With tracing on (the default), the wrapper records each agent invocation (one span for its model calls, with the invocation's accumulated usage) as a span on the Raindrop client's tracing. The span carries:

- the model: `gen_ai.provider.name` (canonical name, such as `openai` or `amazon-bedrock`), `gen_ai.request.model` and `gen_ai.response.model`;
- the token usage the provider reports: `gen_ai.usage.input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_write_input_tokens` and `reasoning_tokens`;
- the event it belongs to: `ai.telemetry.metadata.raindrop.eventId` and the `traceloop.association.properties.*` keys.

Token usage also stays on the event as `ai.usage.*` properties. When an OpenTelemetry auto-instrumentation already records the model calls (for example with `auto_instrument=True` on the client), the wrapper adds no span of its own, so a call is never counted twice.

## Standard options and flush results

When the wrapper builds its own client, it accepts the core SDK's standard
options by the same names and passes them on:

```python
raindrop = RaindropStrands(
    api_key="rk_...",
    user_id="user-123",
    redact_pii=True,        # local PII redaction of event input/output
    flush_timeout=5,        # seconds; deadline for flush() and shutdown()
    debug=True,             # also raises the core SDK's log verbosity
    max_queue_size=5_000,   # any other option `Raindrop(...)` declares
)

result = raindrop.flush()   # the core's FlushResult; safe to ignore
```

- An option the installed `raindrop-ai` does not support is ignored with a
  debug log, never an error. `flush_timeout` and the `FlushResult` return
  value need `raindrop-ai` 0.0.76 or later; on older releases `flush()` and
  `shutdown()` return `None`.
- With `client=` (a `Raindrop` you built), configure these options on that
  client; the wrapper ignores them (debug log).
- `flush()` and `shutdown()` never raise. The result is truthy when
  everything was delivered, and reports `pending`, `dropped` and `timed_out`.

## Testing

```bash
cd packages/strands-python
pip install -e '.[dev]'
python -m pytest tests/ -v   # unit tests (no external services)
```

End-to-end behavior is verified by the cross-SDK **conformance harness**. This
package ships a thin conformance driver at
[`conformance/driver.py`](conformance/driver.py) that maps the shared scenario
corpus onto the wrapper's public API; known gaps are tracked as ticket-linked
entries in [`conformance/failures.txt`](conformance/failures.txt). The **fault
lane** runs on every PR touching `packages/*-python/**`
(`.github/workflows/conformance-wrappers-python.yml`) against a local capture
server; the **prod lane** verifies delivery by reading back through the public
[Query API](https://docs.raindrop.ai/api-reference/overview). The harness is
pinned by commit SHA (`HARNESS_REF`). See the harness docs:
[HOW-IT-WORKS](https://github.com/invisible-tools/raindrop-sdk-harness/blob/main/docs/HOW-IT-WORKS.md) ·
[AGENTS](https://github.com/invisible-tools/raindrop-sdk-harness/blob/main/AGENTS.md) ·
[README](https://github.com/invisible-tools/raindrop-sdk-harness/blob/main/README.md).

## Full Documentation

See the [Raindrop Strands integration docs](https://docs.raindrop.ai/integrations/strands) for full details.

## License

MIT
