Expand description
A ContextProvider that stores and retrieves conversation memories in
Redis, scoped by application/agent/user/thread id.
Mirrors the scoping and prompt-injection behavior of the Python
agent_framework_redis.RedisProvider: after_run() persists the
request/response exchange as JSON entries tagged with the provider’s
scope, and before_run() retrieves matching entries and injects them into
the conversation as a single user-role Message prefixed by
DEFAULT_CONTEXT_PROMPT (same default text as Python’s
ContextProvider.DEFAULT_CONTEXT_PROMPT).
§Divergence from Python: BM25 full-text via RediSearch, but no vector search
Python’s RedisProvider is built on
redisvl and RediSearch: it
maintains a FT.CREATEd index with TAG/TEXT/VECTOR fields, and
before_run() runs a server-side TextQuery (BM25 full-text) or
HybridQuery (BM25 + KNN vector similarity, when a vectorizer is
configured) against that index.
RediSearch is a separate Redis module — bundled with Redis Stack but
not with plain open-source redis-server — so RedisContextProvider
detects it at runtime (once per provider instance, cached — see
FT._LIST/with_force_scan_fallback) and switches behavior accordingly:
- RediSearch available (Redis Stack): each memory is written with
JSON.SET {key_prefix}:entry:{uuid} $ <entry>instead of a plainSET, so it’s visible to anFT.CREATE ... ON JSON PREFIX 1 {key_prefix}:entry: SCHEMA ...index (created lazily, once, and tolerant of a concurrent creator via Redis’s own “Index already exists” error) covering everyMemoryEntryfield:contentisTEXT(BM25-scored full-text);application_id/agent_id/user_id/thread_id/role/message_id/author_nameareTAG(exact-match scope filtering);rankisNUMERIC SORTABLE.before_run()runs a singleFT.SEARCHcombining ANDedTAGfilters for the configured scope with an ORed@content:(...)clause over the same lowercased, stopword-filtered tokens the SCAN path uses (tokenize_query),LIMITed to the configuredRedisContextProvider::with_limit, and ranked by RediSearch’s own default scorer (a BM25 variant — no explicitSCORERargument is sent, since the exact literal scorer name has changed across RediSearch versions —BM25was renamedBM25STDin Redis Open Source 8.4 — while the default scorer has been BM25-family since RediSearch 1.x, so omittingSCORERgets BM25 scoring portably). All caller-supplied scope values and query tokens are backslash-escaped against RediSearch’s reserved query-syntax characters before being interpolated into the query string (escape_redisearch), so they can never be misinterpreted as query operators. - RediSearch unavailable (plain
redis-server), or whenRedisContextProvider::with_force_scan_fallbackis set: the original SCAN-based behavior, documented in detail below, is used unchanged.
Vector/hybrid (KNN) search is still not ported — that remains this crate’s one substantive gap versus Python, since it requires an embedding model/vectorizer this crate has no opinion on. Bring your own vector store (or embed client-side and extend this provider) if you need semantic recall; BM25 full-text (when Redis Stack is available) or token-match (otherwise) is what you get here.
Because RediSearch requires documents to be stored as native JSON
(JSON.SET) rather than opaque strings (SET) to be indexable, the two
storage encodings are not cross-readable: entries written while
RediSearch was available are invisible to the plain SCAN+MGET
fallback (which only sees string-typed keys), and vice versa. This only
matters if a deployment’s effective RediSearch usage changes after
memories already exist (e.g. migrating from OSS Redis to Redis Stack, or
flipping RedisContextProvider::with_force_scan_fallback mid-flight)
— capability is detected once per provider instance and does not
retroactively reconcile entries written under the other encoding.
§The SCAN fallback (RediSearch unavailable)
- Stores each memory as its own key
{key_prefix}:entry:{uuid}holding a JSON blob (content, role, scope fields, a client-assigned recency rank) — no index, no schema, noFT.CREATE/overwrite_index. - On
before_run(), enumerates candidates withSCAN MATCH {key_prefix}:entry:*(cursor-based, non-blocking — neverKEYS),MGETs their values, and filters entirely client-side:- scope filter: exact-match AND over whichever of
application/agent/user/thread id are configured (same semantics as
Python’s
Tag(k) == vconjunction), then - optional naive full-text filter: if the query text is non-empty,
split it into lowercased alphanumeric tokens, drop a small built-in
stopword list (
the,is,of, …), and keep only entries whose content contains (substring, not word-boundary) at least one remaining token — a keyword check, not BM25 ranking or embeddings, then - sort by recency (most recent first) and take the configured
limit(default 10, matching Python’snum_resultsdefault).
- scope filter: exact-match AND over whichever of
application/agent/user/thread id are configured (same semantics as
Python’s
There is no relevance ranking beyond “did a token match” plus recency,
no stemming, and — because every fallback before_run() call does a full
SCAN over the prefix — retrieval is O(entries under key_prefix)
rather than O(log n)/index-accelerated. This is adequate for modest
memory volumes (demos, tests, small deployments); reach for Redis Stack
(giving you the FT.SEARCH path above) for anything larger.
application_id participates in the scope filter here — unlike in the
sibling agent-framework-mem0 crate’s Mem0Provider, where it is
write-only metadata — matching the Python RedisProvider, which
includes application_id in its combined Tag filter for both reads
and writes.
Structs§
- Redis
Context Provider - Redis-backed
ContextProvider: recency-based memory storage/retrieval scoped by application/agent/user/thread id, upgraded to a realFT.SEARCHBM25 full-text index when the connected server supports RediSearch (Redis Stack). See the module docs for the RediSearch-vs-SCAN behavior and the remaining vector-search divergence from Python’sRedisProvider.
Constants§
- DEFAULT_
CONTEXT_ PROMPT - Default context-injection header, byte-for-byte identical to Python’s
agent_framework.ContextProvider.DEFAULT_CONTEXT_PROMPT. - DEFAULT_
KEY_ PREFIX - Default Redis key prefix, matching the Python provider’s default
prefix. - DEFAULT_
LIMIT - Default number of memories returned by
before_run(), matching Python’sRedisProvider._redis_search(..., num_results=10)default.