1use async_trait::async_trait;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use super::base::{BaseChatMemory, BaseMemory, ChatMessageHistory, MemoryError};
12use lc_core::language_models::BaseChatModel;
13use lc_core::language_models::LLMResult;
14use lc_core::runnables::Runnable;
15use lc_core::token_counter::{CharRatioCounter, TiktokenCounter, TokenCounter};
16use lc_prompts::PromptTemplate;
17use lc_schema::Message;
18
19const DEFAULT_SUMMARY_PROMPT: &str =
20 "Progressively summarize the conversation, adding new content to the previous summary.
21
22Current summary:
23{summary}
24
25New lines of conversation:
26{new_lines}
27
28New summary:";
29
30pub struct ConversationSummaryBufferMemory<M: BaseChatModel> {
49 llm: M,
50
51 buffer: String,
53 chat_memory: ChatMessageHistory,
54
55 max_token_limit: usize,
56
57 counter: Arc<dyn TokenCounter>,
60
61 input_key: String,
62 output_key: String,
63 memory_key: String,
64
65 summary_prompt: String,
66 return_messages: bool,
67
68 last_summary_error: Option<String>,
71}
72
73impl<M: BaseChatModel> ConversationSummaryBufferMemory<M> {
74 pub fn new(llm: M, max_token_limit: usize) -> Self {
75 Self {
76 llm,
77 buffer: String::new(),
78 chat_memory: ChatMessageHistory::new(),
79 max_token_limit,
80 counter: Self::default_token_counter(),
81 input_key: "input".to_string(),
82 output_key: "output".to_string(),
83 memory_key: "history".to_string(),
84 summary_prompt: DEFAULT_SUMMARY_PROMPT.to_string(),
85 return_messages: false,
86 last_summary_error: None,
87 }
88 }
89
90 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
91 self.input_key = key.into();
92 self
93 }
94
95 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
96 self.output_key = key.into();
97 self
98 }
99
100 pub fn with_memory_key(mut self, key: impl Into<String>) -> Self {
101 self.memory_key = key.into();
102 self
103 }
104
105 pub fn with_summary_prompt(mut self, prompt: impl Into<String>) -> Self {
106 self.summary_prompt = prompt.into();
107 self
108 }
109
110 pub fn with_return_messages(mut self, return_messages: bool) -> Self {
111 self.return_messages = return_messages;
112 self
113 }
114
115 pub fn with_counter(mut self, counter: Arc<dyn TokenCounter>) -> Self {
120 self.counter = counter;
121 self
122 }
123
124 pub fn set_summary(&mut self, summary: String) {
126 self.buffer = summary;
127 }
128
129 pub fn set_max_token_limit(&mut self, max_token_limit: usize) {
131 self.max_token_limit = max_token_limit;
132 }
133
134 pub fn chat_memory(&self) -> &ChatMessageHistory {
135 &self.chat_memory
136 }
137
138 pub fn chat_memory_mut(&mut self) -> &mut ChatMessageHistory {
139 &mut self.chat_memory
140 }
141
142 pub fn max_token_limit(&self) -> usize {
143 self.max_token_limit
144 }
145
146 pub async fn buffer(&self) -> String {
147 self.buffer.clone()
148 }
149
150 pub fn last_summary_error(&self) -> Option<&str> {
152 self.last_summary_error.as_deref()
153 }
154
155 fn estimate_tokens(&self, text: &str) -> usize {
157 self.counter.count_tokens(text) as usize
158 }
159
160 fn prune_messages(&self, messages: &[Message]) -> Vec<Message> {
161 let total_tokens = messages
162 .iter()
163 .map(|m| self.estimate_tokens(&m.content))
164 .sum::<usize>();
165
166 if total_tokens <= self.max_token_limit {
167 return messages.to_vec();
168 }
169
170 let mut kept_messages = Vec::new();
171 let mut current_tokens = 0;
172
173 for msg in messages.iter().rev() {
174 let msg_tokens = self.estimate_tokens(&msg.content);
175 if current_tokens + msg_tokens <= self.max_token_limit {
176 kept_messages.push(msg.clone());
177 current_tokens += msg_tokens;
178 } else {
179 break;
180 }
181 }
182
183 kept_messages.reverse();
184 kept_messages
185 }
186
187 fn default_token_counter() -> Arc<dyn TokenCounter> {
191 TiktokenCounter::new()
192 .map(|c| Arc::new(c) as Arc<dyn TokenCounter>)
193 .unwrap_or_else(|_| Arc::new(CharRatioCounter::new(4)) as Arc<dyn TokenCounter>)
194 }
195
196 async fn predict_new_summary(&self, new_lines: &str) -> Result<String, MemoryError> {
197 let buffer = self.buffer.clone();
198
199 let prompt = {
200 let template = PromptTemplate::new(&self.summary_prompt);
201 let mut vars: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
202 vars.insert("summary", buffer.as_str());
203 vars.insert("new_lines", new_lines);
204 template
205 .format(&vars)
206 .unwrap_or_else(|_| self.summary_prompt.clone())
207 };
208
209 let messages = vec![Message::human(&prompt)];
210
211 let result =
212 self.llm.invoke(messages, None).await.map_err(|e| {
213 MemoryError::SaveError(format!("LLM summary generation failed: {}", e))
214 })?;
215
216 Ok(result.content)
217 }
218}
219
220#[async_trait]
221impl<M: BaseChatModel + Send + Sync + 'static> BaseMemory for ConversationSummaryBufferMemory<M>
222where
223 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
224{
225 fn memory_variables(&self) -> Vec<&str> {
226 vec![&self.memory_key]
227 }
228
229 async fn load_memory_variables(
230 &self,
231 _inputs: &HashMap<String, String>,
232 ) -> Result<HashMap<String, Value>, MemoryError> {
233 let mut result = HashMap::new();
234
235 let buffer = self.buffer.clone();
236 let messages = self.chat_memory.messages();
237 let pruned = self.prune_messages(messages);
238
239 if self.return_messages {
240 let mut all_messages = Vec::new();
241
242 if !buffer.is_empty() {
243 all_messages.push(Message::system(&buffer));
244 }
245
246 all_messages.extend(pruned);
247
248 let messages_value: Vec<Value> = all_messages
249 .iter()
250 .map(|m| serde_json::to_value(m).unwrap_or(Value::Null))
251 .collect();
252
253 result.insert(self.memory_key.clone(), Value::Array(messages_value));
254 } else {
255 let mut history = String::new();
256
257 if !buffer.is_empty() {
258 history.push_str(&format!("Summary: {}\n\n", buffer));
259 }
260
261 for msg in &pruned {
262 let role = match msg.message_type {
263 lc_schema::MessageType::Human => "Human",
264 lc_schema::MessageType::AI => "AI",
265 lc_schema::MessageType::System => "System",
266 lc_schema::MessageType::Tool { .. } => "Tool",
267 };
268 history.push_str(&format!("{}: {}\n", role, msg.content));
269 }
270
271 result.insert(self.memory_key.clone(), Value::String(history));
272 }
273
274 Ok(result)
275 }
276
277 async fn save_context(
278 &mut self,
279 inputs: &HashMap<String, String>,
280 outputs: &HashMap<String, String>,
281 ) -> Result<(), MemoryError> {
282 let input = inputs.get(&self.input_key).ok_or_else(|| {
285 MemoryError::SaveError(format!("Missing input key '{}'", self.input_key))
286 })?;
287 let output = outputs.get(&self.output_key).ok_or_else(|| {
288 MemoryError::SaveError(format!("Missing output key '{}'", self.output_key))
289 })?;
290
291 self.chat_memory.add_user_message(input);
292 self.chat_memory.add_ai_message(output);
293
294 let messages = self.chat_memory.messages();
295 let total_tokens = messages
296 .iter()
297 .map(|m| self.estimate_tokens(&m.content))
298 .sum::<usize>();
299
300 if total_tokens > self.max_token_limit {
301 let pruned = self.prune_messages(messages);
302
303 let pruned_count = pruned.len();
304
305 if messages.len() > pruned_count {
306 let messages_to_summarize: Vec<&Message> = messages
307 .iter()
308 .take(messages.len() - pruned_count)
309 .collect();
310
311 if !messages_to_summarize.is_empty() {
312 let new_lines: String = messages_to_summarize
313 .iter()
314 .map(|m| {
315 let role = match m.message_type {
316 lc_schema::MessageType::Human => "Human",
317 lc_schema::MessageType::AI => "AI",
318 lc_schema::MessageType::System => "System",
319 lc_schema::MessageType::Tool { .. } => "Tool",
320 };
321 format!("{}: {}", role, m.content)
322 })
323 .collect::<Vec<_>>()
324 .join("\n");
325
326 match self.predict_new_summary(&new_lines).await {
330 Ok(new_summary) => {
331 self.buffer = new_summary;
332 self.last_summary_error = None;
333
334 self.chat_memory.clear();
335 for msg in pruned {
336 if matches!(msg.message_type, lc_schema::MessageType::Human) {
337 self.chat_memory.add_user_message(&msg.content);
338 } else if matches!(msg.message_type, lc_schema::MessageType::AI) {
339 self.chat_memory.add_ai_message(&msg.content);
340 } else if matches!(msg.message_type, lc_schema::MessageType::System)
341 {
342 self.chat_memory.add_system_message(&msg.content);
344 }
345 }
346 }
347 Err(e) => {
348 self.last_summary_error = Some(e.to_string());
349 log::warn!(
350 "ConversationSummaryBufferMemory 摘要失败,保留旧摘要与原始消息待下轮重试: {}",
351 e
352 );
353 }
354 }
355 }
356 }
357 }
358
359 Ok(())
360 }
361
362 async fn clear(&mut self) -> Result<(), MemoryError> {
363 self.buffer = String::new();
364 self.chat_memory.clear();
365 self.last_summary_error = None;
366 Ok(())
367 }
368}
369
370impl<M: BaseChatModel + Send + Sync + 'static> BaseChatMemory for ConversationSummaryBufferMemory<M>
372where
373 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
374{
375 fn messages(&self) -> &[Message] {
376 self.chat_memory.messages()
377 }
378
379 fn add_message(&mut self, message: Message) {
380 self.chat_memory.add_message(message);
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::test_support::MockLlm;
388 use lc_providers::{OpenAIChat, OpenAIConfig};
389
390 fn create_test_config() -> OpenAIConfig {
391 OpenAIConfig::default()
392 }
393
394 #[test]
395 fn test_new() {
396 let llm = OpenAIChat::new(create_test_config());
397 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
398 ConversationSummaryBufferMemory::new(llm, 1000);
399
400 assert_eq!(memory.memory_variables(), vec!["history"]);
401 assert_eq!(memory.max_token_limit(), 1000);
402 }
403
404 #[test]
405 fn test_with_options() {
406 let llm = OpenAIChat::new(create_test_config());
407 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
408 ConversationSummaryBufferMemory::new(llm, 500)
409 .with_input_key("question")
410 .with_output_key("answer")
411 .with_memory_key("context")
412 .with_return_messages(true);
413
414 assert_eq!(memory.input_key, "question");
415 assert_eq!(memory.output_key, "answer");
416 assert_eq!(memory.memory_key, "context");
417 assert!(memory.return_messages);
418 }
419
420 #[test]
421 fn test_estimate_tokens_uses_default_counter() {
422 let llm = OpenAIChat::new(create_test_config());
425 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
426 ConversationSummaryBufferMemory::new(llm, 1000);
427
428 let text1 = "Hello";
429 let text2 = "Hello World";
430 let text3 = "This is some Chinese text";
431
432 assert!(memory.estimate_tokens(text1) > 0);
433 assert!(memory.estimate_tokens(text2) > memory.estimate_tokens(text1));
434 assert!(memory.estimate_tokens(text3) > 0);
435 }
436
437 #[test]
438 fn test_with_counter_injection() {
439 let llm = OpenAIChat::new(create_test_config());
441 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
442 ConversationSummaryBufferMemory::new(llm, 1000)
443 .with_counter(std::sync::Arc::new(CharRatioCounter::new(4)));
444
445 assert_eq!(memory.estimate_tokens("abcdefgh"), 2);
446 }
447
448 #[tokio::test]
449 async fn test_set_summary_and_token_limit() {
450 let llm = OpenAIChat::new(create_test_config());
452 let mut memory: ConversationSummaryBufferMemory<OpenAIChat> =
453 ConversationSummaryBufferMemory::new(llm, 1000);
454
455 memory.set_summary("previous summary".to_string());
456 assert_eq!(memory.buffer().await, "previous summary");
457
458 memory.set_max_token_limit(500);
459 assert_eq!(memory.max_token_limit(), 500);
460 }
461
462 #[test]
463 fn test_prune_messages_within_limit() {
464 let llm = OpenAIChat::new(create_test_config());
465 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
466 ConversationSummaryBufferMemory::new(llm, 1000);
467
468 let messages = vec![
469 Message::human("Short message 1"),
470 Message::ai("Short reply 1"),
471 ];
472
473 let pruned = memory.prune_messages(&messages);
474
475 assert_eq!(pruned.len(), 2);
476 }
477
478 #[tokio::test]
479 async fn test_buffer_initial_empty() {
480 let llm = OpenAIChat::new(create_test_config());
481 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
482 ConversationSummaryBufferMemory::new(llm, 1000);
483
484 let buffer = memory.buffer().await;
485 assert!(buffer.is_empty());
486 }
487
488 #[tokio::test]
489 async fn test_load_memory_variables_empty() {
490 let llm = OpenAIChat::new(create_test_config());
491 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
492 ConversationSummaryBufferMemory::new(llm, 1000);
493
494 let vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
495 let history = vars.get("history").unwrap().as_str().unwrap();
496
497 assert!(history.is_empty());
498 }
499
500 #[tokio::test]
501 async fn test_clear() {
502 let llm = OpenAIChat::new(create_test_config());
503 let mut memory: ConversationSummaryBufferMemory<OpenAIChat> =
504 ConversationSummaryBufferMemory::new(llm, 1000);
505
506 memory.chat_memory.add_user_message("test");
507 memory.chat_memory.add_ai_message("reply");
508
509 memory.buffer = "Test summary".to_string();
510
511 memory.clear().await.unwrap();
512
513 assert!(memory.buffer().await.is_empty());
514 assert_eq!(memory.chat_memory().len(), 0);
515 }
516
517 #[tokio::test]
520 async fn test_prune_summary_failure_keeps_messages_and_retries() {
521 let llm = MockLlm::new(vec![
523 Ok("summary-b".to_string()),
524 Err("summarizer down".to_string()),
525 ]);
526 let mut memory: ConversationSummaryBufferMemory<MockLlm> =
528 ConversationSummaryBufferMemory::new(llm, 5)
529 .with_counter(std::sync::Arc::new(CharRatioCounter::new(4)));
530
531 let long_input =
532 "这是一段足够长的中文消息,用来确保本轮消息总 token 数超过预算并触发剪枝总结逻辑";
533 let inputs = HashMap::from([("input".to_string(), long_input.to_string())]);
534 let outputs = HashMap::from([("output".to_string(), long_input.to_string())]);
535
536 memory.save_context(&inputs, &outputs).await.unwrap();
538 assert!(memory.buffer().await.is_empty(), "失败时不应覆盖旧摘要");
539 assert!(memory
540 .last_summary_error()
541 .unwrap()
542 .contains("summarizer down"));
543 assert_eq!(memory.chat_memory().len(), 2);
545
546 memory.save_context(&inputs, &outputs).await.unwrap();
548 assert_eq!(memory.buffer().await, "summary-b");
549 assert!(memory.last_summary_error().is_none());
550 }
551}