#[cfg(feature = "distill-http")]
pub mod openai_compat;
use serde::{Deserialize, Serialize};
use crate::traits::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistilledMemory {
pub summary: String,
pub facts: Vec<Fact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fact {
pub kind: FactKind,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum FactKind {
Decision,
Correction,
Preference,
State,
}
pub const MAX_FACTS: usize = 64;
pub const MAX_FACT_CHARS: usize = 2_048;
pub const MAX_SUMMARY_CHARS: usize = 8_192;
fn truncate_keep_start(text: &str, max_chars: usize) -> &str {
match text.char_indices().nth(max_chars) {
Some((byte_idx, _)) => &text[..byte_idx],
None => text,
}
}
pub(crate) fn cap_distilled_memory(memory: DistilledMemory) -> DistilledMemory {
let DistilledMemory { summary, mut facts } = memory;
let summary = truncate_keep_start(&summary, MAX_SUMMARY_CHARS).to_owned();
facts.truncate(MAX_FACTS);
for fact in &mut facts {
if fact.text.chars().count() > MAX_FACT_CHARS {
fact.text = truncate_keep_start(&fact.text, MAX_FACT_CHARS).to_owned();
}
}
DistilledMemory { summary, facts }
}
pub trait Distiller: Send + Sync {
fn distill(&self, transcript: &str) -> Result<DistilledMemory>;
}
#[cfg(test)]
mod tests {
use super::{
cap_distilled_memory, DistilledMemory, Fact, FactKind, MAX_FACTS, MAX_FACT_CHARS,
MAX_SUMMARY_CHARS,
};
fn fact(text: impl Into<String>) -> Fact {
Fact {
kind: FactKind::State,
text: text.into(),
}
}
#[test]
fn cap_drops_excess_facts() {
let facts = (0..MAX_FACTS + 50)
.map(|i| fact(format!("fact {i}")))
.collect();
let memory = DistilledMemory {
summary: "summary".to_owned(),
facts,
};
let capped = cap_distilled_memory(memory);
assert_eq!(capped.facts.len(), MAX_FACTS);
}
#[test]
fn cap_truncates_long_fact_text() {
let long_text = "a".repeat(MAX_FACT_CHARS + 1000);
let memory = DistilledMemory {
summary: "summary".to_owned(),
facts: vec![fact(long_text.clone())],
};
let capped = cap_distilled_memory(memory);
assert_eq!(capped.facts[0].text.chars().count(), MAX_FACT_CHARS);
assert!(long_text.starts_with(&capped.facts[0].text));
}
#[test]
fn cap_truncates_long_summary() {
let long_summary = "b".repeat(MAX_SUMMARY_CHARS + 1000);
let memory = DistilledMemory {
summary: long_summary.clone(),
facts: vec![],
};
let capped = cap_distilled_memory(memory);
assert_eq!(capped.summary.chars().count(), MAX_SUMMARY_CHARS);
assert!(long_summary.starts_with(&capped.summary));
}
#[test]
fn cap_respects_char_boundaries() {
let long_text = "é".repeat(MAX_FACT_CHARS + 10);
let memory = DistilledMemory {
summary: "🎉".repeat(MAX_SUMMARY_CHARS + 10),
facts: vec![fact(long_text)],
};
let capped = cap_distilled_memory(memory);
assert_eq!(capped.facts[0].text.chars().count(), MAX_FACT_CHARS);
assert_eq!(capped.summary.chars().count(), MAX_SUMMARY_CHARS);
}
#[test]
fn cap_leaves_compliant_memory_unchanged() {
let memory = DistilledMemory {
summary: "A short summary.".to_owned(),
facts: vec![fact("A short fact.")],
};
let capped = cap_distilled_memory(memory.clone());
assert_eq!(capped.summary, memory.summary);
assert_eq!(capped.facts.len(), memory.facts.len());
assert_eq!(capped.facts[0].text, memory.facts[0].text);
}
}