1use anyhow::Result;
11use serde::{Deserialize, Serialize};
12
13use crate::memory::{PatternRegistry, UnifiedExtractor, UnifiedExtractionResult, FocusDecision, FocusType as MemoryFocusType};
14use crate::providers::{Message, Provider};
15use crate::compress::{
16 CoherenceDetector, ConversationFocus,
17 FocusTracker, ProgressiveCompressor, FocusManager, FocusPoint, FocusStatus,
18 TopicTransition, FocusType,
19 hardcode_config::HardcodeConfig,
20};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ProcessorConfig {
25 pub coherence_threshold: f32,
27 pub focus_threshold: f32,
29 pub target_ratio: f32,
31 pub auto_learn: bool,
33 pub max_tokens_before_compression: u32,
35 pub preserve_last_n: usize,
37 pub inject_focus_message: bool,
39}
40
41impl Default for ProcessorConfig {
42 fn default() -> Self {
43 Self {
44 coherence_threshold: 0.7,
45 focus_threshold: 0.5,
46 target_ratio: 0.6,
47 auto_learn: true,
48 max_tokens_before_compression: 12000,
49 preserve_last_n: 3,
50 inject_focus_message: true,
51 }
52 }
53}
54
55impl ProcessorConfig {
56 pub fn simple_conversation() -> Self {
58 Self {
59 coherence_threshold: 0.6,
60 focus_threshold: 0.4,
61 target_ratio: 0.5,
62 auto_learn: true,
63 max_tokens_before_compression: 8000,
64 preserve_last_n: 2,
65 inject_focus_message: true,
66 }
67 }
68
69 pub fn complex_technical() -> Self {
71 Self {
72 coherence_threshold: 0.8,
73 focus_threshold: 0.6,
74 target_ratio: 0.7,
75 auto_learn: true,
76 max_tokens_before_compression: 20000,
77 preserve_last_n: 5,
78 inject_focus_message: true,
79 }
80 }
81
82 pub fn from_hardcode(hardcode: &HardcodeConfig) -> Self {
84 Self {
85 coherence_threshold: 0.7,
86 focus_threshold: 0.5,
87 target_ratio: 0.6,
88 auto_learn: true,
89 max_tokens_before_compression: 12000,
90 preserve_last_n: hardcode.max_recent_context_count,
91 inject_focus_message: true,
92 }
93 }
94
95 pub fn validate(&self) -> bool {
97 self.coherence_threshold > 0.0
98 && self.coherence_threshold <= 1.0
99 && self.focus_threshold > 0.0
100 && self.focus_threshold <= 1.0
101 && self.target_ratio > 0.0
102 && self.target_ratio <= 1.0
103 && self.max_tokens_before_compression > 0
104 && self.preserve_last_n > 0
105 }
106}
107
108pub struct IntegratedLongContextProcessor {
121 unified_extractor: UnifiedExtractor,
123 pattern_registry: PatternRegistry,
125 focus_tracker: FocusTracker,
127 focus_manager: FocusManager,
129 coherence_detector: CoherenceDetector,
131 progressive_compressor: ProgressiveCompressor,
133 config: ProcessorConfig,
135 hardcode_config: HardcodeConfig,
137}
138
139impl IntegratedLongContextProcessor {
140 pub fn new(provider: Box<dyn Provider>, model: String, config: ProcessorConfig) -> Self {
147 let hardcode_config = HardcodeConfig::default();
148
149 Self {
150 unified_extractor: UnifiedExtractor::new(provider, model),
151 pattern_registry: PatternRegistry::new(),
152 focus_tracker: FocusTracker::new(),
153 focus_manager: FocusManager::new(),
154 coherence_detector: CoherenceDetector::new_with_registry(
155 config.coherence_threshold,
156 PatternRegistry::new(),
157 ),
158 progressive_compressor: ProgressiveCompressor::default_config(),
159 config,
160 hardcode_config,
161 }
162 }
163
164 pub fn with_defaults(provider: Box<dyn Provider>, model: String) -> Self {
166 Self::new(provider, model, ProcessorConfig::default())
167 }
168
169 pub fn for_simple_conversation(provider: Box<dyn Provider>, model: String) -> Self {
171 Self::new(provider, model, ProcessorConfig::simple_conversation())
172 }
173
174 pub fn for_complex_technical(provider: Box<dyn Provider>, model: String) -> Self {
176 Self::new(provider, model, ProcessorConfig::complex_technical())
177 }
178
179 pub fn without_ai_focus(provider: Box<dyn Provider>, model: String) -> Self {
181 let hardcode_config = HardcodeConfig::default();
182
183 Self {
184 unified_extractor: UnifiedExtractor::new(provider, model),
185 pattern_registry: PatternRegistry::new(),
186 focus_tracker: FocusTracker::new(),
187 focus_manager: FocusManager::new(),
188 coherence_detector: CoherenceDetector::new_with_registry(
189 ProcessorConfig::default().coherence_threshold,
190 PatternRegistry::new(),
191 ),
192 progressive_compressor: ProgressiveCompressor::default_config(),
193 config: ProcessorConfig::default(),
194 hardcode_config,
195 }
196 }
197
198 pub fn with_hardcode_config(mut self, config: HardcodeConfig) -> Self {
200 self.hardcode_config = config.clone();
201 self.progressive_compressor = ProgressiveCompressor::default_config()
202 .with_hardcode_config(config);
203 self
204 }
205
206 pub fn config(&self) -> &ProcessorConfig {
208 &self.config
209 }
210
211 pub fn config_mut(&mut self) -> &mut ProcessorConfig {
213 &mut self.config
214 }
215
216 pub fn pattern_registry(&self) -> &PatternRegistry {
218 &self.pattern_registry
219 }
220
221 pub fn pattern_registry_mut(&mut self) -> &mut PatternRegistry {
223 &mut self.pattern_registry
224 }
225
226 pub async fn process(
245 &mut self,
246 messages: Vec<Message>,
247 session_id: Option<&str>,
248 project_path: Option<&str>,
249 ) -> Result<ProcessedResult> {
250 if messages.is_empty() {
251 return Ok(ProcessedResult {
252 messages: messages,
253 extraction: UnifiedExtractionResult::default(),
254 focus: ConversationFocus {
255 current_topic: None,
256 current_question: None,
257 recent_context: Vec::new(),
258 topic_transitions: Vec::new(),
259 detected_at: 0,
260 },
261 segments_count: 0,
262 compression_ratio: 1.0,
263 });
264 }
265
266 let existing_foci: Vec<(&str, &str, &[String])> = self.focus_manager
268 .foci
269 .iter()
270 .filter(|(_, f)| f.status == FocusStatus::Active)
271 .map(|(id, f)| {
272 (id.as_str(), f.topic.as_str(), f.keywords.as_slice())
273 })
274 .collect();
275
276 let text = self.format_messages(&messages);
278 let extraction = self.unified_extractor.extract_unified_with_foci(
279 &text,
280 &existing_foci,
281 session_id,
282 project_path,
283 ).await?;
284
285 let focus = if let Some(decision) = &extraction.focus_decision {
287 self.update_focus_from_decision(decision, messages.len().saturating_sub(1))
288 } else {
289 self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
291 self.focus_tracker.detect_focus(&messages)
292 };
293
294 self.coherence_detector = CoherenceDetector::new_with_registry(
296 self.config.coherence_threshold,
297 self.pattern_registry.clone(),
298 );
299
300 let segments = self.coherence_detector.segment_messages(&messages);
302 let segments_count = segments.len();
303
304 let compressed_messages = self.compress_segments_with_focus(segments, &focus)?;
306
307 let final_messages = if self.config.inject_focus_message {
309 self.inject_focus_message(compressed_messages, &focus)
310 } else {
311 compressed_messages
312 };
313
314 if self.config.auto_learn {
316 self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
317 if let Err(e) = self.pattern_registry.save_to_default_file() {
319 log::warn!("Failed to save patterns: {}", e);
320 }
321 }
322
323 let original_tokens = estimate_tokens(&messages);
325 let final_tokens = estimate_tokens(&final_messages);
326 let compression_ratio = if original_tokens > 0 {
327 final_tokens as f32 / original_tokens as f32
328 } else {
329 1.0
330 };
331
332 Ok(ProcessedResult {
333 messages: final_messages,
334 extraction,
335 focus,
336 segments_count,
337 compression_ratio,
338 })
339 }
340
341 fn update_focus_from_decision(
348 &mut self,
349 decision: &FocusDecision,
350 message_index: usize,
351 ) -> ConversationFocus {
352 use chrono::Utc;
353
354 if decision.need_new_focus {
356 let new_focus = FocusPoint::new_with_ai(
358 format!("focus-{}", Utc::now().timestamp()),
359 decision.new_focus_topic.clone().unwrap_or_else(|| "未知话题".to_string()),
360 decision.focus_keywords.clone(),
361 decision.related_entities.clone(),
362 decision.new_core_question.clone(),
363 None, Vec::new(), decision.confidence,
366 match decision.focus_type {
367 MemoryFocusType::ProblemSolving => FocusType::ProblemSolving,
368 MemoryFocusType::TaskExecution => FocusType::TaskExecution,
369 MemoryFocusType::KnowledgeExploration => FocusType::KnowledgeExploration,
370 MemoryFocusType::DecisionMaking => FocusType::DecisionMaking,
371 MemoryFocusType::CodeOptimization => FocusType::CodeOptimization,
372 MemoryFocusType::General => FocusType::General,
373 },
374 message_index,
375 );
376
377 self.focus_manager.add_focus(new_focus);
378
379 log::info!(
380 "Created new focus: topic={}, confidence={}, reasoning={}",
381 decision.new_focus_topic.as_ref().unwrap_or(&"unknown".to_string()),
382 decision.confidence,
383 decision.reasoning
384 );
385 } else if let Some(focus_id) = &decision.selected_focus_id {
386 self.focus_manager.switch_focus(focus_id);
388
389 if let Some(focus) = self.focus_manager.current_focus_mut() {
391 focus.add_keywords(&decision.focus_keywords);
392 focus.update_message_range(message_index);
393 focus.wake_up();
394 }
395
396 log::info!(
397 "Selected existing focus: id={}, confidence={}, reasoning={}",
398 focus_id,
399 decision.confidence,
400 decision.reasoning
401 );
402 }
403
404 if decision.is_topic_switch && decision.previous_focus_id.is_some() {
406 let prev_id = decision.previous_focus_id.clone();
408 let curr_id = self.focus_manager.current_focus_id.clone();
409
410 if let (Some(prev), Some(curr)) = (prev_id, curr_id) {
411 self.focus_manager.add_focus_transition(&prev, &curr, decision.confidence);
412 }
413 }
414
415 self.focus_manager.current_focus()
417 .map(|f| ConversationFocus {
418 current_topic: Some(f.topic.clone()),
419 current_question: f.core_question.clone(),
420 recent_context: f.keywords.iter().take(5).cloned().collect(),
421 topic_transitions: self.focus_manager.focus_history.iter()
422 .skip(1) .filter_map(|id| {
424 self.focus_manager.foci.get(id).and_then(|f| {
425 f.feedback_history.iter().rev().find(|fb| {
426 fb.feedback_type == crate::compress::FocusFeedbackType::AutoSwitched
427 }).map(|_| TopicTransition {
428 from_topic: f.topic.clone(), to_topic: f.topic.clone(),
430 message_index: f.message_range.start,
431 transition_keyword: "AI detected".to_string(),
432 })
433 })
434 })
435 .collect(),
436 detected_at: message_index,
437 })
438 .unwrap_or_else(|| ConversationFocus {
439 current_topic: decision.new_focus_topic.clone(),
440 current_question: decision.new_core_question.clone(),
441 recent_context: decision.focus_keywords.clone(),
442 topic_transitions: Vec::new(),
443 detected_at: message_index,
444 })
445 }
446
447 pub async fn process_with_provider(
451 &mut self,
452 messages: Vec<Message>,
453 provider: Option<&dyn Provider>,
454 session_id: Option<&str>,
455 project_path: Option<&str>,
456 ) -> Result<ProcessedResult> {
457 if messages.is_empty() {
458 return Ok(ProcessedResult {
459 messages: messages,
460 extraction: UnifiedExtractionResult::default(),
461 focus: ConversationFocus {
462 current_topic: None,
463 current_question: None,
464 recent_context: Vec::new(),
465 topic_transitions: Vec::new(),
466 detected_at: 0,
467 },
468 segments_count: 0,
469 compression_ratio: 1.0,
470 });
471 }
472
473 let text = self.format_messages(&messages);
475 let extraction = self.unified_extractor.extract_unified(
476 &text,
477 session_id,
478 project_path,
479 ).await?;
480
481 self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
483
484 let focus = self.focus_tracker.detect_focus(&messages);
486
487 self.coherence_detector = CoherenceDetector::new_with_registry(
489 self.config.coherence_threshold,
490 self.pattern_registry.clone(),
491 );
492 let segments = self.coherence_detector.segment_messages(&messages);
493 let segments_count = segments.len();
494
495 let current_tokens = estimate_tokens(&messages);
497 let _target_tokens = (self.config.max_tokens_before_compression as f32 * self.config.target_ratio) as u32;
498
499 let compressed_messages = if current_tokens > self.config.max_tokens_before_compression {
500 self.progressive_compressor.set_focus_manager(
502 crate::compress::focus_point::FocusManager::new()
503 );
504
505 self.progressive_compressor.compress(&messages, provider).await?
507 } else {
508 self.compress_segments_with_focus(segments, &focus)?
510 };
511
512 let final_messages = if self.config.inject_focus_message {
514 self.inject_focus_message(compressed_messages, &focus)
515 } else {
516 compressed_messages
517 };
518
519 if self.config.auto_learn {
521 self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
522 if let Err(e) = self.pattern_registry.save_to_default_file() {
523 log::warn!("Failed to save patterns: {}", e);
524 }
525 }
526
527 let final_tokens = estimate_tokens(&final_messages);
529 let compression_ratio = if current_tokens > 0 {
530 final_tokens as f32 / current_tokens as f32
531 } else {
532 1.0
533 };
534
535 Ok(ProcessedResult {
536 messages: final_messages,
537 extraction,
538 focus,
539 segments_count,
540 compression_ratio,
541 })
542 }
543
544 fn compress_segments_with_focus(
549 &self,
550 segments: Vec<Vec<Message>>,
551 focus: &ConversationFocus,
552 ) -> Result<Vec<Message>> {
553 let mut result = Vec::new();
554
555 for segment in segments {
556 let coherence_score = self.coherence_detector.calculate_coherence(&segment);
558
559 let focus_score = self.calculate_segment_focus_score(&segment, focus);
561
562 if coherence_score >= self.config.coherence_threshold && focus_score >= self.config.focus_threshold {
569 log::debug!(
571 "Segment preserved intact: coherence={}, focus={}",
572 coherence_score, focus_score
573 );
574 result.extend(segment);
575 } else if coherence_score >= self.config.coherence_threshold {
576 if segment.len() <= 3 {
579 result.extend(segment);
580 } else {
581 result.push(segment[0].clone());
583 let middle = &segment[1..segment.len() - 1];
584 let summary = self.create_segment_summary(middle);
585 if let Some(summary_msg) = summary {
586 result.push(summary_msg);
587 }
588 result.push(segment[segment.len() - 1].clone());
589 }
590 } else if focus_score >= self.config.focus_threshold {
591 for (i, msg) in segment.iter().enumerate() {
593 let msg_focus_score = self.focus_tracker.focus_score(msg, focus);
594 if msg_focus_score > self.config.focus_threshold * 0.5 || i == 0 || i == segment.len() - 1 {
595 result.push(msg.clone());
596 }
597 }
598 } else {
599 if segment.len() > 1 {
602 let summary = self.create_segment_summary(&segment);
603 if let Some(summary_msg) = summary {
604 result.push(summary_msg);
605 } else {
606 result.push(segment[segment.len() - 1].clone());
608 }
609 } else {
610 result.extend(segment);
611 }
612 }
613 }
614
615 Ok(result)
616 }
617
618 fn calculate_segment_focus_score(&self, segment: &[Message], focus: &ConversationFocus) -> f32 {
620 if segment.is_empty() {
621 return 0.0;
622 }
623
624 let mut total_score = 0.0;
625 for msg in segment {
626 total_score += self.focus_tracker.focus_score(msg, focus);
627 }
628
629 total_score / segment.len() as f32
631 }
632
633 fn create_segment_summary(&self, messages: &[Message]) -> Option<Message> {
635 if messages.is_empty() {
636 return None;
637 }
638
639 let mut key_points = Vec::new();
641 for msg in messages {
642 if let Some(point) = self.extract_key_point(msg) {
643 key_points.push(point);
644 }
645 }
646
647 if key_points.is_empty() {
648 return None;
649 }
650
651 let summary_text = if key_points.len() > 3 {
653 format!("[摘要] {} ...", key_points[..3].join(" | "))
654 } else {
655 format!("[摘要] {}", key_points.join(" | "))
656 };
657
658 Some(Message {
659 role: crate::providers::Role::Assistant,
660 content: crate::providers::MessageContent::Text(summary_text),
661 })
662 }
663
664 fn extract_key_point(&self, message: &Message) -> Option<String> {
666 let text = match &message.content {
667 crate::providers::MessageContent::Text(t) => t.clone(),
668 crate::providers::MessageContent::Blocks(blocks) => {
669 blocks.iter()
670 .filter_map(|b| {
671 if let crate::providers::ContentBlock::Text { text } = b {
672 Some(text.clone())
673 } else {
674 None
675 }
676 })
677 .collect::<Vec<_>>()
678 .join(" ")
679 }
680 };
681
682 let sentence = text
684 .split(|c| c == '.' || c == '。' || c == '\n')
685 .next()
686 .map(|s| s.trim().to_string())?;
687
688 if sentence.len() > self.hardcode_config.min_substantial_text_length {
689 Some(sentence)
690 } else {
691 None
692 }
693 }
694
695 fn inject_focus_message(&self, messages: Vec<Message>, focus: &ConversationFocus) -> Vec<Message> {
700 let focus_msg = self.focus_tracker.create_focus_message(focus);
701
702 let insert_pos = messages.iter()
704 .position(|m| !matches!(m.role, crate::providers::Role::System))
705 .unwrap_or(0);
706
707 let mut result = messages;
708 result.insert(insert_pos, focus_msg);
709
710 log::debug!("Focus message injected at position {}", insert_pos);
711 result
712 }
713
714 fn format_messages(&self, messages: &[Message]) -> String {
716 messages.iter()
717 .map(|m| {
718 let role = match m.role {
719 crate::providers::Role::User => "User",
720 crate::providers::Role::Assistant => "Assistant",
721 crate::providers::Role::System => "System",
722 crate::providers::Role::Tool => "Tool",
723 };
724 let content = match &m.content {
725 crate::providers::MessageContent::Text(t) => t.clone(),
726 crate::providers::MessageContent::Blocks(blocks) => {
727 blocks.iter()
728 .filter_map(|b| {
729 if let crate::providers::ContentBlock::Text { text } = b {
730 Some(text.clone())
731 } else {
732 None
733 }
734 })
735 .collect::<Vec<_>>()
736 .join("\n")
737 }
738 };
739 format!("{}: {}", role, content)
740 })
741 .collect::<Vec<_>>()
742 .join("\n\n")
743 }
744
745 pub fn quick_process(&mut self, messages: Vec<Message>) -> Result<ProcessedResult> {
749 if messages.is_empty() {
750 return Ok(ProcessedResult {
751 messages: messages,
752 extraction: UnifiedExtractionResult::default(),
753 focus: ConversationFocus {
754 current_topic: None,
755 current_question: None,
756 recent_context: Vec::new(),
757 topic_transitions: Vec::new(),
758 detected_at: 0,
759 },
760 segments_count: 0,
761 compression_ratio: 1.0,
762 });
763 }
764
765 let focus = self.focus_tracker.detect_focus(&messages);
767
768 self.coherence_detector = CoherenceDetector::new_with_registry(
770 self.config.coherence_threshold,
771 self.pattern_registry.clone(),
772 );
773 let segments = self.coherence_detector.segment_messages(&messages);
774 let segments_count = segments.len();
775
776 let compressed = self.compress_segments_with_focus(segments, &focus)?;
778
779 let final_messages = if self.config.inject_focus_message {
781 self.inject_focus_message(compressed, &focus)
782 } else {
783 compressed
784 };
785
786 let original_tokens = estimate_tokens(&messages);
788 let final_tokens = estimate_tokens(&final_messages);
789 let compression_ratio = if original_tokens > 0 {
790 final_tokens as f32 / original_tokens as f32
791 } else {
792 1.0
793 };
794
795 Ok(ProcessedResult {
796 messages: final_messages,
797 extraction: UnifiedExtractionResult::default(),
798 focus,
799 segments_count,
800 compression_ratio,
801 })
802 }
803}
804
805#[derive(Debug, Clone)]
807pub struct ProcessedResult {
808 pub messages: Vec<Message>,
810 pub extraction: UnifiedExtractionResult,
812 pub focus: ConversationFocus,
814 pub segments_count: usize,
816 pub compression_ratio: f32,
818}
819
820impl ProcessedResult {
821 pub fn was_compressed(&self) -> bool {
823 self.compression_ratio < 1.0
824 }
825
826 pub fn savings_percentage(&self) -> f32 {
828 (1.0 - self.compression_ratio) * 100.0
829 }
830
831 pub fn memories_count(&self) -> usize {
833 self.extraction.memories.len()
834 }
835
836 pub fn patterns_count(&self) -> usize {
838 self.extraction.conversation_patterns.len()
839 }
840}
841
842fn estimate_tokens(messages: &[Message]) -> u32 {
844 messages.iter()
845 .map(|m| {
846 let content = match &m.content {
847 crate::providers::MessageContent::Text(text) => text.clone(),
848 crate::providers::MessageContent::Blocks(blocks) => {
849 blocks.iter()
850 .filter_map(|b| {
851 if let crate::providers::ContentBlock::Text { text } = b {
852 Some(text.clone())
853 } else {
854 None
855 }
856 })
857 .collect::<Vec<_>>()
858 .join("\n")
859 }
860 };
861 (content.len() / 3) as u32 + 50 })
865 .sum()
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::providers::{Message, MessageContent, Role};
872
873 fn create_text_message(role: Role, text: &str) -> Message {
874 Message {
875 role,
876 content: MessageContent::Text(text.to_string()),
877 }
878 }
879
880 #[test]
881 fn test_processor_config_default() {
882 let config = ProcessorConfig::default();
883 assert!(config.validate());
884 assert_eq!(config.coherence_threshold, 0.7);
885 assert_eq!(config.focus_threshold, 0.5);
886 assert_eq!(config.target_ratio, 0.6);
887 }
888
889 #[test]
890 fn test_processor_config_simple() {
891 let config = ProcessorConfig::simple_conversation();
892 assert!(config.validate());
893 assert!(config.coherence_threshold < ProcessorConfig::default().coherence_threshold);
894 }
895
896 #[test]
897 fn test_processor_config_complex() {
898 let config = ProcessorConfig::complex_technical();
899 assert!(config.validate());
900 assert!(config.coherence_threshold > ProcessorConfig::default().coherence_threshold);
901 }
902
903 #[test]
904 fn test_estimate_tokens() {
905 let messages = vec![
906 create_text_message(Role::User, "This is a test message"),
907 ];
908 let tokens = estimate_tokens(&messages);
909 assert!(tokens > 0);
910 }
911
912 #[test]
913 fn test_estimate_tokens_empty() {
914 let messages: Vec<Message> = vec![];
915 let tokens = estimate_tokens(&messages);
916 assert_eq!(tokens, 0);
917 }
918
919 #[test]
920 fn test_estimate_tokens_long() {
921 let long_text = "x".repeat(1000);
922 let messages = vec![
923 create_text_message(Role::User, &long_text),
924 ];
925 let tokens = estimate_tokens(&messages);
926 assert!(tokens > 300); }
928
929 #[test]
930 fn test_processed_result_was_compressed() {
931 let result = ProcessedResult {
932 messages: vec![],
933 extraction: UnifiedExtractionResult::default(),
934 focus: ConversationFocus {
935 current_topic: None,
936 current_question: None,
937 recent_context: Vec::new(),
938 topic_transitions: Vec::new(),
939 detected_at: 0,
940 },
941 segments_count: 0,
942 compression_ratio: 0.7,
943 };
944 assert!(result.was_compressed());
945 assert!((result.savings_percentage() - 30.0).abs() < 0.01);
947 }
948
949 #[test]
950 fn test_processed_result_no_compression() {
951 let result = ProcessedResult {
952 messages: vec![],
953 extraction: UnifiedExtractionResult::default(),
954 focus: ConversationFocus {
955 current_topic: None,
956 current_question: None,
957 recent_context: Vec::new(),
958 topic_transitions: Vec::new(),
959 detected_at: 0,
960 },
961 segments_count: 0,
962 compression_ratio: 1.0,
963 };
964 assert!(!result.was_compressed());
965 assert_eq!(result.savings_percentage(), 0.0);
966 }
967
968 #[test]
969 fn test_processor_config_from_hardcode() {
970 let hardcode = HardcodeConfig::complex_technical();
971 let config = ProcessorConfig::from_hardcode(&hardcode);
972 assert!(config.validate());
973 assert_eq!(config.preserve_last_n, hardcode.max_recent_context_count);
974 }
975
976 #[test]
977 fn test_quick_process_empty() {
978 let mut processor = create_test_processor();
979 let result = processor.quick_process(vec![]).unwrap();
980 assert!(result.messages.is_empty());
981 assert_eq!(result.compression_ratio, 1.0);
982 }
983
984 #[test]
985 fn test_quick_process_single_message() {
986 let mut processor = create_test_processor();
987 let messages = vec![
988 create_text_message(Role::User, "Test message"),
989 ];
990 let result = processor.quick_process(messages).unwrap();
991 assert!(!result.messages.is_empty());
992 }
993
994 #[test]
995 fn test_calculate_segment_focus_score_empty() {
996 let processor = create_test_processor();
997 let focus = ConversationFocus {
998 current_topic: None,
999 current_question: None,
1000 recent_context: Vec::new(),
1001 topic_transitions: Vec::new(),
1002 detected_at: 0,
1003 };
1004 let score = processor.calculate_segment_focus_score(&[], &focus);
1005 assert_eq!(score, 0.0);
1006 }
1007
1008 #[test]
1009 fn test_inject_focus_message() {
1010 let processor = create_test_processor();
1011 let focus = ConversationFocus {
1012 current_topic: Some("Testing".to_string()),
1013 current_question: Some("How to test?".to_string()),
1014 recent_context: vec!["Context 1".to_string()],
1015 topic_transitions: Vec::new(),
1016 detected_at: 0,
1017 };
1018 let messages = vec![
1019 create_text_message(Role::User, "First message"),
1020 create_text_message(Role::Assistant, "Response"),
1021 ];
1022 let result = processor.inject_focus_message(messages, &focus);
1023 assert_eq!(result.len(), 3);
1024 assert!(matches!(result[0].role, Role::System));
1026 }
1027
1028 fn create_test_processor() -> IntegratedLongContextProcessor {
1029 let config = ProcessorConfig::default();
1031 let hardcode_config = HardcodeConfig::default();
1032
1033 IntegratedLongContextProcessor {
1034 unified_extractor: UnifiedExtractor::new_minimal("test-model".to_string()),
1035 pattern_registry: PatternRegistry::new(),
1036 focus_tracker: FocusTracker::new(),
1037 focus_manager: FocusManager::new(),
1038 coherence_detector: CoherenceDetector::new(config.coherence_threshold),
1039 progressive_compressor: ProgressiveCompressor::default_config(),
1040 config,
1041 hardcode_config,
1042 }
1043 }
1044}