Implement Layer 3.5: Integrated Profile system
Layer 3.5 provides a unified, essential summary of the user by integrating data from Layers 1-3. This addresses the product design gap where individual layers work correctly but lack a cohesive, simple output. Design Philosophy: - "Internal complexity, external simplicity" - AI references get_profile() as primary tool (efficient) - Detailed data still accessible when needed (flexible) - Auto-caching with smart update triggers (performant) Implementation: - UserProfile struct: dominant traits, core interests, core values - Automatic data aggregation from Layer 1-3 - Frequency analysis for topics/values extraction - SQLite caching (user_profiles table, single row) - Update triggers: 10+ new memories, new analysis, or 7+ days old - MCP tool: get_profile (primary), others available for details Data Structure: - dominant_traits: Top 3 Big Five traits - core_interests: Top 5 frequent topics from memories - core_values: Top 5 values from high-priority memories - key_memory_ids: Top 10 priority memories as evidence - data_quality: 0.0-1.0 confidence score Usage Pattern: - Normal: AI calls get_profile() only when needed - Deep dive: AI calls list_memories(), get_memory(id) for details - Efficient: Profile cached, regenerates only when necessary
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
pub mod analysis;
|
pub mod analysis;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
|
pub mod profile;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
pub use analysis::UserAnalysis;
|
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 store::MemoryStore;
|
pub use store::MemoryStore;
|
||||||
|
|||||||
275
src/core/profile.rs
Normal file
275
src/core/profile.rs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::core::{MemoryStore, UserAnalysis};
|
||||||
|
use crate::core::error::Result;
|
||||||
|
|
||||||
|
/// Integrated user profile - the essence of Layer 1-3 data
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UserProfile {
|
||||||
|
/// Dominant personality traits (top 2-3 from Big Five)
|
||||||
|
pub dominant_traits: Vec<TraitScore>,
|
||||||
|
|
||||||
|
/// Core interests (most frequent topics from memories)
|
||||||
|
pub core_interests: Vec<String>,
|
||||||
|
|
||||||
|
/// Core values (extracted from high-priority memories)
|
||||||
|
pub core_values: Vec<String>,
|
||||||
|
|
||||||
|
/// Key memory IDs (top priority memories as evidence)
|
||||||
|
pub key_memory_ids: Vec<String>,
|
||||||
|
|
||||||
|
/// Data quality score (0.0-1.0 based on data volume)
|
||||||
|
pub data_quality: f32,
|
||||||
|
|
||||||
|
/// Last update timestamp
|
||||||
|
pub last_updated: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TraitScore {
|
||||||
|
pub name: String,
|
||||||
|
pub score: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserProfile {
|
||||||
|
/// Generate integrated profile from Layer 1-3 data
|
||||||
|
pub fn generate(store: &MemoryStore) -> Result<Self> {
|
||||||
|
// Get latest personality analysis (Layer 3)
|
||||||
|
let personality = store.get_latest_analysis()?;
|
||||||
|
|
||||||
|
// Get all memories (Layer 1-2)
|
||||||
|
let memories = store.list()?;
|
||||||
|
|
||||||
|
// Extract dominant traits from Big Five
|
||||||
|
let dominant_traits = extract_dominant_traits(&personality);
|
||||||
|
|
||||||
|
// Extract core interests from memory content
|
||||||
|
let core_interests = extract_core_interests(&memories);
|
||||||
|
|
||||||
|
// Extract core values from high-priority memories
|
||||||
|
let core_values = extract_core_values(&memories);
|
||||||
|
|
||||||
|
// Get top priority memory IDs
|
||||||
|
let key_memory_ids = extract_key_memories(&memories);
|
||||||
|
|
||||||
|
// Calculate data quality
|
||||||
|
let data_quality = calculate_data_quality(&memories, &personality);
|
||||||
|
|
||||||
|
Ok(UserProfile {
|
||||||
|
dominant_traits,
|
||||||
|
core_interests,
|
||||||
|
core_values,
|
||||||
|
key_memory_ids,
|
||||||
|
data_quality,
|
||||||
|
last_updated: Utc::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if profile needs update
|
||||||
|
pub fn needs_update(&self, store: &MemoryStore) -> Result<bool> {
|
||||||
|
// Update if 7+ days old
|
||||||
|
let days_old = (Utc::now() - self.last_updated).num_days();
|
||||||
|
if days_old >= 7 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update if 10+ new memories since last update
|
||||||
|
let memory_count = store.count()?;
|
||||||
|
let expected_count = self.key_memory_ids.len() * 2; // Rough estimate
|
||||||
|
if memory_count > expected_count + 10 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update if new personality analysis exists
|
||||||
|
if let Some(latest) = store.get_latest_analysis()? {
|
||||||
|
if latest.analyzed_at > self.last_updated {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract top 2-3 personality traits from Big Five
|
||||||
|
fn extract_dominant_traits(analysis: &Option<UserAnalysis>) -> Vec<TraitScore> {
|
||||||
|
if analysis.is_none() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let analysis = analysis.as_ref().unwrap();
|
||||||
|
|
||||||
|
let mut traits = vec![
|
||||||
|
TraitScore { name: "openness".to_string(), score: analysis.openness },
|
||||||
|
TraitScore { name: "conscientiousness".to_string(), score: analysis.conscientiousness },
|
||||||
|
TraitScore { name: "extraversion".to_string(), score: analysis.extraversion },
|
||||||
|
TraitScore { name: "agreeableness".to_string(), score: analysis.agreeableness },
|
||||||
|
TraitScore { name: "neuroticism".to_string(), score: analysis.neuroticism },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort by score descending
|
||||||
|
traits.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
|
||||||
|
|
||||||
|
// Return top 3
|
||||||
|
traits.into_iter().take(3).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract core interests from memory content (frequency analysis)
|
||||||
|
fn extract_core_interests(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||||
|
let mut word_freq: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for memory in memories {
|
||||||
|
// Extract keywords from content
|
||||||
|
let words = extract_keywords(&memory.content);
|
||||||
|
for word in words {
|
||||||
|
*word_freq.entry(word).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also consider AI interpretation if available
|
||||||
|
if let Some(ref interpretation) = memory.ai_interpretation {
|
||||||
|
let words = extract_keywords(interpretation);
|
||||||
|
for word in words {
|
||||||
|
*word_freq.entry(word).or_insert(0) += 2; // Weight interpretation higher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by frequency and take top 5
|
||||||
|
let mut freq_vec: Vec<_> = word_freq.into_iter().collect();
|
||||||
|
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
|
||||||
|
freq_vec.into_iter()
|
||||||
|
.take(5)
|
||||||
|
.map(|(word, _)| word)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract core values from high-priority memories
|
||||||
|
fn extract_core_values(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||||
|
// Filter high-priority memories (>= 0.7)
|
||||||
|
let high_priority: Vec<_> = memories.iter()
|
||||||
|
.filter(|m| m.priority_score.map(|s| s >= 0.7).unwrap_or(false))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if high_priority.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut value_freq: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
|
for memory in high_priority {
|
||||||
|
// Extract value keywords from interpretation
|
||||||
|
if let Some(ref interpretation) = memory.ai_interpretation {
|
||||||
|
let values = extract_value_keywords(interpretation);
|
||||||
|
for value in values {
|
||||||
|
*value_freq.entry(value).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by frequency and take top 5
|
||||||
|
let mut freq_vec: Vec<_> = value_freq.into_iter().collect();
|
||||||
|
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
|
||||||
|
freq_vec.into_iter()
|
||||||
|
.take(5)
|
||||||
|
.map(|(value, _)| value)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract key memory IDs (top priority)
|
||||||
|
fn extract_key_memories(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||||
|
let mut sorted_memories: Vec<_> = memories.iter()
|
||||||
|
.filter(|m| m.priority_score.is_some())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sorted_memories.sort_by(|a, b| {
|
||||||
|
b.priority_score.unwrap()
|
||||||
|
.partial_cmp(&a.priority_score.unwrap())
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
sorted_memories.into_iter()
|
||||||
|
.take(10)
|
||||||
|
.map(|m| m.id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate data quality based on volume
|
||||||
|
fn calculate_data_quality(memories: &[crate::core::Memory], personality: &Option<UserAnalysis>) -> f32 {
|
||||||
|
let memory_count = memories.len() as f32;
|
||||||
|
let has_personality = if personality.is_some() { 1.0 } else { 0.0 };
|
||||||
|
|
||||||
|
// Quality increases with data volume
|
||||||
|
let memory_quality = (memory_count / 50.0).min(1.0); // Max quality at 50+ memories
|
||||||
|
let personality_quality = has_personality * 0.5;
|
||||||
|
|
||||||
|
// Weighted average
|
||||||
|
(memory_quality * 0.5 + personality_quality).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract keywords from text (simple word frequency)
|
||||||
|
fn extract_keywords(text: &str) -> Vec<String> {
|
||||||
|
// Simple keyword extraction: words longer than 3 chars
|
||||||
|
text.split_whitespace()
|
||||||
|
.filter(|w| w.len() > 3)
|
||||||
|
.map(|w| w.to_lowercase().trim_matches(|c: char| !c.is_alphanumeric()).to_string())
|
||||||
|
.filter(|w| !is_stopword(w))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract value-related keywords from interpretation
|
||||||
|
fn extract_value_keywords(text: &str) -> Vec<String> {
|
||||||
|
let value_indicators = [
|
||||||
|
"重視", "大切", "価値", "重要", "優先", "好む", "志向",
|
||||||
|
"シンプル", "効率", "品質", "安定", "革新", "創造",
|
||||||
|
"value", "important", "priority", "prefer", "focus",
|
||||||
|
"simple", "efficient", "quality", "stable", "creative",
|
||||||
|
];
|
||||||
|
|
||||||
|
let words = extract_keywords(text);
|
||||||
|
words.into_iter()
|
||||||
|
.filter(|w| {
|
||||||
|
value_indicators.iter().any(|indicator| w.contains(indicator))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if word is a stopword
|
||||||
|
fn is_stopword(word: &str) -> bool {
|
||||||
|
let stopwords = [
|
||||||
|
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
|
||||||
|
"of", "with", "by", "from", "as", "is", "was", "are", "were", "been",
|
||||||
|
"be", "have", "has", "had", "do", "does", "did", "will", "would", "could",
|
||||||
|
"should", "may", "might", "can", "this", "that", "these", "those",
|
||||||
|
"です", "ます", "ました", "である", "ある", "いる", "する", "した",
|
||||||
|
"という", "として", "ために", "によって", "について",
|
||||||
|
];
|
||||||
|
|
||||||
|
stopwords.contains(&word)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_keywords() {
|
||||||
|
let text = "Rust architecture design is important for scalability";
|
||||||
|
let keywords = extract_keywords(text);
|
||||||
|
|
||||||
|
assert!(keywords.contains(&"rust".to_string()));
|
||||||
|
assert!(keywords.contains(&"architecture".to_string()));
|
||||||
|
assert!(keywords.contains(&"design".to_string()));
|
||||||
|
assert!(!keywords.contains(&"is".to_string())); // stopword
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stopword() {
|
||||||
|
assert!(is_stopword("the"));
|
||||||
|
assert!(is_stopword("です"));
|
||||||
|
assert!(!is_stopword("rust"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,16 @@ impl MemoryStore {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Create user_profiles table (Layer 3.5 - integrated profile cache)
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS user_profiles (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
last_updated TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +376,60 @@ impl MemoryStore {
|
|||||||
|
|
||||||
Ok(analyses)
|
Ok(analyses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Layer 3.5: Integrated Profile ===
|
||||||
|
|
||||||
|
/// Save integrated profile to cache
|
||||||
|
pub fn save_profile(&self, profile: &super::profile::UserProfile) -> Result<()> {
|
||||||
|
let profile_json = serde_json::to_string(profile)?;
|
||||||
|
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO user_profiles (id, data, last_updated) VALUES (1, ?1, ?2)",
|
||||||
|
params![profile_json, profile.last_updated.to_rfc3339()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cached profile if exists
|
||||||
|
pub fn get_cached_profile(&self) -> Result<Option<super::profile::UserProfile>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT data FROM user_profiles WHERE id = 1")?;
|
||||||
|
|
||||||
|
let result = stmt.query_row([], |row| {
|
||||||
|
let json: String = row.get(0)?;
|
||||||
|
Ok(json)
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(json) => {
|
||||||
|
let profile: super::profile::UserProfile = serde_json::from_str(&json)?;
|
||||||
|
Ok(Some(profile))
|
||||||
|
}
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or generate profile (with automatic caching)
|
||||||
|
pub fn get_profile(&self) -> Result<super::profile::UserProfile> {
|
||||||
|
// Check cache first
|
||||||
|
if let Some(cached) = self.get_cached_profile()? {
|
||||||
|
// Check if needs update
|
||||||
|
if !cached.needs_update(self)? {
|
||||||
|
return Ok(cached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new profile
|
||||||
|
let profile = super::profile::UserProfile::generate(self)?;
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
self.save_profile(&profile)?;
|
||||||
|
|
||||||
|
Ok(profile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ impl BaseMCPServer {
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "get_profile",
|
||||||
|
"description": "Get integrated user profile - the essential summary of personality, interests, and values (Layer 3.5). This is the primary tool for understanding the user.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +284,7 @@ impl BaseMCPServer {
|
|||||||
"delete_memory" => self.tool_delete_memory(arguments),
|
"delete_memory" => self.tool_delete_memory(arguments),
|
||||||
"save_user_analysis" => self.tool_save_user_analysis(arguments),
|
"save_user_analysis" => self.tool_save_user_analysis(arguments),
|
||||||
"get_user_analysis" => self.tool_get_user_analysis(),
|
"get_user_analysis" => self.tool_get_user_analysis(),
|
||||||
|
"get_profile" => self.tool_get_profile(),
|
||||||
_ => json!({
|
_ => json!({
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": format!("Unknown tool: {}", tool_name)
|
"error": format!("Unknown tool: {}", tool_name)
|
||||||
@@ -489,6 +498,26 @@ impl BaseMCPServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_get_profile(&self) -> Value {
|
||||||
|
match self.store.get_profile() {
|
||||||
|
Ok(profile) => json!({
|
||||||
|
"success": true,
|
||||||
|
"profile": {
|
||||||
|
"dominant_traits": profile.dominant_traits,
|
||||||
|
"core_interests": profile.core_interests,
|
||||||
|
"core_values": profile.core_values,
|
||||||
|
"key_memory_ids": profile.key_memory_ids,
|
||||||
|
"data_quality": profile.data_quality,
|
||||||
|
"last_updated": profile.last_updated
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Err(e) => json!({
|
||||||
|
"success": false,
|
||||||
|
"error": e.to_string()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_unknown_method(&self, id: Value) -> Value {
|
fn handle_unknown_method(&self, id: Value) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user