use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use vex_llm::{EmbeddingProvider, LlmError, LlmProvider};
use vex_persist::VectorStoreBackend;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DecayStrategy {
Linear,
Exponential,
Step,
None,
}
impl DecayStrategy {
pub fn calculate(&self, age: Duration, max_age: Duration, exp_rate: f64) -> f64 {
if max_age.num_seconds() == 0 {
return 1.0;
}
let ratio = age.num_seconds() as f64 / max_age.num_seconds() as f64;
let ratio = ratio.clamp(0.0, 1.0);
match self {
Self::Linear => 1.0 - ratio,
Self::Exponential => (-exp_rate * ratio).exp(),
Self::Step => {
if ratio < 0.5 {
1.0
} else {
0.3
}
}
Self::None => 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct TemporalCompressor {
pub strategy: DecayStrategy,
pub max_age: Duration,
pub min_importance: f64,
pub exponential_decay_rate: f64,
}
impl Default for TemporalCompressor {
fn default() -> Self {
Self {
strategy: DecayStrategy::Exponential,
max_age: Duration::hours(24),
min_importance: 0.1,
exponential_decay_rate: 3.0,
}
}
}
impl TemporalCompressor {
pub fn new(strategy: DecayStrategy, max_age: Duration) -> Self {
Self {
strategy,
max_age,
min_importance: 0.1,
exponential_decay_rate: 3.0,
}
}
pub fn importance(&self, created_at: DateTime<Utc>, base_importance: f64) -> f64 {
let age = Utc::now() - created_at;
let decay = self
.strategy
.calculate(age, self.max_age, self.exponential_decay_rate);
(base_importance * decay).max(self.min_importance)
}
pub fn should_evict(&self, created_at: DateTime<Utc>) -> bool {
let age = Utc::now() - created_at;
age > self.max_age
}
pub fn compression_ratio(&self, created_at: DateTime<Utc>) -> f64 {
let age = Utc::now() - created_at;
let ratio = age.num_seconds() as f64 / self.max_age.num_seconds() as f64;
ratio.clamp(0.0, 0.9) }
pub fn compress(&self, content: &str, ratio: f64) -> String {
if ratio <= 0.0 {
return content.to_string();
}
let target_len = ((1.0 - ratio) * content.len() as f64) as usize;
let target_len = target_len.max(20);
if target_len >= content.len() {
content.to_string()
} else {
format!("{}...[compressed]", &content[..target_len])
}
}
pub async fn compress_with_llm<L: LlmProvider + EmbeddingProvider>(
&self,
content: &str,
ratio: f64,
llm: &L,
vector_store: Option<&dyn VectorStoreBackend>,
tenant_id: Option<&str>,
) -> Result<String, LlmError> {
if ratio <= 0.0 || content.len() < 50 {
return Ok(content.to_string());
}
let word_count = content.split_whitespace().count();
let target_words = ((1.0 - ratio) * word_count as f64).max(10.0) as usize;
let prompt = format!(
"Summarize the following text in approximately {} words. \
Preserve the most important facts, decisions, and context. \
Be concise but maintain accuracy.\n\n\
TEXT TO SUMMARIZE:\n{}\n\n\
SUMMARY:",
target_words, content
);
let summary = llm.ask(&prompt).await?;
if let (Some(vs), Some(tid)) = (vector_store, tenant_id) {
match llm.embed(&summary).await {
Ok(vector) => {
let mut metadata = HashMap::new();
metadata.insert("type".to_string(), "temporal_summary".to_string());
metadata.insert("original_len".to_string(), content.len().to_string());
metadata.insert("timestamp".to_string(), Utc::now().to_rfc3339());
let id = format!("summary_{}", uuid::Uuid::new_v4());
if let Err(e) = vs.add(id, tid.to_string(), vector, metadata).await {
tracing::warn!("Failed to store summary embedding: {}", e);
}
}
Err(e) => tracing::warn!("Failed to generate summary embedding: {}", e),
}
}
Ok(summary.trim().to_string())
}
}