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:
Claude
2025-11-06 07:42:33 +00:00
parent 82c8c1c2d2
commit bf21586ad3
3 changed files with 393 additions and 1 deletions

View File

@@ -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
View 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);
}
}

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}; 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",