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:
@@ -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に依存
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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,31 +581,9 @@ 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": false,
|
|
||||||
"error": format!("Failed to get memories: {}", e)
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_profile = match self.store.get_profile() {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => return json!({
|
|
||||||
"success": false,
|
|
||||||
"error": format!("Failed to get profile: {}", e)
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Infer relationship
|
|
||||||
let relationship = RelationshipInference::infer(
|
|
||||||
entity_id.to_string(),
|
|
||||||
&memories,
|
|
||||||
&user_profile,
|
|
||||||
);
|
|
||||||
|
|
||||||
json!({
|
|
||||||
"success": true,
|
"success": true,
|
||||||
"relationship": {
|
"relationship": {
|
||||||
"entity_id": relationship.entity_id,
|
"entity_id": relationship.entity_id,
|
||||||
@@ -617,7 +595,12 @@ impl BaseMCPServer {
|
|||||||
"confidence": relationship.confidence,
|
"confidence": relationship.confidence,
|
||||||
"inferred_at": relationship.inferred_at
|
"inferred_at": relationship.inferred_at
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": format!("Failed to get relationship: {}", e)
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_list_relationships(&self, arguments: &Value) -> Value {
|
fn tool_list_relationships(&self, arguments: &Value) -> Value {
|
||||||
|
|||||||
Reference in New Issue
Block a user