This commit is contained in:
2026-02-27 12:01:07 +09:00
commit a401e91bb4
22 changed files with 4152 additions and 0 deletions

161
src/core/analysis.rs Normal file
View File

@@ -0,0 +1,161 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// User personality analysis based on Big Five model (OCEAN)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAnalysis {
/// Unique identifier using ULID
pub id: String,
/// Openness to Experience (0.0-1.0)
/// Curiosity, imagination, willingness to try new things
pub openness: f32,
/// Conscientiousness (0.0-1.0)
/// Organization, responsibility, self-discipline
pub conscientiousness: f32,
/// Extraversion (0.0-1.0)
/// Sociability, assertiveness, energy level
pub extraversion: f32,
/// Agreeableness (0.0-1.0)
/// Compassion, cooperation, trust
pub agreeableness: f32,
/// Neuroticism (0.0-1.0)
/// Emotional stability, anxiety, mood swings
pub neuroticism: f32,
/// AI-generated summary of the personality analysis
pub summary: String,
/// When this analysis was performed
pub analyzed_at: DateTime<Utc>,
}
impl UserAnalysis {
/// Create a new personality analysis
pub fn new(
openness: f32,
conscientiousness: f32,
extraversion: f32,
agreeableness: f32,
neuroticism: f32,
summary: String,
) -> Self {
let id = Ulid::new().to_string();
let analyzed_at = Utc::now();
Self {
id,
openness: openness.clamp(0.0, 1.0),
conscientiousness: conscientiousness.clamp(0.0, 1.0),
extraversion: extraversion.clamp(0.0, 1.0),
agreeableness: agreeableness.clamp(0.0, 1.0),
neuroticism: neuroticism.clamp(0.0, 1.0),
summary,
analyzed_at,
}
}
/// Get the dominant trait (highest score)
pub fn dominant_trait(&self) -> &str {
let scores = [
(self.openness, "Openness"),
(self.conscientiousness, "Conscientiousness"),
(self.extraversion, "Extraversion"),
(self.agreeableness, "Agreeableness"),
(self.neuroticism, "Neuroticism"),
];
scores
.iter()
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.map(|(_, name)| *name)
.unwrap_or("Unknown")
}
/// Check if a trait is high (>= 0.6)
pub fn is_high(&self, trait_name: &str) -> bool {
let score = match trait_name.to_lowercase().as_str() {
"openness" | "o" => self.openness,
"conscientiousness" | "c" => self.conscientiousness,
"extraversion" | "e" => self.extraversion,
"agreeableness" | "a" => self.agreeableness,
"neuroticism" | "n" => self.neuroticism,
_ => return false,
};
score >= 0.6
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_analysis() {
let analysis = UserAnalysis::new(
0.8,
0.7,
0.4,
0.6,
0.3,
"Test summary".to_string(),
);
assert_eq!(analysis.openness, 0.8);
assert_eq!(analysis.conscientiousness, 0.7);
assert_eq!(analysis.extraversion, 0.4);
assert_eq!(analysis.agreeableness, 0.6);
assert_eq!(analysis.neuroticism, 0.3);
assert!(!analysis.id.is_empty());
}
#[test]
fn test_score_clamping() {
let analysis = UserAnalysis::new(
1.5, // Should clamp to 1.0
-0.2, // Should clamp to 0.0
0.5,
0.5,
0.5,
"Test".to_string(),
);
assert_eq!(analysis.openness, 1.0);
assert_eq!(analysis.conscientiousness, 0.0);
}
#[test]
fn test_dominant_trait() {
let analysis = UserAnalysis::new(
0.9, // Highest
0.5,
0.4,
0.6,
0.3,
"Test".to_string(),
);
assert_eq!(analysis.dominant_trait(), "Openness");
}
#[test]
fn test_is_high() {
let analysis = UserAnalysis::new(
0.8, // High
0.4, // Low
0.6, // Threshold
0.5,
0.3,
"Test".to_string(),
);
assert!(analysis.is_high("openness"));
assert!(!analysis.is_high("conscientiousness"));
assert!(analysis.is_high("extraversion")); // 0.6 is high
}
}

27
src/core/error.rs Normal file
View File

@@ -0,0 +1,27 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MemoryError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Memory not found: {0}")]
NotFound(String),
#[error("Invalid ULID: {0}")]
InvalidId(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Parse error: {0}")]
Parse(String),
}
pub type Result<T> = std::result::Result<T, MemoryError>;

181
src/core/memory.rs Normal file
View File

@@ -0,0 +1,181 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// Represents a single memory entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
/// Unique identifier using ULID (time-sortable)
pub id: String,
/// The actual content of the memory
pub content: String,
/// AI's creative interpretation of the content (Layer 2)
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_interpretation: Option<String>,
/// Priority score evaluated by AI: 0.0 (low) to 1.0 (high) (Layer 2)
#[serde(skip_serializing_if = "Option::is_none")]
pub priority_score: Option<f32>,
/// Related entities (people, places, things) involved in this memory (Layer 4)
#[serde(skip_serializing_if = "Option::is_none")]
pub related_entities: Option<Vec<String>>,
/// When this memory was created
pub created_at: DateTime<Utc>,
/// When this memory was last updated
pub updated_at: DateTime<Utc>,
}
impl Memory {
/// Create a new memory with generated ULID (Layer 1)
pub fn new(content: String) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation: None,
priority_score: None,
related_entities: None,
created_at: now,
updated_at: now,
}
}
/// Create a new AI-interpreted memory (Layer 2)
pub fn new_ai(
content: String,
ai_interpretation: Option<String>,
priority_score: Option<f32>,
) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation,
priority_score,
related_entities: None,
created_at: now,
updated_at: now,
}
}
/// Create a new memory with related entities (Layer 4)
pub fn new_with_entities(
content: String,
ai_interpretation: Option<String>,
priority_score: Option<f32>,
related_entities: Option<Vec<String>>,
) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation,
priority_score,
related_entities,
created_at: now,
updated_at: now,
}
}
/// Update the content of this memory
pub fn update_content(&mut self, content: String) {
self.content = content;
self.updated_at = Utc::now();
}
/// Set or update AI interpretation
pub fn set_ai_interpretation(&mut self, interpretation: String) {
self.ai_interpretation = Some(interpretation);
self.updated_at = Utc::now();
}
/// Set or update priority score
pub fn set_priority_score(&mut self, score: f32) {
self.priority_score = Some(score.clamp(0.0, 1.0));
self.updated_at = Utc::now();
}
/// Set or update related entities
pub fn set_related_entities(&mut self, entities: Vec<String>) {
self.related_entities = Some(entities);
self.updated_at = Utc::now();
}
/// Check if this memory is related to a specific entity
pub fn has_entity(&self, entity_id: &str) -> bool {
self.related_entities
.as_ref()
.map(|entities| entities.iter().any(|e| e == entity_id))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_memory() {
let memory = Memory::new("Test content".to_string());
assert_eq!(memory.content, "Test content");
assert!(!memory.id.is_empty());
assert!(memory.ai_interpretation.is_none());
assert!(memory.priority_score.is_none());
}
#[test]
fn test_new_ai_memory() {
let memory = Memory::new_ai(
"Test content".to_string(),
Some("AI interpretation".to_string()),
Some(0.75),
);
assert_eq!(memory.content, "Test content");
assert_eq!(memory.ai_interpretation, Some("AI interpretation".to_string()));
assert_eq!(memory.priority_score, Some(0.75));
}
#[test]
fn test_update_memory() {
let mut memory = Memory::new("Original".to_string());
let original_time = memory.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
memory.update_content("Updated".to_string());
assert_eq!(memory.content, "Updated");
assert!(memory.updated_at > original_time);
}
#[test]
fn test_set_ai_interpretation() {
let mut memory = Memory::new("Test".to_string());
memory.set_ai_interpretation("Interpretation".to_string());
assert_eq!(memory.ai_interpretation, Some("Interpretation".to_string()));
}
#[test]
fn test_set_priority_score() {
let mut memory = Memory::new("Test".to_string());
memory.set_priority_score(0.8);
assert_eq!(memory.priority_score, Some(0.8));
// Test clamping
memory.set_priority_score(1.5);
assert_eq!(memory.priority_score, Some(1.0));
memory.set_priority_score(-0.5);
assert_eq!(memory.priority_score, Some(0.0));
}
}

13
src/core/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod analysis;
pub mod error;
pub mod memory;
pub mod profile;
pub mod relationship;
pub mod store;
pub use analysis::UserAnalysis;
pub use error::{MemoryError, Result};
pub use memory::Memory;
pub use profile::{UserProfile, TraitScore};
pub use relationship::{RelationshipInference, infer_all_relationships, get_relationship};
pub use store::MemoryStore;

275
src/core/profile.rs Normal file
View 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"));
}
}

317
src/core/relationship.rs Normal file
View File

@@ -0,0 +1,317 @@
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>> {
// Check cache first
if let Some(cached) = store.get_cached_all_relationships()? {
return Ok(cached);
}
// 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)
});
// Cache the result
store.save_all_relationships_cache(&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)]
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);
}
}

693
src/core/store.rs Normal file
View File

@@ -0,0 +1,693 @@
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection};
use std::path::PathBuf;
use super::analysis::UserAnalysis;
use super::error::{MemoryError, Result};
use super::memory::Memory;
/// SQLite-based memory storage
pub struct MemoryStore {
conn: Connection,
}
impl MemoryStore {
/// Create a new MemoryStore with the given database path
pub fn new(db_path: PathBuf) -> Result<Self> {
// Ensure parent directory exists
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(db_path)?;
// Initialize database schema
conn.execute(
"CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
ai_interpretation TEXT,
priority_score REAL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
// Migrate existing tables (add columns if they don't exist)
// SQLite doesn't have "IF NOT EXISTS" for columns, so we check first
let has_ai_interpretation: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='ai_interpretation'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_ai_interpretation {
conn.execute("ALTER TABLE memories ADD COLUMN ai_interpretation TEXT", [])?;
conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?;
}
// Migrate for Layer 4: related_entities
let has_related_entities: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='related_entities'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_related_entities {
conn.execute("ALTER TABLE memories ADD COLUMN related_entities TEXT", [])?;
}
// Create indexes for better query performance
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_updated_at ON memories(updated_at)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_priority_score ON memories(priority_score)",
[],
)?;
// Create user_analyses table (Layer 3)
conn.execute(
"CREATE TABLE IF NOT EXISTS user_analyses (
id TEXT PRIMARY KEY,
openness REAL NOT NULL,
conscientiousness REAL NOT NULL,
extraversion REAL NOT NULL,
agreeableness REAL NOT NULL,
neuroticism REAL NOT NULL,
summary TEXT NOT NULL,
analyzed_at TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_analyzed_at ON user_analyses(analyzed_at)",
[],
)?;
// 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
)",
[],
)?;
// 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 })
}
/// Create a new MemoryStore using default config directory
pub fn default() -> Result<Self> {
let data_dir = dirs::config_dir()
.ok_or_else(|| MemoryError::Config("Could not find config directory".to_string()))?
.join("syui")
.join("ai")
.join("gpt");
let db_path = data_dir.join("memory.db");
Self::new(db_path)
}
/// Insert a new memory
pub fn create(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
self.conn.execute(
"INSERT INTO memories (id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
&memory.id,
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
related_entities_json,
memory.created_at.to_rfc3339(),
memory.updated_at.to_rfc3339(),
],
)?;
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// Get a memory by ID
pub fn get(&self, id: &str) -> Result<Memory> {
let mut stmt = self
.conn
.prepare("SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories WHERE id = ?1")?;
let memory = stmt.query_row(params![id], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?;
Ok(memory)
}
/// Update an existing memory
pub fn update(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
let rows_affected = self.conn.execute(
"UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, related_entities = ?4, updated_at = ?5
WHERE id = ?6",
params![
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
related_entities_json,
memory.updated_at.to_rfc3339(),
&memory.id,
],
)?;
if rows_affected == 0 {
return Err(MemoryError::NotFound(memory.id.clone()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// Delete a memory by ID
pub fn delete(&self, id: &str) -> Result<()> {
let rows_affected = self
.conn
.execute("DELETE FROM memories WHERE id = ?1", params![id])?;
if rows_affected == 0 {
return Err(MemoryError::NotFound(id.to_string()));
}
// Clear relationship cache since memory data changed
self.clear_relationship_cache()?;
Ok(())
}
/// List all memories, ordered by creation time (newest first)
pub fn list(&self) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories ORDER BY created_at DESC",
)?;
let memories = stmt
.query_map([], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(memories)
}
/// Search memories by content or AI interpretation (case-insensitive)
pub fn search(&self, query: &str) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories
WHERE content LIKE ?1 OR ai_interpretation LIKE ?1
ORDER BY created_at DESC",
)?;
let search_pattern = format!("%{}%", query);
let memories = stmt
.query_map(params![search_pattern], |row| {
let created_at: String = row.get(5)?;
let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
6,
rusqlite::types::Type::Text,
Box::new(e),
))?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(memories)
}
/// Count total memories
pub fn count(&self) -> Result<usize> {
let count: usize = self
.conn
.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
Ok(count)
}
// ========== Layer 3: User Analysis Methods ==========
/// Save a new user personality analysis
pub fn save_analysis(&self, analysis: &UserAnalysis) -> Result<()> {
self.conn.execute(
"INSERT INTO user_analyses (id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
&analysis.id,
&analysis.openness,
&analysis.conscientiousness,
&analysis.extraversion,
&analysis.agreeableness,
&analysis.neuroticism,
&analysis.summary,
analysis.analyzed_at.to_rfc3339(),
],
)?;
Ok(())
}
/// Get the most recent user analysis
pub fn get_latest_analysis(&self) -> Result<Option<UserAnalysis>> {
let mut stmt = self.conn.prepare(
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
FROM user_analyses
ORDER BY analyzed_at DESC
LIMIT 1",
)?;
let result = stmt.query_row([], |row| {
let analyzed_at: String = row.get(7)?;
Ok(UserAnalysis {
id: row.get(0)?,
openness: row.get(1)?,
conscientiousness: row.get(2)?,
extraversion: row.get(3)?,
agreeableness: row.get(4)?,
neuroticism: row.get(5)?,
summary: row.get(6)?,
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
7,
rusqlite::types::Type::Text,
Box::new(e),
)
})?,
})
});
match result {
Ok(analysis) => Ok(Some(analysis)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Get all user analyses, ordered by date (newest first)
pub fn list_analyses(&self) -> Result<Vec<UserAnalysis>> {
let mut stmt = self.conn.prepare(
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
FROM user_analyses
ORDER BY analyzed_at DESC",
)?;
let analyses = stmt
.query_map([], |row| {
let analyzed_at: String = row.get(7)?;
Ok(UserAnalysis {
id: row.get(0)?,
openness: row.get(1)?,
conscientiousness: row.get(2)?,
extraversion: row.get(3)?,
agreeableness: row.get(4)?,
neuroticism: row.get(5)?,
summary: row.get(6)?,
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
7,
rusqlite::types::Type::Text,
Box::new(e),
)
})?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
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)
}
// ========== 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_seconds() / 60;
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_seconds() / 60;
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)]
mod tests {
use super::*;
fn create_test_store() -> MemoryStore {
MemoryStore::new(":memory:".into()).unwrap()
}
#[test]
fn test_create_and_get() {
let store = create_test_store();
let memory = Memory::new("Test content".to_string());
store.create(&memory).unwrap();
let retrieved = store.get(&memory.id).unwrap();
assert_eq!(retrieved.id, memory.id);
assert_eq!(retrieved.content, memory.content);
}
#[test]
fn test_update() {
let store = create_test_store();
let mut memory = Memory::new("Original".to_string());
store.create(&memory).unwrap();
memory.update_content("Updated".to_string());
store.update(&memory).unwrap();
let retrieved = store.get(&memory.id).unwrap();
assert_eq!(retrieved.content, "Updated");
}
#[test]
fn test_delete() {
let store = create_test_store();
let memory = Memory::new("To delete".to_string());
store.create(&memory).unwrap();
store.delete(&memory.id).unwrap();
assert!(store.get(&memory.id).is_err());
}
#[test]
fn test_list() {
let store = create_test_store();
let mem1 = Memory::new("First".to_string());
let mem2 = Memory::new("Second".to_string());
store.create(&mem1).unwrap();
store.create(&mem2).unwrap();
let memories = store.list().unwrap();
assert_eq!(memories.len(), 2);
}
#[test]
fn test_search() {
let store = create_test_store();
store
.create(&Memory::new("Hello world".to_string()))
.unwrap();
store
.create(&Memory::new("Goodbye world".to_string()))
.unwrap();
store.create(&Memory::new("Testing".to_string())).unwrap();
let results = store.search("world").unwrap();
assert_eq!(results.len(), 2);
let results = store.search("Hello").unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_count() {
let store = create_test_store();
assert_eq!(store.count().unwrap(), 0);
store.create(&Memory::new("Test".to_string())).unwrap();
assert_eq!(store.count().unwrap(), 1);
}
}

2
src/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod core;
pub mod mcp;

141
src/main.rs Normal file
View File

@@ -0,0 +1,141 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use aigpt::core::{Memory, MemoryStore};
use aigpt::mcp::BaseMCPServer;
#[derive(Parser)]
#[command(name = "aigpt")]
#[command(about = "Simple memory storage for Claude with MCP - Layer 1")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start MCP server
Server {
/// Enable Layer 4 relationship features (for games/companions)
#[arg(long)]
enable_layer4: bool,
},
/// Create a new memory
Create {
/// Content of the memory
content: String,
},
/// Get a memory by ID
Get {
/// Memory ID
id: String,
},
/// Update a memory
Update {
/// Memory ID
id: String,
/// New content
content: String,
},
/// Delete a memory
Delete {
/// Memory ID
id: String,
},
/// List all memories
List,
/// Search memories by content
Search {
/// Search query
query: String,
},
/// Show statistics
Stats,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Server { enable_layer4 } => {
let server = BaseMCPServer::new(enable_layer4)?;
server.run()?;
}
Commands::Create { content } => {
let store = MemoryStore::default()?;
let memory = Memory::new(content);
store.create(&memory)?;
println!("Created memory: {}", memory.id);
}
Commands::Get { id } => {
let store = MemoryStore::default()?;
let memory = store.get(&id)?;
println!("ID: {}", memory.id);
println!("Content: {}", memory.content);
println!("Created: {}", memory.created_at);
println!("Updated: {}", memory.updated_at);
}
Commands::Update { id, content } => {
let store = MemoryStore::default()?;
let mut memory = store.get(&id)?;
memory.update_content(content);
store.update(&memory)?;
println!("Updated memory: {}", memory.id);
}
Commands::Delete { id } => {
let store = MemoryStore::default()?;
store.delete(&id)?;
println!("Deleted memory: {}", id);
}
Commands::List => {
let store = MemoryStore::default()?;
let memories = store.list()?;
if memories.is_empty() {
println!("No memories found");
} else {
for memory in memories {
println!("\n[{}]", memory.id);
println!(" {}", memory.content);
println!(" Created: {}", memory.created_at);
}
}
}
Commands::Search { query } => {
let store = MemoryStore::default()?;
let memories = store.search(&query)?;
if memories.is_empty() {
println!("No memories found matching '{}'", query);
} else {
println!("Found {} memory(ies):", memories.len());
for memory in memories {
println!("\n[{}]", memory.id);
println!(" {}", memory.content);
println!(" Created: {}", memory.created_at);
}
}
}
Commands::Stats => {
let store = MemoryStore::default()?;
let count = store.count()?;
println!("Total memories: {}", count);
}
}
Ok(())
}

648
src/mcp/base.rs Normal file
View File

@@ -0,0 +1,648 @@
use anyhow::Result;
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
use crate::core::{Memory, MemoryStore, UserAnalysis, infer_all_relationships, get_relationship};
pub struct BaseMCPServer {
store: MemoryStore,
enable_layer4: bool,
}
impl BaseMCPServer {
pub fn new(enable_layer4: bool) -> Result<Self> {
let store = MemoryStore::default()?;
Ok(BaseMCPServer { store, enable_layer4 })
}
pub fn run(&self) -> Result<()> {
let stdin = io::stdin();
let mut stdout = io::stdout();
let reader = stdin.lock();
let lines = reader.lines();
for line_result in lines {
match line_result {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(request) = serde_json::from_str::<Value>(&trimmed) {
let response = self.handle_request(request);
let response_str = serde_json::to_string(&response)?;
stdout.write_all(response_str.as_bytes())?;
stdout.write_all(b"\n")?;
stdout.flush()?;
}
}
Err(_) => break,
}
}
Ok(())
}
fn handle_request(&self, request: Value) -> Value {
let method = request["method"].as_str().unwrap_or("");
let id = request["id"].clone();
match method {
"initialize" => self.handle_initialize(id),
"tools/list" => self.handle_tools_list(id),
"tools/call" => self.handle_tools_call(request, id),
_ => self.handle_unknown_method(id),
}
}
fn handle_initialize(&self, id: Value) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "aigpt",
"version": "0.2.0"
}
}
})
}
fn handle_tools_list(&self, id: Value) -> Value {
let tools = self.get_available_tools();
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"tools": tools
}
})
}
fn get_available_tools(&self) -> Vec<Value> {
let mut tools = vec![
json!({
"name": "create_memory",
"description": "Create a new memory entry (Layer 1: simple storage)",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Content of the memory"
}
},
"required": ["content"]
}
}),
json!({
"name": "create_ai_memory",
"description": "Create a memory with AI interpretation and priority score (Layer 2)",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Original content of the memory"
},
"ai_interpretation": {
"type": "string",
"description": "AI's creative interpretation of the content (optional)"
},
"priority_score": {
"type": "number",
"description": "Priority score from 0.0 (low) to 1.0 (high) (optional)",
"minimum": 0.0,
"maximum": 1.0
}
},
"required": ["content"]
}
}),
json!({
"name": "get_memory",
"description": "Get a memory by ID",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Memory ID"
}
},
"required": ["id"]
}
}),
json!({
"name": "search_memories",
"description": "Search memories by content",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
}
},
"required": ["query"]
}
}),
json!({
"name": "list_memories",
"description": "List all memories",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
json!({
"name": "update_memory",
"description": "Update an existing memory entry",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the memory to update"
},
"content": {
"type": "string",
"description": "New content for the memory"
}
},
"required": ["id", "content"]
}
}),
json!({
"name": "delete_memory",
"description": "Delete a memory entry",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the memory to delete"
}
},
"required": ["id"]
}
}),
json!({
"name": "save_user_analysis",
"description": "Save a Big Five personality analysis based on user's memories (Layer 3)",
"inputSchema": {
"type": "object",
"properties": {
"openness": {
"type": "number",
"description": "Openness to Experience (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"conscientiousness": {
"type": "number",
"description": "Conscientiousness (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"extraversion": {
"type": "number",
"description": "Extraversion (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"agreeableness": {
"type": "number",
"description": "Agreeableness (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"neuroticism": {
"type": "number",
"description": "Neuroticism (0.0-1.0)",
"minimum": 0.0,
"maximum": 1.0
},
"summary": {
"type": "string",
"description": "AI-generated summary of the personality analysis"
}
},
"required": ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism", "summary"]
}
}),
json!({
"name": "get_user_analysis",
"description": "Get the most recent Big Five personality analysis (Layer 3)",
"inputSchema": {
"type": "object",
"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": {}
}
}),
];
// Layer 4 tools (optional - only when enabled)
if self.enable_layer4 {
tools.extend(vec![
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)"
}
}
}
}),
]);
}
tools
}
fn handle_tools_call(&self, request: Value, id: Value) -> Value {
let tool_name = request["params"]["name"].as_str().unwrap_or("");
let arguments = &request["params"]["arguments"];
let result = self.execute_tool(tool_name, arguments);
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"content": [{
"type": "text",
"text": result.to_string()
}]
}
})
}
fn execute_tool(&self, tool_name: &str, arguments: &Value) -> Value {
match tool_name {
"create_memory" => self.tool_create_memory(arguments),
"create_ai_memory" => self.tool_create_ai_memory(arguments),
"get_memory" => self.tool_get_memory(arguments),
"search_memories" => self.tool_search_memories(arguments),
"list_memories" => self.tool_list_memories(),
"update_memory" => self.tool_update_memory(arguments),
"delete_memory" => self.tool_delete_memory(arguments),
"save_user_analysis" => self.tool_save_user_analysis(arguments),
"get_user_analysis" => self.tool_get_user_analysis(),
"get_profile" => self.tool_get_profile(),
// Layer 4 tools (require --enable-layer4 flag)
"get_relationship" | "list_relationships" => {
if !self.enable_layer4 {
return json!({
"success": false,
"error": "Layer 4 is not enabled. Start server with --enable-layer4 flag to use relationship features."
});
}
match tool_name {
"get_relationship" => self.tool_get_relationship(arguments),
"list_relationships" => self.tool_list_relationships(arguments),
_ => unreachable!(),
}
}
_ => json!({
"success": false,
"error": format!("Unknown tool: {}", tool_name)
}),
}
}
fn tool_create_memory(&self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let memory = Memory::new(content.to_string());
match self.store.create(&memory) {
Ok(()) => json!({
"success": true,
"id": memory.id,
"message": "Memory created successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_create_ai_memory(&self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let ai_interpretation = arguments["ai_interpretation"]
.as_str()
.map(|s| s.to_string());
let priority_score = arguments["priority_score"].as_f64().map(|f| f as f32);
let memory = Memory::new_ai(content.to_string(), ai_interpretation, priority_score);
match self.store.create(&memory) {
Ok(()) => json!({
"success": true,
"id": memory.id,
"message": "AI memory created successfully",
"has_interpretation": memory.ai_interpretation.is_some(),
"has_score": memory.priority_score.is_some()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
match self.store.get(id) {
Ok(memory) => json!({
"success": true,
"memory": {
"id": memory.id,
"content": memory.content,
"ai_interpretation": memory.ai_interpretation,
"priority_score": memory.priority_score,
"created_at": memory.created_at,
"updated_at": memory.updated_at
}
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_search_memories(&self, arguments: &Value) -> Value {
let query = arguments["query"].as_str().unwrap_or("");
match self.store.search(query) {
Ok(memories) => json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"ai_interpretation": m.ai_interpretation,
"priority_score": m.priority_score,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_list_memories(&self) -> Value {
match self.store.list() {
Ok(memories) => json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"ai_interpretation": m.ai_interpretation,
"priority_score": m.priority_score,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_update_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
let content = arguments["content"].as_str().unwrap_or("");
match self.store.get(id) {
Ok(mut memory) => {
memory.update_content(content.to_string());
match self.store.update(&memory) {
Ok(()) => json!({
"success": true,
"message": "Memory updated successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_delete_memory(&self, arguments: &Value) -> Value {
let id = arguments["id"].as_str().unwrap_or("");
match self.store.delete(id) {
Ok(()) => json!({
"success": true,
"message": "Memory deleted successfully"
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
// ========== Layer 3: User Analysis Tools ==========
fn tool_save_user_analysis(&self, arguments: &Value) -> Value {
let openness = arguments["openness"].as_f64().unwrap_or(0.5) as f32;
let conscientiousness = arguments["conscientiousness"].as_f64().unwrap_or(0.5) as f32;
let extraversion = arguments["extraversion"].as_f64().unwrap_or(0.5) as f32;
let agreeableness = arguments["agreeableness"].as_f64().unwrap_or(0.5) as f32;
let neuroticism = arguments["neuroticism"].as_f64().unwrap_or(0.5) as f32;
let summary = arguments["summary"].as_str().unwrap_or("").to_string();
let analysis = UserAnalysis::new(
openness,
conscientiousness,
extraversion,
agreeableness,
neuroticism,
summary,
);
match self.store.save_analysis(&analysis) {
Ok(()) => json!({
"success": true,
"id": analysis.id,
"message": "User analysis saved successfully",
"dominant_trait": analysis.dominant_trait()
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
fn tool_get_user_analysis(&self) -> Value {
match self.store.get_latest_analysis() {
Ok(Some(analysis)) => json!({
"success": true,
"analysis": {
"id": analysis.id,
"openness": analysis.openness,
"conscientiousness": analysis.conscientiousness,
"extraversion": analysis.extraversion,
"agreeableness": analysis.agreeableness,
"neuroticism": analysis.neuroticism,
"summary": analysis.summary,
"dominant_trait": analysis.dominant_trait(),
"analyzed_at": analysis.analyzed_at
}
}),
Ok(None) => json!({
"success": true,
"analysis": null,
"message": "No analysis found. Run personality analysis first."
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
}),
}
}
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 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 relationship (with caching)
match get_relationship(&self.store, entity_id) {
Ok(relationship) => 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
}
}),
Err(e) => json!({
"success": false,
"error": format!("Failed to get relationship: {}", e)
}),
}
}
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 {
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": "Method not found"
}
})
}
}

3
src/mcp/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod base;
pub use base::BaseMCPServer;

36
src/tmp/ai_interpreter.rs Normal file
View File

@@ -0,0 +1,36 @@
use anyhow::Result;
/// AIInterpreter - Claude Code による解釈を期待する軽量ラッパー
///
/// このモジュールは外部 AI API を呼び出しません。
/// 代わりに、Claude Code 自身がコンテンツを解釈し、スコアを計算することを期待します。
///
/// 完全にローカルで動作し、API コストはゼロです。
pub struct AIInterpreter;
impl AIInterpreter {
pub fn new() -> Self {
AIInterpreter
}
/// コンテンツをそのまま返すClaude Code が解釈を担当)
pub async fn interpret_content(&self, content: &str) -> Result<String> {
Ok(content.to_string())
}
/// デフォルトスコアを返すClaude Code が実際のスコアを決定)
pub async fn calculate_priority_score(&self, _content: &str, _user_context: Option<&str>) -> Result<f32> {
Ok(0.5) // デフォルト値
}
/// 解釈とスコアリングを Claude Code に委ねる
pub async fn analyze(&self, content: &str, _user_context: Option<&str>) -> Result<(String, f32)> {
Ok((content.to_string(), 0.5))
}
}
impl Default for AIInterpreter {
fn default() -> Self {
Self::new()
}
}

433
src/tmp/companion.rs Normal file
View File

@@ -0,0 +1,433 @@
use crate::memory::Memory;
use crate::game_formatter::{MemoryRarity, DiagnosisType};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc, Datelike};
/// コンパニオンキャラクター
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Companion {
pub name: String,
pub personality: CompanionPersonality,
pub relationship_level: u32, // レベル(経験値で上昇)
pub affection_score: f32, // 好感度 (0.0-1.0)
pub trust_level: u32, // 信頼度 (0-100)
pub total_xp: u32, // 総XP
pub last_interaction: DateTime<Utc>,
pub shared_memories: Vec<String>, // 共有された記憶のID
}
/// コンパニオンの性格
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompanionPersonality {
Energetic, // 元気で冒険好き - 革新者と相性◎
Intellectual, // 知的で思慮深い - 哲学者と相性◎
Practical, // 現実的で頼れる - 実務家と相性◎
Dreamy, // 夢見がちでロマンチック - 夢想家と相性◎
Balanced, // バランス型 - 分析家と相性◎
}
impl CompanionPersonality {
pub fn emoji(&self) -> &str {
match self {
CompanionPersonality::Energetic => "",
CompanionPersonality::Intellectual => "📚",
CompanionPersonality::Practical => "🎯",
CompanionPersonality::Dreamy => "🌙",
CompanionPersonality::Balanced => "⚖️",
}
}
pub fn name(&self) -> &str {
match self {
CompanionPersonality::Energetic => "元気で冒険好き",
CompanionPersonality::Intellectual => "知的で思慮深い",
CompanionPersonality::Practical => "現実的で頼れる",
CompanionPersonality::Dreamy => "夢見がちでロマンチック",
CompanionPersonality::Balanced => "バランス型",
}
}
/// ユーザーの診断タイプとの相性
pub fn compatibility(&self, user_type: &DiagnosisType) -> f32 {
match (self, user_type) {
(CompanionPersonality::Energetic, DiagnosisType::Innovator) => 0.95,
(CompanionPersonality::Intellectual, DiagnosisType::Philosopher) => 0.95,
(CompanionPersonality::Practical, DiagnosisType::Pragmatist) => 0.95,
(CompanionPersonality::Dreamy, DiagnosisType::Visionary) => 0.95,
(CompanionPersonality::Balanced, DiagnosisType::Analyst) => 0.95,
// その他の組み合わせ
_ => 0.7,
}
}
}
impl Companion {
pub fn new(name: String, personality: CompanionPersonality) -> Self {
Companion {
name,
personality,
relationship_level: 1,
affection_score: 0.0,
trust_level: 0,
total_xp: 0,
last_interaction: Utc::now(),
shared_memories: Vec::new(),
}
}
/// 記憶を共有して反応を得る
pub fn react_to_memory(&mut self, memory: &Memory, user_type: &DiagnosisType) -> CompanionReaction {
let rarity = MemoryRarity::from_score(memory.priority_score);
let xp = rarity.xp_value();
// XPを加算
self.total_xp += xp;
// 好感度上昇(スコアと相性による)
let compatibility = self.personality.compatibility(user_type);
let affection_gain = memory.priority_score * compatibility * 0.1;
self.affection_score = (self.affection_score + affection_gain).min(1.0);
// 信頼度上昇(高スコアの記憶ほど上昇)
if memory.priority_score >= 0.8 {
self.trust_level = (self.trust_level + 5).min(100);
}
// レベルアップチェック
let old_level = self.relationship_level;
self.relationship_level = (self.total_xp / 1000) + 1;
let level_up = self.relationship_level > old_level;
// 記憶を共有リストに追加
if memory.priority_score >= 0.6 {
self.shared_memories.push(memory.id.clone());
}
self.last_interaction = Utc::now();
// 反応メッセージを生成
let message = self.generate_reaction_message(memory, &rarity, user_type);
CompanionReaction {
message,
affection_gained: affection_gain,
xp_gained: xp,
level_up,
new_level: self.relationship_level,
current_affection: self.affection_score,
special_event: self.check_special_event(),
}
}
/// 記憶に基づく反応メッセージを生成
fn generate_reaction_message(&self, memory: &Memory, rarity: &MemoryRarity, _user_type: &DiagnosisType) -> String {
let content_preview = if memory.content.len() > 50 {
format!("{}...", &memory.content[..50])
} else {
memory.content.clone()
};
match (rarity, &self.personality) {
// LEGENDARY反応
(MemoryRarity::Legendary, CompanionPersonality::Energetic) => {
format!(
"すごい!「{}」って本当に素晴らしいアイデアだね!\n\
一緒に実現させよう!ワクワクするよ!",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Intellectual) => {
format!(
"{}」という考え、とても興味深いわ。\n\
深い洞察力を感じるの。もっと詳しく聞かせて?",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Practical) => {
format!(
"{}」か。実現可能性が高そうだね。\n\
具体的な計画を一緒に立てようよ。",
content_preview
)
}
(MemoryRarity::Legendary, CompanionPersonality::Dreamy) => {
format!(
"{}」...素敵♪ まるで夢みたい。\n\
あなたの想像力、本当に好きよ。",
content_preview
)
}
// EPIC反応
(MemoryRarity::Epic, _) => {
format!(
"おお、「{}」って面白いね!\n\
あなたのそういうところ、好きだな。",
content_preview
)
}
// RARE反応
(MemoryRarity::Rare, _) => {
format!(
"{}」か。なるほどね。\n\
そういう視点、参考になるよ。",
content_preview
)
}
// 通常反応
_ => {
format!(
"{}」について考えてるんだね。\n\
いつも色々考えてて尊敬するよ。",
content_preview
)
}
}
}
/// スペシャルイベントチェック
fn check_special_event(&self) -> Option<SpecialEvent> {
// 好感度MAXイベント
if self.affection_score >= 1.0 {
return Some(SpecialEvent::MaxAffection);
}
// レベル10到達
if self.relationship_level == 10 {
return Some(SpecialEvent::Level10);
}
// 信頼度MAX
if self.trust_level >= 100 {
return Some(SpecialEvent::MaxTrust);
}
None
}
/// デイリーメッセージを生成
pub fn generate_daily_message(&self) -> String {
let messages = match &self.personality {
CompanionPersonality::Energetic => vec![
"おはよう!今日は何か面白いことある?",
"ねえねえ、今日は一緒に新しいことやろうよ!",
"今日も元気出していこー!",
],
CompanionPersonality::Intellectual => vec![
"おはよう。今日はどんな発見があるかしら?",
"最近読んだ本の話、聞かせてくれない?",
"今日も一緒に学びましょう。",
],
CompanionPersonality::Practical => vec![
"おはよう。今日の予定は?",
"やることリスト、一緒に確認しようか。",
"今日も効率よくいこうね。",
],
CompanionPersonality::Dreamy => vec![
"おはよう...まだ夢の続き見てたの。",
"今日はどんな素敵なことが起こるかな♪",
"あなたと過ごす時間、大好き。",
],
CompanionPersonality::Balanced => vec![
"おはよう。今日も頑張ろうね。",
"何か手伝えることある?",
"今日も一緒にいられて嬉しいよ。",
],
};
let today = chrono::Utc::now().ordinal();
messages[today as usize % messages.len()].to_string()
}
}
/// コンパニオンの反応
#[derive(Debug, Serialize)]
pub struct CompanionReaction {
pub message: String,
pub affection_gained: f32,
pub xp_gained: u32,
pub level_up: bool,
pub new_level: u32,
pub current_affection: f32,
pub special_event: Option<SpecialEvent>,
}
/// スペシャルイベント
#[derive(Debug, Serialize)]
pub enum SpecialEvent {
MaxAffection, // 好感度MAX
Level10, // レベル10到達
MaxTrust, // 信頼度MAX
FirstDate, // 初デート
Confession, // 告白
}
impl SpecialEvent {
pub fn message(&self, companion_name: &str) -> String {
match self {
SpecialEvent::MaxAffection => {
format!(
"💕 特別なイベント発生!\n\n\
{}:「ねえ...あのね。\n\
いつも一緒にいてくれてありがとう。\n\
あなたのこと、すごく大切に思ってるの。\n\
これからも、ずっと一緒にいてね?」\n\n\
🎊 {} の好感度がMAXになりました",
companion_name, companion_name
)
}
SpecialEvent::Level10 => {
format!(
"🎉 レベル10到達\n\n\
{}:「ここまで一緒に来られたね。\n\
あなたとなら、どこまでも行けそう。」",
companion_name
)
}
SpecialEvent::MaxTrust => {
format!(
"✨ 信頼度MAX\n\n\
{}:「あなたのこと、心から信頼してる。\n\
何でも話せるって、すごく嬉しいよ。」",
companion_name
)
}
SpecialEvent::FirstDate => {
format!(
"💐 初デートイベント!\n\n\
{}:「今度、二人でどこか行かない?」",
companion_name
)
}
SpecialEvent::Confession => {
format!(
"💝 告白イベント!\n\n\
{}:「好きです。付き合ってください。」",
companion_name
)
}
}
}
}
/// コンパニオンフォーマッター
pub struct CompanionFormatter;
impl CompanionFormatter {
/// 反応を表示
pub fn format_reaction(companion: &Companion, reaction: &CompanionReaction) -> String {
let affection_bar = Self::format_affection_bar(reaction.current_affection);
let level_up_text = if reaction.level_up {
format!("\n🎊 レベルアップ! Lv.{} → Lv.{}", reaction.new_level - 1, reaction.new_level)
} else {
String::new()
};
let special_event_text = if let Some(ref event) = reaction.special_event {
format!("\n\n{}", event.message(&companion.name))
} else {
String::new()
};
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 💕 {} の反応 ║
╚══════════════════════════════════════════════════════════════╝
{} {}:
「{}」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💕 好感度: {} (+{:.1}%)
💎 XP獲得: +{} XP{}
🏆 レベル: Lv.{}
🤝 信頼度: {} / 100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}
"#,
companion.name,
companion.personality.emoji(),
companion.name,
reaction.message,
affection_bar,
reaction.affection_gained * 100.0,
reaction.xp_gained,
level_up_text,
companion.relationship_level,
companion.trust_level,
special_event_text
)
}
/// プロフィール表示
pub fn format_profile(companion: &Companion) -> String {
let affection_bar = Self::format_affection_bar(companion.affection_score);
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 💕 {} のプロフィール ║
╚══════════════════════════════════════════════════════════════╝
{} 性格: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 ステータス
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 関係レベル: Lv.{}
💕 好感度: {}
🤝 信頼度: {} / 100
💎 総XP: {} XP
📚 共有記憶: {}個
🕐 最終交流: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 今日のひとこと:
「{}」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"#,
companion.name,
companion.personality.emoji(),
companion.personality.name(),
companion.relationship_level,
affection_bar,
companion.trust_level,
companion.total_xp,
companion.shared_memories.len(),
companion.last_interaction.format("%Y-%m-%d %H:%M"),
companion.generate_daily_message()
)
}
fn format_affection_bar(affection: f32) -> String {
let hearts = (affection * 10.0) as usize;
let filled = "❤️".repeat(hearts);
let empty = "🤍".repeat(10 - hearts);
format!("{}{} {:.0}%", filled, empty, affection * 100.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_companion_creation() {
let companion = Companion::new(
"エミリー".to_string(),
CompanionPersonality::Energetic,
);
assert_eq!(companion.name, "エミリー");
assert_eq!(companion.relationship_level, 1);
assert_eq!(companion.affection_score, 0.0);
}
#[test]
fn test_compatibility() {
let personality = CompanionPersonality::Energetic;
let innovator = DiagnosisType::Innovator;
assert_eq!(personality.compatibility(&innovator), 0.95);
}
}

296
src/tmp/extended.rs Normal file
View File

@@ -0,0 +1,296 @@
use anyhow::Result;
use serde_json::{json, Value};
use super::base::BaseMCPServer;
pub struct ExtendedMCPServer {
base: BaseMCPServer,
}
impl ExtendedMCPServer {
pub async fn new() -> Result<Self> {
let base = BaseMCPServer::new().await?;
Ok(ExtendedMCPServer { base })
}
pub async fn run(&mut self) -> Result<()> {
self.base.run().await
}
pub async fn handle_request(&mut self, request: Value) -> Value {
self.base.handle_request(request).await
}
// 拡張ツールを追加
pub fn get_available_tools(&self) -> Vec<Value> {
#[allow(unused_mut)]
let mut tools = self.base.get_available_tools();
// AI分析ツールを追加
#[cfg(feature = "ai-analysis")]
{
tools.push(json!({
"name": "analyze_sentiment",
"description": "Analyze sentiment of memories",
"inputSchema": {
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "Time period to analyze"
}
}
}
}));
tools.push(json!({
"name": "extract_insights",
"description": "Extract insights and patterns from memories",
"inputSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Category to analyze"
}
}
}
}));
}
// Web統合ツールを追加
#[cfg(feature = "web-integration")]
{
tools.push(json!({
"name": "import_webpage",
"description": "Import content from a webpage",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to import from"
}
},
"required": ["url"]
}
}));
}
// セマンティック検索強化
#[cfg(feature = "semantic-search")]
{
// create_memoryを拡張版で上書き
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "create_memory") {
tools[pos] = json!({
"name": "create_memory",
"description": "Create a new memory entry with optional AI analysis",
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Content of the memory"
},
"analyze": {
"type": "boolean",
"description": "Enable AI analysis for this memory"
}
},
"required": ["content"]
}
});
}
// search_memoriesを拡張版で上書き
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "search_memories") {
tools[pos] = json!({
"name": "search_memories",
"description": "Search memories with advanced options",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"semantic": {
"type": "boolean",
"description": "Use semantic search"
},
"category": {
"type": "string",
"description": "Filter by category"
},
"time_range": {
"type": "string",
"description": "Filter by time range (e.g., '1week', '1month')"
}
},
"required": ["query"]
}
});
}
}
tools
}
// 拡張ツール実行
pub async fn execute_tool(&mut self, tool_name: &str, arguments: &Value) -> Value {
match tool_name {
// 拡張機能
#[cfg(feature = "ai-analysis")]
"analyze_sentiment" => self.tool_analyze_sentiment(arguments).await,
#[cfg(feature = "ai-analysis")]
"extract_insights" => self.tool_extract_insights(arguments).await,
#[cfg(feature = "web-integration")]
"import_webpage" => self.tool_import_webpage(arguments).await,
// 拡張版の基本ツール (AI分析付き)
"create_memory" => self.tool_create_memory_extended(arguments).await,
"search_memories" => self.tool_search_memories_extended(arguments).await,
// 基本ツールにフォールバック
_ => self.base.execute_tool(tool_name, arguments).await,
}
}
// 拡張ツール実装
async fn tool_create_memory_extended(&mut self, arguments: &Value) -> Value {
let content = arguments["content"].as_str().unwrap_or("");
let analyze = arguments["analyze"].as_bool().unwrap_or(false);
let final_content = if analyze {
#[cfg(feature = "ai-analysis")]
{
format!("[AI分析] 感情: neutral, カテゴリ: general\n{}", content)
}
#[cfg(not(feature = "ai-analysis"))]
{
content.to_string()
}
} else {
content.to_string()
};
match self.base.memory_manager.create_memory(&final_content) {
Ok(id) => json!({
"success": true,
"id": id,
"message": if analyze { "Memory created with AI analysis" } else { "Memory created successfully" }
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
})
}
}
async fn tool_search_memories_extended(&mut self, arguments: &Value) -> Value {
let query = arguments["query"].as_str().unwrap_or("");
let semantic = arguments["semantic"].as_bool().unwrap_or(false);
let memories = if semantic {
#[cfg(feature = "semantic-search")]
{
// モックセマンティック検索
self.base.memory_manager.search_memories(query)
}
#[cfg(not(feature = "semantic-search"))]
{
self.base.memory_manager.search_memories(query)
}
} else {
self.base.memory_manager.search_memories(query)
};
json!({
"success": true,
"memories": memories.into_iter().map(|m| json!({
"id": m.id,
"content": m.content,
"interpreted_content": m.interpreted_content,
"priority_score": m.priority_score,
"user_context": m.user_context,
"created_at": m.created_at,
"updated_at": m.updated_at
})).collect::<Vec<_>>(),
"search_type": if semantic { "semantic" } else { "keyword" }
})
}
#[cfg(feature = "ai-analysis")]
async fn tool_analyze_sentiment(&mut self, _arguments: &Value) -> Value {
json!({
"success": true,
"analysis": {
"positive": 60,
"neutral": 30,
"negative": 10,
"dominant_sentiment": "positive"
},
"message": "Sentiment analysis completed"
})
}
#[cfg(feature = "ai-analysis")]
async fn tool_extract_insights(&mut self, _arguments: &Value) -> Value {
json!({
"success": true,
"insights": {
"most_frequent_topics": ["programming", "ai", "productivity"],
"learning_frequency": "5 times per week",
"growth_trend": "increasing",
"recommendations": ["Focus more on advanced topics", "Consider practical applications"]
},
"message": "Insights extracted successfully"
})
}
#[cfg(feature = "web-integration")]
async fn tool_import_webpage(&mut self, arguments: &Value) -> Value {
let url = arguments["url"].as_str().unwrap_or("");
match self.import_from_web(url).await {
Ok(content) => {
match self.base.memory_manager.create_memory(&content) {
Ok(id) => json!({
"success": true,
"id": id,
"message": format!("Webpage imported successfully from {}", url)
}),
Err(e) => json!({
"success": false,
"error": e.to_string()
})
}
}
Err(e) => json!({
"success": false,
"error": format!("Failed to import webpage: {}", e)
})
}
}
#[cfg(feature = "web-integration")]
async fn import_from_web(&self, url: &str) -> Result<String> {
let response = reqwest::get(url).await?;
let content = response.text().await?;
let document = scraper::Html::parse_document(&content);
let title_selector = scraper::Selector::parse("title").unwrap();
let body_selector = scraper::Selector::parse("p").unwrap();
let title = document.select(&title_selector)
.next()
.map(|el| el.inner_html())
.unwrap_or_else(|| "Untitled".to_string());
let paragraphs: Vec<String> = document.select(&body_selector)
.map(|el| el.inner_html())
.take(5)
.collect();
Ok(format!("# {}\nURL: {}\n\n{}", title, url, paragraphs.join("\n\n")))
}
}

365
src/tmp/game_formatter.rs Normal file
View File

@@ -0,0 +1,365 @@
use crate::memory::Memory;
use serde::{Deserialize, Serialize};
use chrono::Datelike;
/// メモリーのレア度
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MemoryRarity {
Common, // 0.0-0.4
Uncommon, // 0.4-0.6
Rare, // 0.6-0.8
Epic, // 0.8-0.9
Legendary, // 0.9-1.0
}
impl MemoryRarity {
pub fn from_score(score: f32) -> Self {
match score {
s if s >= 0.9 => MemoryRarity::Legendary,
s if s >= 0.8 => MemoryRarity::Epic,
s if s >= 0.6 => MemoryRarity::Rare,
s if s >= 0.4 => MemoryRarity::Uncommon,
_ => MemoryRarity::Common,
}
}
pub fn emoji(&self) -> &str {
match self {
MemoryRarity::Common => "",
MemoryRarity::Uncommon => "🟢",
MemoryRarity::Rare => "🔵",
MemoryRarity::Epic => "🟣",
MemoryRarity::Legendary => "🟡",
}
}
pub fn name(&self) -> &str {
match self {
MemoryRarity::Common => "COMMON",
MemoryRarity::Uncommon => "UNCOMMON",
MemoryRarity::Rare => "RARE",
MemoryRarity::Epic => "EPIC",
MemoryRarity::Legendary => "LEGENDARY",
}
}
pub fn xp_value(&self) -> u32 {
match self {
MemoryRarity::Common => 100,
MemoryRarity::Uncommon => 250,
MemoryRarity::Rare => 500,
MemoryRarity::Epic => 850,
MemoryRarity::Legendary => 1000,
}
}
}
/// 診断タイプ
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DiagnosisType {
Innovator, // 革新者(創造性高、実用性高)
Philosopher, // 哲学者(感情高、新規性高)
Pragmatist, // 実務家(実用性高、関連性高)
Visionary, // 夢想家(新規性高、感情高)
Analyst, // 分析家(全て平均的)
}
impl DiagnosisType {
/// スコアから診断タイプを推定(公開用)
pub fn from_memory(memory: &crate::memory::Memory) -> Self {
// スコア内訳を推定
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
Self::from_score_breakdown(emotional, relevance, novelty, utility)
}
pub fn from_score_breakdown(
emotional: f32,
relevance: f32,
novelty: f32,
utility: f32,
) -> Self {
if utility > 0.2 && novelty > 0.2 {
DiagnosisType::Innovator
} else if emotional > 0.2 && novelty > 0.2 {
DiagnosisType::Philosopher
} else if utility > 0.2 && relevance > 0.2 {
DiagnosisType::Pragmatist
} else if novelty > 0.2 && emotional > 0.18 {
DiagnosisType::Visionary
} else {
DiagnosisType::Analyst
}
}
pub fn emoji(&self) -> &str {
match self {
DiagnosisType::Innovator => "💡",
DiagnosisType::Philosopher => "🧠",
DiagnosisType::Pragmatist => "🎯",
DiagnosisType::Visionary => "",
DiagnosisType::Analyst => "📊",
}
}
pub fn name(&self) -> &str {
match self {
DiagnosisType::Innovator => "革新者",
DiagnosisType::Philosopher => "哲学者",
DiagnosisType::Pragmatist => "実務家",
DiagnosisType::Visionary => "夢想家",
DiagnosisType::Analyst => "分析家",
}
}
pub fn description(&self) -> &str {
match self {
DiagnosisType::Innovator => "創造的で実用的なアイデアを生み出す。常に新しい可能性を探求し、それを現実のものにする力を持つ。",
DiagnosisType::Philosopher => "深い思考と感情を大切にする。抽象的な概念や人生の意味について考えることを好む。",
DiagnosisType::Pragmatist => "現実的で効率的。具体的な問題解決に優れ、確実に結果を出す。",
DiagnosisType::Visionary => "大胆な夢と理想を追い求める。常識にとらわれず、未来の可能性を信じる。",
DiagnosisType::Analyst => "バランスの取れた思考。多角的な視点から物事を分析し、冷静に判断する。",
}
}
}
/// ゲーム風の結果フォーマッター
pub struct GameFormatter;
impl GameFormatter {
/// メモリー作成結果をゲーム風に表示
pub fn format_memory_result(memory: &Memory) -> String {
let rarity = MemoryRarity::from_score(memory.priority_score);
let xp = rarity.xp_value();
let score_percentage = (memory.priority_score * 100.0) as u32;
// スコア内訳を推定各項目最大0.25として)
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
let diagnosis = DiagnosisType::from_score_breakdown(
emotional,
relevance,
novelty,
utility,
);
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 🎲 メモリースコア判定 ║
╚══════════════════════════════════════════════════════════════╝
⚡ 分析完了! あなたの思考が記録されました
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 総合スコア
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{} {} {}点
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 詳細分析
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💓 感情的インパクト: {}
🔗 ユーザー関連性: {}
✨ 新規性・独自性: {}
⚙️ 実用性: {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎊 あなたのタイプ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{} 【{}】
{}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🏆 報酬
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💎 XP獲得: +{} XP
🎁 レア度: {} {}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💬 AI の解釈
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
{}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📤 この結果をシェアしよう!
#aigpt #メモリースコア #{}
"#,
rarity.emoji(),
rarity.name(),
score_percentage,
Self::format_bar(emotional, 0.25),
Self::format_bar(relevance, 0.25),
Self::format_bar(novelty, 0.25),
Self::format_bar(utility, 0.25),
diagnosis.emoji(),
diagnosis.name(),
diagnosis.description(),
xp,
rarity.emoji(),
rarity.name(),
memory.interpreted_content,
diagnosis.name(),
)
}
/// シェア用の短縮テキストを生成
pub fn format_shareable_text(memory: &Memory) -> String {
let rarity = MemoryRarity::from_score(memory.priority_score);
let score_percentage = (memory.priority_score * 100.0) as u32;
let emotional = (memory.priority_score * 0.25).min(0.25);
let relevance = (memory.priority_score * 0.25).min(0.25);
let novelty = (memory.priority_score * 0.25).min(0.25);
let utility = memory.priority_score - emotional - relevance - novelty;
let diagnosis = DiagnosisType::from_score_breakdown(
emotional,
relevance,
novelty,
utility,
);
format!(
r#"🎲 AIメモリースコア診断結果
{} {} {}点
{} 【{}】
{}
#aigpt #メモリースコア #AI診断"#,
rarity.emoji(),
rarity.name(),
score_percentage,
diagnosis.emoji(),
diagnosis.name(),
Self::truncate(&memory.content, 100),
)
}
/// ランキング表示
pub fn format_ranking(memories: &[&Memory], title: &str) -> String {
let mut result = format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 🏆 {} ║
╚══════════════════════════════════════════════════════════════╝
"#,
title
);
for (i, memory) in memories.iter().take(10).enumerate() {
let rank_emoji = match i {
0 => "🥇",
1 => "🥈",
2 => "🥉",
_ => " ",
};
let rarity = MemoryRarity::from_score(memory.priority_score);
let score = (memory.priority_score * 100.0) as u32;
result.push_str(&format!(
"{} {}{} {} {}点 - {}\n",
rank_emoji,
i + 1,
rarity.emoji(),
rarity.name(),
score,
Self::truncate(&memory.content, 40)
));
}
result.push_str("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
result
}
/// デイリーチャレンジ表示
pub fn format_daily_challenge() -> String {
// 今日の日付をシードにランダムなお題を生成
let challenges = vec![
"今日学んだことを記録しよう",
"新しいアイデアを思いついた?",
"感動したことを書き留めよう",
"目標を一つ設定しよう",
"誰かに感謝の気持ちを伝えよう",
];
let today = chrono::Utc::now().ordinal();
let challenge = challenges[today as usize % challenges.len()];
format!(
r#"
╔══════════════════════════════════════════════════════════════╗
║ 📅 今日のチャレンジ ║
╚══════════════════════════════════════════════════════════════╝
✨ {}
🎁 報酬: +200 XP
💎 完了すると特別なバッジが獲得できます!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"#,
challenge
)
}
/// プログレスバーを生成
fn format_bar(value: f32, max: f32) -> String {
let percentage = (value / max * 100.0) as u32;
let filled = (percentage / 10) as usize;
let empty = 10 - filled;
format!(
"[{}{}] {}%",
"".repeat(filled),
"".repeat(empty),
percentage
)
}
/// テキストを切り詰め
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_rarity_from_score() {
assert!(matches!(MemoryRarity::from_score(0.95), MemoryRarity::Legendary));
assert!(matches!(MemoryRarity::from_score(0.85), MemoryRarity::Epic));
assert!(matches!(MemoryRarity::from_score(0.7), MemoryRarity::Rare));
assert!(matches!(MemoryRarity::from_score(0.5), MemoryRarity::Uncommon));
assert!(matches!(MemoryRarity::from_score(0.3), MemoryRarity::Common));
}
#[test]
fn test_diagnosis_type() {
let diagnosis = DiagnosisType::from_score_breakdown(0.1, 0.1, 0.22, 0.22);
assert!(matches!(diagnosis, DiagnosisType::Innovator));
}
#[test]
fn test_format_bar() {
let bar = GameFormatter::format_bar(0.15, 0.25);
assert!(bar.contains("60%"));
}
}

374
src/tmp/memory.rs Normal file
View File

@@ -0,0 +1,374 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use uuid::Uuid;
use crate::ai_interpreter::AIInterpreter;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub id: String,
pub content: String,
#[serde(default = "default_interpreted_content")]
pub interpreted_content: String, // AI解釈後のコンテンツ
#[serde(default = "default_priority_score")]
pub priority_score: f32, // 心理判定スコア (0.0-1.0)
#[serde(default)]
pub user_context: Option<String>, // ユーザー固有性
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
fn default_interpreted_content() -> String {
String::new()
}
fn default_priority_score() -> f32 {
0.5
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: String,
pub title: String,
pub created_at: DateTime<Utc>,
pub message_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTNode {
id: String,
children: Vec<String>,
parent: Option<String>,
message: Option<ChatGPTMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTMessage {
id: String,
author: ChatGPTAuthor,
content: ChatGPTContent,
create_time: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTAuthor {
role: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum ChatGPTContent {
Text {
content_type: String,
parts: Vec<String>,
},
Other(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ChatGPTConversation {
#[serde(default)]
id: String,
#[serde(alias = "conversation_id")]
conversation_id: Option<String>,
title: String,
create_time: f64,
mapping: HashMap<String, ChatGPTNode>,
}
pub struct MemoryManager {
memories: HashMap<String, Memory>,
conversations: HashMap<String, Conversation>,
data_file: PathBuf,
max_memories: usize, // 最大記憶数
#[allow(dead_code)]
min_priority_score: f32, // 最小優先度スコア (将来の機能で使用予定)
ai_interpreter: AIInterpreter, // AI解釈エンジン
}
impl MemoryManager {
pub async fn new() -> Result<Self> {
let data_dir = dirs::config_dir()
.context("Could not find config directory")?
.join("syui")
.join("ai")
.join("gpt");
std::fs::create_dir_all(&data_dir)?;
let data_file = data_dir.join("memory.json");
let (memories, conversations) = if data_file.exists() {
Self::load_data(&data_file)?
} else {
(HashMap::new(), HashMap::new())
};
Ok(MemoryManager {
memories,
conversations,
data_file,
max_memories: 100, // デフォルト: 100件
min_priority_score: 0.3, // デフォルト: 0.3以上
ai_interpreter: AIInterpreter::new(),
})
}
pub fn create_memory(&mut self, content: &str) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content: content.to_string(), // 後でAI解釈を実装
priority_score: 0.5, // 後で心理判定を実装
user_context: None,
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
/// AI解釈と心理判定を使った記憶作成後方互換性のため残す
pub async fn create_memory_with_ai(
&mut self,
content: &str,
user_context: Option<&str>,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
// AI解釈と心理判定を実行
let (interpreted_content, priority_score) = self
.ai_interpreter
.analyze(content, user_context)
.await?;
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content,
priority_score,
user_context: user_context.map(|s| s.to_string()),
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
/// Claude Code から解釈とスコアを受け取ってメモリを作成
pub fn create_memory_with_interpretation(
&mut self,
content: &str,
interpreted_content: &str,
priority_score: f32,
user_context: Option<&str>,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let memory = Memory {
id: id.clone(),
content: content.to_string(),
interpreted_content: interpreted_content.to_string(),
priority_score: priority_score.max(0.0).min(1.0), // 0.0-1.0 に制限
user_context: user_context.map(|s| s.to_string()),
created_at: now,
updated_at: now,
};
self.memories.insert(id.clone(), memory);
// 容量制限チェック
self.prune_memories_if_needed()?;
self.save_data()?;
Ok(id)
}
pub fn update_memory(&mut self, id: &str, content: &str) -> Result<()> {
if let Some(memory) = self.memories.get_mut(id) {
memory.content = content.to_string();
memory.updated_at = Utc::now();
self.save_data()?;
Ok(())
} else {
Err(anyhow::anyhow!("Memory not found: {}", id))
}
}
pub fn delete_memory(&mut self, id: &str) -> Result<()> {
if self.memories.remove(id).is_some() {
self.save_data()?;
Ok(())
} else {
Err(anyhow::anyhow!("Memory not found: {}", id))
}
}
// 容量制限: 優先度が低いものから削除
fn prune_memories_if_needed(&mut self) -> Result<()> {
if self.memories.len() <= self.max_memories {
return Ok(());
}
// 優先度でソートして、低いものから削除
let mut sorted_memories: Vec<_> = self.memories.iter()
.map(|(id, mem)| (id.clone(), mem.priority_score))
.collect();
sorted_memories.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let to_remove = self.memories.len() - self.max_memories;
for (id, _) in sorted_memories.iter().take(to_remove) {
self.memories.remove(id);
}
Ok(())
}
// 優先度順に記憶を取得
pub fn get_memories_by_priority(&self) -> Vec<&Memory> {
let mut memories: Vec<_> = self.memories.values().collect();
memories.sort_by(|a, b| b.priority_score.partial_cmp(&a.priority_score).unwrap_or(std::cmp::Ordering::Equal));
memories
}
pub fn search_memories(&self, query: &str) -> Vec<&Memory> {
let query_lower = query.to_lowercase();
let mut results: Vec<_> = self.memories
.values()
.filter(|memory| memory.content.to_lowercase().contains(&query_lower))
.collect();
results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
results
}
pub fn list_conversations(&self) -> Vec<&Conversation> {
let mut conversations: Vec<_> = self.conversations.values().collect();
conversations.sort_by(|a, b| b.created_at.cmp(&a.created_at));
conversations
}
#[allow(dead_code)]
pub async fn import_chatgpt_conversations(&mut self, file_path: &PathBuf) -> Result<()> {
let content = std::fs::read_to_string(file_path)
.context("Failed to read conversations file")?;
let chatgpt_conversations: Vec<ChatGPTConversation> = serde_json::from_str(&content)
.context("Failed to parse ChatGPT conversations")?;
let mut imported_memories = 0;
let mut imported_conversations = 0;
for conv in chatgpt_conversations {
// Get the actual conversation ID
let conv_id = if !conv.id.is_empty() {
conv.id.clone()
} else if let Some(cid) = conv.conversation_id {
cid
} else {
Uuid::new_v4().to_string()
};
// Add conversation
let conversation = Conversation {
id: conv_id.clone(),
title: conv.title.clone(),
created_at: DateTime::from_timestamp(conv.create_time as i64, 0)
.unwrap_or_else(Utc::now),
message_count: conv.mapping.len() as u32,
};
self.conversations.insert(conv_id.clone(), conversation);
imported_conversations += 1;
// Extract memories from messages
for (_, node) in conv.mapping {
if let Some(message) = node.message {
if let ChatGPTContent::Text { parts, .. } = message.content {
for part in parts {
if !part.trim().is_empty() && part.len() > 10 {
let memory_content = format!("[{}] {}", conv.title, part);
self.create_memory(&memory_content)?;
imported_memories += 1;
}
}
}
}
}
}
println!("Imported {} conversations and {} memories",
imported_conversations, imported_memories);
Ok(())
}
fn load_data(file_path: &PathBuf) -> Result<(HashMap<String, Memory>, HashMap<String, Conversation>)> {
let content = std::fs::read_to_string(file_path)
.context("Failed to read data file")?;
#[derive(Deserialize)]
struct Data {
memories: HashMap<String, Memory>,
conversations: HashMap<String, Conversation>,
}
let data: Data = serde_json::from_str(&content)
.context("Failed to parse data file")?;
Ok((data.memories, data.conversations))
}
// Getter: 単一メモリ取得
pub fn get_memory(&self, id: &str) -> Option<&Memory> {
self.memories.get(id)
}
// Getter: 全メモリ取得
pub fn get_all_memories(&self) -> Vec<&Memory> {
self.memories.values().collect()
}
fn save_data(&self) -> Result<()> {
#[derive(Serialize)]
struct Data<'a> {
memories: &'a HashMap<String, Memory>,
conversations: &'a HashMap<String, Conversation>,
}
let data = Data {
memories: &self.memories,
conversations: &self.conversations,
};
let content = serde_json::to_string_pretty(&data)
.context("Failed to serialize data")?;
std::fs::write(&self.data_file, content)
.context("Failed to write data file")?;
Ok(())
}
}