Skip to main content

matrixcode_core/memory/
unified_extraction.rs

1//! Unified extraction result structure.
2//!
3//! This module defines the result structure for unified extraction,
4//! which captures all extracted information in a single AI call.
5
6use serde::{Deserialize, Serialize};
7
8use super::entry::MemoryEntry;
9use super::conversation_pattern::ConversationPattern;
10use crate::compress::FocusPoint;
11
12/// Result of unified extraction from conversation.
13///
14/// Contains all extracted information from a single AI call:
15/// - Long-term memories (decisions, preferences, solutions, etc.)
16/// - Current focus points (topics being discussed)
17/// - Conversation patterns (reference patterns, code patterns)
18/// - Focus keywords (transition, question, task, tech keywords)
19/// - Focus decision (AI's selection/creation of focus)
20#[derive(Debug, Clone, Default)]
21pub struct UnifiedExtractionResult {
22    /// Extracted long-term memories.
23    pub memories: Vec<MemoryEntry>,
24    /// Extracted focus points (current discussion topics).
25    pub focus_points: Vec<FocusPoint>,
26    /// Extracted conversation patterns.
27    pub conversation_patterns: Vec<ConversationPattern>,
28    /// Extracted focus keywords organized by category.
29    /// These keywords are used in real-time for focus tracking,
30    /// not persisted in the registry.
31    pub focus_keywords: ExtractedKeywords,
32
33    /// AI focus decision: which existing focus matches, or need to create new.
34    /// This is the primary output for focus tracking.
35    pub focus_decision: Option<FocusDecision>,
36}
37
38/// AI focus decision - the AI's judgment on current conversation focus.
39///
40/// Instead of extracting focus from scratch, the AI selects from existing
41/// focuses or decides to create a new one. This ensures focus continuity
42/// and proper history tracking.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct FocusDecision {
45    /// ID of the selected existing focus (if matched).
46    /// None if need_new_focus is true.
47    pub selected_focus_id: Option<String>,
48
49    /// Whether none of the existing focuses match and a new focus is needed.
50    pub need_new_focus: bool,
51
52    /// New focus topic (only if need_new_focus is true).
53    pub new_focus_topic: Option<String>,
54
55    /// Core question/task for the new focus.
56    pub new_core_question: Option<String>,
57
58    /// Confidence of the selection/creation (0.0-1.0).
59    pub confidence: f32,
60
61    /// Focus type classification.
62    pub focus_type: FocusType,
63
64    /// Whether this is a topic switch from a previous focus.
65    pub is_topic_switch: bool,
66
67    /// The previous focus being switched from (if is_topic_switch).
68    pub previous_focus_id: Option<String>,
69
70    /// Core keywords for this focus (3-5 keywords).
71    pub focus_keywords: Vec<String>,
72
73    /// Related entities (files, functions, modules).
74    pub related_entities: Vec<String>,
75
76    /// AI's reasoning for this decision.
77    pub reasoning: String,
78}
79
80/// Focus type classification.
81#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
82#[serde(rename_all = "snake_case")]
83pub enum FocusType {
84    #[default]
85    General,
86    /// Fixing bugs, resolving errors.
87    ProblemSolving,
88    /// Implementing features, completing tasks.
89    TaskExecution,
90    /// Learning, researching, exploring.
91    KnowledgeExploration,
92    /// Technical choices, architecture design.
93    DecisionMaking,
94    /// Performance optimization, refactoring.
95    CodeOptimization,
96}
97
98impl std::fmt::Display for FocusType {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            FocusType::General => write!(f, "general"),
102            FocusType::ProblemSolving => write!(f, "problem_solving"),
103            FocusType::TaskExecution => write!(f, "task_execution"),
104            FocusType::KnowledgeExploration => write!(f, "knowledge_exploration"),
105            FocusType::DecisionMaking => write!(f, "decision_making"),
106            FocusType::CodeOptimization => write!(f, "code_optimization"),
107        }
108    }
109}
110
111/// Extracted keywords organized by category.
112///
113/// These keywords are used for focus tracking and topic detection.
114/// They are passed to FocusTracker in real-time and not persisted.
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct ExtractedKeywords {
117    /// Keywords indicating topic transition/change.
118    /// Examples: "换个话题", "switching", "however"
119    pub transition: Vec<String>,
120    /// Keywords indicating questions.
121    /// Examples: "怎么", "how", "为什么", "why"
122    pub question: Vec<String>,
123    /// Keywords indicating tasks/requests.
124    /// Examples: "帮我", "implement", "创建", "create"
125    pub task: Vec<String>,
126    /// Technical/domain keywords.
127    /// Examples: "rust", "数据库", "api", "performance"
128    pub tech: Vec<String>,
129}
130
131impl ExtractedKeywords {
132    /// Create empty extracted keywords.
133    pub fn new() -> Self {
134        Self::default()
135    }
136
137    /// Check if all keyword categories are empty.
138    pub fn is_empty(&self) -> bool {
139        self.transition.is_empty()
140            && self.question.is_empty()
141            && self.task.is_empty()
142            && self.tech.is_empty()
143    }
144
145    /// Get total keyword count across all categories.
146    pub fn total_count(&self) -> usize {
147        self.transition.len() + self.question.len() + self.task.len() + self.tech.len()
148    }
149
150    /// Merge with another ExtractedKeywords, combining all categories.
151    pub fn merge(&mut self, other: &ExtractedKeywords) {
152        for kw in &other.transition {
153            if !self.transition.contains(kw) {
154                self.transition.push(kw.clone());
155            }
156        }
157        for kw in &other.question {
158            if !self.question.contains(kw) {
159                self.question.push(kw.clone());
160            }
161        }
162        for kw in &other.task {
163            if !self.task.contains(kw) {
164                self.task.push(kw.clone());
165            }
166        }
167        for kw in &other.tech {
168            if !self.tech.contains(kw) {
169                self.tech.push(kw.clone());
170            }
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_extracted_keywords_new() {
181        let keywords = ExtractedKeywords::new();
182        assert!(keywords.is_empty());
183        assert_eq!(keywords.total_count(), 0);
184    }
185
186    #[test]
187    fn test_extracted_keywords_is_empty() {
188        let empty = ExtractedKeywords::new();
189        assert!(empty.is_empty());
190
191        let non_empty = ExtractedKeywords {
192            transition: vec!["test".to_string()],
193            question: vec![],
194            task: vec![],
195            tech: vec![],
196        };
197        assert!(!non_empty.is_empty());
198    }
199
200    #[test]
201    fn test_extracted_keywords_total_count() {
202        let keywords = ExtractedKeywords {
203            transition: vec!["a".to_string(), "b".to_string()],
204            question: vec!["c".to_string()],
205            task: vec!["d".to_string(), "e".to_string(), "f".to_string()],
206            tech: vec!["g".to_string()],
207        };
208        assert_eq!(keywords.total_count(), 7);
209    }
210
211    #[test]
212    fn test_extracted_keywords_merge() {
213        let mut keywords1 = ExtractedKeywords {
214            transition: vec!["switch".to_string()],
215            question: vec!["how".to_string()],
216            task: vec!["create".to_string()],
217            tech: vec!["rust".to_string()],
218        };
219
220        let keywords2 = ExtractedKeywords {
221            transition: vec!["switch".to_string(), "new".to_string()],
222            question: vec!["why".to_string()],
223            task: vec!["create".to_string(), "delete".to_string()],
224            tech: vec!["python".to_string()],
225        };
226
227        keywords1.merge(&keywords2);
228
229        // Should have unique keywords
230        assert_eq!(keywords1.transition.len(), 2); // "switch", "new"
231        assert_eq!(keywords1.question.len(), 2); // "how", "why"
232        assert_eq!(keywords1.task.len(), 2); // "create", "delete"
233        assert_eq!(keywords1.tech.len(), 2); // "rust", "python"
234    }
235
236    #[test]
237    fn test_unified_extraction_result_default() {
238        let result = UnifiedExtractionResult::default();
239        assert!(result.memories.is_empty());
240        assert!(result.focus_points.is_empty());
241        assert!(result.conversation_patterns.is_empty());
242        assert!(result.focus_keywords.is_empty());
243        assert!(result.focus_decision.is_none());
244    }
245
246    #[test]
247    fn test_focus_decision_select_existing() {
248        let decision = FocusDecision {
249            selected_focus_id: Some("focus-1".to_string()),
250            need_new_focus: false,
251            new_focus_topic: None,
252            new_core_question: None,
253            confidence: 0.85,
254            focus_type: FocusType::CodeOptimization,
255            is_topic_switch: false,
256            previous_focus_id: None,
257            focus_keywords: vec!["API".to_string(), "performance".to_string()],
258            related_entities: vec!["api.rs".to_string()],
259            reasoning: "User is continuing API optimization discussion".to_string(),
260        };
261
262        assert!(decision.selected_focus_id.is_some());
263        assert!(!decision.need_new_focus);
264        assert_eq!(decision.confidence, 0.85);
265    }
266
267    #[test]
268    fn test_focus_decision_create_new() {
269        let decision = FocusDecision {
270            selected_focus_id: None,
271            need_new_focus: true,
272            new_focus_topic: Some("Database schema design".to_string()),
273            new_core_question: Some("How to design user table?".to_string()),
274            confidence: 0.9,
275            focus_type: FocusType::DecisionMaking,
276            is_topic_switch: true,
277            previous_focus_id: Some("focus-1".to_string()),
278            focus_keywords: vec!["database".to_string(), "schema".to_string()],
279            related_entities: vec!["user.rs".to_string()],
280            reasoning: "User switched to new database topic".to_string(),
281        };
282
283        assert!(decision.selected_focus_id.is_none());
284        assert!(decision.need_new_focus);
285        assert!(decision.new_focus_topic.is_some());
286        assert!(decision.is_topic_switch);
287    }
288
289    #[test]
290    fn test_focus_type_display() {
291        assert_eq!(FocusType::General.to_string(), "general");
292        assert_eq!(FocusType::ProblemSolving.to_string(), "problem_solving");
293        assert_eq!(FocusType::TaskExecution.to_string(), "task_execution");
294        assert_eq!(FocusType::KnowledgeExploration.to_string(), "knowledge_exploration");
295        assert_eq!(FocusType::DecisionMaking.to_string(), "decision_making");
296        assert_eq!(FocusType::CodeOptimization.to_string(), "code_optimization");
297    }
298
299    #[test]
300    fn test_focus_type_default() {
301        let focus_type = FocusType::default();
302        assert_eq!(focus_type, FocusType::General);
303    }
304
305    #[test]
306    fn test_focus_decision_serialization() {
307        let decision = FocusDecision {
308            selected_focus_id: Some("focus-1".to_string()),
309            need_new_focus: false,
310            new_focus_topic: None,
311            new_core_question: None,
312            confidence: 0.85,
313            focus_type: FocusType::CodeOptimization,
314            is_topic_switch: false,
315            previous_focus_id: None,
316            focus_keywords: vec!["API".to_string()],
317            related_entities: vec![],
318            reasoning: "test".to_string(),
319        };
320
321        // Serialize
322        let json = serde_json::to_string(&decision).unwrap();
323        assert!(json.contains("focus-1"));
324        assert!(json.contains("code_optimization"));
325
326        // Deserialize
327        let parsed: FocusDecision = serde_json::from_str(&json).unwrap();
328        assert_eq!(parsed.selected_focus_id, Some("focus-1".to_string()));
329        assert_eq!(parsed.focus_type, FocusType::CodeOptimization);
330    }
331}