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 AiFocusTracker, 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 {
118 unified_extractor: UnifiedExtractor,
120 pattern_registry: PatternRegistry,
122 ai_focus_tracker: Option<AiFocusTracker>,
124 focus_tracker: FocusTracker,
126 focus_manager: FocusManager,
128 coherence_detector: CoherenceDetector,
130 progressive_compressor: ProgressiveCompressor,
132 config: ProcessorConfig,
134 hardcode_config: HardcodeConfig,
136 use_ai_focus: bool,
138}
139
140impl IntegratedLongContextProcessor {
141 pub fn new(provider: Box<dyn Provider>, model: String, config: ProcessorConfig) -> Self {
148 let hardcode_config = HardcodeConfig::default();
149 let ai_focus_tracker = AiFocusTracker::new(provider.clone_box(), model.clone());
150
151 Self {
152 unified_extractor: UnifiedExtractor::new(provider, model),
153 pattern_registry: PatternRegistry::new(),
154 ai_focus_tracker: Some(ai_focus_tracker),
155 focus_tracker: FocusTracker::new(),
156 focus_manager: FocusManager::new(),
157 coherence_detector: CoherenceDetector::new_with_registry(
158 config.coherence_threshold,
159 PatternRegistry::new(),
160 ),
161 progressive_compressor: ProgressiveCompressor::default_config(),
162 config,
163 hardcode_config,
164 use_ai_focus: true,
165 }
166 }
167
168 pub fn with_defaults(provider: Box<dyn Provider>, model: String) -> Self {
170 Self::new(provider, model, ProcessorConfig::default())
171 }
172
173 pub fn for_simple_conversation(provider: Box<dyn Provider>, model: String) -> Self {
175 Self::new(provider, model, ProcessorConfig::simple_conversation())
176 }
177
178 pub fn for_complex_technical(provider: Box<dyn Provider>, model: String) -> Self {
180 Self::new(provider, model, ProcessorConfig::complex_technical())
181 }
182
183 pub fn without_ai_focus(provider: Box<dyn Provider>, model: String) -> Self {
185 let hardcode_config = HardcodeConfig::default();
186
187 Self {
188 unified_extractor: UnifiedExtractor::new(provider, model),
189 pattern_registry: PatternRegistry::new(),
190 ai_focus_tracker: None,
191 focus_tracker: FocusTracker::new(),
192 focus_manager: FocusManager::new(),
193 coherence_detector: CoherenceDetector::new_with_registry(
194 ProcessorConfig::default().coherence_threshold,
195 PatternRegistry::new(),
196 ),
197 progressive_compressor: ProgressiveCompressor::default_config(),
198 config: ProcessorConfig::default(),
199 hardcode_config,
200 use_ai_focus: false,
201 }
202 }
203
204 pub fn with_hardcode_config(mut self, config: HardcodeConfig) -> Self {
206 self.hardcode_config = config.clone();
207 self.progressive_compressor = ProgressiveCompressor::default_config()
208 .with_hardcode_config(config);
209 self
210 }
211
212 pub fn config(&self) -> &ProcessorConfig {
214 &self.config
215 }
216
217 pub fn config_mut(&mut self) -> &mut ProcessorConfig {
219 &mut self.config
220 }
221
222 pub fn pattern_registry(&self) -> &PatternRegistry {
224 &self.pattern_registry
225 }
226
227 pub fn pattern_registry_mut(&mut self) -> &mut PatternRegistry {
229 &mut self.pattern_registry
230 }
231
232 pub async fn process(
251 &mut self,
252 messages: Vec<Message>,
253 session_id: Option<&str>,
254 project_path: Option<&str>,
255 ) -> Result<ProcessedResult> {
256 if messages.is_empty() {
257 return Ok(ProcessedResult {
258 messages: messages,
259 extraction: UnifiedExtractionResult::default(),
260 focus: ConversationFocus {
261 current_topic: None,
262 current_question: None,
263 recent_context: Vec::new(),
264 topic_transitions: Vec::new(),
265 detected_at: 0,
266 },
267 segments_count: 0,
268 compression_ratio: 1.0,
269 });
270 }
271
272 let existing_foci: Vec<(&str, &str, &[String])> = self.focus_manager
274 .foci
275 .iter()
276 .filter(|(_, f)| f.status == FocusStatus::Active)
277 .map(|(id, f)| {
278 (id.as_str(), f.topic.as_str(), f.keywords.as_slice())
279 })
280 .collect();
281
282 let text = self.format_messages(&messages);
284 let extraction = if self.use_ai_focus {
285 self.unified_extractor.extract_unified_with_foci(
287 &text,
288 &existing_foci,
289 session_id,
290 project_path,
291 ).await?
292 } else {
293 self.unified_extractor.extract_unified(
295 &text,
296 session_id,
297 project_path,
298 ).await?
299 };
300
301 let focus = if let Some(decision) = &extraction.focus_decision {
303 self.update_focus_from_decision(decision, messages.len().saturating_sub(1))
304 } else {
305 self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
307 self.focus_tracker.detect_focus(&messages)
308 };
309
310 self.coherence_detector = CoherenceDetector::new_with_registry(
312 self.config.coherence_threshold,
313 self.pattern_registry.clone(),
314 );
315
316 let segments = self.coherence_detector.segment_messages(&messages);
318 let segments_count = segments.len();
319
320 let compressed_messages = self.compress_segments_with_focus(segments, &focus)?;
322
323 let final_messages = if self.config.inject_focus_message {
325 self.inject_focus_message(compressed_messages, &focus)
326 } else {
327 compressed_messages
328 };
329
330 if self.config.auto_learn {
332 self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
333 if let Err(e) = self.pattern_registry.save_to_default_file() {
335 log::warn!("Failed to save patterns: {}", e);
336 }
337 }
338
339 let original_tokens = estimate_tokens(&messages);
341 let final_tokens = estimate_tokens(&final_messages);
342 let compression_ratio = if original_tokens > 0 {
343 final_tokens as f32 / original_tokens as f32
344 } else {
345 1.0
346 };
347
348 Ok(ProcessedResult {
349 messages: final_messages,
350 extraction,
351 focus,
352 segments_count,
353 compression_ratio,
354 })
355 }
356
357 fn update_focus_from_decision(
364 &mut self,
365 decision: &FocusDecision,
366 message_index: usize,
367 ) -> ConversationFocus {
368 use chrono::Utc;
369
370 if decision.need_new_focus {
372 let new_focus = FocusPoint::new_with_ai(
374 format!("focus-{}", Utc::now().timestamp()),
375 decision.new_focus_topic.clone().unwrap_or_else(|| "未知话题".to_string()),
376 decision.focus_keywords.clone(),
377 decision.related_entities.clone(),
378 decision.new_core_question.clone(),
379 None, Vec::new(), decision.confidence,
382 match decision.focus_type {
383 MemoryFocusType::ProblemSolving => FocusType::ProblemSolving,
384 MemoryFocusType::TaskExecution => FocusType::TaskExecution,
385 MemoryFocusType::KnowledgeExploration => FocusType::KnowledgeExploration,
386 MemoryFocusType::DecisionMaking => FocusType::DecisionMaking,
387 MemoryFocusType::CodeOptimization => FocusType::CodeOptimization,
388 MemoryFocusType::General => FocusType::General,
389 },
390 message_index,
391 );
392
393 self.focus_manager.add_focus(new_focus);
394
395 log::info!(
396 "Created new focus: topic={}, confidence={}, reasoning={}",
397 decision.new_focus_topic.as_ref().unwrap_or(&"unknown".to_string()),
398 decision.confidence,
399 decision.reasoning
400 );
401 } else if let Some(focus_id) = &decision.selected_focus_id {
402 self.focus_manager.switch_focus(focus_id);
404
405 if let Some(focus) = self.focus_manager.current_focus_mut() {
407 focus.add_keywords(&decision.focus_keywords);
408 focus.update_message_range(message_index);
409 focus.wake_up();
410 }
411
412 log::info!(
413 "Selected existing focus: id={}, confidence={}, reasoning={}",
414 focus_id,
415 decision.confidence,
416 decision.reasoning
417 );
418 }
419
420 if decision.is_topic_switch && decision.previous_focus_id.is_some() {
422 let prev_id = decision.previous_focus_id.clone();
424 let curr_id = self.focus_manager.current_focus_id.clone();
425
426 if let (Some(prev), Some(curr)) = (prev_id, curr_id) {
427 self.focus_manager.add_focus_transition(&prev, &curr, decision.confidence);
428 }
429 }
430
431 self.focus_manager.current_focus()
433 .map(|f| ConversationFocus {
434 current_topic: Some(f.topic.clone()),
435 current_question: f.core_question.clone(),
436 recent_context: f.keywords.iter().take(5).cloned().collect(),
437 topic_transitions: self.focus_manager.focus_history.iter()
438 .skip(1) .filter_map(|id| {
440 self.focus_manager.foci.get(id).and_then(|f| {
441 f.feedback_history.iter().rev().find(|fb| {
442 fb.feedback_type == crate::compress::FocusFeedbackType::AutoSwitched
443 }).map(|_| TopicTransition {
444 from_topic: f.topic.clone(), to_topic: f.topic.clone(),
446 message_index: f.message_range.start,
447 transition_keyword: "AI detected".to_string(),
448 })
449 })
450 })
451 .collect(),
452 detected_at: message_index,
453 })
454 .unwrap_or_else(|| ConversationFocus {
455 current_topic: decision.new_focus_topic.clone(),
456 current_question: decision.new_core_question.clone(),
457 recent_context: decision.focus_keywords.clone(),
458 topic_transitions: Vec::new(),
459 detected_at: message_index,
460 })
461 }
462
463 pub async fn process_with_provider(
467 &mut self,
468 messages: Vec<Message>,
469 provider: Option<&dyn Provider>,
470 session_id: Option<&str>,
471 project_path: Option<&str>,
472 ) -> Result<ProcessedResult> {
473 if messages.is_empty() {
474 return Ok(ProcessedResult {
475 messages: messages,
476 extraction: UnifiedExtractionResult::default(),
477 focus: ConversationFocus {
478 current_topic: None,
479 current_question: None,
480 recent_context: Vec::new(),
481 topic_transitions: Vec::new(),
482 detected_at: 0,
483 },
484 segments_count: 0,
485 compression_ratio: 1.0,
486 });
487 }
488
489 let text = self.format_messages(&messages);
491 let extraction = self.unified_extractor.extract_unified(
492 &text,
493 session_id,
494 project_path,
495 ).await?;
496
497 self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
499
500 let focus = self.focus_tracker.detect_focus(&messages);
502
503 self.coherence_detector = CoherenceDetector::new_with_registry(
505 self.config.coherence_threshold,
506 self.pattern_registry.clone(),
507 );
508 let segments = self.coherence_detector.segment_messages(&messages);
509 let segments_count = segments.len();
510
511 let current_tokens = estimate_tokens(&messages);
513 let target_tokens = (self.config.max_tokens_before_compression as f32 * self.config.target_ratio) as u32;
514
515 let compressed_messages = if current_tokens > self.config.max_tokens_before_compression {
516 self.progressive_compressor.set_focus_manager(
518 crate::compress::focus_point::FocusManager::new()
519 );
520
521 self.progressive_compressor.compress(&messages, provider).await?
523 } else {
524 self.compress_segments_with_focus(segments, &focus)?
526 };
527
528 let final_messages = if self.config.inject_focus_message {
530 self.inject_focus_message(compressed_messages, &focus)
531 } else {
532 compressed_messages
533 };
534
535 if self.config.auto_learn {
537 self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
538 if let Err(e) = self.pattern_registry.save_to_default_file() {
539 log::warn!("Failed to save patterns: {}", e);
540 }
541 }
542
543 let final_tokens = estimate_tokens(&final_messages);
545 let compression_ratio = if current_tokens > 0 {
546 final_tokens as f32 / current_tokens as f32
547 } else {
548 1.0
549 };
550
551 Ok(ProcessedResult {
552 messages: final_messages,
553 extraction,
554 focus,
555 segments_count,
556 compression_ratio,
557 })
558 }
559
560 fn compress_segments_with_focus(
565 &self,
566 segments: Vec<Vec<Message>>,
567 focus: &ConversationFocus,
568 ) -> Result<Vec<Message>> {
569 let mut result = Vec::new();
570
571 for segment in segments {
572 let coherence_score = self.coherence_detector.calculate_coherence(&segment);
574
575 let focus_score = self.calculate_segment_focus_score(&segment, focus);
577
578 if coherence_score >= self.config.coherence_threshold && focus_score >= self.config.focus_threshold {
585 log::debug!(
587 "Segment preserved intact: coherence={}, focus={}",
588 coherence_score, focus_score
589 );
590 result.extend(segment);
591 } else if coherence_score >= self.config.coherence_threshold {
592 if segment.len() <= 3 {
595 result.extend(segment);
596 } else {
597 result.push(segment[0].clone());
599 let middle = &segment[1..segment.len() - 1];
600 let summary = self.create_segment_summary(middle);
601 if let Some(summary_msg) = summary {
602 result.push(summary_msg);
603 }
604 result.push(segment[segment.len() - 1].clone());
605 }
606 } else if focus_score >= self.config.focus_threshold {
607 for (i, msg) in segment.iter().enumerate() {
609 let msg_focus_score = self.focus_tracker.focus_score(msg, focus);
610 if msg_focus_score > self.config.focus_threshold * 0.5 || i == 0 || i == segment.len() - 1 {
611 result.push(msg.clone());
612 }
613 }
614 } else {
615 if segment.len() > 1 {
618 let summary = self.create_segment_summary(&segment);
619 if let Some(summary_msg) = summary {
620 result.push(summary_msg);
621 } else {
622 result.push(segment[segment.len() - 1].clone());
624 }
625 } else {
626 result.extend(segment);
627 }
628 }
629 }
630
631 Ok(result)
632 }
633
634 fn calculate_segment_focus_score(&self, segment: &[Message], focus: &ConversationFocus) -> f32 {
636 if segment.is_empty() {
637 return 0.0;
638 }
639
640 let mut total_score = 0.0;
641 for msg in segment {
642 total_score += self.focus_tracker.focus_score(msg, focus);
643 }
644
645 total_score / segment.len() as f32
647 }
648
649 fn create_segment_summary(&self, messages: &[Message]) -> Option<Message> {
651 if messages.is_empty() {
652 return None;
653 }
654
655 let mut key_points = Vec::new();
657 for msg in messages {
658 if let Some(point) = self.extract_key_point(msg) {
659 key_points.push(point);
660 }
661 }
662
663 if key_points.is_empty() {
664 return None;
665 }
666
667 let summary_text = if key_points.len() > 3 {
669 format!("[摘要] {} ...", key_points[..3].join(" | "))
670 } else {
671 format!("[摘要] {}", key_points.join(" | "))
672 };
673
674 Some(Message {
675 role: crate::providers::Role::Assistant,
676 content: crate::providers::MessageContent::Text(summary_text),
677 })
678 }
679
680 fn extract_key_point(&self, message: &Message) -> Option<String> {
682 let text = match &message.content {
683 crate::providers::MessageContent::Text(t) => t.clone(),
684 crate::providers::MessageContent::Blocks(blocks) => {
685 blocks.iter()
686 .filter_map(|b| {
687 if let crate::providers::ContentBlock::Text { text } = b {
688 Some(text.clone())
689 } else {
690 None
691 }
692 })
693 .collect::<Vec<_>>()
694 .join(" ")
695 }
696 };
697
698 let sentence = text
700 .split(|c| c == '.' || c == '。' || c == '\n')
701 .next()
702 .map(|s| s.trim().to_string())?;
703
704 if sentence.len() > self.hardcode_config.min_substantial_text_length {
705 Some(sentence)
706 } else {
707 None
708 }
709 }
710
711 async fn process_with_ai_focus(
716 &mut self,
717 messages: &[Message],
718 extraction: &UnifiedExtractionResult,
719 ) -> Result<ConversationFocus> {
720 let tracker = self.ai_focus_tracker.as_mut().ok_or_else(|| {
721 anyhow::anyhow!("AI focus tracker not available")
722 })?;
723
724 if !extraction.focus_points.is_empty() {
726 let latest_focus = extraction.focus_points.last().unwrap();
727 let focus = ConversationFocus {
728 current_topic: Some(latest_focus.topic.clone()),
729 current_question: latest_focus.core_question.clone(),
730 recent_context: latest_focus.keywords.iter()
731 .take(self.config.preserve_last_n)
732 .cloned()
733 .collect(),
734 topic_transitions: Vec::new(),
735 detected_at: 0,
736 };
737 tracker.set_focus(focus);
738 }
739
740 let max_analysis = std::cmp::min(5, messages.len());
743 let key_indices: Vec<usize> = messages.iter()
744 .enumerate()
745 .filter(|(idx, msg)| {
746 matches!(msg.role, crate::providers::Role::User)
748 || *idx == 0
749 || *idx == messages.len() - 1
750 })
751 .map(|(idx, _)| idx)
752 .take(max_analysis)
753 .collect();
754
755 for idx in key_indices {
757 let msg = &messages[idx];
758 let result = tracker.analyze_message(msg).await?;
759
760 log::debug!(
761 "Focus analysis for message {}: relevance={}, is_update={}, reason={}",
762 idx, result.relevance, result.is_focus_update, result.reason
763 );
764
765 if result.is_focus_update {
767 log::info!(
768 "Focus updated: new_topic={}, new_question={}",
769 result.new_topic.as_ref().unwrap_or(&"none".to_string()),
770 result.new_question.as_ref().unwrap_or(&"none".to_string())
771 );
772 }
773 }
774
775 Ok(tracker.current_focus()
777 .cloned()
778 .unwrap_or_else(|| tracker.detect_focus_fallback(messages)))
779 }
780
781 fn inject_focus_message(&self, messages: Vec<Message>, focus: &ConversationFocus) -> Vec<Message> {
786 let focus_msg = self.focus_tracker.create_focus_message(focus);
787
788 let insert_pos = messages.iter()
790 .position(|m| !matches!(m.role, crate::providers::Role::System))
791 .unwrap_or(0);
792
793 let mut result = messages;
794 result.insert(insert_pos, focus_msg);
795
796 log::debug!("Focus message injected at position {}", insert_pos);
797 result
798 }
799
800 fn format_messages(&self, messages: &[Message]) -> String {
802 messages.iter()
803 .map(|m| {
804 let role = match m.role {
805 crate::providers::Role::User => "User",
806 crate::providers::Role::Assistant => "Assistant",
807 crate::providers::Role::System => "System",
808 crate::providers::Role::Tool => "Tool",
809 };
810 let content = match &m.content {
811 crate::providers::MessageContent::Text(t) => t.clone(),
812 crate::providers::MessageContent::Blocks(blocks) => {
813 blocks.iter()
814 .filter_map(|b| {
815 if let crate::providers::ContentBlock::Text { text } = b {
816 Some(text.clone())
817 } else {
818 None
819 }
820 })
821 .collect::<Vec<_>>()
822 .join("\n")
823 }
824 };
825 format!("{}: {}", role, content)
826 })
827 .collect::<Vec<_>>()
828 .join("\n\n")
829 }
830
831 pub fn quick_process(&mut self, messages: Vec<Message>) -> Result<ProcessedResult> {
835 if messages.is_empty() {
836 return Ok(ProcessedResult {
837 messages: messages,
838 extraction: UnifiedExtractionResult::default(),
839 focus: ConversationFocus {
840 current_topic: None,
841 current_question: None,
842 recent_context: Vec::new(),
843 topic_transitions: Vec::new(),
844 detected_at: 0,
845 },
846 segments_count: 0,
847 compression_ratio: 1.0,
848 });
849 }
850
851 let focus = self.focus_tracker.detect_focus(&messages);
853
854 self.coherence_detector = CoherenceDetector::new_with_registry(
856 self.config.coherence_threshold,
857 self.pattern_registry.clone(),
858 );
859 let segments = self.coherence_detector.segment_messages(&messages);
860 let segments_count = segments.len();
861
862 let compressed = self.compress_segments_with_focus(segments, &focus)?;
864
865 let final_messages = if self.config.inject_focus_message {
867 self.inject_focus_message(compressed, &focus)
868 } else {
869 compressed
870 };
871
872 let original_tokens = estimate_tokens(&messages);
874 let final_tokens = estimate_tokens(&final_messages);
875 let compression_ratio = if original_tokens > 0 {
876 final_tokens as f32 / original_tokens as f32
877 } else {
878 1.0
879 };
880
881 Ok(ProcessedResult {
882 messages: final_messages,
883 extraction: UnifiedExtractionResult::default(),
884 focus,
885 segments_count,
886 compression_ratio,
887 })
888 }
889}
890
891#[derive(Debug, Clone)]
893pub struct ProcessedResult {
894 pub messages: Vec<Message>,
896 pub extraction: UnifiedExtractionResult,
898 pub focus: ConversationFocus,
900 pub segments_count: usize,
902 pub compression_ratio: f32,
904}
905
906impl ProcessedResult {
907 pub fn was_compressed(&self) -> bool {
909 self.compression_ratio < 1.0
910 }
911
912 pub fn savings_percentage(&self) -> f32 {
914 (1.0 - self.compression_ratio) * 100.0
915 }
916
917 pub fn memories_count(&self) -> usize {
919 self.extraction.memories.len()
920 }
921
922 pub fn patterns_count(&self) -> usize {
924 self.extraction.conversation_patterns.len()
925 }
926}
927
928fn estimate_tokens(messages: &[Message]) -> u32 {
930 messages.iter()
931 .map(|m| {
932 let content = match &m.content {
933 crate::providers::MessageContent::Text(text) => text.clone(),
934 crate::providers::MessageContent::Blocks(blocks) => {
935 blocks.iter()
936 .filter_map(|b| {
937 if let crate::providers::ContentBlock::Text { text } = b {
938 Some(text.clone())
939 } else {
940 None
941 }
942 })
943 .collect::<Vec<_>>()
944 .join("\n")
945 }
946 };
947 (content.len() / 3) as u32 + 50 })
951 .sum()
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957 use crate::providers::{Message, MessageContent, Role};
958
959 fn create_text_message(role: Role, text: &str) -> Message {
960 Message {
961 role,
962 content: MessageContent::Text(text.to_string()),
963 }
964 }
965
966 #[test]
967 fn test_processor_config_default() {
968 let config = ProcessorConfig::default();
969 assert!(config.validate());
970 assert_eq!(config.coherence_threshold, 0.7);
971 assert_eq!(config.focus_threshold, 0.5);
972 assert_eq!(config.target_ratio, 0.6);
973 }
974
975 #[test]
976 fn test_processor_config_simple() {
977 let config = ProcessorConfig::simple_conversation();
978 assert!(config.validate());
979 assert!(config.coherence_threshold < ProcessorConfig::default().coherence_threshold);
980 }
981
982 #[test]
983 fn test_processor_config_complex() {
984 let config = ProcessorConfig::complex_technical();
985 assert!(config.validate());
986 assert!(config.coherence_threshold > ProcessorConfig::default().coherence_threshold);
987 }
988
989 #[test]
990 fn test_estimate_tokens() {
991 let messages = vec![
992 create_text_message(Role::User, "This is a test message"),
993 ];
994 let tokens = estimate_tokens(&messages);
995 assert!(tokens > 0);
996 }
997
998 #[test]
999 fn test_estimate_tokens_empty() {
1000 let messages: Vec<Message> = vec![];
1001 let tokens = estimate_tokens(&messages);
1002 assert_eq!(tokens, 0);
1003 }
1004
1005 #[test]
1006 fn test_estimate_tokens_long() {
1007 let long_text = "x".repeat(1000);
1008 let messages = vec![
1009 create_text_message(Role::User, &long_text),
1010 ];
1011 let tokens = estimate_tokens(&messages);
1012 assert!(tokens > 300); }
1014
1015 #[test]
1016 fn test_processed_result_was_compressed() {
1017 let result = ProcessedResult {
1018 messages: vec![],
1019 extraction: UnifiedExtractionResult::default(),
1020 focus: ConversationFocus {
1021 current_topic: None,
1022 current_question: None,
1023 recent_context: Vec::new(),
1024 topic_transitions: Vec::new(),
1025 detected_at: 0,
1026 },
1027 segments_count: 0,
1028 compression_ratio: 0.7,
1029 };
1030 assert!(result.was_compressed());
1031 assert!((result.savings_percentage() - 30.0).abs() < 0.01);
1033 }
1034
1035 #[test]
1036 fn test_processed_result_no_compression() {
1037 let result = ProcessedResult {
1038 messages: vec![],
1039 extraction: UnifiedExtractionResult::default(),
1040 focus: ConversationFocus {
1041 current_topic: None,
1042 current_question: None,
1043 recent_context: Vec::new(),
1044 topic_transitions: Vec::new(),
1045 detected_at: 0,
1046 },
1047 segments_count: 0,
1048 compression_ratio: 1.0,
1049 };
1050 assert!(!result.was_compressed());
1051 assert_eq!(result.savings_percentage(), 0.0);
1052 }
1053
1054 #[test]
1055 fn test_processor_config_from_hardcode() {
1056 let hardcode = HardcodeConfig::complex_technical();
1057 let config = ProcessorConfig::from_hardcode(&hardcode);
1058 assert!(config.validate());
1059 assert_eq!(config.preserve_last_n, hardcode.max_recent_context_count);
1060 }
1061
1062 #[test]
1063 fn test_quick_process_empty() {
1064 let mut processor = create_test_processor();
1065 let result = processor.quick_process(vec![]).unwrap();
1066 assert!(result.messages.is_empty());
1067 assert_eq!(result.compression_ratio, 1.0);
1068 }
1069
1070 #[test]
1071 fn test_quick_process_single_message() {
1072 let mut processor = create_test_processor();
1073 let messages = vec![
1074 create_text_message(Role::User, "Test message"),
1075 ];
1076 let result = processor.quick_process(messages).unwrap();
1077 assert!(!result.messages.is_empty());
1078 }
1079
1080 #[test]
1081 fn test_calculate_segment_focus_score_empty() {
1082 let processor = create_test_processor();
1083 let focus = ConversationFocus {
1084 current_topic: None,
1085 current_question: None,
1086 recent_context: Vec::new(),
1087 topic_transitions: Vec::new(),
1088 detected_at: 0,
1089 };
1090 let score = processor.calculate_segment_focus_score(&[], &focus);
1091 assert_eq!(score, 0.0);
1092 }
1093
1094 #[test]
1095 fn test_inject_focus_message() {
1096 let processor = create_test_processor();
1097 let focus = ConversationFocus {
1098 current_topic: Some("Testing".to_string()),
1099 current_question: Some("How to test?".to_string()),
1100 recent_context: vec!["Context 1".to_string()],
1101 topic_transitions: Vec::new(),
1102 detected_at: 0,
1103 };
1104 let messages = vec![
1105 create_text_message(Role::User, "First message"),
1106 create_text_message(Role::Assistant, "Response"),
1107 ];
1108 let result = processor.inject_focus_message(messages, &focus);
1109 assert_eq!(result.len(), 3);
1110 assert!(matches!(result[0].role, Role::System));
1112 }
1113
1114 fn create_test_processor() -> IntegratedLongContextProcessor {
1115 let config = ProcessorConfig::default();
1117 let hardcode_config = HardcodeConfig::default();
1118
1119 IntegratedLongContextProcessor {
1120 unified_extractor: UnifiedExtractor::new_minimal("test-model".to_string()),
1121 pattern_registry: PatternRegistry::new(),
1122 ai_focus_tracker: None,
1123 focus_tracker: FocusTracker::new(),
1124 focus_manager: FocusManager::new(),
1125 coherence_detector: CoherenceDetector::new(config.coherence_threshold),
1126 progressive_compressor: ProgressiveCompressor::default_config(),
1127 config,
1128 hardcode_config,
1129 use_ai_focus: false,
1130 }
1131 }
1132}