SelentialCore / src /router.rs
S4ntyC1t's picture
Upload 23 files
18e0633 verified
Raw
History Blame Contribute Delete
9.62 kB
//! 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<String>,
/// Hashtag β†’ expert mapping (e.g., "rust" β†’ "rust_coding")
tag_to_expert: HashMap<String, String>,
/// Expert priority β€” higher number = higher priority when multiple tags match.
/// Domain/code experts get higher priority than tone/style experts.
expert_priority: HashMap<String, i32>,
}
/// 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<String, String> {
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<String, i32> {
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
}