post_cortex_core/summary/
mod.rs1pub mod presentation;
25
26pub use presentation::{
27 ConceptSummary, DecisionSummary, EntitySummary, QuestionSummary, SessionStats,
28 StructuredSummaryView,
29};
30
31use crate::session::active_session::ActiveSession;
32use chrono::Utc;
33
34#[derive(Debug, Clone)]
36#[derive(Default)]
37pub struct SummaryOptions {
38 pub decisions_limit: Option<usize>,
40 pub entities_limit: Option<usize>,
42 pub questions_limit: Option<usize>,
44 pub concepts_limit: Option<usize>,
46 pub min_confidence: Option<f32>,
48 pub compact: bool,
50}
51
52
53impl SummaryOptions {
54 pub fn compact() -> Self {
56 Self {
57 decisions_limit: Some(10), entities_limit: Some(15), questions_limit: Some(5),
60 concepts_limit: Some(5),
61 min_confidence: Some(0.4), compact: true,
63 }
64 }
65
66 pub fn with_limits(
68 decisions: usize,
69 entities: usize,
70 questions: usize,
71 concepts: usize,
72 ) -> Self {
73 Self {
74 decisions_limit: Some(decisions),
75 entities_limit: Some(entities),
76 questions_limit: Some(questions),
77 concepts_limit: Some(concepts),
78 min_confidence: None,
79 compact: false,
80 }
81 }
82}
83
84pub struct SummaryGenerator;
87
88impl SummaryGenerator {
89 pub fn generate_structured_summary(session: &ActiveSession) -> StructuredSummaryView {
91 Self::generate_structured_summary_filtered(session, &SummaryOptions::default())
92 }
93
94 pub fn generate_structured_summary_filtered(
96 session: &ActiveSession,
97 options: &SummaryOptions,
98 ) -> StructuredSummaryView {
99 let session_stats = Self::calculate_session_stats(session);
100
101 let entity_limit = if options.compact {
103 10
104 } else {
105 options.entities_limit.unwrap_or(20)
106 };
107
108 let mut entity_analysis = session.entity_graph.analyze_entity_importance();
110 entity_analysis.truncate(entity_limit);
111
112 let important_entities: Vec<String> = entity_analysis
114 .iter()
115 .map(|e| e.entity_name.clone())
116 .collect();
117
118 let mut key_decisions: Vec<DecisionSummary> = session
120 .current_state
121 .key_decisions
122 .iter()
123 .filter(|d| {
124 if let Some(min_conf) = options.min_confidence {
125 d.confidence >= min_conf
126 } else {
127 true
128 }
129 })
130 .map(DecisionSummary::from_decision_item)
131 .collect();
132
133 key_decisions.sort_by(|a, b| {
135 b.confidence
136 .partial_cmp(&a.confidence)
137 .unwrap_or(std::cmp::Ordering::Equal)
138 .then_with(|| b.timestamp.cmp(&a.timestamp))
139 });
140
141 if let Some(limit) = options.decisions_limit {
142 key_decisions.truncate(limit);
143 }
144
145 let mut open_questions: Vec<QuestionSummary> = session
147 .current_state
148 .open_questions
149 .iter()
150 .map(QuestionSummary::from_question_item)
151 .collect();
152
153 open_questions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
155
156 if let Some(limit) = options.questions_limit {
157 open_questions.truncate(limit);
158 }
159
160 let mut key_concepts: Vec<ConceptSummary> = session
162 .current_state
163 .key_concepts
164 .iter()
165 .map(ConceptSummary::from_concept_item)
166 .collect();
167
168 key_concepts.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
170
171 if let Some(limit) = options.concepts_limit {
172 key_concepts.truncate(limit);
173 }
174
175 StructuredSummaryView {
176 session_id: session.id(),
177 generated_at: Utc::now(),
178
179 key_decisions,
181 open_questions,
182 key_concepts,
183
184 important_entities,
186 entity_summaries: entity_analysis
187 .into_iter()
188 .map(|analysis| EntitySummary::from_entity_analysis(&analysis))
189 .collect(),
190
191 session_stats,
193 }
194 }
195
196 pub fn calculate_session_stats(session: &ActiveSession) -> SessionStats {
199 use crate::summary::presentation::SessionStatsBuilder;
200
201 SessionStatsBuilder::new(session.id(), session.created_at(), session.last_updated)
202 .with_context_sizes(
203 session.hot_context.len(),
204 session.warm_context.len(),
205 session.cold_context.len(),
206 )
207 .with_counts(
208 session.incremental_updates.len(),
209 session.entity_graph.entities.len(),
210 session.current_state.key_decisions.len(),
211 )
212 .with_references(
213 session.current_state.open_questions.len(),
214 session.current_state.key_concepts.len(),
215 session.code_references.values().map(|v| v.len()).sum(),
216 )
217 .build()
218 }
219
220 pub fn estimate_summary_size(session: &ActiveSession, max_tokens: usize) -> (usize, bool) {
223 const DECISION_AVG_TOKENS: usize = 150;
225 const ENTITY_AVG_TOKENS: usize = 80;
226 const QUESTION_AVG_TOKENS: usize = 100;
227 const CONCEPT_AVG_TOKENS: usize = 120;
228 const BASE_OVERHEAD_TOKENS: usize = 500;
229
230 let decision_count = session.current_state.key_decisions.len();
231 let entity_count = session.entity_graph.entities.len();
232 let question_count = session.current_state.open_questions.len();
233 let concept_count = session.current_state.key_concepts.len();
234
235 let estimated_tokens = BASE_OVERHEAD_TOKENS
236 + (decision_count * DECISION_AVG_TOKENS)
237 + (entity_count * ENTITY_AVG_TOKENS)
238 + (question_count * QUESTION_AVG_TOKENS)
239 + (concept_count * CONCEPT_AVG_TOKENS);
240
241 (estimated_tokens, estimated_tokens > max_tokens)
242 }
243
244 pub fn extract_key_insights(session: &ActiveSession, limit: usize) -> Vec<String> {
246 let mut insights = Vec::new();
247
248 for decision in &session.current_state.key_decisions {
250 if decision.confidence > 0.8 {
251 insights.push(format!(
252 "High-confidence decision: {}",
253 decision.description
254 ));
255 }
256 }
257
258 let top_entities = session.entity_graph.get_most_important_entities(3);
260 if !top_entities.is_empty() {
261 let entity_names: Vec<String> = top_entities.iter().map(|e| e.name.clone()).collect();
262 let entity_list = entity_names.join(", ");
263 insights.push(format!("Primary focus areas: {}", entity_list));
264 }
265
266 let total_updates = session.incremental_updates.len();
268 if total_updates > 10 {
269 insights.push(format!(
270 "Comprehensive discussion with {} updates",
271 total_updates
272 ));
273 }
274
275 let code_files: Vec<_> = session.code_references.keys().collect();
277 if code_files.len() > 1 {
278 insights.push(format!(
279 "Multi-file code analysis covering {} files",
280 code_files.len()
281 ));
282 }
283
284 insights.truncate(limit);
286 insights
287 }
288
289 pub fn extract_decision_timeline(session: &ActiveSession) -> Vec<DecisionSummary> {
291 let mut decisions: Vec<_> = session
292 .current_state
293 .key_decisions
294 .iter()
295 .map(DecisionSummary::from_decision_item)
296 .collect();
297
298 decisions.sort_by_key(|d| d.timestamp);
300 decisions
301 }
302}
303
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::session::active_session::ActiveSession;
309 use uuid::Uuid;
310
311 #[test]
312 fn test_summary_generation() {
313 let session = ActiveSession::new(
314 Uuid::new_v4(),
315 Some("Test Session".to_string()),
316 Some("Test session for summary generation".to_string()),
317 );
318
319 let summary = SummaryGenerator::generate_structured_summary(&session);
320
321 assert_eq!(summary.session_id, session.id());
322 assert!(summary.generated_at <= Utc::now());
323 assert_eq!(summary.session_stats.hot_context_size, 0); }
325
326 #[test]
327 fn test_key_insights_extraction() {
328 let session = ActiveSession::new(
329 Uuid::new_v4(),
330 Some("Test Session".to_string()),
331 Some("Test session for insights".to_string()),
332 );
333
334 let insights = SummaryGenerator::extract_key_insights(&session, 5);
335
336 assert!(insights.len() <= 5);
338 }
339}