xz_memory_engine/layered/
summary.rs1use async_trait::async_trait;
4use chrono::Utc;
5use uuid::Uuid;
6use xz_memory_core::{
7 Entry, EntryStore, IndexSearcher, QueryOptions, SortOrder, StoreError, TimeRange,
8};
9
10use super::traits::SummaryMemory;
11
12impl<S: EntryStore, I: IndexSearcher> super::default::LayeredMemory<S, I> {
13 fn summary_partition(scope: &str) -> String {
14 format!("summary:{}", scope)
15 }
16}
17
18fn pack_summary(source: &str, summary: &str) -> String {
19 format!("{}\n{}", source, summary)
20}
21
22fn unpack_summary(body: &str) -> (String, String) {
23 if let Some((source, summary)) = body.split_once('\n') {
24 (source.to_string(), summary.to_string())
25 } else {
26 (String::new(), body.to_string())
27 }
28}
29
30#[async_trait]
31impl<S: EntryStore, I: IndexSearcher> SummaryMemory for super::default::LayeredMemory<S, I> {
32 async fn get_latest(&self, scope: &str) -> Result<Option<(String, String)>, StoreError> {
33 let partition = Self::summary_partition(scope);
34 let opts = QueryOptions { limit: 1, sort: SortOrder::Descending };
35 let range = TimeRange { start: None, end: None };
36 let mut entries = self.store.query(&partition, &range, &opts).await?;
37 Ok(entries.pop().map(|e| {
38 let (source, summary) = unpack_summary(&e.body);
39 (summary, source)
40 }))
41 }
42
43 async fn store(&self, scope: &str, summary: &str, source: &str) -> Result<(), StoreError> {
44 let entry = Entry {
45 id: Uuid::new_v4().to_string(),
46 partition: Self::summary_partition(scope),
47 body: pack_summary(source, summary),
48 recorded_at: Utc::now().timestamp_millis() as u64,
49 };
50 self.store.append(entry).await
51 }
52
53 async fn history(
54 &self,
55 scope: &str,
56 limit: usize,
57 ) -> Result<Vec<(String, String, u64)>, StoreError> {
58 let partition = Self::summary_partition(scope);
59 let opts = QueryOptions { limit, sort: SortOrder::Descending };
60 let range = TimeRange { start: None, end: None };
61 let entries = self.store.query(&partition, &range, &opts).await?;
62 Ok(entries
63 .into_iter()
64 .map(|e| {
65 let (source, summary) = unpack_summary(&e.body);
66 (summary, source, e.recorded_at)
67 })
68 .collect())
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use std::sync::Arc;
76 use tokio::time::{Duration, sleep};
77 use xz_memory_core::ScoredEntry;
78
79 use crate::backends::InMemoryEntryStore;
80
81 struct MockSearcher;
82
83 #[async_trait]
84 impl IndexSearcher for MockSearcher {
85 async fn search(
86 &self,
87 _partitions: &[String],
88 _query: &str,
89 _opts: &xz_memory_core::SearchOptions,
90 ) -> Result<Vec<ScoredEntry>, StoreError> {
91 Ok(vec![])
92 }
93 }
94
95 fn setup() -> Arc<super::super::default::LayeredMemory<InMemoryEntryStore, MockSearcher>> {
96 Arc::new(super::super::default::LayeredMemory::new(
97 Arc::new(InMemoryEntryStore::new()),
98 Arc::new(MockSearcher),
99 ))
100 }
101
102 #[tokio::test]
103 async fn test_store_and_get_latest() {
104 let memory = setup();
105
106 memory.store("sess-1", "User asked about weather.", "raw-sess-1").await.unwrap();
107 sleep(Duration::from_millis(2)).await;
108 memory.store("sess-1", "User asked about travel.", "raw-sess-1").await.unwrap();
109
110 let latest = memory.get_latest("sess-1").await.unwrap();
111 assert!(latest.is_some());
112 let (summary, source) = latest.unwrap();
113 assert_eq!(summary, "User asked about travel.");
114 assert_eq!(source, "raw-sess-1");
115 }
116
117 #[tokio::test]
118 async fn test_get_latest_empty() {
119 let memory = setup();
120 let result = memory.get_latest("nonexistent").await.unwrap();
121 assert!(result.is_none());
122 }
123
124 #[tokio::test]
125 async fn test_history() {
126 let memory = setup();
127
128 memory.store("sess-1", "First summary", "src-1").await.unwrap();
129 sleep(Duration::from_millis(2)).await;
130 memory.store("sess-1", "Second summary", "src-2").await.unwrap();
131 sleep(Duration::from_millis(2)).await;
132 memory.store("sess-1", "Third summary", "src-3").await.unwrap();
133
134 let history = memory.history("sess-1", 10).await.unwrap();
135 assert_eq!(history.len(), 3);
136 assert_eq!(history[0].0, "Third summary");
137 assert_eq!(history[1].0, "Second summary");
138 assert_eq!(history[2].0, "First summary");
139 assert_eq!(history[0].1, "src-3");
140 assert_eq!(history[1].1, "src-2");
141 assert_eq!(history[2].1, "src-1");
142 }
143
144 #[tokio::test]
145 async fn test_history_with_limit() {
146 let memory = setup();
147
148 for i in 0..5 {
149 memory.store("sess-1", &format!("Summary {}", i), "src").await.unwrap();
150 sleep(Duration::from_millis(2)).await;
151 }
152
153 let history = memory.history("sess-1", 2).await.unwrap();
154 assert_eq!(history.len(), 2);
155 assert_eq!(history[0].0, "Summary 4");
156 assert_eq!(history[1].0, "Summary 3");
157 }
158
159 #[tokio::test]
160 async fn test_isolated_scopes() {
161 let memory = setup();
162
163 memory.store("scope-a", "Summary A", "src-a").await.unwrap();
164 memory.store("scope-b", "Summary B", "src-b").await.unwrap();
165
166 let latest_a = memory.get_latest("scope-a").await.unwrap().unwrap();
167 let latest_b = memory.get_latest("scope-b").await.unwrap().unwrap();
168 assert_eq!(latest_a.0, "Summary A");
169 assert_eq!(latest_b.0, "Summary B");
170
171 let history_a = memory.history("scope-a", 10).await.unwrap();
172 assert_eq!(history_a.len(), 1);
173 let history_b = memory.history("scope-b", 10).await.unwrap();
174 assert_eq!(history_b.len(), 1);
175 }
176
177 #[tokio::test]
178 async fn test_pack_unpack_roundtrip() {
179 let body = pack_summary("src", "my summary");
180 let (source, summary) = unpack_summary(&body);
181 assert_eq!(source, "src");
182 assert_eq!(summary, "my summary");
183 }
184
185 #[tokio::test]
186 async fn test_unpack_no_newline() {
187 let (source, summary) = unpack_summary("just a summary");
188 assert_eq!(source, "");
189 assert_eq!(summary, "just a summary");
190 }
191}