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, MessageType};
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 {
76 Self {
77 llm,
78 buffer: String::new(),
79 chat_memory: ChatMessageHistory::new(),
80 max_token_limit,
81 counter: Self::default_token_counter(),
82 input_key: "input".to_string(),
83 output_key: "output".to_string(),
84 memory_key: "history".to_string(),
85 summary_prompt: DEFAULT_SUMMARY_PROMPT.to_string(),
86 return_messages: false,
87 last_summary_error: None,
88 }
89 }
90
91 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
93 self.input_key = key.into();
94 self
95 }
96
97 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
99 self.output_key = key.into();
100 self
101 }
102
103 pub fn with_memory_key(mut self, key: impl Into<String>) -> Self {
105 self.memory_key = key.into();
106 self
107 }
108
109 pub fn with_summary_prompt(mut self, prompt: impl Into<String>) -> Self {
111 self.summary_prompt = prompt.into();
112 self
113 }
114
115 pub fn with_return_messages(mut self, return_messages: bool) -> Self {
117 self.return_messages = return_messages;
118 self
119 }
120
121 pub fn with_counter(mut self, counter: Arc<dyn TokenCounter>) -> Self {
126 self.counter = counter;
127 self
128 }
129
130 pub fn set_summary(&mut self, summary: impl Into<String>) {
132 self.buffer = summary.into();
133 }
134
135 pub fn set_max_token_limit(&mut self, max_token_limit: usize) {
137 self.max_token_limit = max_token_limit;
138 }
139
140 pub fn chat_memory(&self) -> &ChatMessageHistory {
142 &self.chat_memory
143 }
144
145 pub fn chat_memory_mut(&mut self) -> &mut ChatMessageHistory {
147 &mut self.chat_memory
148 }
149
150 pub fn max_token_limit(&self) -> usize {
152 self.max_token_limit
153 }
154
155 pub async fn buffer(&self) -> String {
157 self.buffer.clone()
158 }
159
160 pub fn last_summary_error(&self) -> Option<&str> {
162 self.last_summary_error.as_deref()
163 }
164
165 fn estimate_tokens(&self, text: &str) -> usize {
167 self.counter.count_tokens(text) as usize
168 }
169
170 fn prune_messages(&self, messages: &[Message]) -> Vec<Message> {
171 let total_tokens = messages
172 .iter()
173 .map(|m| self.estimate_tokens(&m.content))
174 .sum::<usize>();
175
176 if total_tokens <= self.max_token_limit {
177 return messages.to_vec();
178 }
179
180 let mut kept_messages = Vec::new();
181 let mut current_tokens = 0;
182
183 for msg in messages.iter().rev() {
184 let msg_tokens = self.estimate_tokens(&msg.content);
185 if current_tokens + msg_tokens <= self.max_token_limit {
186 kept_messages.push(msg.clone());
187 current_tokens += msg_tokens;
188 } else {
189 break;
190 }
191 }
192
193 kept_messages.reverse();
194
195 while kept_messages
203 .first()
204 .is_some_and(|m| matches!(m.message_type, MessageType::Tool { .. }))
205 {
206 kept_messages.remove(0);
207 if kept_messages.is_empty() {
210 break;
211 }
212 }
213
214 kept_messages
215 }
216
217 fn default_token_counter() -> Arc<dyn TokenCounter> {
221 TiktokenCounter::new()
222 .map(|c| Arc::new(c) as Arc<dyn TokenCounter>)
223 .unwrap_or_else(|_| Arc::new(CharRatioCounter::new(4)) as Arc<dyn TokenCounter>)
224 }
225
226 async fn predict_new_summary(&self, new_lines: &str) -> Result<String, MemoryError> {
227 let buffer = self.buffer.clone();
228
229 let prompt = {
230 let template = PromptTemplate::new(&self.summary_prompt);
231 let mut vars: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
232 vars.insert("summary", buffer.as_str());
233 vars.insert("new_lines", new_lines);
234 template
235 .format(&vars)
236 .unwrap_or_else(|_| self.summary_prompt.clone())
237 };
238
239 let messages = vec![Message::human(&prompt)];
240
241 let result =
242 self.llm.invoke(messages, None).await.map_err(|e| {
243 MemoryError::SaveError(format!("LLM summary generation failed: {}", e))
244 })?;
245
246 Ok(result.content)
247 }
248}
249
250#[async_trait]
251impl<M: BaseChatModel + Send + Sync + 'static> BaseMemory for ConversationSummaryBufferMemory<M>
252where
253 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
254{
255 fn memory_variables(&self) -> Vec<&str> {
256 vec![&self.memory_key]
257 }
258
259 async fn load_memory_variables(
260 &self,
261 _inputs: &HashMap<String, String>,
262 ) -> Result<HashMap<String, Value>, MemoryError> {
263 let mut result = HashMap::new();
264
265 let buffer = self.buffer.clone();
266 let messages = self.chat_memory.messages();
267 let pruned = self.prune_messages(messages);
268
269 if self.return_messages {
270 let mut all_messages = Vec::new();
271
272 if !buffer.is_empty() {
273 all_messages.push(Message::system(&buffer));
274 }
275
276 all_messages.extend(pruned);
277
278 let messages_value: Vec<Value> = all_messages
279 .iter()
280 .map(|m| serde_json::to_value(m).unwrap_or(Value::Null))
281 .collect();
282
283 result.insert(self.memory_key.clone(), Value::Array(messages_value));
284 } else {
285 let mut history = String::new();
286
287 if !buffer.is_empty() {
288 history.push_str(&format!("Summary: {}\n\n", buffer));
289 }
290
291 for msg in &pruned {
292 let role = match msg.message_type {
293 lc_schema::MessageType::Human => "Human",
294 lc_schema::MessageType::AI => "AI",
295 lc_schema::MessageType::System => "System",
296 lc_schema::MessageType::Tool { .. } => "Tool",
297 };
298 history.push_str(&format!("{}: {}\n", role, msg.content));
299 }
300
301 result.insert(self.memory_key.clone(), Value::String(history));
302 }
303
304 Ok(result)
305 }
306
307 async fn save_context(
308 &mut self,
309 inputs: &HashMap<String, String>,
310 outputs: &HashMap<String, String>,
311 ) -> Result<(), MemoryError> {
312 let input = inputs.get(&self.input_key).ok_or_else(|| {
315 MemoryError::SaveError(format!("Missing input key '{}'", self.input_key))
316 })?;
317 let output = outputs.get(&self.output_key).ok_or_else(|| {
318 MemoryError::SaveError(format!("Missing output key '{}'", self.output_key))
319 })?;
320
321 self.chat_memory.add_user_message(input);
322 self.chat_memory.add_ai_message(output);
323
324 let messages = self.chat_memory.messages();
325 let total_tokens = messages
326 .iter()
327 .map(|m| self.estimate_tokens(&m.content))
328 .sum::<usize>();
329
330 if total_tokens > self.max_token_limit {
331 let pruned = self.prune_messages(messages);
332
333 let pruned_count = pruned.len();
334
335 if messages.len() > pruned_count {
336 let messages_to_summarize: Vec<&Message> = messages
337 .iter()
338 .take(messages.len() - pruned_count)
339 .collect();
340
341 if !messages_to_summarize.is_empty() {
342 let new_lines: String = messages_to_summarize
343 .iter()
344 .map(|m| {
345 let role = match m.message_type {
346 lc_schema::MessageType::Human => "Human",
347 lc_schema::MessageType::AI => "AI",
348 lc_schema::MessageType::System => "System",
349 lc_schema::MessageType::Tool { .. } => "Tool",
350 };
351 format!("{}: {}", role, m.content)
352 })
353 .collect::<Vec<_>>()
354 .join("\n");
355
356 match self.predict_new_summary(&new_lines).await {
360 Ok(new_summary) => {
361 self.buffer = new_summary;
362 self.last_summary_error = None;
363
364 self.chat_memory.clear();
365 for msg in pruned {
366 if matches!(msg.message_type, lc_schema::MessageType::Human) {
367 self.chat_memory.add_user_message(&msg.content);
368 } else if matches!(msg.message_type, lc_schema::MessageType::AI) {
369 self.chat_memory.add_ai_message(&msg.content);
370 } else if matches!(msg.message_type, lc_schema::MessageType::System)
371 {
372 self.chat_memory.add_system_message(&msg.content);
374 }
375 }
376 }
377 Err(e) => {
378 self.last_summary_error = Some(e.to_string());
379 log::warn!(
380 "ConversationSummaryBufferMemory summarization failed, keeping old summary and original messages for next retry: {}",
381 e
382 );
383 }
384 }
385 }
386 }
387 }
388
389 Ok(())
390 }
391
392 async fn clear(&mut self) -> Result<(), MemoryError> {
393 self.buffer = String::new();
394 self.chat_memory.clear();
395 self.last_summary_error = None;
396 Ok(())
397 }
398}
399
400impl<M: BaseChatModel + Send + Sync + 'static> BaseChatMemory for ConversationSummaryBufferMemory<M>
402where
403 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
404{
405 fn messages(&self) -> &[Message] {
406 self.chat_memory.messages()
407 }
408
409 fn add_message(&mut self, message: Message) {
410 self.chat_memory.add_message(message);
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::test_support::MockLlm;
418 use lc_providers::{OpenAIChat, OpenAIConfig};
419
420 fn create_test_config() -> OpenAIConfig {
421 OpenAIConfig::default()
422 }
423
424 #[test]
425 fn test_new() {
426 let llm = OpenAIChat::new(create_test_config());
427 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
428 ConversationSummaryBufferMemory::new(llm, 1000);
429
430 assert_eq!(memory.memory_variables(), vec!["history"]);
431 assert_eq!(memory.max_token_limit(), 1000);
432 }
433
434 #[test]
435 fn test_with_options() {
436 let llm = OpenAIChat::new(create_test_config());
437 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
438 ConversationSummaryBufferMemory::new(llm, 500)
439 .with_input_key("question")
440 .with_output_key("answer")
441 .with_memory_key("context")
442 .with_return_messages(true);
443
444 assert_eq!(memory.input_key, "question");
445 assert_eq!(memory.output_key, "answer");
446 assert_eq!(memory.memory_key, "context");
447 assert!(memory.return_messages);
448 }
449
450 #[test]
451 fn test_estimate_tokens_uses_default_counter() {
452 let llm = OpenAIChat::new(create_test_config());
455 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
456 ConversationSummaryBufferMemory::new(llm, 1000);
457
458 let text1 = "Hello";
459 let text2 = "Hello World";
460 let text3 = "This is some Chinese text";
461
462 assert!(memory.estimate_tokens(text1) > 0);
463 assert!(memory.estimate_tokens(text2) > memory.estimate_tokens(text1));
464 assert!(memory.estimate_tokens(text3) > 0);
465 }
466
467 #[test]
468 fn test_with_counter_injection() {
469 let llm = OpenAIChat::new(create_test_config());
471 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
472 ConversationSummaryBufferMemory::new(llm, 1000)
473 .with_counter(std::sync::Arc::new(CharRatioCounter::new(4)));
474
475 assert_eq!(memory.estimate_tokens("abcdefgh"), 2);
476 }
477
478 #[tokio::test]
479 async fn test_set_summary_and_token_limit() {
480 let llm = OpenAIChat::new(create_test_config());
482 let mut memory: ConversationSummaryBufferMemory<OpenAIChat> =
483 ConversationSummaryBufferMemory::new(llm, 1000);
484
485 memory.set_summary("previous summary".to_string());
486 assert_eq!(memory.buffer().await, "previous summary");
487
488 memory.set_max_token_limit(500);
489 assert_eq!(memory.max_token_limit(), 500);
490 }
491
492 #[test]
493 fn test_prune_messages_within_limit() {
494 let llm = OpenAIChat::new(create_test_config());
495 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
496 ConversationSummaryBufferMemory::new(llm, 1000);
497
498 let messages = vec![
499 Message::human("Short message 1"),
500 Message::ai("Short reply 1"),
501 ];
502
503 let pruned = memory.prune_messages(&messages);
504
505 assert_eq!(pruned.len(), 2);
506 }
507
508 #[test]
513 fn test_prune_never_opens_on_orphan_tool() {
514 let llm = MockLlm::new(vec![Ok("summary".to_string())]);
515 let memory: ConversationSummaryBufferMemory<MockLlm> =
517 ConversationSummaryBufferMemory::new(llm, 40)
518 .with_counter(std::sync::Arc::new(CharRatioCounter::new(4)));
519
520 let messages = vec![
524 Message::ai("z".repeat(1000)), Message {
526 content: "r".repeat(40),
527 message_type: MessageType::Tool {
528 tool_call_id: "c1".into(),
529 },
530 ..Message::human("")
531 },
532 Message::human("m".repeat(40)),
533 Message::ai("f".repeat(40)),
534 ];
535
536 let pruned = memory.prune_messages(&messages);
537
538 assert!(
540 !matches!(
541 pruned.first().map(|m| &m.message_type),
542 Some(MessageType::Tool { .. })
543 ),
544 "pruned window started on an orphaned Tool message: {:?}",
545 pruned.first().map(|m| &m.message_type)
546 );
547 assert!(!pruned.is_empty());
549 assert!(matches!(pruned[0].message_type, MessageType::Human));
550 assert!(matches!(pruned[1].message_type, MessageType::AI));
551 }
552
553 #[tokio::test]
554 async fn test_buffer_initial_empty() {
555 let llm = OpenAIChat::new(create_test_config());
556 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
557 ConversationSummaryBufferMemory::new(llm, 1000);
558
559 let buffer = memory.buffer().await;
560 assert!(buffer.is_empty());
561 }
562
563 #[tokio::test]
564 async fn test_load_memory_variables_empty() {
565 let llm = OpenAIChat::new(create_test_config());
566 let memory: ConversationSummaryBufferMemory<OpenAIChat> =
567 ConversationSummaryBufferMemory::new(llm, 1000);
568
569 let vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
570 let history = vars.get("history").unwrap().as_str().unwrap();
571
572 assert!(history.is_empty());
573 }
574
575 #[tokio::test]
576 async fn test_clear() {
577 let llm = OpenAIChat::new(create_test_config());
578 let mut memory: ConversationSummaryBufferMemory<OpenAIChat> =
579 ConversationSummaryBufferMemory::new(llm, 1000);
580
581 memory.chat_memory.add_user_message("test");
582 memory.chat_memory.add_ai_message("reply");
583
584 memory.buffer = "Test summary".to_string();
585
586 memory.clear().await.unwrap();
587
588 assert!(memory.buffer().await.is_empty());
589 assert_eq!(memory.chat_memory().len(), 0);
590 }
591
592 #[tokio::test]
595 async fn test_prune_summary_failure_keeps_messages_and_retries() {
596 let llm = MockLlm::new(vec![
598 Ok("summary-b".to_string()),
599 Err("summarizer down".to_string()),
600 ]);
601 let mut memory: ConversationSummaryBufferMemory<MockLlm> =
603 ConversationSummaryBufferMemory::new(llm, 5)
604 .with_counter(std::sync::Arc::new(CharRatioCounter::new(4)));
605
606 let long_input =
607 "这是一段足够长的中文消息,用来确保本轮消息总 token 数超过预算并触发剪枝总结逻辑";
608 let inputs = HashMap::from([("input".to_string(), long_input.to_string())]);
609 let outputs = HashMap::from([("output".to_string(), long_input.to_string())]);
610
611 memory.save_context(&inputs, &outputs).await.unwrap();
613 assert!(memory.buffer().await.is_empty(), "失败时不应覆盖旧摘要");
614 assert!(memory
615 .last_summary_error()
616 .unwrap()
617 .contains("summarizer down"));
618 assert_eq!(memory.chat_memory().len(), 2);
620
621 memory.save_context(&inputs, &outputs).await.unwrap();
623 assert_eq!(memory.buffer().await, "summary-b");
624 assert!(memory.last_summary_error().is_none());
625 }
626}