Ingestion Flow
SmartMemory's ingestion pipeline transforms raw input into enriched, interconnected memories through a sophisticated multi-stage process.
Overview
The ingestion flow consists of 11 stages that process memories from initial input to final storage:
- classify - Determine memory type (semantic, episodic, procedural, pending, zettel)
- coreference - Resolve pronouns and entity mentions
- simplify - Optional sentence simplification for cleaner extraction
- entity_ruler - Pattern-based entity tagging (public knowledge patterns + workspace patterns)
- llm_extract - LLM entity & relation extraction (SpacyExtractor as deterministic fallback)
- ontology_constrain - Constrain extracted types against the active ontology
- store - Persist memory node + entity nodes (and embeddings) in FalkorDB
- link - Connect the new item to related existing memories
- enrich - Optional enrichers (basic, sentiment, topic, temporal, link expansion, skills/tools)
- ground - Link entities to Wikidata/Wikipedia via
GROUNDED_INedges - evolve - Run evolvers (episodic→semantic, episodic→zettel, decay, opinion/observation synthesis, …)
Entity clustering and bi-temporal version records run as separate post-pipeline operations rather than as in-pipeline stages — see Hybrid Storage and memory.run_clustering().
Stage Details
Raw input can be a plain string or a dict with content, metadata, and an explicit type:
memory.ingest("I learned Python programming in 2020")
memory.ingest({
"content": "Meeting notes from today",
"metadata": {"participants": ["Alice", "Bob"]},
"memory_type": "episodic"
})Note: Use ingest() for full pipeline processing. Use add() for simple storage without extraction/linking/evolution. Use ingest_structured() for schema-mapped data that should bypass the NLP pipeline entirely, see Structured Ingestion below.
1. classify
Decides which memory type the item becomes.
Automatic classification:
- Temporal markers → Episodic
- Factual statements → Semantic
- Process descriptions → Procedural
- Atomic, cross-linked notes → Zettel
- Everything else stays → Pending, the staging type
Manual override:
memory.add(content, memory_type="semantic", force=True)2. coreference
Resolves pronouns and repeated entity mentions to a single referent, so "she" and "Dr. Chen" collapse into one entity downstream instead of two.
3. simplify
Optional sentence simplification. Long, clause-heavy sentences are broken into simpler ones so the extractor sees cleaner subject-verb-object structure. Disabled in profiles that skip LLM work.
4. entity_ruler
Deterministic, pattern-based entity tagging that runs before any model is called. It applies the shipped public-knowledge patterns plus any workspace-specific patterns you have seeded, which makes well-known names and product terms stable across ingests and cheaper than an LLM pass.
5. llm_extract
Entity and relation extraction. This is where the bulk of the knowledge graph comes from.
Entity extraction:
- People, places, organizations
- Dates, times, durations
- Technical concepts and terms
- Actions and events
Triple / relationship extraction:
- Subject-predicate-object triples
- Temporal relationships (BEFORE, AFTER, DURING)
- Causal connections (CAUSES, RESULTS_IN)
- Hierarchical structures (PART_OF, CONTAINS)
- Conversational flow (RESPONDS_TO, FOLLOWS)
Available Extractors
The shipped plugins live in smartmemory/plugins/extractors/. The llm_extract stage routes through whichever extractor is configured, and SpacyExtractor is used as a deterministic fallback when LLM extraction is unavailable.
1. LLMExtractor (llm) — default LLM-based extractor with structured triple output (entities + relationships).
2. LLMSingleExtractor (llm_single) — single-pass LLM variant, lower latency, used by the lite/CC profile and as a CC default. GroqExtractor is a Groq-API subclass selected automatically when GROQ_API_KEY is set.
3. ConversationAwareLLMExtractor — LLMExtractor subclass that adds short-term conversation context for chat-style ingestion.
4. ReasoningExtractor — extracts chain-of-thought / reasoning traces from text containing explicit reasoning (used for the reasoning extended memory type).
5. DecisionExtractor — extracts structured decision claims (used for the decision extended memory type).
6. SpacyExtractor (spacy) — local spaCy-based NER fallback for entity extraction without LLM access.
Provider routing
PipelineModelRouter maps stages (e.g. llm_extract) to model identifiers and providers. Presets include cost_optimized(), quality_optimized(), and balanced(). Precedence: explicit model= > router > config > get_default_model().
When no LLM is available, PipelineConfig.lite() disables LLM extraction and the pipeline falls back to SpacyExtractor (DEGRADE-1d auto-detects this from OPENAI_API_KEY / GROQ_API_KEY env vars).
Large text handling
Texts over 8000 characters are automatically chunked:
- Split by sentence boundaries
- Processed in parallel (ThreadPoolExecutor)
- Results aggregated with entity deduplication
6. ontology_constrain
Constrains the extracted entity and relation types against the active ontology, so extraction cannot invent types the workspace has not declared.
- Entity type hierarchies
- Relationship type definitions
- Knowledge graph schema enforcement
The stage is omitted entirely when SmartMemory is built with enable_ontology=False. Triple extraction and ontology management are separate concerns, so you can extract relationships without running full ontology governance. See Ontology Management.
7. store
Persists the memory node, the extracted entity nodes, and the embeddings.
FalkorDB HNSW index:
- Native vector indexing with the
vecf32type - Configurable HNSW parameters (M, efConstruction, efRuntime)
- Cosine similarity search
- Automatic tenant isolation via
ScopeProvider
8. link
Connects the new item to related existing memories.
Similarity-based linking:
- Semantic similarity using embeddings
- Temporal proximity for episodic memories
- Conceptual overlap detection
- Entity co-occurrence analysis
Explicit relationship creation:
- Causal relationships
- Part-whole relationships
- Temporal sequences
- Conceptual hierarchies
9. enrich
Runs the selected enrichers over the stored item. Choose them per call with enricher_names=[...].
Semantic enhancement:
- Concept expansion and synonyms
- Related topic identification
- Contextual information addition
Temporal processing:
- Time normalization
- Event sequencing
- Duration calculation
- Temporal relationship mapping
Shipped enrichers include basic, sentiment, topic, temporal, link expansion, and skills/tools.
10. ground
Links extracted entities to public knowledge for provenance.
- Look entities up in Wikidata/Wikipedia
- Create Wikipedia nodes, which are shared globally rather than per workspace
- Create
GROUNDED_INedges from entities to those nodes - Track source attribution
Set SMARTMEMORY_GROUNDING_OFFLINE=true to skip this stage in air-gapped environments.
11. evolve
Runs the evolvers, which move memories between types and adjust confidence over time.
Memory promotion:
- Pending → Episodic (threshold: 3+ items)
- Pending → Procedural (threshold: 5+ items)
- Episodic → Semantic (stable facts)
- Episodic → Zettel (significant memories become atomic notes)
- Episodic and semantic decay, plus opinion and observation synthesis
See Evolution Algorithms for the full evolver catalogue.
Configuration
The pipeline is controlled at two levels: a profile set at construction time
that applies to every ingest, and per-call selectors that override the
pipeline for a single ingest().
Construction-time profile and stage toggles:
from smartmemory import SmartMemory
from smartmemory.pipeline.config import PipelineConfig
memory = SmartMemory(
pipeline_profile=PipelineConfig.lite(), # preset: disable network/LLM-bound stages
compaction="standard", # strip extraction intermediates after store
enable_ontology=False, # omit the ontology_constrain stage entirely
observability=False, # disable metrics/event emission
)Per-call component selection — swap or scope individual stages for one ingest:
item_id = memory.ingest(
"Quarterly planning notes...",
extractor_name="spacy", # choose the extraction stage
enricher_names=["sentiment", "topic"], # run exactly these enrichers
extract_decisions=True, # toggle decision extraction
sync=True, # run inline vs. queue for background
)For the full ingest() signature and every selector, see the
SmartMemory API reference.
Performance Characteristics
- Fast Ingestion: Immediate storage with background enrichment
- Scalable Processing: Parallel pipeline stages
- Fault Tolerance: Graceful degradation and retry mechanisms
- Memory Efficiency: Streaming processing for large inputs
Monitoring and Debugging
# Enable detailed logging
memory.set_log_level("DEBUG")
# Access ingestion metrics
stats = memory.get_ingestion_stats()
print(f"Processed: {stats.total_memories}")
print(f"Average processing time: {stats.avg_processing_time}ms")The ingestion flow is designed to balance speed, accuracy, and resource efficiency while providing rich, interconnected memories for intelligent retrieval and reasoning.
Structured Ingestion
Not all data needs the full 11-stage pipeline. Structured data — decisions, tool calls, plan tasks, code entities — already has its schema defined. Running it through LLM extraction is wasteful and lossy.
ingest_structured() bypasses the NLP pipeline entirely. A registered handler validates the data, converts it to a MemoryItem, declares edges and indexes, and controls whether an embedding is generated.
Three Strategies
| Strategy | Embedding | Entity Extraction | Best For |
|---|---|---|---|
| FULL | Immediate | Yes | User-facing knowledge that needs semantic search |
| INDEXED | No (can override) | No | System structures queried by typed fields |
| APPEND | Never | Never | High-volume telemetry with recency-only access |
Usage
# Structured decision (INDEXED — queried by status, domain)
memory.ingest_structured(
{"content": "Use JWT for auth", "domain": "security", "status": "active"},
schema="decision"
)
# Tool call telemetry (APPEND — recency + tool name filter)
memory.ingest_structured(
{"tool_name": "grep", "result": "found 3 matches", "success": True},
schema="tool_call"
)
# Seed pack item (FULL — reference knowledge with embedding)
memory.ingest_structured(
{"content": "Python is a programming language", "memory_type": "semantic"},
schema="seed_item"
)Available Schemas
| Schema | Strategy | Handler | Description |
|---|---|---|---|
decision | INDEXED | DecisionHandler | Structured decisions with provenance edges |
plan | INDEXED | PlanHandler | Plan containers with task tracking |
plan_task | INDEXED | PlanTaskHandler | Individual task DAG nodes |
code_entity | INDEXED (+embed) | CodeEntityHandler | Code entities from repo indexing |
seed_item | FULL | SeedItemHandler | Reference knowledge from seed packs |
tool_call | APPEND | ToolCallHandler | Tool invocation telemetry |
conversation_turn | APPEND | ConversationTurnHandler | Conversation replay |
hook_capture | APPEND | HookCaptureHandler | Hook event telemetry |
Custom Handlers
Create a handler by implementing the StructuredHandler protocol — no base class required:
class MyHandler:
strategy = IngestionStrategy.INDEXED
schema_name = "my_type"
embed = None # None = defer to strategy default
def validate(self, data: dict) -> bool: ...
def to_memory_item(self, data: dict) -> MemoryItem: ...
def get_edges(self, data: dict, item_id: str) -> list[tuple]: ...
def get_indexes(self) -> list[str]: ...Register it via get_structured_registry().register("my_type", MyHandler()).