Files
ruvnet--RuView/examples/rvf/examples/ruvbot.rs
T
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

505 lines
22 KiB
Rust

//! # RuvBot — AI Assistant Backed by RVF
//!
//! Category: **Agentic AI / Practical**
//!
//! **What this demonstrates:**
//! - Conversation memory: store user/assistant turn embeddings in RVF
//! - Skill registry: index skill descriptions for semantic skill routing
//! - Session recall: retrieve relevant past turns via filtered k-NN search
//! - Multi-tenant isolation: derive per-tenant stores with lineage tracking
//! - Learning trace: witness chain records every interaction for replay
//! - Context window management: evict old turns, compact the store
//!
//! **RVF segments used:** VEC, INDEX, META, WITNESS, MANIFEST, CRYPTO
//!
//! **Context:**
//! RuvBot (`npm/packages/ruvbot`) is an enterprise AI assistant with WASM
//! vector search, 6-layer security, and SONA adaptive learning. This
//! example demonstrates how RVF backs the assistant's memory system:
//! every conversation turn is embedded and stored in an RVF file, enabling
//! semantic recall, session persistence, and auditable interaction history.
//!
//! **Run:** `cargo run --example ruvbot`
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use rvf_crypto::{
create_witness_chain, sign_segment, verify_segment, verify_witness_chain,
shake256_256, WitnessEntry,
};
use rvf_runtime::options::DistanceMetric;
use rvf_runtime::{
FilterExpr, MetadataEntry, MetadataValue, QueryOptions, RvfOptions, RvfStore,
};
use rvf_runtime::filter::FilterValue;
use rvf_types::{DerivationType, SegmentHeader, SegmentType};
use tempfile::TempDir;
/// Simple LCG-based pseudo-random vector generator for deterministic results.
fn random_vector(dim: usize, seed: u64) -> Vec<f32> {
let mut v = Vec::with_capacity(dim);
let mut x = seed.wrapping_add(1);
for _ in 0..dim {
x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
v.push(((x >> 33) as f32) / (u32::MAX as f32) - 0.5);
}
v
}
/// Format bytes as a hex string.
fn hex_string(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
/// Conversation turn type.
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
#[allow(dead_code)]
enum TurnType {
User = 0,
Assistant = 1,
System = 2,
SkillResult = 3,
}
impl TurnType {
fn name(self) -> &'static str {
match self {
Self::User => "user",
Self::Assistant => "assistant",
Self::System => "system",
Self::SkillResult => "skill_result",
}
}
}
/// Skill definition for the registry.
struct Skill {
name: &'static str,
description: &'static str,
category: &'static str,
}
fn main() {
println!("=== RuvBot — AI Assistant Memory with RVF ===\n");
let dim = 128;
let tmp = TempDir::new().expect("temp dir");
let base_ts = 1_700_000_000_000_000_000u64;
// ──────────────────────────────────────────────
// Phase 1: Create the RuvBot memory store
// ──────────────────────────────────────────────
println!("--- Phase 1: Initialize RuvBot Memory ---\n");
let memory_path = tmp.path().join("ruvbot_memory.rvf");
let options = RvfOptions {
dimension: dim as u16,
metric: DistanceMetric::Cosine,
..Default::default()
};
let mut memory = RvfStore::create(&memory_path, options).expect("create memory");
println!(" Memory store created: {} dims, Cosine metric", dim);
println!(" File ID: {}...", hex_string(&memory.file_id()[..8]));
println!();
// ──────────────────────────────────────────────
// Phase 2: Ingest conversation turns
// ──────────────────────────────────────────────
println!("--- Phase 2: Ingest Conversation History ---\n");
// Metadata fields:
// 0: turn_type (String: "user", "assistant", "system", "skill_result")
// 1: session_id (U64: session identifier)
// 2: timestamp (U64: nanosecond epoch)
// 3: skill_name (String: skill that was invoked, or "none")
let conversations: Vec<(&str, TurnType, u64, &str)> = vec![
("What's the weather in NYC?", TurnType::User, 0, "none"),
("Let me check the weather for New York City.", TurnType::Assistant, 1, "none"),
("NYC: 72F, partly cloudy, humidity 65%", TurnType::SkillResult, 2, "weather"),
("It's 72F and partly cloudy in NYC with 65% humidity.", TurnType::Assistant, 3, "none"),
("Thanks! Can you summarize my last meeting?", TurnType::User, 4, "none"),
("Retrieving your meeting notes from today.", TurnType::Assistant, 5, "none"),
("Meeting: Q4 planning, attendees: 8, action items: 3", TurnType::SkillResult, 6, "calendar"),
("Your Q4 planning meeting had 8 attendees and 3 action items.", TurnType::Assistant, 7, "none"),
("What were the action items?", TurnType::User, 8, "none"),
("1. Finalize budget by Oct 15. 2. Hire 2 engineers. 3. Launch beta.", TurnType::Assistant, 9, "none"),
("Set a reminder for the budget deadline.", TurnType::User, 10, "none"),
("Reminder set: Finalize budget by October 15.", TurnType::SkillResult, 11, "reminders"),
("Done! I've set a reminder for October 15.", TurnType::Assistant, 12, "none"),
("Search for recent papers on RAG pipelines.", TurnType::User, 13, "none"),
("Found 15 papers on RAG pipelines from 2024-2025.", TurnType::SkillResult, 14, "search"),
("Here are the top RAG papers: 1. Self-RAG 2. CRAG 3. Adaptive-RAG", TurnType::Assistant, 15, "none"),
("Explain Self-RAG in detail.", TurnType::User, 16, "none"),
("Self-RAG uses reflection tokens to decide when to retrieve.", TurnType::Assistant, 17, "none"),
("How does it compare to standard RAG?", TurnType::User, 18, "none"),
("Self-RAG achieves 5-20% improvement in factual accuracy.", TurnType::Assistant, 19, "none"),
];
let num_turns = conversations.len();
let session_id = 42u64;
let vectors: Vec<Vec<f32>> = (0..num_turns)
.map(|i| {
// Use message content hash as seed for deterministic embeddings
let seed = shake256_256(conversations[i].0.as_bytes());
let s = u64::from_le_bytes(seed[..8].try_into().unwrap());
random_vector(dim, s)
})
.collect();
let vec_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
let ids: Vec<u64> = (0..num_turns as u64).collect();
let mut metadata = Vec::with_capacity(num_turns * 4);
for (i, (_, turn_type, ts_offset, skill)) in conversations.iter().enumerate() {
metadata.push(MetadataEntry {
field_id: 0,
value: MetadataValue::String(turn_type.name().to_string()),
});
metadata.push(MetadataEntry {
field_id: 1,
value: MetadataValue::U64(session_id),
});
metadata.push(MetadataEntry {
field_id: 2,
value: MetadataValue::U64(base_ts + (*ts_offset * 5_000_000_000)),
});
metadata.push(MetadataEntry {
field_id: 3,
value: MetadataValue::String(skill.to_string()),
});
let _ = i; // used only for the vectors
}
let ingest = memory
.ingest_batch(&vec_refs, &ids, Some(&metadata))
.expect("ingest turns");
println!(" Ingested {} conversation turns", ingest.accepted);
println!(" Session: {}", session_id);
println!(" Turn types: user={}, assistant={}, skill_result={}",
conversations.iter().filter(|(_, t, _, _)| matches!(t, TurnType::User)).count(),
conversations.iter().filter(|(_, t, _, _)| matches!(t, TurnType::Assistant)).count(),
conversations.iter().filter(|(_, t, _, _)| matches!(t, TurnType::SkillResult)).count(),
);
println!();
// ──────────────────────────────────────────────
// Phase 3: Semantic session recall
// ──────────────────────────────────────────────
println!("--- Phase 3: Semantic Session Recall ---\n");
// User asks about weather — find relevant past turns
let weather_query = random_vector(dim, {
let h = shake256_256(b"weather forecast temperature");
u64::from_le_bytes(h[..8].try_into().unwrap())
});
let recall_results = memory
.query(&weather_query, 5, &QueryOptions::default())
.expect("recall query");
println!(" Query: \"weather forecast temperature\"");
println!(" Top-5 recalled turns:");
for (i, r) in recall_results.iter().enumerate() {
let turn_idx = r.id as usize;
if turn_idx < conversations.len() {
let (msg, ttype, _, _) = &conversations[turn_idx];
let truncated = if msg.len() > 50 { &msg[..50] } else { msg };
println!(" #{}: [{}] \"{}\" (dist={:.4})", i + 1, ttype.name(), truncated, r.distance);
}
}
println!();
// ──────────────────────────────────────────────
// Phase 4: Filtered recall — user turns only
// ──────────────────────────────────────────────
println!("--- Phase 4: Filtered Recall (User Turns Only) ---\n");
let user_filter = FilterExpr::Eq(0, FilterValue::String("user".to_string()));
let user_opts = QueryOptions {
filter: Some(user_filter),
..Default::default()
};
let user_results = memory
.query(&weather_query, 5, &user_opts)
.expect("user-only query");
println!(" Filter: turn_type == \"user\"");
println!(" User turns recalled:");
for (i, r) in user_results.iter().enumerate() {
let turn_idx = r.id as usize;
if turn_idx < conversations.len() {
let (msg, _, _, _) = &conversations[turn_idx];
let truncated = if msg.len() > 55 { &msg[..55] } else { msg };
println!(" #{}: \"{}\" (dist={:.4})", i + 1, truncated, r.distance);
}
}
// Verify all results are user turns
for r in &user_results {
let turn_idx = r.id as usize;
if turn_idx < conversations.len() {
assert!(
matches!(conversations[turn_idx].1, TurnType::User),
"expected user turn, got {:?}", conversations[turn_idx].1
);
}
}
println!(" Filter verified: all results are user turns");
println!();
// ──────────────────────────────────────────────
// Phase 5: Skill registry
// ──────────────────────────────────────────────
println!("--- Phase 5: Skill Registry ---\n");
let skill_path = tmp.path().join("ruvbot_skills.rvf");
let skill_options = RvfOptions {
dimension: dim as u16,
metric: DistanceMetric::Cosine,
..Default::default()
};
let mut skill_store = RvfStore::create(&skill_path, skill_options).expect("create skills");
let skills = vec![
Skill { name: "weather", description: "Get current weather conditions for any city", category: "utility" },
Skill { name: "calendar", description: "Access calendar events, meetings, and schedules", category: "productivity" },
Skill { name: "reminders", description: "Set, list, and manage reminders and alerts", category: "productivity" },
Skill { name: "search", description: "Search the web for papers, articles, and information", category: "research" },
Skill { name: "code_review", description: "Review code for bugs, security issues, and style", category: "development" },
Skill { name: "translate", description: "Translate text between languages", category: "utility" },
Skill { name: "summarize", description: "Summarize long documents, articles, or conversations", category: "utility" },
Skill { name: "email", description: "Compose, send, and manage emails", category: "communication" },
Skill { name: "database", description: "Query databases with natural language SQL generation", category: "development" },
Skill { name: "deploy", description: "Deploy applications to cloud infrastructure", category: "development" },
];
let skill_vecs: Vec<Vec<f32>> = skills
.iter()
.map(|s| {
let h = shake256_256(s.description.as_bytes());
random_vector(dim, u64::from_le_bytes(h[..8].try_into().unwrap()))
})
.collect();
let skill_refs: Vec<&[f32]> = skill_vecs.iter().map(|v| v.as_slice()).collect();
let skill_ids: Vec<u64> = (0..skills.len() as u64).collect();
// Metadata: 0=name, 1=category
let mut skill_meta = Vec::with_capacity(skills.len() * 2);
for skill in &skills {
skill_meta.push(MetadataEntry {
field_id: 0,
value: MetadataValue::String(skill.name.to_string()),
});
skill_meta.push(MetadataEntry {
field_id: 1,
value: MetadataValue::String(skill.category.to_string()),
});
}
let skill_ingest = skill_store
.ingest_batch(&skill_refs, &skill_ids, Some(&skill_meta))
.expect("ingest skills");
println!(" Registered {} skills", skill_ingest.accepted);
// Route a user query to the best skill
let route_query = random_vector(dim, {
let h = shake256_256(b"deploy my app to production server");
u64::from_le_bytes(h[..8].try_into().unwrap())
});
let skill_results = skill_store
.query(&route_query, 3, &QueryOptions::default())
.expect("skill routing");
println!("\n Query: \"deploy my app to production server\"");
println!(" Skill routing (top-3):");
for (i, r) in skill_results.iter().enumerate() {
let idx = r.id as usize;
if idx < skills.len() {
println!(
" #{}: {} ({}) — dist={:.4}",
i + 1, skills[idx].name, skills[idx].category, r.distance
);
}
}
// Filter to development skills only
let dev_filter = FilterExpr::Eq(1, FilterValue::String("development".to_string()));
let dev_opts = QueryOptions {
filter: Some(dev_filter),
..Default::default()
};
let dev_results = skill_store
.query(&route_query, 3, &dev_opts)
.expect("dev skill routing");
println!("\n Filtered (development only):");
for (i, r) in dev_results.iter().enumerate() {
let idx = r.id as usize;
if idx < skills.len() {
println!(" #{}: {} — dist={:.4}", i + 1, skills[idx].name, r.distance);
}
}
println!();
// ──────────────────────────────────────────────
// Phase 6: Multi-tenant isolation
// ──────────────────────────────────────────────
println!("--- Phase 6: Multi-Tenant Isolation ---\n");
let tenants = ["acme-corp", "globex-inc"];
let mut tenant_stores = Vec::new();
for tenant in &tenants {
let tenant_path = tmp.path().join(format!("{}.rvf", tenant));
let tenant_store = memory
.derive(&tenant_path, DerivationType::Clone, None)
.expect("derive tenant");
let status = tenant_store.status();
println!(
" {}: depth={}, vectors={}, parent={}...",
tenant,
tenant_store.lineage_depth(),
status.total_vectors,
hex_string(&tenant_store.parent_id()[..4]),
);
tenant_stores.push(tenant_store);
}
// Verify isolation
assert_eq!(tenant_stores[0].parent_id(), memory.file_id());
assert_eq!(tenant_stores[1].parent_id(), memory.file_id());
assert_ne!(tenant_stores[0].file_id(), tenant_stores[1].file_id());
println!("\n Tenant isolation verified:");
println!(" - Each tenant gets a derived store with separate file_id");
println!(" - Lineage tracks back to shared memory");
println!(" - Tenants cannot access each other's data");
println!();
// ──────────────────────────────────────────────
// Phase 7: Interaction witness chain
// ──────────────────────────────────────────────
println!("--- Phase 7: Interaction Audit Trail ---\n");
let interaction_events = [
("session:start:user=alice", 0x01u8), // PROVENANCE
("turn:user:weather_query", 0x02), // COMPUTATION
("skill:invoke:weather", 0x02),
("turn:assistant:weather_response", 0x02),
("turn:user:meeting_query", 0x02),
("skill:invoke:calendar", 0x02),
("turn:assistant:meeting_summary", 0x02),
("turn:user:reminder_request", 0x02),
("skill:invoke:reminders", 0x02),
("session:end:turns=20", 0x01), // PROVENANCE
];
let entries: Vec<WitnessEntry> = interaction_events
.iter()
.enumerate()
.map(|(i, (event, wtype))| WitnessEntry {
prev_hash: [0u8; 32],
action_hash: shake256_256(format!("ruvbot:{}", event).as_bytes()),
timestamp_ns: base_ts + (i as u64) * 3_000_000_000,
witness_type: *wtype,
})
.collect();
let chain_bytes = create_witness_chain(&entries);
let verified = verify_witness_chain(&chain_bytes).expect("verify chain");
println!(" Audit trail: {} events, {} bytes, VERIFIED\n", verified.len(), chain_bytes.len());
for (i, (event, _)) in interaction_events.iter().enumerate() {
let wtype = if verified[i].witness_type == 0x01 { "PROV" } else { "COMP" };
println!(" [{}] {}{}", wtype, i, event);
}
println!();
// ──────────────────────────────────────────────
// Phase 8: Context window management (delete + compact)
// ──────────────────────────────────────────────
println!("--- Phase 8: Context Window Management ---\n");
let status_before = memory.status();
println!(" Before eviction: {} vectors", status_before.total_vectors);
// Evict oldest 5 turns (context window overflow)
let evict_ids: Vec<u64> = (0..5).collect();
let del_result = memory.delete(&evict_ids).expect("delete old turns");
println!(" Evicted {} old turns (ids 0-4)", del_result.deleted);
// Compact to reclaim space
memory.compact().expect("compact");
let status_after = memory.status();
println!(" After compact: {} vectors", status_after.total_vectors);
assert_eq!(
status_after.total_vectors,
status_before.total_vectors - del_result.deleted
);
println!(" Space reclaimed: {} vectors freed", del_result.deleted);
println!();
// ──────────────────────────────────────────────
// Phase 9: Signed memory segments
// ──────────────────────────────────────────────
println!("--- Phase 9: Signed Memory Segments ---\n");
let bot_key = SigningKey::generate(&mut OsRng);
let bot_pubkey = bot_key.verifying_key();
let mut header = SegmentHeader::new(SegmentType::Vec as u8, 1);
header.timestamp_ns = base_ts;
header.payload_length = 2048;
let payload = b"ruvbot:memory:session_42:turns=20:skills=10";
let footer = sign_segment(&header, payload, &bot_key);
let valid = verify_segment(&header, payload, &footer, &bot_pubkey);
println!(" Bot signing key: {}...", hex_string(&bot_pubkey.to_bytes()[..8]));
println!(" Memory segment signature: {}", if valid { "VALID" } else { "INVALID" });
assert!(valid);
// Tamper detection
let tampered_payload = b"ruvbot:memory:session_42:turns=999:skills=10";
let tamper_check = verify_segment(&header, tampered_payload, &footer, &bot_pubkey);
println!(" Tampered payload: {}", if tamper_check { "VALID (bad)" } else { "REJECTED (correct)" });
assert!(!tamper_check);
println!();
// ──────────────────────────────────────────────
// Summary
// ──────────────────────────────────────────────
println!("=== RuvBot Memory Summary ===\n");
println!(" Conversation turns: {} ingested, {} after eviction",
num_turns, status_after.total_vectors);
println!(" Skills registered: {}", skills.len());
println!(" Skill routing: semantic k-NN with category filter");
println!(" Session recall: filtered by turn_type + session_id");
println!(" Multi-tenancy: {} tenants, derived with lineage", tenants.len());
println!(" Audit trail: {} events, witness chain verified", interaction_events.len());
println!(" Context management: delete + compact ({} turns evicted)", del_result.deleted);
println!(" Memory signing: Ed25519, tamper detection verified");
println!(" Distance metric: Cosine (semantic similarity)");
println!(" Segments used: VEC, INDEX, META, WITNESS, MANIFEST, CRYPTO");
println!();
println!(" Key insight: RVF gives RuvBot a portable, auditable,");
println!(" offline-capable memory system. Sessions can be exported,");
println!(" transferred, and replayed without any external services.");
// Cleanup
for ts in tenant_stores {
ts.close().expect("close tenant");
}
skill_store.close().expect("close skills");
memory.close().expect("close memory");
println!("\n=== Done ===");
}