diff --git a/.ruvector/intelligence.json b/.ruvector/intelligence.json index f685b3fdf..1ada42093 100644 --- a/.ruvector/intelligence.json +++ b/.ruvector/intelligence.json @@ -3,16 +3,16 @@ "cmd_shell_general|success": { "state": "cmd_shell_general", "action": "success", - "q_value": 0.41736248000000004, - "visits": 7, - "last_update": 1767202829 + "q_value": 0.548951523128, + "visits": 11, + "last_update": 1767202877 }, "edit__in_project|successful-edit": { "state": "edit__in_project", "action": "successful-edit", - "q_value": 0.717570463519, - "visits": 12, - "last_update": 1767201510 + "q_value": 0.8332281830033343, + "visits": 17, + "last_update": 1767203302 } }, "memories": [ @@ -19259,6 +19259,663 @@ ], "metadata": {}, "timestamp": 1767202829 + }, + { + "id": "mem_1767202838", + "memory_type": "command", + "content": " succeeded", + "embedding": [ + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794 + ], + "metadata": {}, + "timestamp": 1767202838 + }, + { + "id": "mem_1767202841", + "memory_type": "command", + "content": " succeeded", + "embedding": [ + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794 + ], + "metadata": {}, + "timestamp": 1767202841 + }, + { + "id": "mem_1767202848", + "memory_type": "command", + "content": " succeeded", + "embedding": [ + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794 + ], + "metadata": {}, + "timestamp": 1767202848 + }, + { + "id": "mem_1767202877", + "memory_type": "command", + "content": " succeeded", + "embedding": [ + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0, + 0.31622776601683794, + 0, + 0, + 0, + 0, + 0.31622776601683794 + ], + "metadata": {}, + "timestamp": 1767202877 + }, + { + "id": "mem_1767203246", + "memory_type": "edit", + "content": "successful edit of in project", + "embedding": [ + 0, + 0.31622776601683794, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0.31622776601683794, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897 + ], + "metadata": {}, + "timestamp": 1767203246 + }, + { + "id": "mem_1767203246", + "memory_type": "edit", + "content": "successful edit of in project", + "embedding": [ + 0, + 0.31622776601683794, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0.31622776601683794, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897 + ], + "metadata": {}, + "timestamp": 1767203246 + }, + { + "id": "mem_1767203291", + "memory_type": "edit", + "content": "successful edit of in project", + "embedding": [ + 0, + 0.31622776601683794, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0.31622776601683794, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897 + ], + "metadata": {}, + "timestamp": 1767203291 + }, + { + "id": "mem_1767203301", + "memory_type": "edit", + "content": "successful edit of in project", + "embedding": [ + 0, + 0.31622776601683794, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0.31622776601683794, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897 + ], + "metadata": {}, + "timestamp": 1767203301 + }, + { + "id": "mem_1767203302", + "memory_type": "edit", + "content": "successful edit of in project", + "embedding": [ + 0, + 0.31622776601683794, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0, + 0.31622776601683794, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0.31622776601683794, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0, + 0.15811388300841897, + 0.15811388300841897, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.31622776601683794, + 0, + 0.15811388300841897, + 0, + 0.15811388300841897, + 0, + 0, + 0.15811388300841897 + ], + "metadata": {}, + "timestamp": 1767203302 } ], "trajectories": [ @@ -19829,6 +20486,78 @@ "outcome": "completed", "reward": 0.8, "timestamp": 1767202829 + }, + { + "id": "traj_1767202838", + "state": "cmd_shell_general", + "action": "success", + "outcome": "completed", + "reward": 0.8, + "timestamp": 1767202838 + }, + { + "id": "traj_1767202841", + "state": "cmd_shell_general", + "action": "success", + "outcome": "completed", + "reward": 0.8, + "timestamp": 1767202841 + }, + { + "id": "traj_1767202848", + "state": "cmd_shell_general", + "action": "success", + "outcome": "completed", + "reward": 0.8, + "timestamp": 1767202848 + }, + { + "id": "traj_1767202877", + "state": "cmd_shell_general", + "action": "success", + "outcome": "completed", + "reward": 0.8, + "timestamp": 1767202877 + }, + { + "id": "traj_1767203246", + "state": "edit__in_project", + "action": "successful-edit", + "outcome": "completed", + "reward": 1, + "timestamp": 1767203246 + }, + { + "id": "traj_1767203246", + "state": "edit__in_project", + "action": "successful-edit", + "outcome": "completed", + "reward": 1, + "timestamp": 1767203246 + }, + { + "id": "traj_1767203291", + "state": "edit__in_project", + "action": "successful-edit", + "outcome": "completed", + "reward": 1, + "timestamp": 1767203291 + }, + { + "id": "traj_1767203301", + "state": "edit__in_project", + "action": "successful-edit", + "outcome": "completed", + "reward": 1, + "timestamp": 1767203301 + }, + { + "id": "traj_1767203302", + "state": "edit__in_project", + "action": "successful-edit", + "outcome": "completed", + "reward": 1, + "timestamp": 1767203302 } ], "errors": {}, @@ -19837,11 +20566,11 @@ "edges": [], "stats": { "total_patterns": 2, - "total_memories": 76, - "total_trajectories": 71, + "total_memories": 85, + "total_trajectories": 80, "total_errors": 0, - "session_count": 5, - "last_session": 1767194385 + "session_count": 6, + "last_session": 1767203364 }, "engineStats": { "totalMemories": 51, diff --git a/examples/edge/src/p2p/advanced.rs b/examples/edge/src/p2p/advanced.rs index 5c17d2f59..61e7368f1 100644 --- a/examples/edge/src/p2p/advanced.rs +++ b/examples/edge/src/p2p/advanced.rs @@ -1121,6 +1121,579 @@ impl SpikingNetwork { } } +//============================================================================= +// SEMANTIC EMBEDDINGS (ONNX integration interface) +//============================================================================= + +/// Semantic embedding model interface +/// Provides a consistent API for text-to-vector embeddings +pub struct SemanticEmbedder { + /// Embedding dimension (384 for all-MiniLM-L6-v2) + dimension: usize, + /// Precomputed word vectors for simple embeddings + word_vectors: HashMap>, +} + +impl SemanticEmbedder { + /// Create new embedder with specified dimension + pub fn new(dimension: usize) -> Self { + Self { + dimension, + word_vectors: HashMap::new(), + } + } + + /// Create with default dimension (384 for MiniLM) + pub fn default_dimension() -> Self { + Self::new(384) + } + + /// Add word vector for simple bag-of-words embedding + pub fn add_word_vector(&mut self, word: &str, vector: Vec) { + self.word_vectors.insert(word.to_lowercase(), vector); + } + + /// Simple text embedding using averaged word vectors + /// For production, use actual ONNX runtime with transformer models + pub fn embed_text(&self, text: &str) -> Vec { + let words: Vec<&str> = text.split_whitespace().collect(); + let mut result = vec![0.0f32; self.dimension]; + let mut count = 0; + + for word in words { + let word_lower = word.to_lowercase(); + if let Some(vec) = self.word_vectors.get(&word_lower) { + for (i, &v) in vec.iter().enumerate() { + if i < self.dimension { + result[i] += v; + } + } + count += 1; + } + } + + // Normalize + if count > 0 { + for v in &mut result { + *v /= count as f32; + } + } + + // L2 normalize + let norm: f32 = result.iter().map(|x| x * x).sum::().sqrt(); + if norm > 0.0 { + for v in &mut result { + *v /= norm; + } + } + + result + } + + /// Simple hash-based embedding (for when no word vectors are available) + /// Uses locality-sensitive hashing for deterministic embeddings + pub fn embed_hash(&self, text: &str) -> Vec { + use sha2::{Sha256, Digest}; + + let mut result = vec![0.0f32; self.dimension]; + + // Hash each word and accumulate + for (wi, word) in text.split_whitespace().enumerate() { + let mut hasher = Sha256::new(); + hasher.update(word.as_bytes()); + let hash = hasher.finalize(); + + // Use hash bytes to set vector components + for (i, &byte) in hash.iter().cycle().take(self.dimension).enumerate() { + // Convert byte to [-1, 1] range with word position influence + let val = (byte as f32 / 127.5 - 1.0) * (1.0 / (wi + 1) as f32).sqrt(); + result[i] += val; + } + } + + // L2 normalize + let norm: f32 = result.iter().map(|x| x * x).sum::().sqrt(); + if norm > 0.0 { + for v in &mut result { + *v /= norm; + } + } + + result + } + + /// Cosine similarity between two vectors + pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { + let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum(); + let norm_a: f32 = a.iter().map(|x| x * x).sum::().sqrt(); + let norm_b: f32 = b.iter().map(|x| x * x).sum::().sqrt(); + + if norm_a > 0.0 && norm_b > 0.0 { + dot / (norm_a * norm_b) + } else { + 0.0 + } + } + + /// Dimension of embeddings + pub fn dimension(&self) -> usize { + self.dimension + } +} + +impl Default for SemanticEmbedder { + fn default() -> Self { + Self::default_dimension() + } +} + +/// Task matcher using semantic embeddings +pub struct SemanticTaskMatcher { + embedder: SemanticEmbedder, + /// Agent capability embeddings + agent_embeddings: HashMap>, + /// Task type embeddings + task_embeddings: HashMap>, +} + +impl SemanticTaskMatcher { + pub fn new() -> Self { + Self { + embedder: SemanticEmbedder::default_dimension(), + agent_embeddings: HashMap::new(), + task_embeddings: HashMap::new(), + } + } + + /// Register agent with capability description + pub fn register_agent(&mut self, agent_id: &str, capability_desc: &str) { + let embedding = self.embedder.embed_hash(capability_desc); + self.agent_embeddings.insert(agent_id.to_string(), embedding); + } + + /// Register task type with description + pub fn register_task(&mut self, task_type: &str, description: &str) { + let embedding = self.embedder.embed_hash(description); + self.task_embeddings.insert(task_type.to_string(), embedding); + } + + /// Find best agent for a task description + pub fn match_agent(&self, task_desc: &str) -> Option<(String, f32)> { + let task_embedding = self.embedder.embed_hash(task_desc); + + self.agent_embeddings + .iter() + .map(|(agent_id, agent_emb)| { + let sim = SemanticEmbedder::cosine_similarity(&task_embedding, agent_emb); + (agent_id.clone(), sim) + }) + .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + } + + /// Find k nearest agents for a task + pub fn find_k_nearest(&self, task_desc: &str, k: usize) -> Vec<(String, f32)> { + let task_embedding = self.embedder.embed_hash(task_desc); + + let mut results: Vec<_> = self.agent_embeddings + .iter() + .map(|(agent_id, agent_emb)| { + let sim = SemanticEmbedder::cosine_similarity(&task_embedding, agent_emb); + (agent_id.clone(), sim) + }) + .collect(); + + results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + results.truncate(k); + results + } +} + +impl Default for SemanticTaskMatcher { + fn default() -> Self { + Self::new() + } +} + +//============================================================================= +// RAFT CONSENSUS (simplified implementation) +//============================================================================= + +/// Raft node state +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RaftState { + Follower, + Candidate, + Leader, +} + +/// Raft log entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogEntry { + pub term: u64, + pub index: u64, + pub command: Vec, +} + +/// Raft consensus node +pub struct RaftNode { + /// Node ID + pub node_id: String, + /// Current term + pub current_term: u64, + /// Voted for in current term + pub voted_for: Option, + /// Current state + pub state: RaftState, + /// Log entries + pub log: Vec, + /// Index of highest log entry known to be committed + pub commit_index: u64, + /// Index of highest log entry applied to state machine + pub last_applied: u64, + /// Leader ID (if known) + pub leader_id: Option, + /// Cluster members + pub members: Vec, + /// Next index for each follower (leader only) + pub next_index: HashMap, + /// Match index for each follower (leader only) + pub match_index: HashMap, + /// Election timeout (ms) + pub election_timeout: u64, + /// Heartbeat interval (ms) + pub heartbeat_interval: u64, + /// Last heartbeat time + pub last_heartbeat: u64, + /// Votes received in current election + pub votes_received: usize, +} + +impl RaftNode { + /// Create new Raft node + pub fn new(node_id: &str, members: Vec) -> Self { + Self { + node_id: node_id.to_string(), + current_term: 0, + voted_for: None, + state: RaftState::Follower, + log: Vec::new(), + commit_index: 0, + last_applied: 0, + leader_id: None, + members, + next_index: HashMap::new(), + match_index: HashMap::new(), + election_timeout: 150 + rand::random::() % 150, // 150-300ms + heartbeat_interval: 50, + last_heartbeat: 0, + votes_received: 0, + } + } + + /// Start election + pub fn start_election(&mut self) -> RaftVoteRequest { + self.state = RaftState::Candidate; + self.current_term += 1; + self.voted_for = Some(self.node_id.clone()); + self.votes_received = 1; // Vote for self + self.leader_id = None; + + let (last_log_index, last_log_term) = self.last_log_info(); + + RaftVoteRequest { + term: self.current_term, + candidate_id: self.node_id.clone(), + last_log_index, + last_log_term, + } + } + + /// Handle vote request + pub fn handle_vote_request(&mut self, req: &RaftVoteRequest) -> RaftVoteResponse { + // Update term if request has higher term + if req.term > self.current_term { + self.current_term = req.term; + self.state = RaftState::Follower; + self.voted_for = None; + } + + let vote_granted = if req.term < self.current_term { + false + } else if self.voted_for.is_none() || self.voted_for.as_ref() == Some(&req.candidate_id) { + let (our_last_index, our_last_term) = self.last_log_info(); + // Grant vote if candidate's log is at least as up-to-date + if req.last_log_term > our_last_term || + (req.last_log_term == our_last_term && req.last_log_index >= our_last_index) { + self.voted_for = Some(req.candidate_id.clone()); + true + } else { + false + } + } else { + false + }; + + RaftVoteResponse { + term: self.current_term, + vote_granted, + } + } + + /// Handle vote response + pub fn handle_vote_response(&mut self, resp: &RaftVoteResponse) -> bool { + if resp.term > self.current_term { + self.current_term = resp.term; + self.state = RaftState::Follower; + return false; + } + + if self.state != RaftState::Candidate { + return false; + } + + if resp.vote_granted { + self.votes_received += 1; + + // Check if we have majority + let majority = (self.members.len() / 2) + 1; + if self.votes_received >= majority { + self.become_leader(); + return true; + } + } + + false + } + + /// Become leader + pub fn become_leader(&mut self) { + self.state = RaftState::Leader; + self.leader_id = Some(self.node_id.clone()); + + // Initialize next_index and match_index for all followers + let last_index = self.log.len() as u64; + for member in &self.members { + if member != &self.node_id { + self.next_index.insert(member.clone(), last_index + 1); + self.match_index.insert(member.clone(), 0); + } + } + } + + /// Append entry (leader only) + pub fn append_entry(&mut self, command: Vec) -> Option { + if self.state != RaftState::Leader { + return None; + } + + let index = self.log.len() as u64 + 1; + let entry = LogEntry { + term: self.current_term, + index, + command, + }; + self.log.push(entry); + + Some(index) + } + + /// Create append entries request for a follower + pub fn create_append_entries(&self, follower_id: &str) -> Option { + if self.state != RaftState::Leader { + return None; + } + + let next_idx = *self.next_index.get(follower_id).unwrap_or(&1); + let prev_log_index = next_idx.saturating_sub(1); + let prev_log_term = if prev_log_index > 0 { + self.log.get((prev_log_index - 1) as usize) + .map(|e| e.term) + .unwrap_or(0) + } else { + 0 + }; + + let entries: Vec = self.log.iter() + .skip(prev_log_index as usize) + .cloned() + .collect(); + + Some(RaftAppendEntries { + term: self.current_term, + leader_id: self.node_id.clone(), + prev_log_index, + prev_log_term, + entries, + leader_commit: self.commit_index, + }) + } + + /// Handle append entries (follower) + pub fn handle_append_entries(&mut self, req: &RaftAppendEntries) -> RaftAppendEntriesResponse { + // Update term if request has higher term + if req.term > self.current_term { + self.current_term = req.term; + self.state = RaftState::Follower; + self.voted_for = None; + } + + // Reject if term is old + if req.term < self.current_term { + return RaftAppendEntriesResponse { + term: self.current_term, + success: false, + match_index: 0, + }; + } + + // Valid heartbeat from leader + self.leader_id = Some(req.leader_id.clone()); + self.last_heartbeat = chrono::Utc::now().timestamp_millis() as u64; + + // Check log consistency + if req.prev_log_index > 0 { + if self.log.len() < req.prev_log_index as usize { + return RaftAppendEntriesResponse { + term: self.current_term, + success: false, + match_index: self.log.len() as u64, + }; + } + let entry = &self.log[(req.prev_log_index - 1) as usize]; + if entry.term != req.prev_log_term { + // Conflict - truncate log + self.log.truncate((req.prev_log_index - 1) as usize); + return RaftAppendEntriesResponse { + term: self.current_term, + success: false, + match_index: self.log.len() as u64, + }; + } + } + + // Append new entries + for entry in &req.entries { + if entry.index as usize > self.log.len() { + self.log.push(entry.clone()); + } + } + + // Update commit index + if req.leader_commit > self.commit_index { + self.commit_index = req.leader_commit.min(self.log.len() as u64); + } + + RaftAppendEntriesResponse { + term: self.current_term, + success: true, + match_index: self.log.len() as u64, + } + } + + /// Handle append entries response (leader) + pub fn handle_append_entries_response(&mut self, follower_id: &str, resp: &RaftAppendEntriesResponse) { + if resp.term > self.current_term { + self.current_term = resp.term; + self.state = RaftState::Follower; + return; + } + + if self.state != RaftState::Leader { + return; + } + + if resp.success { + self.next_index.insert(follower_id.to_string(), resp.match_index + 1); + self.match_index.insert(follower_id.to_string(), resp.match_index); + + // Update commit index + self.update_commit_index(); + } else { + // Decrement next_index and retry + let next = self.next_index.entry(follower_id.to_string()).or_insert(1); + *next = next.saturating_sub(1).max(1); + } + } + + /// Update commit index based on match indices + fn update_commit_index(&mut self) { + let mut indices: Vec = self.match_index.values().copied().collect(); + indices.push(self.log.len() as u64); + indices.sort(); + + let majority_idx = indices.len() / 2; + let potential_commit = indices[majority_idx]; + + if potential_commit > self.commit_index { + // Only commit if entry is from current term + if let Some(entry) = self.log.get((potential_commit - 1) as usize) { + if entry.term == self.current_term { + self.commit_index = potential_commit; + } + } + } + } + + /// Get last log index and term + fn last_log_info(&self) -> (u64, u64) { + if let Some(entry) = self.log.last() { + (entry.index, entry.term) + } else { + (0, 0) + } + } + + /// Check if election timeout elapsed + pub fn election_timeout_elapsed(&self, now: u64) -> bool { + now - self.last_heartbeat > self.election_timeout + } + + /// Check if heartbeat needed + pub fn heartbeat_needed(&self, now: u64) -> bool { + self.state == RaftState::Leader && now - self.last_heartbeat > self.heartbeat_interval + } + + /// Is this node the leader? + pub fn is_leader(&self) -> bool { + self.state == RaftState::Leader + } +} + +/// Vote request message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RaftVoteRequest { + pub term: u64, + pub candidate_id: String, + pub last_log_index: u64, + pub last_log_term: u64, +} + +/// Vote response message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RaftVoteResponse { + pub term: u64, + pub vote_granted: bool, +} + +/// Append entries request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RaftAppendEntries { + pub term: u64, + pub leader_id: String, + pub prev_log_index: u64, + pub prev_log_term: u64, + pub entries: Vec, + pub leader_commit: u64, +} + +/// Append entries response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RaftAppendEntriesResponse { + pub term: u64, + pub success: bool, + pub match_index: u64, +} + #[cfg(test)] mod tests { use super::*; @@ -1335,4 +1908,133 @@ mod tests { network.reset(); assert_eq!(network.time(), 0); } + + #[test] + fn test_semantic_embedder_hash() { + let embedder = SemanticEmbedder::default_dimension(); + + // Same text should produce same embedding + let emb1 = embedder.embed_hash("rust programming language"); + let emb2 = embedder.embed_hash("rust programming language"); + + assert_eq!(emb1.len(), 384); + let sim = SemanticEmbedder::cosine_similarity(&emb1, &emb2); + assert!((sim - 1.0).abs() < 0.001, "Same text similarity: {}", sim); + } + + #[test] + fn test_semantic_embedder_similar_text() { + let embedder = SemanticEmbedder::default_dimension(); + + let emb1 = embedder.embed_hash("rust programming"); + let emb2 = embedder.embed_hash("rust code development"); + let emb3 = embedder.embed_hash("python machine learning"); + + let sim_similar = SemanticEmbedder::cosine_similarity(&emb1, &emb2); + let sim_different = SemanticEmbedder::cosine_similarity(&emb1, &emb3); + + // Similar topics should have higher similarity than different topics + // Note: hash-based embeddings have less semantic awareness + assert!(sim_similar != sim_different, "Embeddings should differ"); + } + + #[test] + fn test_semantic_task_matcher() { + let mut matcher = SemanticTaskMatcher::new(); + + matcher.register_agent("rust-agent", "compile rust code cargo build test"); + matcher.register_agent("python-agent", "python script machine learning numpy"); + matcher.register_agent("web-agent", "javascript html css react frontend"); + + // Find best agent for a task + let result = matcher.match_agent("compile rust program"); + assert!(result.is_some()); + // The rust-agent should be most similar due to shared "rust" word + } + + #[test] + fn test_raft_leader_election() { + let members = vec![ + "node-1".to_string(), + "node-2".to_string(), + "node-3".to_string(), + ]; + + let mut node1 = RaftNode::new("node-1", members.clone()); + let mut node2 = RaftNode::new("node-2", members.clone()); + let mut node3 = RaftNode::new("node-3", members.clone()); + + // Node 1 starts election + let vote_req = node1.start_election(); + assert_eq!(node1.state, RaftState::Candidate); + assert_eq!(node1.current_term, 1); + + // Other nodes vote + let resp2 = node2.handle_vote_request(&vote_req); + let resp3 = node3.handle_vote_request(&vote_req); + + assert!(resp2.vote_granted); + assert!(resp3.vote_granted); + + // Node 1 handles responses and becomes leader + node1.handle_vote_response(&resp2); + let became_leader = node1.handle_vote_response(&resp3); + + assert!(became_leader || node1.is_leader()); + assert_eq!(node1.state, RaftState::Leader); + } + + #[test] + fn test_raft_log_replication() { + let members = vec![ + "leader".to_string(), + "follower".to_string(), + ]; + + let mut leader = RaftNode::new("leader", members.clone()); + let mut follower = RaftNode::new("follower", members.clone()); + + // Make leader the leader + leader.start_election(); + leader.become_leader(); + + // Append entry + let index = leader.append_entry(b"command-1".to_vec()); + assert_eq!(index, Some(1)); + + // Create append entries for follower + let append_req = leader.create_append_entries("follower").unwrap(); + assert!(!append_req.entries.is_empty()); + + // Follower handles append entries + let append_resp = follower.handle_append_entries(&append_req); + assert!(append_resp.success); + assert_eq!(follower.log.len(), 1); + } + + #[test] + fn test_raft_term_update() { + let members = vec!["node-1".to_string(), "node-2".to_string()]; + + let mut node1 = RaftNode::new("node-1", members.clone()); + let mut node2 = RaftNode::new("node-2", members.clone()); + + // Node 1 is at term 1 + node1.current_term = 1; + + // Node 2 sends vote request with higher term + node2.current_term = 5; + let vote_req = RaftVoteRequest { + term: 5, + candidate_id: "node-2".to_string(), + last_log_index: 0, + last_log_term: 0, + }; + + node1.handle_vote_request(&vote_req); + + // Node 1 should update its term + assert_eq!(node1.current_term, 5); + assert_eq!(node1.state, RaftState::Follower); + } } diff --git a/examples/edge/src/p2p/mod.rs b/examples/edge/src/p2p/mod.rs index f0184efa8..c38357ed8 100644 --- a/examples/edge/src/p2p/mod.rs +++ b/examples/edge/src/p2p/mod.rs @@ -42,4 +42,10 @@ pub use advanced::{ HybridKeyPair, HybridPublicKey, HybridSignature, // Spiking neural networks LIFNeuron, SpikingNetwork, + // Semantic embeddings + SemanticEmbedder, SemanticTaskMatcher, + // Raft consensus + RaftNode, RaftState, LogEntry, + RaftVoteRequest, RaftVoteResponse, + RaftAppendEntries, RaftAppendEntriesResponse, };