diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b76110..3eae659 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -362,10 +362,16 @@ if user.extraversion < 0.5 { ### Design Philosophy -**推測のみ、保存なし**: +**推測ベース + 短期キャッシング**: - 毎回Layer 1-3.5から計算 -- キャッシュなし(シンプルさ優先) -- 後でキャッシング追加可能 +- 5分間の短期キャッシュで負荷軽減 +- メモリ更新時にキャッシュ無効化 + +**キャッシング戦略**: +- SQLiteテーブル(`relationship_cache`)に保存 +- 個別エンティティ: `get_relationship(entity_id)` +- 全体リスト: `list_relationships()` +- メモリ作成/更新/削除時に自動クリア **独立性**: - Layer 1-3.5に依存 diff --git a/src/core/mod.rs b/src/core/mod.rs index b76a188..6d134b2 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -9,5 +9,5 @@ pub use analysis::UserAnalysis; pub use error::{MemoryError, Result}; pub use memory::Memory; pub use profile::{UserProfile, TraitScore}; -pub use relationship::{RelationshipInference, infer_all_relationships}; +pub use relationship::{RelationshipInference, infer_all_relationships, get_relationship}; pub use store::MemoryStore; diff --git a/src/core/relationship.rs b/src/core/relationship.rs index 4972be2..3083f71 100644 --- a/src/core/relationship.rs +++ b/src/core/relationship.rs @@ -184,6 +184,11 @@ impl RelationshipInference { pub fn infer_all_relationships( store: &MemoryStore, ) -> Result> { + // Check cache first + if let Some(cached) = store.get_cached_all_relationships()? { + return Ok(cached); + } + // Get all memories let memories = store.list()?; @@ -219,9 +224,41 @@ pub fn infer_all_relationships( .unwrap_or(std::cmp::Ordering::Equal) }); + // Cache the result + store.save_all_relationships_cache(&relationships)?; + Ok(relationships) } +/// Get relationship inference for a specific entity (with caching) +pub fn get_relationship( + store: &MemoryStore, + entity_id: &str, +) -> Result { + // Check cache first + if let Some(cached) = store.get_cached_relationship(entity_id)? { + return Ok(cached); + } + + // Get all memories + let memories = store.list()?; + + // Get user profile + let user_profile = store.get_profile()?; + + // Infer relationship + let relationship = RelationshipInference::infer( + entity_id.to_string(), + &memories, + &user_profile, + ); + + // Cache it + store.save_relationship_cache(entity_id, &relationship)?; + + Ok(relationship) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/core/store.rs b/src/core/store.rs index e807e0a..b9f5b04 100644 --- a/src/core/store.rs +++ b/src/core/store.rs @@ -102,6 +102,17 @@ impl MemoryStore { [], )?; + // Create relationship_cache table (Layer 4 - relationship inference cache) + // entity_id = "" for all_relationships cache + conn.execute( + "CREATE TABLE IF NOT EXISTS relationship_cache ( + entity_id TEXT PRIMARY KEY, + data TEXT NOT NULL, + cached_at TEXT NOT NULL + )", + [], + )?; + Ok(Self { conn }) } @@ -137,6 +148,10 @@ impl MemoryStore { memory.updated_at.to_rfc3339(), ], )?; + + // Clear relationship cache since memory data changed + self.clear_relationship_cache()?; + Ok(()) } @@ -204,6 +219,9 @@ impl MemoryStore { return Err(MemoryError::NotFound(memory.id.clone())); } + // Clear relationship cache since memory data changed + self.clear_relationship_cache()?; + Ok(()) } @@ -217,6 +235,9 @@ impl MemoryStore { return Err(MemoryError::NotFound(id.to_string())); } + // Clear relationship cache since memory data changed + self.clear_relationship_cache()?; + Ok(()) } @@ -464,6 +485,123 @@ impl MemoryStore { Ok(profile) } + + // ========== Layer 4: Relationship Cache Methods ========== + + /// Cache duration in minutes + const RELATIONSHIP_CACHE_DURATION_MINUTES: i64 = 5; + + /// Save relationship inference to cache + pub fn save_relationship_cache( + &self, + entity_id: &str, + relationship: &super::relationship::RelationshipInference, + ) -> Result<()> { + let data = serde_json::to_string(relationship)?; + let cached_at = Utc::now().to_rfc3339(); + + self.conn.execute( + "INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES (?1, ?2, ?3)", + params![entity_id, data, cached_at], + )?; + + Ok(()) + } + + /// Get cached relationship inference + pub fn get_cached_relationship( + &self, + entity_id: &str, + ) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ?1")?; + + let result = stmt.query_row([entity_id], |row| { + let data: String = row.get(0)?; + let cached_at: String = row.get(1)?; + Ok((data, cached_at)) + }); + + match result { + Ok((data, cached_at_str)) => { + // Check if cache is still valid (within 5 minutes) + let cached_at = DateTime::parse_from_rfc3339(&cached_at_str) + .map_err(|e| MemoryError::Parse(e.to_string()))? + .with_timezone(&Utc); + + let age_minutes = (Utc::now() - cached_at).num_minutes(); + + if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES { + let relationship: super::relationship::RelationshipInference = + serde_json::from_str(&data)?; + Ok(Some(relationship)) + } else { + // Cache expired + Ok(None) + } + } + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Save all relationships list to cache (use empty string as entity_id) + pub fn save_all_relationships_cache( + &self, + relationships: &[super::relationship::RelationshipInference], + ) -> Result<()> { + let data = serde_json::to_string(relationships)?; + let cached_at = Utc::now().to_rfc3339(); + + self.conn.execute( + "INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES ('', ?1, ?2)", + params![data, cached_at], + )?; + + Ok(()) + } + + /// Get cached all relationships list + pub fn get_cached_all_relationships( + &self, + ) -> Result>> { + let mut stmt = self + .conn + .prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ''")?; + + let result = stmt.query_row([], |row| { + let data: String = row.get(0)?; + let cached_at: String = row.get(1)?; + Ok((data, cached_at)) + }); + + match result { + Ok((data, cached_at_str)) => { + let cached_at = DateTime::parse_from_rfc3339(&cached_at_str) + .map_err(|e| MemoryError::Parse(e.to_string()))? + .with_timezone(&Utc); + + let age_minutes = (Utc::now() - cached_at).num_minutes(); + + if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES { + let relationships: Vec = + serde_json::from_str(&data)?; + Ok(Some(relationships)) + } else { + Ok(None) + } + } + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Clear all relationship caches (call when memories are modified) + pub fn clear_relationship_cache(&self) -> Result<()> { + self.conn.execute("DELETE FROM relationship_cache", [])?; + Ok(()) + } } #[cfg(test)] diff --git a/src/mcp/base.rs b/src/mcp/base.rs index f5fcb8d..e18ccc8 100644 --- a/src/mcp/base.rs +++ b/src/mcp/base.rs @@ -2,7 +2,7 @@ use anyhow::Result; use serde_json::{json, Value}; use std::io::{self, BufRead, Write}; -use crate::core::{Memory, MemoryStore, UserAnalysis, RelationshipInference, infer_all_relationships}; +use crate::core::{Memory, MemoryStore, UserAnalysis, RelationshipInference, infer_all_relationships, get_relationship}; pub struct BaseMCPServer { store: MemoryStore, @@ -581,43 +581,26 @@ impl BaseMCPServer { }); } - // Get memories and user profile - let memories = match self.store.list() { - Ok(m) => m, - Err(e) => return json!({ - "success": false, - "error": format!("Failed to get memories: {}", e) + // Get relationship (with caching) + match get_relationship(&self.store, entity_id) { + Ok(relationship) => json!({ + "success": true, + "relationship": { + "entity_id": relationship.entity_id, + "interaction_count": relationship.interaction_count, + "avg_priority": relationship.avg_priority, + "days_since_last": relationship.days_since_last, + "bond_strength": relationship.bond_strength, + "relationship_type": relationship.relationship_type, + "confidence": relationship.confidence, + "inferred_at": relationship.inferred_at + } }), - }; - - let user_profile = match self.store.get_profile() { - Ok(p) => p, - Err(e) => return json!({ + Err(e) => json!({ "success": false, - "error": format!("Failed to get profile: {}", e) + "error": format!("Failed to get relationship: {}", e) }), - }; - - // Infer relationship - let relationship = RelationshipInference::infer( - entity_id.to_string(), - &memories, - &user_profile, - ); - - json!({ - "success": true, - "relationship": { - "entity_id": relationship.entity_id, - "interaction_count": relationship.interaction_count, - "avg_priority": relationship.avg_priority, - "days_since_last": relationship.days_since_last, - "bond_strength": relationship.bond_strength, - "relationship_type": relationship.relationship_type, - "confidence": relationship.confidence, - "inferred_at": relationship.inferred_at - } - }) + } } fn tool_list_relationships(&self, arguments: &Value) -> Value {