Files
wifi-densepose/vendor/ruvector/examples/rvf/examples/tool_cache.rs

334 lines
11 KiB
Rust

//! Tool Call Result Cache — Agentic AI
//!
//! Demonstrates using an RVF store as a semantic cache for tool invocation results:
//! 1. Create a store for caching tool call input embeddings
//! 2. Insert embeddings with metadata: tool_name, call_hash, ttl_seconds, result_size
//! 3. Query to find cached results for similar tool calls
//! 4. Demonstrate metadata filtering: find all "search_web" results with ttl > 3600
//! 5. Show cache hit vs miss patterns
//! 6. Delete expired entries with delete_by_filter
//! 7. Compact to reclaim space
//! 8. Print cache stats
//!
//! RVF segments used: VEC_SEG, MANIFEST_SEG, JOURNAL_SEG (via RvfStore)
//!
//! Run with:
//! cargo run --example tool_cache
use rvf_runtime::{
FilterExpr, MetadataEntry, MetadataValue, QueryOptions, RvfOptions, RvfStore, SearchResult,
};
use rvf_runtime::filter::FilterValue;
use rvf_runtime::options::DistanceMetric;
use tempfile::TempDir;
/// Simple pseudo-random number generator (LCG) 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
}
fn main() {
println!("=== RVF Tool Call Result Cache Example ===\n");
let dim = 64;
let tmp_dir = TempDir::new().expect("failed to create temp dir");
let store_path = tmp_dir.path().join("tool_cache.rvf");
// -- Step 1: Create tool cache store --
println!("--- 1. Creating Tool Cache Store ---");
let options = RvfOptions {
dimension: dim as u16,
metric: DistanceMetric::L2,
..Default::default()
};
let mut store = RvfStore::create(&store_path, options).expect("failed to create store");
println!(" Cache store created ({} dims, L2 metric)", dim);
// -- Step 2: Insert cached tool call results --
// Metadata fields:
// field_id 0: tool_name (String)
// field_id 1: call_hash (U64: hash of the call arguments)
// field_id 2: ttl_seconds (U64: time-to-live)
// field_id 3: result_size (U64: size of cached result in bytes)
println!("\n--- 2. Populating Tool Cache ---");
let tools = [
("search_web", 7200u64, 4096u64), // 2h TTL, ~4KB results
("read_file", 86400, 1024), // 24h TTL, ~1KB results
("execute_code", 300, 2048), // 5min TTL, ~2KB results
("query_database", 1800, 8192), // 30min TTL, ~8KB results
("call_api", 3600, 512), // 1h TTL, ~512B results
];
let entries_per_tool = 10;
let total_entries = tools.len() * entries_per_tool;
let mut next_id: u64 = 0;
for (tool_name, base_ttl, base_result_size) in &tools {
let vectors: Vec<Vec<f32>> = (0..entries_per_tool)
.map(|i| random_vector(dim, next_id + i as u64))
.collect();
let vec_refs: Vec<&[f32]> = vectors.iter().map(|v| v.as_slice()).collect();
let ids: Vec<u64> = (next_id..next_id + entries_per_tool as u64).collect();
let mut metadata = Vec::with_capacity(entries_per_tool * 4);
for i in 0..entries_per_tool {
metadata.push(MetadataEntry {
field_id: 0,
value: MetadataValue::String(tool_name.to_string()),
});
// Deterministic call hash
let call_hash = (next_id + i as u64) * 0xDEADBEEF;
metadata.push(MetadataEntry {
field_id: 1,
value: MetadataValue::U64(call_hash),
});
// Vary TTL slightly per entry
let ttl = base_ttl + (i as u64) * 100;
metadata.push(MetadataEntry {
field_id: 2,
value: MetadataValue::U64(ttl),
});
// Vary result size
let result_size = base_result_size + (i as u64) * 128;
metadata.push(MetadataEntry {
field_id: 3,
value: MetadataValue::U64(result_size),
});
}
store
.ingest_batch(&vec_refs, &ids, Some(&metadata))
.expect("failed to ingest cache entries");
println!(
" Tool '{}': {} entries cached (base TTL: {}s)",
tool_name, entries_per_tool, base_ttl
);
next_id += entries_per_tool as u64;
}
println!(" Total cache entries: {}", total_entries);
// -- Step 3: Cache lookup (similarity search) --
println!("\n--- 3. Cache Lookup (Semantic Similarity) ---");
// Look up a cached result for a search_web call
let query_seed = 3; // close to the 4th search_web entry
let query = random_vector(dim, query_seed);
let k = 5;
let all_results = store
.query(&query, k, &QueryOptions::default())
.expect("query failed");
println!(" Query (seed={}): top-{} cache hits:", query_seed, k);
print_cache_results(&all_results, &tools, entries_per_tool);
// Determine hit/miss
let closest = &all_results[0];
let hit_threshold = 0.5;
if closest.distance < hit_threshold {
println!(
"\n CACHE HIT: ID {} (distance {:.6} < threshold {:.1})",
closest.id, closest.distance, hit_threshold
);
} else {
println!(
"\n CACHE MISS: closest distance {:.6} >= threshold {:.1}",
closest.distance, hit_threshold
);
}
// -- Step 4: Filtered search — search_web with ttl > 3600 --
println!("\n--- 4. Filtered Search: search_web with TTL > 3600 ---");
let filter_web_long_ttl = FilterExpr::And(vec![
FilterExpr::Eq(0, FilterValue::String("search_web".to_string())),
FilterExpr::Gt(2, FilterValue::U64(3600)),
]);
let opts_filtered = QueryOptions {
filter: Some(filter_web_long_ttl),
..Default::default()
};
let filtered_results = store
.query(&query, k, &opts_filtered)
.expect("filtered query failed");
println!(
" search_web entries with TTL > 3600: {} results",
filtered_results.len()
);
print_cache_results(&filtered_results, &tools, entries_per_tool);
// Verify all results match the filter
for r in &filtered_results {
let tool_idx = (r.id as usize) / entries_per_tool;
assert_eq!(
tools[tool_idx].0, "search_web",
"ID {} should be search_web",
r.id
);
let entry_idx = (r.id as usize) % entries_per_tool;
let ttl = tools[tool_idx].1 + (entry_idx as u64) * 100;
assert!(ttl > 3600, "ID {} TTL {} should be > 3600", r.id, ttl);
}
if !filtered_results.is_empty() {
println!(" All results verified: search_web with TTL > 3600.");
}
// -- Step 5: Cache hit vs miss pattern --
println!("\n--- 5. Cache Hit/Miss Pattern ---");
let test_queries: Vec<(u64, &str)> = vec![
(0, "exact match for search_web[0]"),
(15, "exact match for execute_code[5]"),
(9999, "no match (random query)"),
(25, "exact match for read_file[5]"),
];
println!(
" {:>40} {:>12} {:>8}",
"Query Description", "Distance", "Hit?"
);
println!(" {:->40} {:->12} {:->8}", "", "", "");
for (seed, desc) in &test_queries {
let q = random_vector(dim, *seed);
let results = store
.query(&q, 1, &QueryOptions::default())
.expect("query failed");
if let Some(r) = results.first() {
let is_hit = r.distance < hit_threshold;
println!(
" {:>40} {:>12.6} {:>8}",
desc,
r.distance,
if is_hit { "HIT" } else { "MISS" }
);
}
}
// -- Step 6: Delete expired entries --
println!("\n--- 6. Deleting Expired Cache Entries ---");
// Delete all entries with TTL <= 500 (short-lived execute_code entries)
let status_before = store.status();
println!(" Before deletion: {} vectors", status_before.total_vectors);
let filter_expired = FilterExpr::And(vec![
FilterExpr::Eq(0, FilterValue::String("execute_code".to_string())),
FilterExpr::Le(2, FilterValue::U64(500)),
]);
let del_result = store
.delete_by_filter(&filter_expired)
.expect("delete failed");
println!(
" Deleted {} expired execute_code entries (TTL <= 500)",
del_result.deleted
);
let status_after_delete = store.status();
println!(
" After deletion: {} vectors (epoch {})",
status_after_delete.total_vectors, status_after_delete.current_epoch
);
// -- Step 7: Compact to reclaim space --
println!("\n--- 7. Compaction ---");
println!(
" Dead space ratio: {:.2}%",
status_after_delete.dead_space_ratio * 100.0
);
let compact_result = store.compact().expect("compaction failed");
println!(
" Compacted: {} segments, {} bytes reclaimed (epoch {})",
compact_result.segments_compacted,
compact_result.bytes_reclaimed,
compact_result.epoch
);
let status_after_compact = store.status();
println!(
" After compaction: {} vectors, {} bytes",
status_after_compact.total_vectors, status_after_compact.file_size
);
// Verify queries still work after compaction
let post_compact_results = store
.query(&query, k, &QueryOptions::default())
.expect("post-compact query failed");
println!(
" Post-compaction query: {} results (cache still functional)",
post_compact_results.len()
);
store.close().expect("failed to close store");
// -- Summary --
println!("\n=== Tool Cache Summary ===\n");
println!(
" {:>16} {:>8} {:>10} {:>12}",
"Tool", "Entries", "Base TTL", "Result Size"
);
println!(
" {:->16} {:->8} {:->10} {:->12}",
"", "", "", ""
);
for (name, ttl, size) in &tools {
println!(
" {:>16} {:>8} {:>9}s {:>11}B",
name, entries_per_tool, ttl, size
);
}
println!("\n Initial entries: {}", total_entries);
println!(" Deleted (expired): {}", del_result.deleted);
println!(
" Final entries: {}",
status_after_compact.total_vectors
);
println!(" Compaction: {} bytes reclaimed", compact_result.bytes_reclaimed);
println!("\nDone.");
}
fn print_cache_results(
results: &[SearchResult],
tools: &[(&str, u64, u64)],
entries_per_tool: usize,
) {
println!(
" {:>6} {:>12} {:>16} {:>10} {:>12}",
"ID", "Distance", "Tool", "TTL (s)", "Result (B)"
);
println!(
" {:->6} {:->12} {:->16} {:->10} {:->12}",
"", "", "", "", ""
);
for r in results {
let tool_idx = (r.id as usize) / entries_per_tool;
let entry_idx = (r.id as usize) % entries_per_tool;
let (tool_name, base_ttl, base_size) = if tool_idx < tools.len() {
tools[tool_idx]
} else {
("unknown", 0, 0)
};
let ttl = base_ttl + (entry_idx as u64) * 100;
let result_size = base_size + (entry_idx as u64) * 128;
println!(
" {:>6} {:>12.6} {:>16} {:>10} {:>12}",
r.id, r.distance, tool_name, ttl, result_size
);
}
}