1use serde::{Deserialize, Serialize};
7
8use super::entry::MemoryEntry;
9use super::conversation_pattern::ConversationPattern;
10use crate::compress::FocusPoint;
11
12#[derive(Debug, Clone, Default)]
21pub struct UnifiedExtractionResult {
22 pub memories: Vec<MemoryEntry>,
24 pub focus_points: Vec<FocusPoint>,
26 pub conversation_patterns: Vec<ConversationPattern>,
28 pub focus_keywords: ExtractedKeywords,
32
33 pub focus_decision: Option<FocusDecision>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct FocusDecision {
45 pub selected_focus_id: Option<String>,
48
49 pub need_new_focus: bool,
51
52 pub new_focus_topic: Option<String>,
54
55 pub new_core_question: Option<String>,
57
58 pub confidence: f32,
60
61 pub focus_type: FocusType,
63
64 pub is_topic_switch: bool,
66
67 pub previous_focus_id: Option<String>,
69
70 pub focus_keywords: Vec<String>,
72
73 pub related_entities: Vec<String>,
75
76 pub reasoning: String,
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
82#[serde(rename_all = "snake_case")]
83pub enum FocusType {
84 #[default]
85 General,
86 ProblemSolving,
88 TaskExecution,
90 KnowledgeExploration,
92 DecisionMaking,
94 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct ExtractedKeywords {
117 pub transition: Vec<String>,
120 pub question: Vec<String>,
123 pub task: Vec<String>,
126 pub tech: Vec<String>,
129}
130
131impl ExtractedKeywords {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 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 pub fn total_count(&self) -> usize {
147 self.transition.len() + self.question.len() + self.task.len() + self.tech.len()
148 }
149
150 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 assert_eq!(keywords1.transition.len(), 2); assert_eq!(keywords1.question.len(), 2); assert_eq!(keywords1.task.len(), 2); assert_eq!(keywords1.tech.len(), 2); }
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 let json = serde_json::to_string(&decision).unwrap();
323 assert!(json.contains("focus-1"));
324 assert!(json.contains("code_optimization"));
325
326 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}