Implement Layer 4: Relationship Inference System
Layer 4 provides relationship inference by analyzing memory patterns and user personality. This is an optional layer used when game or companion features are active. Core Philosophy: - Independent from Layers 1-3.5 (optional feature) - Inference-based, not stored (computed on-demand) - Simple calculation using existing data - Foundation for external applications (games, companions, etc.) Implementation (src/core/relationship.rs): - RelationshipInference struct with key metrics: * interaction_count: number of memories with entity * avg_priority: average priority of those memories * days_since_last: recency of interaction * bond_strength: inferred strength (0.0-1.0) * relationship_type: close_friend, friend, acquaintance, etc. * confidence: data quality indicator (0.0-1.0) Inference Logic: - Personality-aware: introverts favor count, extroverts favor quality - Simple rules: bond_strength from interaction patterns - Automatic type classification based on metrics - Confidence increases with more data MCP Tools (src/mcp/base.rs): - get_relationship(entity_id): Get specific relationship - list_relationships(limit): List all relationships sorted by strength Design Decisions: - No caching: compute on-demand for simplicity - No persistence: relationships are derived, not stored - Leverages Layer 1 (related_entities) and Layer 3.5 (profile) - Can be extended later with caching if needed Usage Pattern: - Normal use: Layers 1-3.5 only - Game/Companion mode: Enable Layer 4 tools - Frontend calls get_relationship() for character interactions - aigpt backend provides inference, frontend handles presentation
This commit is contained in:
@@ -2,10 +2,12 @@ pub mod analysis;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod relationship;
|
||||||
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 profile::{UserProfile, TraitScore};
|
||||||
|
pub use relationship::{RelationshipInference, infer_all_relationships};
|
||||||
pub use store::MemoryStore;
|
pub use store::MemoryStore;
|
||||||
|
|||||||
280
src/core/relationship.rs
Normal file
280
src/core/relationship.rs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::core::{Memory, MemoryStore, UserProfile};
|
||||||
|
use crate::core::error::Result;
|
||||||
|
|
||||||
|
/// Inferred relationship with an entity (Layer 4)
|
||||||
|
///
|
||||||
|
/// This is not stored permanently but generated on-demand from
|
||||||
|
/// Layer 1 memories and Layer 3.5 user profile.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RelationshipInference {
|
||||||
|
/// Entity identifier
|
||||||
|
pub entity_id: String,
|
||||||
|
|
||||||
|
/// Total interaction count with this entity
|
||||||
|
pub interaction_count: u32,
|
||||||
|
|
||||||
|
/// Average priority score of memories with this entity
|
||||||
|
pub avg_priority: f32,
|
||||||
|
|
||||||
|
/// Days since last interaction
|
||||||
|
pub days_since_last: i64,
|
||||||
|
|
||||||
|
/// Inferred bond strength (0.0-1.0)
|
||||||
|
pub bond_strength: f32,
|
||||||
|
|
||||||
|
/// Inferred relationship type
|
||||||
|
pub relationship_type: String,
|
||||||
|
|
||||||
|
/// Confidence in this inference (0.0-1.0, based on data volume)
|
||||||
|
pub confidence: f32,
|
||||||
|
|
||||||
|
/// When this inference was generated
|
||||||
|
pub inferred_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelationshipInference {
|
||||||
|
/// Infer relationship from memories and user profile
|
||||||
|
pub fn infer(
|
||||||
|
entity_id: String,
|
||||||
|
memories: &[Memory],
|
||||||
|
user_profile: &UserProfile,
|
||||||
|
) -> Self {
|
||||||
|
// Filter memories related to this entity
|
||||||
|
let entity_memories: Vec<_> = memories
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.has_entity(&entity_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let interaction_count = entity_memories.len() as u32;
|
||||||
|
|
||||||
|
// Calculate average priority
|
||||||
|
let total_priority: f32 = entity_memories
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| m.priority_score)
|
||||||
|
.sum();
|
||||||
|
let priority_count = entity_memories
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.priority_score.is_some())
|
||||||
|
.count() as f32;
|
||||||
|
let avg_priority = if priority_count > 0.0 {
|
||||||
|
total_priority / priority_count
|
||||||
|
} else {
|
||||||
|
0.5 // Default to neutral if no scores
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate days since last interaction
|
||||||
|
let days_since_last = entity_memories
|
||||||
|
.iter()
|
||||||
|
.map(|m| (Utc::now() - m.created_at).num_days())
|
||||||
|
.min()
|
||||||
|
.unwrap_or(999);
|
||||||
|
|
||||||
|
// Infer bond strength based on user personality
|
||||||
|
let bond_strength = Self::calculate_bond_strength(
|
||||||
|
interaction_count,
|
||||||
|
avg_priority,
|
||||||
|
user_profile,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Infer relationship type
|
||||||
|
let relationship_type = Self::infer_relationship_type(
|
||||||
|
interaction_count,
|
||||||
|
avg_priority,
|
||||||
|
bond_strength,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate confidence
|
||||||
|
let confidence = Self::calculate_confidence(interaction_count);
|
||||||
|
|
||||||
|
RelationshipInference {
|
||||||
|
entity_id,
|
||||||
|
interaction_count,
|
||||||
|
avg_priority,
|
||||||
|
days_since_last,
|
||||||
|
bond_strength,
|
||||||
|
relationship_type,
|
||||||
|
confidence,
|
||||||
|
inferred_at: Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate bond strength from interaction data and user personality
|
||||||
|
fn calculate_bond_strength(
|
||||||
|
interaction_count: u32,
|
||||||
|
avg_priority: f32,
|
||||||
|
user_profile: &UserProfile,
|
||||||
|
) -> f32 {
|
||||||
|
// Extract extraversion score (if available)
|
||||||
|
let extraversion = user_profile
|
||||||
|
.dominant_traits
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.name == "extraversion")
|
||||||
|
.map(|t| t.score)
|
||||||
|
.unwrap_or(0.5);
|
||||||
|
|
||||||
|
let bond_strength = if extraversion < 0.5 {
|
||||||
|
// Introverted: fewer but deeper relationships
|
||||||
|
// Interaction count matters more
|
||||||
|
let count_factor = (interaction_count as f32 / 20.0).min(1.0);
|
||||||
|
let priority_factor = avg_priority;
|
||||||
|
|
||||||
|
// Weight: 60% count, 40% priority
|
||||||
|
count_factor * 0.6 + priority_factor * 0.4
|
||||||
|
} else {
|
||||||
|
// Extroverted: many relationships, quality varies
|
||||||
|
// Priority matters more
|
||||||
|
let count_factor = (interaction_count as f32 / 50.0).min(1.0);
|
||||||
|
let priority_factor = avg_priority;
|
||||||
|
|
||||||
|
// Weight: 40% count, 60% priority
|
||||||
|
count_factor * 0.4 + priority_factor * 0.6
|
||||||
|
};
|
||||||
|
|
||||||
|
bond_strength.clamp(0.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Infer relationship type from metrics
|
||||||
|
fn infer_relationship_type(
|
||||||
|
interaction_count: u32,
|
||||||
|
avg_priority: f32,
|
||||||
|
bond_strength: f32,
|
||||||
|
) -> String {
|
||||||
|
if bond_strength >= 0.8 {
|
||||||
|
"close_friend".to_string()
|
||||||
|
} else if bond_strength >= 0.6 {
|
||||||
|
"friend".to_string()
|
||||||
|
} else if bond_strength >= 0.4 {
|
||||||
|
if avg_priority >= 0.6 {
|
||||||
|
"valued_acquaintance".to_string()
|
||||||
|
} else {
|
||||||
|
"acquaintance".to_string()
|
||||||
|
}
|
||||||
|
} else if interaction_count >= 5 {
|
||||||
|
"regular_contact".to_string()
|
||||||
|
} else {
|
||||||
|
"distant".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate confidence based on data volume
|
||||||
|
fn calculate_confidence(interaction_count: u32) -> f32 {
|
||||||
|
// Confidence increases with more data
|
||||||
|
// 1-2 interactions: low confidence (0.2-0.3)
|
||||||
|
// 5 interactions: medium confidence (0.5)
|
||||||
|
// 10+ interactions: high confidence (0.8+)
|
||||||
|
let confidence = match interaction_count {
|
||||||
|
0 => 0.0,
|
||||||
|
1 => 0.2,
|
||||||
|
2 => 0.3,
|
||||||
|
3 => 0.4,
|
||||||
|
4 => 0.45,
|
||||||
|
5..=9 => 0.5 + (interaction_count - 5) as f32 * 0.05,
|
||||||
|
_ => 0.8 + ((interaction_count - 10) as f32 * 0.02).min(0.2),
|
||||||
|
};
|
||||||
|
|
||||||
|
confidence.clamp(0.0, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate relationship inferences for all entities in memories
|
||||||
|
pub fn infer_all_relationships(
|
||||||
|
store: &MemoryStore,
|
||||||
|
) -> Result<Vec<RelationshipInference>> {
|
||||||
|
// Get all memories
|
||||||
|
let memories = store.list()?;
|
||||||
|
|
||||||
|
// Get user profile
|
||||||
|
let user_profile = store.get_profile()?;
|
||||||
|
|
||||||
|
// Extract all unique entities
|
||||||
|
let mut entities: HashMap<String, ()> = HashMap::new();
|
||||||
|
for memory in &memories {
|
||||||
|
if let Some(ref entity_list) = memory.related_entities {
|
||||||
|
for entity in entity_list {
|
||||||
|
entities.insert(entity.clone(), ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infer relationship for each entity
|
||||||
|
let mut relationships: Vec<_> = entities
|
||||||
|
.keys()
|
||||||
|
.map(|entity_id| {
|
||||||
|
RelationshipInference::infer(
|
||||||
|
entity_id.clone(),
|
||||||
|
&memories,
|
||||||
|
&user_profile,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by bond strength (descending)
|
||||||
|
relationships.sort_by(|a, b| {
|
||||||
|
b.bond_strength
|
||||||
|
.partial_cmp(&a.bond_strength)
|
||||||
|
.unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(relationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::core::profile::TraitScore;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_confidence_calculation() {
|
||||||
|
assert_eq!(RelationshipInference::calculate_confidence(0), 0.0);
|
||||||
|
assert_eq!(RelationshipInference::calculate_confidence(1), 0.2);
|
||||||
|
assert_eq!(RelationshipInference::calculate_confidence(5), 0.5);
|
||||||
|
assert!(RelationshipInference::calculate_confidence(10) >= 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_relationship_type() {
|
||||||
|
assert_eq!(
|
||||||
|
RelationshipInference::infer_relationship_type(20, 0.9, 0.85),
|
||||||
|
"close_friend"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RelationshipInference::infer_relationship_type(10, 0.7, 0.65),
|
||||||
|
"friend"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
RelationshipInference::infer_relationship_type(5, 0.5, 0.45),
|
||||||
|
"acquaintance"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bond_strength_introverted() {
|
||||||
|
let user_profile = UserProfile {
|
||||||
|
dominant_traits: vec![
|
||||||
|
TraitScore {
|
||||||
|
name: "extraversion".to_string(),
|
||||||
|
score: 0.3, // Introverted
|
||||||
|
},
|
||||||
|
],
|
||||||
|
core_interests: vec![],
|
||||||
|
core_values: vec![],
|
||||||
|
key_memory_ids: vec![],
|
||||||
|
data_quality: 1.0,
|
||||||
|
last_updated: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Introverted: count matters more
|
||||||
|
let strength = RelationshipInference::calculate_bond_strength(
|
||||||
|
20, // Many interactions
|
||||||
|
0.5, // Medium priority
|
||||||
|
&user_profile,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should be high due to high interaction count
|
||||||
|
assert!(strength > 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/mcp/base.rs
112
src/mcp/base.rs
@@ -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};
|
use crate::core::{Memory, MemoryStore, UserAnalysis, RelationshipInference, infer_all_relationships};
|
||||||
|
|
||||||
pub struct BaseMCPServer {
|
pub struct BaseMCPServer {
|
||||||
store: MemoryStore,
|
store: MemoryStore,
|
||||||
@@ -252,6 +252,33 @@ impl BaseMCPServer {
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "get_relationship",
|
||||||
|
"description": "Get inferred relationship with a specific entity (Layer 4). Analyzes memories and user profile to infer bond strength and relationship type. Use only when game/relationship features are active.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Entity identifier (e.g., 'alice', 'companion_miku')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["entity_id"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"name": "list_relationships",
|
||||||
|
"description": "List all inferred relationships sorted by bond strength (Layer 4). Returns relationships with all tracked entities. Use only when game/relationship features are active.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"limit": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum number of relationships to return (default: 10)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +312,8 @@ impl BaseMCPServer {
|
|||||||
"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(),
|
"get_profile" => self.tool_get_profile(),
|
||||||
|
"get_relationship" => self.tool_get_relationship(arguments),
|
||||||
|
"list_relationships" => self.tool_list_relationships(arguments),
|
||||||
_ => json!({
|
_ => json!({
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": format!("Unknown tool: {}", tool_name)
|
"error": format!("Unknown tool: {}", tool_name)
|
||||||
@@ -518,6 +547,87 @@ impl BaseMCPServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_get_relationship(&self, arguments: &Value) -> Value {
|
||||||
|
let entity_id = arguments["entity_id"].as_str().unwrap_or("");
|
||||||
|
|
||||||
|
if entity_id.is_empty() {
|
||||||
|
return json!({
|
||||||
|
"success": false,
|
||||||
|
"error": "entity_id is required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
"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 {
|
||||||
|
let limit = arguments["limit"].as_u64().unwrap_or(10) as usize;
|
||||||
|
|
||||||
|
match infer_all_relationships(&self.store) {
|
||||||
|
Ok(mut relationships) => {
|
||||||
|
// Limit results
|
||||||
|
if relationships.len() > limit {
|
||||||
|
relationships.truncate(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"success": true,
|
||||||
|
"relationships": relationships.iter().map(|r| {
|
||||||
|
json!({
|
||||||
|
"entity_id": r.entity_id,
|
||||||
|
"interaction_count": r.interaction_count,
|
||||||
|
"avg_priority": r.avg_priority,
|
||||||
|
"days_since_last": r.days_since_last,
|
||||||
|
"bond_strength": r.bond_strength,
|
||||||
|
"relationship_type": r.relationship_type,
|
||||||
|
"confidence": r.confidence
|
||||||
|
})
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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