Skip to main content

agentdb/
conversations.rs

1use crate::error::{AgentDbError, Result};
2use crate::schema::now_ms;
3use rusqlite::params;
4use rusqlite::Connection;
5use serde_json::Value;
6use std::sync::{Arc, Mutex};
7use uuid::Uuid;
8
9/// A conversation thread.
10#[derive(Debug, Clone)]
11pub struct Conversation {
12    /// Unique identifier for this conversation.
13    pub id: String,
14    /// Optional human-readable title.
15    pub title: Option<String>,
16    /// Arbitrary JSON payload attached to the conversation.
17    pub metadata: Option<Value>,
18    /// Unix-millisecond timestamp when the conversation was created.
19    pub created_at: i64,
20    /// Unix-millisecond timestamp of the most recent update.
21    pub updated_at: i64,
22}
23
24/// A single message within a conversation.
25#[derive(Debug, Clone)]
26pub struct Message {
27    /// Unique identifier for this message.
28    pub id: String,
29    /// ID of the parent conversation.
30    pub conversation_id: String,
31    /// Role of the sender (e.g. `"user"`, `"assistant"`, `"system"`).
32    pub role: String,
33    /// Text content of the message.
34    pub content: String,
35    /// Arbitrary JSON payload attached to the message.
36    pub metadata: Option<Value>,
37    /// Unix-millisecond timestamp when the message was created.
38    pub created_at: i64,
39}
40
41/// Manages conversation threads and their messages.
42pub struct ConversationStore {
43    conn: Arc<Mutex<Connection>>,
44}
45
46impl ConversationStore {
47    pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
48        Self { conn }
49    }
50
51    /// Create a new conversation. The `id` must be unique; use
52    /// `uuid::Uuid::new_v4().to_string()` if you do not have a stable ID.
53    pub fn create_conversation(
54        &self,
55        id: &str,
56        title: Option<&str>,
57        metadata: Option<Value>,
58    ) -> Result<()> {
59        let conn = self.conn.lock().unwrap();
60        let meta_str = metadata.as_ref().map(|m| m.to_string());
61        let now = now_ms();
62        conn.execute(
63            "INSERT INTO _adb_conversations (id, title, metadata, created_at, updated_at)
64             VALUES (?1, ?2, ?3, ?4, ?5)",
65            params![id, title, meta_str, now, now],
66        )?;
67        Ok(())
68    }
69
70    /// Append a message to an existing conversation.
71    ///
72    /// Returns the newly generated message ID.
73    pub fn add_message(
74        &self,
75        conversation_id: &str,
76        role: &str,
77        content: &str,
78        metadata: Option<Value>,
79    ) -> Result<String> {
80        let msg_id = Uuid::new_v4().to_string();
81        let meta_str = metadata.as_ref().map(|m| m.to_string());
82        let now = now_ms();
83        {
84            let conn = self.conn.lock().unwrap();
85            conn.execute(
86                "INSERT INTO _adb_messages
87                     (id, conversation_id, role, content, metadata, created_at)
88                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
89                params![msg_id, conversation_id, role, content, meta_str, now],
90            )?;
91        }
92        // Bump the conversation's updated_at timestamp.
93        {
94            let conn = self.conn.lock().unwrap();
95            conn.execute(
96                "UPDATE _adb_conversations SET updated_at = ?1 WHERE id = ?2",
97                params![now, conversation_id],
98            )?;
99        }
100        Ok(msg_id)
101    }
102
103    /// Return messages for a conversation in chronological order.
104    ///
105    /// If `limit` is `Some(n)` only the most-recent `n` messages are returned
106    /// (still in ascending chronological order). Pass `None` for all messages.
107    pub fn get_messages(
108        &self,
109        conversation_id: &str,
110        limit: Option<usize>,
111    ) -> Result<Vec<Message>> {
112        let conn = self.conn.lock().unwrap();
113        let rows: Vec<Message> = match limit {
114            Some(n) => {
115                // Fetch the last n rows via a sub-query so they come back in
116                // ascending (chronological) order.
117                let mut stmt = conn.prepare(
118                    "SELECT id, conversation_id, role, content, metadata, created_at
119                     FROM (
120                         SELECT id, conversation_id, role, content, metadata, created_at
121                         FROM _adb_messages
122                         WHERE conversation_id = ?1
123                         ORDER BY created_at DESC
124                         LIMIT ?2
125                     )
126                     ORDER BY created_at ASC",
127                )?;
128                let rows = stmt.query_map(params![conversation_id, n as i64], parse_message)?;
129                rows.map(|r| r.map_err(AgentDbError::Sqlite))
130                    .collect::<Result<Vec<_>>>()?
131            }
132            None => {
133                let mut stmt = conn.prepare(
134                    "SELECT id, conversation_id, role, content, metadata, created_at
135                     FROM _adb_messages
136                     WHERE conversation_id = ?1
137                     ORDER BY created_at ASC",
138                )?;
139                let rows = stmt.query_map(params![conversation_id], parse_message)?;
140                rows.map(|r| r.map_err(AgentDbError::Sqlite))
141                    .collect::<Result<Vec<_>>>()?
142            }
143        };
144        Ok(rows)
145    }
146
147    /// List all conversations ordered by most-recently updated first.
148    pub fn list_conversations(&self) -> Result<Vec<Conversation>> {
149        let conn = self.conn.lock().unwrap();
150        let mut stmt = conn.prepare(
151            "SELECT id, title, metadata, created_at, updated_at
152             FROM _adb_conversations
153             ORDER BY updated_at DESC",
154        )?;
155        let rows = stmt.query_map([], parse_conversation)?;
156        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
157    }
158
159    /// Delete a conversation and all its messages (via ON DELETE CASCADE).
160    pub fn delete_conversation(&self, id: &str) -> Result<()> {
161        let conn = self.conn.lock().unwrap();
162        conn.execute("DELETE FROM _adb_conversations WHERE id = ?1", params![id])?;
163        Ok(())
164    }
165}
166
167// ── Row parsers ──────────────────────────────────────────────────────────────
168
169fn parse_conversation(row: &rusqlite::Row) -> rusqlite::Result<Conversation> {
170    let meta_str: Option<String> = row.get(2)?;
171    Ok(Conversation {
172        id: row.get(0)?,
173        title: row.get(1)?,
174        metadata: meta_str
175            .as_deref()
176            .and_then(|s| serde_json::from_str(s).ok()),
177        created_at: row.get(3)?,
178        updated_at: row.get(4)?,
179    })
180}
181
182fn parse_message(row: &rusqlite::Row) -> rusqlite::Result<Message> {
183    let meta_str: Option<String> = row.get(4)?;
184    Ok(Message {
185        id: row.get(0)?,
186        conversation_id: row.get(1)?,
187        role: row.get(2)?,
188        content: row.get(3)?,
189        metadata: meta_str
190            .as_deref()
191            .and_then(|s| serde_json::from_str(s).ok()),
192        created_at: row.get(5)?,
193    })
194}