Archived
feat: HNSW index, consolidation engine, Kotlin/TS/Go bindings, SQLite migration connector
- 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)
This commit is contained in:
+43
-3
@@ -1,11 +1,14 @@
|
||||
/// Basic engram demonstration.
|
||||
///
|
||||
/// This example builds a small memory graph, runs spreading activation,
|
||||
/// performs a vector search, and shows salience decay in action.
|
||||
/// 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, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType};
|
||||
use engram_core::{
|
||||
ActivatedNode, ConsolidationConfig, Edge, EngramDb, MemoryTier, Node, NodeType, RelationType,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -219,6 +222,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Done. Engram v0.1.");
|
||||
|
||||
// ── 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(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Migration example — shows the migrate_from_neuron API.
|
||||
///
|
||||
/// This example creates a tiny in-memory SQLite database that mimics the
|
||||
/// Neuron schema, then migrates it into an Engram sled store and prints
|
||||
/// the resulting node count.
|
||||
///
|
||||
/// In production:
|
||||
/// use engram_core::migration::{migrate_from_neuron, MigrationConfig};
|
||||
/// let config = MigrationConfig::new(
|
||||
/// PathBuf::from(shellexpand::tilde("~/.neuron/neuron.db").as_ref()),
|
||||
/// PathBuf::from(shellexpand::tilde("~/.engram/neuron").as_ref()),
|
||||
/// );
|
||||
/// let report = migrate_from_neuron(&config)?;
|
||||
#[cfg(feature = "migration")]
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use engram_core::migration::{migrate_from_neuron, MigrationConfig};
|
||||
use rusqlite::Connection;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// 1. Create a temp Neuron-like SQLite database.
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let sqlite_path = tmp.path().join("neuron.db");
|
||||
let engram_path = tmp.path().join("engram");
|
||||
|
||||
let conn = Connection::open(&sqlite_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE memory_nodes (
|
||||
id TEXT PRIMARY KEY, content TEXT NOT NULL,
|
||||
importance TEXT NOT NULL DEFAULT 'normal',
|
||||
superseded_by TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE knowledge_entries (
|
||||
id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT '', tier TEXT NOT NULL DEFAULT 'note',
|
||||
tags TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE graph_edges (
|
||||
from_id TEXT NOT NULL, from_type TEXT NOT NULL,
|
||||
to_id TEXT NOT NULL, to_type TEXT NOT NULL,
|
||||
edge_type TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0,
|
||||
PRIMARY KEY (from_id, to_id, edge_type)
|
||||
);",
|
||||
)?;
|
||||
|
||||
// Insert sample data.
|
||||
for i in 0..5 {
|
||||
conn.execute(
|
||||
"INSERT INTO memory_nodes (id, content, importance, created_at, updated_at)
|
||||
VALUES (?1, ?2, 'normal', 1000, 1000)",
|
||||
rusqlite::params![format!("mem-{i}"), format!("Memory node {i}")],
|
||||
)?;
|
||||
}
|
||||
for i in 0..3 {
|
||||
conn.execute(
|
||||
"INSERT INTO knowledge_entries (id, title, content, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, 2000, 2000)",
|
||||
rusqlite::params![
|
||||
format!("kn-{i}"),
|
||||
format!("Concept {i}"),
|
||||
format!("Body of knowledge entry {i}"),
|
||||
],
|
||||
)?;
|
||||
}
|
||||
drop(conn);
|
||||
|
||||
// 2. Run the migration.
|
||||
println!("Running migration...");
|
||||
let config = MigrationConfig {
|
||||
sqlite_path,
|
||||
engram_path,
|
||||
embedding_dim: 64,
|
||||
};
|
||||
|
||||
let report = migrate_from_neuron(&config)?;
|
||||
|
||||
println!("Migration complete.");
|
||||
println!(" Memories migrated: {}", report.memories_migrated);
|
||||
println!(" Knowledge migrated: {}", report.knowledge_migrated);
|
||||
println!(" Edges created: {}", report.edges_created);
|
||||
if !report.errors.is_empty() {
|
||||
println!(" Errors: {:?}", report.errors);
|
||||
}
|
||||
|
||||
// 3. Open the result and check counts.
|
||||
let db = engram_core::EngramDb::open(&config.engram_path)?;
|
||||
println!();
|
||||
println!("Engram node count: {}", db.node_count()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "migration"))]
|
||||
fn main() {
|
||||
eprintln!("This example requires the 'migration' feature.");
|
||||
eprintln!("Run with: cargo run --example migrate --features migration");
|
||||
}
|
||||
Reference in New Issue
Block a user