Skip to main content

sparrow/chat/
mod.rs

1//! Interactive chat session for Sparrow.
2//!
3//! Persistent chat with history, save/load, and streaming responses.
4
5use std::path::Path;
6
7pub mod composer;
8
9/// A single message in the chat history.
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11pub struct ChatMessage {
12    pub role: String, // "user" or "assistant"
13    pub content: String,
14    pub timestamp: String,
15}
16
17/// A persistent chat session with full history.
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct ChatSession {
20    pub messages: Vec<ChatMessage>,
21    pub created_at: String,
22    pub model: Option<String>,
23}
24
25impl ChatSession {
26    /// Create a new empty chat session.
27    pub fn new() -> Self {
28        Self {
29            messages: Vec::new(),
30            created_at: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
31            model: None,
32        }
33    }
34
35    /// Add a user message to the history.
36    pub fn add_user_message(&mut self, content: &str) {
37        self.messages.push(ChatMessage {
38            role: "user".to_string(),
39            content: content.to_string(),
40            timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
41        });
42    }
43
44    /// Add an assistant message to the history.
45    pub fn add_assistant_message(&mut self, content: &str) {
46        self.messages.push(ChatMessage {
47            role: "assistant".to_string(),
48            content: content.to_string(),
49            timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
50        });
51    }
52
53    /// Get the conversation history as (user, assistant) pairs.
54    pub fn history(&self) -> Vec<(&str, &str)> {
55        let mut pairs = Vec::new();
56        let mut current_user: Option<&str> = None;
57
58        for msg in &self.messages {
59            match msg.role.as_str() {
60                "user" => current_user = Some(&msg.content),
61                "assistant" => {
62                    if let Some(user_msg) = current_user.take() {
63                        pairs.push((user_msg, msg.content.as_str()));
64                    }
65                }
66                _ => {}
67            }
68        }
69
70        pairs
71    }
72
73    /// Get the last N messages as context.
74    pub fn last_context(&self, n: usize) -> Vec<&ChatMessage> {
75        self.messages.iter().rev().take(n).rev().collect()
76    }
77
78    /// Save the session to a JSON file.
79    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
80        let json = serde_json::to_string_pretty(self)?;
81        std::fs::write(path, json)?;
82        Ok(())
83    }
84
85    /// Load a session from a JSON file.
86    pub fn load(path: &Path) -> anyhow::Result<Self> {
87        let json = std::fs::read_to_string(path)?;
88        let session: ChatSession = serde_json::from_str(&json)?;
89        Ok(session)
90    }
91
92    /// Clear all messages.
93    pub fn clear(&mut self) {
94        self.messages.clear();
95    }
96
97    /// Get the number of messages.
98    pub fn len(&self) -> usize {
99        self.messages.len()
100    }
101
102    /// Check if the session is empty.
103    pub fn is_empty(&self) -> bool {
104        self.messages.is_empty()
105    }
106}
107
108impl Default for ChatSession {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_chat_session() {
120        let mut session = ChatSession::new();
121        assert!(session.is_empty());
122
123        session.add_user_message("Hello");
124        session.add_assistant_message("Hi there!");
125        assert_eq!(session.len(), 2);
126
127        let history = session.history();
128        assert_eq!(history.len(), 1);
129        assert_eq!(history[0].0, "Hello");
130        assert_eq!(history[0].1, "Hi there!");
131    }
132
133    #[test]
134    fn test_save_load() {
135        let mut session = ChatSession::new();
136        session.add_user_message("test");
137        session.add_assistant_message("response");
138
139        let tmp = std::env::temp_dir().join("sparrow_test_chat.json");
140        session.save(&tmp).unwrap();
141
142        let loaded = ChatSession::load(&tmp).unwrap();
143        assert_eq!(loaded.len(), 2);
144        assert_eq!(loaded.messages[0].content, "test");
145
146        let _ = std::fs::remove_file(&tmp);
147    }
148}