Archived
f33d789471
- vector.rs: replace flat O(n) scan with instant-distance HNSW for stores >= 100 nodes; flat scan retained as fallback for small graphs; dirty-flag persistence in sled triggers index rebuild only when nodes are added - consolidation.rs: Episodic → Semantic promotion based on activation_count and salience_floor thresholds; global decay pass after each cycle; ConsolidationConfig + ConsolidationReport types; 8 tests - migration.rs: reads Neuron SQLite (memory_nodes, knowledge_entries, graph_edges) and writes to Engram sled; placeholder unit-vector embeddings with TODO for ONNX; 5 tests including full in-memory DB roundtrip - crates/engram-migrate: CLI binary (engram-migrate --sqlite / --output) - crates/engram-jni: JNI cdylib exposing open/close/put_node/get_node/ activate/search_embedding/touch/decay/node_count/edge_count via Java_ai_neuron_engram_EngramDb_* entry points; 6 tests - bindings/kotlin: EngramDb.kt (AutoCloseable JNI wrapper), EngramNode, EngramEdge, ActivatedNode, EngramTypes; build.gradle.kts; settings.gradle.kts - bindings/typescript: engram-wasm crate (wasm-bindgen, serde-wasm-bindgen); WasmEngramDb with in-memory backend (sled not available in WASM); TypeScript wrapper (index.ts, types.ts, package.json, tsconfig.json) - bindings/go: engram.go (CGo wrapper), engram.h (C header), engram_test.go (4 tests covering open/close/put_node/get_node/node_count/decay); go.mod - engram-core: wasm feature gate for in-memory backend; mem_storage.rs; activation.activate_mem for WASM path; Node::with_id helper; salience.rs doctest fixed (text block) - examples/basic.rs: consolidation section added - examples/migrate.rs: migration API demonstration Build: cargo build --workspace -- zero warnings, zero errors Tests: 38 pass (25 engram-core + 7 engram-ffi + 6 engram-jni)
265 lines
10 KiB
Rust
265 lines
10 KiB
Rust
/// Basic engram demonstration.
|
|
///
|
|
/// This example builds a small memory graph, runs spreading activation,
|
|
/// performs a vector search, shows salience decay, and demonstrates
|
|
/// the consolidation engine promoting Episodic nodes to Semantic.
|
|
///
|
|
/// The nodes represent a tiny knowledge graph about the spreading activation
|
|
/// model itself — somewhat recursive, intentionally.
|
|
use engram_core::{
|
|
ActivatedNode, ConsolidationConfig, 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!();
|
|
|
|
// ── 8. Consolidation ──────────────────────────────────────────────────────
|
|
//
|
|
// Touch node2 (Hebbian learning — Episodic) several times to simulate it
|
|
// being frequently recalled, then run consolidation to promote it.
|
|
|
|
println!("=== Memory Consolidation ===");
|
|
|
|
// Simulate repeated activation: touch node2 six times so it crosses the
|
|
// default threshold of 5 activations.
|
|
for _ in 0..6 {
|
|
db.touch(id2)?;
|
|
}
|
|
|
|
// Confirm the node's activation count has increased.
|
|
if let Some(n) = db.get_node(id2)? {
|
|
println!(" node2 (Episodic) activation_count before consolidation: {}", n.activation_count);
|
|
println!(" node2 tier before consolidation: {:?}", n.tier);
|
|
}
|
|
|
|
let config = ConsolidationConfig {
|
|
episodic_to_semantic_threshold: 5,
|
|
salience_floor: 0.0, // no salience floor — accept any salient node
|
|
max_promotions_per_run: 10,
|
|
decay_factor: 0.98,
|
|
};
|
|
|
|
let report = db.consolidate(&config)?;
|
|
println!(" Promoted {} Episodic → Semantic", report.promoted);
|
|
println!(" Decayed {} nodes", report.decayed);
|
|
|
|
// Confirm node2 has been promoted.
|
|
if let Some(n) = db.get_node(id2)? {
|
|
println!(" node2 tier after consolidation: {:?}", n.tier);
|
|
}
|
|
|
|
println!();
|
|
println!("Done. Engram v0.1.1.");
|
|
Ok(())
|
|
}
|