From a401e91bb42eaf22ada6584a11927557a3ff1210 Mon Sep 17 00:00:00 2001 From: syui Date: Fri, 27 Feb 2026 12:01:07 +0900 Subject: [PATCH] init --- .gitignore | 24 ++ CLAUDE.md | 0 Cargo.toml | 37 ++ README.md | 3 + claude.md | 57 ++++ docs/DOCS.md | 66 ++++ src/core/analysis.rs | 161 +++++++++ src/core/error.rs | 27 ++ src/core/memory.rs | 181 ++++++++++ src/core/mod.rs | 13 + src/core/profile.rs | 275 +++++++++++++++ src/core/relationship.rs | 317 +++++++++++++++++ src/core/store.rs | 693 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/main.rs | 141 ++++++++ src/mcp/base.rs | 648 +++++++++++++++++++++++++++++++++++ src/mcp/mod.rs | 3 + src/tmp/ai_interpreter.rs | 36 ++ src/tmp/companion.rs | 433 ++++++++++++++++++++++++ src/tmp/extended.rs | 296 ++++++++++++++++ src/tmp/game_formatter.rs | 365 ++++++++++++++++++++ src/tmp/memory.rs | 374 ++++++++++++++++++++ 22 files changed, 4152 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 claude.md create mode 100644 docs/DOCS.md create mode 100644 src/core/analysis.rs create mode 100644 src/core/error.rs create mode 100644 src/core/memory.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/profile.rs create mode 100644 src/core/relationship.rs create mode 100644 src/core/store.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/mcp/base.rs create mode 100644 src/mcp/mod.rs create mode 100644 src/tmp/ai_interpreter.rs create mode 100644 src/tmp/companion.rs create mode 100644 src/tmp/extended.rs create mode 100644 src/tmp/game_formatter.rs create mode 100644 src/tmp/memory.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66f9a9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Rust +target/ +Cargo.lock + +# Database files +*.db +*.db-shm +*.db-wal + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +json +gpt +.claude diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cd94099 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "aigpt" +version = "0.3.0" +edition = "2021" +authors = ["syui"] +description = "AI memory system with personality analysis and relationship inference - Layers 1-4 Complete" + +[lib] +name = "aigpt" +path = "src/lib.rs" + +[[bin]] +name = "aigpt" +path = "src/main.rs" + +[dependencies] +# CLI and async +clap = { version = "4.5", features = ["derive"] } +tokio = { version = "1.40", features = ["rt", "rt-multi-thread", "macros", "io-std"] } + +# Database +rusqlite = { version = "0.30", features = ["bundled"] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Date/time and ULID +chrono = { version = "0.4", features = ["serde"] } +ulid = "1.1" + +# Error handling +thiserror = "1.0" +anyhow = "1.0" + +# Utilities +dirs = "5.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c81b955 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# aigpt + +MCP server for AI memory management reads/writes core.md and memory.md with 4 tools (read_core, read_memory, save_memory, compress). diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..ea976b5 --- /dev/null +++ b/claude.md @@ -0,0 +1,57 @@ +# aigpt + +## これは何か + +アイの記憶を管理するMCPサーバー。core.mdとmemory.mdの読み書きだけを行う。 + +## 設計原則 + +- AIが判断し、ツールは記録する +- ファイルI/Oのみ。データベースは使わない +- MCPツールは4つだけ: read_core, read_memory, save_memory, compress +- シンプルで壊れない。ずっと使える + +## アーキテクチャ + +``` +aigpt +├── mcp/ +│ └── server.rs ← JSON-RPC over stdio +├── core/ +│ ├── reader.rs ← core.md, memory.mdの読み込み +│ └── writer.rs ← memory.mdの書き込み +└── main.rs ← CLI + MCPサーバー起動 +``` + +## MCPツール定義 + +### read_core +- 引数: なし +- 戻り値: core.mdの内容 (string) + +### read_memory +- 引数: なし +- 戻り値: memory.mdの内容 (string) + +### save_memory +- 引数: content (string) +- 動作: memory.mdを上書き + +### compress +- 引数: conversation (string) +- 動作: 現在のmemory.mdを読み、会話内容と合わせて圧縮し、memory.mdに書く +- 注: 圧縮の判断はAI側が行う。ツールは受け取った内容をそのまま書くだけ + +## データ + +``` +~/.config/aigpt/ +├── core.md ← read only (このツールからは書かない) +└── memory.md ← read/write +``` + +## 開発方針 + +1. まずMCPサーバーで4ツールを実装 +2. CLIでも同じ操作ができるようにする +3. それ以上は作らない diff --git a/docs/DOCS.md b/docs/DOCS.md new file mode 100644 index 0000000..09aa560 --- /dev/null +++ b/docs/DOCS.md @@ -0,0 +1,66 @@ +# aigpt docs + +## Overview + +MCP server for AI memory. Reads/writes core.md and memory.md. Nothing more. + +## Design + +- AI decides, tool records +- File I/O only, no database +- 4 MCP tools: read_core, read_memory, save_memory, compress +- Simple, unbreakable, long-lasting + +## MCP Tools + +| Tool | Args | Description | +|------|------|-------------| +| read_core | none | Returns core.md content | +| read_memory | none | Returns memory.md content | +| save_memory | content: string | Overwrites memory.md | +| compress | conversation: string | Reads memory.md + conversation, writes compressed result to memory.md | + +compress note: AI decides what to keep/discard. Tool just writes. + +## Data + +``` +~/.config/aigpt/ +├── core.md ← read only (identity, settings) +└── memory.md ← read/write (memories, grows over time) +``` + +## Architecture + +``` +src/ +├── mcp/server.rs ← JSON-RPC over stdio +├── core/reader.rs ← read core.md, memory.md +├── core/writer.rs ← write memory.md +└── main.rs ← CLI + MCP server +``` + +## Compression Rules + +When compress is called, AI should: +- Keep facts and decisions +- Discard procedures and processes +- Resolve contradictions (keep newer) +- Don't duplicate core.md content + +## Usage + +```bash +aigpt serve # start MCP server +aigpt read-core # CLI: read core.md +aigpt read-memory # CLI: read memory.md +aigpt save-memory "content" # CLI: write memory.md +``` + +## Tech + +- Rust, MCP (JSON-RPC over stdio), file I/O only + +## History + +Previous versions (v0.1-v0.3) had multi-layer architecture with SQLite, Big Five personality analysis, relationship inference, gamification, and companion systems. Rewritten to current simple design. Old docs preserved in docs/archive/. diff --git a/src/core/analysis.rs b/src/core/analysis.rs new file mode 100644 index 0000000..951f8d4 --- /dev/null +++ b/src/core/analysis.rs @@ -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, +} + +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 + } +} diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..2f86938 --- /dev/null +++ b/src/core/error.rs @@ -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 = std::result::Result; diff --git a/src/core/memory.rs b/src/core/memory.rs new file mode 100644 index 0000000..883d978 --- /dev/null +++ b/src/core/memory.rs @@ -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, + + /// 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, + + /// Related entities (people, places, things) involved in this memory (Layer 4) + #[serde(skip_serializing_if = "Option::is_none")] + pub related_entities: Option>, + + /// When this memory was created + pub created_at: DateTime, + + /// When this memory was last updated + pub updated_at: DateTime, +} + +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, + priority_score: Option, + ) -> 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, + priority_score: Option, + related_entities: Option>, + ) -> 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) { + 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)); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..6d134b2 --- /dev/null +++ b/src/core/mod.rs @@ -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; diff --git a/src/core/profile.rs b/src/core/profile.rs new file mode 100644 index 0000000..f84ceaf --- /dev/null +++ b/src/core/profile.rs @@ -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, + + /// Core interests (most frequent topics from memories) + pub core_interests: Vec, + + /// Core values (extracted from high-priority memories) + pub core_values: Vec, + + /// Key memory IDs (top priority memories as evidence) + pub key_memory_ids: Vec, + + /// Data quality score (0.0-1.0 based on data volume) + pub data_quality: f32, + + /// Last update timestamp + pub last_updated: DateTime, +} + +#[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 { + // 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 { + // 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) -> Vec { + 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 { + let mut word_freq: HashMap = 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 { + // 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 = 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 { + 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) -> 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 { + // 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 { + 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")); + } +} diff --git a/src/core/relationship.rs b/src/core/relationship.rs new file mode 100644 index 0000000..3083f71 --- /dev/null +++ b/src/core/relationship.rs @@ -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, +} + +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> { + // 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 = 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 { + // 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); + } +} diff --git a/src/core/store.rs b/src/core/store.rs new file mode 100644 index 0000000..537b10c --- /dev/null +++ b/src/core/store.rs @@ -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 { + // 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 { + 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 { + 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 = 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> { + 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 = 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::, _>>()?; + + Ok(memories) + } + + /// Search memories by content or AI interpretation (case-insensitive) + pub fn search(&self, query: &str) -> Result> { + 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 = 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::, _>>()?; + + Ok(memories) + } + + /// Count total memories + pub fn count(&self) -> Result { + 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> { + 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> { + 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::, _>>()?; + + 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> { + 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 { + // 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> { + 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>> { + 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 = + 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); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e27b615 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod core; +pub mod mcp; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..374446b --- /dev/null +++ b/src/main.rs @@ -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(()) +} \ No newline at end of file diff --git a/src/mcp/base.rs b/src/mcp/base.rs new file mode 100644 index 0000000..180db77 --- /dev/null +++ b/src/mcp/base.rs @@ -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 { + 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::(&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 { + 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::>() + }), + 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::>() + }), + 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::>() + }) + } + 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" + } + }) + } +} diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs new file mode 100644 index 0000000..4ee51cc --- /dev/null +++ b/src/mcp/mod.rs @@ -0,0 +1,3 @@ +pub mod base; + +pub use base::BaseMCPServer; \ No newline at end of file diff --git a/src/tmp/ai_interpreter.rs b/src/tmp/ai_interpreter.rs new file mode 100644 index 0000000..87c52f6 --- /dev/null +++ b/src/tmp/ai_interpreter.rs @@ -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 { + Ok(content.to_string()) + } + + /// デフォルトスコアを返す(Claude Code が実際のスコアを決定) + pub async fn calculate_priority_score(&self, _content: &str, _user_context: Option<&str>) -> Result { + 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() + } +} diff --git a/src/tmp/companion.rs b/src/tmp/companion.rs new file mode 100644 index 0000000..b669e2d --- /dev/null +++ b/src/tmp/companion.rs @@ -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, + pub shared_memories: Vec, // 共有された記憶の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 { + // 好感度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, +} + +/// スペシャルイベント +#[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); + } +} diff --git a/src/tmp/extended.rs b/src/tmp/extended.rs new file mode 100644 index 0000000..eb9d4d0 --- /dev/null +++ b/src/tmp/extended.rs @@ -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 { + 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 { + #[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::>(), + "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 { + 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 = document.select(&body_selector) + .map(|el| el.inner_html()) + .take(5) + .collect(); + + Ok(format!("# {}\nURL: {}\n\n{}", title, url, paragraphs.join("\n\n"))) + } +} \ No newline at end of file diff --git a/src/tmp/game_formatter.rs b/src/tmp/game_formatter.rs new file mode 100644 index 0000000..9391949 --- /dev/null +++ b/src/tmp/game_formatter.rs @@ -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%")); + } +} diff --git a/src/tmp/memory.rs b/src/tmp/memory.rs new file mode 100644 index 0000000..532c3fc --- /dev/null +++ b/src/tmp/memory.rs @@ -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, // ユーザー固有性 + pub created_at: DateTime, + pub updated_at: DateTime, +} + +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, + pub message_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTNode { + id: String, + children: Vec, + parent: Option, + message: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTMessage { + id: String, + author: ChatGPTAuthor, + content: ChatGPTContent, + create_time: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTAuthor { + role: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum ChatGPTContent { + Text { + content_type: String, + parts: Vec, + }, + Other(serde_json::Value), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ChatGPTConversation { + #[serde(default)] + id: String, + #[serde(alias = "conversation_id")] + conversation_id: Option, + title: String, + create_time: f64, + mapping: HashMap, +} + +pub struct MemoryManager { + memories: HashMap, + conversations: HashMap, + data_file: PathBuf, + max_memories: usize, // 最大記憶数 + #[allow(dead_code)] + min_priority_score: f32, // 最小優先度スコア (将来の機能で使用予定) + ai_interpreter: AIInterpreter, // AI解釈エンジン +} + +impl MemoryManager { + pub async fn new() -> Result { + 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 { + 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 { + 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 { + 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 = 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, HashMap)> { + let content = std::fs::read_to_string(file_path) + .context("Failed to read data file")?; + + #[derive(Deserialize)] + struct Data { + memories: HashMap, + conversations: HashMap, + } + + 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, + conversations: &'a HashMap, + } + + 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(()) + } +}