Skip to main content
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

  1. Client calls memory.add(messages, user_id=...) with conversation messages
  2. Message Parser normalizes input into a flat string
  3. LLM Fact Extraction sends conversation + system prompt → receives JSON array of discrete facts
  4. 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
  5. Execute each event against the vector store
  6. Log every operation to SQLite history table
  7. Optionally extract entities and relationships to graph store
  8. Return list of memory events to the caller

2. Data Model

2.1 MemoryItem

The core data structure representing a single stored memory:
Hash computation: hash = md5(memory_text).hexdigest(). Used to detect exact duplicate memories before insertion.

2.2 MemoryEvent

Represents a single operation performed during an add() 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:
Validation rule: At least one of 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

The constructor initializes three subsystems:
  1. Vector store — configured via config.vector_store
  2. LLM — configured via config.llm
  3. Embedder — configured via config.embedder
  4. History store — SQLite database (always initialized, path configurable)
  5. Graph store (optional) — Neo4j, configured via config.graph_store
If no config is provided, use sensible defaults:
  • 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:
Algorithm (detailed in Section 4):
  1. Parse messages into a flat conversation string
  2. Call LLM with fact extraction prompt → get JSON array of facts
  3. For each fact: embed → search existing (limit 5) → call LLM update decision → execute event
  4. Log all events to history
  5. If graph store configured, extract entities/relationships
  6. Return events

3.3 Method: search(query, ...scope, limit?, filters?)

Purpose: Retrieve memories semantically similar to a query. Parameters:
Algorithm:
  1. Generate embedding for query text
  2. Build metadata filter from scope + any additional filters
  3. Query vector store: vectorStore.search(embedding, limit, filters)
  4. 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.
Algorithm: Query vector store with scope-based metadata filter, no embedding (list all matching records).

3.6 Method: update(memory_id, new_text)

Purpose: Directly overwrite a memory’s text. Algorithm:
  1. Retrieve existing memory by ID
  2. Generate new embedding for new_text
  3. Compute new hash: md5(new_text)
  4. Update vector store record: text, embedding, hash, updated_at
  5. Log UPDATE event to history

3.7 Method: delete(memory_id)

Purpose: Remove a single memory. Algorithm:
  1. Retrieve existing memory by ID (for history logging)
  2. Delete from vector store
  3. Log DELETE event to history

3.8 Method: delete_all(...scope)

Purpose: Remove all memories for a given scope. Algorithm:
  1. Retrieve all memories for scope via get_all()
  2. Delete each from vector store
  3. 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:
  1. Drop and recreate vector store collection
  2. Truncate history table (or drop and recreate)

4. LLM-Driven Memory Pipeline (Core Algorithm)

This is the heart of the system. The add() 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):
User message: The parsed conversation string. LLM call configuration:
  • Temperature: 0 (deterministic extraction)
  • Response format: JSON mode (if available) or parse JSON from response text
Parse result: Extract JSON array from LLM response. If parsing fails, try to find JSON array pattern ([...]) 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):
User message construction:
LLM call configuration:
  • Temperature: 0
  • Response format: JSON
Parse result: Extract JSON array of event objects from the LLM response.

4.4 Step 4: Execute Memory Events

For each event returned by the update decision LLM: ADD event:
  1. Generate a new UUID v4 for the memory
  2. Compute embedding for the fact text
  3. Compute hash: md5(fact_text)
  1. Log to history: historyStore.log(memory_id, "ADD", null, fact_text)
UPDATE event:
  1. Get the target memory ID from the event
  2. Compute new embedding for the updated text
  3. Compute new hash
  1. Log to history: historyStore.log(memory_id, "UPDATE", old_text, new_text)
DELETE event:
  1. Get the target memory ID
  2. Delete from vector store: vectorStore.delete(id)
  3. Log to history: historyStore.log(memory_id, "DELETE", old_text, null, is_deleted=true)
NONE event: No action. Optionally log for analytics.

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:
For each extracted relationship:
When searching with graph memory enabled, also query the graph for entities related to the search query and merge those results with vector search results. Use BM25 reranking if the graph store supports it to score relevance of graph-retrieved memories.

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:
Cosine similarity:

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 a translateFilter(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/messages with tool use for structured extraction
  • Google: Gemini API with JSON schema in generationConfig
  • Ollama: POST /api/chat with 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):
In languages with native async (Python asyncio, JavaScript), the async class should use async HTTP clients (aiohttp, fetch) for LLM and vector store calls rather than blocking.

12. REST API Wrapper (Optional Server Mode)

For serving memory as a standalone service:

12.1 Endpoints

12.2 Authentication

Bearer token authentication via Authorization: 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

  1. Search by semantics — After adding “User likes Python”, search("programming languages") → returns the Python memory with score > 0.5
  1. Get by ID — After ADD, get(returned_id) → returns the memory item
  2. Get nonexistentget("fake-id") → returns null
  1. Update overwritesupdate(id, "new text")get(id).memory equals “new text”
  2. Update changes hash — After update, hash should equal md5("new text")
  3. Delete removesdelete(id)get(id) returns null
  1. Reset clears everythingreset() → all collections and history are empty

Memory Update Intelligence

  1. Deduplication — Add “User likes Python” then add “User likes Python” again → second call returns NONE event
  2. Update on contradiction — Add “User lives in NYC” then add “User moved to San Francisco” → returns UPDATE event changing NYC to SF
  3. 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
  4. Delete on negation — Add “User is vegetarian” then add “User started eating meat again” → returns DELETE or UPDATE removing vegetarian claim
  5. Multiple events per add — Single conversation may produce multiple ADD + UPDATE events in one call

Scoping

  1. Scope isolation — Memories added with user_id: "alice" are NOT returned when searching with user_id: "bob"
  1. Missing scope error — Calling add(msg, {}) with no scope fields → throws ScopeError
  2. Run ID isolation — Memories for run_id: "session-1" are separate from run_id: "session-2"

History

  1. ADD creates history — After add(), history(memory_id) returns one record with event “ADD”
  2. UPDATE appends history — After update(), history has ADD then UPDATE records
  3. DELETE marks in history — After delete(), history shows DELETE with is_deleted: true
  4. History ordered by time — History records are returned in chronological order

Filters

  1. In filteroperator: "in", value: ["a","b"] matches records where field is “a” or “b”
  2. AND composition — Both conditions must match
  3. OR composition — Either condition matches
  4. NOT negation — Excludes matching records
  5. Contains stringoperator: "contains", value: "Python" matches “User likes Python for ML”

Graph Memory

  1. Entity extraction — After adding conversation about “Alice at Google”, graph contains entities “Alice” (person) and “Google” (organization)
  2. Relationship extraction — Graph contains relationship “Alice” —works_at—> “Google”
  3. Graph-enhanced search — Search that matches a graph entity also returns related memories from connected entities

Error Handling

  1. LLM failure graceful — If LLM API is down, add() returns empty results (no crash)
  2. Partial failure continues — If embedding fails for one of 3 facts, the other 2 are still processed
  3. Invalid scope rejected — Empty scope object throws descriptive error

Custom Configuration

  1. Custom extraction prompt — Providing prompt parameter to add() changes the fact extraction behavior
  2. Custom LLM provider — Memory works with Anthropic/Google/Ollama as LLM backend
  3. Custom vector store — Memory works with Qdrant/pgvector/ChromaDB backends
  4. Default config worksnew Memory() with no config uses in-memory store and OpenAI defaults

16. Implementation Priorities

Phase 1: Core (MVP)

  1. Memory class with add/search/get/get_all/update/delete
  2. In-memory vector store
  3. OpenAI LLM + embedder
  4. SQLite history
  5. Fact extraction + update decision pipeline

Phase 2: Production Backends

  1. Qdrant vector store backend
  2. pgvector backend
  3. ChromaDB backend
  4. Filter expression system with backend translation

Phase 3: Advanced Features

  1. Graph memory (Neo4j)
  2. Async API
  3. REST server wrapper
  4. Additional LLM providers (Anthropic, Google, Ollama)
  5. Additional vector store backends

Phase 4: Optimization

  1. Batch embedding for multiple facts
  2. Connection pooling for vector stores
  3. LLM response caching for identical conversations
  4. Configurable concurrency for parallel fact processing