//! Query classifier/router for expert selection. //! //! Uses keyword matching + hashtag awareness for routing. //! Infrastructure ready for SmolLM2 model-based classification. use std::collections::HashMap; /// Router classifies user input into an expert domain pub struct Router { experts: Vec, /// Hashtag → expert mapping (e.g., "rust" → "rust_coding") tag_to_expert: HashMap, /// Expert priority — higher number = higher priority when multiple tags match. /// Domain/code experts get higher priority than tone/style experts. expert_priority: HashMap, } /// Keyword-based routing rules (Sential 2.0 — orchestra-based routing) /// /// Three-layer architecture: /// structural → struct, impl, trait, enum, generics /// flow_error → match, result, option, error, concurrency /// system_io → io, file, collections, regex const ROUTING_KEYWORDS: &[(&str, &[&str])] = &[ // ── Orchestra: Structural (architects) ── ( "structural", &[ "struct", "impl", "trait", "enum", "generics", "derive", "type", "where", "associated", "implement", "implementation", "constructor", ], ), // ── Orchestra: Flow & Error (error handling + concurrency) ── ( "flow_error", &[ "match", "result", "option", "error", "unwrap", "expect", "panic", "map_err", "and_then", "thread", "spawn", "async", "await", "tokio", "mutex", "lock", "arc", "atomic", ], ), // ── Orchestra: System & IO (file ops + collections) ── ( "system_io", &[ "io", "file", "read", "write", "hashmap", "hashset", "vec", "iterator", "bufreader", "open", "create", "append", "stdin", "stdout", "serialize", ], ), // ── Legacy: rust_coding (backward compat) ── ( "rust_coding", &[ "rust", "cargo", "rustc", "unsafe", "compile", "borrow", "lifetime", "macro", "crate", "module", "rustfmt", "clippy", ], ), // ── Legacy: system_ops ── ( "system_ops", &[ "linux", "bash", "shell", "server", "deploy", "docker", "nginx", "ssh", "sudo", "systemd", "cron", "devops", "container", "kubernetes", "ansible", "terraform", ], ), ( "planning", &[ "plan", "roadmap", "architecture", "design", "strategy", "proposal", "timeline", "milestone", "sprint", "backlog", "requirement", "specification", ], ), ]; #[allow(dead_code)] impl Router { /// Create a new router with the given expert names pub fn new(experts: &[String]) -> Self { let expert_priority = build_expert_priority(); Self { experts: experts.to_vec(), tag_to_expert: build_tag_to_expert_map(), expert_priority, } } /// Classify using both keywords and hashtags (primary: hashtags, fallback: keywords). /// /// Strategy: score ALL matching hashtag→expert mappings by expert priority, /// picking the highest-priority expert (not just the first match). pub fn classify_with_tags(&self, query: &str, tags: &[String]) -> String { // Collect all tag→expert matches with their priorities let mut best_expert: Option<&str> = None; let mut best_priority: i32 = -1; for tag in tags { let clean = tag.trim_start_matches('#'); if let Some(expert) = self.tag_to_expert.get(clean) { if self.experts.contains(expert) { let priority = self .expert_priority .get(expert.as_str()) .copied() .unwrap_or(0); tracing::debug!("Router: tag #{clean} → expert {expert} (priority={priority})"); if priority > best_priority { best_priority = priority; best_expert = Some(expert); } } } } if let Some(expert) = best_expert { return expert.to_string(); } // Fallback to keyword matching self.classify(query) } /// Classify a query into the most relevant expert domain pub fn classify(&self, query: &str) -> String { let result = self.classify_keyword(query); // Fallback to "general" if no match if self.experts.contains(&result) { result } else { "general".to_string() } } /// Keyword-based classification (fast, no model needed) fn classify_keyword(&self, query: &str) -> String { let query_lower = query.to_lowercase(); let mut best_match = "general".to_string(); let mut best_score = 0i32; for (expert, keywords) in ROUTING_KEYWORDS { let score: i32 = keywords .iter() .map(|kw| { // Count occurrences of the keyword in the query let count = query_lower.matches(kw).count() as i32; // Bonus for exact word matches let exact_count = query_lower .split_whitespace() .filter(|&w| { w == *kw || w.trim_matches(|c: char| !c.is_alphanumeric()) == *kw }) .count() as i32; count + exact_count * 2 }) .sum(); if score > best_score { best_score = score; best_match = expert.to_string(); } } best_match } /// Get the list of available experts pub fn experts(&self) -> &[String] { &self.experts } /// Get a description of the routing rules pub fn routing_info(&self) -> String { let mut info = String::from("Available experts and routing keywords:\n"); for (expert, keywords) in ROUTING_KEYWORDS { info.push_str(&format!(" - {}: {}\n", expert, keywords.join(", "))); } info.push_str(" - general: default (fallback)\n"); info.push_str("\nHashtag → expert mapping:\n"); for (tag, expert) in &self.tag_to_expert { info.push_str(&format!(" - #{tag} → {expert}\n")); } info } } /// Build mapping from hashtags to expert (orchestra) names. /// /// Sential 2.0 — three-layer orchestra architecture: /// structural → #struct, #impl, #trait, #enum /// flow_error → #match, #result, #option, #error, #concurrency /// system_io → #io, #collections, #file fn build_tag_to_expert_map() -> HashMap { let mut map = HashMap::new(); // ── Orchestra: Structural Layer (architects) ── for tag in &["struct", "impl", "trait", "enum"] { map.insert(tag.to_string(), "structural".to_string()); } // ── Orchestra: Flow & Error Layer ── for tag in &["match", "result", "option", "error", "concurrency"] { map.insert(tag.to_string(), "flow_error".to_string()); } // ── Orchestra: System & IO Layer ── for tag in &["io", "file", "collections", "regex"] { map.insert(tag.to_string(), "system_io".to_string()); } // ── Legacy adapters (backward compat) ── for tag in &["rust", "cargo", "tokio", "borrow", "lifetime"] { map.insert(tag.to_string(), "rust_coding".to_string()); } map.insert("casual".to_string(), "friendly_chat".to_string()); map.insert("chat".to_string(), "friendly_chat".to_string()); for tag in &["teaching", "learn", "tutorial"] { map.insert(tag.to_string(), "teaching".to_string()); } for tag in &["algorithms", "math", "memory"] { map.insert(tag.to_string(), "rust_coding".to_string()); } for tag in &[ "devops", "linux", "networking", "database", "web", "security", ] { map.insert(tag.to_string(), "general".to_string()); } map } /// Build expert priority map. /// /// Priority values: /// 20 = Orchestra experts (structural, flow_error, system_io) — highest /// 10 = Domain experts (rust_coding, teaching) /// 5 = General-purpose /// 0 = Tone/style (friendly_chat) fn build_expert_priority() -> HashMap { let mut map = HashMap::new(); // Orchestra layers — highest priority (Sential 2.0) map.insert("structural".to_string(), 20); map.insert("flow_error".to_string(), 20); map.insert("system_io".to_string(), 20); // Domain experts map.insert("rust_coding".to_string(), 10); map.insert("teaching".to_string(), 10); // General map.insert("general".to_string(), 5); // Tone/style — lowest map.insert("friendly_chat".to_string(), 0); map }