/// Reasoning API routes — graph-native inference over the Engram knowledge graph. /// /// POST /reason — evaluate a hypothesis /// POST /reason/causal — find causal chains for a concept /// POST /reason/contradictions — detect contradictions around a topic use axum::{extract::State, http::StatusCode, Json}; use engram_reasoning::{ CausalDirection, Hypothesis, HypothesisType, ReasoningConfig, ReasoningEngine, ReasoningResult, EvidenceChain, EvidenceNode, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::state::AppState; // ── POST /reason ────────────────────────────────────────────────────────────── #[derive(Deserialize)] pub struct ReasonRequest { pub hypothesis: String, pub hypothesis_type: HypothesisTypeParam, pub embedding: Vec, #[serde(default)] pub config: ReasoningConfigParam, } /// JSON-friendly version of HypothesisType (mirrors the enum for serde) #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] pub enum HypothesisTypeParam { IsTrue, WhatCauses, HowTo, WhatIs, Compare, } impl From for HypothesisType { fn from(p: HypothesisTypeParam) -> Self { match p { HypothesisTypeParam::IsTrue => HypothesisType::IsTrue, HypothesisTypeParam::WhatCauses => HypothesisType::WhatCauses, HypothesisTypeParam::HowTo => HypothesisType::HowTo, HypothesisTypeParam::WhatIs => HypothesisType::WhatIs, HypothesisTypeParam::Compare => HypothesisType::Compare, } } } #[derive(Deserialize, Default)] pub struct ReasoningConfigParam { pub max_depth: Option, pub min_confidence: Option, pub max_evidence_nodes: Option, pub contradiction_threshold: Option, } impl From for ReasoningConfig { fn from(p: ReasoningConfigParam) -> Self { let def = ReasoningConfig::default(); ReasoningConfig { max_depth: p.max_depth.unwrap_or(def.max_depth), min_confidence: p.min_confidence.unwrap_or(def.min_confidence), max_evidence_nodes: p.max_evidence_nodes.unwrap_or(def.max_evidence_nodes), contradiction_threshold: p .contradiction_threshold .unwrap_or(def.contradiction_threshold), } } } pub async fn reason( State(state): State>, Json(req): Json, ) -> Result, StatusCode> { let config: ReasoningConfig = req.config.into(); let hypothesis = Hypothesis::new(req.hypothesis, req.embedding, req.hypothesis_type.into()); let db = state.db.clone(); let mut engine = ReasoningEngine::new(db, config); let result = engine .reason(&hypothesis) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(result)) } // ── POST /reason/causal ─────────────────────────────────────────────────────── #[derive(Deserialize)] pub struct CausalRequest { pub concept_embedding: Vec, #[serde(default = "default_causal_direction")] pub direction: CausalDirectionParam, } fn default_causal_direction() -> CausalDirectionParam { CausalDirectionParam::Forward } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] pub enum CausalDirectionParam { Forward, Backward, Both, } impl From for CausalDirection { fn from(p: CausalDirectionParam) -> Self { match p { CausalDirectionParam::Forward => CausalDirection::Forward, CausalDirectionParam::Backward => CausalDirection::Backward, CausalDirectionParam::Both => CausalDirection::Both, } } } #[derive(Serialize)] pub struct CausalResponse { pub chains: Vec, } pub async fn causal( State(state): State>, Json(req): Json, ) -> Result, StatusCode> { let db = state.db.clone(); let mut engine = ReasoningEngine::with_default_config(db); let chains = engine .causal_chain(&req.concept_embedding, req.direction.into()) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(CausalResponse { chains })) } // ── POST /reason/contradictions ─────────────────────────────────────────────── #[derive(Deserialize)] pub struct ContradictionsRequest { pub topic_embedding: Vec, } #[derive(Serialize)] pub struct ContradictionPair { pub supporting: EvidenceNode, pub refuting: EvidenceNode, } #[derive(Serialize)] pub struct ContradictionsResponse { pub contradictions: Vec, } pub async fn contradictions( State(state): State>, Json(req): Json, ) -> Result, StatusCode> { let db = state.db.clone(); let mut engine = ReasoningEngine::with_default_config(db); let pairs = engine .find_contradictions(&req.topic_embedding) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(ContradictionsResponse { contradictions: pairs .into_iter() .map(|(s, r)| ContradictionPair { supporting: s, refuting: r, }) .collect(), })) }