Skip to main content

matrixcode_core/compress/
integrated_processor.rs

1//! Integrated Long Context Processor.
2//!
3//! This module integrates all long-context processing modules into a unified workflow:
4//! - UnifiedExtractor: One-time extraction of all information
5//! - AiFocusTracker: AI-driven focus detection and analysis
6//! - CoherenceDetector: Semantic coherence segmentation
7//! - ProgressiveCompressor: Segment-based compression
8//! - PatternRegistry: Pattern learning from conversations
9
10use 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/// Configuration for the integrated processor.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ProcessorConfig {
25    /// Coherence threshold (segments above this are kept intact).
26    pub coherence_threshold: f32,
27    /// Focus score threshold (segments above this are prioritized).
28    pub focus_threshold: f32,
29    /// Target token ratio after compression.
30    pub target_ratio: f32,
31    /// Whether to auto-learn patterns from conversations.
32    pub auto_learn: bool,
33    /// Maximum tokens before triggering compression.
34    pub max_tokens_before_compression: u32,
35    /// Number of recent messages to always preserve.
36    pub preserve_last_n: usize,
37    /// Whether to inject focus message into compressed output.
38    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    /// Create config for simple conversations.
57    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    /// Create config for complex technical discussions.
70    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    /// Create config from hardcode config.
83    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    /// Validate configuration.
96    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
108/// Integrated long context processor that orchestrates all modules.
109///
110/// This processor coordinates:
111/// 1. Unified extraction (one AI call for all information)
112/// 2. AI-driven focus tracking with intelligent analysis
113/// 3. Coherence-based segmentation
114/// 4. Progressive compression with focus priority
115/// 5. Focus message injection
116/// 6. Pattern learning
117pub struct IntegratedLongContextProcessor {
118    /// Unified extractor for one-time extraction.
119    unified_extractor: UnifiedExtractor,
120    /// Pattern registry for learning patterns.
121    pattern_registry: PatternRegistry,
122    /// AI-driven focus tracker for intelligent focus analysis.
123    ai_focus_tracker: Option<AiFocusTracker>,
124    /// Rule-based focus tracker (fallback when AI is not available).
125    focus_tracker: FocusTracker,
126    /// Focus manager for multi-focus tracking and selection.
127    focus_manager: FocusManager,
128    /// Coherence detector for segmentation.
129    coherence_detector: CoherenceDetector,
130    /// Progressive compressor for compression.
131    progressive_compressor: ProgressiveCompressor,
132    /// Processor configuration.
133    config: ProcessorConfig,
134    /// Hardcode configuration.
135    hardcode_config: HardcodeConfig,
136    /// Whether to use AI-driven focus tracking.
137    use_ai_focus: bool,
138}
139
140impl IntegratedLongContextProcessor {
141    /// Create a new integrated processor.
142    ///
143    /// # Arguments
144    /// * `provider` - Provider for AI extraction.
145    /// * `model` - Model name for extraction.
146    /// * `config` - Processor configuration.
147    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    /// Create with default configuration.
169    pub fn with_defaults(provider: Box<dyn Provider>, model: String) -> Self {
170        Self::new(provider, model, ProcessorConfig::default())
171    }
172
173    /// Create for simple conversations.
174    pub fn for_simple_conversation(provider: Box<dyn Provider>, model: String) -> Self {
175        Self::new(provider, model, ProcessorConfig::simple_conversation())
176    }
177
178    /// Create for complex technical discussions.
179    pub fn for_complex_technical(provider: Box<dyn Provider>, model: String) -> Self {
180        Self::new(provider, model, ProcessorConfig::complex_technical())
181    }
182
183    /// Create without AI focus tracking (rule-based only).
184    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    /// Set custom hardcode config.
205    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    /// Get configuration reference.
213    pub fn config(&self) -> &ProcessorConfig {
214        &self.config
215    }
216
217    /// Get mutable configuration reference.
218    pub fn config_mut(&mut self) -> &mut ProcessorConfig {
219        &mut self.config
220    }
221
222    /// Get pattern registry reference.
223    pub fn pattern_registry(&self) -> &PatternRegistry {
224        &self.pattern_registry
225    }
226
227    /// Get mutable pattern registry reference.
228    pub fn pattern_registry_mut(&mut self) -> &mut PatternRegistry {
229        &mut self.pattern_registry
230    }
231
232    /// Process long context with the integrated workflow.
233    ///
234    /// # Workflow
235    /// 1. Get existing focuses from FocusManager
236    /// 2. Extract all information with focus selection (one AI call)
237    /// 3. Update FocusManager based on AI's focus_decision
238    /// 4. Segment messages by coherence
239    /// 5. Compress segments with focus priority
240    /// 6. Inject focus message
241    /// 7. Learn patterns
242    ///
243    /// # Arguments
244    /// * `messages` - Messages to process.
245    /// * `session_id` - Optional session ID.
246    /// * `project_path` - Optional project path.
247    ///
248    /// # Returns
249    /// Processed messages and extraction result.
250    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        // Step 1: Get existing focuses from FocusManager for AI selection
273        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        // Step 2: One-time extraction with focus selection
283        let text = self.format_messages(&messages);
284        let extraction = if self.use_ai_focus {
285            // Use new method that includes focus selection
286            self.unified_extractor.extract_unified_with_foci(
287                &text,
288                &existing_foci,
289                session_id,
290                project_path,
291            ).await?
292        } else {
293            // Fallback: use old method without focus selection
294            self.unified_extractor.extract_unified(
295                &text,
296                session_id,
297                project_path,
298            ).await?
299        };
300
301        // Step 3: Update FocusManager based on AI's focus_decision
302        let focus = if let Some(decision) = &extraction.focus_decision {
303            self.update_focus_from_decision(decision, messages.len().saturating_sub(1))
304        } else {
305            // Fallback: rule-based focus detection
306            self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
307            self.focus_tracker.detect_focus(&messages)
308        };
309
310        // Step 4: Update coherence detector with pattern registry
311        self.coherence_detector = CoherenceDetector::new_with_registry(
312            self.config.coherence_threshold,
313            self.pattern_registry.clone(),
314        );
315
316        // Step 5: Segment messages by coherence
317        let segments = self.coherence_detector.segment_messages(&messages);
318        let segments_count = segments.len();
319
320        // Step 6: Compress segments with focus priority
321        let compressed_messages = self.compress_segments_with_focus(segments, &focus)?;
322
323        // Step 7: Inject focus message if configured
324        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        // Step 8: Learn patterns from extraction
331        if self.config.auto_learn {
332            self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
333            // Save patterns to file (optional, may fail silently)
334            if let Err(e) = self.pattern_registry.save_to_default_file() {
335                log::warn!("Failed to save patterns: {}", e);
336            }
337        }
338
339        // Calculate compression ratio
340        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    /// Update FocusManager based on AI's focus decision.
358    ///
359    /// This method handles:
360    /// - Selecting an existing focus (switch_focus)
361    /// - Creating a new focus (add_focus)
362    /// - Recording topic transitions
363    fn update_focus_from_decision(
364        &mut self,
365        decision: &FocusDecision,
366        message_index: usize,
367    ) -> ConversationFocus {
368        use chrono::Utc;
369
370        // Handle focus selection/creation
371        if decision.need_new_focus {
372            // Create new focus
373            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, // semantic_summary
380                Vec::new(), // related_files
381                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            // Switch to existing focus
403            self.focus_manager.switch_focus(focus_id);
404
405            // Update focus keywords
406            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        // Handle topic transition recording
421        if decision.is_topic_switch && decision.previous_focus_id.is_some() {
422            // Clone values before mutable borrow
423            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        // Convert FocusManager state to ConversationFocus for compression
432        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) // Skip first (initial focus)
439                    .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(), // Approximation
445                                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    /// Process with a provider reference (for progressive compressor).
464    ///
465    /// This variant allows using a provider for AI-based compression stages.
466    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        // Step 1: One-time extraction
490        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        // Step 2: Set keywords
498        self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
499
500        // Step 3: Detect focus
501        let focus = self.focus_tracker.detect_focus(&messages);
502
503        // Step 4: Segment by coherence
504        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        // Step 5: Check if compression is needed
512        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            // Compress using progressive compressor with provider
517            self.progressive_compressor.set_focus_manager(
518                crate::compress::focus_point::FocusManager::new()
519            );
520
521            // Use progressive compression
522            self.progressive_compressor.compress(&messages, provider).await?
523        } else {
524            // Just apply segment-based focus prioritization
525            self.compress_segments_with_focus(segments, &focus)?
526        };
527
528        // Step 6: Inject focus message
529        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        // Step 7: Learn patterns
536        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        // Calculate ratio
544        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    /// Compress segments while considering focus priority.
561    ///
562    /// High coherence + high focus relevance = preserve intact.
563    /// Low coherence or low focus = compress more aggressively.
564    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            // Calculate coherence score for this segment
573            let coherence_score = self.coherence_detector.calculate_coherence(&segment);
574
575            // Calculate focus score for this segment
576            let focus_score = self.calculate_segment_focus_score(&segment, focus);
577
578            // Decision logic:
579            // - High coherence (> threshold) + High focus (> threshold) = preserve intact
580            // - High coherence + Low focus = preserve but may summarize
581            // - Low coherence + High focus = keep key messages
582            // - Low coherence + Low focus = aggressive compression
583
584            if coherence_score >= self.config.coherence_threshold && focus_score >= self.config.focus_threshold {
585                // High coherence + High focus: preserve intact
586                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                // High coherence but lower focus: preserve mostly intact
593                // Keep first and last messages, summarize middle if large
594                if segment.len() <= 3 {
595                    result.extend(segment);
596                } else {
597                    // Keep first and last, compress middle
598                    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                // Low coherence but high focus: keep key messages
608                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                // Low coherence + Low focus: aggressive compression
616                // Create a single summary for the segment
617                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                        // Fallback: keep last message only
623                        result.push(segment[segment.len() - 1].clone());
624                    }
625                } else {
626                    result.extend(segment);
627                }
628            }
629        }
630
631        Ok(result)
632    }
633
634    /// Calculate focus score for a segment of messages.
635    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        // Average score weighted by segment size
646        total_score / segment.len() as f32
647    }
648
649    /// Create a summary message for a segment.
650    fn create_segment_summary(&self, messages: &[Message]) -> Option<Message> {
651        if messages.is_empty() {
652            return None;
653        }
654
655        // Simple inline summary: concatenate key points
656        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        // Limit summary length
668        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    /// Extract key point from a message.
681    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        // Extract first sentence
699        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    /// Process messages with AI-driven focus tracking.
712    ///
713    /// This method analyzes key messages using the AI focus tracker
714    /// to determine relevance and update focus intelligently.
715    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        // Initialize focus from extraction if available
725        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        // Analyze key messages (only user messages and important ones)
741        // Limit to avoid excessive API calls
742        let max_analysis = std::cmp::min(5, messages.len());
743        let key_indices: Vec<usize> = messages.iter()
744            .enumerate()
745            .filter(|(idx, msg)| {
746                // Analyze: user messages, first message, last message
747                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        // Analyze each key message
756        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            // Update focus based on analysis
766            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        // Return current focus (or fallback if none established)
776        Ok(tracker.current_focus()
777            .cloned()
778            .unwrap_or_else(|| tracker.detect_focus_fallback(messages)))
779    }
780
781    /// Inject focus message into the message list.
782    ///
783    /// The focus message is inserted after system messages to provide
784    /// context about the current conversation focus.
785    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        // Find insertion point: after system messages
789        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    /// Format messages for extraction.
801    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    /// Quick process without AI extraction (for testing or fallback).
832    ///
833    /// This skips extraction step and uses only rule-based processing.
834    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        // Detect focus without keywords (uses fallback presets)
852        let focus = self.focus_tracker.detect_focus(&messages);
853
854        // Segment by coherence
855        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        // Compress segments
863        let compressed = self.compress_segments_with_focus(segments, &focus)?;
864
865        // Inject focus message
866        let final_messages = if self.config.inject_focus_message {
867            self.inject_focus_message(compressed, &focus)
868        } else {
869            compressed
870        };
871
872        // Calculate ratio
873        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/// Result of processing long context.
892#[derive(Debug, Clone)]
893pub struct ProcessedResult {
894    /// Processed messages.
895    pub messages: Vec<Message>,
896    /// Extraction result (memories, focus points, patterns, keywords).
897    pub extraction: UnifiedExtractionResult,
898    /// Detected conversation focus.
899    pub focus: ConversationFocus,
900    /// Number of segments identified.
901    pub segments_count: usize,
902    /// Compression ratio (final / original tokens).
903    pub compression_ratio: f32,
904}
905
906impl ProcessedResult {
907    /// Check if compression was applied.
908    pub fn was_compressed(&self) -> bool {
909        self.compression_ratio < 1.0
910    }
911
912    /// Get compression savings percentage.
913    pub fn savings_percentage(&self) -> f32 {
914        (1.0 - self.compression_ratio) * 100.0
915    }
916
917    /// Get extracted memories count.
918    pub fn memories_count(&self) -> usize {
919        self.extraction.memories.len()
920    }
921
922    /// Get extracted patterns count.
923    pub fn patterns_count(&self) -> usize {
924        self.extraction.conversation_patterns.len()
925    }
926}
927
928/// Estimate tokens in messages (simple approximation).
929fn 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            // Rough estimation: 4 chars per token for English, 2 for Chinese
948            // Use average of 3 chars per token
949            (content.len() / 3) as u32 + 50 // +50 for metadata
950        })
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); // ~333 tokens for 1000 chars + 50 metadata
1013    }
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        // Use approximate comparison for floating point
1032        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        // Focus message should be first (no system messages)
1111        assert!(matches!(result[0].role, Role::System));
1112    }
1113
1114    fn create_test_processor() -> IntegratedLongContextProcessor {
1115        // Create a minimal processor for testing (without AI focus)
1116        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}