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