vex_temporal/
compression.rs1use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use vex_llm::{EmbeddingProvider, LlmError, LlmProvider};
7use vex_persist::VectorStoreBackend;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum DecayStrategy {
12 Linear,
14 Exponential,
16 Step,
18 None,
20}
21
22impl DecayStrategy {
23 pub fn calculate(&self, age: Duration, max_age: Duration, exp_rate: f64) -> f64 {
27 if max_age.num_seconds() == 0 {
28 return 1.0;
29 }
30
31 let ratio = age.num_seconds() as f64 / max_age.num_seconds() as f64;
32 let ratio = ratio.clamp(0.0, 1.0);
33
34 match self {
35 Self::Linear => 1.0 - ratio,
36 Self::Exponential => (-exp_rate * ratio).exp(),
37 Self::Step => {
38 if ratio < 0.5 {
39 1.0
40 } else {
41 0.3
42 }
43 }
44 Self::None => 1.0,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct TemporalCompressor {
52 pub strategy: DecayStrategy,
54 pub max_age: Duration,
56 pub min_importance: f64,
58 pub exponential_decay_rate: f64,
61}
62
63impl Default for TemporalCompressor {
64 fn default() -> Self {
65 Self {
66 strategy: DecayStrategy::Exponential,
67 max_age: Duration::hours(24),
68 min_importance: 0.1,
69 exponential_decay_rate: 3.0,
70 }
71 }
72}
73
74impl TemporalCompressor {
75 pub fn new(strategy: DecayStrategy, max_age: Duration) -> Self {
77 Self {
78 strategy,
79 max_age,
80 min_importance: 0.1,
81 exponential_decay_rate: 3.0,
82 }
83 }
84
85 pub fn importance(&self, created_at: DateTime<Utc>, base_importance: f64) -> f64 {
87 let age = Utc::now() - created_at;
88 let decay = self
89 .strategy
90 .calculate(age, self.max_age, self.exponential_decay_rate);
91 (base_importance * decay).max(self.min_importance)
92 }
93
94 pub fn should_evict(&self, created_at: DateTime<Utc>) -> bool {
96 let age = Utc::now() - created_at;
97 age > self.max_age
98 }
99
100 pub fn compression_ratio(&self, created_at: DateTime<Utc>) -> f64 {
102 let age = Utc::now() - created_at;
103 let ratio = age.num_seconds() as f64 / self.max_age.num_seconds() as f64;
104 ratio.clamp(0.0, 0.9) }
106
107 pub fn compress(&self, content: &str, ratio: f64) -> String {
109 if ratio <= 0.0 {
110 return content.to_string();
111 }
112
113 let target_len = ((1.0 - ratio) * content.len() as f64) as usize;
114 let target_len = target_len.max(20);
115
116 if target_len >= content.len() {
117 content.to_string()
118 } else {
119 format!("{}...[compressed]", &content[..target_len])
120 }
121 }
122
123 pub async fn compress_with_llm<L: LlmProvider + EmbeddingProvider>(
132 &self,
133 content: &str,
134 ratio: f64,
135 llm: &L,
136 vector_store: Option<&dyn VectorStoreBackend>,
137 tenant_id: Option<&str>,
138 ) -> Result<String, LlmError> {
139 if ratio <= 0.0 || content.len() < 50 {
141 return Ok(content.to_string());
142 }
143
144 let word_count = content.split_whitespace().count();
146 let target_words = ((1.0 - ratio) * word_count as f64).max(10.0) as usize;
147
148 let prompt = format!(
150 "Summarize the following text in approximately {} words. \
151 Preserve the most important facts, decisions, and context. \
152 Be concise but maintain accuracy.\n\n\
153 TEXT TO SUMMARIZE:\n{}\n\n\
154 SUMMARY:",
155 target_words, content
156 );
157
158 let summary = llm.ask(&prompt).await?;
159
160 if let (Some(vs), Some(tid)) = (vector_store, tenant_id) {
162 match llm.embed(&summary).await {
163 Ok(vector) => {
164 let mut metadata = HashMap::new();
165 metadata.insert("type".to_string(), "temporal_summary".to_string());
166 metadata.insert("original_len".to_string(), content.len().to_string());
167 metadata.insert("timestamp".to_string(), Utc::now().to_rfc3339());
168
169 let id = format!("summary_{}", uuid::Uuid::new_v4());
170 if let Err(e) = vs.add(id, tid.to_string(), vector, metadata).await {
171 tracing::warn!("Failed to store summary embedding: {}", e);
172 }
173 }
174 Err(e) => tracing::warn!("Failed to generate summary embedding: {}", e),
175 }
176 }
177
178 Ok(summary.trim().to_string())
179 }
180}