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    fn inject_focus_message(&self, messages: Vec<Message>, focus: &ConversationFocus) -> Vec<Message> {
700        let focus_msg = self.focus_tracker.create_focus_message(focus);
701
702        // Find insertion point: after system messages
703        let insert_pos = messages.iter()
704            .position(|m| !matches!(m.role, crate::providers::Role::System))
705            .unwrap_or(0);
706
707        let mut result = messages;
708        result.insert(insert_pos, focus_msg);
709
710        log::debug!("Focus message injected at position {}", insert_pos);
711        result
712    }
713
714    /// Format messages for extraction.
715    fn format_messages(&self, messages: &[Message]) -> String {
716        messages.iter()
717            .map(|m| {
718                let role = match m.role {
719                    crate::providers::Role::User => "User",
720                    crate::providers::Role::Assistant => "Assistant",
721                    crate::providers::Role::System => "System",
722                    crate::providers::Role::Tool => "Tool",
723                };
724                let content = match &m.content {
725                    crate::providers::MessageContent::Text(t) => t.clone(),
726                    crate::providers::MessageContent::Blocks(blocks) => {
727                        blocks.iter()
728                            .filter_map(|b| {
729                                if let crate::providers::ContentBlock::Text { text } = b {
730                                    Some(text.clone())
731                                } else {
732                                    None
733                                }
734                            })
735                            .collect::<Vec<_>>()
736                            .join("\n")
737                    }
738                };
739                format!("{}: {}", role, content)
740            })
741            .collect::<Vec<_>>()
742            .join("\n\n")
743    }
744
745    /// Quick process without AI extraction (for testing or fallback).
746    ///
747    /// This skips extraction step and uses only rule-based processing.
748    pub fn quick_process(&mut self, messages: Vec<Message>) -> Result<ProcessedResult> {
749        if messages.is_empty() {
750            return Ok(ProcessedResult {
751                messages: messages,
752                extraction: UnifiedExtractionResult::default(),
753                focus: ConversationFocus {
754                    current_topic: None,
755                    current_question: None,
756                    recent_context: Vec::new(),
757                    topic_transitions: Vec::new(),
758                    detected_at: 0,
759                },
760                segments_count: 0,
761                compression_ratio: 1.0,
762            });
763        }
764
765        // Detect focus without keywords (uses fallback presets)
766        let focus = self.focus_tracker.detect_focus(&messages);
767
768        // Segment by coherence
769        self.coherence_detector = CoherenceDetector::new_with_registry(
770            self.config.coherence_threshold,
771            self.pattern_registry.clone(),
772        );
773        let segments = self.coherence_detector.segment_messages(&messages);
774        let segments_count = segments.len();
775
776        // Compress segments
777        let compressed = self.compress_segments_with_focus(segments, &focus)?;
778
779        // Inject focus message
780        let final_messages = if self.config.inject_focus_message {
781            self.inject_focus_message(compressed, &focus)
782        } else {
783            compressed
784        };
785
786        // Calculate ratio
787        let original_tokens = estimate_tokens(&messages);
788        let final_tokens = estimate_tokens(&final_messages);
789        let compression_ratio = if original_tokens > 0 {
790            final_tokens as f32 / original_tokens as f32
791        } else {
792            1.0
793        };
794
795        Ok(ProcessedResult {
796            messages: final_messages,
797            extraction: UnifiedExtractionResult::default(),
798            focus,
799            segments_count,
800            compression_ratio,
801        })
802    }
803}
804
805/// Result of processing long context.
806#[derive(Debug, Clone)]
807pub struct ProcessedResult {
808    /// Processed messages.
809    pub messages: Vec<Message>,
810    /// Extraction result (memories, focus points, patterns, keywords).
811    pub extraction: UnifiedExtractionResult,
812    /// Detected conversation focus.
813    pub focus: ConversationFocus,
814    /// Number of segments identified.
815    pub segments_count: usize,
816    /// Compression ratio (final / original tokens).
817    pub compression_ratio: f32,
818}
819
820impl ProcessedResult {
821    /// Check if compression was applied.
822    pub fn was_compressed(&self) -> bool {
823        self.compression_ratio < 1.0
824    }
825
826    /// Get compression savings percentage.
827    pub fn savings_percentage(&self) -> f32 {
828        (1.0 - self.compression_ratio) * 100.0
829    }
830
831    /// Get extracted memories count.
832    pub fn memories_count(&self) -> usize {
833        self.extraction.memories.len()
834    }
835
836    /// Get extracted patterns count.
837    pub fn patterns_count(&self) -> usize {
838        self.extraction.conversation_patterns.len()
839    }
840}
841
842/// Estimate tokens in messages (simple approximation).
843fn estimate_tokens(messages: &[Message]) -> u32 {
844    messages.iter()
845        .map(|m| {
846            let content = match &m.content {
847                crate::providers::MessageContent::Text(text) => text.clone(),
848                crate::providers::MessageContent::Blocks(blocks) => {
849                    blocks.iter()
850                        .filter_map(|b| {
851                            if let crate::providers::ContentBlock::Text { text } = b {
852                                Some(text.clone())
853                            } else {
854                                None
855                            }
856                        })
857                        .collect::<Vec<_>>()
858                        .join("\n")
859                }
860            };
861            // Rough estimation: 4 chars per token for English, 2 for Chinese
862            // Use average of 3 chars per token
863            (content.len() / 3) as u32 + 50 // +50 for metadata
864        })
865        .sum()
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871    use crate::providers::{Message, MessageContent, Role};
872
873    fn create_text_message(role: Role, text: &str) -> Message {
874        Message {
875            role,
876            content: MessageContent::Text(text.to_string()),
877        }
878    }
879
880    #[test]
881    fn test_processor_config_default() {
882        let config = ProcessorConfig::default();
883        assert!(config.validate());
884        assert_eq!(config.coherence_threshold, 0.7);
885        assert_eq!(config.focus_threshold, 0.5);
886        assert_eq!(config.target_ratio, 0.6);
887    }
888
889    #[test]
890    fn test_processor_config_simple() {
891        let config = ProcessorConfig::simple_conversation();
892        assert!(config.validate());
893        assert!(config.coherence_threshold < ProcessorConfig::default().coherence_threshold);
894    }
895
896    #[test]
897    fn test_processor_config_complex() {
898        let config = ProcessorConfig::complex_technical();
899        assert!(config.validate());
900        assert!(config.coherence_threshold > ProcessorConfig::default().coherence_threshold);
901    }
902
903    #[test]
904    fn test_estimate_tokens() {
905        let messages = vec![
906            create_text_message(Role::User, "This is a test message"),
907        ];
908        let tokens = estimate_tokens(&messages);
909        assert!(tokens > 0);
910    }
911
912    #[test]
913    fn test_estimate_tokens_empty() {
914        let messages: Vec<Message> = vec![];
915        let tokens = estimate_tokens(&messages);
916        assert_eq!(tokens, 0);
917    }
918
919    #[test]
920    fn test_estimate_tokens_long() {
921        let long_text = "x".repeat(1000);
922        let messages = vec![
923            create_text_message(Role::User, &long_text),
924        ];
925        let tokens = estimate_tokens(&messages);
926        assert!(tokens > 300); // ~333 tokens for 1000 chars + 50 metadata
927    }
928
929    #[test]
930    fn test_processed_result_was_compressed() {
931        let result = ProcessedResult {
932            messages: vec![],
933            extraction: UnifiedExtractionResult::default(),
934            focus: ConversationFocus {
935                current_topic: None,
936                current_question: None,
937                recent_context: Vec::new(),
938                topic_transitions: Vec::new(),
939                detected_at: 0,
940            },
941            segments_count: 0,
942            compression_ratio: 0.7,
943        };
944        assert!(result.was_compressed());
945        // Use approximate comparison for floating point
946        assert!((result.savings_percentage() - 30.0).abs() < 0.01);
947    }
948
949    #[test]
950    fn test_processed_result_no_compression() {
951        let result = ProcessedResult {
952            messages: vec![],
953            extraction: UnifiedExtractionResult::default(),
954            focus: ConversationFocus {
955                current_topic: None,
956                current_question: None,
957                recent_context: Vec::new(),
958                topic_transitions: Vec::new(),
959                detected_at: 0,
960            },
961            segments_count: 0,
962            compression_ratio: 1.0,
963        };
964        assert!(!result.was_compressed());
965        assert_eq!(result.savings_percentage(), 0.0);
966    }
967
968    #[test]
969    fn test_processor_config_from_hardcode() {
970        let hardcode = HardcodeConfig::complex_technical();
971        let config = ProcessorConfig::from_hardcode(&hardcode);
972        assert!(config.validate());
973        assert_eq!(config.preserve_last_n, hardcode.max_recent_context_count);
974    }
975
976    #[test]
977    fn test_quick_process_empty() {
978        let mut processor = create_test_processor();
979        let result = processor.quick_process(vec![]).unwrap();
980        assert!(result.messages.is_empty());
981        assert_eq!(result.compression_ratio, 1.0);
982    }
983
984    #[test]
985    fn test_quick_process_single_message() {
986        let mut processor = create_test_processor();
987        let messages = vec![
988            create_text_message(Role::User, "Test message"),
989        ];
990        let result = processor.quick_process(messages).unwrap();
991        assert!(!result.messages.is_empty());
992    }
993
994    #[test]
995    fn test_calculate_segment_focus_score_empty() {
996        let processor = create_test_processor();
997        let focus = ConversationFocus {
998            current_topic: None,
999            current_question: None,
1000            recent_context: Vec::new(),
1001            topic_transitions: Vec::new(),
1002            detected_at: 0,
1003        };
1004        let score = processor.calculate_segment_focus_score(&[], &focus);
1005        assert_eq!(score, 0.0);
1006    }
1007
1008    #[test]
1009    fn test_inject_focus_message() {
1010        let processor = create_test_processor();
1011        let focus = ConversationFocus {
1012            current_topic: Some("Testing".to_string()),
1013            current_question: Some("How to test?".to_string()),
1014            recent_context: vec!["Context 1".to_string()],
1015            topic_transitions: Vec::new(),
1016            detected_at: 0,
1017        };
1018        let messages = vec![
1019            create_text_message(Role::User, "First message"),
1020            create_text_message(Role::Assistant, "Response"),
1021        ];
1022        let result = processor.inject_focus_message(messages, &focus);
1023        assert_eq!(result.len(), 3);
1024        // Focus message should be first (no system messages)
1025        assert!(matches!(result[0].role, Role::System));
1026    }
1027
1028    fn create_test_processor() -> IntegratedLongContextProcessor {
1029        // Create a minimal processor for testing
1030        let config = ProcessorConfig::default();
1031        let hardcode_config = HardcodeConfig::default();
1032
1033        IntegratedLongContextProcessor {
1034            unified_extractor: UnifiedExtractor::new_minimal("test-model".to_string()),
1035            pattern_registry: PatternRegistry::new(),
1036            focus_tracker: FocusTracker::new(),
1037            focus_manager: FocusManager::new(),
1038            coherence_detector: CoherenceDetector::new(config.coherence_threshold),
1039            progressive_compressor: ProgressiveCompressor::default_config(),
1040            config,
1041            hardcode_config,
1042        }
1043    }
1044}