fuga-2.0 / src /ai /answer_engine.rs
Kateira1123's picture
Fuga 2.0 core modules: AI, VSA, TM, anomaly detection
33b4056 verified
Raw
History Blame Contribute Delete
16.7 kB
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<const N: usize, const S: usize> {
pub cube: WaveCube<N, S>,
pub memory: MemoryStore,
pub dim: usize,
}
pub struct AnswerResult {
pub query: String,
pub route: String,
pub hits: Vec<AnswerHit>,
pub cube_entropy: f64,
}
pub struct AnswerHit {
pub source_doc: String,
pub similarity: f64,
pub text: String,
pub snippet: Option<String>,
}
impl<const N: usize, const S: usize> AnswerEngine<N, S> {
pub fn load(cube_path: &str) -> Result<Self, String> {
let cube = WaveCube::<N, S>::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<TokenInfo> = query.split_whitespace().enumerate().map(|(_, w)| TokenInfo {
id: crate::weaver::token_id(&w),
text: w.to_string(),
}).collect();
let mut ai = FugaAI::<N, S>::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<AnswerHit> = 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<TokenInfo> = query.split_whitespace().enumerate().map(|(_, w)| TokenInfo {
id: crate::weaver::token_id(&w),
text: w.to_string(),
}).collect();
let mut ai = FugaAI::<N, S>::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<AnswerHit> = 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<String> {
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::<Vec<_>>()
.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<crate::multi::LanguageId> {
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<String> {
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()
}