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) -> f64 {
26 if max_age.num_seconds() == 0 {
27 return 1.0;
28 }
29
30 let ratio = age.num_seconds() as f64 / max_age.num_seconds() as f64;
31 let ratio = ratio.clamp(0.0, 1.0);
32
33 match self {
34 Self::Linear => 1.0 - ratio,
35 Self::Exponential => (-3.0 * ratio).exp(),
36 Self::Step => {
37 if ratio < 0.5 {
38 1.0
39 } else {
40 0.3
41 }
42 }
43 Self::None => 1.0,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct TemporalCompressor {
51 pub strategy: DecayStrategy,
53 pub max_age: Duration,
55 pub min_importance: f64,
57}
58
59impl Default for TemporalCompressor {
60 fn default() -> Self {
61 Self {
62 strategy: DecayStrategy::Exponential,
63 max_age: Duration::hours(24),
64 min_importance: 0.1,
65 }
66 }
67}
68
69impl TemporalCompressor {
70 pub fn new(strategy: DecayStrategy, max_age: Duration) -> Self {
72 Self {
73 strategy,
74 max_age,
75 min_importance: 0.1,
76 }
77 }
78
79 pub fn importance(&self, created_at: DateTime<Utc>, base_importance: f64) -> f64 {
81 let age = Utc::now() - created_at;
82 let decay = self.strategy.calculate(age, self.max_age);
83 (base_importance * decay).max(self.min_importance)
84 }
85
86 pub fn should_evict(&self, created_at: DateTime<Utc>) -> bool {
88 let age = Utc::now() - created_at;
89 age > self.max_age
90 }
91
92 pub fn compression_ratio(&self, created_at: DateTime<Utc>) -> f64 {
94 let age = Utc::now() - created_at;
95 let ratio = age.num_seconds() as f64 / self.max_age.num_seconds() as f64;
96 ratio.clamp(0.0, 0.9) }
98
99 pub fn compress(&self, content: &str, ratio: f64) -> String {
101 if ratio <= 0.0 {
102 return content.to_string();
103 }
104
105 let target_len = ((1.0 - ratio) * content.len() as f64) as usize;
106 let target_len = target_len.max(20);
107
108 if target_len >= content.len() {
109 content.to_string()
110 } else {
111 format!("{}...[compressed]", &content[..target_len])
112 }
113 }
114
115 pub async fn compress_with_llm<L: LlmProvider + EmbeddingProvider>(
124 &self,
125 content: &str,
126 ratio: f64,
127 llm: &L,
128 vector_store: Option<&dyn VectorStoreBackend>,
129 tenant_id: Option<&str>,
130 ) -> Result<String, LlmError> {
131 if ratio <= 0.0 || content.len() < 50 {
133 return Ok(content.to_string());
134 }
135
136 let word_count = content.split_whitespace().count();
138 let target_words = ((1.0 - ratio) * word_count as f64).max(10.0) as usize;
139
140 let prompt = format!(
142 "Summarize the following text in approximately {} words. \
143 Preserve the most important facts, decisions, and context. \
144 Be concise but maintain accuracy.\n\n\
145 TEXT TO SUMMARIZE:\n{}\n\n\
146 SUMMARY:",
147 target_words, content
148 );
149
150 let summary = llm.ask(&prompt).await?;
151
152 if let (Some(vs), Some(tid)) = (vector_store, tenant_id) {
154 match llm.embed(&summary).await {
155 Ok(vector) => {
156 let mut metadata = HashMap::new();
157 metadata.insert("type".to_string(), "temporal_summary".to_string());
158 metadata.insert("original_len".to_string(), content.len().to_string());
159 metadata.insert("timestamp".to_string(), Utc::now().to_rfc3339());
160
161 let id = format!("summary_{}", uuid::Uuid::new_v4());
162 if let Err(e) = vs.add(id, tid.to_string(), vector, metadata).await {
163 tracing::warn!("Failed to store summary embedding: {}", e);
164 }
165 }
166 Err(e) => tracing::warn!("Failed to generate summary embedding: {}", e),
167 }
168 }
169
170 Ok(summary.trim().to_string())
171 }
172}