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    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. Rule-based focus tracking
113/// 3. Coherence-based segmentation
114/// 4. Progressive compression with focus priority
115/// 5. Focus message injection
116/// 6. Pattern learning
117///
118/// Note: This processor is fully implemented and tested but not yet integrated
119/// into the main agent loop. Reserved for future use.
120pub struct IntegratedLongContextProcessor {
121    /// Unified extractor for one-time extraction.
122    unified_extractor: UnifiedExtractor,
123    /// Pattern registry for learning patterns.
124    pattern_registry: PatternRegistry,
125    /// Rule-based focus tracker.
126    focus_tracker: FocusTracker,
127    /// Focus manager for multi-focus tracking and selection.
128    focus_manager: FocusManager,
129    /// Coherence detector for segmentation.
130    coherence_detector: CoherenceDetector,
131    /// Progressive compressor for compression.
132    progressive_compressor: ProgressiveCompressor,
133    /// Processor configuration.
134    config: ProcessorConfig,
135    /// Hardcode configuration.
136    hardcode_config: HardcodeConfig,
137}
138
139impl IntegratedLongContextProcessor {
140    /// Create a new integrated processor.
141    ///
142    /// # Arguments
143    /// * `provider` - Provider for AI extraction.
144    /// * `model` - Model name for extraction.
145    /// * `config` - Processor configuration.
146    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    /// Create with default configuration.
165    pub fn with_defaults(provider: Box<dyn Provider>, model: String) -> Self {
166        Self::new(provider, model, ProcessorConfig::default())
167    }
168
169    /// Create for simple conversations.
170    pub fn for_simple_conversation(provider: Box<dyn Provider>, model: String) -> Self {
171        Self::new(provider, model, ProcessorConfig::simple_conversation())
172    }
173
174    /// Create for complex technical discussions.
175    pub fn for_complex_technical(provider: Box<dyn Provider>, model: String) -> Self {
176        Self::new(provider, model, ProcessorConfig::complex_technical())
177    }
178
179    /// Create without AI focus tracking (rule-based only).
180    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    /// Set custom hardcode config.
199    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    /// Get configuration reference.
207    pub fn config(&self) -> &ProcessorConfig {
208        &self.config
209    }
210
211    /// Get mutable configuration reference.
212    pub fn config_mut(&mut self) -> &mut ProcessorConfig {
213        &mut self.config
214    }
215
216    /// Get pattern registry reference.
217    pub fn pattern_registry(&self) -> &PatternRegistry {
218        &self.pattern_registry
219    }
220
221    /// Get mutable pattern registry reference.
222    pub fn pattern_registry_mut(&mut self) -> &mut PatternRegistry {
223        &mut self.pattern_registry
224    }
225
226    /// Process long context with the integrated workflow.
227    ///
228    /// # Workflow
229    /// 1. Get existing focuses from FocusManager
230    /// 2. Extract all information with focus selection (one AI call)
231    /// 3. Update FocusManager based on AI's focus_decision
232    /// 4. Segment messages by coherence
233    /// 5. Compress segments with focus priority
234    /// 6. Inject focus message
235    /// 7. Learn patterns
236    ///
237    /// # Arguments
238    /// * `messages` - Messages to process.
239    /// * `session_id` - Optional session ID.
240    /// * `project_path` - Optional project path.
241    ///
242    /// # Returns
243    /// Processed messages and extraction result.
244    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        // Step 1: Get existing focuses from FocusManager for AI selection
267        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        // Step 2: One-time extraction with focus selection
277        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        // Step 3: Update FocusManager based on AI's focus_decision
286        let focus = if let Some(decision) = &extraction.focus_decision {
287            self.update_focus_from_decision(decision, messages.len().saturating_sub(1))
288        } else {
289            // Fallback: rule-based focus detection
290            self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
291            self.focus_tracker.detect_focus(&messages)
292        };
293
294        // Step 4: Update coherence detector with pattern registry
295        self.coherence_detector = CoherenceDetector::new_with_registry(
296            self.config.coherence_threshold,
297            self.pattern_registry.clone(),
298        );
299
300        // Step 5: Segment messages by coherence
301        let segments = self.coherence_detector.segment_messages(&messages);
302        let segments_count = segments.len();
303
304        // Step 6: Compress segments with focus priority
305        let compressed_messages = self.compress_segments_with_focus(segments, &focus)?;
306
307        // Step 7: Inject focus message if configured
308        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        // Step 8: Learn patterns from extraction
315        if self.config.auto_learn {
316            self.pattern_registry.learn_patterns(&extraction.conversation_patterns);
317            // Save patterns to file (optional, may fail silently)
318            if let Err(e) = self.pattern_registry.save_to_default_file() {
319                log::warn!("Failed to save patterns: {}", e);
320            }
321        }
322
323        // Calculate compression ratio
324        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    /// Update FocusManager based on AI's focus decision.
342    ///
343    /// This method handles:
344    /// - Selecting an existing focus (switch_focus)
345    /// - Creating a new focus (add_focus)
346    /// - Recording topic transitions
347    fn update_focus_from_decision(
348        &mut self,
349        decision: &FocusDecision,
350        message_index: usize,
351    ) -> ConversationFocus {
352        use chrono::Utc;
353
354        // Handle focus selection/creation
355        if decision.need_new_focus {
356            // Create new focus
357            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, // semantic_summary
364                Vec::new(), // related_files
365                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            // Switch to existing focus
387            self.focus_manager.switch_focus(focus_id);
388
389            // Update focus keywords
390            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        // Handle topic transition recording
405        if decision.is_topic_switch && decision.previous_focus_id.is_some() {
406            // Clone values before mutable borrow
407            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        // Convert FocusManager state to ConversationFocus for compression
416        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) // Skip first (initial focus)
423                    .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(), // Approximation
429                                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    /// Process with a provider reference (for progressive compressor).
448    ///
449    /// This variant allows using a provider for AI-based compression stages.
450    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        // Step 1: One-time extraction
474        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        // Step 2: Set keywords
482        self.focus_tracker.set_current_keywords(&extraction.focus_keywords);
483
484        // Step 3: Detect focus
485        let focus = self.focus_tracker.detect_focus(&messages);
486
487        // Step 4: Segment by coherence
488        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        // Step 5: Check if compression is needed
496        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            // Compress using progressive compressor with provider
501            self.progressive_compressor.set_focus_manager(
502                crate::compress::focus_point::FocusManager::new()
503            );
504
505            // Use progressive compression
506            self.progressive_compressor.compress(&messages, provider).await?
507        } else {
508            // Just apply segment-based focus prioritization
509            self.compress_segments_with_focus(segments, &focus)?
510        };
511
512        // Step 6: Inject focus message
513        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        // Step 7: Learn patterns
520        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        // Calculate ratio
528        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    /// Compress segments while considering focus priority.
545    ///
546    /// High coherence + high focus relevance = preserve intact.
547    /// Low coherence or low focus = compress more aggressively.
548    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            // Calculate coherence score for this segment
557            let coherence_score = self.coherence_detector.calculate_coherence(&segment);
558
559            // Calculate focus score for this segment
560            let focus_score = self.calculate_segment_focus_score(&segment, focus);
561
562            // Decision logic:
563            // - High coherence (> threshold) + High focus (> threshold) = preserve intact
564            // - High coherence + Low focus = preserve but may summarize
565            // - Low coherence + High focus = keep key messages
566            // - Low coherence + Low focus = aggressive compression
567
568            if coherence_score >= self.config.coherence_threshold && focus_score >= self.config.focus_threshold {
569                // High coherence + High focus: preserve intact
570                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                // High coherence but lower focus: preserve mostly intact
577                // Keep first and last messages, summarize middle if large
578                if segment.len() <= 3 {
579                    result.extend(segment);
580                } else {
581                    // Keep first and last, compress middle
582                    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                // Low coherence but high focus: keep key messages
592                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                // Low coherence + Low focus: aggressive compression
600                // Create a single summary for the segment
601                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                        // Fallback: keep last message only
607                        result.push(segment[segment.len() - 1].clone());
608                    }
609                } else {
610                    result.extend(segment);
611                }
612            }
613        }
614
615        Ok(result)
616    }
617
618    /// Calculate focus score for a segment of messages.
619    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        // Average score weighted by segment size
630        total_score / segment.len() as f32
631    }
632
633    /// Create a summary message for a segment.
634    fn create_segment_summary(&self, messages: &[Message]) -> Option<Message> {
635        if messages.is_empty() {
636            return None;
637        }
638
639        // Simple inline summary: concatenate key points
640        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        // Limit summary length
652        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    /// Extract key point from a message.
665    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        // Extract first sentence
683        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    /// Inject focus message into the message list.
696    ///
697    /// The focus message is inserted after system messages to provide
698    /// context about the current conversation focus.
699    /// If a focus message already exists, it will be replaced (not duplicated).
700    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        // Check if a focus message already exists (System message containing "焦点" or "Focus")
704        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            // Replace existing focus message
721            result[pos] = focus_msg;
722            log::debug!("Focus message replaced at position {}", pos);
723        } else {
724            // Insert new focus message after system messages
725            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    /// Format messages for extraction.
737    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    /// Quick process without AI extraction (for testing or fallback).
768    ///
769    /// This skips extraction step and uses only rule-based processing.
770    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        // Detect focus without keywords (uses fallback presets)
788        let focus = self.focus_tracker.detect_focus(&messages);
789
790        // Segment by coherence
791        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        // Compress segments
799        let compressed = self.compress_segments_with_focus(segments, &focus)?;
800
801        // Inject focus message
802        let final_messages = if self.config.inject_focus_message {
803            self.inject_focus_message(compressed, &focus)
804        } else {
805            compressed
806        };
807
808        // Calculate ratio
809        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/// Result of processing long context.
828#[derive(Debug, Clone)]
829pub struct ProcessedResult {
830    /// Processed messages.
831    pub messages: Vec<Message>,
832    /// Extraction result (memories, focus points, patterns, keywords).
833    pub extraction: UnifiedExtractionResult,
834    /// Detected conversation focus.
835    pub focus: ConversationFocus,
836    /// Number of segments identified.
837    pub segments_count: usize,
838    /// Compression ratio (final / original tokens).
839    pub compression_ratio: f32,
840}
841
842impl ProcessedResult {
843    /// Check if compression was applied.
844    pub fn was_compressed(&self) -> bool {
845        self.compression_ratio < 1.0
846    }
847
848    /// Get compression savings percentage.
849    pub fn savings_percentage(&self) -> f32 {
850        (1.0 - self.compression_ratio) * 100.0
851    }
852
853    /// Get extracted memories count.
854    pub fn memories_count(&self) -> usize {
855        self.extraction.memories.len()
856    }
857
858    /// Get extracted patterns count.
859    pub fn patterns_count(&self) -> usize {
860        self.extraction.conversation_patterns.len()
861    }
862}
863
864/// Estimate tokens in messages (simple approximation).
865fn 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            // Rough estimation: 4 chars per token for English, 2 for Chinese
884            // Use average of 3 chars per token
885            (content.len() / 3) as u32 + 50 // +50 for metadata
886        })
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); // ~333 tokens for 1000 chars + 50 metadata
949    }
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        // Use approximate comparison for floating point
968        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        // Focus message should be first (no system messages)
1047        assert!(matches!(result[0].role, Role::System));
1048    }
1049
1050    fn create_test_processor() -> IntegratedLongContextProcessor {
1051        // Create a minimal processor for testing
1052        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}