Skip to main content

xz_memory_engine/layered/
summary.rs

1//! [`SummaryMemory`] implementation for [`LayeredMemory`].
2
3use 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}