Skip to main content

lc_memory/
window.rs

1// lc-memory/src/window.rs
2//! Conversation Buffer Window Memory
3//!
4//! Conversation memory with window, keeping only the last k rounds.
5
6use async_trait::async_trait;
7use serde_json::Value;
8use std::collections::HashMap;
9
10use super::base::{BaseChatMemory, BaseMemory, ChatMessageHistory, MemoryError};
11use lc_schema::Message;
12
13/// Conversation Buffer Window Memory
14///
15/// Keeps only the last k rounds of conversation to avoid overly long context.
16///
17/// # Example
18/// ```ignore
19/// use lc_memory::ConversationBufferWindowMemory;
20///
21/// // Keep only the last 2 rounds
22/// let mut memory = ConversationBufferWindowMemory::new(2);
23/// ```
24#[derive(Debug)]
25pub struct ConversationBufferWindowMemory {
26    /// Chat history
27    chat_memory: ChatMessageHistory,
28
29    /// Window size (keep last k rounds, default 5)
30    k: usize,
31
32    /// Input key name
33    input_key: String,
34
35    /// Output key name
36    output_key: String,
37
38    /// Memory variable name
39    memory_key: String,
40
41    /// Whether to return message objects
42    return_messages: bool,
43}
44
45impl ConversationBufferWindowMemory {
46    /// Create new window memory
47    ///
48    /// # Arguments
49    /// * `k` - Keep last k rounds (each round includes user message and AI message)
50    pub fn new(k: usize) -> Self {
51        Self {
52            chat_memory: ChatMessageHistory::new(),
53            k,
54            input_key: "input".to_string(),
55            output_key: "output".to_string(),
56            memory_key: "history".to_string(),
57            return_messages: false,
58        }
59    }
60
61    /// Set input key name
62    pub fn with_input_key(mut self, key: String) -> Self {
63        self.input_key = key;
64        self
65    }
66
67    /// Set output key name
68    pub fn with_output_key(mut self, key: String) -> Self {
69        self.output_key = key;
70        self
71    }
72
73    /// Set memory variable name
74    pub fn with_memory_key(mut self, key: String) -> Self {
75        self.memory_key = key;
76        self
77    }
78
79    /// Set whether to return message objects
80    pub fn with_return_messages(mut self, return_messages: bool) -> Self {
81        self.return_messages = return_messages;
82        self
83    }
84
85    /// Get chat history
86    pub fn chat_memory(&self) -> &ChatMessageHistory {
87        &self.chat_memory
88    }
89
90    /// Get window size
91    pub fn k(&self) -> usize {
92        self.k
93    }
94
95    /// Get messages within the window
96    ///
97    /// Only keeps the last k rounds (2*k messages)
98    fn get_window_messages(&self) -> Vec<Message> {
99        let messages = self.chat_memory.messages();
100        let total = messages.len();
101
102        // Each round includes 2 messages (user + AI)
103        let max_messages = self.k * 2;
104
105        if total <= max_messages {
106            messages.to_vec()
107        } else {
108            messages[total - max_messages..].to_vec()
109        }
110    }
111
112    /// Convert to string
113    fn buffer_as_string(&self) -> String {
114        self.get_window_messages()
115            .iter()
116            .map(|msg| {
117                let role = match msg.message_type {
118                    lc_schema::MessageType::Human => "Human",
119                    lc_schema::MessageType::AI => "AI",
120                    lc_schema::MessageType::System => "System",
121                    lc_schema::MessageType::Tool { .. } => "Tool",
122                };
123                format!("{}: {}", role, msg.content)
124            })
125            .collect::<Vec<_>>()
126            .join("\n")
127    }
128}
129
130impl Default for ConversationBufferWindowMemory {
131    fn default() -> Self {
132        Self::new(5)
133    }
134}
135
136/// P0-1: `ConversationBufferWindowMemory` 实现 `BaseChatMemory`。
137impl BaseChatMemory for ConversationBufferWindowMemory {
138    fn messages(&self) -> &[Message] {
139        self.chat_memory.messages()
140    }
141
142    fn add_message(&mut self, message: Message) {
143        self.chat_memory.add_message(message);
144    }
145}
146
147#[async_trait]
148impl BaseMemory for ConversationBufferWindowMemory {
149    fn memory_variables(&self) -> Vec<&str> {
150        vec![&self.memory_key]
151    }
152
153    async fn load_memory_variables(
154        &self,
155        _inputs: &HashMap<String, String>,
156    ) -> Result<HashMap<String, Value>, MemoryError> {
157        let mut result = HashMap::new();
158
159        if self.return_messages {
160            let messages: Vec<Value> = self
161                .get_window_messages()
162                .into_iter()
163                .map(|msg| serde_json::to_value(&msg).unwrap_or(Value::Null))
164                .collect();
165            result.insert(self.memory_key.clone(), Value::Array(messages));
166        } else {
167            result.insert(
168                self.memory_key.clone(),
169                Value::String(self.buffer_as_string()),
170            );
171        }
172
173        Ok(result)
174    }
175
176    async fn save_context(
177        &mut self,
178        inputs: &HashMap<String, String>,
179        outputs: &HashMap<String, String>,
180    ) -> Result<(), MemoryError> {
181        // M76: Return error when required keys are missing instead of silently skipping
182        let input = inputs.get(&self.input_key).ok_or_else(|| {
183            MemoryError::SaveError(format!("Missing input key '{}'", self.input_key))
184        })?;
185        self.chat_memory.add_user_message(input);
186
187        let output = outputs.get(&self.output_key).ok_or_else(|| {
188            MemoryError::SaveError(format!("Missing output key '{}'", self.output_key))
189        })?;
190        self.chat_memory.add_ai_message(output);
191
192        Ok(())
193    }
194
195    async fn clear(&mut self) -> Result<(), MemoryError> {
196        self.chat_memory.clear();
197        Ok(())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[tokio::test]
206    async fn test_window_memory() {
207        let mut memory = ConversationBufferWindowMemory::new(2);
208
209        // Add 3 rounds (6 messages total)
210        for i in 1..=3 {
211            let inputs = HashMap::from([("input".to_string(), format!("Question{}", i))]);
212            let outputs = HashMap::from([("output".to_string(), format!("Answer{}", i))]);
213            memory.save_context(&inputs, &outputs).await.unwrap();
214        }
215
216        // Full history has 6 messages
217        assert_eq!(memory.chat_memory().len(), 6);
218
219        // But only returns last 2 rounds (4 messages)
220        let memory_vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
221        let history = memory_vars.get("history").unwrap().as_str().unwrap();
222
223        // Should contain Question2, Answer2, Question3, Answer3
224        assert!(!history.contains("Question1"));
225        assert!(!history.contains("Answer1"));
226        assert!(history.contains("Question2"));
227        assert!(history.contains("Answer3"));
228    }
229
230    #[tokio::test]
231    async fn test_window_memory_smaller_than_k() {
232        let mut memory = ConversationBufferWindowMemory::new(5);
233
234        // Only add 2 rounds
235        for i in 1..=2 {
236            let inputs = HashMap::from([("input".to_string(), format!("Question{}", i))]);
237            let outputs = HashMap::from([("output".to_string(), format!("Answer{}", i))]);
238            memory.save_context(&inputs, &outputs).await.unwrap();
239        }
240
241        // Should return all 4 messages
242        let memory_vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
243        let history = memory_vars.get("history").unwrap().as_str().unwrap();
244
245        assert!(history.contains("Question1"));
246        assert!(history.contains("Question2"));
247    }
248
249    #[tokio::test]
250    async fn test_window_memory_clear() {
251        let mut memory = ConversationBufferWindowMemory::new(2);
252
253        let inputs = HashMap::from([("input".to_string(), "test".to_string())]);
254        let outputs = HashMap::from([("output".to_string(), "received".to_string())]);
255        memory.save_context(&inputs, &outputs).await.unwrap();
256
257        assert_eq!(memory.chat_memory().len(), 2);
258
259        memory.clear().await.unwrap();
260        assert_eq!(memory.chat_memory().len(), 0);
261    }
262}