Skip to main content

post_cortex_core/summary/
presentation.rs

1// Copyright (c) 2025 Julius ML
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20//! Presentation-layer types for structured summaries.
21//!
22//! Defines the serialisable view models, level enums, and helper builders
23//! used to render a session's accumulated context for downstream consumers.
24use crate::core::structured_context::{ConceptItem, DecisionItem, QuestionItem, QuestionStatus};
25use crate::graph::entity_graph::EntityAnalysis;
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30/// Main structured summary view that combines all existing data
31#[derive(Serialize, Deserialize, Clone, Debug)]
32pub struct StructuredSummaryView {
33    /// Unique session identifier
34    pub session_id: Uuid,
35    /// Timestamp when this summary was generated
36    pub generated_at: DateTime<Utc>,
37
38    // From StructuredContext
39    /// Key decisions made during the session
40    pub key_decisions: Vec<DecisionSummary>,
41    /// Open (unresolved) questions tracked in the session
42    pub open_questions: Vec<QuestionSummary>,
43    /// Key concepts defined during the session
44    pub key_concepts: Vec<ConceptSummary>,
45
46    // From SimpleEntityGraph
47    /// Names of the most important entities in the session
48    pub important_entities: Vec<String>,
49    /// Detailed summaries for each important entity
50    pub entity_summaries: Vec<EntitySummary>,
51
52    // Session metadata
53    /// Aggregate session statistics
54    pub session_stats: SessionStats,
55}
56
57/// Decision summary from DecisionItem
58#[derive(Serialize, Deserialize, Clone, Debug)]
59pub struct DecisionSummary {
60    /// Human-readable description of the decision
61    pub description: String,
62    /// Context in which the decision was made
63    pub context: String,
64    /// Alternative options that were considered
65    pub alternatives: Vec<String>,
66    /// Confidence score from 0.0 to 1.0
67    pub confidence: f32,
68    /// When the decision was recorded
69    pub timestamp: DateTime<Utc>,
70    /// Discretized confidence bucket
71    pub confidence_level: ConfidenceLevel,
72}
73
74impl DecisionSummary {
75    /// Convert a raw `DecisionItem` into a presentation `DecisionSummary`.
76    pub fn from_decision_item(decision: &DecisionItem) -> Self {
77        let confidence_level = match decision.confidence {
78            f if f >= 0.8 => ConfidenceLevel::High,
79            f if f >= 0.6 => ConfidenceLevel::Medium,
80            f if f >= 0.4 => ConfidenceLevel::Low,
81            _ => ConfidenceLevel::VeryLow,
82        };
83
84        Self {
85            description: decision.description.clone(),
86            context: decision.context.clone(),
87            alternatives: decision.alternatives.clone(),
88            confidence: decision.confidence,
89            timestamp: decision.timestamp,
90            confidence_level,
91        }
92    }
93}
94
95/// Question summary from QuestionItem
96#[derive(Serialize, Deserialize, Clone, Debug)]
97pub struct QuestionSummary {
98    /// The question text
99    pub question: String,
100    /// Context surrounding the question
101    pub context: String,
102    /// Current resolution status of the question
103    pub status: QuestionStatus,
104    /// When the question was first asked
105    pub timestamp: DateTime<Utc>,
106    /// When the question was last updated
107    pub last_updated: DateTime<Utc>,
108    /// Number of days the question has been open
109    pub days_open: i64,
110    /// Computed urgency classification
111    pub urgency_level: UrgencyLevel,
112}
113
114impl QuestionSummary {
115    /// Convert a raw `QuestionItem` into a presentation `QuestionSummary`.
116    pub fn from_question_item(question: &QuestionItem) -> Self {
117        let now = Utc::now();
118        let days_open = (now - question.timestamp).num_days();
119
120        let urgency_level = match (&question.status, days_open) {
121            (QuestionStatus::Open, days) if days > 7 => UrgencyLevel::High,
122            (QuestionStatus::Open, days) if days > 3 => UrgencyLevel::Medium,
123            (QuestionStatus::Open, _) => UrgencyLevel::Low,
124            (QuestionStatus::InProgress, days) if days > 14 => UrgencyLevel::High,
125            (QuestionStatus::InProgress, _) => UrgencyLevel::Medium,
126            _ => UrgencyLevel::Low,
127        };
128
129        Self {
130            question: question.question.clone(),
131            context: question.context.clone(),
132            status: question.status.clone(),
133            timestamp: question.timestamp,
134            last_updated: question.last_updated,
135            days_open,
136            urgency_level,
137        }
138    }
139}
140
141/// Concept summary from ConceptItem
142#[derive(Serialize, Deserialize, Clone, Debug)]
143pub struct ConceptSummary {
144    /// Name of the concept
145    pub name: String,
146    /// Definition or explanation of the concept
147    pub definition: String,
148    /// Illustrative examples of the concept
149    pub examples: Vec<String>,
150    /// Names of related concepts
151    pub related_concepts: Vec<String>,
152    /// When the concept was defined
153    pub timestamp: DateTime<Utc>,
154    /// Computed complexity classification
155    pub complexity_level: ComplexityLevel,
156}
157
158impl ConceptSummary {
159    /// Convert a raw `ConceptItem` into a presentation `ConceptSummary`.
160    pub fn from_concept_item(concept: &ConceptItem) -> Self {
161        let complexity_level = match (concept.examples.len(), concept.related_concepts.len()) {
162            (examples, related) if examples > 3 && related > 5 => ComplexityLevel::High,
163            (examples, related) if examples > 1 && related > 2 => ComplexityLevel::Medium,
164            _ => ComplexityLevel::Low,
165        };
166
167        Self {
168            name: concept.name.clone(),
169            definition: concept.definition.clone(),
170            examples: concept.examples.clone(),
171            related_concepts: concept.related_concepts.clone(),
172            timestamp: concept.timestamp,
173            complexity_level,
174        }
175    }
176}
177
178/// Entity summary from EntityAnalysis
179#[derive(Serialize, Deserialize, Clone, Debug)]
180pub struct EntitySummary {
181    /// Name of the entity
182    pub entity_name: String,
183    /// Computed importance score
184    pub importance_score: f32,
185    /// Number of times the entity was mentioned
186    pub mention_count: u32,
187    /// Number of relationships connected to this entity
188    pub relationship_count: usize,
189    /// When the entity was first observed
190    pub first_seen: DateTime<Utc>,
191    /// When the entity was last observed
192    pub last_seen: DateTime<Utc>,
193    /// Discretized importance bucket
194    pub importance_level: ImportanceLevel,
195    /// How recently the entity was seen
196    pub recency_level: RecencyLevel,
197}
198
199impl EntitySummary {
200    /// Convert a raw `EntityAnalysis` into a presentation `EntitySummary`.
201    pub fn from_entity_analysis(analysis: &EntityAnalysis) -> Self {
202        let importance_level = match analysis.importance_score {
203            score if score >= 2.0 => ImportanceLevel::Critical,
204            score if score >= 1.5 => ImportanceLevel::High,
205            score if score >= 1.0 => ImportanceLevel::Medium,
206            score if score >= 0.5 => ImportanceLevel::Low,
207            _ => ImportanceLevel::Minimal,
208        };
209
210        let now = Utc::now();
211        let days_since_last_seen = (now - analysis.last_seen).num_days();
212        let recency_level = match days_since_last_seen {
213            0 => RecencyLevel::Today,
214            1..=3 => RecencyLevel::Recent,
215            4..=7 => RecencyLevel::ThisWeek,
216            8..=30 => RecencyLevel::ThisMonth,
217            _ => RecencyLevel::Old,
218        };
219
220        Self {
221            entity_name: analysis.entity_name.clone(),
222            importance_score: analysis.importance_score,
223            mention_count: analysis.mention_count,
224            relationship_count: analysis.relationship_count,
225            first_seen: analysis.first_seen,
226            last_seen: analysis.last_seen,
227            importance_level,
228            recency_level,
229        }
230    }
231}
232
233/// Session statistics from ActiveSession data
234#[derive(Serialize, Deserialize, Clone, Debug)]
235pub struct SessionStats {
236    /// Unique session identifier
237    pub session_id: Uuid,
238    /// Number of entries in the hot (recent) context tier
239    pub hot_context_size: usize,
240    /// Number of entries in the warm (compressed) context tier
241    pub warm_context_size: usize,
242    /// Number of entries in the cold (summary) context tier
243    pub cold_context_size: usize,
244    /// Total number of incremental updates recorded
245    pub total_updates: usize,
246    /// Number of distinct entities tracked
247    pub entity_count: usize,
248    /// Number of key decisions recorded
249    pub decision_count: usize,
250    /// Number of currently open questions
251    pub open_question_count: usize,
252    /// Number of concepts defined
253    pub concept_count: usize,
254    /// Number of code file references tracked
255    pub code_reference_count: usize,
256    /// When the session was created
257    pub created_at: DateTime<Utc>,
258    /// When the session was last updated
259    pub last_updated: DateTime<Utc>,
260    /// Categorized session duration
261    pub session_duration: SessionDuration,
262    /// Categorized activity intensity
263    pub activity_level: ActivityLevel,
264}
265
266/// Builder pattern for SessionStats
267pub struct SessionStatsBuilder {
268    session_id: Uuid,
269    hot_context_size: usize,
270    warm_context_size: usize,
271    cold_context_size: usize,
272    total_updates: usize,
273    entity_count: usize,
274    decision_count: usize,
275    open_question_count: usize,
276    concept_count: usize,
277    code_reference_count: usize,
278    created_at: DateTime<Utc>,
279    last_updated: DateTime<Utc>,
280}
281
282impl SessionStatsBuilder {
283    /// Create a new builder with the required identity and timestamp fields.
284    pub fn new(session_id: Uuid, created_at: DateTime<Utc>, last_updated: DateTime<Utc>) -> Self {
285        Self {
286            session_id,
287            hot_context_size: 0,
288            warm_context_size: 0,
289            cold_context_size: 0,
290            total_updates: 0,
291            entity_count: 0,
292            decision_count: 0,
293            open_question_count: 0,
294            concept_count: 0,
295            code_reference_count: 0,
296            created_at,
297            last_updated,
298        }
299    }
300
301    /// Set the hot, warm, and cold context tier sizes.
302    pub fn with_context_sizes(mut self, hot: usize, warm: usize, cold: usize) -> Self {
303        self.hot_context_size = hot;
304        self.warm_context_size = warm;
305        self.cold_context_size = cold;
306        self
307    }
308
309    /// Set total update, entity, and decision counts.
310    pub fn with_counts(mut self, updates: usize, entities: usize, decisions: usize) -> Self {
311        self.total_updates = updates;
312        self.entity_count = entities;
313        self.decision_count = decisions;
314        self
315    }
316
317    /// Set question, concept, and code reference counts.
318    pub fn with_references(mut self, questions: usize, concepts: usize, code_refs: usize) -> Self {
319        self.open_question_count = questions;
320        self.concept_count = concepts;
321        self.code_reference_count = code_refs;
322        self
323    }
324
325    /// Consume the builder and produce a `SessionStats`.
326    pub fn build(self) -> SessionStats {
327        SessionStats::from_builder(self)
328    }
329}
330
331impl SessionStats {
332    fn from_builder(builder: SessionStatsBuilder) -> Self {
333        let duration_hours = (builder.last_updated - builder.created_at).num_hours();
334        let session_duration = match duration_hours {
335            0..=1 => SessionDuration::Short,
336            2..=4 => SessionDuration::Medium,
337            5..=8 => SessionDuration::Long,
338            _ => SessionDuration::Extended,
339        };
340
341        let activity_level = match (builder.total_updates, duration_hours.max(1)) {
342            (updates, hours) if updates as i64 / hours > 10 => ActivityLevel::VeryHigh,
343            (updates, hours) if updates as i64 / hours > 5 => ActivityLevel::High,
344            (updates, hours) if updates as i64 / hours > 2 => ActivityLevel::Medium,
345            (updates, hours) if updates as i64 / hours > 0 => ActivityLevel::Low,
346            _ => ActivityLevel::Minimal,
347        };
348
349        Self {
350            session_id: builder.session_id,
351            hot_context_size: builder.hot_context_size,
352            warm_context_size: builder.warm_context_size,
353            cold_context_size: builder.cold_context_size,
354            total_updates: builder.total_updates,
355            entity_count: builder.entity_count,
356            decision_count: builder.decision_count,
357            open_question_count: builder.open_question_count,
358            concept_count: builder.concept_count,
359            code_reference_count: builder.code_reference_count,
360            created_at: builder.created_at,
361            last_updated: builder.last_updated,
362            session_duration,
363            activity_level,
364        }
365    }
366}
367
368/// Confidence level for decisions
369#[derive(Serialize, Deserialize, Clone, Debug)]
370pub enum ConfidenceLevel {
371    /// Confidence score 0.0 – 0.4
372    VeryLow,
373    /// Confidence score 0.4 – 0.6
374    Low,
375    /// Confidence score 0.6 – 0.8
376    Medium,
377    /// Confidence score 0.8 – 1.0
378    High,
379}
380
381/// Urgency level for questions
382#[derive(Serialize, Deserialize, Clone, Debug)]
383pub enum UrgencyLevel {
384    /// Not time-sensitive
385    Low,
386    /// Moderately time-sensitive
387    Medium,
388    /// Requires prompt attention
389    High,
390}
391
392/// Complexity level for concepts
393#[derive(Serialize, Deserialize, Clone, Debug)]
394pub enum ComplexityLevel {
395    /// Few examples and related concepts
396    Low,
397    /// Moderate examples and related concepts
398    Medium,
399    /// Many examples and related concepts
400    High,
401}
402
403/// Importance level for entities
404#[derive(Serialize, Deserialize, Clone, Debug)]
405pub enum ImportanceLevel {
406    /// Importance score below 0.5
407    Minimal,
408    /// Importance score 0.5 – 1.0
409    Low,
410    /// Importance score 1.0 – 1.5
411    Medium,
412    /// Importance score 1.5 – 2.0
413    High,
414    /// Importance score 2.0 or above
415    Critical,
416}
417
418/// Recency level for entities
419#[derive(Serialize, Deserialize, Clone, Debug)]
420pub enum RecencyLevel {
421    /// Seen today
422    Today,
423    /// Seen 1–3 days ago
424    Recent,
425    /// Seen 4–7 days ago
426    ThisWeek,
427    /// Seen 8–30 days ago
428    ThisMonth,
429    /// Seen more than 30 days ago
430    Old,
431}
432
433/// Session duration categorization
434#[derive(Serialize, Deserialize, Clone, Debug)]
435pub enum SessionDuration {
436    /// Session lasted 0–1 hours
437    Short,
438    /// Session lasted 2–4 hours
439    Medium,
440    /// Session lasted 5–8 hours
441    Long,
442    /// Session lasted more than 8 hours
443    Extended,
444}
445
446/// Activity level categorization
447#[derive(Serialize, Deserialize, Clone, Debug)]
448pub enum ActivityLevel {
449    /// Fewer than 1 update per hour
450    Minimal,
451    /// 1–2 updates per hour
452    Low,
453    /// 2–5 updates per hour
454    Medium,
455    /// 5–10 updates per hour
456    High,
457    /// More than 10 updates per hour
458    VeryHigh,
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_confidence_level_mapping() {
467        let decision = DecisionItem {
468            description: "Test decision".to_string(),
469            context: "Test context".to_string(),
470            alternatives: vec![],
471            confidence: 0.9,
472            timestamp: Utc::now(),
473        };
474
475        let summary = DecisionSummary::from_decision_item(&decision);
476        assert!(matches!(summary.confidence_level, ConfidenceLevel::High));
477    }
478
479    #[test]
480    fn test_urgency_level_calculation() {
481        let old_timestamp = Utc::now() - chrono::Duration::days(10);
482        let question = QuestionItem {
483            question: "Test question".to_string(),
484            context: "Test context".to_string(),
485            status: QuestionStatus::Open,
486            timestamp: old_timestamp,
487            last_updated: old_timestamp,
488        };
489
490        let summary = QuestionSummary::from_question_item(&question);
491        assert!(summary.days_open >= 10);
492        assert!(matches!(summary.urgency_level, UrgencyLevel::High));
493    }
494
495    #[test]
496    fn test_session_duration_calculation() {
497        let created = Utc::now() - chrono::Duration::hours(3);
498        let updated = Utc::now();
499
500        let stats = SessionStatsBuilder::new(Uuid::new_v4(), created, updated)
501            .with_context_sizes(10, 5, 2)
502            .with_counts(15, 20, 3)
503            .with_references(2, 5, 1)
504            .build();
505
506        assert!(matches!(stats.session_duration, SessionDuration::Medium));
507    }
508}