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> {
701 let focus_msg = self.focus_tracker.create_focus_message(focus);
702
703 let existing_focus_pos = messages.iter().position(|m| {
705 if matches!(m.role, crate::providers::Role::System) {
706 match &m.content {
707 crate::providers::MessageContent::Text(t) => {
708 t.contains("焦点") || t.contains("Focus") || t.contains("【焦点上下文】")
709 }
710 _ => false
711 }
712 } else {
713 false
714 }
715 });
716
717 let mut result = messages;
718
719 if let Some(pos) = existing_focus_pos {
720 result[pos] = focus_msg;
722 log::debug!("Focus message replaced at position {}", pos);
723 } else {
724 let insert_pos = result.iter()
726 .position(|m| !matches!(m.role, crate::providers::Role::System))
727 .unwrap_or(0);
728
729 result.insert(insert_pos, focus_msg);
730 log::debug!("Focus message injected at position {}", insert_pos);
731 }
732
733 result
734 }
735
736 fn format_messages(&self, messages: &[Message]) -> String {
738 messages.iter()
739 .map(|m| {
740 let role = match m.role {
741 crate::providers::Role::User => "User",
742 crate::providers::Role::Assistant => "Assistant",
743 crate::providers::Role::System => "System",
744 crate::providers::Role::Tool => "Tool",
745 };
746 let content = match &m.content {
747 crate::providers::MessageContent::Text(t) => t.clone(),
748 crate::providers::MessageContent::Blocks(blocks) => {
749 blocks.iter()
750 .filter_map(|b| {
751 if let crate::providers::ContentBlock::Text { text } = b {
752 Some(text.clone())
753 } else {
754 None
755 }
756 })
757 .collect::<Vec<_>>()
758 .join("\n")
759 }
760 };
761 format!("{}: {}", role, content)
762 })
763 .collect::<Vec<_>>()
764 .join("\n\n")
765 }
766
767 pub fn quick_process(&mut self, messages: Vec<Message>) -> Result<ProcessedResult> {
771 if messages.is_empty() {
772 return Ok(ProcessedResult {
773 messages: messages,
774 extraction: UnifiedExtractionResult::default(),
775 focus: ConversationFocus {
776 current_topic: None,
777 current_question: None,
778 recent_context: Vec::new(),
779 topic_transitions: Vec::new(),
780 detected_at: 0,
781 },
782 segments_count: 0,
783 compression_ratio: 1.0,
784 });
785 }
786
787 let focus = self.focus_tracker.detect_focus(&messages);
789
790 self.coherence_detector = CoherenceDetector::new_with_registry(
792 self.config.coherence_threshold,
793 self.pattern_registry.clone(),
794 );
795 let segments = self.coherence_detector.segment_messages(&messages);
796 let segments_count = segments.len();
797
798 let compressed = self.compress_segments_with_focus(segments, &focus)?;
800
801 let final_messages = if self.config.inject_focus_message {
803 self.inject_focus_message(compressed, &focus)
804 } else {
805 compressed
806 };
807
808 let original_tokens = estimate_tokens(&messages);
810 let final_tokens = estimate_tokens(&final_messages);
811 let compression_ratio = if original_tokens > 0 {
812 final_tokens as f32 / original_tokens as f32
813 } else {
814 1.0
815 };
816
817 Ok(ProcessedResult {
818 messages: final_messages,
819 extraction: UnifiedExtractionResult::default(),
820 focus,
821 segments_count,
822 compression_ratio,
823 })
824 }
825}
826
827#[derive(Debug, Clone)]
829pub struct ProcessedResult {
830 pub messages: Vec<Message>,
832 pub extraction: UnifiedExtractionResult,
834 pub focus: ConversationFocus,
836 pub segments_count: usize,
838 pub compression_ratio: f32,
840}
841
842impl ProcessedResult {
843 pub fn was_compressed(&self) -> bool {
845 self.compression_ratio < 1.0
846 }
847
848 pub fn savings_percentage(&self) -> f32 {
850 (1.0 - self.compression_ratio) * 100.0
851 }
852
853 pub fn memories_count(&self) -> usize {
855 self.extraction.memories.len()
856 }
857
858 pub fn patterns_count(&self) -> usize {
860 self.extraction.conversation_patterns.len()
861 }
862}
863
864fn estimate_tokens(messages: &[Message]) -> u32 {
866 messages.iter()
867 .map(|m| {
868 let content = match &m.content {
869 crate::providers::MessageContent::Text(text) => text.clone(),
870 crate::providers::MessageContent::Blocks(blocks) => {
871 blocks.iter()
872 .filter_map(|b| {
873 if let crate::providers::ContentBlock::Text { text } = b {
874 Some(text.clone())
875 } else {
876 None
877 }
878 })
879 .collect::<Vec<_>>()
880 .join("\n")
881 }
882 };
883 (content.len() / 3) as u32 + 50 })
887 .sum()
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use crate::providers::{Message, MessageContent, Role};
894
895 fn create_text_message(role: Role, text: &str) -> Message {
896 Message {
897 role,
898 content: MessageContent::Text(text.to_string()),
899 }
900 }
901
902 #[test]
903 fn test_processor_config_default() {
904 let config = ProcessorConfig::default();
905 assert!(config.validate());
906 assert_eq!(config.coherence_threshold, 0.7);
907 assert_eq!(config.focus_threshold, 0.5);
908 assert_eq!(config.target_ratio, 0.6);
909 }
910
911 #[test]
912 fn test_processor_config_simple() {
913 let config = ProcessorConfig::simple_conversation();
914 assert!(config.validate());
915 assert!(config.coherence_threshold < ProcessorConfig::default().coherence_threshold);
916 }
917
918 #[test]
919 fn test_processor_config_complex() {
920 let config = ProcessorConfig::complex_technical();
921 assert!(config.validate());
922 assert!(config.coherence_threshold > ProcessorConfig::default().coherence_threshold);
923 }
924
925 #[test]
926 fn test_estimate_tokens() {
927 let messages = vec![
928 create_text_message(Role::User, "This is a test message"),
929 ];
930 let tokens = estimate_tokens(&messages);
931 assert!(tokens > 0);
932 }
933
934 #[test]
935 fn test_estimate_tokens_empty() {
936 let messages: Vec<Message> = vec![];
937 let tokens = estimate_tokens(&messages);
938 assert_eq!(tokens, 0);
939 }
940
941 #[test]
942 fn test_estimate_tokens_long() {
943 let long_text = "x".repeat(1000);
944 let messages = vec![
945 create_text_message(Role::User, &long_text),
946 ];
947 let tokens = estimate_tokens(&messages);
948 assert!(tokens > 300); }
950
951 #[test]
952 fn test_processed_result_was_compressed() {
953 let result = ProcessedResult {
954 messages: vec![],
955 extraction: UnifiedExtractionResult::default(),
956 focus: ConversationFocus {
957 current_topic: None,
958 current_question: None,
959 recent_context: Vec::new(),
960 topic_transitions: Vec::new(),
961 detected_at: 0,
962 },
963 segments_count: 0,
964 compression_ratio: 0.7,
965 };
966 assert!(result.was_compressed());
967 assert!((result.savings_percentage() - 30.0).abs() < 0.01);
969 }
970
971 #[test]
972 fn test_processed_result_no_compression() {
973 let result = ProcessedResult {
974 messages: vec![],
975 extraction: UnifiedExtractionResult::default(),
976 focus: ConversationFocus {
977 current_topic: None,
978 current_question: None,
979 recent_context: Vec::new(),
980 topic_transitions: Vec::new(),
981 detected_at: 0,
982 },
983 segments_count: 0,
984 compression_ratio: 1.0,
985 };
986 assert!(!result.was_compressed());
987 assert_eq!(result.savings_percentage(), 0.0);
988 }
989
990 #[test]
991 fn test_processor_config_from_hardcode() {
992 let hardcode = HardcodeConfig::complex_technical();
993 let config = ProcessorConfig::from_hardcode(&hardcode);
994 assert!(config.validate());
995 assert_eq!(config.preserve_last_n, hardcode.max_recent_context_count);
996 }
997
998 #[test]
999 fn test_quick_process_empty() {
1000 let mut processor = create_test_processor();
1001 let result = processor.quick_process(vec![]).unwrap();
1002 assert!(result.messages.is_empty());
1003 assert_eq!(result.compression_ratio, 1.0);
1004 }
1005
1006 #[test]
1007 fn test_quick_process_single_message() {
1008 let mut processor = create_test_processor();
1009 let messages = vec![
1010 create_text_message(Role::User, "Test message"),
1011 ];
1012 let result = processor.quick_process(messages).unwrap();
1013 assert!(!result.messages.is_empty());
1014 }
1015
1016 #[test]
1017 fn test_calculate_segment_focus_score_empty() {
1018 let processor = create_test_processor();
1019 let focus = ConversationFocus {
1020 current_topic: None,
1021 current_question: None,
1022 recent_context: Vec::new(),
1023 topic_transitions: Vec::new(),
1024 detected_at: 0,
1025 };
1026 let score = processor.calculate_segment_focus_score(&[], &focus);
1027 assert_eq!(score, 0.0);
1028 }
1029
1030 #[test]
1031 fn test_inject_focus_message() {
1032 let processor = create_test_processor();
1033 let focus = ConversationFocus {
1034 current_topic: Some("Testing".to_string()),
1035 current_question: Some("How to test?".to_string()),
1036 recent_context: vec!["Context 1".to_string()],
1037 topic_transitions: Vec::new(),
1038 detected_at: 0,
1039 };
1040 let messages = vec![
1041 create_text_message(Role::User, "First message"),
1042 create_text_message(Role::Assistant, "Response"),
1043 ];
1044 let result = processor.inject_focus_message(messages, &focus);
1045 assert_eq!(result.len(), 3);
1046 assert!(matches!(result[0].role, Role::System));
1048 }
1049
1050 fn create_test_processor() -> IntegratedLongContextProcessor {
1051 let config = ProcessorConfig::default();
1053 let hardcode_config = HardcodeConfig::default();
1054
1055 IntegratedLongContextProcessor {
1056 unified_extractor: UnifiedExtractor::new_minimal("test-model".to_string()),
1057 pattern_registry: PatternRegistry::new(),
1058 focus_tracker: FocusTracker::new(),
1059 focus_manager: FocusManager::new(),
1060 coherence_detector: CoherenceDetector::new(config.coherence_threshold),
1061 progressive_compressor: ProgressiveCompressor::default_config(),
1062 config,
1063 hardcode_config,
1064 }
1065 }
1066}