ds_api/conversation/
summarizer.rs1use crate::raw::request::message::{Message, Role};
7
8pub trait Summarizer: Send + Sync {
12 fn should_summarize(&self, history: &[Message]) -> bool;
14
15 fn summarize(&self, history: &mut Vec<Message>) -> Option<Message>;
19}
20
21#[derive(Clone, Debug)]
27pub struct TokenBasedSummarizer {
28 pub threshold: usize,
29 pub retain_last: usize,
30 pub max_summary_chars: usize,
31}
32
33impl Default for TokenBasedSummarizer {
34 fn default() -> Self {
35 Self {
36 threshold: 80_000,
37 retain_last: 10,
38 max_summary_chars: 2_000, }
40 }
41}
42
43impl TokenBasedSummarizer {
44 fn estimate_tokens(history: &[Message]) -> usize {
47 history
49 .iter()
50 .filter(|m| !matches!(m.role, Role::System))
51 .filter_map(|m| m.content.as_ref())
52 .map(|s| {
53 s.chars()
54 .map(|c| if c.is_ascii() { 1 } else { 4 })
55 .sum::<usize>()
56 })
57 .sum::<usize>()
58 / 4
59 }
60}
61
62impl Summarizer for TokenBasedSummarizer {
63 fn should_summarize(&self, history: &[Message]) -> bool {
64 let est = Self::estimate_tokens(history);
65 est >= self.threshold
66 }
67
68 fn summarize(&self, history: &mut Vec<Message>) -> Option<Message> {
69 if history.len() <= self.retain_last {
70 return None;
71 }
72
73 let split = history.len().saturating_sub(self.retain_last);
75 if split == 0 {
76 return None;
77 }
78
79 let older: Vec<String> = history.drain(0..split).filter_map(|m| m.content).collect();
81
82 if older.is_empty() {
83 return None;
85 }
86
87 let joined = older.join("\n");
89 let summary_text = if joined.len() > self.max_summary_chars {
90 let mut s = joined;
91 s.truncate(self.max_summary_chars);
92 format!("Short summary of earlier conversation:\n{}\n(Truncated)", s)
93 } else {
94 format!("Short summary of earlier conversation:\n{}", joined)
95 };
96
97 let mut summary_msg = Message::new(Role::System, summary_text.as_str());
99 summary_msg.name = Some("[auto-summary]".to_string());
101
102 history.insert(0, summary_msg.clone());
103 Some(summary_msg)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::raw::request::message::{Message, Role};
111
112 #[test]
113 fn summarizer_should_trigger_and_replace() {
114 let mut hist = vec![
115 Message::new(Role::User, "User message 1"),
116 Message::new(Role::Assistant, "Assistant reply 1"),
117 Message::new(Role::User, "User message 2"),
118 Message::new(Role::Assistant, "Assistant reply 2"),
119 ];
120
121 let summ = TokenBasedSummarizer {
123 threshold: 0, retain_last: 1,
125 max_summary_chars: 100,
126 };
127
128 assert!(summ.should_summarize(&hist));
129 let maybe_summary = summ.summarize(&mut hist);
130 assert!(maybe_summary.is_some());
131 assert!(hist.len() <= (1 + 1));
133 assert!(matches!(hist[0].role, Role::System));
135 }
136}