use ai::error::AiResult; use ai::memory::{MemoryEntry, MemoryProvider}; use async_trait::async_trait; use chrono::Utc; use db::sqlx; use uuid::Uuid; #[derive(Clone)] pub struct SimpleMemoryProvider { db: db::AppDatabase, } impl SimpleMemoryProvider { pub fn new(db: db::AppDatabase) -> Self { Self { db } } } #[async_trait] impl MemoryProvider for SimpleMemoryProvider { fn name(&self) -> &'static str { "simple" } async fn save( &self, session_id: Uuid, key: &str, value: &str, importance: i32, ) -> AiResult<()> { let now = Utc::now(); sqlx::query( "INSERT INTO agent_long_term_memory \ (id, session, key, value, importance, last_used_at, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $6, $6) \ ON CONFLICT ON CONSTRAINT idx_agent_ltm_session_key \ DO UPDATE SET value = $4, importance = $5, last_used_at = $6, updated_at = $6", ) .bind(Uuid::now_v7()) .bind(session_id) .bind(key) .bind(value) .bind(importance) .bind(now) .execute(self.db.writer()) .await .map_err(|e| { ai::error::AiError::Response(format!("memory save error: {e}")) })?; Ok(()) } async fn recall( &self, session_id: Uuid, query: &str, limit: usize, ) -> AiResult> { use db::sqlx::FromRow; #[derive(Debug, FromRow)] struct Row { key: String, value: String, importance: i32, last_used_at: Option>, } let rows: Vec = sqlx::query_as( "SELECT key, value, importance, last_used_at \ FROM agent_long_term_memory \ WHERE session = $1 \ AND deleted_at IS NULL \ AND (value ILIKE $2 OR key ILIKE $2) \ ORDER BY importance DESC, last_used_at DESC NULLS LAST \ LIMIT $3", ) .bind(session_id) .bind(format!("%{query}%")) .bind(limit as i64) .fetch_all(self.db.reader()) .await .map_err(|e| { ai::error::AiError::Response(format!("memory recall error: {e}")) })?; let entries: Vec = rows .into_iter() .map(|r| MemoryEntry { key: r.key, value: r.value, importance: r.importance, last_used_at: r.last_used_at.map(|dt| dt.to_rfc3339()), }) .collect(); if !entries.is_empty() { let now = Utc::now(); let _ = sqlx::query( "UPDATE agent_long_term_memory \ SET last_used_at = $1, updated_at = $1 \ WHERE session = $2 AND value ILIKE $3 AND deleted_at IS NULL", ) .bind(now) .bind(session_id) .bind(format!("%{query}%")) .execute(self.db.writer()) .await; } Ok(entries) } async fn forget(&self, session_id: Uuid, key: &str) -> AiResult<()> { let now = Utc::now(); sqlx::query( "UPDATE agent_long_term_memory \ SET deleted_at = $1, updated_at = $1 \ WHERE session = $2 AND key = $3 AND deleted_at IS NULL", ) .bind(now) .bind(session_id) .bind(key) .execute(self.db.writer()) .await .map_err(|e| { ai::error::AiError::Response(format!("memory forget error: {e}")) })?; Ok(()) } async fn build_context_block(&self, session_id: Uuid) -> AiResult { use db::sqlx::FromRow; #[derive(Debug, FromRow)] struct Entry { key: String, value: String, } let rows: Vec = sqlx::query_as( "SELECT key, value \ FROM agent_long_term_memory \ WHERE session = $1 AND deleted_at IS NULL \ ORDER BY importance DESC, last_used_at DESC NULLS LAST \ LIMIT 20", ) .bind(session_id) .fetch_all(self.db.reader()) .await .map_err(|e| { ai::error::AiError::Response(format!("memory context error: {e}")) })?; if rows.is_empty() { return Ok(String::new()); } let mut block = String::from("\n"); for row in &rows { block.push_str(&format!("- {}: {}\n", row.key, row.value)); } block.push_str(""); Ok(block) } }