oxi/store/
memory_sqlite.rs1use oxi_agent::tools::{MemoryBackend, MemoryItem, ToolError};
8use rusqlite::{Connection, params};
9use std::path::Path;
10use std::pin::Pin;
11use tokio::sync::Mutex;
12
13#[derive(Debug)]
19pub struct SqliteMemoryStore {
20 db: Mutex<Connection>,
21}
22
23impl SqliteMemoryStore {
24 pub fn open(path: &Path) -> Result<Self, rusqlite::Error> {
32 let is_memory = path == Path::new(":memory:");
33 let mode = if is_memory {
37 oxi_mnemopi::journal::JournalMode::Wal
38 } else {
39 oxi_mnemopi::journal::JournalMode::for_db_path(path)
40 };
41 let conn = Connection::open(path)?;
42
43 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
44 conn.execute_batch(&format!(
45 "PRAGMA busy_timeout = {};",
46 mode.busy_timeout_ms()
47 ))?;
48
49 if !is_memory {
50 if let Err(e) = conn.execute_batch(&format!("PRAGMA journal_mode = {};", mode.as_str()))
54 {
55 tracing::warn!(
56 mode = mode.as_str(),
57 path = %path.display(),
58 error = %e,
59 "failed to set journal_mode; SQLite default will be used"
60 );
61 }
62 }
63
64 conn.execute_batch(
65 "CREATE TABLE IF NOT EXISTS memories (
66 id TEXT PRIMARY KEY,
67 subject TEXT NOT NULL,
68 kind TEXT NOT NULL,
69 content TEXT NOT NULL,
70 embedding BLOB,
71 created_at TEXT NOT NULL DEFAULT (datetime('now')),
72 updated_at TEXT NOT NULL DEFAULT (datetime('now')),
73 metadata TEXT
74 );",
75 )?;
76
77 Ok(Self {
78 db: Mutex::new(conn),
79 })
80 }
81}
82
83impl MemoryBackend for SqliteMemoryStore {
84 fn put<'a>(
85 &'a self,
86 content: &'a str,
87 kind: &'a str,
88 subject: &'a str,
89 ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>> {
90 Box::pin(async move {
91 let id = uuid::Uuid::new_v4().to_string();
92 let db = self.db.lock().await;
93 db.execute(
94 "INSERT INTO memories (id, subject, kind, content)
95 VALUES (?1, ?2, ?3, ?4)",
96 params![id, subject, kind, content],
97 )
98 .map_err(|e| format!("Failed to store memory: {e}"))?;
99 Ok(id)
100 })
101 }
102
103 fn search<'a>(
104 &'a self,
105 query: &'a str,
106 k: usize,
107 ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>> {
108 Box::pin(async move {
109 let db = self.db.lock().await;
110 let pattern = format!("%{}%", query.replace('%', "\\%").replace('_', "\\_"));
111 let mut stmt = db
112 .prepare(
113 "SELECT id, kind, content, subject
114 FROM memories
115 WHERE content LIKE ?1 ESCAPE '\\'
116 ORDER BY length(content) ASC
117 LIMIT ?2",
118 )
119 .map_err(|e| format!("Failed to prepare search: {e}"))?;
120
121 let results: Vec<MemoryItem> = stmt
122 .query_map(params![pattern, k as i64], |row| {
123 Ok(MemoryItem {
124 id: row.get(0)?,
125 kind: row.get(1)?,
126 content: row.get(2)?,
127 subject: row.get(3)?,
128 })
129 })
130 .map_err(|e| format!("Failed to search memories: {e}"))?
131 .filter_map(|r| r.ok())
132 .collect();
133
134 Ok(results)
135 })
136 }
137
138 fn list<'a>(
139 &'a self,
140 subject: &'a str,
141 ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>> {
142 Box::pin(async move {
143 let db = self.db.lock().await;
144 let mut stmt = db
145 .prepare(
146 "SELECT id, kind, content, subject
147 FROM memories
148 WHERE subject = ?1
149 ORDER BY updated_at DESC",
150 )
151 .map_err(|e| format!("Failed to prepare list: {e}"))?;
152
153 let results: Vec<MemoryItem> = stmt
154 .query_map(params![subject], |row| {
155 Ok(MemoryItem {
156 id: row.get(0)?,
157 kind: row.get(1)?,
158 content: row.get(2)?,
159 subject: row.get(3)?,
160 })
161 })
162 .map_err(|e| format!("Failed to list memories: {e}"))?
163 .filter_map(|r| r.ok())
164 .collect();
165
166 Ok(results)
167 })
168 }
169
170 fn delete<'a>(
171 &'a self,
172 id: &'a str,
173 ) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + 'a>> {
174 Box::pin(async move {
175 let db = self.db.lock().await;
176 db.execute("DELETE FROM memories WHERE id = ?1", params![id])
177 .map_err(|e| format!("Failed to delete memory: {e}"))?;
178 Ok(())
179 })
180 }
181}