jeevesagent.memory¶
Memory backends.
Pick one of these and pass it to Agent(..., memory=...):
InMemoryMemory— naive dict-backed, no embeddings. The default; great for tests and tiny demos.VectorMemory— in-memory with embedding-based cosine recall. Pure Python, no infrastructure. Scales to a few thousand episodes.ChromaMemory— backed by Chroma (local persistent or in-memory client). Lazychromadbimport.PostgresMemory— Postgres + pgvector with HNSW index. Lazyasyncpg+pgvectorimports. Production-grade.RedisMemory— Redis with optional RediSearch HNSW vector index, falling back to brute-force when RediSearch isn’t available. Lazyredisimport.
Embedders live in jeevesagent.memory.embedder:
HashEmbedder— deterministic, zero-dep, perfect for tests.OpenAIEmbedder— real semantic embeddings via OpenAI.
Submodules¶
- jeevesagent.memory.chroma
- jeevesagent.memory.chroma_facts
- jeevesagent.memory.consolidator
- jeevesagent.memory.embedder
- jeevesagent.memory.facts
- jeevesagent.memory.inmemory
- jeevesagent.memory.postgres
- jeevesagent.memory.postgres_facts
- jeevesagent.memory.redis
- jeevesagent.memory.redis_facts
- jeevesagent.memory.sqlite_facts
- jeevesagent.memory.vector
- jeevesagent.memory.worker
Classes¶
Bi-temporal fact store backed by a Chroma collection. |
|
Memory backed by |
|
Embeddings via Cohere's |
|
Periodic consolidator for any |
|
Wraps a |
|
Storage surface for bi-temporal facts. |
|
Deterministic SHA256-seeded unit vectors. |
|
Dict-backed bi-temporal fact store. |
|
Dict-backed implementation of |
|
Embeddings via OpenAI's |
|
Postgres-backed bi-temporal fact store. |
|
Postgres-backed |
|
Bi-temporal fact store over plain Redis hashes. |
|
Redis-backed |
|
Durable bi-temporal fact store rooted at a sqlite file. |
|
Pure-Python embedding-backed |
|
Embeddings via Voyage AI's |
Package Contents¶
- class jeevesagent.memory.ChromaFactStore(client: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_FACTS_COLLECTION)[source]¶
Bi-temporal fact store backed by a Chroma collection.
- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
- classmethod ephemeral(*, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_FACTS_COLLECTION) ChromaFactStore[source]¶
- classmethod local(persist_directory: str, *, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_FACTS_COLLECTION) ChromaFactStore[source]¶
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- property embedder: jeevesagent.core.protocols.Embedder¶
- class jeevesagent.memory.ChromaMemory(client: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_COLLECTION, fact_store: Any | None = None)[source]¶
Memory backed by
chromadb.Construct via
local()for an on-disk persistent client orephemeral()for a process-local in-memory client.- classmethod ephemeral(*, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_COLLECTION, with_facts: bool = False, facts_collection_name: str = 'jeeves_facts') ChromaMemory[source]¶
In-memory client (lost on process exit). Great for tests.
- classmethod local(persist_directory: str, *, embedder: jeevesagent.core.protocols.Embedder | None = None, collection_name: str = DEFAULT_COLLECTION, with_facts: bool = False, facts_collection_name: str = 'jeeves_facts') ChromaMemory[source]¶
Persistent on-disk client at
persist_directory.with_facts=Trueattaches aChromaFactStorerooted at the same client so facts persist alongside episodes in the same on-disk store.
- async recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) list[jeevesagent.core.types.Episode][source]¶
- async recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async remember(episode: jeevesagent.core.types.Episode) str[source]¶
- async session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) list[jeevesagent.core.types.Message][source]¶
- async working() list[jeevesagent.core.types.MemoryBlock][source]¶
- class jeevesagent.memory.CohereEmbedder(model: str = 'embed-english-v3.0', *, client: Any | None = None, api_key: str | None = None, input_type: str = 'search_document')[source]¶
Embeddings via Cohere’s
cohereSDK.Models and dimensions:
embed-english-v3.0/embed-multilingual-v3.0-> 1024embed-english-light-v3.0/embed-multilingual-light-v3.0-> 384
input_typeis required by Cohere v3 models:"search_document"(default) — corpus / fact-store entries"search_query"— retrieval queries"classification"/"clustering"for non-retrieval uses
- class jeevesagent.memory.ConsolidationWorker(memory: jeevesagent.core.protocols.Memory, *, interval_seconds: float = 60.0, on_consolidated: OnConsolidatedCb | None = None, on_error: OnErrorCb | None = None)[source]¶
Periodic consolidator for any
Memorybackend.- async run_forever() None[source]¶
Sleep
interval_secondsthen consolidate. Repeat until cancelled.Spawn this in an
anyio.create_task_group()— the cancel scope at scope exit terminates the worker cooperatively.
- async run_once() int[source]¶
Run a single consolidation pass. Returns the number of new facts extracted (
0when no fact store / nothing changed).Errors in
memory.consolidate()are routed toon_errorand not re-raised, so callers can use this in a polling loop without wrapping it in their own try/except.
- class jeevesagent.memory.Consolidator(*, model: jeevesagent.core.protocols.Model, system_prompt: str = DEFAULT_SYSTEM_PROMPT, max_facts_per_episode: int = 20)[source]¶
Wraps a
Modelto extractFactrows from episodes.- async consolidate(episodes: collections.abc.Iterable[jeevesagent.core.types.Episode], *, store: jeevesagent.memory.facts.FactStore) list[jeevesagent.core.types.Fact][source]¶
Process
episodes; append extracted facts tostore; return the newFactinstances in extraction order.Uses
store.append_manywhen available so the underlying store can batch the embedder calls (oneembed_batchAPI round-trip instead of N individualembedcalls). Falls back to per-factappendfor stores that haven’t implementedappend_many.
- class jeevesagent.memory.FactStore[source]¶
Bases:
ProtocolStorage surface for bi-temporal facts.
- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- class jeevesagent.memory.HashEmbedder(dimensions: int = DEFAULT_HASH_DIMENSIONS)[source]¶
Deterministic SHA256-seeded unit vectors.
Each text gets a fresh
random.Randomseeded by the SHA256 of its UTF-8 bytes, then samplesdimensionsGaussian values and L2-normalises the result. Same text always produces the same vector; different texts produce well-distributed vectors with cosine distances that correlate with literal text equality (not semantic similarity).Use this in tests (fast, no network) and as a default for in-memory backends that need some vector but don’t need real semantic recall.
- class jeevesagent.memory.InMemoryFactStore(*, embedder: jeevesagent.core.protocols.Embedder | None = None)[source]¶
Dict-backed bi-temporal fact store.
All operations are coordinated by an
anyio.Lockso concurrent appends from the consolidator and reads from the agent loop don’t tear the index.When an
embedderis supplied, every appended fact’s triple ("subject predicate object") is embedded and stored alongside the fact, andrecall_text()ranks by cosine similarity against the query’s embedding. When no embedder is given,recall_text()falls back to token-overlap matching.- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
Append a fact, invalidating any superseded predecessors.
Supersession rule: any existing fact with matching subject + predicate, currently valid (
valid_until is None), and a differentobjectgets itsvalid_untilset to the new fact’svalid_from.
- async append_many(facts: collections.abc.Iterable[jeevesagent.core.types.Fact]) list[str][source]¶
Append a batch of facts. Embedder calls are coalesced via
Embedder.embed_batch()when an embedder is configured — one network round-trip for the batch instead of N.
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
Rank facts against
query.With an embedder configured: cosine-similarity over the query’s embedding vs each fact triple’s stored embedding. Without one: token-overlap with a small stop-word list (longer overlaps win, ties break by shorter haystack = more specific match).
user_idpartitions the candidate set as a hard namespace boundary — seeFactfor semantics.
- snapshot() dict[str, jeevesagent.core.types.Fact][source]¶
- property embedder: jeevesagent.core.protocols.Embedder | None¶
- class jeevesagent.memory.InMemoryMemory(*, consolidator: jeevesagent.memory.consolidator.Consolidator | None = None, fact_store: jeevesagent.memory.facts.FactStore | None = None)[source]¶
Dict-backed implementation of
Memory.- async consolidate() None[source]¶
Process unconsolidated episodes through the configured
Consolidator, appending facts toself.facts.
- async recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) list[jeevesagent.core.types.Episode][source]¶
- async recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async remember(episode: jeevesagent.core.types.Episode) str[source]¶
- async session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) list[jeevesagent.core.types.Message][source]¶
Return user/assistant pairs from prior runs of this session.
Materialises each persisted
Episodefor the givensession_id(within theuser_idpartition) into a[USER input, ASSISTANT output]pair, ordered oldest-first and capped atlimitturns total — i.e. up tolimit / 2Q/A exchanges. Tool-call traces are not replayed; the final assistant text per turn is sufficient context for follow-ups.
- async working() list[jeevesagent.core.types.MemoryBlock][source]¶
- class jeevesagent.memory.OpenAIEmbedder(model: str = 'text-embedding-3-small', *, dimensions: int | None = None, client: Any | None = None, api_key: str | None = None)[source]¶
Embeddings via OpenAI’s
embeddings.createAPI.Dimensions are fixed by the model:
text-embedding-3-small-> 1536text-embedding-3-large-> 3072text-embedding-ada-002-> 1536
Pass
dimensions=only fortext-embedding-3-*models, which support thedimensionsparameter for projection.
- class jeevesagent.memory.PostgresFactStore(pool: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None)[source]¶
Postgres-backed bi-temporal fact store.
- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
- async append_many(facts: collections.abc.Iterable[jeevesagent.core.types.Fact]) list[str][source]¶
- classmethod connect(dsn: str, *, embedder: jeevesagent.core.protocols.Embedder | None = None, min_size: int = 1, max_size: int = 10) PostgresFactStore[source]¶
- Async:
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- schema_sql() list[str][source]¶
Return the DDL for this fact store’s schema.
Exposed so tests can assert on the SQL strings, and so migration scripts can apply the schema in their own transaction.
- property embedder: jeevesagent.core.protocols.Embedder | None¶
- class jeevesagent.memory.PostgresMemory(pool: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None, namespace: str = DEFAULT_NAMESPACE, fact_store: Any | None = None)[source]¶
Postgres-backed
Memory.poolis anasyncpg.Pool(or anything with the same API). Tests can pass a fake pool whoseacquire()returns a fake connection.- classmethod connect(dsn: str, *, embedder: jeevesagent.core.protocols.Embedder | None = None, namespace: str = DEFAULT_NAMESPACE, min_size: int = 1, max_size: int = 10, with_facts: bool = False) PostgresMemory[source]¶
- Async:
Open an asyncpg pool and register the pgvector codec.
When
with_facts=TrueaPostgresFactStorerooted at the same pool is attached asself.factsso the agent loop’s memory.facts integration just works.
- async init_schema() None[source]¶
Apply
schema_sql()against the connected pool.When a
PostgresFactStoreis attached asself.facts, its schema is initialised in the same call.
- async recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) list[jeevesagent.core.types.Episode][source]¶
- async recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async remember(episode: jeevesagent.core.types.Episode) str[source]¶
- schema_sql() list[str][source]¶
Return the DDL needed to bootstrap this backend’s schema.
Exposed so tests can assert on the SQL without running it; also usable from migration scripts that want to apply the schema in their own transaction.
- async session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) list[jeevesagent.core.types.Message][source]¶
- async working() list[jeevesagent.core.types.MemoryBlock][source]¶
- class jeevesagent.memory.RedisFactStore(client: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None, key_prefix: str = DEFAULT_KEY_PREFIX)[source]¶
Bi-temporal fact store over plain Redis hashes.
- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
- classmethod connect(url: str = 'redis://localhost:6379/0', *, embedder: jeevesagent.core.protocols.Embedder | None = None, key_prefix: str = DEFAULT_KEY_PREFIX) RedisFactStore[source]¶
- Async:
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- property embedder: jeevesagent.core.protocols.Embedder¶
- class jeevesagent.memory.RedisMemory(client: Any, *, embedder: jeevesagent.core.protocols.Embedder | None = None, key_prefix: str = DEFAULT_KEY_PREFIX, index_name: str = DEFAULT_INDEX_NAME, use_vector_index: bool = True, fact_store: Any | None = None)[source]¶
Redis-backed
Memory. Useconnect()to construct.- classmethod connect(url: str = 'redis://localhost:6379/0', *, embedder: jeevesagent.core.protocols.Embedder | None = None, key_prefix: str = DEFAULT_KEY_PREFIX, index_name: str = DEFAULT_INDEX_NAME, use_vector_index: bool = True, with_facts: bool = False, fact_key_prefix: str = 'jeeves:fact:') RedisMemory[source]¶
- Async:
Open an async Redis connection.
with_facts=Trueattaches aRedisFactStoresharing the same client; facts go to{fact_key_prefix}*keys so they don’t collide with episode keys.
- async ensure_index() None[source]¶
Create the RediSearch HNSW index, if not already present.
Skipped silently when
use_vector_index=Falseor when RediSearch isn’t available on the server.
- async recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) list[jeevesagent.core.types.Episode][source]¶
- async recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async remember(episode: jeevesagent.core.types.Episode) str[source]¶
- async session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) list[jeevesagent.core.types.Message][source]¶
- async working() list[jeevesagent.core.types.MemoryBlock][source]¶
- class jeevesagent.memory.SqliteFactStore(path: str | pathlib.Path, *, embedder: jeevesagent.core.protocols.Embedder | None = None)[source]¶
Durable bi-temporal fact store rooted at a sqlite file.
- async all_facts() list[jeevesagent.core.types.Fact][source]¶
- async append(fact: jeevesagent.core.types.Fact) str[source]¶
Append a fact, invalidating any superseded predecessors.
Same supersession rule as
InMemoryFactStore: if there’s an existing currently-valid fact with matching subject + predicate but different object, set itsvalid_untilto the new fact’svalid_from.
- async query(*, subject: str | None = None, predicate: str | None = None, object_: str | None = None, valid_at: datetime.datetime | None = None, limit: int = 10, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async recall_text(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- property embedder: jeevesagent.core.protocols.Embedder | None¶
- property path: pathlib.Path¶
- class jeevesagent.memory.VectorMemory(*, embedder: jeevesagent.core.protocols.Embedder | None = None, max_episodes: int | None = None, consolidator: jeevesagent.memory.consolidator.Consolidator | None = None, fact_store: jeevesagent.memory.facts.FactStore | None = None)[source]¶
Pure-Python embedding-backed
Memory.- async consolidate() None[source]¶
Process unconsolidated episodes through the configured
Consolidator, appending facts toself.facts.No-op when no consolidator is configured.
- async recall(query: str, *, kind: str = 'episodic', limit: int = 5, time_range: tuple[datetime.datetime, datetime.datetime] | None = None, user_id: str | None = None) list[jeevesagent.core.types.Episode][source]¶
- async recall_facts(query: str, *, limit: int = 5, valid_at: datetime.datetime | None = None, user_id: str | None = None) list[jeevesagent.core.types.Fact][source]¶
- async remember(episode: jeevesagent.core.types.Episode) str[source]¶
- async session_messages(session_id: str, *, user_id: str | None = None, limit: int = 20) list[jeevesagent.core.types.Message][source]¶
- async working() list[jeevesagent.core.types.MemoryBlock][source]¶
- property embedder: jeevesagent.core.protocols.Embedder¶
- class jeevesagent.memory.VoyageEmbedder(model: str = 'voyage-3', *, client: Any | None = None, api_key: str | None = None, input_type: str = 'document')[source]¶
Embeddings via Voyage AI’s
voyageaiSDK.Models and dimensions:
voyage-3/voyage-3-large/voyage-code-3-> 1024voyage-3-lite-> 512
input_typecontrols how Voyage encodes the text:"document"(default) — for corpus / fact-store entries"query"— for retrieval queries
Pass an explicit
input_type=if your embedder is dedicated to one role; for the agent loop’s mixed use (we embed both stored triples and recall queries through the same embedder), the"document"default is the safer choice.