Normalized for Mintlify from
knowledge-base/neurigraph-memory-architecture/neurigraph-tool-references/10-Lightweight-Fact-Based-AI-Memory-API.mdx.Clean-Room Specification: Lightweight Fact-Based AI Memory API
Purpose of This Document
This document specifies the architecture for a fact-based AI memory system that automatically extracts, stores, deduplicates, and retrieves discrete factual memories from conversations. Rather than storing raw conversation transcripts, the system uses an LLM to distill conversations into atomic facts (e.g., “User prefers dark mode,” “User works at Acme Corp”), stores them as vector embeddings for semantic retrieval, and maintains a full audit history of every memory operation. The system supports user/agent/session scoping, pluggable vector store backends, optional graph-based entity-relationship memory, and both synchronous and asynchronous APIs. This specification enables independent implementation from scratch.1. System Overview
1.1 Core Concept
Traditional memory systems store raw conversation logs. This system takes a fundamentally different approach: it uses an LLM as a memory curator that reads conversations, extracts discrete facts, compares them against existing memories, and decides whether to ADD new facts, UPDATE existing ones, DELETE obsolete ones, or take NO action. The result is a clean, deduplicated factual memory store that grows smarter over time.1.2 High-Level Architecture
1.3 Data Flow Summary
- Client calls
memory.add(messages, user_id=...)with conversation messages - Message Parser normalizes input into a flat string
- LLM Fact Extraction sends conversation + system prompt → receives JSON array of discrete facts
- For each extracted fact: a. Generate embedding vector b. Search vector store for similar existing memories (top 5) c. LLM Memory Update Decision compares new fact against existing memories → produces ADD/UPDATE/DELETE/NONE events
- Execute each event against the vector store
- Log every operation to SQLite history table
- Optionally extract entities and relationships to graph store
- Return list of memory events to the caller
2. Data Model
2.1 MemoryItem
The core data structure representing a single stored memory:hash = md5(memory_text).hexdigest(). Used to detect exact duplicate memories before insertion.
2.2 MemoryEvent
Represents a single operation performed during anadd() call:
2.3 Message Format
Input messages follow the standard chat message format:2.4 Scoping Model
Every memory operation requires at least one scope identifier. These are used as metadata filters on the vector store to isolate memories:user_id, agent_id, or run_id MUST be provided on every API call. If none are provided, raise an error: "At least one of user_id, agent_id, or run_id must be provided".
Filter construction: When scoping, build a metadata filter that matches ALL provided scope fields. For example, if both user_id="alice" and agent_id="helper" are provided, the vector store query filters for records where metadata.user_id == "alice" AND metadata.agent_id == "helper".
3. Memory Class — Public API
3.1 Constructor
- Vector store — configured via
config.vector_store - LLM — configured via
config.llm - Embedder — configured via
config.embedder - History store — SQLite database (always initialized, path configurable)
- Graph store (optional) — Neo4j, configured via
config.graph_store
- Vector store: In-memory (e.g., a simple array with brute-force cosine similarity)
- LLM: OpenAI
gpt-4o-mini - Embedder: OpenAI
text-embedding-3-small(dimension 1536) - History: SQLite at
~/.memory/history.db
3.2 Method: add(messages, ...scope, metadata?, filters?)
Purpose: Extract facts from messages and store them as memories.
Parameters:
- Parse messages into a flat conversation string
- Call LLM with fact extraction prompt → get JSON array of facts
- For each fact: embed → search existing (limit 5) → call LLM update decision → execute event
- Log all events to history
- If graph store configured, extract entities/relationships
- Return events
3.3 Method: search(query, ...scope, limit?, filters?)
Purpose: Retrieve memories semantically similar to a query.
Parameters:
- Generate embedding for query text
- Build metadata filter from scope + any additional filters
- Query vector store:
vectorStore.search(embedding, limit, filters) - Return results with similarity scores
3.4 Method: get(memory_id)
Purpose: Retrieve a single memory by its ID.
Returns: MemoryItem or null if not found.
3.5 Method: get_all(...scope, limit?)
Purpose: Retrieve all memories for a given scope.
Parameters: Same scope parameters. limit defaults to 100.
3.6 Method: update(memory_id, new_text)
Purpose: Directly overwrite a memory’s text.
Algorithm:
- Retrieve existing memory by ID
- Generate new embedding for
new_text - Compute new hash:
md5(new_text) - Update vector store record: text, embedding, hash, updated_at
- Log UPDATE event to history
3.7 Method: delete(memory_id)
Purpose: Remove a single memory.
Algorithm:
- Retrieve existing memory by ID (for history logging)
- Delete from vector store
- Log DELETE event to history
3.8 Method: delete_all(...scope)
Purpose: Remove all memories for a given scope.
Algorithm:
- Retrieve all memories for scope via
get_all() - Delete each from vector store
- Log DELETE event for each to history
3.9 Method: history(memory_id)
Purpose: Retrieve the full audit trail for a specific memory.
Returns: Array of history records, ordered by timestamp ascending:
3.10 Method: reset()
Purpose: Delete ALL memories and history. Nuclear option.
Algorithm:
- Drop and recreate vector store collection
- Truncate history table (or drop and recreate)
4. LLM-Driven Memory Pipeline (Core Algorithm)
This is the heart of the system. Theadd() method orchestrates a multi-step pipeline that uses LLM calls to intelligently manage memories.
4.1 Step 1: Message Parsing
Convert input to a flat string for the LLM:4.2 Step 2: Fact Extraction via LLM
Send the conversation to the LLM with a system prompt that instructs it to extract discrete facts. FACT_EXTRACTION_PROMPT (system message):- Temperature: 0 (deterministic extraction)
- Response format: JSON mode (if available) or parse JSON from response text
[...]) in the response text. If still fails, return empty array.
Custom prompt support: If the caller provides a prompt parameter to add(), use that as the system message instead of FACT_EXTRACTION_PROMPT. This allows domain-specific fact extraction.
4.3 Step 3: Per-Fact Processing Loop
For each extracted fact string, execute the following sub-steps:4.3.1 Generate Embedding
4.3.2 Search Existing Memories
Query the vector store for the top 5 most similar existing memories within the current scope:4.3.3 LLM Memory Update Decision
This is the critical decision-making step. Send the new fact AND the retrieved existing memories to the LLM, which decides what action to take. UPDATE_MEMORY_PROMPT (system message):- Temperature: 0
- Response format: JSON
4.4 Step 4: Execute Memory Events
For each event returned by the update decision LLM: ADD event:- Generate a new UUID v4 for the memory
- Compute embedding for the fact text
- Compute hash:
md5(fact_text)
- Log to history:
historyStore.log(memory_id, "ADD", null, fact_text)
- Get the target memory ID from the event
- Compute new embedding for the updated text
- Compute new hash
- Log to history:
historyStore.log(memory_id, "UPDATE", old_text, new_text)
- Get the target memory ID
- Delete from vector store:
vectorStore.delete(id) - Log to history:
historyStore.log(memory_id, "DELETE", old_text, null, is_deleted=true)
4.5 Step 5: Graph Memory Extraction (Optional)
If a graph store is configured, additionally extract entities and relationships.Entity Extraction
Use an LLM tool call with the following tool definition: EXTRACT_ENTITIES_TOOL:Relationship Extraction
EXTRACT_RELATIONS_TOOL:Graph Store Operations
For each extracted entity, perform an upsert in the graph database:5. History Store (SQLite)
5.1 Schema
5.2 Logging Function
5.3 Query Function
6. Vector Store Abstraction
6.1 VectorStoreBase Interface
All vector store backends implement this interface:6.2 In-Memory Vector Store (Default)
For development and testing, implement a simple in-memory store:6.3 Qdrant Backend
6.4 PostgreSQL/pgvector Backend
6.5 ChromaDB Backend
6.6 Additional Backend Targets
The interface should support these backends (implementation details vary but all implement VectorStoreBase):- Pinecone: REST API with namespaces for scoping
- Weaviate: GraphQL-based queries with class schemas
- Milvus: gRPC client with collection/partition model
- FAISS: Local file-based index with separate metadata store
- Elasticsearch: kNN search with dense_vector field type
- Azure AI Search: REST API with vector search profiles
- Redis: RediSearch with VECTOR field type (HNSW/FLAT)
7. Filter Expression System
7.1 Filter Syntax
Filters allow complex metadata queries across all vector store backends. The system defines a portable filter expression that is translated to each backend’s native syntax.7.2 Operator Semantics
7.3 Composition
7.4 Backend Translation
Each vector store backend implements atranslateFilter(expr: FilterExpression) method that converts the portable expression to the backend’s native format. For example:
- pgvector:
WHERE payload->>'field' = 'x'
8. Configuration System
8.1 MemoryConfig
8.2 Environment Variable Fallbacks
The system checks environment variables as fallbacks for API keys and configuration:9. Embedder Abstraction
9.1 EmbedderBase Interface
9.2 OpenAI Embedder
9.3 Ollama Embedder (Local)
10. LLM Abstraction
10.1 LLMBase Interface
10.2 Provider Implementations
Each LLM provider maps to its respective API:- Anthropic:
POST /v1/messageswith tool use for structured extraction - Google: Gemini API with JSON schema in
generationConfig - Ollama:
POST /api/chatwith local models
11. Async API
11.1 AsyncMemory Class
Provide an async variant that wraps the synchronous Memory class (or implements natively with async I/O):12. REST API Wrapper (Optional Server Mode)
For serving memory as a standalone service:12.1 Endpoints
12.2 Authentication
Bearer token authentication viaAuthorization: Bearer <token> header. Tokens can be project-scoped API keys.
13. Usage Examples
13.1 Basic Usage
13.2 Multi-Scope Usage
13.3 Custom Configuration
13.4 With Filters
14. Error Handling
14.1 Error Types
14.2 Retry Logic
LLM and embedding calls should implement exponential backoff retry:14.3 Graceful Degradation
- If fact extraction LLM call fails, return empty results (don’t crash)
- If embedding call fails for one fact, skip that fact and continue with others
- If history DB is unavailable, log warning but continue with memory operations
- If graph store is unavailable, skip graph extraction but complete vector operations
15. Behavioral Test Cases
Memory CRUD
- Search by semantics — After adding “User likes Python”,
search("programming languages")→ returns the Python memory with score > 0.5
- Get by ID — After ADD,
get(returned_id)→ returns the memory item - Get nonexistent —
get("fake-id")→ returns null
- Update overwrites —
update(id, "new text")→get(id).memoryequals “new text” - Update changes hash — After update, hash should equal
md5("new text") - Delete removes —
delete(id)→get(id)returns null
- Reset clears everything —
reset()→ all collections and history are empty
Memory Update Intelligence
- Deduplication — Add “User likes Python” then add “User likes Python” again → second call returns NONE event
- Update on contradiction — Add “User lives in NYC” then add “User moved to San Francisco” → returns UPDATE event changing NYC to SF
- Merge on refinement — Add “User works in tech” then add “User works at Google as a senior engineer” → returns UPDATE with merged, more specific memory
- Delete on negation — Add “User is vegetarian” then add “User started eating meat again” → returns DELETE or UPDATE removing vegetarian claim
- Multiple events per add — Single conversation may produce multiple ADD + UPDATE events in one call
Scoping
- Scope isolation — Memories added with
user_id: "alice"are NOT returned when searching withuser_id: "bob"
- Missing scope error — Calling
add(msg, {})with no scope fields → throws ScopeError - Run ID isolation — Memories for
run_id: "session-1"are separate fromrun_id: "session-2"
History
- ADD creates history — After
add(),history(memory_id)returns one record with event “ADD” - UPDATE appends history — After
update(), history has ADD then UPDATE records - DELETE marks in history — After
delete(), history shows DELETE withis_deleted: true - History ordered by time — History records are returned in chronological order
Filters
- In filter —
operator: "in", value: ["a","b"]matches records where field is “a” or “b” - AND composition — Both conditions must match
- OR composition — Either condition matches
- NOT negation — Excludes matching records
- Contains string —
operator: "contains", value: "Python"matches “User likes Python for ML”
Graph Memory
- Entity extraction — After adding conversation about “Alice at Google”, graph contains entities “Alice” (person) and “Google” (organization)
- Relationship extraction — Graph contains relationship “Alice” —works_at—> “Google”
- Graph-enhanced search — Search that matches a graph entity also returns related memories from connected entities
Error Handling
- LLM failure graceful — If LLM API is down,
add()returns empty results (no crash) - Partial failure continues — If embedding fails for one of 3 facts, the other 2 are still processed
- Invalid scope rejected — Empty scope object throws descriptive error
Custom Configuration
- Custom extraction prompt — Providing
promptparameter toadd()changes the fact extraction behavior - Custom LLM provider — Memory works with Anthropic/Google/Ollama as LLM backend
- Custom vector store — Memory works with Qdrant/pgvector/ChromaDB backends
- Default config works —
new Memory()with no config uses in-memory store and OpenAI defaults
16. Implementation Priorities
Phase 1: Core (MVP)
- Memory class with add/search/get/get_all/update/delete
- In-memory vector store
- OpenAI LLM + embedder
- SQLite history
- Fact extraction + update decision pipeline
Phase 2: Production Backends
- Qdrant vector store backend
- pgvector backend
- ChromaDB backend
- Filter expression system with backend translation
Phase 3: Advanced Features
- Graph memory (Neo4j)
- Async API
- REST server wrapper
- Additional LLM providers (Anthropic, Google, Ollama)
- Additional vector store backends
Phase 4: Optimization
- Batch embedding for multiple facts
- Connection pooling for vector stores
- LLM response caching for identical conversations
- Configurable concurrency for parallel fact processing