Skip to main content

graphrag_core/retrieval/
explained.rs

1//! Structured answer types with reasoning trace.
2//!
3//! `ExplainedAnswer` and its supporting types (`SourceReference`, `SourceType`,
4//! `ReasoningStep`) were extracted from `retrieval/mod.rs` to keep that module
5//! focused on retrieval orchestration.
6
7use super::{ResultType, SearchResult};
8use std::collections::HashSet;
9
10/// An answer with detailed explanation of the reasoning process
11///
12/// This struct provides transparency into how the GraphRAG system
13/// arrived at its answer, including confidence scores, source references,
14/// and step-by-step reasoning.
15///
16/// # Example
17/// ```no_run
18/// use graphrag_core::prelude::*;
19///
20/// # async fn example() -> graphrag_core::Result<()> {
21/// let mut graphrag = GraphRAG::quick_start("Your document").await?;
22/// let explained = graphrag.ask_explained("What is the main topic?").await?;
23///
24/// println!("Answer: {}", explained.answer);
25/// println!("Confidence: {:.0}%", explained.confidence * 100.0);
26///
27/// for step in &explained.reasoning_steps {
28///     println!("Step {}: {} (confidence: {:.0}%)",
29///         step.step_number, step.description, step.confidence * 100.0);
30/// }
31/// # Ok(())
32/// # }
33/// ```
34#[derive(Debug, Clone)]
35pub struct ExplainedAnswer {
36    /// The answer text
37    pub answer: String,
38    /// Confidence score (0.0 to 1.0)
39    pub confidence: f32,
40    /// Sources used to generate the answer
41    pub sources: Vec<SourceReference>,
42    /// Step-by-step reasoning trace
43    pub reasoning_steps: Vec<ReasoningStep>,
44    /// Entities that were key to the answer
45    pub key_entities: Vec<String>,
46    /// Query analysis that guided retrieval
47    pub query_analysis: Option<super::QueryAnalysis>,
48}
49
50/// Reference to a source document or chunk used in the answer
51#[derive(Debug, Clone)]
52pub struct SourceReference {
53    /// Identifier of the source (chunk ID, document ID, or entity ID)
54    pub id: String,
55    /// Type of source
56    pub source_type: SourceType,
57    /// Relevant excerpt from the source
58    pub excerpt: String,
59    /// Relevance score to the query
60    pub relevance_score: f32,
61}
62
63/// Type of source reference
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum SourceType {
66    /// A text chunk from a document
67    TextChunk,
68    /// An entity in the knowledge graph
69    Entity,
70    /// A relationship between entities
71    Relationship,
72    /// A document-level summary
73    Summary,
74}
75
76/// A single step in the reasoning process
77#[derive(Debug, Clone)]
78pub struct ReasoningStep {
79    /// Step number (1-indexed)
80    pub step_number: u8,
81    /// Description of what was done in this step
82    pub description: String,
83    /// IDs of entities involved in this step
84    pub entities_used: Vec<String>,
85    /// Evidence snippet that supports this step
86    pub evidence_snippet: Option<String>,
87    /// Confidence for this specific step
88    pub confidence: f32,
89}
90
91impl ExplainedAnswer {
92    /// Create a new explained answer from search results
93    pub fn from_results(answer: String, search_results: &[SearchResult], query: &str) -> Self {
94        // Calculate overall confidence from result scores
95        let confidence = if search_results.is_empty() {
96            0.0
97        } else {
98            let total_score: f32 = search_results.iter().map(|r| r.score).sum();
99            let avg_score = total_score / search_results.len() as f32;
100            // Normalize to 0-1 range (assuming scores are already somewhat normalized)
101            (avg_score * 0.7 + 0.3).clamp(0.0, 1.0)
102        };
103
104        // Build source references
105        let sources: Vec<SourceReference> = search_results
106            .iter()
107            .take(5) // Top 5 sources
108            .map(|r| SourceReference {
109                id: r.id.clone(),
110                source_type: match r.result_type {
111                    ResultType::Entity => SourceType::Entity,
112                    ResultType::Chunk => SourceType::TextChunk,
113                    ResultType::GraphPath => SourceType::Relationship,
114                    ResultType::HierarchicalSummary => SourceType::Summary,
115                    ResultType::Hybrid => SourceType::TextChunk,
116                },
117                excerpt: if r.content.len() > 200 {
118                    format!("{}...", &r.content[..200])
119                } else {
120                    r.content.clone()
121                },
122                relevance_score: r.score,
123            })
124            .collect();
125
126        // Build reasoning steps
127        let mut reasoning_steps = Vec::new();
128        let mut step_num = 1u8;
129
130        // Step 1: Query analysis
131        reasoning_steps.push(ReasoningStep {
132            step_number: step_num,
133            description: format!("Analyzed query: \"{}\"", query),
134            entities_used: vec![],
135            evidence_snippet: None,
136            confidence: 0.95,
137        });
138        step_num += 1;
139
140        // Step 2: Entity retrieval
141        let unique_entities: HashSet<_> = search_results
142            .iter()
143            .flat_map(|r| r.entities.iter().cloned())
144            .collect();
145        if !unique_entities.is_empty() {
146            reasoning_steps.push(ReasoningStep {
147                step_number: step_num,
148                description: format!("Found {} relevant entities", unique_entities.len()),
149                entities_used: unique_entities.iter().take(5).cloned().collect(),
150                evidence_snippet: None,
151                confidence: 0.85,
152            });
153            step_num += 1;
154        }
155
156        // Step 3: Chunk retrieval
157        let chunk_count = search_results
158            .iter()
159            .filter(|r| r.result_type == ResultType::Chunk || r.result_type == ResultType::Hybrid)
160            .count();
161        if chunk_count > 0 {
162            reasoning_steps.push(ReasoningStep {
163                step_number: step_num,
164                description: format!("Retrieved {} relevant text chunks", chunk_count),
165                entities_used: vec![],
166                evidence_snippet: search_results.first().map(|r| {
167                    if r.content.len() > 100 {
168                        format!("{}...", &r.content[..100])
169                    } else {
170                        r.content.clone()
171                    }
172                }),
173                confidence,
174            });
175            step_num += 1;
176        }
177
178        // Step 4: Answer synthesis
179        reasoning_steps.push(ReasoningStep {
180            step_number: step_num,
181            description: "Synthesized answer from retrieved information".to_string(),
182            entities_used: unique_entities.into_iter().take(3).collect(),
183            evidence_snippet: None,
184            confidence,
185        });
186
187        // Collect key entities
188        let key_entities: Vec<String> = search_results
189            .iter()
190            .flat_map(|r| r.entities.iter().cloned())
191            .take(10)
192            .collect();
193
194        Self {
195            answer,
196            confidence,
197            sources,
198            reasoning_steps,
199            key_entities,
200            query_analysis: None,
201        }
202    }
203
204    /// Format the explained answer for display
205    pub fn format_display(&self) -> String {
206        let mut output = String::new();
207
208        // Answer
209        output.push_str(&format!("**Answer:** {}\n\n", self.answer));
210
211        // Confidence
212        output.push_str(&format!(
213            "**Confidence:** {:.0}%\n\n",
214            self.confidence * 100.0
215        ));
216
217        // Reasoning steps
218        if !self.reasoning_steps.is_empty() {
219            output.push_str("**Reasoning:**\n");
220            for step in &self.reasoning_steps {
221                output.push_str(&format!(
222                    "{}. {} (confidence: {:.0}%)\n",
223                    step.step_number,
224                    step.description,
225                    step.confidence * 100.0
226                ));
227                if let Some(evidence) = &step.evidence_snippet {
228                    output.push_str(&format!("   Evidence: \"{}\"\n", evidence));
229                }
230            }
231            output.push('\n');
232        }
233
234        // Sources
235        if !self.sources.is_empty() {
236            output.push_str("**Sources:**\n");
237            for (i, source) in self.sources.iter().enumerate() {
238                output.push_str(&format!(
239                    "{}. [{:?}] {} (relevance: {:.0}%)\n",
240                    i + 1,
241                    source.source_type,
242                    source.id,
243                    source.relevance_score * 100.0
244                ));
245            }
246        }
247
248        output
249    }
250}