Metadata-Version: 2.4
Name: raindrop-bedrock
Version: 0.0.12
Summary: Raindrop observability integration for AWS Bedrock
Author-email: Raindrop AI <sdk@raindrop.ai>
License-Expression: MIT
Project-URL: Homepage, https://raindrop.ai
Project-URL: Documentation, https://docs.raindrop.ai
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: raindrop-ai>=0.0.70
Requires-Dist: boto3>=1.34.0
Provides-Extra: async
Requires-Dist: aioboto3>=12.0.0; extra == "async"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: requests>=2.28.0; extra == "dev"
Dynamic: license-file

# raindrop-bedrock

[![PyPI version](https://img.shields.io/pypi/v/raindrop-bedrock)](https://pypi.org/project/raindrop-bedrock/)
[![Python](https://img.shields.io/pypi/pyversions/raindrop-bedrock)](https://pypi.org/project/raindrop-bedrock/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

[Raindrop](https://raindrop.ai) observability integration for **AWS Bedrock** (Python). Automatically captures `converse()` and `invoke_model()` calls by wrapping the boto3 bedrock-runtime client.

## Installation

```bash
pip install raindrop-bedrock
```

For async support with `aioboto3`:

```bash
pip install raindrop-bedrock[async]
```

## Quick Start

```python
import boto3
from raindrop_bedrock import RaindropBedrock

rb = RaindropBedrock(api_key="your-write-key", user_id="user-123")

client = boto3.client("bedrock-runtime", region_name="us-east-1")
rb.wrap(client)

response = client.converse(
    modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
)

rb.flush()
```

## Projects

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

```python
rb = RaindropBedrock(
    api_key="your-write-key",
    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_bedrock(...)` factory. Invalid slugs are ignored with a warning and no header is sent.

### Debug Mode

Enable verbose logging with the `debug` flag:

```python
rb = RaindropBedrock(api_key="your-write-key", user_id="user-123", debug=True)
```

### Async Usage

```python
import aioboto3
from raindrop_bedrock import RaindropBedrock

rb = RaindropBedrock(api_key="rk_...", user_id="user-123")

session = aioboto3.Session()
async with session.client("bedrock-runtime", region_name="us-east-1") as client:
    rb.async_wrap(client)
    response = await client.converse(
        modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
        messages=[{"role": "user", "content": [{"text": "Hello!"}]}],
    )

rb.flush()
```

## Per-call properties and identity

`converse` / `invoke_model` 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 constructor values for those
events:

```python
from raindrop_bedrock import event_context

with event_context(user_id="user-123", convo_id="conv-9", properties={"plan": "pro"}):
    client.converse(modelId="anthropic.claude-3-5-sonnet-20241022-v2:0", messages=[{"role": "user", "content": [{"text": "Hello!"}]}])
```

## 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 = rb.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 `bedrock.*`; 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.

`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.

## identify()

Associate a user with optional traits:

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

## track_signal()

Track feedback, edits, or custom signals:

```python
rb.track_signal(
    event_id="evt_abc123",
    name="thumbs_up",
    signal_type="feedback",
    sentiment="POSITIVE",
    comment="Great answer!",
)
```

## flush() / shutdown()

Always call `flush()` before your process exits to ensure all telemetry is shipped:

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

### Legacy Factory Function

The `create_raindrop_bedrock()` factory function is still supported for backwards compatibility:

```python
from raindrop_bedrock import create_raindrop_bedrock

raindrop = create_raindrop_bedrock(api_key="your-write-key", user_id="user-123")
client = boto3.client("bedrock-runtime", region_name="us-east-1")
raindrop.wrap(client)
```

## What Gets Captured

| Method | Captured Data |
|--------|--------------|
| `converse()` | Input messages, output text, model ID, token usage (`inputTokens`/`outputTokens`), stop reason (`stopReason`), cached tokens (`cacheReadInputTokenCount`, `cacheWriteInputTokenCount`), conversation ID |
| `invoke_model()` | Raw request/response bodies, model ID, token usage (Claude, Titan, and Llama formats), stop reason (Claude: `stop_reason`, Llama: `stop_reason`), cached tokens (Claude: `cache_read_input_tokens`) |
| **Errors** | Error type and message are captured in event properties, then the exception is re-raised |

### Captured Properties

| Property Key | Source | Description |
|---|---|---|
| `ai.usage.prompt_tokens` | Both APIs | Input/prompt token count |
| `ai.usage.completion_tokens` | Both APIs | Output/completion token count |
| `ai.usage.cached_tokens` | Converse: `cacheReadInputTokenCount`; Claude InvokeModel: `cache_read_input_tokens` | Tokens read from cache |
| `ai.usage.cache_write_tokens` | Converse: `cacheWriteInputTokenCount` | Tokens written to cache |
| `bedrock.finish_reason` | Converse: `stopReason`; InvokeModel: varies by model | Why the model stopped generating |

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

Every model span produced under a wrapped `converse()` / `converse_stream()` call records the request's `toolConfig.tools[].toolSpec` as `ai.prompt.tools`; Anthropic `invoke_model()` / `invoke_model_with_response_stream()` calls record the request body's `tools` (`input_schema`). Capture is **exact**: the wrapper reads the list off the request it is about to send (the OpenLLMetry `bedrock` instrumentor emits no tool definitions itself). Each tool is one JSON document in an OTLP string array: `{"type":"function","name","description","inputSchema"}` for function tools (the JSON Schema is recorded as given — properties, enums, `required` and per-field descriptions intact) or `{"type":"provider-defined","id","name","args"}` for provider-owned tools, which get no invented schema. Converse `cachePoint` entries are not tools and are skipped. A call without a visible tool list records no attribute; an explicit empty list records `[]`.

Pass `tools=` to `RaindropBedrock(...)` / `create_raindrop_bedrock(...)` to record a different catalog instead: it replaces the inferred list for every call through that wrapper (never merges), and `tools=[]` records an empty catalog. The wrapper 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. The list lands on the wrapper's own model span (see [Model spans](#model-spans); on by default with tracing, no library instrumentation needed) and, with `disable_auto_instrument=False`, on the instrumentor's span. Streaming calls get only the latter. Capture requires `raindrop-ai>=0.0.70` (older cores log at debug and record nothing).

## API Reference

### `RaindropBedrock(api_key=None, user_id=None, convo_id=None, project_id=None, tracing_enabled=True, bypass_otel_for_tools=True, disable_auto_instrument=True, debug=False, tools=None)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | `str \| None` | `None` | Raindrop API key. Warns if not provided. |
| `user_id` | `str \| None` | `None` | Default user ID for events (required to send: without a user id, here or via `event_context`, an event is skipped with one warning) |
| `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 Raindrop tracing |
| `bypass_otel_for_tools` | `bool` | `True` | Bypass OpenTelemetry for tool-level instrumentation |
| `disable_auto_instrument` | `bool` | `True` | Library auto-instrumentation is opt-in (see below) |
| `debug` | `bool` | `False` | Enable verbose DEBUG-level 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.4`, `disable_auto_instrument` defaults to `True`: the
integration no longer lets Traceloop monkey-patch every LLM client library
it recognizes in your process (botocore client creation, OpenAI, Anthropic, etc.). The
wrapper captures input/output, token usage, model name, and stop reason
directly from the wrapped boto3 client's responses, so no library patching is needed for full
dashboards.

Model spans for non-streaming calls need no library instrumentation: the
wrapper records its own (see [Model spans](#model-spans)). If you
specifically want the instrumentor's spans as well (for example for
streaming calls) and have verified compatibility in your environment, opt
back in with `disable_auto_instrument=False`.

### Methods

| Method | Description |
|--------|-------------|
| `wrap(client)` | Instrument a sync boto3 bedrock-runtime client |
| `async_wrap(client)` | Instrument an async aioboto3 bedrock-runtime client |
| `identify(user_id, traits=None)` | Identify a user with optional traits |
| `track_signal(event_id, name, ...)` | Track a signal event |
| `last_event_id` | `event_id` of the most recent event 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 |
| `flush()` | Flush pending events |
| `shutdown()` | Flush and shut down |

## 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 `converse` and `invoke_model` call as a span on the Raindrop client's tracing. Streaming calls (`converse_stream`, `invoke_model_with_response_stream`) are not captured by the wrapper (no event and no span of its own; only their tool list is recorded for an auto-instrumentation to pick up). 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 instrumentation that sees the call already records it (the provider's own instrumentation, for example with `auto_instrument=True` on the client), the wrapper adds no span of its own, so a call is never counted twice. An instrumentation for another provider or library does not suppress the wrapper's span.

## 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 = RaindropBedrock(
    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/bedrock-python
pip install -e ".[async]"
pip install pytest
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).

## Known Limitations

- **InvokeModel body replacement**: After consuming the response body stream, it's replaced with a `BytesIO` object. Callers using `StreamingBody.read()` will get the same bytes, but the original `StreamingBody` API is not preserved.
- **Async support** requires the `[async]` extra (`aioboto3>=12.0.0`).

## Application Git metadata

`RaindropBedrock(...)` and `create_raindrop_bedrock(...)` 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.

## Full Documentation

[docs.raindrop.ai/integrations/bedrock](https://docs.raindrop.ai/integrations/bedrock)

## License

MIT
