Add Layer 4 caching to reduce AI load

Implemented 5-minute short-term caching for relationship inference:

**store.rs**:
- Added relationship_cache SQLite table
- save_relationship_cache(), get_cached_relationship()
- save_all_relationships_cache(), get_cached_all_relationships()
- clear_relationship_cache() - called on memory create/update/delete
- Cache duration: 5 minutes (configurable constant)

**relationship.rs**:
- Modified infer_all_relationships() to use cache
- Added get_relationship() function with caching support
- Cache hit: return immediately
- Cache miss: compute, save to cache, return

**base.rs**:
- Updated tool_get_relationship() to use cached version
- Reduced load from O(n) scan to O(1) cache lookup

**Benefits**:
- Reduces AI load when frequently querying relationships
- Automatic cache invalidation on data changes
- Scales better with growing memory count
- No user-facing changes

**Documentation**:
- Updated ARCHITECTURE.md with caching strategy details

This addresses scalability concerns for Layer 4 as memory data grows.
This commit is contained in:
Claude
2025-11-06 09:33:42 +00:00
parent 8037477104
commit 2579312029
5 changed files with 203 additions and 39 deletions

View File

@@ -362,10 +362,16 @@ if user.extraversion < 0.5 {
### Design Philosophy ### Design Philosophy
**推測のみ、保存なし**: **推測ベース + 短期キャッシング**:
- 毎回Layer 1-3.5から計算 - 毎回Layer 1-3.5から計算
- キャッシュなし(シンプルさ優先) - 5分間の短期キャッシュで負荷軽減
- 後でキャッシング追加可能 - メモリ更新時にキャッシュ無効化
**キャッシング戦略**:
- SQLiteテーブル`relationship_cache`)に保存
- 個別エンティティ: `get_relationship(entity_id)`
- 全体リスト: `list_relationships()`
- メモリ作成/更新/削除時に自動クリア
**独立性**: **独立性**:
- Layer 1-3.5に依存 - Layer 1-3.5に依存

View File

@@ -9,5 +9,5 @@ pub use analysis::UserAnalysis;
pub use error::{MemoryError, Result}; pub use error::{MemoryError, Result};
pub use memory::Memory; pub use memory::Memory;
pub use profile::{UserProfile, TraitScore}; 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; pub use store::MemoryStore;

View File

@@ -184,6 +184,11 @@ impl RelationshipInference {
pub fn infer_all_relationships( pub fn infer_all_relationships(
store: &MemoryStore, store: &MemoryStore,
) -> Result<Vec<RelationshipInference>> { ) -> Result<Vec<RelationshipInference>> {
// Check cache first
if let Some(cached) = store.get_cached_all_relationships()? {
return Ok(cached);
}
// Get all memories // Get all memories
let memories = store.list()?; let memories = store.list()?;
@@ -219,9 +224,41 @@ pub fn infer_all_relationships(
.unwrap_or(std::cmp::Ordering::Equal) .unwrap_or(std::cmp::Ordering::Equal)
}); });
// Cache the result
store.save_all_relationships_cache(&relationships)?;
Ok(relationships) Ok(relationships)
} }
/// Get relationship inference for a specific entity (with caching)
pub fn get_relationship(
store: &MemoryStore,
entity_id: &str,
) -> Result<RelationshipInference> {
// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -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 }) Ok(Self { conn })
} }
@@ -137,6 +148,10 @@ impl MemoryStore {
memory.updated_at.to_rfc3339(), memory.updated_at.to_rfc3339(),
], ],
)?; )?;
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(()) Ok(())
} }
@@ -204,6 +219,9 @@ impl MemoryStore {
return Err(MemoryError::NotFound(memory.id.clone())); return Err(MemoryError::NotFound(memory.id.clone()));
} }
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(()) Ok(())
} }
@@ -217,6 +235,9 @@ impl MemoryStore {
return Err(MemoryError::NotFound(id.to_string())); return Err(MemoryError::NotFound(id.to_string()));
} }
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(()) Ok(())
} }
@@ -464,6 +485,123 @@ impl MemoryStore {
Ok(profile) 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<Option<super::relationship::RelationshipInference>> {
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<Option<Vec<super::relationship::RelationshipInference>>> {
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<super::relationship::RelationshipInference> =
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)] #[cfg(test)]

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::io::{self, BufRead, Write}; 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 { pub struct BaseMCPServer {
store: MemoryStore, store: MemoryStore,
@@ -581,43 +581,26 @@ impl BaseMCPServer {
}); });
} }
// Get memories and user profile // Get relationship (with caching)
let memories = match self.store.list() { match get_relationship(&self.store, entity_id) {
Ok(m) => m, Ok(relationship) => json!({
Err(e) => return json!({ "success": true,
"success": false, "relationship": {
"error": format!("Failed to get memories: {}", e) "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
}
}), }),
}; Err(e) => json!({
let user_profile = match self.store.get_profile() {
Ok(p) => p,
Err(e) => return json!({
"success": false, "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 { fn tool_list_relationships(&self, arguments: &Value) -> Value {