Metadata-Version: 2.4
Name: pensyve-langchain
Version: 1.1.0
Summary: Pensyve memory backend for LangChain / LangGraph
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pensyve>=0.1.0
Provides-Extra: capture
Requires-Dist: langchain-core>=0.3.0; extra == 'capture'
Provides-Extra: cloud
Requires-Dist: httpx>=0.27; extra == 'cloud'
Provides-Extra: dev
Requires-Dist: langchain-core>=0.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# Pensyve LangChain / LangGraph Integration

Drop-in Pensyve memory backend for LangGraph agents. Implements the same
`put` / `get` / `search` / `delete` interface as LangGraph's `InMemoryStore`,
backed by Pensyve's 8-signal fusion retrieval engine.

## Authentication

```python
from pensyve import PensyveClient

# Explicit API key
client = PensyveClient(api_key="psy_your_key_here")

# Or from environment variable
# export PENSYVE_API_KEY="psy_your_key_here"
client = PensyveClient()
```

Create an API key at [pensyve.com/settings/api-keys](https://pensyve.com/settings/api-keys).

## Installation

```bash
pip install pensyve-langchain
```

Or copy the `pensyve_langchain.py` file into your project.

## Quick Start

```python
from pensyve_langchain import PensyveStore

store = PensyveStore()

# Store memories
store.put(("user_123", "memories"), "pref-1", {"text": "likes dark mode"})
store.put(("user_123", "memories"), "pref-2", {"text": "prefers Python"})

# Search by semantic query
items = store.search(("user_123", "memories"), query="color preferences")
for item in items:
    print(f"{item.key}: {item.value}")

# Get by exact key
item = store.get(("user_123", "memories"), "pref-1")
print(item.value)  # {"text": "likes dark mode"}

# Delete
store.delete(("user_123", "memories"), "pref-1")
```

## Usage with LangGraph

```python
from langgraph.graph import StateGraph, START, END
from pensyve_langchain import PensyveStore

store = PensyveStore()

# Pre-populate some user preferences
store.put(("preferences",), "user-42", {"theme": "dark", "lang": "en"})

def my_node(state, *, store):
    # Read from the store
    prefs = store.get(("preferences",), "user-42")
    theme = prefs.value["theme"] if prefs else "light"

    # Write to the store
    store.put(("user-42", "memories"), "last-query", {"q": state["query"]})

    return {"response": f"Using {theme} theme"}

builder = StateGraph(...)
builder.add_node("node", my_node)
# ...
graph = builder.compile(store=store)
```

## Local vs Cloud Mode

The store auto-detects which mode to use:

| Condition                     | Mode      | Backend                            |
| ----------------------------- | --------- | ---------------------------------- |
| No API key set                | **Local** | Pensyve PyO3 engine (zero latency) |
| `PENSYVE_API_KEY` env var set | **Cloud** | Pensyve REST API                   |
| `api_key=` argument passed    | **Cloud** | Pensyve REST API                   |

```python
# Local mode (default)
store = PensyveStore()

# Cloud mode via argument
store = PensyveStore(api_key="psy_your_key_here")

# Cloud mode via environment variable
# export PENSYVE_API_KEY=psy_your_key_here
store = PensyveStore()
```

## API Reference

### `PensyveStore(namespace, path, api_key, base_url)`

| Parameter   | Type          | Default     | Description                                     |
| ----------- | ------------- | ----------- | ----------------------------------------------- |
| `namespace` | `str`         | `"default"` | Pensyve namespace for isolation                 |
| `path`      | `str \| None` | `None`      | Local storage directory (local mode)            |
| `api_key`   | `str \| None` | `None`      | Cloud API key (falls back to `PENSYVE_API_KEY`) |
| `base_url`  | `str \| None` | `None`      | Override cloud API URL                          |

### Methods

| Method                                       | Description                            |
| -------------------------------------------- | -------------------------------------- |
| `put(namespace, key, value)`                 | Store a dict under namespace/key       |
| `get(namespace, key)`                        | Get a single item by key, or `None`    |
| `search(namespace, *, query, filter, limit)` | Semantic search within a namespace     |
| `delete(namespace, key)`                     | Delete memories for a namespace entity |
| `list_namespaces(*, prefix, limit, offset)`  | List known namespaces                  |

All methods have async variants prefixed with `a` (e.g. `aput`, `aget`).

### `Item`

Returned by `get()` and `search()`. Matches `langgraph.store.base.Item`.

| Field        | Type              | Description               |
| ------------ | ----------------- | ------------------------- |
| `namespace`  | `tuple[str, ...]` | The namespace tuple       |
| `key`        | `str`             | The item key              |
| `value`      | `dict[str, Any]`  | The stored value dict     |
| `created_at` | `float`           | Unix timestamp            |
| `updated_at` | `float`           | Unix timestamp            |
| `score`      | `float \| None`   | Retrieval relevance score |

## Running Tests

```bash
cd integrations/langchain
pytest tests/ -v
```

## Intelligent Memory Capture

The `PensyveCaptureHandler` is a LangChain callback handler that automatically
captures decisions, errors, and tool outputs during chain execution. Signals are
classified into tiers:

- **Tier 1** — high-confidence memories (architecture decisions, constraints) are auto-stored
- **Tier 2** — lower-confidence candidates (root causes, failed approaches) are held for review

```python
from pensyve_capture import PensyveCaptureHandler

handler = PensyveCaptureHandler(client=my_pensyve_client)

# Attach to any chain
result = chain.invoke(inputs, config={"callbacks": [handler]})

# Review tier 2 candidates after execution
for mem in handler.get_pending_review():
    print(f"[review] {mem.fact} (confidence={mem.confidence})")

handler.clear_pending_review()
```

Install with capture dependencies:

```bash
pip install pensyve-langchain[capture]
```

## Namespace Mapping

LangGraph namespace tuples are joined with `/` to form Pensyve entity names:

| LangGraph namespace        | Pensyve entity      |
| -------------------------- | ------------------- |
| `("user_123", "memories")` | `user_123/memories` |
| `("global",)`              | `global`            |
| `()`                       | `default`           |
