use serde::{Deserialize, Serialize};
use crate::memory::ExtractedKeywords;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FocusTrackerConfig {
#[serde(skip)]
current_keywords: Option<ExtractedKeywords>,
pub fallback_topic_word_count: usize,
pub focus_window_size: usize,
pub max_recent_context_count: usize,
pub max_question_extract_length: usize,
pub min_substantial_text_length: usize,
pub focus_score_boost: f32,
pub max_focus_score: f32,
}
impl Default for FocusTrackerConfig {
fn default() -> Self {
Self {
current_keywords: None,
fallback_topic_word_count: 3,
focus_window_size: 10, max_recent_context_count: 5, max_question_extract_length: 100, min_substantial_text_length: 10,
focus_score_boost: 0.3, max_focus_score: 1.0, }
}
}
impl FocusTrackerConfig {
pub fn simple_conversation() -> Self {
Self {
focus_window_size: 5,
max_recent_context_count: 3,
min_substantial_text_length: 5,
..Self::default()
}
}
pub fn complex_technical() -> Self {
Self {
focus_window_size: 15,
max_recent_context_count: 7,
max_question_extract_length: 150,
min_substantial_text_length: 20,
focus_score_boost: 0.4,
..Self::default()
}
}
pub fn from_complexity(level: crate::compress::complexity::ComplexityLevel) -> Self {
match level {
crate::compress::complexity::ComplexityLevel::High => Self::complex_technical(),
crate::compress::complexity::ComplexityLevel::Medium => Self::default(),
crate::compress::complexity::ComplexityLevel::Low => Self::simple_conversation(),
}
}
pub fn set_keywords(&mut self, keywords: &ExtractedKeywords) {
self.current_keywords = Some(keywords.clone());
}
pub fn get_keywords(&self) -> Option<&ExtractedKeywords> {
self.current_keywords.as_ref()
}
pub fn transition_keywords(&self) -> Vec<String> {
if let Some(kw) = &self.current_keywords {
kw.transition.clone()
} else {
vec![
"however".to_string(), "but".to_string(), "switching".to_string(),
"转换".to_string(), "切换".to_string(), "换个话题".to_string(),
]
}
}
pub fn question_keywords(&self) -> Vec<String> {
if let Some(kw) = &self.current_keywords {
kw.question.clone()
} else {
vec![
"how".to_string(), "what".to_string(), "why".to_string(),
"如何".to_string(), "什么".to_string(), "为什么".to_string(),
]
}
}
pub fn task_keywords(&self) -> Vec<String> {
if let Some(kw) = &self.current_keywords {
kw.task.clone()
} else {
vec![
"implement".to_string(), "create".to_string(), "fix".to_string(),
"实现".to_string(), "创建".to_string(), "修复".to_string(),
]
}
}
pub fn tech_keywords(&self) -> Vec<String> {
if let Some(kw) = &self.current_keywords {
kw.tech.clone()
} else {
vec![
"rust".to_string(), "python".to_string(), "javascript".to_string(),
"api".to_string(), "database".to_string(), "performance".to_string(),
]
}
}
pub fn matches_transition(&self, text: &str) -> bool {
let lower = text.to_lowercase();
self.transition_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
}
pub fn matches_question(&self, text: &str) -> bool {
let lower = text.to_lowercase();
self.question_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
}
pub fn matches_task(&self, text: &str) -> bool {
let lower = text.to_lowercase();
self.task_keywords().iter().any(|kw| lower.contains(&kw.to_lowercase()))
}
pub fn find_tech_keywords(&self, text: &str) -> Vec<String> {
let lower = text.to_lowercase();
self.tech_keywords()
.iter()
.filter(|kw| lower.contains(&kw.to_lowercase()))
.cloned()
.collect()
}
pub fn merge_keywords(&mut self, additional: &ExtractedKeywords) {
match self.current_keywords.take() {
Some(mut current) => {
current.merge(additional);
self.current_keywords = Some(current);
}
None => {
self.current_keywords = Some(additional.clone());
}
}
}
pub fn clear_keywords(&mut self) {
self.current_keywords = None;
}
pub fn validate(&self) -> bool {
self.focus_window_size > 0 &&
self.max_recent_context_count > 0 &&
self.max_question_extract_length > 0 &&
self.min_substantial_text_length > 0 &&
self.focus_score_boost > 0.0 &&
self.max_focus_score > 0.0 &&
self.fallback_topic_word_count > 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeywordType {
Transition,
Question,
Task,
Tech,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = FocusTrackerConfig::default();
assert!(config.validate());
assert_eq!(config.focus_window_size, 10);
assert_eq!(config.max_recent_context_count, 5);
}
#[test]
fn test_simple_conversation_config() {
let config = FocusTrackerConfig::simple_conversation();
assert_eq!(config.focus_window_size, 5);
assert_eq!(config.max_recent_context_count, 3);
}
#[test]
fn test_complex_technical_config() {
let config = FocusTrackerConfig::complex_technical();
assert_eq!(config.focus_window_size, 15);
assert_eq!(config.max_question_extract_length, 150);
}
#[test]
fn test_set_keywords() {
let mut config = FocusTrackerConfig::default();
assert!(config.get_keywords().is_none());
let keywords = ExtractedKeywords {
transition: vec!["new_transition".to_string()],
question: vec!["new_question".to_string()],
task: vec!["new_task".to_string()],
tech: vec!["new_tech".to_string()],
};
config.set_keywords(&keywords);
assert!(config.get_keywords().is_some());
assert_eq!(config.transition_keywords(), vec!["new_transition".to_string()]);
assert_eq!(config.question_keywords(), vec!["new_question".to_string()]);
}
#[test]
fn test_fallback_keywords() {
let config = FocusTrackerConfig::default();
assert!(!config.transition_keywords().is_empty());
assert!(!config.question_keywords().is_empty());
assert!(!config.task_keywords().is_empty());
assert!(!config.tech_keywords().is_empty());
assert!(config.transition_keywords().contains(&"however".to_string()));
assert!(config.question_keywords().contains(&"how".to_string()));
assert!(config.task_keywords().contains(&"implement".to_string()));
assert!(config.tech_keywords().contains(&"rust".to_string()));
}
#[test]
fn test_matches_keywords() {
let config = FocusTrackerConfig::default();
assert!(config.matches_question("How do I do this?"));
assert!(config.matches_task("Please implement this"));
assert!(config.matches_transition("However, let's move on"));
}
#[test]
fn test_find_tech_keywords() {
let config = FocusTrackerConfig::default();
let found = config.find_tech_keywords("Using Rust and Python for development");
assert!(found.contains(&"rust".to_string()));
assert!(found.contains(&"python".to_string()));
}
#[test]
fn test_merge_keywords() {
let mut config = FocusTrackerConfig::default();
let initial = ExtractedKeywords {
transition: vec!["switch".to_string()],
question: vec!["how".to_string()],
task: vec!["create".to_string()],
tech: vec!["rust".to_string()],
};
config.set_keywords(&initial);
let additional = ExtractedKeywords {
transition: vec!["new".to_string()],
question: vec!["why".to_string()],
task: vec!["delete".to_string()],
tech: vec!["python".to_string()],
};
config.merge_keywords(&additional);
let merged = config.get_keywords().unwrap();
assert!(merged.transition.contains(&"switch".to_string()));
assert!(merged.transition.contains(&"new".to_string()));
assert!(merged.tech.contains(&"rust".to_string()));
assert!(merged.tech.contains(&"python".to_string()));
}
#[test]
fn test_clear_keywords() {
let mut config = FocusTrackerConfig::default();
let keywords = ExtractedKeywords {
transition: vec!["test".to_string()],
question: vec![],
task: vec![],
tech: vec![],
};
config.set_keywords(&keywords);
assert!(config.get_keywords().is_some());
config.clear_keywords();
assert!(config.get_keywords().is_none());
assert!(config.transition_keywords().contains(&"however".to_string()));
}
}