Skip to main content

synaptic_memory/
file_history.rs

1use std::path::PathBuf;
2
3use async_trait::async_trait;
4use synaptic_core::{MemoryStore, Message, SynapticError};
5
6/// A chat message history backed by a JSON file on disk.
7///
8/// Messages are stored as a JSON array. Each session gets its own file
9/// by appending the session ID to the base path.
10pub struct FileChatMessageHistory {
11    path: PathBuf,
12}
13
14impl FileChatMessageHistory {
15    /// Create a new file-backed message history.
16    ///
17    /// The `path` specifies the base directory where session files are stored.
18    /// Each session is stored as `{path}/{session_id}.json`.
19    pub fn new(path: impl Into<PathBuf>) -> Self {
20        Self { path: path.into() }
21    }
22
23    fn session_path(&self, session_id: &str) -> PathBuf {
24        self.path.join(format!("{session_id}.json"))
25    }
26
27    async fn read_messages(&self, session_id: &str) -> Result<Vec<Message>, SynapticError> {
28        let path = self.session_path(session_id);
29        match tokio::fs::read_to_string(&path).await {
30            Ok(contents) => {
31                let messages: Vec<Message> = serde_json::from_str(&contents).map_err(|e| {
32                    SynapticError::Memory(format!("failed to parse {}: {e}", path.display()))
33                })?;
34                Ok(messages)
35            }
36            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
37            Err(e) => Err(SynapticError::Memory(format!(
38                "failed to read {}: {e}",
39                path.display()
40            ))),
41        }
42    }
43
44    async fn write_messages(
45        &self,
46        session_id: &str,
47        messages: &[Message],
48    ) -> Result<(), SynapticError> {
49        // Ensure directory exists
50        if let Some(parent) = self.session_path(session_id).parent() {
51            tokio::fs::create_dir_all(parent)
52                .await
53                .map_err(|e| SynapticError::Memory(format!("failed to create directory: {e}")))?;
54        }
55
56        let path = self.session_path(session_id);
57        let json = serde_json::to_string_pretty(messages)
58            .map_err(|e| SynapticError::Memory(format!("failed to serialize messages: {e}")))?;
59        tokio::fs::write(&path, json)
60            .await
61            .map_err(|e| SynapticError::Memory(format!("failed to write {}: {e}", path.display())))
62    }
63}
64
65#[async_trait]
66impl MemoryStore for FileChatMessageHistory {
67    async fn append(&self, session_id: &str, message: Message) -> Result<(), SynapticError> {
68        let mut messages = self.read_messages(session_id).await?;
69        messages.push(message);
70        self.write_messages(session_id, &messages).await
71    }
72
73    async fn load(&self, session_id: &str) -> Result<Vec<Message>, SynapticError> {
74        self.read_messages(session_id).await
75    }
76
77    async fn clear(&self, session_id: &str) -> Result<(), SynapticError> {
78        self.write_messages(session_id, &[]).await
79    }
80}