Metadata-Version: 2.4
Name: langgraph-store-upstash
Version: 0.1.0
Summary: LangGraph BaseStore implementation backed by Upstash Redis (REST client).
Project-URL: Homepage, https://github.com/Tghez/langgraph-store-upstash
Project-URL: Repository, https://github.com/Tghez/langgraph-store-upstash
Project-URL: Issues, https://github.com/Tghez/langgraph-store-upstash/issues
Author: Tal
License: MIT
License-File: LICENSE
Keywords: agents,langchain,langgraph,memory,redis,store,upstash
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
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
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: langgraph-checkpoint>=2.0.0
Requires-Dist: upstash-redis>=1.2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.6.0; extra == 'dev'
Description-Content-Type: text/markdown

# langgraph-store-upstash

A [LangGraph](https://github.com/langchain-ai/langgraph) `BaseStore`
implementation backed by [Upstash Redis](https://upstash.com) — talks to
Redis exclusively over its REST API (`upstash-redis`), so it works from
serverless/edge runtimes that can't hold a TCP connection open.

See [DESIGN.md](./DESIGN.md) for the Redis key schema and the reasoning
behind the `batch()`/`abatch()` dispatcher — Upstash's REST client has no
RediSearch, no vector index, and no automatic JSON serialization, so the
schema looks different from the official Postgres/Redis-TCP stores.

## Install

```bash
pip install langgraph-store-upstash
```

## Usage

### Sync

```python
from langgraph_store_upstash import UpstashStore

store = UpstashStore(url="UPSTASH_REDIS_REST_URL", token="UPSTASH_REDIS_REST_TOKEN")
# or: store = UpstashStore.from_env()  # reads the same two env vars

store.put(("documents", "user123"), "report1", {"memory": "Will likes ai"})
store.get(("documents", "user123"), "report1")
store.search(("documents",), filter={"memory": {"$ne": None}})
store.list_namespaces(prefix=("documents",))
store.delete(("documents", "user123"), "report1")
```

### Async

```python
from langgraph_store_upstash.aio import AsyncUpstashStore

store = AsyncUpstashStore.from_env()
await store.aput(("documents", "user123"), "report1", {"memory": "Will likes ai"})
await store.aget(("documents", "user123"), "report1")
```

Both classes also accept an existing client instead of `url`/`token`:

```python
from upstash_redis import Redis
from langgraph_store_upstash import UpstashStore

store = UpstashStore(client=Redis(url=..., token=...))
```

## What's implemented

- `get`/`put`/`delete`/`search`/`list_namespaces` (+ async equivalents),
  all built on `batch()`/`abatch()` per the `BaseStore` contract — only
  `batch()`/`abatch()` are implemented directly.
- Per-item TTL (`put(..., ttl=minutes)`), backed by native Redis `EXPIRE`
  (one Redis key per item, so this doesn't depend on newer hash-field-TTL
  Redis versions). `refresh_ttl` is honored on `get()`.
- `search()`'s `filter` — one operator per field (`$eq`, `$ne`, `$gt`,
  `$gte`, `$lt`, `$lte`), applied client-side after fetching candidates
  from a namespace-prefix-narrowed Redis range scan.
- `list_namespaces()` — `prefix`/`suffix` matching including `"*"`
  wildcard segments, `max_depth`, `limit`/`offset`, matching LangGraph's
  reference stores' dedupe-then-sort-then-paginate behavior.

## What's not (yet) implemented

- **Semantic search.** `search(..., query=...)` is accepted but ignored —
  plain Upstash Redis has no vector index (Upstash's Vector product is a
  separate service/client). `IndexConfig`/embeddings aren't wired up.
- **`created_at` preservation across overwrites.** Every `put()` to an
  existing key currently resets both `created_at` and `updated_at` to the
  time of the write, rather than preserving the original `created_at`.
  Correct behavior would need a pre-read on every put; deferred for now.
- **`refresh_ttl` on `search()`** is a no-op — only `get()` currently
  refreshes TTLs on read.
- **Suffix matching in `list_namespaces()` is O(namespace count)**, not
  indexed — it scans the full namespace index and filters client-side,
  since a forward-sorted Redis sorted set can't do an efficient suffix
  range query the way it can for prefixes. Namespace cardinality is
  expected to stay well below item cardinality, so this is intended to
  stay cheap in practice; DESIGN.md documents the reverse-index
  alternative that was considered and deferred.

## Testing

```bash
pip install -e ".[dev]"
pytest tests/test_shared.py -v          # no network required

# optional: full read/write test against a real Upstash database
export UPSTASH_REDIS_REST_URL="https://<db>.upstash.io"
export UPSTASH_REDIS_REST_TOKEN="<token>"
pytest tests/test_live_integration.py -v
```

## License

MIT
