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::{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#[async_trait]
137impl BaseMemory for ConversationBufferWindowMemory {
138    fn memory_variables(&self) -> Vec<&str> {
139        vec![&self.memory_key]
140    }
141
142    async fn load_memory_variables(
143        &self,
144        _inputs: &HashMap<String, String>,
145    ) -> Result<HashMap<String, Value>, MemoryError> {
146        let mut result = HashMap::new();
147
148        if self.return_messages {
149            let messages: Vec<Value> = self
150                .get_window_messages()
151                .into_iter()
152                .map(|msg| serde_json::to_value(&msg).unwrap_or(Value::Null))
153                .collect();
154            result.insert(self.memory_key.clone(), Value::Array(messages));
155        } else {
156            result.insert(
157                self.memory_key.clone(),
158                Value::String(self.buffer_as_string()),
159            );
160        }
161
162        Ok(result)
163    }
164
165    async fn save_context(
166        &mut self,
167        inputs: &HashMap<String, String>,
168        outputs: &HashMap<String, String>,
169    ) -> Result<(), MemoryError> {
170        // M76: Return error when required keys are missing instead of silently skipping
171        let input = inputs.get(&self.input_key).ok_or_else(|| {
172            MemoryError::SaveError(format!("Missing input key '{}'", self.input_key))
173        })?;
174        self.chat_memory.add_user_message(input);
175
176        let output = outputs.get(&self.output_key).ok_or_else(|| {
177            MemoryError::SaveError(format!("Missing output key '{}'", self.output_key))
178        })?;
179        self.chat_memory.add_ai_message(output);
180
181        Ok(())
182    }
183
184    async fn clear(&mut self) -> Result<(), MemoryError> {
185        self.chat_memory.clear();
186        Ok(())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[tokio::test]
195    async fn test_window_memory() {
196        let mut memory = ConversationBufferWindowMemory::new(2);
197
198        // Add 3 rounds (6 messages total)
199        for i in 1..=3 {
200            let inputs = HashMap::from([("input".to_string(), format!("Question{}", i))]);
201            let outputs = HashMap::from([("output".to_string(), format!("Answer{}", i))]);
202            memory.save_context(&inputs, &outputs).await.unwrap();
203        }
204
205        // Full history has 6 messages
206        assert_eq!(memory.chat_memory().len(), 6);
207
208        // But only returns last 2 rounds (4 messages)
209        let memory_vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
210        let history = memory_vars.get("history").unwrap().as_str().unwrap();
211
212        // Should contain Question2, Answer2, Question3, Answer3
213        assert!(!history.contains("Question1"));
214        assert!(!history.contains("Answer1"));
215        assert!(history.contains("Question2"));
216        assert!(history.contains("Answer3"));
217    }
218
219    #[tokio::test]
220    async fn test_window_memory_smaller_than_k() {
221        let mut memory = ConversationBufferWindowMemory::new(5);
222
223        // Only add 2 rounds
224        for i in 1..=2 {
225            let inputs = HashMap::from([("input".to_string(), format!("Question{}", i))]);
226            let outputs = HashMap::from([("output".to_string(), format!("Answer{}", i))]);
227            memory.save_context(&inputs, &outputs).await.unwrap();
228        }
229
230        // Should return all 4 messages
231        let memory_vars = memory.load_memory_variables(&HashMap::new()).await.unwrap();
232        let history = memory_vars.get("history").unwrap().as_str().unwrap();
233
234        assert!(history.contains("Question1"));
235        assert!(history.contains("Question2"));
236    }
237
238    #[tokio::test]
239    async fn test_window_memory_clear() {
240        let mut memory = ConversationBufferWindowMemory::new(2);
241
242        let inputs = HashMap::from([("input".to_string(), "test".to_string())]);
243        let outputs = HashMap::from([("output".to_string(), "received".to_string())]);
244        memory.save_context(&inputs, &outputs).await.unwrap();
245
246        assert_eq!(memory.chat_memory().len(), 2);
247
248        memory.clear().await.unwrap();
249        assert_eq!(memory.chat_memory().len(), 0);
250    }
251}