1use async_trait::async_trait;
7use meerkat_core::memory::{MemoryMetadata, MemoryResult, MemoryStore, MemoryStoreError};
8use tokio::sync::RwLock;
9
10#[derive(Debug, Clone)]
12struct MemoryEntry {
13 content: String,
14 metadata: MemoryMetadata,
15}
16
17pub struct SimpleMemoryStore {
23 entries: RwLock<Vec<MemoryEntry>>,
24}
25
26impl SimpleMemoryStore {
27 pub fn new() -> Self {
29 Self {
30 entries: RwLock::new(Vec::new()),
31 }
32 }
33}
34
35impl Default for SimpleMemoryStore {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41#[async_trait]
42impl MemoryStore for SimpleMemoryStore {
43 async fn index(&self, content: &str, metadata: MemoryMetadata) -> Result<(), MemoryStoreError> {
44 let mut entries = self.entries.write().await;
45 entries.push(MemoryEntry {
46 content: content.to_string(),
47 metadata,
48 });
49 Ok(())
50 }
51
52 async fn search(
53 &self,
54 query: &str,
55 limit: usize,
56 ) -> Result<Vec<MemoryResult>, MemoryStoreError> {
57 let entries = self.entries.read().await;
58
59 let query_lower = query.to_lowercase();
60 let query_words: Vec<&str> = query_lower.split_whitespace().collect();
61
62 let mut results: Vec<MemoryResult> = entries
63 .iter()
64 .filter_map(|entry| {
65 let content_lower = entry.content.to_lowercase();
66 let matching_words = query_words
67 .iter()
68 .filter(|w| content_lower.contains(**w))
69 .count();
70
71 if matching_words == 0 {
72 return None;
73 }
74
75 let score = matching_words as f32 / query_words.len().max(1) as f32;
76 Some(MemoryResult {
77 content: entry.content.clone(),
78 metadata: entry.metadata.clone(),
79 score,
80 })
81 })
82 .collect();
83
84 results.sort_by(|a, b| {
86 b.score
87 .partial_cmp(&a.score)
88 .unwrap_or(std::cmp::Ordering::Equal)
89 });
90 results.truncate(limit);
91
92 Ok(results)
93 }
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used, clippy::expect_used)]
98mod tests {
99 use super::*;
100 use meerkat_core::types::SessionId;
101 use std::time::SystemTime;
102
103 fn meta(_label: &str) -> MemoryMetadata {
104 MemoryMetadata {
105 session_id: SessionId::new(),
106 turn: Some(1),
107 indexed_at: SystemTime::now(),
108 }
109 }
110
111 #[tokio::test]
112 async fn test_index_and_search() {
113 let store = SimpleMemoryStore::new();
114
115 store
116 .index("The user wants to implement a REST API", meta("s1"))
117 .await
118 .unwrap();
119 store
120 .index("Configuration uses TOML format", meta("s2"))
121 .await
122 .unwrap();
123 store
124 .index("Authentication uses JWT tokens", meta("s3"))
125 .await
126 .unwrap();
127
128 let results = store.search("REST API", 10).await.unwrap();
129 assert!(!results.is_empty());
130 assert!(results[0].content.contains("REST API"));
131 }
132
133 #[tokio::test]
134 async fn test_search_empty_store() {
135 let store = SimpleMemoryStore::new();
136 let results = store.search("anything", 10).await.unwrap();
137 assert!(results.is_empty());
138 }
139
140 #[tokio::test]
141 async fn test_search_limit() {
142 let store = SimpleMemoryStore::new();
143
144 for i in 0..10 {
145 store
146 .index(&format!("Item {i} with keyword test"), meta("s1"))
147 .await
148 .unwrap();
149 }
150
151 let results = store.search("test", 3).await.unwrap();
152 assert_eq!(results.len(), 3);
153 }
154
155 #[tokio::test]
156 async fn test_search_no_match() {
157 let store = SimpleMemoryStore::new();
158 store.index("Hello world", meta("s1")).await.unwrap();
159
160 let results = store.search("quantum computing", 10).await.unwrap();
161 assert!(results.is_empty());
162 }
163}