graphrag_core/retrieval/
explained.rs1use super::{ResultType, SearchResult};
8use std::collections::HashSet;
9
10#[derive(Debug, Clone)]
35pub struct ExplainedAnswer {
36 pub answer: String,
38 pub confidence: f32,
40 pub sources: Vec<SourceReference>,
42 pub reasoning_steps: Vec<ReasoningStep>,
44 pub key_entities: Vec<String>,
46 pub query_analysis: Option<super::QueryAnalysis>,
48}
49
50#[derive(Debug, Clone)]
52pub struct SourceReference {
53 pub id: String,
55 pub source_type: SourceType,
57 pub excerpt: String,
59 pub relevance_score: f32,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum SourceType {
66 TextChunk,
68 Entity,
70 Relationship,
72 Summary,
74}
75
76#[derive(Debug, Clone)]
78pub struct ReasoningStep {
79 pub step_number: u8,
81 pub description: String,
83 pub entities_used: Vec<String>,
85 pub evidence_snippet: Option<String>,
87 pub confidence: f32,
89}
90
91impl ExplainedAnswer {
92 pub fn from_results(answer: String, search_results: &[SearchResult], query: &str) -> Self {
94 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 (avg_score * 0.7 + 0.3).clamp(0.0, 1.0)
102 };
103
104 let sources: Vec<SourceReference> = search_results
106 .iter()
107 .take(5) .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 let mut reasoning_steps = Vec::new();
128 let mut step_num = 1u8;
129
130 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 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 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 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 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 pub fn format_display(&self) -> String {
206 let mut output = String::new();
207
208 output.push_str(&format!("**Answer:** {}\n\n", self.answer));
210
211 output.push_str(&format!(
213 "**Confidence:** {:.0}%\n\n",
214 self.confidence * 100.0
215 ));
216
217 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 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}