use crate::core::wave_cube::WaveCube; use crate::core::hypervector::Hypervector; use crate::weaver::pattern_matcher::TokenInfo; use crate::ai::memory_store::{MemoryStore, MemoryEntry}; use crate::ai::FugaAI; use std::path::Path; pub struct AnswerEngine { pub cube: WaveCube, pub memory: MemoryStore, pub dim: usize, } pub struct AnswerResult { pub query: String, pub route: String, pub hits: Vec, pub cube_entropy: f64, } pub struct AnswerHit { pub source_doc: String, pub similarity: f64, pub text: String, pub snippet: Option, } impl AnswerEngine { pub fn load(cube_path: &str) -> Result { let cube = WaveCube::::load_bin(cube_path)?; let mem_path = cube_path.replace(".bin", "_mem.bin"); let memory = if std::path::Path::new(&mem_path).exists() { MemoryStore::load_bin(&mem_path)? } else { MemoryStore::new() }; let dim = cube.dim; Ok(Self { cube, memory, dim }) } pub fn search(&self, query: &str) -> AnswerResult { let route = self.route_query(query); let tokens: Vec = query.split_whitespace().enumerate().map(|(_, w)| TokenInfo { id: crate::weaver::token_id(&w), text: w.to_string(), }).collect(); let mut ai = FugaAI::::new(self.dim, 3); ai.cube = self.cube.clone(); let output = ai.think(&tokens); let entropy = ai.cube.global_entropy(); let mut all_hits: Vec<(f64, String, String)> = Vec::new(); let text_results = search_memory_by_words(&self.memory, query, 30); for (_idx, sim, entry) in &text_results { all_hits.push((*sim, entry.source_doc.clone(), entry.text.clone())); } if all_hits.len() < 5 { for st in &output.super_tokens { let results = self.memory.search(&st.vector, 10); for (_idx, sim, entry) in &results { all_hits.push((*sim, entry.source_doc.clone(), entry.text.clone())); } } } all_hits.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); let query_owned = query.to_string(); let mut seen = std::collections::HashSet::new(); let hits: Vec = all_hits.into_iter() .filter(|(_, doc, text)| { let key = format!("{}::{}", doc, text); seen.insert(key) }) .take(10) .map(|(sim, doc, text)| { let snippet = if sim > 0.35 { extract_relevant_snippet(&doc, &query_owned) } else { None }; let func_match = text.split(';').find(|part| { query_owned.split_whitespace().any(|w| part.to_lowercase().contains(&w.to_lowercase())) }).unwrap_or("").trim().to_string(); AnswerHit { source_doc: doc, similarity: sim, text: func_match, snippet } }) .collect(); AnswerResult { query: query_owned, route, hits, cube_entropy: entropy } } pub fn search_with_prompts(&self, query: &str, prompt_names: &[String]) -> AnswerResult { if prompt_names.is_empty() { return self.search(query); } let dim = self.dim; let prompt_vecs = crate::ai::prompts::PromptVectors::resolve(prompt_names, dim); let prompt_refs: Vec<&Hypervector> = prompt_vecs.iter().collect(); let route = self.route_query(query); let tokens: Vec = query.split_whitespace().enumerate().map(|(_, w)| TokenInfo { id: crate::weaver::token_id(&w), text: w.to_string(), }).collect(); let mut ai = FugaAI::::new(self.dim, 3); ai.cube = self.cube.clone(); let output = ai.think(&tokens); let entropy = ai.cube.global_entropy(); let mut all_hits: Vec<(f64, String, String)> = Vec::new(); for st in &output.super_tokens { let modulated = st.vector.bind(prompt_refs[0]); let results = self.memory.search(&modulated, 10); for (_idx, sim, entry) in &results { all_hits.push((*sim, entry.source_doc.clone(), entry.text.clone())); } } let text_results = search_memory_by_words(&self.memory, query, 30); for (_idx, sim, entry) in &text_results { all_hits.push((*sim, entry.source_doc.clone(), entry.text.clone())); } all_hits.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); let query_owned = query.to_string(); let mut seen = std::collections::HashSet::new(); let hits: Vec = all_hits.into_iter() .filter(|(_, doc, text)| { let key = format!("{}::{}", doc, text); seen.insert(key) }) .take(10) .map(|(sim, doc, text)| { AnswerHit { source_doc: doc, similarity: sim, text, snippet: None } }) .collect(); AnswerResult { query: query_owned, route, hits, cube_entropy: entropy } } fn route_query(&self, query: &str) -> String { let code_keywords = ["fn ", "func ", "def ", "struct ", "class ", "impl ", "int ", "void ", "char ", "return ", "static ", "pub ", "const ", "let ", "mut ", "->", "=>", "::", "#include", "import "]; let has_code = code_keywords.iter().any(|kw| query.contains(kw)); if has_code { "CodeLogic".to_string() } else { "GeneralLanguage".to_string() } } pub fn format_summary(&self, result: &AnswerResult) -> String { let mut out = String::new(); out.push_str(&format!("Fuga AI — Structured Answer\n")); out.push_str(&format!("Query: {}\n", result.query)); out.push_str(&format!("Route: {}\n", result.route)); out.push_str(&format!("Matches: {}\n", result.hits.len())); out.push_str(&format!("Entropy: {:.4}\n", result.cube_entropy)); out.push('\n'); if result.hits.is_empty() { out.push_str("No resonance in cube.\n"); return out; } let top = &result.hits[0]; out.push_str(&format!("Top match: {} (sim={:.3})\n", top.source_doc, top.similarity)); if let Some(ref snippet) = top.snippet { out.push_str("```\n"); out.push_str(snippet); out.push_str("\n```\n"); } else { let truncated = if top.text.len() > 200 { format!("{}...", &top.text[..200]) } else { top.text.clone() }; out.push_str(&truncated); out.push('\n'); } out.push('\n'); if result.hits.len() > 1 { out.push_str("Related:\n"); for hit in result.hits.iter().skip(1).take(5) { let label = Path::new(&hit.source_doc) .file_name() .map(|f| f.to_string_lossy()) .unwrap_or(std::borrow::Cow::Borrowed(&hit.source_doc)); out.push_str(&format!(" {} (sim={:.3})\n", label, hit.similarity)); } } out } pub fn format_explain(&self, result: &AnswerResult) -> String { let mut out = String::new(); out.push_str("**Fuga AI — Resonance Response**\n\n"); if result.hits.is_empty() { out.push_str("No resonant context found in cube.\n"); return out; } let top = &result.hits[0]; let file_name = Path::new(&top.source_doc) .file_name() .map(|f| f.to_string_lossy()) .unwrap_or(std::borrow::Cow::Borrowed(&top.source_doc)); let parent = Path::new(&top.source_doc) .parent() .and_then(|p| p.file_name()) .map(|f| f.to_string_lossy()) .unwrap_or(std::borrow::Cow::Borrowed("?")); out.push_str(&format!("**Module:** `{}` ({})\n", file_name, parent)); out.push_str(&format!("**Resonance:** {:.3} (phase lock)\n", top.similarity)); if let Some(ref snippet) = top.snippet { let lang_hint = match Path::new(&top.source_doc) .extension() .and_then(|e| e.to_str()) { Some("rs") => "rust", Some("c") | Some("h") => "c", Some("py") => "python", Some("go") => "go", Some("ts") | Some("tsx") => "typescript", Some("js") | Some("jsx") => "javascript", Some("cpp") | Some("cc") | Some("hpp") => "cpp", _ => "", }; out.push_str(&format!("**Key fragment:**\n```{}\n{}\n```\n", lang_hint, snippet)); } if result.hits.len() > 1 { out.push_str("---\n**Related modules:**\n"); for hit in result.hits.iter().skip(1).take(4) { let label = Path::new(&hit.source_doc) .file_name() .map(|f| f.to_string_lossy()) .unwrap_or(std::borrow::Cow::Borrowed(&hit.source_doc)); out.push_str(&format!("- `{}` (sim={:.3})\n", label, hit.similarity)); } } out.push_str(&format!("\n*Status: phase-locked at entropy {:.4}*\n", result.cube_entropy)); out } } fn search_memory_by_words<'a>(memory: &'a MemoryStore, query: &str, top_k: usize) -> Vec<(usize, f64, &'a MemoryEntry)> { let query_lower = query.to_lowercase(); let words: Vec<&str> = query_lower.split_whitespace() .filter(|w| w.len() > 2) .collect(); if words.is_empty() { return memory.search_by_text(query, top_k); } let mut scores: Vec<(usize, f64, usize)> = memory.all_entries().iter().enumerate() .map(|(i, e)| { let source_lower = e.source_doc.to_lowercase(); let fname = std::path::Path::new(&e.source_doc) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("") .to_lowercase(); let mut match_count = 0; let is_source = source_lower.ends_with(".c") || source_lower.ends_with(".rs") || source_lower.ends_with(".go") || source_lower.ends_with(".py"); for w in &words { if fname.contains(w) { match_count += 5; } else if source_lower.contains(w) { match_count += 2; } let text_lower = e.text.to_lowercase(); let text_hits = text_lower.matches(w).count(); if text_hits > 0 { match_count += 3 + text_hits.min(3); } } if is_source { match_count += 2; } (i, match_count as f64 / (words.len() as f64 * 11.0 + 2.0), match_count) }) .collect(); scores.sort_by(|a, b| b.2.cmp(&a.2)); scores.truncate(top_k); let mut results: Vec<(usize, f64, &MemoryEntry)> = scores.into_iter() .filter(|(_, _, total)| *total > 0) .map(|(i, s, _)| (i, s.min(1.0), &memory.all_entries()[i])) .collect(); results.sort_by(|a, b| { b.1.partial_cmp(&a.1).unwrap().then_with(|| { let a_src = a.2.source_doc.ends_with(".c") || a.2.source_doc.ends_with(".rs") || a.2.source_doc.ends_with(".go") || a.2.source_doc.ends_with(".py"); let b_src = b.2.source_doc.ends_with(".c") || b.2.source_doc.ends_with(".rs") || b.2.source_doc.ends_with(".go") || b.2.source_doc.ends_with(".py"); b_src.cmp(&a_src) }) }); results } fn extract_relevant_snippet(file_path: &str, query_text: &str) -> Option { let content = std::fs::read_to_string(file_path).ok()?; let lang = lang_from_extension(file_path); let query_lower = query_text.to_lowercase(); let query_words: Vec<&str> = query_lower.split_whitespace().filter(|w| w.len() > 1).collect(); if query_words.is_empty() { return None; } if let Some(lang_id) = lang { if let Some(snippet) = extract_ast_node(&content, lang_id, &query_words) { return Some(snippet); } } let lines: Vec<&str> = content.lines().collect(); let mut best_line = None; let mut best_score = 0usize; for (i, l) in lines.iter().enumerate() { let lower = l.to_lowercase(); let score: usize = query_words.iter().map(|kw| lower.matches(kw).count()).sum(); let is_comment = lower.trim_start().starts_with("//") || lower.trim_start().starts_with("/*") || lower.trim_start().starts_with("#") || lower.trim_start().starts_with('*'); let adjusted = if is_comment { score / 2 } else { score * 2 }; if adjusted > best_score { best_score = adjusted; best_line = Some(i); } } let match_line = best_line?; let context_start = match_line.saturating_sub(3); let mut context_end = lines.len().min(match_line + 12); for i in match_line..lines.len() { let trimmed = lines[i].trim(); if trimmed.is_empty() && i > match_line + 3 { context_end = i + 1; break; } if trimmed.starts_with('}') || (trimmed.len() < 2 && i > match_line + 2) { context_end = i + 1; break; } } let snippet: String = lines[context_start..context_end] .iter() .map(|l| l.to_string()) .collect::>() .join("\n"); if snippet.len() > 2000 { let truncated: String = snippet.chars().take(2000).collect(); return Some(format!("{}...\n--- (truncated) ---", truncated)); } if snippet.trim().is_empty() { None } else { Some(snippet) } } fn lang_from_extension(path: &str) -> Option { let ext = std::path::Path::new(path).extension()?.to_str()?; crate::multi::LanguageId::from_extension(ext) } fn extract_ast_node(content: &str, lang: crate::multi::LanguageId, query_words: &[&str]) -> Option { let mut parser = tree_sitter::Parser::new(); parser.set_language(&lang.tree_sitter_language()).ok()?; let tree = parser.parse(content, None)?; let root = tree.root_node(); let func_kinds = lang.function_kinds(); let struct_kinds: &[&str] = match lang { crate::multi::LanguageId::Rust => &["struct_item", "impl_item", "trait_item", "enum_item"], crate::multi::LanguageId::C | crate::multi::LanguageId::Cpp => &["struct_specifier", "enum_specifier"], _ => &["class_declaration", "struct_specifier"], }; let mut candidates: Vec<(String, usize)> = Vec::new(); collect_ast_nodes_recursive(root, content, func_kinds, struct_kinds, query_words, &mut candidates); if candidates.is_empty() { return None; } candidates.sort_by(|a, b| b.1.cmp(&a.1)); let (best_text, _) = &candidates[0]; let lines: Vec<&str> = best_text.lines().collect(); let display_lines = lines.len().min(40); let display: String = lines[..display_lines].join("\n"); if display.len() > 2000 { Some(format!("{}...\n--- (truncated, {} lines) ---", &display[..2000], lines.len())) } else { Some(display) } } fn collect_ast_nodes_recursive( node: tree_sitter::Node, source: &str, func_kinds: &[&str], struct_kinds: &[&str], query_words: &[&str], candidates: &mut Vec<(String, usize)>, ) { let kind = node.kind(); let is_target = func_kinds.contains(&kind) || struct_kinds.contains(&kind); if is_target { let byte_range = node.byte_range(); if byte_range.start < byte_range.end && byte_range.end <= source.len() { let text = &source[byte_range.clone()]; let score = count_keyword_matches(text, query_words); if score > 0 { candidates.push((text.to_string(), score)); } } } let mut cursor = node.walk(); for child in node.children(&mut cursor) { collect_ast_nodes_recursive(child, source, func_kinds, struct_kinds, query_words, candidates); } } fn count_keyword_matches(text: &str, query_words: &[&str]) -> usize { let lower = text.to_lowercase(); query_words.iter().filter(|w| lower.contains(*w)).count() }