mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-27 00:25:10 +00:00
* feat(postgres): Add 7 advanced AI modules to ruvector-postgres Comprehensive implementation of advanced AI capabilities: ## New Modules (23,541 lines of code) ### 1. Self-Learning / ReasoningBank (`src/learning/`) - Trajectory tracking for query optimization - Pattern extraction using K-means clustering - ReasoningBank for pattern storage and matching - Adaptive search parameter optimization ### 2. Attention Mechanisms (`src/attention/`) - Scaled dot-product attention (core) - Multi-head attention with parallel heads - Flash Attention v2 (memory-efficient) - 10 attention types with PostgresEnum support ### 3. GNN Layers (`src/gnn/`) - Message passing framework - GCN (Graph Convolutional Network) - GraphSAGE with mean/max aggregation - Configurable aggregation methods ### 4. Hyperbolic Embeddings (`src/hyperbolic/`) - Poincaré ball model - Lorentz hyperboloid model - Hyperbolic distance metrics - Möbius operations ### 5. Sparse Vectors (`src/sparse/`) - COO format sparse vector type - Efficient sparse-sparse distance functions - BM25/SPLADE compatible - Top-k pruning operations ### 6. Graph Operations & Cypher (`src/graph/`) - Property graph storage (nodes/edges) - BFS, DFS, Dijkstra traversal - Cypher query parser (AST-based) - Query executor with pattern matching ### 7. Tiny Dancer Routing (`src/routing/`) - FastGRNN neural network - Agent registry with capabilities - Multi-objective routing optimization - Cost/latency/quality balancing ## Docker Infrastructure - Dockerfile with pgrx 0.12.6 and PostgreSQL 16 - docker-compose.yml with test runner - Initialization SQL with test tables - Shell scripts for dev/test/benchmark ## Feature Flags - `learning`, `attention`, `gnn`, `hyperbolic` - `sparse`, `graph`, `routing` - `ai-complete` and `graph-complete` bundles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(docker): Copy entire workspace for pgrx build 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(docker): Build standalone crate without workspace 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: Update README to enhance clarity and structure * fix(postgres): Resolve compilation errors and Docker build issues - Fix simsimd Option/Result type mismatch in scaled_dot.rs - Fix f32/f64 type conversions in poincare.rs and lorentz.rs - Fix AVX512 missing wrapper functions by using AVX2 fallback - Fix Vec<Vec<f32>> to JsonB for pgrx pg_extern compatibility - Fix DashMap get() to get_mut() for mutable access - Fix router.rs dereference for best_score comparison - Update Dockerfile to copy pre-written SQL file for pgrx - Simplify init.sql to use correct function names - Add postgres-cli npm package for CLI tooling All changes tested successfully in Docker with: - Extension loads with AVX2 SIMD support (8 floats/op) - Distance functions verified working - PostgreSQL 16 container runs successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add ruvLLM examples and enhanced postgres-cli Added from claude/ruvector-lfm2-llm-01YS5Tc7i64PyYCLecT9L1dN branch: - examples/ruvLLM: Complete LLM inference system with SIMD optimization - Pretraining, benchmarking, and optimization system - Real SIMD-optimized CPU inference engine - Comprehensive SOTA benchmark suite - Attention mechanisms, memory management, router Enhanced postgres-cli with full ruvector-postgres integration: - Sparse vector operations (BM25, top-k, prune, conversions) - Hyperbolic geometry (Poincare, Lorentz, Mobius operations) - Agent routing (Tiny Dancer system) - Vector quantization (binary, scalar, product) - Enhanced graph and learning commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(postgres-cli): Use native ruvector type instead of pgvector - Change createVectorTable to use ruvector type (native RuVector extension) - Add dimensions column for metadata since ruvector is variable-length - Update index creation to use simple btree (HNSW/IVFFlat TBD) - Tested against Docker container with ruvector extension 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(postgres): Add 53 SQL function definitions for all advanced modules Enable all advanced PostgreSQL extension functions by adding their SQL definitions to the extension file. This exposes all Rust #[pg_extern] functions to PostgreSQL. ## New SQL Functions (53 total) ### Hyperbolic Geometry (8 functions) - ruvector_poincare_distance, ruvector_lorentz_distance - ruvector_mobius_add, ruvector_exp_map, ruvector_log_map - ruvector_poincare_to_lorentz, ruvector_lorentz_to_poincare - ruvector_minkowski_dot ### Sparse Vectors (14 functions) - ruvector_sparse_create, ruvector_sparse_from_dense - ruvector_sparse_dot, ruvector_sparse_cosine, ruvector_sparse_l2_distance - ruvector_sparse_add, ruvector_sparse_scale, ruvector_sparse_to_dense - ruvector_sparse_nnz, ruvector_sparse_dim - ruvector_bm25_score, ruvector_tf_idf, ruvector_sparse_normalize - ruvector_sparse_topk ### GNN - Graph Neural Networks (5 functions) - ruvector_gnn_gcn_layer, ruvector_gnn_graphsage_layer - ruvector_gnn_gat_layer, ruvector_gnn_message_pass - ruvector_gnn_aggregate ### Routing/Agents - "Tiny Dancer" (11 functions) - ruvector_route_query, ruvector_route_with_context - ruvector_calculate_agent_affinity, ruvector_select_best_agent - ruvector_multi_agent_route, ruvector_create_agent_embedding - ruvector_get_routing_stats, ruvector_register_agent - ruvector_update_agent_performance, ruvector_adaptive_route - ruvector_fastgrnn_forward ### Learning/ReasoningBank (7 functions) - ruvector_record_trajectory, ruvector_get_verdict - ruvector_distill_memory, ruvector_adaptive_search - ruvector_learning_feedback, ruvector_get_learning_patterns - ruvector_optimize_search_params ### Graph/Cypher (8 functions) - ruvector_graph_create_node, ruvector_graph_create_edge - ruvector_graph_get_neighbors, ruvector_graph_shortest_path - ruvector_graph_pagerank, ruvector_cypher_query - ruvector_graph_traverse, ruvector_graph_similarity_search ## CLI Updates - Enabled hyperbolic geometry commands in postgres-cli - Added vector distance and normalize commands - Enhanced client with connection pooling and retry logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
495 lines
15 KiB
Rust
495 lines
15 KiB
Rust
//! Integration tests for RuvLLM
|
|
//!
|
|
//! Tests the complete pipeline from request to response.
|
|
|
|
use ruvllm::{Config, RuvLLM, Request};
|
|
use ruvllm::types::{MemoryNode, MemoryEdge, NodeType, EdgeType, Feedback};
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
/// Atomic counter for unique test directories
|
|
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
/// Helper to create test config with unique database path
|
|
fn test_config() -> Config {
|
|
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
let db_path = format!("/tmp/ruvllm_test_{}.db", id);
|
|
Config::builder()
|
|
.db_path(&db_path)
|
|
.embedding_dim(128)
|
|
.router_hidden_dim(32)
|
|
.learning_enabled(false)
|
|
.build()
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_basic_query() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
let response = llm.query("What is machine learning?").await.unwrap();
|
|
|
|
assert!(!response.text.is_empty());
|
|
assert!(!response.request_id.is_empty());
|
|
assert!(response.confidence >= 0.0 && response.confidence <= 1.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_query_with_context() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
// Preload some context
|
|
// (In real tests, we'd inject memory nodes)
|
|
|
|
let response = llm.query("Explain neural networks").await.unwrap();
|
|
|
|
assert!(!response.text.is_empty());
|
|
assert!(response.latency.total_ms > 0.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_session_management() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
// Create a session
|
|
let session = llm.new_session();
|
|
assert!(!session.id.is_empty());
|
|
|
|
// Query with session
|
|
let response = llm.query_session(&session, "Hello").await.unwrap();
|
|
assert!(!response.text.is_empty());
|
|
|
|
// Query again in same session
|
|
let response2 = llm.query_session(&session, "Follow up question").await.unwrap();
|
|
assert!(!response2.text.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_routing_decision() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
let response = llm.query("Simple question").await.unwrap();
|
|
|
|
// Check routing info is populated
|
|
assert!(response.routing_info.confidence >= 0.0);
|
|
assert!(response.routing_info.temperature > 0.0);
|
|
assert!(response.routing_info.top_p > 0.0);
|
|
assert!(response.routing_info.context_size > 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_latency_breakdown() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
let response = llm.query("Test query for latency").await.unwrap();
|
|
|
|
// All latency components should be non-negative
|
|
assert!(response.latency.embedding_ms >= 0.0);
|
|
assert!(response.latency.retrieval_ms >= 0.0);
|
|
assert!(response.latency.routing_ms >= 0.0);
|
|
assert!(response.latency.attention_ms >= 0.0);
|
|
assert!(response.latency.generation_ms >= 0.0);
|
|
|
|
// Total should be sum of components (approximately)
|
|
let sum = response.latency.embedding_ms
|
|
+ response.latency.retrieval_ms
|
|
+ response.latency.routing_ms
|
|
+ response.latency.attention_ms
|
|
+ response.latency.generation_ms;
|
|
|
|
// Allow some variance for overhead
|
|
assert!(response.latency.total_ms >= sum * 0.9);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_feedback() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
let response = llm.query("Test for feedback").await.unwrap();
|
|
|
|
// Provide feedback
|
|
let feedback = Feedback {
|
|
request_id: response.request_id.clone(),
|
|
rating: Some(5),
|
|
correction: None,
|
|
task_success: Some(true),
|
|
};
|
|
|
|
// Should not error
|
|
llm.feedback(feedback).await.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_queries() {
|
|
let config = test_config();
|
|
let llm = std::sync::Arc::new(RuvLLM::new(config).await.unwrap());
|
|
|
|
// Run multiple queries concurrently
|
|
let mut handles = Vec::new();
|
|
for i in 0..5 {
|
|
let llm_clone = llm.clone();
|
|
let handle = tokio::spawn(async move {
|
|
let query = format!("Concurrent query {}", i);
|
|
llm_clone.query(query).await.unwrap()
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Wait for all
|
|
for handle in handles {
|
|
let response = handle.await.unwrap();
|
|
assert!(!response.text.is_empty());
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_shutdown() {
|
|
let config = test_config();
|
|
let llm = RuvLLM::new(config).await.unwrap();
|
|
|
|
// Query first
|
|
llm.query("Before shutdown").await.unwrap();
|
|
|
|
// Shutdown should succeed
|
|
llm.shutdown().await.unwrap();
|
|
}
|
|
|
|
// Module-specific integration tests
|
|
|
|
mod memory_integration {
|
|
use super::*;
|
|
use ruvllm::memory::MemoryService;
|
|
use ruvllm::config::MemoryConfig;
|
|
|
|
#[tokio::test]
|
|
async fn test_memory_pipeline() {
|
|
let config = MemoryConfig::default();
|
|
let memory = MemoryService::new(&config).await.unwrap();
|
|
|
|
// Insert nodes
|
|
let nodes: Vec<MemoryNode> = (0..100)
|
|
.map(|i| {
|
|
let mut vec: Vec<f32> = vec![0.0; 128];
|
|
vec[i % 128] = 1.0;
|
|
MemoryNode {
|
|
id: format!("node-{}", i),
|
|
vector: vec,
|
|
text: format!("Document {} about topic {}", i, i % 10),
|
|
node_type: NodeType::Document,
|
|
source: "test".into(),
|
|
metadata: HashMap::new(),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
for node in nodes {
|
|
memory.insert_node(node).unwrap();
|
|
}
|
|
|
|
// Insert edges
|
|
for i in 0..99 {
|
|
let edge = MemoryEdge {
|
|
id: format!("edge-{}", i),
|
|
src: format!("node-{}", i),
|
|
dst: format!("node-{}", i + 1),
|
|
edge_type: EdgeType::Follows,
|
|
weight: 0.8,
|
|
metadata: HashMap::new(),
|
|
};
|
|
memory.insert_edge(edge).unwrap();
|
|
}
|
|
|
|
// Search
|
|
let mut query = vec![0.0f32; 128];
|
|
query[50] = 1.0;
|
|
|
|
let result = memory.search_with_graph(&query, 10, 64, 2).await.unwrap();
|
|
|
|
assert!(!result.candidates.is_empty());
|
|
assert!(result.candidates.len() <= 10);
|
|
|
|
// First result should be close to node-50
|
|
assert_eq!(result.candidates[0].id, "node-50");
|
|
|
|
// Subgraph should include neighbors
|
|
assert!(!result.subgraph.nodes.is_empty());
|
|
}
|
|
}
|
|
|
|
mod router_integration {
|
|
use super::*;
|
|
use ruvllm::router::FastGRNNRouter;
|
|
use ruvllm::config::RouterConfig;
|
|
use ruvllm::types::RouterSample;
|
|
|
|
#[test]
|
|
fn test_router_training_cycle() {
|
|
let config = RouterConfig::default();
|
|
let mut router = FastGRNNRouter::new(&config).unwrap();
|
|
|
|
// Create training samples
|
|
let samples: Vec<RouterSample> = (0..100)
|
|
.map(|i| RouterSample {
|
|
features: vec![0.1; config.input_dim],
|
|
label_model: i % 4,
|
|
label_context: i % 5,
|
|
label_temperature: 0.7,
|
|
label_top_p: 0.9,
|
|
quality: 0.8,
|
|
latency_ms: 100.0 + (i as f32) * 10.0,
|
|
})
|
|
.collect();
|
|
|
|
// Train
|
|
let metrics = router.train_batch(&samples, 0.001, 0.0, None, None);
|
|
|
|
assert!(metrics.total_loss >= 0.0);
|
|
assert!(metrics.model_accuracy >= 0.0);
|
|
|
|
// Forward pass should work
|
|
let features = vec![0.1; config.input_dim];
|
|
let hidden = vec![0.0; config.hidden_dim];
|
|
let decision = router.forward(&features, &hidden).unwrap();
|
|
|
|
assert!(decision.confidence >= 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_router_ewc() {
|
|
let config = RouterConfig::default();
|
|
let mut router = FastGRNNRouter::new(&config).unwrap();
|
|
|
|
// Initial training
|
|
let samples1: Vec<RouterSample> = (0..50)
|
|
.map(|_| RouterSample {
|
|
features: vec![0.1; config.input_dim],
|
|
label_model: 0,
|
|
label_context: 0,
|
|
label_temperature: 0.5,
|
|
label_top_p: 0.9,
|
|
quality: 0.9,
|
|
latency_ms: 50.0,
|
|
})
|
|
.collect();
|
|
|
|
router.train_batch(&samples1, 0.001, 0.0, None, None);
|
|
|
|
// Compute Fisher information
|
|
let fisher = router.compute_fisher(&samples1);
|
|
|
|
// Train on new task with EWC (using same weights as optimal for test)
|
|
let samples2: Vec<RouterSample> = (0..50)
|
|
.map(|_| RouterSample {
|
|
features: vec![0.5; config.input_dim],
|
|
label_model: 3,
|
|
label_context: 4,
|
|
label_temperature: 0.9,
|
|
label_top_p: 0.95,
|
|
quality: 0.7,
|
|
latency_ms: 200.0,
|
|
})
|
|
.collect();
|
|
|
|
// Train with EWC regularization (using fisher as a proxy for optimal weights)
|
|
let metrics = router.train_batch(
|
|
&samples2,
|
|
0.001,
|
|
0.4,
|
|
Some(&fisher),
|
|
Some(&fisher), // Using fisher as placeholder for optimal weights
|
|
);
|
|
|
|
// Total loss should be non-negative
|
|
assert!(metrics.total_loss >= 0.0);
|
|
assert!(metrics.samples_processed > 0);
|
|
}
|
|
}
|
|
|
|
mod attention_integration {
|
|
use super::*;
|
|
use ruvllm::attention::GraphAttentionEngine;
|
|
use ruvllm::memory::SubGraph;
|
|
use ruvllm::config::EmbeddingConfig;
|
|
|
|
#[test]
|
|
fn test_attention_with_complex_graph() {
|
|
let config = EmbeddingConfig::default();
|
|
let engine = GraphAttentionEngine::new(&config).unwrap();
|
|
|
|
// Create a complex subgraph
|
|
let nodes: Vec<MemoryNode> = (0..20)
|
|
.map(|i| {
|
|
let mut vec = vec![0.1; config.dimension];
|
|
vec[i % config.dimension] += 0.5;
|
|
// Normalize
|
|
let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
|
|
vec.iter_mut().for_each(|x| *x /= norm);
|
|
|
|
MemoryNode {
|
|
id: format!("n-{}", i),
|
|
vector: vec,
|
|
text: format!("Node {}", i),
|
|
node_type: NodeType::Document,
|
|
source: "test".into(),
|
|
metadata: HashMap::new(),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// Create edges forming a more complex structure
|
|
let mut edges = Vec::new();
|
|
for i in 0..19 {
|
|
edges.push(MemoryEdge {
|
|
id: format!("e-{}-{}", i, i + 1),
|
|
src: format!("n-{}", i),
|
|
dst: format!("n-{}", i + 1),
|
|
edge_type: EdgeType::Follows,
|
|
weight: 0.9,
|
|
metadata: HashMap::new(),
|
|
});
|
|
}
|
|
// Add some cross-links
|
|
for i in (0..15).step_by(5) {
|
|
edges.push(MemoryEdge {
|
|
id: format!("cross-{}", i),
|
|
src: format!("n-{}", i),
|
|
dst: format!("n-{}", i + 5),
|
|
edge_type: EdgeType::SameTopic,
|
|
weight: 0.7,
|
|
metadata: HashMap::new(),
|
|
});
|
|
}
|
|
|
|
let subgraph = SubGraph {
|
|
nodes,
|
|
edges,
|
|
center_ids: vec!["n-0".into()],
|
|
};
|
|
|
|
// Query
|
|
let query = vec![0.2; config.dimension];
|
|
let context = engine.attend(&query, &subgraph).unwrap();
|
|
|
|
// Validate
|
|
assert_eq!(context.ranked_nodes.len(), 20);
|
|
assert_eq!(context.attention_weights.len(), 20);
|
|
|
|
// Weights sum to 1
|
|
let sum: f32 = context.attention_weights.iter().sum();
|
|
assert!((sum - 1.0).abs() < 0.01);
|
|
|
|
// Multi-head weights
|
|
assert!(!context.head_weights.is_empty());
|
|
|
|
// Summary stats
|
|
assert_eq!(context.summary.num_nodes, 20);
|
|
assert!(context.summary.num_edges > 0);
|
|
}
|
|
}
|
|
|
|
mod embedding_integration {
|
|
use super::*;
|
|
use ruvllm::embedding::{EmbeddingService, PoolingStrategy};
|
|
use ruvllm::config::EmbeddingConfig;
|
|
|
|
#[test]
|
|
fn test_embedding_batch_processing() {
|
|
let config = EmbeddingConfig::default();
|
|
let service = EmbeddingService::new(&config).unwrap();
|
|
|
|
let texts: Vec<&str> = vec![
|
|
"The quick brown fox",
|
|
"Jumps over the lazy dog",
|
|
"Machine learning is fascinating",
|
|
"Neural networks process information",
|
|
"Vector databases store embeddings",
|
|
];
|
|
|
|
let embeddings = service.embed_batch(&texts).unwrap();
|
|
|
|
assert_eq!(embeddings.len(), 5);
|
|
|
|
// Check pairwise similarities
|
|
let mut similarities = Vec::new();
|
|
for i in 0..embeddings.len() {
|
|
for j in (i + 1)..embeddings.len() {
|
|
let dot: f32 = embeddings[i].vector.iter()
|
|
.zip(embeddings[j].vector.iter())
|
|
.map(|(a, b)| a * b)
|
|
.sum();
|
|
similarities.push((i, j, dot));
|
|
}
|
|
}
|
|
|
|
// Related texts should have higher similarity
|
|
// (In mock embeddings this may not hold, but structure should work)
|
|
assert_eq!(similarities.len(), 10); // 5 choose 2
|
|
}
|
|
|
|
#[test]
|
|
fn test_embedding_pooling_comparison() {
|
|
let config = EmbeddingConfig::default();
|
|
let service = EmbeddingService::new(&config).unwrap();
|
|
|
|
let text = "This is a test sentence for comparing pooling strategies";
|
|
|
|
let mean = service.embed_with_pooling(text, PoolingStrategy::Mean).unwrap();
|
|
let max = service.embed_with_pooling(text, PoolingStrategy::Max).unwrap();
|
|
let cls = service.embed_with_pooling(text, PoolingStrategy::CLS).unwrap();
|
|
let last = service.embed_with_pooling(text, PoolingStrategy::LastToken).unwrap();
|
|
|
|
// All should produce valid embeddings
|
|
for emb in [&mean, &max, &cls, &last] {
|
|
let norm: f32 = emb.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
|
|
assert!((norm - 1.0).abs() < 0.01);
|
|
}
|
|
|
|
// CLS and Mean should differ
|
|
let cls_mean_dot: f32 = cls.vector.iter()
|
|
.zip(mean.vector.iter())
|
|
.map(|(a, b)| a * b)
|
|
.sum();
|
|
assert!(cls_mean_dot.abs() < 0.999);
|
|
}
|
|
}
|
|
|
|
mod compression_integration {
|
|
use super::*;
|
|
use ruvllm::compression::CompressionService;
|
|
use ruvllm::memory::MemoryService;
|
|
use ruvllm::config::MemoryConfig;
|
|
|
|
#[tokio::test]
|
|
async fn test_compression_pipeline() {
|
|
let config = MemoryConfig::default();
|
|
let memory = MemoryService::new(&config).await.unwrap();
|
|
|
|
// Insert nodes
|
|
for i in 0..50 {
|
|
let node = MemoryNode {
|
|
id: format!("compress-{}", i),
|
|
vector: vec![0.1; 128],
|
|
text: format!("Document {} for compression", i),
|
|
node_type: NodeType::Document,
|
|
source: "test".into(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
memory.insert_node(node).unwrap();
|
|
}
|
|
|
|
// Create compression service
|
|
let compression = CompressionService::new(5, 0.5);
|
|
|
|
// Run compression
|
|
let stats = compression.run_compression(&memory).await.unwrap();
|
|
|
|
// Stats should be populated (even if 0 for mock)
|
|
assert!(stats.clusters_found >= 0);
|
|
}
|
|
}
|