Files
poimen-memory/crates/mem-cli/src/query/query_reasoner.rs
T
rock b0cc00f63b
CI / CI (pull_request) Successful in 11m46s
fix: resolve integration test compilation + CI errors
Test compilation fixes (8 integration test files):
  1. Ambiguous float types — added f32/f64 annotations
  2. chrono API — replaced with_hour() with date_naive().and_hms_opt()
  3. Missing dev-dependencies — added sqlx + base64
  4. Generic parse — wrapped f32 comparison in parens
  5. Incorrect assertion — 3^5=243 > 100, changed nodes to 1000

CI fixes:
  6. Missing benchmark fixtures — created 3 files in fixtures/benchmarks/
  7. clippy absurd_extreme_comparisons — usize >= 0 always true
  8. authentik_jwt test — Option<SystemTime> type mismatch
  9. http_server tests — removed broken RBAC test module (types deleted)

Result: cargo build --all clean, cargo test --all --lib passes
2026-09-08 17:53:19 -07:00

416 lines
13 KiB
Rust

//! 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<String>,
/// Relation types to follow
pub relation_types: Vec<String>,
/// Constraints to apply
pub constraints: Vec<Constraint>,
/// 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<String>,
/// 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<String>,
/// Answer confidence
pub confidence: f32,
/// Reasoning steps
pub reasoning_steps: Vec<ReasoningStep>,
/// Evidence supporting answer
pub evidence: Vec<String>,
/// 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<Vec<SubQuery>, 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<SubQuery>,
project_id: &str,
) -> Result<ReasonedAnswer, String> {
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<String> = all_results.into_iter().collect::<std::collections::HashSet<_>>().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<bool, String> {
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<String> {
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::<std::collections::HashSet<_>>().into_iter().collect()
}
/// Extract relation keywords from question
fn extract_relations_from_question(&self, question: &str) -> Vec<String> {
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<Constraint> {
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<Vec<String>, String> {
// Stub: would query database based on sub_query
Ok(vec![])
}
/// Apply constraints to results
fn apply_constraints(&self, results: &[String], constraints: &[Constraint]) -> Vec<String> {
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
}
}