use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use super::config::*;
use super::entry::{MemoryCategory, MemoryEntry};
use super::retrieval::{
TfIdfSearch, compute_relevance, expand_semantic_keywords, extract_context_keywords,
has_contradiction_signal,
};
use crate::providers::Message;
use crate::truncate::truncate_with_suffix;
fn compare_scored_entries(
a: (&MemoryEntry, f64),
b: (&MemoryEntry, f64),
relevance_weight: f64,
importance_weight: f64,
) -> std::cmp::Ordering {
if a.0.is_manual && !b.0.is_manual {
return std::cmp::Ordering::Less;
}
if !a.0.is_manual && b.0.is_manual {
return std::cmp::Ordering::Greater;
}
let score_a =
a.1 * relevance_weight + (a.0.importance / MAX_IMPORTANCE_CEILING) * importance_weight;
let score_b =
b.1 * relevance_weight + (b.0.importance / MAX_IMPORTANCE_CEILING) * importance_weight;
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
}
#[derive(Debug, Clone)]
pub struct SearchIndex {
content_lower: Vec<String>,
by_category: HashMap<MemoryCategory, Vec<usize>>,
by_importance: Vec<usize>,
#[allow(dead_code)]
word_freq: HashMap<String, usize>,
}
impl SearchIndex {
pub fn build(entries: &[MemoryEntry]) -> Self {
let content_lower: Vec<String> = entries.iter().map(|e| e.content.to_lowercase()).collect();
let mut by_category: HashMap<MemoryCategory, Vec<usize>> = HashMap::new();
for (i, entry) in entries.iter().enumerate() {
by_category.entry(entry.category).or_default().push(i);
}
let mut by_importance: Vec<usize> = (0..entries.len()).collect();
by_importance.sort_by(|a, b| {
entries[*b]
.importance
.partial_cmp(&entries[*a].importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut word_freq: HashMap<String, usize> = HashMap::new();
for content in &content_lower {
for word in content.split_whitespace() {
*word_freq.entry(word.to_string()).or_default() += 1;
}
}
Self {
content_lower,
by_category,
by_importance,
word_freq,
}
}
pub fn search(
&self,
_entries: &[MemoryEntry],
query_lower: &str,
limit: Option<usize>,
) -> Vec<usize> {
let matches: Vec<usize> = self
.by_importance
.iter()
.filter(|&idx| self.content_lower[*idx].contains(query_lower))
.copied()
.collect();
if let Some(max) = limit {
matches.into_iter().take(max).collect()
} else {
matches
}
}
pub fn search_multi(&self, keywords_lower: &[String]) -> Vec<usize> {
self.by_importance
.iter()
.filter(|&idx| {
let content = &self.content_lower[*idx];
keywords_lower.iter().any(|k| content.contains(k))
})
.copied()
.collect()
}
}
fn default_max_entries() -> usize {
100
}
fn default_min_importance() -> f64 {
30.0
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoMemory {
pub entries: Vec<MemoryEntry>,
#[serde(default)]
pub config: MemoryConfig,
#[serde(default = "default_max_entries")]
pub max_entries: usize,
#[serde(default = "default_min_importance")]
pub min_importance: f64,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(skip)]
search_index: Option<SearchIndex>,
}
impl Default for AutoMemory {
fn default() -> Self {
let config = MemoryConfig::default();
Self {
entries: Vec::new(),
config: config.clone(),
max_entries: config.max_entries,
min_importance: config.min_importance,
enabled: config.enabled,
search_index: None,
}
}
}
impl AutoMemory {
pub fn new() -> Self {
Self::default()
}
fn ensure_index(&mut self) {
if self.search_index.is_none() {
self.rebuild_index();
}
}
pub fn rebuild_index(&mut self) {
self.search_index = Some(SearchIndex::build(&self.entries));
}
fn invalidate_index(&mut self) {
self.search_index = None;
}
pub fn with_config(config: MemoryConfig) -> Self {
Self {
entries: Vec::new(),
config: config.clone(),
max_entries: config.max_entries,
min_importance: config.min_importance,
enabled: config.enabled,
search_index: None,
}
}
pub fn minimal() -> Self {
Self::with_config(MemoryConfig::minimal())
}
pub fn archival() -> Self {
Self::with_config(MemoryConfig::archival())
}
pub fn add(&mut self, entry: MemoryEntry) {
if self.has_similar(&entry.content) {
log::debug!("Skipping duplicate memory: {}", entry.content);
return;
}
if let Some(conflict_idx) = self.find_conflict(&entry.content, entry.category) {
let old_content = self.entries[conflict_idx].content.clone();
log::info!(
"Memory conflict: '{}' supersedes '{}'",
entry.content,
old_content
);
self.entries.remove(conflict_idx);
self.invalidate_index();
}
self.entries.push(entry);
self.invalidate_index();
self.prune();
}
pub fn add_memory(
&mut self,
category: MemoryCategory,
content: String,
source_session: Option<String>,
) {
let entry = MemoryEntry::new(category, content, source_session, None);
self.add(entry);
}
fn find_conflict(&self, new_content: &str, category: MemoryCategory) -> Option<usize> {
let new_lower = new_content.to_lowercase();
let new_words: HashSet<&str> = new_lower.split_whitespace().collect();
let has_change_signal = has_contradiction_signal("", &new_lower);
let overlap_threshold = if has_change_signal {
CONFLICT_OVERLAY_THRESHOLD_WITH_SIGNAL
} else {
CONFLICT_OVERLAY_THRESHOLD
};
for (i, entry) in self.entries.iter().enumerate() {
if entry.category != category {
continue;
}
let entry_lower = entry.content.to_lowercase();
let entry_words: HashSet<&str> = entry_lower.split_whitespace().collect();
let intersection = new_words.intersection(&entry_words).count();
let min_len = new_words.len().min(entry_words.len());
if min_len == 0 {
continue;
}
let topic_overlap = intersection as f64 / min_len as f64;
let jaccard = Self::calculate_similarity(&entry_lower, &new_lower);
if topic_overlap > overlap_threshold
&& jaccard < SIMILARITY_THRESHOLD
&& has_contradiction_signal(&entry_lower, &new_lower)
{
return Some(i);
}
if has_change_signal {
let old_key_terms: Vec<&str> = entry_words
.iter()
.filter(|w| w.len() > 2)
.copied()
.collect();
let referenced = old_key_terms.iter().any(|term| new_lower.contains(term));
if referenced {
return Some(i);
}
}
}
None
}
pub fn has_similar(&self, content: &str) -> bool {
let content_lower = content.to_lowercase();
if content_lower.len() < MIN_SIMILARITY_LENGTH {
return false;
}
for e in &self.entries {
let entry_lower = e.content.to_lowercase();
if entry_lower == content_lower {
log::debug!("Exact duplicate found: {}", content);
return true;
}
if entry_lower.len() < MIN_SIMILARITY_LENGTH {
continue;
}
let similarity = Self::calculate_similarity(&entry_lower, &content_lower);
if similarity >= SIMILARITY_THRESHOLD {
log::debug!(
"Similar memory found (similarity={:.2}): '{}' vs '{}'",
similarity,
e.content,
content
);
crate::debug::debug_log().log(
"MEMORY_DUPLICATE",
&format!(
"similarity={:.2}, existing='{}', new='{}'",
similarity,
truncate_with_suffix(&e.content, 50),
truncate_with_suffix(content, 50)
),
);
return true;
}
}
false
}
pub fn calculate_similarity(a: &str, b: &str) -> f64 {
let a_words: HashSet<&str> = a.split_whitespace().collect();
let b_words: HashSet<&str> = b.split_whitespace().collect();
if a_words.is_empty() || b_words.is_empty() {
return 0.0;
}
let intersection = a_words.intersection(&b_words).count();
let union = a_words.union(&b_words).count();
if union == 0 {
0.0
} else {
intersection as f64 / union as f64
}
}
pub fn prune(&mut self) {
if self.entries.len() <= self.max_entries {
return;
}
let (manual_entries, auto_entries): (Vec<_>, Vec<_>) =
self.entries.iter().cloned().partition(|e| e.is_manual);
let mut sorted_auto = auto_entries;
sorted_auto.sort_by(|a, b| {
let importance_cmp = b
.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal);
if importance_cmp == std::cmp::Ordering::Equal {
b.last_referenced.cmp(&a.last_referenced)
} else {
importance_cmp
}
});
let kept_auto: Vec<_> = sorted_auto
.into_iter()
.filter(|e| e.importance >= self.min_importance)
.take(self.max_entries.saturating_sub(manual_entries.len()))
.collect();
self.entries = manual_entries.into_iter().chain(kept_auto).collect();
if self.entries.len() > self.max_entries {
self.entries.sort_by(|a, b| {
let importance_cmp = b
.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal);
if importance_cmp == std::cmp::Ordering::Equal {
b.last_referenced.cmp(&a.last_referenced)
} else {
importance_cmp
}
});
self.entries.truncate(self.max_entries);
}
self.invalidate_index();
}
pub fn smart_merge(&mut self) -> usize {
if self.entries.len() < 2 {
return 0;
}
let mut merged_count = 0;
let mut to_remove: Vec<String> = Vec::new();
let mut new_entries: Vec<MemoryEntry> = Vec::new();
let mut processed: HashSet<String> = HashSet::new();
for i in 0..self.entries.len() {
let entry_i = &self.entries[i];
if processed.contains(&entry_i.id) {
continue;
}
let mut similar_group: Vec<usize> = vec![i];
for j in (i + 1)..self.entries.len() {
let entry_j = &self.entries[j];
if processed.contains(&entry_j.id) {
continue;
}
if entry_i.category != entry_j.category {
continue;
}
let similarity = Self::calculate_similarity(&entry_i.content, &entry_j.content);
if similarity >= MERGE_SIMILARITY_THRESHOLD {
similar_group.push(j);
}
}
if similar_group.len() >= 2 {
let group_entries: Vec<&MemoryEntry> = similar_group
.iter()
.map(|&idx| &self.entries[idx])
.collect();
let merged = self.merge_group(&group_entries);
for entry in &group_entries {
to_remove.push(entry.id.clone());
processed.insert(entry.id.clone());
}
new_entries.push(merged);
merged_count += similar_group.len() - 1;
} else {
processed.insert(entry_i.id.clone());
}
}
for id in &to_remove {
self.remove(id);
}
for entry in new_entries {
self.add(entry);
}
if merged_count > 0 {
log::debug!("Smart merge: reduced {} entries", merged_count);
self.invalidate_index();
}
merged_count
}
fn merge_group(&self, entries: &[&MemoryEntry]) -> MemoryEntry {
let best = entries
.iter()
.max_by(|a, b| {
let score_a = a.importance + (a.content.len() as f64 / 100.0);
let score_b = b.importance + (b.content.len() as f64 / 100.0);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("merge_group called with empty entries");
let all_same = entries
.iter()
.all(|e| Self::calculate_similarity(&e.content, &best.content) >= 0.95);
if all_same {
let mut merged: MemoryEntry = (*best).clone();
merged.importance = entries
.iter()
.map(|e| e.importance)
.fold(best.importance, |max, val| val.max(max));
merged.tags.push("merged".to_string());
return merged;
}
let mut merged_content = best.content.clone();
for entry in entries {
if entry.id == best.id {
continue;
}
let unique_words = entry
.content
.split_whitespace()
.filter(|word| !best.content.contains(word))
.take(3)
.collect::<Vec<_>>();
if !unique_words.is_empty() {
let additions = unique_words.join(", ");
if additions.len() > 10 {
merged_content =
format!("{} ({})", merged_content.trim_end_matches('.'), additions);
}
}
}
let mut merged = MemoryEntry::new(best.category, merged_content, None, None);
merged.importance = entries
.iter()
.map(|e| e.importance)
.fold(best.importance, |max, val| val.max(max))
+ 5.0;
merged.importance = merged.importance.min(MAX_IMPORTANCE_CEILING);
merged.tags.push("merged".to_string());
for entry in entries {
for tag in &entry.tags {
if !merged.tags.contains(tag) && !tag.starts_with("merged") {
merged.tags.push(tag.clone());
}
}
}
merged.is_manual = entries.iter().any(|e| e.is_manual);
merged
}
pub fn by_category(&self, category: MemoryCategory) -> Vec<&MemoryEntry> {
self.entries
.iter()
.filter(|e| e.category == category)
.collect()
}
pub fn by_category_fast(&mut self, category: MemoryCategory) -> Vec<&MemoryEntry> {
self.ensure_index();
if let Some(ref index) = self.search_index {
index
.by_category
.get(&category)
.map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
.unwrap_or_default()
} else {
self.by_category(category)
}
}
pub fn top_n(&self, n: usize) -> Vec<&MemoryEntry> {
let mut sorted: Vec<_> = self.entries.iter().collect();
sorted.sort_by(|a, b| {
b.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted.into_iter().take(n).collect()
}
pub fn top_n_fast(&mut self, n: usize) -> Vec<&MemoryEntry> {
self.ensure_index();
if let Some(ref index) = self.search_index {
index
.by_importance
.iter()
.take(n)
.map(|&i| &self.entries[i])
.collect()
} else {
self.top_n(n)
}
}
pub fn search(&self, query: &str) -> Vec<&MemoryEntry> {
self.search_with_limit(query, None)
}
pub fn search_with_limit(&self, query: &str, limit: Option<usize>) -> Vec<&MemoryEntry> {
let query_lower = query.to_lowercase();
let mut results: Vec<_> = self
.entries
.iter()
.filter(|e| {
e.content.to_lowercase().contains(&query_lower)
|| e.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower))
})
.collect();
results.sort_by(|a, b| {
b.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(max) = limit {
results.into_iter().take(max).collect()
} else {
results
}
}
pub fn search_fast(&mut self, query: &str, limit: Option<usize>) -> Vec<&MemoryEntry> {
self.ensure_index();
let query_lower = query.to_lowercase();
if let Some(ref index) = self.search_index {
let indices = index.search(&self.entries, &query_lower, limit);
indices.iter().map(|&i| &self.entries[i]).collect()
} else {
self.search_with_limit(query, limit)
}
}
pub fn search_multi(&self, keywords: &[&str]) -> Vec<&MemoryEntry> {
if keywords.is_empty() {
return Vec::new();
}
let keywords_lower: Vec<String> = keywords.iter().map(|k| k.to_lowercase()).collect();
self.entries
.iter()
.filter(|e| {
let content_lower = e.content.to_lowercase();
keywords_lower.iter().any(|k| content_lower.contains(k))
})
.collect()
}
pub fn search_multi_fast(&mut self, keywords: &[&str]) -> Vec<&MemoryEntry> {
if keywords.is_empty() {
return Vec::new();
}
self.ensure_index();
let keywords_lower: Vec<String> = keywords.iter().map(|k| k.to_lowercase()).collect();
if let Some(ref index) = self.search_index {
let indices = index.search_multi(&keywords_lower);
indices.iter().map(|&i| &self.entries[i]).collect()
} else {
self.search_multi(keywords)
}
}
pub fn add_batch(&mut self, entries: Vec<MemoryEntry>) {
for entry in entries {
if !self.has_similar(&entry.content) {
self.entries.push(entry);
}
}
self.prune();
}
pub fn update_references(&mut self, messages: &[Message]) {
let increment = self.config.reference_increment;
let texts_lower: Vec<String> = messages
.iter()
.filter_map(Self::extract_message_text_lower)
.collect();
let entry_contents_lower: Vec<String> = self
.entries
.iter()
.map(|e| e.content.to_lowercase())
.collect();
for (i, entry) in self.entries.iter_mut().enumerate() {
let entry_lower = &entry_contents_lower[i];
if texts_lower.iter().any(|t| t.contains(entry_lower)) {
entry.mark_referenced_with_increment(increment);
}
}
}
fn extract_message_text_lower(msg: &Message) -> Option<String> {
match &msg.content {
crate::providers::MessageContent::Text(t) => Some(t.to_lowercase()),
crate::providers::MessageContent::Blocks(blocks) => {
let text = blocks
.iter()
.filter_map(|b| {
if let crate::providers::ContentBlock::Text { text } = b {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join(" ");
Some(text.to_lowercase())
}
}
}
pub fn generate_manifest(&self, max_entries: usize) -> String {
if self.entries.is_empty() {
return String::new();
}
let mut sorted_entries: Vec<_> = self.entries.iter().enumerate().collect();
sorted_entries.sort_by(|a, b| {
b.1.importance
.partial_cmp(&a.1.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted_entries.truncate(max_entries);
let mut manifest = String::new();
for (original_idx, entry) in sorted_entries.iter() {
let preview: String = entry.content.chars().take(80).collect();
let preview = preview.trim_end_matches('\n');
manifest.push_str(&format!(
"{}. {} {} {} (重要性: {:.0})\n",
original_idx,
entry.category.icon(),
preview,
entry.category.display_name(),
entry.importance
));
}
manifest
}
pub fn get_entries_by_indices(&self, indices: &[usize]) -> Vec<&MemoryEntry> {
indices
.iter()
.filter_map(|i| self.entries.get(*i))
.collect()
}
pub fn generate_prompt_summary(&self, max_entries: usize) -> String {
if self.entries.is_empty() {
return String::new();
}
let top_entries = self.top_n(max_entries);
if top_entries.is_empty() {
return String::new();
}
let mut summary = String::from("【自动记忆摘要】\n\n");
let mut by_cat: HashMap<MemoryCategory, Vec<&MemoryEntry>> = HashMap::new();
for entry in top_entries {
by_cat.entry(entry.category).or_default().push(entry);
}
for (cat, entries) in by_cat {
summary.push_str(&format!("{} {}:\n", cat.icon(), cat.display_name()));
for entry in entries {
summary.push_str(&format!(" {}\n", entry.format_for_prompt()));
}
summary.push('\n');
}
summary
}
pub fn generate_contextual_summary(&self, context: &str, max_entries: usize) -> String {
let keywords = extract_context_keywords(context);
self.generate_contextual_summary_with_keywords(&keywords, max_entries)
}
pub fn generate_contextual_summary_with_keywords(
&self,
context_keywords: &[String],
max_entries: usize,
) -> String {
if self.entries.is_empty() {
return String::new();
}
let expanded_keywords = expand_semantic_keywords(context_keywords);
let mut tfidf = TfIdfSearch::new();
tfidf.index(self);
let keywords_slice: Vec<&str> = expanded_keywords.iter().map(|s| s.as_str()).collect();
let tfidf_results = tfidf.search_multi(&keywords_slice, Some(max_entries * 2));
let mut tfidf_scores: HashMap<String, f64> = HashMap::new();
for (content, score) in &tfidf_results {
if let Some(entry) = self.entries.iter().find(|e| &e.content == content) {
tfidf_scores.insert(entry.id.clone(), *score);
}
}
let mut scored: Vec<(&MemoryEntry, f64)> = self
.entries
.iter()
.map(|entry| {
let relevance = compute_relevance(entry, &expanded_keywords);
let tfidf = tfidf_scores.get(&entry.id).copied().unwrap_or(0.0);
let combined = tfidf * 0.4 + relevance * 0.6;
(entry, combined)
})
.collect();
scored.sort_by(|a, b| {
compare_scored_entries(*a, *b, CONTEXT_RELEVANCE_WEIGHT, CONTEXT_IMPORTANCE_WEIGHT)
});
let selected: Vec<&MemoryEntry> = scored
.iter()
.take(max_entries)
.map(|(entry, _)| *entry)
.collect();
if selected.is_empty() {
return String::new();
}
let mut summary = String::from("【跨会话记忆】\n\n");
let mut by_cat: HashMap<MemoryCategory, Vec<&MemoryEntry>> = HashMap::new();
for entry in selected {
by_cat.entry(entry.category).or_default().push(entry);
}
for (cat, entries) in by_cat {
summary.push_str(&format!("{} {}:\n", cat.icon(), cat.display_name()));
for entry in entries {
summary.push_str(&format!(" {}\n", entry.format_for_prompt()));
}
summary.push('\n');
}
summary
}
pub fn update_retrieval_stats(&mut self, retrieved_ids: &[String]) {
for id in retrieved_ids {
if let Some(entry) = self.entries.iter_mut().find(|e| &e.id == id) {
entry.mark_referenced();
log::debug!("Updated reference stats for memory {}", id);
}
}
}
pub fn get_retrieval_ids(
&self,
context_keywords: &[String],
max_entries: usize,
) -> Vec<String> {
if self.entries.is_empty() {
return Vec::new();
}
let expanded_keywords = expand_semantic_keywords(context_keywords);
let mut scored: Vec<(&MemoryEntry, f64)> = self
.entries
.iter()
.map(|entry| {
let relevance = compute_relevance(entry, &expanded_keywords);
(entry, relevance)
})
.collect();
scored.sort_by(|a, b| compare_scored_entries(*a, *b, 1.0, 1.0));
scored
.iter()
.take(max_entries)
.map(|(e, _)| e.id.clone())
.collect()
}
pub async fn generate_contextual_summary_async(
&self,
context: &str,
max_entries: usize,
_fast_provider: Option<&dyn crate::providers::Provider>,
) -> String {
if self.entries.is_empty() {
return String::new();
}
let context_keywords = extract_context_keywords(context);
let mut scored: Vec<(&MemoryEntry, f64)> = self
.entries
.iter()
.map(|entry| {
let relevance = compute_relevance(entry, &context_keywords);
(entry, relevance)
})
.collect();
scored.sort_by(|a, b| {
compare_scored_entries(*a, *b, CONTEXT_RELEVANCE_WEIGHT, CONTEXT_IMPORTANCE_WEIGHT)
});
let selected: Vec<&MemoryEntry> = scored
.iter()
.take(max_entries)
.map(|(entry, _)| *entry)
.collect();
if selected.is_empty() {
return String::new();
}
let mut summary = String::from("【跨会话记忆】\n\n");
let mut by_cat: HashMap<MemoryCategory, Vec<&MemoryEntry>> = HashMap::new();
for entry in selected {
by_cat.entry(entry.category).or_default().push(entry);
}
for (cat, entries) in by_cat {
summary.push_str(&format!("{} {}:\n", cat.icon(), cat.display_name()));
for entry in entries {
summary.push_str(&format!(" {}\n", entry.format_for_prompt()));
}
summary.push('\n');
}
summary
}
pub fn format_all(&self) -> String {
if self.entries.is_empty() {
return "[no memories accumulated]".to_string();
}
let mut result = String::from("Accumulated memories:\n\n");
let mut sorted: Vec<_> = self.entries.iter().collect();
sorted.sort_by(|a, b| {
b.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
for entry in sorted {
result.push_str(&entry.format_line());
result.push('\n');
}
result
}
pub fn generate_statistics(&self) -> MemoryStatistics {
let total = self.entries.len();
let manual = self.entries.iter().filter(|e| e.is_manual).count();
let auto = total - manual;
let by_category: HashMap<MemoryCategory, usize> =
self.entries.iter().fold(HashMap::new(), |mut acc, e| {
*acc.entry(e.category).or_default() += 1;
acc
});
let avg_importance = if total > 0 {
self.entries.iter().map(|e| e.importance).sum::<f64>() / total as f64
} else {
0.0
};
let oldest = self
.entries
.iter()
.min_by_key(|e| e.created_at)
.map(|e| e.created_at);
let newest = self
.entries
.iter()
.max_by_key(|e| e.created_at)
.map(|e| e.created_at);
let highly_referenced = self
.entries
.iter()
.filter(|e| e.reference_count >= 3)
.count();
MemoryStatistics {
total,
manual,
auto,
by_category,
avg_importance,
oldest,
newest,
highly_referenced,
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.invalidate_index();
}
pub fn remove(&mut self, id: &str) -> bool {
let idx = self.entries.iter().position(|e| e.id == id);
if let Some(i) = idx {
self.entries.remove(i);
self.invalidate_index();
true
} else {
false
}
}
pub fn apply_time_decay(&mut self) {
let now = Utc::now();
let decay_start_days = self.config.decay_start_days;
let decay_rate = self.config.decay_rate;
let decay_period_days = 30;
for entry in &mut self.entries {
if entry.is_manual {
continue;
}
let days_since_reference = (now - entry.last_referenced).num_days().max(0);
if days_since_reference > decay_start_days {
let decay_periods = (days_since_reference - decay_start_days) / decay_period_days;
let decay_factor = decay_rate.powi(decay_periods as i32);
entry.importance *= decay_factor;
entry.importance = entry.importance.max(self.min_importance * 0.5);
}
}
self.prune();
}
}
#[derive(Debug, Clone)]
pub struct MemoryStatistics {
pub total: usize,
pub manual: usize,
pub auto: usize,
pub by_category: HashMap<MemoryCategory, usize>,
pub avg_importance: f64,
pub oldest: Option<DateTime<Utc>>,
pub newest: Option<DateTime<Utc>>,
pub highly_referenced: usize,
}
impl MemoryStatistics {
pub fn format_summary(&self) -> String {
let mut output = String::new();
output.push_str("记忆统计:\n");
output.push_str(&format!(" 总计: {} 条\n", self.total));
output.push_str(&format!(" ├─ 手动添加: {} 条\n", self.manual));
output.push_str(&format!(" └─ 自动检测: {} 条\n", self.auto));
output.push('\n');
output.push_str("分类统计:\n");
for (cat, count) in &self.by_category {
output.push_str(&format!(
" {} {}: {} 条\n",
cat.icon(),
cat.display_name(),
count
));
}
output.push('\n');
output.push_str("质量指标:\n");
output.push_str(&format!(" 平均重要性: {:.1} 分\n", self.avg_importance));
output.push_str(&format!(
" 高频引用: {} 条 (≥3次)\n",
self.highly_referenced
));
if let Some(oldest) = self.oldest {
let days = (Utc::now() - oldest).num_days();
output.push_str(&format!(" 记忆跨度: {} 天\n", days));
}
output
}
}