Metadata-Version: 2.5
Name: raindrop-langchain
Version: 0.0.15
Summary: Raindrop integration for LangChain
Project-URL: Homepage, https://raindrop.ai
Project-URL: Documentation, https://docs.raindrop.ai
Author-email: Raindrop AI <sdk@raindrop.ai>
License-Expression: MIT
License-File: LICENSE
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: langchain-core>=0.3.0
Requires-Dist: raindrop-ai>=0.0.77
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# raindrop-langchain

Raindrop integration for LangChain (Python). Automatically captures LLM calls, tool usage, chains, and retrievers via LangChain's callback system.

## Installation

```bash
pip install raindrop-langchain langchain-core
```

## Quick Start

```python
from raindrop_langchain import RaindropLangchain
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

raindrop = RaindropLangchain(
    api_key="rk_...",
    user_id="user-123",
)

model = ChatOpenAI(model="gpt-4o")

result = model.invoke(
    [HumanMessage(content="Hello!")],
    config={"callbacks": [raindrop.handler]},
)

raindrop.flush()
```

### Factory Function (alternative)

```python
from raindrop_langchain import create_raindrop_langchain

raindrop = create_raindrop_langchain(api_key="rk_...", user_id="user-123")
model = ChatOpenAI(model="gpt-4o")
result = model.invoke("Hello!", config={"callbacks": [raindrop.handler]})
raindrop.flush()
```

## One event per turn

A turn (the outermost run your handler sees: a chain, an agent or a bare model
call) is **one** Raindrop event, as in the TypeScript integration:

- Model calls inside the turn update that event. Its input is the turn's first
  model input; its output, model and token usage are the **last** model call's.
- Tool calls are `TOOL_CALL` spans on the event, never events of their own.
- If a nested model call fails, the event carries its `error.type` /
  `error.message` (the most recent failure), even when a later call in the
  turn succeeds; the output is still the turn's last successful output. Only a
  failed turn (the outermost run itself fails) ends with the error and no
  output.
- Each model call's own input, output, model and token usage stay on a span
  for that call (a `task` span under the event, exported when `tracing_enabled`
  is on).

Earlier versions also sent every nested model call as a separate event. A tool
or retriever run that is itself the outermost run is still one event of its own.

Events that update only properties or usage need `raindrop-ai` 0.0.77 or later;
the turn's finishing update always carries the input, so it is sent on older
cores too.

## Projects

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

```python
raindrop = RaindropLangchain(
    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_langchain(...)` factory. Invalid slugs are ignored with a warning and no header is sent.

## What Gets Captured

- **LLM calls** — model name, input, output, token usage, finish reason
- **Tool calls** — tool name, input arguments, output, duration (via `interaction.track_tool()` spans)
- **Chains** — execution tracking
- **Retrievers** — query and document count
- **Errors** — error type and message captured in event properties
- **Extended token categories** — cached tokens (`ai.usage.cached_tokens`) and reasoning tokens (`ai.usage.thoughts_tokens`) when available from the provider (e.g. OpenAI)
- **Finish reason** — captured as `ai.finish_reason` in event properties (e.g. `"stop"`, `"length"`)

**Key clashes.** If one of your properties (LangChain `tags` / `metadata`) 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 `langchain.*`; avoid them
unless you mean to replace the wrapper's value.

## Debug Mode

Enable verbose logging with `debug=True`:

```python
raindrop = RaindropLangchain(
    api_key="rk_...",
    debug=True,
)
```

## Identify Users

Associate events with a user after initialization:

```python
raindrop.identify("user-123", {"name": "Alice", "plan": "pro"})
```

## Track Signals

Send feedback, edits, or custom signals:

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

## Updating the last event

`last_event_id` is the `event_id` of the root event of the most recent run
the handler captured (a run whose event was not sent, for example for lack of a
user id, leaves it unchanged). 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()
```

Property-only updates need `raindrop-ai` 0.0.77 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 runs it is whichever
run started last.

## Flushing and Shutdown

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

## API Reference

### `RaindropLangchain`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | `Optional[str]` | `None` | Raindrop API key. If `None`, telemetry is disabled |
| `user_id` | `Optional[str]` | `None` | Associate all events with a user |
| `convo_id` | `Optional[str]` | `None` | Group events into a conversation |
| `project_id` | `Optional[str]` | `None` | Route events to a specific [project](https://docs.raindrop.ai/platform/projects) (slug); omit for the default **Production** project |
| `trace_chains` | `bool` | `True` | Track chain execution |
| `trace_retrievers` | `bool` | `True` | Track retriever calls |
| `filter_langgraph_internals` | `bool` | `True` | Filter LangGraph-internal chain events and deduplicate LLM callbacks |
| `event_name` | `str` | `"ai_generation"` | Event name applied to every event (the `event` field in the dashboard) |
| `tracing_enabled` | `bool` | `True` | Enable distributed tracing |
| `bypass_otel_for_tools` | `bool` | `True` | Bypass OTEL for tool spans |
| `debug` | `bool` | `False` | Enable debug logging |

#### Methods

| Method | Description |
|--------|-------------|
| `handler` | Property — the LangChain callback handler to pass into `config={"callbacks": [...]}` |
| `flush()` | Flush all pending events to the Raindrop API |
| `shutdown()` | Flush remaining events and release resources |
| `identify(user_id, traits)` | 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 |

## Async Support

The callback handler inherits from LangChain's `AsyncCallbackHandler` and works with both synchronous and asynchronous LangChain invocations.

```python
result = await model.ainvoke(
    [HumanMessage(content="Hello!")],
    config={"callbacks": [raindrop.handler]},
)
```

## LangGraph Support

Works with LangGraph out of the box. The handler automatically filters LangGraph-internal chain events and deduplicates LLM callbacks. Pass the handler to the model inside your LLM node — **not** to `graph.invoke()`. See `examples/langchain-langgraph-python-basic/` for a full example.

## LangSmith Coexistence

Raindrop and LangSmith can run simultaneously. Set `LANGSMITH_TRACING=false` to disable LangSmith if you only want Raindrop.

## Payload size bounds

Payloads the handler serializes itself — multi-modal chat content lists and
agent-action tool inputs — are bounded to 1,000,000 characters with a
`...[truncated by raindrop]` marker. The bound is enforced *during*
serialization (cost proportional to the cap, not the payload), so a multi-MB
content list (e.g. base64 image parts) can't stall your event loop inside a
synchronous callback. Plain-string prompts and tool outputs are capped by
the Raindrop SDK's own per-field limit (`max_text_field_chars`, raindrop-ai
>= 0.0.51).

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

Every model span records the tools LangChain handed the provider for that call as `ai.prompt.tools`: an OTel string array, one JSON document per tool with `name`, `description` and the full JSON Schema as `inputSchema` (`{"type": "function", ...}`), or `{"type": "provider-defined", ...}` for provider-owned tools. The capture is **exact, per call**: `RaindropCallbackHandler.on_chat_model_start` / `on_llm_start` receive `invocation_params["tools"]` (or the legacy `functions`), which is the list `bind_tools()` / `bind(tools=...)` put on the request. The handler binds that list through the core's `prompt_tools_scope` mechanism for the duration of the model call and releases it in `on_llm_end` / `on_llm_error`, so the model span the OpenLLMetry LangChain instrumentor (or the OpenAI instrumentor under it) starts inside the call carries the attribute. A call whose `invocation_params` has no `tools` or `functions` records `[]`. Sync, async (`ainvoke`, the handler runs inline in the caller's task) and streaming calls are covered because the span starts inside the call.

Callback and instrumentor agree: the instrumentor writes the same list to `gen_ai.tool.definitions` (OpenLLMetry 0.62 emits that attribute, not `llm.request.functions.*`; `raindrop-ai` 0.0.70/0.0.71 do not rebuild `ai.prompt.tools` from it and 0.0.72 does; the wrapper binds the list itself so capture does not depend on the core version, and the bound list takes precedence). The callback's `invocation_params` is the source of truth; the test suite asserts both carry the same tools.

There is no model span when the instrumentor is off, so there is nothing to carry the attribute. `RaindropLangchain(...)` builds its own `Raindrop` client with `auto_instrument=False`; pass `client=Raindrop(..., tracing_enabled=True, instruments={Instruments.LANGCHAIN})` (or `auto_instrument=True`) to get model spans.

Override:

```python
# Default for every model span this handler sees
rd = RaindropLangchain(api_key="...", user_id="u1", tools=[{"type": "function", "name": "get_weather", "inputSchema": {...}}])
handler = RaindropCallbackHandler(user_id="u1", tools=[...])          # same option on the raw handler

# Per call, through LangChain config metadata (stripped from the event properties)
chain.invoke(messages, config={"callbacks": [handler], "metadata": {"raindrop_tools": [...]}})
```

The per-call value replaces the constructor default; the constructor default replaces the inferred list; `tools=[]` at either level records an empty catalog. An enclosing `raindrop.prompt_tools(...)` / `begin(..., tools=...)` scope is also treated as an override and is never shadowed by the inferred list.

Semantics shared with every Raindrop SDK:

- **Attribute absent**: the wrapper could not see the tool list (or there is no model span, see below).
- **`[]`**: the model had no tools.
- **Override replaces, never merges.** A `tools=` you pass wins over anything inferred; `tools=[]` records an empty catalog.
- **Content gate.** Tool definitions are prompt content. With `TRACELOOP_TRACE_CONTENT=false` nothing is recorded, overrides included.
- Malformed entries are skipped with a debug log; nothing here raises into your application.

Accepted shapes for `tools=`: canonical `{"type": "function", "name", "description", "inputSchema"}`, OpenAI `{"type": "function", "function": {...}}`, Anthropic `{"name", "description", "input_schema"}`, and `{"type": "provider-defined", "name", ...}`. The core normalises them; the JSON Schema is kept exactly as given.

Needs `raindrop-ai>=0.0.70`; on an older core the wrapper logs at debug and records nothing.

## User id

Every event needs a real user id: set `user_id=` on the wrapper. 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 model call in a turn 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.

## Known Limitations

- **Multi-LLM turns**: the turn's event carries the last model call's output, model and usage; earlier calls are on their spans (see [One event per turn](#one-event-per-turn)).

## Application Git metadata

`RaindropLangchain(...)` and `create_raindrop_langchain(...)` 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. `RaindropCallbackHandler(...)` is unchanged and does not accept this option.

## 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 = RaindropLangchain(
    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.77 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/langchain-python
pip install -e .
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).
