use std::collections::HashSet;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
const FILLER_PHRASES: &[&str] = &[
"i think",
"basically",
"you know",
"kind of",
"sort of",
"i mean",
"like",
"actually",
"to be honest",
"in my opinion",
"i believe",
"i guess",
"i suppose",
"it seems like",
"more or less",
"pretty much",
"at the end of the day",
"as a matter of fact",
"the thing is",
"to be fair",
"honestly",
"literally",
"obviously",
"clearly",
"just",
"simply",
"basically speaking",
"needless to say",
"as you know",
"for what it's worth",
];
const HEDGING_PHRASES: &[&str] = &[
"maybe",
"perhaps",
"sort of",
"kind of",
"somewhat",
"rather",
"fairly",
"quite",
"a bit",
"a little",
"in a way",
"in some ways",
"to some extent",
"to a degree",
"more or less",
];
static PROPER_NOUN_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)\b").expect("valid regex"));
static NUMBER_DATE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b(\d{1,4}[/-]\d{1,2}[/-]\d{1,4}|\d{4}|\d+\.\d+|\d{1,3}(?:,\d{3})*(?:\.\d+)?)\b")
.expect("valid regex")
});
static SENTENCE_SPLIT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[.!?]\s+").expect("valid regex"));
static COMMON_VERBS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b(is|are|was|were|has|have|had|will|can|could|should|would|does|did|do|provides|uses|returns|creates|stores|contains|supports|requires|enables|implements|defines|allows|includes|handles|manages)\b")
.expect("valid regex")
});
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressionConfig {
pub target_ratio: f32,
pub min_content_length: usize,
pub preserve_entities: bool,
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
target_ratio: 0.1,
min_content_length: 100,
preserve_entities: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressedMemory {
pub original_tokens: usize,
pub compressed_tokens: usize,
pub ratio: f32,
pub structured_content: String,
pub key_entities: Vec<String>,
pub key_facts: Vec<String>,
}
pub struct SemanticCompressor {
config: CompressionConfig,
}
impl SemanticCompressor {
pub fn new(config: CompressionConfig) -> Self {
Self { config }
}
pub fn compress(&self, text: &str) -> CompressedMemory {
let original_tokens = estimate_tokens(text);
if text.trim().is_empty() {
return CompressedMemory {
original_tokens: 0,
compressed_tokens: 0,
ratio: 1.0,
structured_content: String::new(),
key_entities: Vec::new(),
key_facts: Vec::new(),
};
}
if text.trim().len() < self.config.min_content_length {
return CompressedMemory {
original_tokens,
compressed_tokens: original_tokens,
ratio: 1.0,
structured_content: text.trim().to_string(),
key_entities: Vec::new(),
key_facts: Vec::new(),
};
}
let sentences = split_sentences(text);
let cleaned: Vec<String> = sentences
.iter()
.map(|s| strip_filler(s))
.filter(|s| !s.trim().is_empty())
.collect();
let key_entities = if self.config.preserve_entities {
extract_entities(&sentences)
} else {
Vec::new()
};
let deduped = deduplicate_sentences(&cleaned);
let cores: Vec<String> = deduped.iter().map(|s| extract_svo_core(s)).collect();
let structured_content = cores.join(". ");
let key_facts = extract_key_facts(&deduped, &key_entities);
let compressed_tokens = estimate_tokens(&structured_content);
let ratio = if original_tokens == 0 {
1.0
} else {
compressed_tokens as f32 / original_tokens as f32
};
CompressedMemory {
original_tokens,
compressed_tokens,
ratio,
structured_content,
key_entities,
key_facts,
}
}
pub fn decompress(&self, compressed: &CompressedMemory) -> String {
if compressed.structured_content.is_empty() {
return String::new();
}
if compressed.key_facts.is_empty() {
return compressed.structured_content.clone();
}
let entity_context = if !compressed.key_entities.is_empty() {
format!(" (entities: {})", compressed.key_entities.join(", "))
} else {
String::new()
};
let mut parts: Vec<String> = compressed.key_facts.clone();
if let Some(last) = parts.last_mut() {
last.push_str(&entity_context);
}
parts.join(". ")
}
pub fn compress_batch(&self, texts: &[&str]) -> Vec<CompressedMemory> {
texts.iter().map(|t| self.compress(t)).collect()
}
}
fn estimate_tokens(text: &str) -> usize {
text.len().div_ceil(4)
}
fn split_sentences(text: &str) -> Vec<String> {
let terminators: Vec<(usize, usize, char)> = SENTENCE_SPLIT_RE
.find_iter(text)
.map(|m| {
let punct = text[m.start()..].chars().next().unwrap_or('.');
(m.start(), m.end(), punct)
})
.collect();
if terminators.is_empty() {
let trimmed = text.trim().to_string();
return if trimmed.is_empty() {
vec![]
} else {
vec![trimmed]
};
}
let mut sentences: Vec<String> = Vec::new();
let mut cursor = 0usize;
for (t_start, t_end, punct) in &terminators {
let fragment = text[cursor..*t_start].trim().to_string();
if !fragment.is_empty() {
sentences.push(format!("{fragment}{punct}"));
}
cursor = *t_end;
}
let tail = text[cursor..].trim().to_string();
if !tail.is_empty() {
sentences.push(tail);
}
sentences
}
fn strip_filler(text: &str) -> String {
let mut result = text.to_string();
let mut phrases: Vec<&str> = FILLER_PHRASES
.iter()
.chain(HEDGING_PHRASES.iter())
.copied()
.collect();
phrases.sort_by_key(|b| std::cmp::Reverse(b.len()));
phrases.dedup();
for phrase in phrases {
let escaped = regex::escape(phrase);
if let Ok(re) = Regex::new(&format!(r"(?i)\b{escaped}\b[,\s]*")) {
result = re.replace_all(&result, " ").to_string();
}
}
let collapsed = result.split_whitespace().collect::<Vec<_>>().join(" ");
collapsed
}
fn extract_entities(sentences: &[String]) -> Vec<String> {
let sentence_starters: HashSet<String> = sentences
.iter()
.filter_map(|s| s.split_whitespace().next())
.map(|w| w.to_lowercase())
.collect();
let full_text = sentences.join(" ");
let mut entities: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for cap in PROPER_NOUN_RE.captures_iter(&full_text) {
let entity = cap[1].to_string();
let entity_lower = entity.to_lowercase();
let count = PROPER_NOUN_RE
.find_iter(&full_text)
.filter(|m| full_text[m.start()..m.end()].to_lowercase() == entity_lower)
.count();
if (!sentence_starters.contains(&entity_lower) || count > 1) && seen.insert(entity.clone())
{
entities.push(entity);
}
}
for cap in NUMBER_DATE_RE.captures_iter(&full_text) {
let token = cap[1].to_string();
if seen.insert(token.clone()) {
entities.push(token);
}
}
entities
}
fn jaccard_similarity(a: &str, b: &str) -> f64 {
let set_a: HashSet<&str> = a.split_whitespace().collect();
let set_b: HashSet<&str> = b.split_whitespace().collect();
if set_a.is_empty() && set_b.is_empty() {
return 1.0;
}
let intersection = set_a.intersection(&set_b).count();
let union = set_a.union(&set_b).count();
if union == 0 {
1.0
} else {
intersection as f64 / union as f64
}
}
fn technical_tokens(text: &str) -> Vec<String> {
text.split_whitespace()
.map(|token| {
token
.trim_start_matches(|c: char| !c.is_alphanumeric() && c != '/')
.trim_end_matches(|c: char| !c.is_alphanumeric())
})
.filter(|token| !token.is_empty())
.filter(|token| token.contains('/') || token.chars().any(|c| c.is_ascii_digit()))
.map(|token| token.to_lowercase())
.collect()
}
fn has_distinct_technical_content(a: &str, b: &str) -> bool {
let a_tokens = technical_tokens(a);
let b_tokens = technical_tokens(b);
if a_tokens.is_empty() || b_tokens.is_empty() {
return false;
}
let a_set: HashSet<&str> = a_tokens.iter().map(|s| s.as_str()).collect();
let b_set: HashSet<&str> = b_tokens.iter().map(|s| s.as_str()).collect();
a_set != b_set
}
fn deduplicate_sentences(sentences: &[String]) -> Vec<String> {
let mut kept: Vec<String> = Vec::new();
'outer: for sentence in sentences {
for existing in &kept {
if jaccard_similarity(sentence, existing) > 0.6
&& !has_distinct_technical_content(sentence, existing)
{
continue 'outer;
}
}
kept.push(sentence.clone());
}
kept
}
fn extract_svo_core(sentence: &str) -> String {
let words: Vec<&str> = sentence.split_whitespace().collect();
if words.len() <= 6 {
return sentence.trim().to_string();
}
if let Some(verb_match) = COMMON_VERBS.find(sentence) {
let pre = &sentence[..verb_match.start()].trim();
let post = &sentence[verb_match.end()..].trim();
let object_words: Vec<&str> = post.split_whitespace().take(5).collect();
let object = object_words.join(" ");
let verb = verb_match.as_str();
let parts = [*pre, verb, &object]
.iter()
.filter(|p| !p.is_empty())
.copied()
.collect::<Vec<_>>();
return parts.join(" ");
}
words[..words.len().min(8)].join(" ")
}
fn extract_key_facts(sentences: &[String], entities: &[String]) -> Vec<String> {
sentences
.iter()
.filter(|s| {
let has_verb = COMMON_VERBS.is_match(s);
let s_lower = s.to_lowercase();
let has_entity = entities.iter().any(|e| s_lower.contains(&e.to_lowercase()))
|| NUMBER_DATE_RE.is_match(s);
has_verb && has_entity
})
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn default_compressor() -> SemanticCompressor {
SemanticCompressor::new(CompressionConfig::default())
}
#[test]
fn test_short_text_returned_verbatim() {
let compressor = default_compressor();
let short = "Hello world.";
assert!(short.len() < 100);
let result = compressor.compress(short);
assert_eq!(result.structured_content, short.trim());
assert!((result.ratio - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_filler_removal_reduces_content() {
let original = "I think basically you know we should sort of consider the proposal. \
Actually to be honest I believe we need to look at it more carefully. \
Kind of like the previous plan but maybe with more flexibility and scope.";
let stripped = strip_filler(original);
assert!(
stripped.len() < original.len(),
"stripped ({}) should be shorter than original ({})",
stripped.len(),
original.len()
);
assert!(
stripped.to_lowercase().contains("proposal")
|| stripped.to_lowercase().contains("consider")
);
}
#[test]
fn test_entity_extraction_proper_nouns() {
let sentences = vec![
"Alice works at Google in San Francisco.".to_string(),
"Bob joined Microsoft last year.".to_string(),
];
let entities = extract_entities(&sentences);
assert!(
!entities.is_empty(),
"expected entities, got none from: {sentences:?}"
);
}
#[test]
fn test_number_date_extraction() {
let sentences = vec![
"The project started on 2024-01-15 and costs 1500.00 dollars.".to_string(),
"There were 42 participants in 2023.".to_string(),
];
let entities = extract_entities(&sentences);
let has_number = entities
.iter()
.any(|e| e.chars().any(|c| c.is_ascii_digit()));
assert!(has_number, "expected numeric entities; got {entities:?}");
}
#[test]
fn test_deduplication_removes_near_duplicates() {
let sentences = vec![
"The cat sat on the mat.".to_string(),
"The cat sat on the mat.".to_string(), "The cat is sitting on the mat.".to_string(), "Dogs love to play in the park every afternoon.".to_string(),
];
let deduped = deduplicate_sentences(&sentences);
assert!(
deduped.len() < sentences.len(),
"deduped len {} should be < original len {}",
deduped.len(),
sentences.len()
);
assert!(deduped.iter().any(|s| s.contains("Dogs")));
}
#[test]
fn test_deduplication_preserves_distinct_technical_endpoints() {
let sentences = vec![
"Error 404 on /api/v1/memories indicates missing resource.".to_string(),
"Error 404 on /api/v1/search indicates invalid query parameters.".to_string(),
"Error 404 on /api/v1/search indicates invalid query parameters.".to_string(), ];
let deduped = deduplicate_sentences(&sentences);
assert_eq!(deduped.len(), 2);
assert!(
deduped.iter().any(|s| s.contains("/api/v1/memories")),
"expected /api/v1/memories to remain"
);
assert!(
deduped.iter().any(|s| s.contains("/api/v1/search")),
"expected /api/v1/search to remain"
);
}
#[test]
fn test_technical_tokens_normalize_trailing_punctuation() {
let tokens = technical_tokens("GET /api/v1/memories. failed for trace-123, not v1.2.3.");
assert!(tokens.contains(&"/api/v1/memories".to_string()));
assert!(!tokens.contains(&"/api/v1/memories.".to_string()));
assert!(tokens.contains(&"trace-123".to_string()));
assert!(tokens.contains(&"v1.2.3".to_string()));
}
#[test]
fn test_deduplication_preserves_superset_technical_facts() {
let sentences = vec![
"Error 404 on /api/v1/search indicates invalid query parameters.".to_string(),
"Error 404 on /api/v1/search indicates invalid query parameters after 120ms."
.to_string(),
];
let deduped = deduplicate_sentences(&sentences);
assert_eq!(deduped.len(), 2);
assert!(
deduped.iter().any(|s| s.contains("120ms")),
"expected richer technical fact to remain"
);
}
#[test]
fn test_compression_ratio_computed() {
let compressor = default_compressor();
let text = "I think basically we need to understand that the system, \
you know, is sort of designed to handle large amounts of data. \
Actually to be honest the architecture was I believe chosen to \
support scalability. At the end of the day the database stores \
records and provides search functionality for the application. \
The API layer handles authentication and rate limiting as well.";
let result = compressor.compress(text);
assert!(
result.ratio > 0.0 && result.ratio <= 1.0,
"ratio {} should be in (0, 1]",
result.ratio
);
assert_eq!(
result.ratio,
result.compressed_tokens as f32 / result.original_tokens as f32
);
}
#[test]
fn test_decompress_produces_non_empty_text() {
let compressor = default_compressor();
let text = "Alice joined Google in 2022 as a senior engineer. \
She works on distributed systems and handles large scale data pipelines. \
The team uses Rust and Go for backend services in the cloud infrastructure.";
let compressed = compressor.compress(text);
let decompressed = compressor.decompress(&compressed);
assert!(
!decompressed.is_empty(),
"decompress should produce non-empty text"
);
}
#[test]
fn test_batch_compression() {
let compressor = default_compressor();
let texts = &[
"Short text.",
"Alice works at Google as a software engineer and manages infrastructure projects in California.",
"The system provides search and storage capabilities for large enterprise applications.",
];
let results = compressor.compress_batch(texts);
assert_eq!(results.len(), texts.len());
}
#[test]
fn test_empty_input_handled() {
let compressor = default_compressor();
let result = compressor.compress("");
assert_eq!(result.original_tokens, 0);
assert_eq!(result.compressed_tokens, 0);
assert!(result.structured_content.is_empty());
assert!(result.key_entities.is_empty());
}
#[test]
fn test_whitespace_only_input_handled() {
let compressor = default_compressor();
let result = compressor.compress(" \n\t ");
assert!(result.structured_content.is_empty());
}
#[test]
fn test_jaccard_identical_sentences() {
let a = "the cat sat on the mat";
assert!((jaccard_similarity(a, a) - 1.0).abs() < 1e-9);
}
#[test]
fn test_jaccard_disjoint_sentences() {
let a = "apple orange banana";
let b = "car truck motorcycle";
assert_eq!(jaccard_similarity(a, b), 0.0);
}
#[test]
fn test_canonical_memory_unchanged_after_compression() {
let compressor = default_compressor();
let original = "Authentication is required for every request. \
Tokens rotate daily at midnight UTC. \
The incident was created by user admin at 10:14 UTC and resolved by revoking key ABC-123.";
let original_snapshot = original.to_string();
let result = compressor.compress(original);
assert_eq!(original, original_snapshot.as_str());
assert_ne!(result.structured_content, original_snapshot);
assert!(result.compressed_tokens < result.original_tokens);
}
#[test]
fn test_three_mode_compression_comparison() {
use crate::intelligence::content_utils::{soft_trim, SoftTrimConfig};
let input = "Authentication is required for every single request that comes in. \
I think basically every token rotates daily at midnight UTC, you know. \
The incident was created by user admin at 10:14 UTC and resolved by revoking key ABC-123. \
To be honest, the system is pretty much stable most of the time. \
Monitoring dashboards are updated every five minutes and alert on p99 latency spikes.";
let tokens_none = input.len() / 4;
let trim_result = soft_trim(input, &SoftTrimConfig::default());
let tokens_soft_trim = trim_result.content.len() / 4;
let compressor = default_compressor();
let semantic_result = compressor.compress(input);
let tokens_semantic = semantic_result.compressed_tokens;
assert!(
tokens_none >= tokens_soft_trim,
"soft trim should not expand: none={tokens_none} soft_trim={tokens_soft_trim}"
);
assert!(
tokens_soft_trim >= tokens_semantic,
"semantic should be at least as compact as soft-trim: soft_trim={tokens_soft_trim} semantic={tokens_semantic}"
);
assert!(
(tokens_semantic as f64) < (tokens_none as f64) * 0.9,
"semantic should reduce tokens by >10%: none={tokens_none} semantic={tokens_semantic}"
);
assert!(
input.contains("Authentication is required"),
"original input unchanged after compression"
);
}
}