//! Query Reasoning (Phase 5.3) //! //! Complex question decomposition, multi-hop reasoning, constraint satisfaction, //! and answer validation. use std::collections::HashMap; use sqlx::PgPool; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; /// Question type/intent #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum QuestionType { /// "What is X?" - Simple fact lookup Factual, /// "How does A relate to B?" - Relationship query Relationship, /// "Find all X that satisfy Y" - Set query with constraints SetQuery, /// "Why is X true?" - Multi-hop reasoning Causal, /// "Compare A vs B" - Comparative reasoning Comparative, /// "What are consequences of X?" - Forward chaining Consequence, } /// Decomposed sub-query #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubQuery { /// Sub-query ID pub id: String, /// The actual question (natural language) pub question: String, /// Question type pub question_type: QuestionType, /// Entity IDs to query pub entity_ids: Vec, /// Relation types to follow pub relation_types: Vec, /// Constraints to apply pub constraints: Vec, /// Expected result type pub result_type: ResultType, } /// Constraint on results #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Constraint { /// Constraint type (e.g., "confidence", "relation_type", "distance") pub constraint_type: String, /// Operator (e.g., ">=", "==", "in", "not_in") pub operator: String, /// Value to compare against pub value: String, } /// Result type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ResultType { /// Single entity Entity, /// Multiple entities Entities, /// Relationship/edge Edge, /// Multiple relationships Edges, /// Boolean (yes/no) Boolean, /// Count Count, } /// Reasoning step result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReasoningStep { /// Step index pub step_id: usize, /// Sub-query executed pub sub_query: SubQuery, /// Results from this step pub results: Vec, /// Confidence in results pub confidence: f32, /// Constraints satisfied pub constraints_satisfied: usize, /// Constraints total pub constraints_total: usize, } /// Final answer with reasoning #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReasonedAnswer { /// Original question pub question: String, /// Final answer(s) pub answers: Vec, /// Answer confidence pub confidence: f32, /// Reasoning steps pub reasoning_steps: Vec, /// Evidence supporting answer pub evidence: Vec, /// Explanation pub explanation: String, } /// Query Reasoner pub struct QueryReasoner { pool: PgPool, } impl QueryReasoner { pub fn new(pool: PgPool) -> Self { QueryReasoner { pool } } /// Decompose complex question into sub-queries pub fn decompose_question(&self, question: &str) -> Result, String> { if question.is_empty() { return Ok(vec![]); } let question_lower = question.to_lowercase(); let question_type = self.classify_question(question); let mut sub_queries = Vec::new(); // Detect entities in question (simple heuristic: capitalized words) let entities = self.extract_entities_from_question(question); // Detect relation keywords let relations = self.extract_relations_from_question(question); // Create base sub-query let base_query = SubQuery { id: "sq_1".to_string(), question: question.to_string(), question_type: question_type.clone(), entity_ids: entities.clone(), relation_types: relations.clone(), constraints: self.extract_constraints_from_question(question), result_type: self.infer_result_type(&question_type), }; sub_queries.push(base_query); // For complex questions, generate follow-up sub-queries if matches!(question_type, QuestionType::Causal | QuestionType::Comparative) { // Add explanation sub-query sub_queries.push(SubQuery { id: "sq_2".to_string(), question: format!("Explain the reasoning for: {}", question), question_type: QuestionType::Causal, entity_ids: entities, relation_types: relations, constraints: vec![], result_type: ResultType::Entities, }); } Ok(sub_queries) } /// Execute reasoning over sub-queries pub async fn reason_over_subqueries( &self, sub_queries: Vec, project_id: &str, ) -> Result { let original_question = sub_queries .first() .map(|q| q.question.clone()) .unwrap_or_default(); let mut reasoning_steps = Vec::new(); let mut all_results = Vec::new(); let mut total_confidence = 0.0; for (idx, sub_query) in sub_queries.iter().enumerate() { // Execute sub-query let results = self.execute_subquery(sub_query, project_id).await?; // Apply constraints let filtered_results = self.apply_constraints(&results, &sub_query.constraints); let constraint_satisfaction = if sub_query.constraints.is_empty() { 1.0 } else { (filtered_results.len() as f32 / results.len().max(1) as f32).min(1.0) }; let confidence = 0.9 * constraint_satisfaction; reasoning_steps.push(ReasoningStep { step_id: idx + 1, sub_query: sub_query.clone(), results: filtered_results.clone(), confidence, constraints_satisfied: filtered_results.len(), constraints_total: sub_query.constraints.len(), }); all_results.extend(filtered_results); total_confidence += confidence; } let avg_confidence = if reasoning_steps.is_empty() { 0.0 } else { total_confidence / reasoning_steps.len() as f32 }; // Deduplicate results let unique_results: Vec = all_results.into_iter().collect::>().into_iter().collect(); // Generate explanation let explanation = self.generate_explanation(&reasoning_steps, &unique_results); Ok(ReasonedAnswer { question: original_question, answers: unique_results.clone(), confidence: avg_confidence, reasoning_steps, evidence: unique_results.clone(), explanation, }) } /// Validate answer against constraints pub fn validate_answer( &self, answer: &str, constraints: &[Constraint], ) -> Result { if constraints.is_empty() { return Ok(true); } for constraint in constraints { if !self.check_constraint(answer, constraint) { return Ok(false); } } Ok(true) } /// Check if answer satisfies single constraint pub fn check_constraint(&self, value: &str, constraint: &Constraint) -> bool { match constraint.operator.as_str() { "==" | "eq" => value == constraint.value, "!=" | "ne" => value != constraint.value, "contains" => value.contains(&constraint.value), "not_contains" => !value.contains(&constraint.value), "in" => { let values: Vec<&str> = constraint.value.split(',').map(|s| s.trim()).collect(); values.contains(&value) } "not_in" => { let values: Vec<&str> = constraint.value.split(',').map(|s| s.trim()).collect(); !values.contains(&value) } _ => true, } } // ========== Private Helper Methods ========== /// Classify question intent fn classify_question(&self, question: &str) -> QuestionType { let lower = question.to_lowercase(); if lower.contains("how does") || lower.contains("how is") { QuestionType::Relationship } else if lower.contains("why") { QuestionType::Causal } else if lower.contains("compare") || lower.contains("versus") || lower.contains(" vs ") { QuestionType::Comparative } else if lower.contains("consequences") || lower.contains("results in") { QuestionType::Consequence } else if lower.contains("find all") || lower.contains("list all") { QuestionType::SetQuery } else { QuestionType::Factual } } /// Extract entity names from question fn extract_entities_from_question(&self, question: &str) -> Vec { let words: Vec<&str> = question.split_whitespace().collect(); let mut entities = Vec::new(); for word in words { if word.chars().next().map_or(false, |c| c.is_uppercase()) && word.len() > 2 { entities.push(word.to_string()); } } entities.into_iter().collect::>().into_iter().collect() } /// Extract relation keywords from question fn extract_relations_from_question(&self, question: &str) -> Vec { let lower = question.to_lowercase(); let mut relations = Vec::new(); if lower.contains("depend") { relations.push("depends_on".to_string()); } if lower.contains("relate") { relations.push("related_to".to_string()); } if lower.contains("use") { relations.push("uses".to_string()); } if lower.contains("contain") { relations.push("contains".to_string()); } if lower.contains("require") { relations.push("requires".to_string()); } relations } /// Extract constraints from question fn extract_constraints_from_question(&self, question: &str) -> Vec { let mut constraints = Vec::new(); let lower = question.to_lowercase(); if lower.contains("high confidence") || lower.contains("high reliability") { constraints.push(Constraint { constraint_type: "confidence".to_string(), operator: ">=".to_string(), value: "0.8".to_string(), }); } if lower.contains("low confidence") { constraints.push(Constraint { constraint_type: "confidence".to_string(), operator: "<".to_string(), value: "0.5".to_string(), }); } constraints } /// Infer expected result type fn infer_result_type(&self, question_type: &QuestionType) -> ResultType { match question_type { QuestionType::Factual => ResultType::Entity, QuestionType::Relationship => ResultType::Edge, QuestionType::SetQuery => ResultType::Entities, QuestionType::Causal => ResultType::Entities, QuestionType::Comparative => ResultType::Edges, QuestionType::Consequence => ResultType::Entities, } } /// Execute single sub-query async fn execute_subquery( &self, _sub_query: &SubQuery, _project_id: &str, ) -> Result, String> { // Stub: would query database based on sub_query Ok(vec![]) } /// Apply constraints to results fn apply_constraints(&self, results: &[String], constraints: &[Constraint]) -> Vec { if constraints.is_empty() { return results.to_vec(); } results .iter() .filter(|result| { constraints.iter().all(|c| self.check_constraint(result, c)) }) .cloned() .collect() } /// Generate human-readable explanation fn generate_explanation( &self, steps: &[ReasoningStep], answers: &[String], ) -> String { if steps.is_empty() { return "No reasoning steps available".to_string(); } let mut explanation = format!("Found {} answer(s) through {} reasoning step(s): ", answers.len(), steps.len()); for (idx, step) in steps.iter().enumerate() { explanation.push_str(&format!( "Step {}: {} (confidence: {:.2}, {} constraints satisfied). ", step.step_id, step.sub_query.question, step.confidence, step.constraints_satisfied )); } explanation } }