Archived
0a8312b263
Memory is not stored and retrieved — it is activated and propagated. Implements the spreading activation model with salience decay, typed edges, four memory tiers, and flat cosine vector search over a sled embedded store.
225 lines
9.0 KiB
Rust
225 lines
9.0 KiB
Rust
/// Basic engram demonstration.
|
|
///
|
|
/// This example builds a small memory graph, runs spreading activation,
|
|
/// performs a vector search, and shows salience decay in action.
|
|
///
|
|
/// The nodes represent a tiny knowledge graph about the spreading activation
|
|
/// model itself — somewhat recursive, intentionally.
|
|
use engram_core::{ActivatedNode, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType};
|
|
use std::path::Path;
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
// ── 1. Open database ──────────────────────────────────────────────────────
|
|
let db_path = Path::new("/tmp/engram-test");
|
|
// Clean up any previous run so we start fresh
|
|
if db_path.exists() {
|
|
std::fs::remove_dir_all(db_path)?;
|
|
}
|
|
let db = EngramDb::open(db_path)?;
|
|
println!("Engram opened at {}\n", db_path.display());
|
|
|
|
// ── 2. Insert nodes ───────────────────────────────────────────────────────
|
|
//
|
|
// We use 8-dimensional embeddings. In production these would come from a
|
|
// language model. Here they're hand-crafted to illustrate semantic proximity:
|
|
// the "activation" and "memory" cluster at [high, high, low, ...]
|
|
// while "forgetting" and "decay" cluster at [low, low, high, ...]
|
|
|
|
let node0 = Node::new(
|
|
NodeType::Concept,
|
|
vec![0.9, 0.8, 0.1, 0.2, 0.7, 0.3, 0.1, 0.4],
|
|
b"Spreading activation: memory retrieval as propagation through weighted graph".to_vec(),
|
|
MemoryTier::Semantic,
|
|
0.95,
|
|
);
|
|
|
|
let node1 = Node::new(
|
|
NodeType::Concept,
|
|
vec![0.8, 0.9, 0.2, 0.1, 0.6, 0.4, 0.2, 0.3],
|
|
b"Long-term potentiation: synaptic strengthening through co-activation".to_vec(),
|
|
MemoryTier::Semantic,
|
|
0.90,
|
|
);
|
|
|
|
let node2 = Node::new(
|
|
NodeType::Memory,
|
|
vec![0.7, 0.6, 0.3, 0.4, 0.8, 0.2, 0.1, 0.5],
|
|
b"Hebbian learning: neurons that fire together wire together".to_vec(),
|
|
MemoryTier::Episodic,
|
|
0.85,
|
|
);
|
|
|
|
let node3 = Node::new(
|
|
NodeType::Concept,
|
|
vec![0.6, 0.7, 0.4, 0.3, 0.9, 0.1, 0.2, 0.6],
|
|
b"Associative memory: retrieval by pattern completion, not address lookup".to_vec(),
|
|
MemoryTier::Semantic,
|
|
0.88,
|
|
);
|
|
|
|
let node4 = Node::new(
|
|
NodeType::Process,
|
|
vec![0.2, 0.3, 0.8, 0.9, 0.1, 0.7, 0.6, 0.2],
|
|
b"Salience decay: forgetting as adaptive pruning, not failure".to_vec(),
|
|
MemoryTier::Procedural,
|
|
0.75,
|
|
);
|
|
|
|
let node5 = Node::new(
|
|
NodeType::Event,
|
|
vec![0.3, 0.2, 0.7, 0.8, 0.2, 0.6, 0.7, 0.1],
|
|
b"Memory consolidation during sleep: hippocampal replay to neocortex".to_vec(),
|
|
MemoryTier::Episodic,
|
|
0.70,
|
|
);
|
|
|
|
let id0 = db.put_node(node0)?;
|
|
let id1 = db.put_node(node1)?;
|
|
let id2 = db.put_node(node2)?;
|
|
let id3 = db.put_node(node3)?;
|
|
let id4 = db.put_node(node4)?;
|
|
let id5 = db.put_node(node5)?;
|
|
|
|
println!("Inserted {} nodes", db.node_count()?);
|
|
println!(" [0] Spreading activation concept (seed)");
|
|
println!(" [1] Long-term potentiation");
|
|
println!(" [2] Hebbian learning");
|
|
println!(" [3] Associative memory");
|
|
println!(" [4] Salience decay (procedural)");
|
|
println!(" [5] Memory consolidation (episodic)");
|
|
println!();
|
|
|
|
// ── 3. Create edges ───────────────────────────────────────────────────────
|
|
//
|
|
// Edge weights model associative strength. Strong weights (0.9) mean these
|
|
// concepts reliably co-activate. Weaker weights mean looser association.
|
|
|
|
// Spreading activation Causes long-term potentiation (strong causal link)
|
|
db.put_edge(Edge::new(id0, id1, RelationType::Causes, 0.9))?;
|
|
// LTP is Referenced by Hebbian learning
|
|
db.put_edge(Edge::new(id1, id2, RelationType::References, 0.85))?;
|
|
// Spreading activation Activates associative memory
|
|
db.put_edge(Edge::new(id0, id3, RelationType::Activates, 0.88))?;
|
|
// Hebbian learning Exemplifies associative memory
|
|
db.put_edge(Edge::new(id2, id3, RelationType::Exemplifies, 0.80))?;
|
|
// Salience decay Supersedes naive forgetting
|
|
db.put_edge(Edge::new(id4, id5, RelationType::TemporallyPrecedes, 0.65))?;
|
|
// LTP TemporallyPrecedes memory consolidation
|
|
db.put_edge(Edge::new(id1, id5, RelationType::TemporallyPrecedes, 0.72))?;
|
|
|
|
println!("Inserted {} edges", db.edge_count()?);
|
|
println!(" node0 --[Causes]--> node1");
|
|
println!(" node1 --[References]--> node2");
|
|
println!(" node0 --[Activates]--> node3");
|
|
println!(" node2 --[Exemplifies]--> node3");
|
|
println!(" node4 --[TemporallyPrecedes]--> node5");
|
|
println!(" node1 --[TemporallyPrecedes]--> node5");
|
|
println!();
|
|
|
|
// ── 4. Spreading activation ───────────────────────────────────────────────
|
|
//
|
|
// Seed: node0 (spreading activation concept)
|
|
// Query embedding: similar to node3 (associative memory) — high in dims 4,5
|
|
// This should surface node3 strongly and pull in node2 via the Hebbian path.
|
|
|
|
let query_embedding = vec![0.65, 0.72, 0.35, 0.28, 0.92, 0.12, 0.18, 0.58];
|
|
|
|
println!("=== Spreading Activation ===");
|
|
println!("Seed: node0 (spreading activation concept)");
|
|
println!("Query: similar to node3 (associative memory)");
|
|
println!("Max depth: 3 hops, returning top 10");
|
|
println!();
|
|
|
|
let activated: Vec<ActivatedNode> = db.activate(&[id0], &query_embedding, 3, 10)?;
|
|
|
|
if activated.is_empty() {
|
|
println!(" (no nodes activated — check salience values)");
|
|
} else {
|
|
for a in &activated {
|
|
let content = String::from_utf8_lossy(&a.node.content);
|
|
// Truncate content for display
|
|
let display = if content.len() > 60 {
|
|
format!("{}...", &content[..60])
|
|
} else {
|
|
content.to_string()
|
|
};
|
|
println!(
|
|
" strength={:.4} hops={} salience={:.4} tier={:?}",
|
|
a.activation_strength, a.hops, a.node.salience, a.node.tier
|
|
);
|
|
println!(" \"{}\"", display);
|
|
}
|
|
}
|
|
println!();
|
|
|
|
// ── 5. Vector similarity search ───────────────────────────────────────────
|
|
//
|
|
// Pure cosine scan: no graph structure, just embedding proximity.
|
|
// Should return nodes with embeddings most similar to the query.
|
|
|
|
println!("=== Vector Similarity Search (top 3) ===");
|
|
println!("Query: associative memory embedding");
|
|
println!();
|
|
|
|
let scored = db.search_embedding(&query_embedding, 3)?;
|
|
for s in &scored {
|
|
let content = String::from_utf8_lossy(&s.node.content);
|
|
let display = if content.len() > 60 {
|
|
format!("{}...", &content[..60])
|
|
} else {
|
|
content.to_string()
|
|
};
|
|
println!(
|
|
" cosine={:.4} tier={:?} type={:?}",
|
|
s.score, s.node.tier, s.node.node_type
|
|
);
|
|
println!(" \"{}\"", display);
|
|
}
|
|
println!();
|
|
|
|
// ── 6. Node and edge counts ───────────────────────────────────────────────
|
|
|
|
println!("=== Database Statistics ===");
|
|
println!(" Nodes: {}", db.node_count()?);
|
|
println!(" Edges: {}", db.edge_count()?);
|
|
println!();
|
|
|
|
// ── 7. Salience decay ─────────────────────────────────────────────────────
|
|
//
|
|
// Apply 5% decay to all node saliences. This simulates the passage of time.
|
|
// Nodes that haven't been activated recently become less salient,
|
|
// modeling the adaptive nature of forgetting.
|
|
|
|
println!("=== Salience Decay (factor=0.95) ===");
|
|
let updated = db.decay(0.95)?;
|
|
println!(" Updated {} nodes", updated);
|
|
println!();
|
|
|
|
// Show salience before/after for a sample node
|
|
if let Some(n) = db.get_node(id0)? {
|
|
println!(
|
|
" node0 salience after decay: {:.6}",
|
|
n.salience
|
|
);
|
|
}
|
|
|
|
// Show traversal from node0
|
|
println!();
|
|
println!("=== Graph Traversal from node0 (depth=2) ===");
|
|
let reachable = db.traverse(id0, None, 2)?;
|
|
println!(" Reachable nodes (any relation, max 2 hops): {}", reachable.len());
|
|
for n in &reachable {
|
|
let content = String::from_utf8_lossy(&n.content);
|
|
let display = if content.len() > 55 {
|
|
format!("{}...", &content[..55])
|
|
} else {
|
|
content.to_string()
|
|
};
|
|
println!(" [{:?}] \"{}\"", n.tier, display);
|
|
}
|
|
|
|
println!();
|
|
println!("Done. Engram v0.1.");
|
|
Ok(())
|
|
}
|