init
This commit is contained in:
161
src/core/analysis.rs
Normal file
161
src/core/analysis.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// User personality analysis based on Big Five model (OCEAN)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserAnalysis {
|
||||
/// Unique identifier using ULID
|
||||
pub id: String,
|
||||
|
||||
/// Openness to Experience (0.0-1.0)
|
||||
/// Curiosity, imagination, willingness to try new things
|
||||
pub openness: f32,
|
||||
|
||||
/// Conscientiousness (0.0-1.0)
|
||||
/// Organization, responsibility, self-discipline
|
||||
pub conscientiousness: f32,
|
||||
|
||||
/// Extraversion (0.0-1.0)
|
||||
/// Sociability, assertiveness, energy level
|
||||
pub extraversion: f32,
|
||||
|
||||
/// Agreeableness (0.0-1.0)
|
||||
/// Compassion, cooperation, trust
|
||||
pub agreeableness: f32,
|
||||
|
||||
/// Neuroticism (0.0-1.0)
|
||||
/// Emotional stability, anxiety, mood swings
|
||||
pub neuroticism: f32,
|
||||
|
||||
/// AI-generated summary of the personality analysis
|
||||
pub summary: String,
|
||||
|
||||
/// When this analysis was performed
|
||||
pub analyzed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl UserAnalysis {
|
||||
/// Create a new personality analysis
|
||||
pub fn new(
|
||||
openness: f32,
|
||||
conscientiousness: f32,
|
||||
extraversion: f32,
|
||||
agreeableness: f32,
|
||||
neuroticism: f32,
|
||||
summary: String,
|
||||
) -> Self {
|
||||
let id = Ulid::new().to_string();
|
||||
let analyzed_at = Utc::now();
|
||||
|
||||
Self {
|
||||
id,
|
||||
openness: openness.clamp(0.0, 1.0),
|
||||
conscientiousness: conscientiousness.clamp(0.0, 1.0),
|
||||
extraversion: extraversion.clamp(0.0, 1.0),
|
||||
agreeableness: agreeableness.clamp(0.0, 1.0),
|
||||
neuroticism: neuroticism.clamp(0.0, 1.0),
|
||||
summary,
|
||||
analyzed_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the dominant trait (highest score)
|
||||
pub fn dominant_trait(&self) -> &str {
|
||||
let scores = [
|
||||
(self.openness, "Openness"),
|
||||
(self.conscientiousness, "Conscientiousness"),
|
||||
(self.extraversion, "Extraversion"),
|
||||
(self.agreeableness, "Agreeableness"),
|
||||
(self.neuroticism, "Neuroticism"),
|
||||
];
|
||||
|
||||
scores
|
||||
.iter()
|
||||
.max_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
|
||||
.map(|(_, name)| *name)
|
||||
.unwrap_or("Unknown")
|
||||
}
|
||||
|
||||
/// Check if a trait is high (>= 0.6)
|
||||
pub fn is_high(&self, trait_name: &str) -> bool {
|
||||
let score = match trait_name.to_lowercase().as_str() {
|
||||
"openness" | "o" => self.openness,
|
||||
"conscientiousness" | "c" => self.conscientiousness,
|
||||
"extraversion" | "e" => self.extraversion,
|
||||
"agreeableness" | "a" => self.agreeableness,
|
||||
"neuroticism" | "n" => self.neuroticism,
|
||||
_ => return false,
|
||||
};
|
||||
score >= 0.6
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_analysis() {
|
||||
let analysis = UserAnalysis::new(
|
||||
0.8,
|
||||
0.7,
|
||||
0.4,
|
||||
0.6,
|
||||
0.3,
|
||||
"Test summary".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(analysis.openness, 0.8);
|
||||
assert_eq!(analysis.conscientiousness, 0.7);
|
||||
assert_eq!(analysis.extraversion, 0.4);
|
||||
assert_eq!(analysis.agreeableness, 0.6);
|
||||
assert_eq!(analysis.neuroticism, 0.3);
|
||||
assert!(!analysis.id.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_score_clamping() {
|
||||
let analysis = UserAnalysis::new(
|
||||
1.5, // Should clamp to 1.0
|
||||
-0.2, // Should clamp to 0.0
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
"Test".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(analysis.openness, 1.0);
|
||||
assert_eq!(analysis.conscientiousness, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dominant_trait() {
|
||||
let analysis = UserAnalysis::new(
|
||||
0.9, // Highest
|
||||
0.5,
|
||||
0.4,
|
||||
0.6,
|
||||
0.3,
|
||||
"Test".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(analysis.dominant_trait(), "Openness");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_high() {
|
||||
let analysis = UserAnalysis::new(
|
||||
0.8, // High
|
||||
0.4, // Low
|
||||
0.6, // Threshold
|
||||
0.5,
|
||||
0.3,
|
||||
"Test".to_string(),
|
||||
);
|
||||
|
||||
assert!(analysis.is_high("openness"));
|
||||
assert!(!analysis.is_high("conscientiousness"));
|
||||
assert!(analysis.is_high("extraversion")); // 0.6 is high
|
||||
}
|
||||
}
|
||||
27
src/core/error.rs
Normal file
27
src/core/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MemoryError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("Memory not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Invalid ULID: {0}")]
|
||||
InvalidId(String),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, MemoryError>;
|
||||
181
src/core/memory.rs
Normal file
181
src/core/memory.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// Represents a single memory entry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Memory {
|
||||
/// Unique identifier using ULID (time-sortable)
|
||||
pub id: String,
|
||||
|
||||
/// The actual content of the memory
|
||||
pub content: String,
|
||||
|
||||
/// AI's creative interpretation of the content (Layer 2)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_interpretation: Option<String>,
|
||||
|
||||
/// Priority score evaluated by AI: 0.0 (low) to 1.0 (high) (Layer 2)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority_score: Option<f32>,
|
||||
|
||||
/// Related entities (people, places, things) involved in this memory (Layer 4)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub related_entities: Option<Vec<String>>,
|
||||
|
||||
/// When this memory was created
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
/// When this memory was last updated
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
/// Create a new memory with generated ULID (Layer 1)
|
||||
pub fn new(content: String) -> Self {
|
||||
let now = Utc::now();
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
Self {
|
||||
id,
|
||||
content,
|
||||
ai_interpretation: None,
|
||||
priority_score: None,
|
||||
related_entities: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new AI-interpreted memory (Layer 2)
|
||||
pub fn new_ai(
|
||||
content: String,
|
||||
ai_interpretation: Option<String>,
|
||||
priority_score: Option<f32>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
Self {
|
||||
id,
|
||||
content,
|
||||
ai_interpretation,
|
||||
priority_score,
|
||||
related_entities: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new memory with related entities (Layer 4)
|
||||
pub fn new_with_entities(
|
||||
content: String,
|
||||
ai_interpretation: Option<String>,
|
||||
priority_score: Option<f32>,
|
||||
related_entities: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
Self {
|
||||
id,
|
||||
content,
|
||||
ai_interpretation,
|
||||
priority_score,
|
||||
related_entities,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the content of this memory
|
||||
pub fn update_content(&mut self, content: String) {
|
||||
self.content = content;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Set or update AI interpretation
|
||||
pub fn set_ai_interpretation(&mut self, interpretation: String) {
|
||||
self.ai_interpretation = Some(interpretation);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Set or update priority score
|
||||
pub fn set_priority_score(&mut self, score: f32) {
|
||||
self.priority_score = Some(score.clamp(0.0, 1.0));
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Set or update related entities
|
||||
pub fn set_related_entities(&mut self, entities: Vec<String>) {
|
||||
self.related_entities = Some(entities);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Check if this memory is related to a specific entity
|
||||
pub fn has_entity(&self, entity_id: &str) -> bool {
|
||||
self.related_entities
|
||||
.as_ref()
|
||||
.map(|entities| entities.iter().any(|e| e == entity_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_memory() {
|
||||
let memory = Memory::new("Test content".to_string());
|
||||
assert_eq!(memory.content, "Test content");
|
||||
assert!(!memory.id.is_empty());
|
||||
assert!(memory.ai_interpretation.is_none());
|
||||
assert!(memory.priority_score.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_ai_memory() {
|
||||
let memory = Memory::new_ai(
|
||||
"Test content".to_string(),
|
||||
Some("AI interpretation".to_string()),
|
||||
Some(0.75),
|
||||
);
|
||||
assert_eq!(memory.content, "Test content");
|
||||
assert_eq!(memory.ai_interpretation, Some("AI interpretation".to_string()));
|
||||
assert_eq!(memory.priority_score, Some(0.75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_memory() {
|
||||
let mut memory = Memory::new("Original".to_string());
|
||||
let original_time = memory.updated_at;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
memory.update_content("Updated".to_string());
|
||||
|
||||
assert_eq!(memory.content, "Updated");
|
||||
assert!(memory.updated_at > original_time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_ai_interpretation() {
|
||||
let mut memory = Memory::new("Test".to_string());
|
||||
memory.set_ai_interpretation("Interpretation".to_string());
|
||||
assert_eq!(memory.ai_interpretation, Some("Interpretation".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_priority_score() {
|
||||
let mut memory = Memory::new("Test".to_string());
|
||||
memory.set_priority_score(0.8);
|
||||
assert_eq!(memory.priority_score, Some(0.8));
|
||||
|
||||
// Test clamping
|
||||
memory.set_priority_score(1.5);
|
||||
assert_eq!(memory.priority_score, Some(1.0));
|
||||
|
||||
memory.set_priority_score(-0.5);
|
||||
assert_eq!(memory.priority_score, Some(0.0));
|
||||
}
|
||||
}
|
||||
13
src/core/mod.rs
Normal file
13
src/core/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod analysis;
|
||||
pub mod error;
|
||||
pub mod memory;
|
||||
pub mod profile;
|
||||
pub mod relationship;
|
||||
pub mod store;
|
||||
|
||||
pub use analysis::UserAnalysis;
|
||||
pub use error::{MemoryError, Result};
|
||||
pub use memory::Memory;
|
||||
pub use profile::{UserProfile, TraitScore};
|
||||
pub use relationship::{RelationshipInference, infer_all_relationships, get_relationship};
|
||||
pub use store::MemoryStore;
|
||||
275
src/core/profile.rs
Normal file
275
src/core/profile.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::core::{MemoryStore, UserAnalysis};
|
||||
use crate::core::error::Result;
|
||||
|
||||
/// Integrated user profile - the essence of Layer 1-3 data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserProfile {
|
||||
/// Dominant personality traits (top 2-3 from Big Five)
|
||||
pub dominant_traits: Vec<TraitScore>,
|
||||
|
||||
/// Core interests (most frequent topics from memories)
|
||||
pub core_interests: Vec<String>,
|
||||
|
||||
/// Core values (extracted from high-priority memories)
|
||||
pub core_values: Vec<String>,
|
||||
|
||||
/// Key memory IDs (top priority memories as evidence)
|
||||
pub key_memory_ids: Vec<String>,
|
||||
|
||||
/// Data quality score (0.0-1.0 based on data volume)
|
||||
pub data_quality: f32,
|
||||
|
||||
/// Last update timestamp
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TraitScore {
|
||||
pub name: String,
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
impl UserProfile {
|
||||
/// Generate integrated profile from Layer 1-3 data
|
||||
pub fn generate(store: &MemoryStore) -> Result<Self> {
|
||||
// Get latest personality analysis (Layer 3)
|
||||
let personality = store.get_latest_analysis()?;
|
||||
|
||||
// Get all memories (Layer 1-2)
|
||||
let memories = store.list()?;
|
||||
|
||||
// Extract dominant traits from Big Five
|
||||
let dominant_traits = extract_dominant_traits(&personality);
|
||||
|
||||
// Extract core interests from memory content
|
||||
let core_interests = extract_core_interests(&memories);
|
||||
|
||||
// Extract core values from high-priority memories
|
||||
let core_values = extract_core_values(&memories);
|
||||
|
||||
// Get top priority memory IDs
|
||||
let key_memory_ids = extract_key_memories(&memories);
|
||||
|
||||
// Calculate data quality
|
||||
let data_quality = calculate_data_quality(&memories, &personality);
|
||||
|
||||
Ok(UserProfile {
|
||||
dominant_traits,
|
||||
core_interests,
|
||||
core_values,
|
||||
key_memory_ids,
|
||||
data_quality,
|
||||
last_updated: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if profile needs update
|
||||
pub fn needs_update(&self, store: &MemoryStore) -> Result<bool> {
|
||||
// Update if 7+ days old
|
||||
let days_old = (Utc::now() - self.last_updated).num_days();
|
||||
if days_old >= 7 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Update if 10+ new memories since last update
|
||||
let memory_count = store.count()?;
|
||||
let expected_count = self.key_memory_ids.len() * 2; // Rough estimate
|
||||
if memory_count > expected_count + 10 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Update if new personality analysis exists
|
||||
if let Some(latest) = store.get_latest_analysis()? {
|
||||
if latest.analyzed_at > self.last_updated {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract top 2-3 personality traits from Big Five
|
||||
fn extract_dominant_traits(analysis: &Option<UserAnalysis>) -> Vec<TraitScore> {
|
||||
if analysis.is_none() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let analysis = analysis.as_ref().unwrap();
|
||||
|
||||
let mut traits = vec![
|
||||
TraitScore { name: "openness".to_string(), score: analysis.openness },
|
||||
TraitScore { name: "conscientiousness".to_string(), score: analysis.conscientiousness },
|
||||
TraitScore { name: "extraversion".to_string(), score: analysis.extraversion },
|
||||
TraitScore { name: "agreeableness".to_string(), score: analysis.agreeableness },
|
||||
TraitScore { name: "neuroticism".to_string(), score: analysis.neuroticism },
|
||||
];
|
||||
|
||||
// Sort by score descending
|
||||
traits.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
|
||||
|
||||
// Return top 3
|
||||
traits.into_iter().take(3).collect()
|
||||
}
|
||||
|
||||
/// Extract core interests from memory content (frequency analysis)
|
||||
fn extract_core_interests(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||
let mut word_freq: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for memory in memories {
|
||||
// Extract keywords from content
|
||||
let words = extract_keywords(&memory.content);
|
||||
for word in words {
|
||||
*word_freq.entry(word).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// Also consider AI interpretation if available
|
||||
if let Some(ref interpretation) = memory.ai_interpretation {
|
||||
let words = extract_keywords(interpretation);
|
||||
for word in words {
|
||||
*word_freq.entry(word).or_insert(0) += 2; // Weight interpretation higher
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by frequency and take top 5
|
||||
let mut freq_vec: Vec<_> = word_freq.into_iter().collect();
|
||||
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
freq_vec.into_iter()
|
||||
.take(5)
|
||||
.map(|(word, _)| word)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract core values from high-priority memories
|
||||
fn extract_core_values(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||
// Filter high-priority memories (>= 0.7)
|
||||
let high_priority: Vec<_> = memories.iter()
|
||||
.filter(|m| m.priority_score.map(|s| s >= 0.7).unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
if high_priority.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut value_freq: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for memory in high_priority {
|
||||
// Extract value keywords from interpretation
|
||||
if let Some(ref interpretation) = memory.ai_interpretation {
|
||||
let values = extract_value_keywords(interpretation);
|
||||
for value in values {
|
||||
*value_freq.entry(value).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by frequency and take top 5
|
||||
let mut freq_vec: Vec<_> = value_freq.into_iter().collect();
|
||||
freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
freq_vec.into_iter()
|
||||
.take(5)
|
||||
.map(|(value, _)| value)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract key memory IDs (top priority)
|
||||
fn extract_key_memories(memories: &[crate::core::Memory]) -> Vec<String> {
|
||||
let mut sorted_memories: Vec<_> = memories.iter()
|
||||
.filter(|m| m.priority_score.is_some())
|
||||
.collect();
|
||||
|
||||
sorted_memories.sort_by(|a, b| {
|
||||
b.priority_score.unwrap()
|
||||
.partial_cmp(&a.priority_score.unwrap())
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
sorted_memories.into_iter()
|
||||
.take(10)
|
||||
.map(|m| m.id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculate data quality based on volume
|
||||
fn calculate_data_quality(memories: &[crate::core::Memory], personality: &Option<UserAnalysis>) -> f32 {
|
||||
let memory_count = memories.len() as f32;
|
||||
let has_personality = if personality.is_some() { 1.0 } else { 0.0 };
|
||||
|
||||
// Quality increases with data volume
|
||||
let memory_quality = (memory_count / 50.0).min(1.0); // Max quality at 50+ memories
|
||||
let personality_quality = has_personality * 0.5;
|
||||
|
||||
// Weighted average
|
||||
(memory_quality * 0.5 + personality_quality).min(1.0)
|
||||
}
|
||||
|
||||
/// Extract keywords from text (simple word frequency)
|
||||
fn extract_keywords(text: &str) -> Vec<String> {
|
||||
// Simple keyword extraction: words longer than 3 chars
|
||||
text.split_whitespace()
|
||||
.filter(|w| w.len() > 3)
|
||||
.map(|w| w.to_lowercase().trim_matches(|c: char| !c.is_alphanumeric()).to_string())
|
||||
.filter(|w| !is_stopword(w))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract value-related keywords from interpretation
|
||||
fn extract_value_keywords(text: &str) -> Vec<String> {
|
||||
let value_indicators = [
|
||||
"重視", "大切", "価値", "重要", "優先", "好む", "志向",
|
||||
"シンプル", "効率", "品質", "安定", "革新", "創造",
|
||||
"value", "important", "priority", "prefer", "focus",
|
||||
"simple", "efficient", "quality", "stable", "creative",
|
||||
];
|
||||
|
||||
let words = extract_keywords(text);
|
||||
words.into_iter()
|
||||
.filter(|w| {
|
||||
value_indicators.iter().any(|indicator| w.contains(indicator))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if word is a stopword
|
||||
fn is_stopword(word: &str) -> bool {
|
||||
let stopwords = [
|
||||
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
|
||||
"of", "with", "by", "from", "as", "is", "was", "are", "were", "been",
|
||||
"be", "have", "has", "had", "do", "does", "did", "will", "would", "could",
|
||||
"should", "may", "might", "can", "this", "that", "these", "those",
|
||||
"です", "ます", "ました", "である", "ある", "いる", "する", "した",
|
||||
"という", "として", "ために", "によって", "について",
|
||||
];
|
||||
|
||||
stopwords.contains(&word)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_keywords() {
|
||||
let text = "Rust architecture design is important for scalability";
|
||||
let keywords = extract_keywords(text);
|
||||
|
||||
assert!(keywords.contains(&"rust".to_string()));
|
||||
assert!(keywords.contains(&"architecture".to_string()));
|
||||
assert!(keywords.contains(&"design".to_string()));
|
||||
assert!(!keywords.contains(&"is".to_string())); // stopword
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stopword() {
|
||||
assert!(is_stopword("the"));
|
||||
assert!(is_stopword("です"));
|
||||
assert!(!is_stopword("rust"));
|
||||
}
|
||||
}
|
||||
317
src/core/relationship.rs
Normal file
317
src/core/relationship.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::core::{Memory, MemoryStore, UserProfile};
|
||||
use crate::core::error::Result;
|
||||
|
||||
/// Inferred relationship with an entity (Layer 4)
|
||||
///
|
||||
/// This is not stored permanently but generated on-demand from
|
||||
/// Layer 1 memories and Layer 3.5 user profile.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RelationshipInference {
|
||||
/// Entity identifier
|
||||
pub entity_id: String,
|
||||
|
||||
/// Total interaction count with this entity
|
||||
pub interaction_count: u32,
|
||||
|
||||
/// Average priority score of memories with this entity
|
||||
pub avg_priority: f32,
|
||||
|
||||
/// Days since last interaction
|
||||
pub days_since_last: i64,
|
||||
|
||||
/// Inferred bond strength (0.0-1.0)
|
||||
pub bond_strength: f32,
|
||||
|
||||
/// Inferred relationship type
|
||||
pub relationship_type: String,
|
||||
|
||||
/// Confidence in this inference (0.0-1.0, based on data volume)
|
||||
pub confidence: f32,
|
||||
|
||||
/// When this inference was generated
|
||||
pub inferred_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl RelationshipInference {
|
||||
/// Infer relationship from memories and user profile
|
||||
pub fn infer(
|
||||
entity_id: String,
|
||||
memories: &[Memory],
|
||||
user_profile: &UserProfile,
|
||||
) -> Self {
|
||||
// Filter memories related to this entity
|
||||
let entity_memories: Vec<_> = memories
|
||||
.iter()
|
||||
.filter(|m| m.has_entity(&entity_id))
|
||||
.collect();
|
||||
|
||||
let interaction_count = entity_memories.len() as u32;
|
||||
|
||||
// Calculate average priority
|
||||
let total_priority: f32 = entity_memories
|
||||
.iter()
|
||||
.filter_map(|m| m.priority_score)
|
||||
.sum();
|
||||
let priority_count = entity_memories
|
||||
.iter()
|
||||
.filter(|m| m.priority_score.is_some())
|
||||
.count() as f32;
|
||||
let avg_priority = if priority_count > 0.0 {
|
||||
total_priority / priority_count
|
||||
} else {
|
||||
0.5 // Default to neutral if no scores
|
||||
};
|
||||
|
||||
// Calculate days since last interaction
|
||||
let days_since_last = entity_memories
|
||||
.iter()
|
||||
.map(|m| (Utc::now() - m.created_at).num_days())
|
||||
.min()
|
||||
.unwrap_or(999);
|
||||
|
||||
// Infer bond strength based on user personality
|
||||
let bond_strength = Self::calculate_bond_strength(
|
||||
interaction_count,
|
||||
avg_priority,
|
||||
user_profile,
|
||||
);
|
||||
|
||||
// Infer relationship type
|
||||
let relationship_type = Self::infer_relationship_type(
|
||||
interaction_count,
|
||||
avg_priority,
|
||||
bond_strength,
|
||||
);
|
||||
|
||||
// Calculate confidence
|
||||
let confidence = Self::calculate_confidence(interaction_count);
|
||||
|
||||
RelationshipInference {
|
||||
entity_id,
|
||||
interaction_count,
|
||||
avg_priority,
|
||||
days_since_last,
|
||||
bond_strength,
|
||||
relationship_type,
|
||||
confidence,
|
||||
inferred_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate bond strength from interaction data and user personality
|
||||
fn calculate_bond_strength(
|
||||
interaction_count: u32,
|
||||
avg_priority: f32,
|
||||
user_profile: &UserProfile,
|
||||
) -> f32 {
|
||||
// Extract extraversion score (if available)
|
||||
let extraversion = user_profile
|
||||
.dominant_traits
|
||||
.iter()
|
||||
.find(|t| t.name == "extraversion")
|
||||
.map(|t| t.score)
|
||||
.unwrap_or(0.5);
|
||||
|
||||
let bond_strength = if extraversion < 0.5 {
|
||||
// Introverted: fewer but deeper relationships
|
||||
// Interaction count matters more
|
||||
let count_factor = (interaction_count as f32 / 20.0).min(1.0);
|
||||
let priority_factor = avg_priority;
|
||||
|
||||
// Weight: 60% count, 40% priority
|
||||
count_factor * 0.6 + priority_factor * 0.4
|
||||
} else {
|
||||
// Extroverted: many relationships, quality varies
|
||||
// Priority matters more
|
||||
let count_factor = (interaction_count as f32 / 50.0).min(1.0);
|
||||
let priority_factor = avg_priority;
|
||||
|
||||
// Weight: 40% count, 60% priority
|
||||
count_factor * 0.4 + priority_factor * 0.6
|
||||
};
|
||||
|
||||
bond_strength.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Infer relationship type from metrics
|
||||
fn infer_relationship_type(
|
||||
interaction_count: u32,
|
||||
avg_priority: f32,
|
||||
bond_strength: f32,
|
||||
) -> String {
|
||||
if bond_strength >= 0.8 {
|
||||
"close_friend".to_string()
|
||||
} else if bond_strength >= 0.6 {
|
||||
"friend".to_string()
|
||||
} else if bond_strength >= 0.4 {
|
||||
if avg_priority >= 0.6 {
|
||||
"valued_acquaintance".to_string()
|
||||
} else {
|
||||
"acquaintance".to_string()
|
||||
}
|
||||
} else if interaction_count >= 5 {
|
||||
"regular_contact".to_string()
|
||||
} else {
|
||||
"distant".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate confidence based on data volume
|
||||
fn calculate_confidence(interaction_count: u32) -> f32 {
|
||||
// Confidence increases with more data
|
||||
// 1-2 interactions: low confidence (0.2-0.3)
|
||||
// 5 interactions: medium confidence (0.5)
|
||||
// 10+ interactions: high confidence (0.8+)
|
||||
let confidence = match interaction_count {
|
||||
0 => 0.0,
|
||||
1 => 0.2,
|
||||
2 => 0.3,
|
||||
3 => 0.4,
|
||||
4 => 0.45,
|
||||
5..=9 => 0.5 + (interaction_count - 5) as f32 * 0.05,
|
||||
_ => 0.8 + ((interaction_count - 10) as f32 * 0.02).min(0.2),
|
||||
};
|
||||
|
||||
confidence.clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate relationship inferences for all entities in memories
|
||||
pub fn infer_all_relationships(
|
||||
store: &MemoryStore,
|
||||
) -> Result<Vec<RelationshipInference>> {
|
||||
// Check cache first
|
||||
if let Some(cached) = store.get_cached_all_relationships()? {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
// Get all memories
|
||||
let memories = store.list()?;
|
||||
|
||||
// Get user profile
|
||||
let user_profile = store.get_profile()?;
|
||||
|
||||
// Extract all unique entities
|
||||
let mut entities: HashMap<String, ()> = HashMap::new();
|
||||
for memory in &memories {
|
||||
if let Some(ref entity_list) = memory.related_entities {
|
||||
for entity in entity_list {
|
||||
entities.insert(entity.clone(), ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Infer relationship for each entity
|
||||
let mut relationships: Vec<_> = entities
|
||||
.keys()
|
||||
.map(|entity_id| {
|
||||
RelationshipInference::infer(
|
||||
entity_id.clone(),
|
||||
&memories,
|
||||
&user_profile,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by bond strength (descending)
|
||||
relationships.sort_by(|a, b| {
|
||||
b.bond_strength
|
||||
.partial_cmp(&a.bond_strength)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Cache the result
|
||||
store.save_all_relationships_cache(&relationships)?;
|
||||
|
||||
Ok(relationships)
|
||||
}
|
||||
|
||||
/// Get relationship inference for a specific entity (with caching)
|
||||
pub fn get_relationship(
|
||||
store: &MemoryStore,
|
||||
entity_id: &str,
|
||||
) -> Result<RelationshipInference> {
|
||||
// Check cache first
|
||||
if let Some(cached) = store.get_cached_relationship(entity_id)? {
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
// Get all memories
|
||||
let memories = store.list()?;
|
||||
|
||||
// Get user profile
|
||||
let user_profile = store.get_profile()?;
|
||||
|
||||
// Infer relationship
|
||||
let relationship = RelationshipInference::infer(
|
||||
entity_id.to_string(),
|
||||
&memories,
|
||||
&user_profile,
|
||||
);
|
||||
|
||||
// Cache it
|
||||
store.save_relationship_cache(entity_id, &relationship)?;
|
||||
|
||||
Ok(relationship)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::profile::TraitScore;
|
||||
|
||||
#[test]
|
||||
fn test_confidence_calculation() {
|
||||
assert_eq!(RelationshipInference::calculate_confidence(0), 0.0);
|
||||
assert_eq!(RelationshipInference::calculate_confidence(1), 0.2);
|
||||
assert_eq!(RelationshipInference::calculate_confidence(5), 0.5);
|
||||
assert!(RelationshipInference::calculate_confidence(10) >= 0.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relationship_type() {
|
||||
assert_eq!(
|
||||
RelationshipInference::infer_relationship_type(20, 0.9, 0.85),
|
||||
"close_friend"
|
||||
);
|
||||
assert_eq!(
|
||||
RelationshipInference::infer_relationship_type(10, 0.7, 0.65),
|
||||
"friend"
|
||||
);
|
||||
assert_eq!(
|
||||
RelationshipInference::infer_relationship_type(5, 0.5, 0.45),
|
||||
"acquaintance"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bond_strength_introverted() {
|
||||
let user_profile = UserProfile {
|
||||
dominant_traits: vec![
|
||||
TraitScore {
|
||||
name: "extraversion".to_string(),
|
||||
score: 0.3, // Introverted
|
||||
},
|
||||
],
|
||||
core_interests: vec![],
|
||||
core_values: vec![],
|
||||
key_memory_ids: vec![],
|
||||
data_quality: 1.0,
|
||||
last_updated: Utc::now(),
|
||||
};
|
||||
|
||||
// Introverted: count matters more
|
||||
let strength = RelationshipInference::calculate_bond_strength(
|
||||
20, // Many interactions
|
||||
0.5, // Medium priority
|
||||
&user_profile,
|
||||
);
|
||||
|
||||
// Should be high due to high interaction count
|
||||
assert!(strength > 0.5);
|
||||
}
|
||||
}
|
||||
693
src/core/store.rs
Normal file
693
src/core/store.rs
Normal file
@@ -0,0 +1,693 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::analysis::UserAnalysis;
|
||||
use super::error::{MemoryError, Result};
|
||||
use super::memory::Memory;
|
||||
|
||||
/// SQLite-based memory storage
|
||||
pub struct MemoryStore {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
/// Create a new MemoryStore with the given database path
|
||||
pub fn new(db_path: PathBuf) -> Result<Self> {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = db_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// Initialize database schema
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS memories (
|
||||
id TEXT PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
ai_interpretation TEXT,
|
||||
priority_score REAL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Migrate existing tables (add columns if they don't exist)
|
||||
// SQLite doesn't have "IF NOT EXISTS" for columns, so we check first
|
||||
let has_ai_interpretation: bool = conn
|
||||
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='ai_interpretation'")?
|
||||
.query_row([], |row| row.get(0))
|
||||
.map(|count: i32| count > 0)?;
|
||||
|
||||
if !has_ai_interpretation {
|
||||
conn.execute("ALTER TABLE memories ADD COLUMN ai_interpretation TEXT", [])?;
|
||||
conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?;
|
||||
}
|
||||
|
||||
// Migrate for Layer 4: related_entities
|
||||
let has_related_entities: bool = conn
|
||||
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='related_entities'")?
|
||||
.query_row([], |row| row.get(0))
|
||||
.map(|count: i32| count > 0)?;
|
||||
|
||||
if !has_related_entities {
|
||||
conn.execute("ALTER TABLE memories ADD COLUMN related_entities TEXT", [])?;
|
||||
}
|
||||
|
||||
// Create indexes for better query performance
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_updated_at ON memories(updated_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_priority_score ON memories(priority_score)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create user_analyses table (Layer 3)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS user_analyses (
|
||||
id TEXT PRIMARY KEY,
|
||||
openness REAL NOT NULL,
|
||||
conscientiousness REAL NOT NULL,
|
||||
extraversion REAL NOT NULL,
|
||||
agreeableness REAL NOT NULL,
|
||||
neuroticism REAL NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
analyzed_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_analyzed_at ON user_analyses(analyzed_at)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create user_profiles table (Layer 3.5 - integrated profile cache)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS user_profiles (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
data TEXT NOT NULL,
|
||||
last_updated TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create relationship_cache table (Layer 4 - relationship inference cache)
|
||||
// entity_id = "" for all_relationships cache
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS relationship_cache (
|
||||
entity_id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
cached_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
/// Create a new MemoryStore using default config directory
|
||||
pub fn default() -> Result<Self> {
|
||||
let data_dir = dirs::config_dir()
|
||||
.ok_or_else(|| MemoryError::Config("Could not find config directory".to_string()))?
|
||||
.join("syui")
|
||||
.join("ai")
|
||||
.join("gpt");
|
||||
|
||||
let db_path = data_dir.join("memory.db");
|
||||
Self::new(db_path)
|
||||
}
|
||||
|
||||
/// Insert a new memory
|
||||
pub fn create(&self, memory: &Memory) -> Result<()> {
|
||||
let related_entities_json = memory.related_entities
|
||||
.as_ref()
|
||||
.map(|entities| serde_json::to_string(entities).ok())
|
||||
.flatten();
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT INTO memories (id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
&memory.id,
|
||||
&memory.content,
|
||||
&memory.ai_interpretation,
|
||||
&memory.priority_score,
|
||||
related_entities_json,
|
||||
memory.created_at.to_rfc3339(),
|
||||
memory.updated_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
|
||||
// Clear relationship cache since memory data changed
|
||||
self.clear_relationship_cache()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a memory by ID
|
||||
pub fn get(&self, id: &str) -> Result<Memory> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
|
||||
FROM memories WHERE id = ?1")?;
|
||||
|
||||
let memory = stmt.query_row(params![id], |row| {
|
||||
let created_at: String = row.get(5)?;
|
||||
let updated_at: String = row.get(6)?;
|
||||
let related_entities_json: Option<String> = row.get(4)?;
|
||||
let related_entities = related_entities_json
|
||||
.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
Ok(Memory {
|
||||
id: row.get(0)?,
|
||||
content: row.get(1)?,
|
||||
ai_interpretation: row.get(2)?,
|
||||
priority_score: row.get(3)?,
|
||||
related_entities,
|
||||
created_at: DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
5,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
updated_at: DateTime::parse_from_rfc3339(&updated_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
6,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Update an existing memory
|
||||
pub fn update(&self, memory: &Memory) -> Result<()> {
|
||||
let related_entities_json = memory.related_entities
|
||||
.as_ref()
|
||||
.map(|entities| serde_json::to_string(entities).ok())
|
||||
.flatten();
|
||||
|
||||
let rows_affected = self.conn.execute(
|
||||
"UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, related_entities = ?4, updated_at = ?5
|
||||
WHERE id = ?6",
|
||||
params![
|
||||
&memory.content,
|
||||
&memory.ai_interpretation,
|
||||
&memory.priority_score,
|
||||
related_entities_json,
|
||||
memory.updated_at.to_rfc3339(),
|
||||
&memory.id,
|
||||
],
|
||||
)?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(MemoryError::NotFound(memory.id.clone()));
|
||||
}
|
||||
|
||||
// Clear relationship cache since memory data changed
|
||||
self.clear_relationship_cache()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a memory by ID
|
||||
pub fn delete(&self, id: &str) -> Result<()> {
|
||||
let rows_affected = self
|
||||
.conn
|
||||
.execute("DELETE FROM memories WHERE id = ?1", params![id])?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(MemoryError::NotFound(id.to_string()));
|
||||
}
|
||||
|
||||
// Clear relationship cache since memory data changed
|
||||
self.clear_relationship_cache()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all memories, ordered by creation time (newest first)
|
||||
pub fn list(&self) -> Result<Vec<Memory>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
|
||||
FROM memories ORDER BY created_at DESC",
|
||||
)?;
|
||||
|
||||
let memories = stmt
|
||||
.query_map([], |row| {
|
||||
let created_at: String = row.get(5)?;
|
||||
let updated_at: String = row.get(6)?;
|
||||
let related_entities_json: Option<String> = row.get(4)?;
|
||||
let related_entities = related_entities_json
|
||||
.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
Ok(Memory {
|
||||
id: row.get(0)?,
|
||||
content: row.get(1)?,
|
||||
ai_interpretation: row.get(2)?,
|
||||
priority_score: row.get(3)?,
|
||||
related_entities,
|
||||
created_at: DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
5,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
updated_at: DateTime::parse_from_rfc3339(&updated_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
6,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(memories)
|
||||
}
|
||||
|
||||
/// Search memories by content or AI interpretation (case-insensitive)
|
||||
pub fn search(&self, query: &str) -> Result<Vec<Memory>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
|
||||
FROM memories
|
||||
WHERE content LIKE ?1 OR ai_interpretation LIKE ?1
|
||||
ORDER BY created_at DESC",
|
||||
)?;
|
||||
|
||||
let search_pattern = format!("%{}%", query);
|
||||
let memories = stmt
|
||||
.query_map(params![search_pattern], |row| {
|
||||
let created_at: String = row.get(5)?;
|
||||
let updated_at: String = row.get(6)?;
|
||||
let related_entities_json: Option<String> = row.get(4)?;
|
||||
let related_entities = related_entities_json
|
||||
.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
Ok(Memory {
|
||||
id: row.get(0)?,
|
||||
content: row.get(1)?,
|
||||
ai_interpretation: row.get(2)?,
|
||||
priority_score: row.get(3)?,
|
||||
related_entities,
|
||||
created_at: DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
5,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
updated_at: DateTime::parse_from_rfc3339(&updated_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
|
||||
6,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
))?,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(memories)
|
||||
}
|
||||
|
||||
/// Count total memories
|
||||
pub fn count(&self) -> Result<usize> {
|
||||
let count: usize = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM memories", [], |row| row.get(0))?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// ========== Layer 3: User Analysis Methods ==========
|
||||
|
||||
/// Save a new user personality analysis
|
||||
pub fn save_analysis(&self, analysis: &UserAnalysis) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO user_analyses (id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
&analysis.id,
|
||||
&analysis.openness,
|
||||
&analysis.conscientiousness,
|
||||
&analysis.extraversion,
|
||||
&analysis.agreeableness,
|
||||
&analysis.neuroticism,
|
||||
&analysis.summary,
|
||||
analysis.analyzed_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the most recent user analysis
|
||||
pub fn get_latest_analysis(&self) -> Result<Option<UserAnalysis>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
|
||||
FROM user_analyses
|
||||
ORDER BY analyzed_at DESC
|
||||
LIMIT 1",
|
||||
)?;
|
||||
|
||||
let result = stmt.query_row([], |row| {
|
||||
let analyzed_at: String = row.get(7)?;
|
||||
|
||||
Ok(UserAnalysis {
|
||||
id: row.get(0)?,
|
||||
openness: row.get(1)?,
|
||||
conscientiousness: row.get(2)?,
|
||||
extraversion: row.get(3)?,
|
||||
agreeableness: row.get(4)?,
|
||||
neuroticism: row.get(5)?,
|
||||
summary: row.get(6)?,
|
||||
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
7,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
)
|
||||
})?,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(analysis) => Ok(Some(analysis)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all user analyses, ordered by date (newest first)
|
||||
pub fn list_analyses(&self) -> Result<Vec<UserAnalysis>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, openness, conscientiousness, extraversion, agreeableness, neuroticism, summary, analyzed_at
|
||||
FROM user_analyses
|
||||
ORDER BY analyzed_at DESC",
|
||||
)?;
|
||||
|
||||
let analyses = stmt
|
||||
.query_map([], |row| {
|
||||
let analyzed_at: String = row.get(7)?;
|
||||
|
||||
Ok(UserAnalysis {
|
||||
id: row.get(0)?,
|
||||
openness: row.get(1)?,
|
||||
conscientiousness: row.get(2)?,
|
||||
extraversion: row.get(3)?,
|
||||
agreeableness: row.get(4)?,
|
||||
neuroticism: row.get(5)?,
|
||||
summary: row.get(6)?,
|
||||
analyzed_at: DateTime::parse_from_rfc3339(&analyzed_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::FromSqlConversionFailure(
|
||||
7,
|
||||
rusqlite::types::Type::Text,
|
||||
Box::new(e),
|
||||
)
|
||||
})?,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(analyses)
|
||||
}
|
||||
|
||||
// === Layer 3.5: Integrated Profile ===
|
||||
|
||||
/// Save integrated profile to cache
|
||||
pub fn save_profile(&self, profile: &super::profile::UserProfile) -> Result<()> {
|
||||
let profile_json = serde_json::to_string(profile)?;
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO user_profiles (id, data, last_updated) VALUES (1, ?1, ?2)",
|
||||
params![profile_json, profile.last_updated.to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached profile if exists
|
||||
pub fn get_cached_profile(&self) -> Result<Option<super::profile::UserProfile>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT data FROM user_profiles WHERE id = 1")?;
|
||||
|
||||
let result = stmt.query_row([], |row| {
|
||||
let json: String = row.get(0)?;
|
||||
Ok(json)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(json) => {
|
||||
let profile: super::profile::UserProfile = serde_json::from_str(&json)?;
|
||||
Ok(Some(profile))
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or generate profile (with automatic caching)
|
||||
pub fn get_profile(&self) -> Result<super::profile::UserProfile> {
|
||||
// Check cache first
|
||||
if let Some(cached) = self.get_cached_profile()? {
|
||||
// Check if needs update
|
||||
if !cached.needs_update(self)? {
|
||||
return Ok(cached);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new profile
|
||||
let profile = super::profile::UserProfile::generate(self)?;
|
||||
|
||||
// Cache it
|
||||
self.save_profile(&profile)?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
// ========== Layer 4: Relationship Cache Methods ==========
|
||||
|
||||
/// Cache duration in minutes
|
||||
const RELATIONSHIP_CACHE_DURATION_MINUTES: i64 = 5;
|
||||
|
||||
/// Save relationship inference to cache
|
||||
pub fn save_relationship_cache(
|
||||
&self,
|
||||
entity_id: &str,
|
||||
relationship: &super::relationship::RelationshipInference,
|
||||
) -> Result<()> {
|
||||
let data = serde_json::to_string(relationship)?;
|
||||
let cached_at = Utc::now().to_rfc3339();
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES (?1, ?2, ?3)",
|
||||
params![entity_id, data, cached_at],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached relationship inference
|
||||
pub fn get_cached_relationship(
|
||||
&self,
|
||||
entity_id: &str,
|
||||
) -> Result<Option<super::relationship::RelationshipInference>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ?1")?;
|
||||
|
||||
let result = stmt.query_row([entity_id], |row| {
|
||||
let data: String = row.get(0)?;
|
||||
let cached_at: String = row.get(1)?;
|
||||
Ok((data, cached_at))
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((data, cached_at_str)) => {
|
||||
// Check if cache is still valid (within 5 minutes)
|
||||
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
|
||||
.map_err(|e| MemoryError::Parse(e.to_string()))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let age_minutes = (Utc::now() - cached_at).num_seconds() / 60;
|
||||
|
||||
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
|
||||
let relationship: super::relationship::RelationshipInference =
|
||||
serde_json::from_str(&data)?;
|
||||
Ok(Some(relationship))
|
||||
} else {
|
||||
// Cache expired
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Save all relationships list to cache (use empty string as entity_id)
|
||||
pub fn save_all_relationships_cache(
|
||||
&self,
|
||||
relationships: &[super::relationship::RelationshipInference],
|
||||
) -> Result<()> {
|
||||
let data = serde_json::to_string(relationships)?;
|
||||
let cached_at = Utc::now().to_rfc3339();
|
||||
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO relationship_cache (entity_id, data, cached_at) VALUES ('', ?1, ?2)",
|
||||
params![data, cached_at],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached all relationships list
|
||||
pub fn get_cached_all_relationships(
|
||||
&self,
|
||||
) -> Result<Option<Vec<super::relationship::RelationshipInference>>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT data, cached_at FROM relationship_cache WHERE entity_id = ''")?;
|
||||
|
||||
let result = stmt.query_row([], |row| {
|
||||
let data: String = row.get(0)?;
|
||||
let cached_at: String = row.get(1)?;
|
||||
Ok((data, cached_at))
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok((data, cached_at_str)) => {
|
||||
let cached_at = DateTime::parse_from_rfc3339(&cached_at_str)
|
||||
.map_err(|e| MemoryError::Parse(e.to_string()))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
let age_minutes = (Utc::now() - cached_at).num_seconds() / 60;
|
||||
|
||||
if age_minutes < Self::RELATIONSHIP_CACHE_DURATION_MINUTES {
|
||||
let relationships: Vec<super::relationship::RelationshipInference> =
|
||||
serde_json::from_str(&data)?;
|
||||
Ok(Some(relationships))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all relationship caches (call when memories are modified)
|
||||
pub fn clear_relationship_cache(&self) -> Result<()> {
|
||||
self.conn.execute("DELETE FROM relationship_cache", [])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_store() -> MemoryStore {
|
||||
MemoryStore::new(":memory:".into()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_and_get() {
|
||||
let store = create_test_store();
|
||||
let memory = Memory::new("Test content".to_string());
|
||||
|
||||
store.create(&memory).unwrap();
|
||||
let retrieved = store.get(&memory.id).unwrap();
|
||||
|
||||
assert_eq!(retrieved.id, memory.id);
|
||||
assert_eq!(retrieved.content, memory.content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
let store = create_test_store();
|
||||
let mut memory = Memory::new("Original".to_string());
|
||||
|
||||
store.create(&memory).unwrap();
|
||||
|
||||
memory.update_content("Updated".to_string());
|
||||
store.update(&memory).unwrap();
|
||||
|
||||
let retrieved = store.get(&memory.id).unwrap();
|
||||
assert_eq!(retrieved.content, "Updated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
let store = create_test_store();
|
||||
let memory = Memory::new("To delete".to_string());
|
||||
|
||||
store.create(&memory).unwrap();
|
||||
store.delete(&memory.id).unwrap();
|
||||
|
||||
assert!(store.get(&memory.id).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let store = create_test_store();
|
||||
|
||||
let mem1 = Memory::new("First".to_string());
|
||||
let mem2 = Memory::new("Second".to_string());
|
||||
|
||||
store.create(&mem1).unwrap();
|
||||
store.create(&mem2).unwrap();
|
||||
|
||||
let memories = store.list().unwrap();
|
||||
assert_eq!(memories.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search() {
|
||||
let store = create_test_store();
|
||||
|
||||
store
|
||||
.create(&Memory::new("Hello world".to_string()))
|
||||
.unwrap();
|
||||
store
|
||||
.create(&Memory::new("Goodbye world".to_string()))
|
||||
.unwrap();
|
||||
store.create(&Memory::new("Testing".to_string())).unwrap();
|
||||
|
||||
let results = store.search("world").unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
|
||||
let results = store.search("Hello").unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count() {
|
||||
let store = create_test_store();
|
||||
assert_eq!(store.count().unwrap(), 0);
|
||||
|
||||
store.create(&Memory::new("Test".to_string())).unwrap();
|
||||
assert_eq!(store.count().unwrap(), 1);
|
||||
}
|
||||
}
|
||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod core;
|
||||
pub mod mcp;
|
||||
141
src/main.rs
Normal file
141
src/main.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use aigpt::core::{Memory, MemoryStore};
|
||||
use aigpt::mcp::BaseMCPServer;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "aigpt")]
|
||||
#[command(about = "Simple memory storage for Claude with MCP - Layer 1")]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Start MCP server
|
||||
Server {
|
||||
/// Enable Layer 4 relationship features (for games/companions)
|
||||
#[arg(long)]
|
||||
enable_layer4: bool,
|
||||
},
|
||||
|
||||
/// Create a new memory
|
||||
Create {
|
||||
/// Content of the memory
|
||||
content: String,
|
||||
},
|
||||
|
||||
/// Get a memory by ID
|
||||
Get {
|
||||
/// Memory ID
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Update a memory
|
||||
Update {
|
||||
/// Memory ID
|
||||
id: String,
|
||||
/// New content
|
||||
content: String,
|
||||
},
|
||||
|
||||
/// Delete a memory
|
||||
Delete {
|
||||
/// Memory ID
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// List all memories
|
||||
List,
|
||||
|
||||
/// Search memories by content
|
||||
Search {
|
||||
/// Search query
|
||||
query: String,
|
||||
},
|
||||
|
||||
/// Show statistics
|
||||
Stats,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Server { enable_layer4 } => {
|
||||
let server = BaseMCPServer::new(enable_layer4)?;
|
||||
server.run()?;
|
||||
}
|
||||
|
||||
Commands::Create { content } => {
|
||||
let store = MemoryStore::default()?;
|
||||
let memory = Memory::new(content);
|
||||
store.create(&memory)?;
|
||||
println!("Created memory: {}", memory.id);
|
||||
}
|
||||
|
||||
Commands::Get { id } => {
|
||||
let store = MemoryStore::default()?;
|
||||
let memory = store.get(&id)?;
|
||||
println!("ID: {}", memory.id);
|
||||
println!("Content: {}", memory.content);
|
||||
println!("Created: {}", memory.created_at);
|
||||
println!("Updated: {}", memory.updated_at);
|
||||
}
|
||||
|
||||
Commands::Update { id, content } => {
|
||||
let store = MemoryStore::default()?;
|
||||
let mut memory = store.get(&id)?;
|
||||
memory.update_content(content);
|
||||
store.update(&memory)?;
|
||||
println!("Updated memory: {}", memory.id);
|
||||
}
|
||||
|
||||
Commands::Delete { id } => {
|
||||
let store = MemoryStore::default()?;
|
||||
store.delete(&id)?;
|
||||
println!("Deleted memory: {}", id);
|
||||
}
|
||||
|
||||
Commands::List => {
|
||||
let store = MemoryStore::default()?;
|
||||
let memories = store.list()?;
|
||||
if memories.is_empty() {
|
||||
println!("No memories found");
|
||||
} else {
|
||||
for memory in memories {
|
||||
println!("\n[{}]", memory.id);
|
||||
println!(" {}", memory.content);
|
||||
println!(" Created: {}", memory.created_at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Search { query } => {
|
||||
let store = MemoryStore::default()?;
|
||||
let memories = store.search(&query)?;
|
||||
if memories.is_empty() {
|
||||
println!("No memories found matching '{}'", query);
|
||||
} else {
|
||||
println!("Found {} memory(ies):", memories.len());
|
||||
for memory in memories {
|
||||
println!("\n[{}]", memory.id);
|
||||
println!(" {}", memory.content);
|
||||
println!(" Created: {}", memory.created_at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Stats => {
|
||||
let store = MemoryStore::default()?;
|
||||
let count = store.count()?;
|
||||
println!("Total memories: {}", count);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
648
src/mcp/base.rs
Normal file
648
src/mcp/base.rs
Normal file
@@ -0,0 +1,648 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use crate::core::{Memory, MemoryStore, UserAnalysis, infer_all_relationships, get_relationship};
|
||||
|
||||
pub struct BaseMCPServer {
|
||||
store: MemoryStore,
|
||||
enable_layer4: bool,
|
||||
}
|
||||
|
||||
impl BaseMCPServer {
|
||||
pub fn new(enable_layer4: bool) -> Result<Self> {
|
||||
let store = MemoryStore::default()?;
|
||||
Ok(BaseMCPServer { store, enable_layer4 })
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<()> {
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let reader = stdin.lock();
|
||||
let lines = reader.lines();
|
||||
|
||||
for line_result in lines {
|
||||
match line_result {
|
||||
Ok(line) => {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(request) = serde_json::from_str::<Value>(&trimmed) {
|
||||
let response = self.handle_request(request);
|
||||
let response_str = serde_json::to_string(&response)?;
|
||||
stdout.write_all(response_str.as_bytes())?;
|
||||
stdout.write_all(b"\n")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(&self, request: Value) -> Value {
|
||||
let method = request["method"].as_str().unwrap_or("");
|
||||
let id = request["id"].clone();
|
||||
|
||||
match method {
|
||||
"initialize" => self.handle_initialize(id),
|
||||
"tools/list" => self.handle_tools_list(id),
|
||||
"tools/call" => self.handle_tools_call(request, id),
|
||||
_ => self.handle_unknown_method(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_initialize(&self, id: Value) -> Value {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "aigpt",
|
||||
"version": "0.2.0"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_tools_list(&self, id: Value) -> Value {
|
||||
let tools = self.get_available_tools();
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": {
|
||||
"tools": tools
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_available_tools(&self) -> Vec<Value> {
|
||||
let mut tools = vec![
|
||||
json!({
|
||||
"name": "create_memory",
|
||||
"description": "Create a new memory entry (Layer 1: simple storage)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content of the memory"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "create_ai_memory",
|
||||
"description": "Create a memory with AI interpretation and priority score (Layer 2)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Original content of the memory"
|
||||
},
|
||||
"ai_interpretation": {
|
||||
"type": "string",
|
||||
"description": "AI's creative interpretation of the content (optional)"
|
||||
},
|
||||
"priority_score": {
|
||||
"type": "number",
|
||||
"description": "Priority score from 0.0 (low) to 1.0 (high) (optional)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "get_memory",
|
||||
"description": "Get a memory by ID",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Memory ID"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "search_memories",
|
||||
"description": "Search memories by content",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "list_memories",
|
||||
"description": "List all memories",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "update_memory",
|
||||
"description": "Update an existing memory entry",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the memory to update"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "New content for the memory"
|
||||
}
|
||||
},
|
||||
"required": ["id", "content"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "delete_memory",
|
||||
"description": "Delete a memory entry",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the memory to delete"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "save_user_analysis",
|
||||
"description": "Save a Big Five personality analysis based on user's memories (Layer 3)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"openness": {
|
||||
"type": "number",
|
||||
"description": "Openness to Experience (0.0-1.0)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"conscientiousness": {
|
||||
"type": "number",
|
||||
"description": "Conscientiousness (0.0-1.0)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"extraversion": {
|
||||
"type": "number",
|
||||
"description": "Extraversion (0.0-1.0)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"agreeableness": {
|
||||
"type": "number",
|
||||
"description": "Agreeableness (0.0-1.0)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"neuroticism": {
|
||||
"type": "number",
|
||||
"description": "Neuroticism (0.0-1.0)",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "AI-generated summary of the personality analysis"
|
||||
}
|
||||
},
|
||||
"required": ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism", "summary"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "get_user_analysis",
|
||||
"description": "Get the most recent Big Five personality analysis (Layer 3)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "get_profile",
|
||||
"description": "Get integrated user profile - the essential summary of personality, interests, and values (Layer 3.5). This is the primary tool for understanding the user.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
// Layer 4 tools (optional - only when enabled)
|
||||
if self.enable_layer4 {
|
||||
tools.extend(vec![
|
||||
json!({
|
||||
"name": "get_relationship",
|
||||
"description": "Get inferred relationship with a specific entity (Layer 4). Analyzes memories and user profile to infer bond strength and relationship type. Use only when game/relationship features are active.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entity_id": {
|
||||
"type": "string",
|
||||
"description": "Entity identifier (e.g., 'alice', 'companion_miku')"
|
||||
}
|
||||
},
|
||||
"required": ["entity_id"]
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "list_relationships",
|
||||
"description": "List all inferred relationships sorted by bond strength (Layer 4). Returns relationships with all tracked entities. Use only when game/relationship features are active.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "number",
|
||||
"description": "Maximum number of relationships to return (default: 10)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn handle_tools_call(&self, request: Value, id: Value) -> Value {
|
||||
let tool_name = request["params"]["name"].as_str().unwrap_or("");
|
||||
let arguments = &request["params"]["arguments"];
|
||||
|
||||
let result = self.execute_tool(tool_name, arguments);
|
||||
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": result.to_string()
|
||||
}]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_tool(&self, tool_name: &str, arguments: &Value) -> Value {
|
||||
match tool_name {
|
||||
"create_memory" => self.tool_create_memory(arguments),
|
||||
"create_ai_memory" => self.tool_create_ai_memory(arguments),
|
||||
"get_memory" => self.tool_get_memory(arguments),
|
||||
"search_memories" => self.tool_search_memories(arguments),
|
||||
"list_memories" => self.tool_list_memories(),
|
||||
"update_memory" => self.tool_update_memory(arguments),
|
||||
"delete_memory" => self.tool_delete_memory(arguments),
|
||||
"save_user_analysis" => self.tool_save_user_analysis(arguments),
|
||||
"get_user_analysis" => self.tool_get_user_analysis(),
|
||||
"get_profile" => self.tool_get_profile(),
|
||||
|
||||
// Layer 4 tools (require --enable-layer4 flag)
|
||||
"get_relationship" | "list_relationships" => {
|
||||
if !self.enable_layer4 {
|
||||
return json!({
|
||||
"success": false,
|
||||
"error": "Layer 4 is not enabled. Start server with --enable-layer4 flag to use relationship features."
|
||||
});
|
||||
}
|
||||
|
||||
match tool_name {
|
||||
"get_relationship" => self.tool_get_relationship(arguments),
|
||||
"list_relationships" => self.tool_list_relationships(arguments),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
_ => json!({
|
||||
"success": false,
|
||||
"error": format!("Unknown tool: {}", tool_name)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_create_memory(&self, arguments: &Value) -> Value {
|
||||
let content = arguments["content"].as_str().unwrap_or("");
|
||||
let memory = Memory::new(content.to_string());
|
||||
|
||||
match self.store.create(&memory) {
|
||||
Ok(()) => json!({
|
||||
"success": true,
|
||||
"id": memory.id,
|
||||
"message": "Memory created successfully"
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_create_ai_memory(&self, arguments: &Value) -> Value {
|
||||
let content = arguments["content"].as_str().unwrap_or("");
|
||||
let ai_interpretation = arguments["ai_interpretation"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string());
|
||||
let priority_score = arguments["priority_score"].as_f64().map(|f| f as f32);
|
||||
|
||||
let memory = Memory::new_ai(content.to_string(), ai_interpretation, priority_score);
|
||||
|
||||
match self.store.create(&memory) {
|
||||
Ok(()) => json!({
|
||||
"success": true,
|
||||
"id": memory.id,
|
||||
"message": "AI memory created successfully",
|
||||
"has_interpretation": memory.ai_interpretation.is_some(),
|
||||
"has_score": memory.priority_score.is_some()
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_get_memory(&self, arguments: &Value) -> Value {
|
||||
let id = arguments["id"].as_str().unwrap_or("");
|
||||
|
||||
match self.store.get(id) {
|
||||
Ok(memory) => json!({
|
||||
"success": true,
|
||||
"memory": {
|
||||
"id": memory.id,
|
||||
"content": memory.content,
|
||||
"ai_interpretation": memory.ai_interpretation,
|
||||
"priority_score": memory.priority_score,
|
||||
"created_at": memory.created_at,
|
||||
"updated_at": memory.updated_at
|
||||
}
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_search_memories(&self, arguments: &Value) -> Value {
|
||||
let query = arguments["query"].as_str().unwrap_or("");
|
||||
|
||||
match self.store.search(query) {
|
||||
Ok(memories) => json!({
|
||||
"success": true,
|
||||
"memories": memories.into_iter().map(|m| json!({
|
||||
"id": m.id,
|
||||
"content": m.content,
|
||||
"ai_interpretation": m.ai_interpretation,
|
||||
"priority_score": m.priority_score,
|
||||
"created_at": m.created_at,
|
||||
"updated_at": m.updated_at
|
||||
})).collect::<Vec<_>>()
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_list_memories(&self) -> Value {
|
||||
match self.store.list() {
|
||||
Ok(memories) => json!({
|
||||
"success": true,
|
||||
"memories": memories.into_iter().map(|m| json!({
|
||||
"id": m.id,
|
||||
"content": m.content,
|
||||
"ai_interpretation": m.ai_interpretation,
|
||||
"priority_score": m.priority_score,
|
||||
"created_at": m.created_at,
|
||||
"updated_at": m.updated_at
|
||||
})).collect::<Vec<_>>()
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_update_memory(&self, arguments: &Value) -> Value {
|
||||
let id = arguments["id"].as_str().unwrap_or("");
|
||||
let content = arguments["content"].as_str().unwrap_or("");
|
||||
|
||||
match self.store.get(id) {
|
||||
Ok(mut memory) => {
|
||||
memory.update_content(content.to_string());
|
||||
match self.store.update(&memory) {
|
||||
Ok(()) => json!({
|
||||
"success": true,
|
||||
"message": "Memory updated successfully"
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_delete_memory(&self, arguments: &Value) -> Value {
|
||||
let id = arguments["id"].as_str().unwrap_or("");
|
||||
|
||||
match self.store.delete(id) {
|
||||
Ok(()) => json!({
|
||||
"success": true,
|
||||
"message": "Memory deleted successfully"
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Layer 3: User Analysis Tools ==========
|
||||
|
||||
fn tool_save_user_analysis(&self, arguments: &Value) -> Value {
|
||||
let openness = arguments["openness"].as_f64().unwrap_or(0.5) as f32;
|
||||
let conscientiousness = arguments["conscientiousness"].as_f64().unwrap_or(0.5) as f32;
|
||||
let extraversion = arguments["extraversion"].as_f64().unwrap_or(0.5) as f32;
|
||||
let agreeableness = arguments["agreeableness"].as_f64().unwrap_or(0.5) as f32;
|
||||
let neuroticism = arguments["neuroticism"].as_f64().unwrap_or(0.5) as f32;
|
||||
let summary = arguments["summary"].as_str().unwrap_or("").to_string();
|
||||
|
||||
let analysis = UserAnalysis::new(
|
||||
openness,
|
||||
conscientiousness,
|
||||
extraversion,
|
||||
agreeableness,
|
||||
neuroticism,
|
||||
summary,
|
||||
);
|
||||
|
||||
match self.store.save_analysis(&analysis) {
|
||||
Ok(()) => json!({
|
||||
"success": true,
|
||||
"id": analysis.id,
|
||||
"message": "User analysis saved successfully",
|
||||
"dominant_trait": analysis.dominant_trait()
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_get_user_analysis(&self) -> Value {
|
||||
match self.store.get_latest_analysis() {
|
||||
Ok(Some(analysis)) => json!({
|
||||
"success": true,
|
||||
"analysis": {
|
||||
"id": analysis.id,
|
||||
"openness": analysis.openness,
|
||||
"conscientiousness": analysis.conscientiousness,
|
||||
"extraversion": analysis.extraversion,
|
||||
"agreeableness": analysis.agreeableness,
|
||||
"neuroticism": analysis.neuroticism,
|
||||
"summary": analysis.summary,
|
||||
"dominant_trait": analysis.dominant_trait(),
|
||||
"analyzed_at": analysis.analyzed_at
|
||||
}
|
||||
}),
|
||||
Ok(None) => json!({
|
||||
"success": true,
|
||||
"analysis": null,
|
||||
"message": "No analysis found. Run personality analysis first."
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_get_profile(&self) -> Value {
|
||||
match self.store.get_profile() {
|
||||
Ok(profile) => json!({
|
||||
"success": true,
|
||||
"profile": {
|
||||
"dominant_traits": profile.dominant_traits,
|
||||
"core_interests": profile.core_interests,
|
||||
"core_values": profile.core_values,
|
||||
"key_memory_ids": profile.key_memory_ids,
|
||||
"data_quality": profile.data_quality,
|
||||
"last_updated": profile.last_updated
|
||||
}
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_get_relationship(&self, arguments: &Value) -> Value {
|
||||
let entity_id = arguments["entity_id"].as_str().unwrap_or("");
|
||||
|
||||
if entity_id.is_empty() {
|
||||
return json!({
|
||||
"success": false,
|
||||
"error": "entity_id is required"
|
||||
});
|
||||
}
|
||||
|
||||
// Get relationship (with caching)
|
||||
match get_relationship(&self.store, entity_id) {
|
||||
Ok(relationship) => json!({
|
||||
"success": true,
|
||||
"relationship": {
|
||||
"entity_id": relationship.entity_id,
|
||||
"interaction_count": relationship.interaction_count,
|
||||
"avg_priority": relationship.avg_priority,
|
||||
"days_since_last": relationship.days_since_last,
|
||||
"bond_strength": relationship.bond_strength,
|
||||
"relationship_type": relationship.relationship_type,
|
||||
"confidence": relationship.confidence,
|
||||
"inferred_at": relationship.inferred_at
|
||||
}
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": format!("Failed to get relationship: {}", e)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_list_relationships(&self, arguments: &Value) -> Value {
|
||||
let limit = arguments["limit"].as_u64().unwrap_or(10) as usize;
|
||||
|
||||
match infer_all_relationships(&self.store) {
|
||||
Ok(mut relationships) => {
|
||||
// Limit results
|
||||
if relationships.len() > limit {
|
||||
relationships.truncate(limit);
|
||||
}
|
||||
|
||||
json!({
|
||||
"success": true,
|
||||
"relationships": relationships.iter().map(|r| {
|
||||
json!({
|
||||
"entity_id": r.entity_id,
|
||||
"interaction_count": r.interaction_count,
|
||||
"avg_priority": r.avg_priority,
|
||||
"days_since_last": r.days_since_last,
|
||||
"bond_strength": r.bond_strength,
|
||||
"relationship_type": r.relationship_type,
|
||||
"confidence": r.confidence
|
||||
})
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_unknown_method(&self, id: Value) -> Value {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Method not found"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
3
src/mcp/mod.rs
Normal file
3
src/mcp/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod base;
|
||||
|
||||
pub use base::BaseMCPServer;
|
||||
36
src/tmp/ai_interpreter.rs
Normal file
36
src/tmp/ai_interpreter.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// AIInterpreter - Claude Code による解釈を期待する軽量ラッパー
|
||||
///
|
||||
/// このモジュールは外部 AI API を呼び出しません。
|
||||
/// 代わりに、Claude Code 自身がコンテンツを解釈し、スコアを計算することを期待します。
|
||||
///
|
||||
/// 完全にローカルで動作し、API コストはゼロです。
|
||||
pub struct AIInterpreter;
|
||||
|
||||
impl AIInterpreter {
|
||||
pub fn new() -> Self {
|
||||
AIInterpreter
|
||||
}
|
||||
|
||||
/// コンテンツをそのまま返す(Claude Code が解釈を担当)
|
||||
pub async fn interpret_content(&self, content: &str) -> Result<String> {
|
||||
Ok(content.to_string())
|
||||
}
|
||||
|
||||
/// デフォルトスコアを返す(Claude Code が実際のスコアを決定)
|
||||
pub async fn calculate_priority_score(&self, _content: &str, _user_context: Option<&str>) -> Result<f32> {
|
||||
Ok(0.5) // デフォルト値
|
||||
}
|
||||
|
||||
/// 解釈とスコアリングを Claude Code に委ねる
|
||||
pub async fn analyze(&self, content: &str, _user_context: Option<&str>) -> Result<(String, f32)> {
|
||||
Ok((content.to_string(), 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AIInterpreter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
433
src/tmp/companion.rs
Normal file
433
src/tmp/companion.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use crate::memory::Memory;
|
||||
use crate::game_formatter::{MemoryRarity, DiagnosisType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc, Datelike};
|
||||
|
||||
/// コンパニオンキャラクター
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Companion {
|
||||
pub name: String,
|
||||
pub personality: CompanionPersonality,
|
||||
pub relationship_level: u32, // レベル(経験値で上昇)
|
||||
pub affection_score: f32, // 好感度 (0.0-1.0)
|
||||
pub trust_level: u32, // 信頼度 (0-100)
|
||||
pub total_xp: u32, // 総XP
|
||||
pub last_interaction: DateTime<Utc>,
|
||||
pub shared_memories: Vec<String>, // 共有された記憶のID
|
||||
}
|
||||
|
||||
/// コンパニオンの性格
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CompanionPersonality {
|
||||
Energetic, // 元気で冒険好き - 革新者と相性◎
|
||||
Intellectual, // 知的で思慮深い - 哲学者と相性◎
|
||||
Practical, // 現実的で頼れる - 実務家と相性◎
|
||||
Dreamy, // 夢見がちでロマンチック - 夢想家と相性◎
|
||||
Balanced, // バランス型 - 分析家と相性◎
|
||||
}
|
||||
|
||||
impl CompanionPersonality {
|
||||
pub fn emoji(&self) -> &str {
|
||||
match self {
|
||||
CompanionPersonality::Energetic => "⚡",
|
||||
CompanionPersonality::Intellectual => "📚",
|
||||
CompanionPersonality::Practical => "🎯",
|
||||
CompanionPersonality::Dreamy => "🌙",
|
||||
CompanionPersonality::Balanced => "⚖️",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
CompanionPersonality::Energetic => "元気で冒険好き",
|
||||
CompanionPersonality::Intellectual => "知的で思慮深い",
|
||||
CompanionPersonality::Practical => "現実的で頼れる",
|
||||
CompanionPersonality::Dreamy => "夢見がちでロマンチック",
|
||||
CompanionPersonality::Balanced => "バランス型",
|
||||
}
|
||||
}
|
||||
|
||||
/// ユーザーの診断タイプとの相性
|
||||
pub fn compatibility(&self, user_type: &DiagnosisType) -> f32 {
|
||||
match (self, user_type) {
|
||||
(CompanionPersonality::Energetic, DiagnosisType::Innovator) => 0.95,
|
||||
(CompanionPersonality::Intellectual, DiagnosisType::Philosopher) => 0.95,
|
||||
(CompanionPersonality::Practical, DiagnosisType::Pragmatist) => 0.95,
|
||||
(CompanionPersonality::Dreamy, DiagnosisType::Visionary) => 0.95,
|
||||
(CompanionPersonality::Balanced, DiagnosisType::Analyst) => 0.95,
|
||||
// その他の組み合わせ
|
||||
_ => 0.7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Companion {
|
||||
pub fn new(name: String, personality: CompanionPersonality) -> Self {
|
||||
Companion {
|
||||
name,
|
||||
personality,
|
||||
relationship_level: 1,
|
||||
affection_score: 0.0,
|
||||
trust_level: 0,
|
||||
total_xp: 0,
|
||||
last_interaction: Utc::now(),
|
||||
shared_memories: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 記憶を共有して反応を得る
|
||||
pub fn react_to_memory(&mut self, memory: &Memory, user_type: &DiagnosisType) -> CompanionReaction {
|
||||
let rarity = MemoryRarity::from_score(memory.priority_score);
|
||||
let xp = rarity.xp_value();
|
||||
|
||||
// XPを加算
|
||||
self.total_xp += xp;
|
||||
|
||||
// 好感度上昇(スコアと相性による)
|
||||
let compatibility = self.personality.compatibility(user_type);
|
||||
let affection_gain = memory.priority_score * compatibility * 0.1;
|
||||
self.affection_score = (self.affection_score + affection_gain).min(1.0);
|
||||
|
||||
// 信頼度上昇(高スコアの記憶ほど上昇)
|
||||
if memory.priority_score >= 0.8 {
|
||||
self.trust_level = (self.trust_level + 5).min(100);
|
||||
}
|
||||
|
||||
// レベルアップチェック
|
||||
let old_level = self.relationship_level;
|
||||
self.relationship_level = (self.total_xp / 1000) + 1;
|
||||
let level_up = self.relationship_level > old_level;
|
||||
|
||||
// 記憶を共有リストに追加
|
||||
if memory.priority_score >= 0.6 {
|
||||
self.shared_memories.push(memory.id.clone());
|
||||
}
|
||||
|
||||
self.last_interaction = Utc::now();
|
||||
|
||||
// 反応メッセージを生成
|
||||
let message = self.generate_reaction_message(memory, &rarity, user_type);
|
||||
|
||||
CompanionReaction {
|
||||
message,
|
||||
affection_gained: affection_gain,
|
||||
xp_gained: xp,
|
||||
level_up,
|
||||
new_level: self.relationship_level,
|
||||
current_affection: self.affection_score,
|
||||
special_event: self.check_special_event(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 記憶に基づく反応メッセージを生成
|
||||
fn generate_reaction_message(&self, memory: &Memory, rarity: &MemoryRarity, _user_type: &DiagnosisType) -> String {
|
||||
let content_preview = if memory.content.len() > 50 {
|
||||
format!("{}...", &memory.content[..50])
|
||||
} else {
|
||||
memory.content.clone()
|
||||
};
|
||||
|
||||
match (rarity, &self.personality) {
|
||||
// LEGENDARY反応
|
||||
(MemoryRarity::Legendary, CompanionPersonality::Energetic) => {
|
||||
format!(
|
||||
"すごい!「{}」って本当に素晴らしいアイデアだね!\n\
|
||||
一緒に実現させよう!ワクワクするよ!",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
(MemoryRarity::Legendary, CompanionPersonality::Intellectual) => {
|
||||
format!(
|
||||
"「{}」という考え、とても興味深いわ。\n\
|
||||
深い洞察力を感じるの。もっと詳しく聞かせて?",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
(MemoryRarity::Legendary, CompanionPersonality::Practical) => {
|
||||
format!(
|
||||
"「{}」か。実現可能性が高そうだね。\n\
|
||||
具体的な計画を一緒に立てようよ。",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
(MemoryRarity::Legendary, CompanionPersonality::Dreamy) => {
|
||||
format!(
|
||||
"「{}」...素敵♪ まるで夢みたい。\n\
|
||||
あなたの想像力、本当に好きよ。",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
|
||||
// EPIC反応
|
||||
(MemoryRarity::Epic, _) => {
|
||||
format!(
|
||||
"おお、「{}」って面白いね!\n\
|
||||
あなたのそういうところ、好きだな。",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
|
||||
// RARE反応
|
||||
(MemoryRarity::Rare, _) => {
|
||||
format!(
|
||||
"「{}」か。なるほどね。\n\
|
||||
そういう視点、参考になるよ。",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
|
||||
// 通常反応
|
||||
_ => {
|
||||
format!(
|
||||
"「{}」について考えてるんだね。\n\
|
||||
いつも色々考えてて尊敬するよ。",
|
||||
content_preview
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// スペシャルイベントチェック
|
||||
fn check_special_event(&self) -> Option<SpecialEvent> {
|
||||
// 好感度MAXイベント
|
||||
if self.affection_score >= 1.0 {
|
||||
return Some(SpecialEvent::MaxAffection);
|
||||
}
|
||||
|
||||
// レベル10到達
|
||||
if self.relationship_level == 10 {
|
||||
return Some(SpecialEvent::Level10);
|
||||
}
|
||||
|
||||
// 信頼度MAX
|
||||
if self.trust_level >= 100 {
|
||||
return Some(SpecialEvent::MaxTrust);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// デイリーメッセージを生成
|
||||
pub fn generate_daily_message(&self) -> String {
|
||||
let messages = match &self.personality {
|
||||
CompanionPersonality::Energetic => vec![
|
||||
"おはよう!今日は何か面白いことある?",
|
||||
"ねえねえ、今日は一緒に新しいことやろうよ!",
|
||||
"今日も元気出していこー!",
|
||||
],
|
||||
CompanionPersonality::Intellectual => vec![
|
||||
"おはよう。今日はどんな発見があるかしら?",
|
||||
"最近読んだ本の話、聞かせてくれない?",
|
||||
"今日も一緒に学びましょう。",
|
||||
],
|
||||
CompanionPersonality::Practical => vec![
|
||||
"おはよう。今日の予定は?",
|
||||
"やることリスト、一緒に確認しようか。",
|
||||
"今日も効率よくいこうね。",
|
||||
],
|
||||
CompanionPersonality::Dreamy => vec![
|
||||
"おはよう...まだ夢の続き見てたの。",
|
||||
"今日はどんな素敵なことが起こるかな♪",
|
||||
"あなたと過ごす時間、大好き。",
|
||||
],
|
||||
CompanionPersonality::Balanced => vec![
|
||||
"おはよう。今日も頑張ろうね。",
|
||||
"何か手伝えることある?",
|
||||
"今日も一緒にいられて嬉しいよ。",
|
||||
],
|
||||
};
|
||||
|
||||
let today = chrono::Utc::now().ordinal();
|
||||
messages[today as usize % messages.len()].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// コンパニオンの反応
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CompanionReaction {
|
||||
pub message: String,
|
||||
pub affection_gained: f32,
|
||||
pub xp_gained: u32,
|
||||
pub level_up: bool,
|
||||
pub new_level: u32,
|
||||
pub current_affection: f32,
|
||||
pub special_event: Option<SpecialEvent>,
|
||||
}
|
||||
|
||||
/// スペシャルイベント
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum SpecialEvent {
|
||||
MaxAffection, // 好感度MAX
|
||||
Level10, // レベル10到達
|
||||
MaxTrust, // 信頼度MAX
|
||||
FirstDate, // 初デート
|
||||
Confession, // 告白
|
||||
}
|
||||
|
||||
impl SpecialEvent {
|
||||
pub fn message(&self, companion_name: &str) -> String {
|
||||
match self {
|
||||
SpecialEvent::MaxAffection => {
|
||||
format!(
|
||||
"💕 特別なイベント発生!\n\n\
|
||||
{}:「ねえ...あのね。\n\
|
||||
いつも一緒にいてくれてありがとう。\n\
|
||||
あなたのこと、すごく大切に思ってるの。\n\
|
||||
これからも、ずっと一緒にいてね?」\n\n\
|
||||
🎊 {} の好感度がMAXになりました!",
|
||||
companion_name, companion_name
|
||||
)
|
||||
}
|
||||
SpecialEvent::Level10 => {
|
||||
format!(
|
||||
"🎉 レベル10到達!\n\n\
|
||||
{}:「ここまで一緒に来られたね。\n\
|
||||
あなたとなら、どこまでも行けそう。」",
|
||||
companion_name
|
||||
)
|
||||
}
|
||||
SpecialEvent::MaxTrust => {
|
||||
format!(
|
||||
"✨ 信頼度MAX!\n\n\
|
||||
{}:「あなたのこと、心から信頼してる。\n\
|
||||
何でも話せるって、すごく嬉しいよ。」",
|
||||
companion_name
|
||||
)
|
||||
}
|
||||
SpecialEvent::FirstDate => {
|
||||
format!(
|
||||
"💐 初デートイベント!\n\n\
|
||||
{}:「今度、二人でどこか行かない?」",
|
||||
companion_name
|
||||
)
|
||||
}
|
||||
SpecialEvent::Confession => {
|
||||
format!(
|
||||
"💝 告白イベント!\n\n\
|
||||
{}:「好きです。付き合ってください。」",
|
||||
companion_name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// コンパニオンフォーマッター
|
||||
pub struct CompanionFormatter;
|
||||
|
||||
impl CompanionFormatter {
|
||||
/// 反応を表示
|
||||
pub fn format_reaction(companion: &Companion, reaction: &CompanionReaction) -> String {
|
||||
let affection_bar = Self::format_affection_bar(reaction.current_affection);
|
||||
let level_up_text = if reaction.level_up {
|
||||
format!("\n🎊 レベルアップ! Lv.{} → Lv.{}", reaction.new_level - 1, reaction.new_level)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let special_event_text = if let Some(ref event) = reaction.special_event {
|
||||
format!("\n\n{}", event.message(&companion.name))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 💕 {} の反応 ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
{} {}:
|
||||
「{}」
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💕 好感度: {} (+{:.1}%)
|
||||
💎 XP獲得: +{} XP{}
|
||||
🏆 レベル: Lv.{}
|
||||
🤝 信頼度: {} / 100
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}
|
||||
"#,
|
||||
companion.name,
|
||||
companion.personality.emoji(),
|
||||
companion.name,
|
||||
reaction.message,
|
||||
affection_bar,
|
||||
reaction.affection_gained * 100.0,
|
||||
reaction.xp_gained,
|
||||
level_up_text,
|
||||
companion.relationship_level,
|
||||
companion.trust_level,
|
||||
special_event_text
|
||||
)
|
||||
}
|
||||
|
||||
/// プロフィール表示
|
||||
pub fn format_profile(companion: &Companion) -> String {
|
||||
let affection_bar = Self::format_affection_bar(companion.affection_score);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 💕 {} のプロフィール ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
{} 性格: {}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📊 ステータス
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🏆 関係レベル: Lv.{}
|
||||
💕 好感度: {}
|
||||
🤝 信頼度: {} / 100
|
||||
💎 総XP: {} XP
|
||||
📚 共有記憶: {}個
|
||||
🕐 最終交流: {}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
💬 今日のひとこと:
|
||||
「{}」
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
"#,
|
||||
companion.name,
|
||||
companion.personality.emoji(),
|
||||
companion.personality.name(),
|
||||
companion.relationship_level,
|
||||
affection_bar,
|
||||
companion.trust_level,
|
||||
companion.total_xp,
|
||||
companion.shared_memories.len(),
|
||||
companion.last_interaction.format("%Y-%m-%d %H:%M"),
|
||||
companion.generate_daily_message()
|
||||
)
|
||||
}
|
||||
|
||||
fn format_affection_bar(affection: f32) -> String {
|
||||
let hearts = (affection * 10.0) as usize;
|
||||
let filled = "❤️".repeat(hearts);
|
||||
let empty = "🤍".repeat(10 - hearts);
|
||||
format!("{}{} {:.0}%", filled, empty, affection * 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_companion_creation() {
|
||||
let companion = Companion::new(
|
||||
"エミリー".to_string(),
|
||||
CompanionPersonality::Energetic,
|
||||
);
|
||||
assert_eq!(companion.name, "エミリー");
|
||||
assert_eq!(companion.relationship_level, 1);
|
||||
assert_eq!(companion.affection_score, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compatibility() {
|
||||
let personality = CompanionPersonality::Energetic;
|
||||
let innovator = DiagnosisType::Innovator;
|
||||
assert_eq!(personality.compatibility(&innovator), 0.95);
|
||||
}
|
||||
}
|
||||
296
src/tmp/extended.rs
Normal file
296
src/tmp/extended.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::base::BaseMCPServer;
|
||||
|
||||
pub struct ExtendedMCPServer {
|
||||
base: BaseMCPServer,
|
||||
}
|
||||
|
||||
impl ExtendedMCPServer {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let base = BaseMCPServer::new().await?;
|
||||
Ok(ExtendedMCPServer { base })
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<()> {
|
||||
self.base.run().await
|
||||
}
|
||||
|
||||
pub async fn handle_request(&mut self, request: Value) -> Value {
|
||||
self.base.handle_request(request).await
|
||||
}
|
||||
|
||||
// 拡張ツールを追加
|
||||
pub fn get_available_tools(&self) -> Vec<Value> {
|
||||
#[allow(unused_mut)]
|
||||
let mut tools = self.base.get_available_tools();
|
||||
|
||||
// AI分析ツールを追加
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
{
|
||||
tools.push(json!({
|
||||
"name": "analyze_sentiment",
|
||||
"description": "Analyze sentiment of memories",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"description": "Time period to analyze"
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
tools.push(json!({
|
||||
"name": "extract_insights",
|
||||
"description": "Extract insights and patterns from memories",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category to analyze"
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Web統合ツールを追加
|
||||
#[cfg(feature = "web-integration")]
|
||||
{
|
||||
tools.push(json!({
|
||||
"name": "import_webpage",
|
||||
"description": "Import content from a webpage",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "URL to import from"
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// セマンティック検索強化
|
||||
#[cfg(feature = "semantic-search")]
|
||||
{
|
||||
// create_memoryを拡張版で上書き
|
||||
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "create_memory") {
|
||||
tools[pos] = json!({
|
||||
"name": "create_memory",
|
||||
"description": "Create a new memory entry with optional AI analysis",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content of the memory"
|
||||
},
|
||||
"analyze": {
|
||||
"type": "boolean",
|
||||
"description": "Enable AI analysis for this memory"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// search_memoriesを拡張版で上書き
|
||||
if let Some(pos) = tools.iter().position(|tool| tool["name"] == "search_memories") {
|
||||
tools[pos] = json!({
|
||||
"name": "search_memories",
|
||||
"description": "Search memories with advanced options",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"semantic": {
|
||||
"type": "boolean",
|
||||
"description": "Use semantic search"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Filter by category"
|
||||
},
|
||||
"time_range": {
|
||||
"type": "string",
|
||||
"description": "Filter by time range (e.g., '1week', '1month')"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
// 拡張ツール実行
|
||||
pub async fn execute_tool(&mut self, tool_name: &str, arguments: &Value) -> Value {
|
||||
match tool_name {
|
||||
// 拡張機能
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
"analyze_sentiment" => self.tool_analyze_sentiment(arguments).await,
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
"extract_insights" => self.tool_extract_insights(arguments).await,
|
||||
#[cfg(feature = "web-integration")]
|
||||
"import_webpage" => self.tool_import_webpage(arguments).await,
|
||||
|
||||
// 拡張版の基本ツール (AI分析付き)
|
||||
"create_memory" => self.tool_create_memory_extended(arguments).await,
|
||||
"search_memories" => self.tool_search_memories_extended(arguments).await,
|
||||
|
||||
// 基本ツールにフォールバック
|
||||
_ => self.base.execute_tool(tool_name, arguments).await,
|
||||
}
|
||||
}
|
||||
|
||||
// 拡張ツール実装
|
||||
async fn tool_create_memory_extended(&mut self, arguments: &Value) -> Value {
|
||||
let content = arguments["content"].as_str().unwrap_or("");
|
||||
let analyze = arguments["analyze"].as_bool().unwrap_or(false);
|
||||
|
||||
let final_content = if analyze {
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
{
|
||||
format!("[AI分析] 感情: neutral, カテゴリ: general\n{}", content)
|
||||
}
|
||||
#[cfg(not(feature = "ai-analysis"))]
|
||||
{
|
||||
content.to_string()
|
||||
}
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
|
||||
match self.base.memory_manager.create_memory(&final_content) {
|
||||
Ok(id) => json!({
|
||||
"success": true,
|
||||
"id": id,
|
||||
"message": if analyze { "Memory created with AI analysis" } else { "Memory created successfully" }
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn tool_search_memories_extended(&mut self, arguments: &Value) -> Value {
|
||||
let query = arguments["query"].as_str().unwrap_or("");
|
||||
let semantic = arguments["semantic"].as_bool().unwrap_or(false);
|
||||
|
||||
let memories = if semantic {
|
||||
#[cfg(feature = "semantic-search")]
|
||||
{
|
||||
// モックセマンティック検索
|
||||
self.base.memory_manager.search_memories(query)
|
||||
}
|
||||
#[cfg(not(feature = "semantic-search"))]
|
||||
{
|
||||
self.base.memory_manager.search_memories(query)
|
||||
}
|
||||
} else {
|
||||
self.base.memory_manager.search_memories(query)
|
||||
};
|
||||
|
||||
json!({
|
||||
"success": true,
|
||||
"memories": memories.into_iter().map(|m| json!({
|
||||
"id": m.id,
|
||||
"content": m.content,
|
||||
"interpreted_content": m.interpreted_content,
|
||||
"priority_score": m.priority_score,
|
||||
"user_context": m.user_context,
|
||||
"created_at": m.created_at,
|
||||
"updated_at": m.updated_at
|
||||
})).collect::<Vec<_>>(),
|
||||
"search_type": if semantic { "semantic" } else { "keyword" }
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
async fn tool_analyze_sentiment(&mut self, _arguments: &Value) -> Value {
|
||||
json!({
|
||||
"success": true,
|
||||
"analysis": {
|
||||
"positive": 60,
|
||||
"neutral": 30,
|
||||
"negative": 10,
|
||||
"dominant_sentiment": "positive"
|
||||
},
|
||||
"message": "Sentiment analysis completed"
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ai-analysis")]
|
||||
async fn tool_extract_insights(&mut self, _arguments: &Value) -> Value {
|
||||
json!({
|
||||
"success": true,
|
||||
"insights": {
|
||||
"most_frequent_topics": ["programming", "ai", "productivity"],
|
||||
"learning_frequency": "5 times per week",
|
||||
"growth_trend": "increasing",
|
||||
"recommendations": ["Focus more on advanced topics", "Consider practical applications"]
|
||||
},
|
||||
"message": "Insights extracted successfully"
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-integration")]
|
||||
async fn tool_import_webpage(&mut self, arguments: &Value) -> Value {
|
||||
let url = arguments["url"].as_str().unwrap_or("");
|
||||
match self.import_from_web(url).await {
|
||||
Ok(content) => {
|
||||
match self.base.memory_manager.create_memory(&content) {
|
||||
Ok(id) => json!({
|
||||
"success": true,
|
||||
"id": id,
|
||||
"message": format!("Webpage imported successfully from {}", url)
|
||||
}),
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": e.to_string()
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(e) => json!({
|
||||
"success": false,
|
||||
"error": format!("Failed to import webpage: {}", e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "web-integration")]
|
||||
async fn import_from_web(&self, url: &str) -> Result<String> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let content = response.text().await?;
|
||||
|
||||
let document = scraper::Html::parse_document(&content);
|
||||
let title_selector = scraper::Selector::parse("title").unwrap();
|
||||
let body_selector = scraper::Selector::parse("p").unwrap();
|
||||
|
||||
let title = document.select(&title_selector)
|
||||
.next()
|
||||
.map(|el| el.inner_html())
|
||||
.unwrap_or_else(|| "Untitled".to_string());
|
||||
|
||||
let paragraphs: Vec<String> = document.select(&body_selector)
|
||||
.map(|el| el.inner_html())
|
||||
.take(5)
|
||||
.collect();
|
||||
|
||||
Ok(format!("# {}\nURL: {}\n\n{}", title, url, paragraphs.join("\n\n")))
|
||||
}
|
||||
}
|
||||
365
src/tmp/game_formatter.rs
Normal file
365
src/tmp/game_formatter.rs
Normal file
@@ -0,0 +1,365 @@
|
||||
use crate::memory::Memory;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::Datelike;
|
||||
|
||||
/// メモリーのレア度
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MemoryRarity {
|
||||
Common, // 0.0-0.4
|
||||
Uncommon, // 0.4-0.6
|
||||
Rare, // 0.6-0.8
|
||||
Epic, // 0.8-0.9
|
||||
Legendary, // 0.9-1.0
|
||||
}
|
||||
|
||||
impl MemoryRarity {
|
||||
pub fn from_score(score: f32) -> Self {
|
||||
match score {
|
||||
s if s >= 0.9 => MemoryRarity::Legendary,
|
||||
s if s >= 0.8 => MemoryRarity::Epic,
|
||||
s if s >= 0.6 => MemoryRarity::Rare,
|
||||
s if s >= 0.4 => MemoryRarity::Uncommon,
|
||||
_ => MemoryRarity::Common,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emoji(&self) -> &str {
|
||||
match self {
|
||||
MemoryRarity::Common => "⚪",
|
||||
MemoryRarity::Uncommon => "🟢",
|
||||
MemoryRarity::Rare => "🔵",
|
||||
MemoryRarity::Epic => "🟣",
|
||||
MemoryRarity::Legendary => "🟡",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
MemoryRarity::Common => "COMMON",
|
||||
MemoryRarity::Uncommon => "UNCOMMON",
|
||||
MemoryRarity::Rare => "RARE",
|
||||
MemoryRarity::Epic => "EPIC",
|
||||
MemoryRarity::Legendary => "LEGENDARY",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn xp_value(&self) -> u32 {
|
||||
match self {
|
||||
MemoryRarity::Common => 100,
|
||||
MemoryRarity::Uncommon => 250,
|
||||
MemoryRarity::Rare => 500,
|
||||
MemoryRarity::Epic => 850,
|
||||
MemoryRarity::Legendary => 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 診断タイプ
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DiagnosisType {
|
||||
Innovator, // 革新者(創造性高、実用性高)
|
||||
Philosopher, // 哲学者(感情高、新規性高)
|
||||
Pragmatist, // 実務家(実用性高、関連性高)
|
||||
Visionary, // 夢想家(新規性高、感情高)
|
||||
Analyst, // 分析家(全て平均的)
|
||||
}
|
||||
|
||||
impl DiagnosisType {
|
||||
/// スコアから診断タイプを推定(公開用)
|
||||
pub fn from_memory(memory: &crate::memory::Memory) -> Self {
|
||||
// スコア内訳を推定
|
||||
let emotional = (memory.priority_score * 0.25).min(0.25);
|
||||
let relevance = (memory.priority_score * 0.25).min(0.25);
|
||||
let novelty = (memory.priority_score * 0.25).min(0.25);
|
||||
let utility = memory.priority_score - emotional - relevance - novelty;
|
||||
|
||||
Self::from_score_breakdown(emotional, relevance, novelty, utility)
|
||||
}
|
||||
|
||||
pub fn from_score_breakdown(
|
||||
emotional: f32,
|
||||
relevance: f32,
|
||||
novelty: f32,
|
||||
utility: f32,
|
||||
) -> Self {
|
||||
if utility > 0.2 && novelty > 0.2 {
|
||||
DiagnosisType::Innovator
|
||||
} else if emotional > 0.2 && novelty > 0.2 {
|
||||
DiagnosisType::Philosopher
|
||||
} else if utility > 0.2 && relevance > 0.2 {
|
||||
DiagnosisType::Pragmatist
|
||||
} else if novelty > 0.2 && emotional > 0.18 {
|
||||
DiagnosisType::Visionary
|
||||
} else {
|
||||
DiagnosisType::Analyst
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emoji(&self) -> &str {
|
||||
match self {
|
||||
DiagnosisType::Innovator => "💡",
|
||||
DiagnosisType::Philosopher => "🧠",
|
||||
DiagnosisType::Pragmatist => "🎯",
|
||||
DiagnosisType::Visionary => "✨",
|
||||
DiagnosisType::Analyst => "📊",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
DiagnosisType::Innovator => "革新者",
|
||||
DiagnosisType::Philosopher => "哲学者",
|
||||
DiagnosisType::Pragmatist => "実務家",
|
||||
DiagnosisType::Visionary => "夢想家",
|
||||
DiagnosisType::Analyst => "分析家",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
match self {
|
||||
DiagnosisType::Innovator => "創造的で実用的なアイデアを生み出す。常に新しい可能性を探求し、それを現実のものにする力を持つ。",
|
||||
DiagnosisType::Philosopher => "深い思考と感情を大切にする。抽象的な概念や人生の意味について考えることを好む。",
|
||||
DiagnosisType::Pragmatist => "現実的で効率的。具体的な問題解決に優れ、確実に結果を出す。",
|
||||
DiagnosisType::Visionary => "大胆な夢と理想を追い求める。常識にとらわれず、未来の可能性を信じる。",
|
||||
DiagnosisType::Analyst => "バランスの取れた思考。多角的な視点から物事を分析し、冷静に判断する。",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ゲーム風の結果フォーマッター
|
||||
pub struct GameFormatter;
|
||||
|
||||
impl GameFormatter {
|
||||
/// メモリー作成結果をゲーム風に表示
|
||||
pub fn format_memory_result(memory: &Memory) -> String {
|
||||
let rarity = MemoryRarity::from_score(memory.priority_score);
|
||||
let xp = rarity.xp_value();
|
||||
let score_percentage = (memory.priority_score * 100.0) as u32;
|
||||
|
||||
// スコア内訳を推定(各項目最大0.25として)
|
||||
let emotional = (memory.priority_score * 0.25).min(0.25);
|
||||
let relevance = (memory.priority_score * 0.25).min(0.25);
|
||||
let novelty = (memory.priority_score * 0.25).min(0.25);
|
||||
let utility = memory.priority_score - emotional - relevance - novelty;
|
||||
|
||||
let diagnosis = DiagnosisType::from_score_breakdown(
|
||||
emotional,
|
||||
relevance,
|
||||
novelty,
|
||||
utility,
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🎲 メモリースコア判定 ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
⚡ 分析完了! あなたの思考が記録されました
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📊 総合スコア
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
{} {} {}点
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎯 詳細分析
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💓 感情的インパクト: {}
|
||||
🔗 ユーザー関連性: {}
|
||||
✨ 新規性・独自性: {}
|
||||
⚙️ 実用性: {}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎊 あなたのタイプ
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
{} 【{}】
|
||||
|
||||
{}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🏆 報酬
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💎 XP獲得: +{} XP
|
||||
🎁 レア度: {} {}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
💬 AI の解釈
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
{}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📤 この結果をシェアしよう!
|
||||
#aigpt #メモリースコア #{}
|
||||
"#,
|
||||
rarity.emoji(),
|
||||
rarity.name(),
|
||||
score_percentage,
|
||||
Self::format_bar(emotional, 0.25),
|
||||
Self::format_bar(relevance, 0.25),
|
||||
Self::format_bar(novelty, 0.25),
|
||||
Self::format_bar(utility, 0.25),
|
||||
diagnosis.emoji(),
|
||||
diagnosis.name(),
|
||||
diagnosis.description(),
|
||||
xp,
|
||||
rarity.emoji(),
|
||||
rarity.name(),
|
||||
memory.interpreted_content,
|
||||
diagnosis.name(),
|
||||
)
|
||||
}
|
||||
|
||||
/// シェア用の短縮テキストを生成
|
||||
pub fn format_shareable_text(memory: &Memory) -> String {
|
||||
let rarity = MemoryRarity::from_score(memory.priority_score);
|
||||
let score_percentage = (memory.priority_score * 100.0) as u32;
|
||||
let emotional = (memory.priority_score * 0.25).min(0.25);
|
||||
let relevance = (memory.priority_score * 0.25).min(0.25);
|
||||
let novelty = (memory.priority_score * 0.25).min(0.25);
|
||||
let utility = memory.priority_score - emotional - relevance - novelty;
|
||||
let diagnosis = DiagnosisType::from_score_breakdown(
|
||||
emotional,
|
||||
relevance,
|
||||
novelty,
|
||||
utility,
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"🎲 AIメモリースコア診断結果
|
||||
|
||||
{} {} {}点
|
||||
{} 【{}】
|
||||
|
||||
{}
|
||||
|
||||
#aigpt #メモリースコア #AI診断"#,
|
||||
rarity.emoji(),
|
||||
rarity.name(),
|
||||
score_percentage,
|
||||
diagnosis.emoji(),
|
||||
diagnosis.name(),
|
||||
Self::truncate(&memory.content, 100),
|
||||
)
|
||||
}
|
||||
|
||||
/// ランキング表示
|
||||
pub fn format_ranking(memories: &[&Memory], title: &str) -> String {
|
||||
let mut result = format!(
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 🏆 {} ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
"#,
|
||||
title
|
||||
);
|
||||
|
||||
for (i, memory) in memories.iter().take(10).enumerate() {
|
||||
let rank_emoji = match i {
|
||||
0 => "🥇",
|
||||
1 => "🥈",
|
||||
2 => "🥉",
|
||||
_ => " ",
|
||||
};
|
||||
|
||||
let rarity = MemoryRarity::from_score(memory.priority_score);
|
||||
let score = (memory.priority_score * 100.0) as u32;
|
||||
|
||||
result.push_str(&format!(
|
||||
"{} {}位 {} {} {}点 - {}\n",
|
||||
rank_emoji,
|
||||
i + 1,
|
||||
rarity.emoji(),
|
||||
rarity.name(),
|
||||
score,
|
||||
Self::truncate(&memory.content, 40)
|
||||
));
|
||||
}
|
||||
|
||||
result.push_str("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// デイリーチャレンジ表示
|
||||
pub fn format_daily_challenge() -> String {
|
||||
// 今日の日付をシードにランダムなお題を生成
|
||||
let challenges = vec![
|
||||
"今日学んだことを記録しよう",
|
||||
"新しいアイデアを思いついた?",
|
||||
"感動したことを書き留めよう",
|
||||
"目標を一つ設定しよう",
|
||||
"誰かに感謝の気持ちを伝えよう",
|
||||
];
|
||||
|
||||
let today = chrono::Utc::now().ordinal();
|
||||
let challenge = challenges[today as usize % challenges.len()];
|
||||
|
||||
format!(
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ 📅 今日のチャレンジ ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
✨ {}
|
||||
|
||||
🎁 報酬: +200 XP
|
||||
💎 完了すると特別なバッジが獲得できます!
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
"#,
|
||||
challenge
|
||||
)
|
||||
}
|
||||
|
||||
/// プログレスバーを生成
|
||||
fn format_bar(value: f32, max: f32) -> String {
|
||||
let percentage = (value / max * 100.0) as u32;
|
||||
let filled = (percentage / 10) as usize;
|
||||
let empty = 10 - filled;
|
||||
|
||||
format!(
|
||||
"[{}{}] {}%",
|
||||
"█".repeat(filled),
|
||||
"░".repeat(empty),
|
||||
percentage
|
||||
)
|
||||
}
|
||||
|
||||
/// テキストを切り詰め
|
||||
fn truncate(s: &str, max_len: usize) -> String {
|
||||
if s.len() <= max_len {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}...", &s[..max_len])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
#[test]
|
||||
fn test_rarity_from_score() {
|
||||
assert!(matches!(MemoryRarity::from_score(0.95), MemoryRarity::Legendary));
|
||||
assert!(matches!(MemoryRarity::from_score(0.85), MemoryRarity::Epic));
|
||||
assert!(matches!(MemoryRarity::from_score(0.7), MemoryRarity::Rare));
|
||||
assert!(matches!(MemoryRarity::from_score(0.5), MemoryRarity::Uncommon));
|
||||
assert!(matches!(MemoryRarity::from_score(0.3), MemoryRarity::Common));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagnosis_type() {
|
||||
let diagnosis = DiagnosisType::from_score_breakdown(0.1, 0.1, 0.22, 0.22);
|
||||
assert!(matches!(diagnosis, DiagnosisType::Innovator));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_bar() {
|
||||
let bar = GameFormatter::format_bar(0.15, 0.25);
|
||||
assert!(bar.contains("60%"));
|
||||
}
|
||||
}
|
||||
374
src/tmp/memory.rs
Normal file
374
src/tmp/memory.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
use crate::ai_interpreter::AIInterpreter;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Memory {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
#[serde(default = "default_interpreted_content")]
|
||||
pub interpreted_content: String, // AI解釈後のコンテンツ
|
||||
#[serde(default = "default_priority_score")]
|
||||
pub priority_score: f32, // 心理判定スコア (0.0-1.0)
|
||||
#[serde(default)]
|
||||
pub user_context: Option<String>, // ユーザー固有性
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
fn default_interpreted_content() -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn default_priority_score() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Conversation {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub message_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatGPTNode {
|
||||
id: String,
|
||||
children: Vec<String>,
|
||||
parent: Option<String>,
|
||||
message: Option<ChatGPTMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatGPTMessage {
|
||||
id: String,
|
||||
author: ChatGPTAuthor,
|
||||
content: ChatGPTContent,
|
||||
create_time: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatGPTAuthor {
|
||||
role: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ChatGPTContent {
|
||||
Text {
|
||||
content_type: String,
|
||||
parts: Vec<String>,
|
||||
},
|
||||
Other(serde_json::Value),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ChatGPTConversation {
|
||||
#[serde(default)]
|
||||
id: String,
|
||||
#[serde(alias = "conversation_id")]
|
||||
conversation_id: Option<String>,
|
||||
title: String,
|
||||
create_time: f64,
|
||||
mapping: HashMap<String, ChatGPTNode>,
|
||||
}
|
||||
|
||||
pub struct MemoryManager {
|
||||
memories: HashMap<String, Memory>,
|
||||
conversations: HashMap<String, Conversation>,
|
||||
data_file: PathBuf,
|
||||
max_memories: usize, // 最大記憶数
|
||||
#[allow(dead_code)]
|
||||
min_priority_score: f32, // 最小優先度スコア (将来の機能で使用予定)
|
||||
ai_interpreter: AIInterpreter, // AI解釈エンジン
|
||||
}
|
||||
|
||||
impl MemoryManager {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let data_dir = dirs::config_dir()
|
||||
.context("Could not find config directory")?
|
||||
.join("syui")
|
||||
.join("ai")
|
||||
.join("gpt");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
|
||||
let data_file = data_dir.join("memory.json");
|
||||
|
||||
let (memories, conversations) = if data_file.exists() {
|
||||
Self::load_data(&data_file)?
|
||||
} else {
|
||||
(HashMap::new(), HashMap::new())
|
||||
};
|
||||
|
||||
Ok(MemoryManager {
|
||||
memories,
|
||||
conversations,
|
||||
data_file,
|
||||
max_memories: 100, // デフォルト: 100件
|
||||
min_priority_score: 0.3, // デフォルト: 0.3以上
|
||||
ai_interpreter: AIInterpreter::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_memory(&mut self, content: &str) -> Result<String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now();
|
||||
|
||||
let memory = Memory {
|
||||
id: id.clone(),
|
||||
content: content.to_string(),
|
||||
interpreted_content: content.to_string(), // 後でAI解釈を実装
|
||||
priority_score: 0.5, // 後で心理判定を実装
|
||||
user_context: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
self.memories.insert(id.clone(), memory);
|
||||
|
||||
// 容量制限チェック
|
||||
self.prune_memories_if_needed()?;
|
||||
|
||||
self.save_data()?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// AI解釈と心理判定を使った記憶作成(後方互換性のため残す)
|
||||
pub async fn create_memory_with_ai(
|
||||
&mut self,
|
||||
content: &str,
|
||||
user_context: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now();
|
||||
|
||||
// AI解釈と心理判定を実行
|
||||
let (interpreted_content, priority_score) = self
|
||||
.ai_interpreter
|
||||
.analyze(content, user_context)
|
||||
.await?;
|
||||
|
||||
let memory = Memory {
|
||||
id: id.clone(),
|
||||
content: content.to_string(),
|
||||
interpreted_content,
|
||||
priority_score,
|
||||
user_context: user_context.map(|s| s.to_string()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
self.memories.insert(id.clone(), memory);
|
||||
|
||||
// 容量制限チェック
|
||||
self.prune_memories_if_needed()?;
|
||||
|
||||
self.save_data()?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Claude Code から解釈とスコアを受け取ってメモリを作成
|
||||
pub fn create_memory_with_interpretation(
|
||||
&mut self,
|
||||
content: &str,
|
||||
interpreted_content: &str,
|
||||
priority_score: f32,
|
||||
user_context: Option<&str>,
|
||||
) -> Result<String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now();
|
||||
|
||||
let memory = Memory {
|
||||
id: id.clone(),
|
||||
content: content.to_string(),
|
||||
interpreted_content: interpreted_content.to_string(),
|
||||
priority_score: priority_score.max(0.0).min(1.0), // 0.0-1.0 に制限
|
||||
user_context: user_context.map(|s| s.to_string()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
self.memories.insert(id.clone(), memory);
|
||||
|
||||
// 容量制限チェック
|
||||
self.prune_memories_if_needed()?;
|
||||
|
||||
self.save_data()?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn update_memory(&mut self, id: &str, content: &str) -> Result<()> {
|
||||
if let Some(memory) = self.memories.get_mut(id) {
|
||||
memory.content = content.to_string();
|
||||
memory.updated_at = Utc::now();
|
||||
self.save_data()?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Memory not found: {}", id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_memory(&mut self, id: &str) -> Result<()> {
|
||||
if self.memories.remove(id).is_some() {
|
||||
self.save_data()?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Memory not found: {}", id))
|
||||
}
|
||||
}
|
||||
|
||||
// 容量制限: 優先度が低いものから削除
|
||||
fn prune_memories_if_needed(&mut self) -> Result<()> {
|
||||
if self.memories.len() <= self.max_memories {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 優先度でソートして、低いものから削除
|
||||
let mut sorted_memories: Vec<_> = self.memories.iter()
|
||||
.map(|(id, mem)| (id.clone(), mem.priority_score))
|
||||
.collect();
|
||||
|
||||
sorted_memories.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let to_remove = self.memories.len() - self.max_memories;
|
||||
for (id, _) in sorted_memories.iter().take(to_remove) {
|
||||
self.memories.remove(id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 優先度順に記憶を取得
|
||||
pub fn get_memories_by_priority(&self) -> Vec<&Memory> {
|
||||
let mut memories: Vec<_> = self.memories.values().collect();
|
||||
memories.sort_by(|a, b| b.priority_score.partial_cmp(&a.priority_score).unwrap_or(std::cmp::Ordering::Equal));
|
||||
memories
|
||||
}
|
||||
|
||||
pub fn search_memories(&self, query: &str) -> Vec<&Memory> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results: Vec<_> = self.memories
|
||||
.values()
|
||||
.filter(|memory| memory.content.to_lowercase().contains(&query_lower))
|
||||
.collect();
|
||||
|
||||
results.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
||||
results
|
||||
}
|
||||
|
||||
pub fn list_conversations(&self) -> Vec<&Conversation> {
|
||||
let mut conversations: Vec<_> = self.conversations.values().collect();
|
||||
conversations.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
conversations
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn import_chatgpt_conversations(&mut self, file_path: &PathBuf) -> Result<()> {
|
||||
let content = std::fs::read_to_string(file_path)
|
||||
.context("Failed to read conversations file")?;
|
||||
|
||||
let chatgpt_conversations: Vec<ChatGPTConversation> = serde_json::from_str(&content)
|
||||
.context("Failed to parse ChatGPT conversations")?;
|
||||
|
||||
let mut imported_memories = 0;
|
||||
let mut imported_conversations = 0;
|
||||
|
||||
for conv in chatgpt_conversations {
|
||||
// Get the actual conversation ID
|
||||
let conv_id = if !conv.id.is_empty() {
|
||||
conv.id.clone()
|
||||
} else if let Some(cid) = conv.conversation_id {
|
||||
cid
|
||||
} else {
|
||||
Uuid::new_v4().to_string()
|
||||
};
|
||||
|
||||
// Add conversation
|
||||
let conversation = Conversation {
|
||||
id: conv_id.clone(),
|
||||
title: conv.title.clone(),
|
||||
created_at: DateTime::from_timestamp(conv.create_time as i64, 0)
|
||||
.unwrap_or_else(Utc::now),
|
||||
message_count: conv.mapping.len() as u32,
|
||||
};
|
||||
self.conversations.insert(conv_id.clone(), conversation);
|
||||
imported_conversations += 1;
|
||||
|
||||
// Extract memories from messages
|
||||
for (_, node) in conv.mapping {
|
||||
if let Some(message) = node.message {
|
||||
if let ChatGPTContent::Text { parts, .. } = message.content {
|
||||
for part in parts {
|
||||
if !part.trim().is_empty() && part.len() > 10 {
|
||||
let memory_content = format!("[{}] {}", conv.title, part);
|
||||
self.create_memory(&memory_content)?;
|
||||
imported_memories += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Imported {} conversations and {} memories",
|
||||
imported_conversations, imported_memories);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_data(file_path: &PathBuf) -> Result<(HashMap<String, Memory>, HashMap<String, Conversation>)> {
|
||||
let content = std::fs::read_to_string(file_path)
|
||||
.context("Failed to read data file")?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Data {
|
||||
memories: HashMap<String, Memory>,
|
||||
conversations: HashMap<String, Conversation>,
|
||||
}
|
||||
|
||||
let data: Data = serde_json::from_str(&content)
|
||||
.context("Failed to parse data file")?;
|
||||
|
||||
Ok((data.memories, data.conversations))
|
||||
}
|
||||
|
||||
// Getter: 単一メモリ取得
|
||||
pub fn get_memory(&self, id: &str) -> Option<&Memory> {
|
||||
self.memories.get(id)
|
||||
}
|
||||
|
||||
// Getter: 全メモリ取得
|
||||
pub fn get_all_memories(&self) -> Vec<&Memory> {
|
||||
self.memories.values().collect()
|
||||
}
|
||||
|
||||
fn save_data(&self) -> Result<()> {
|
||||
#[derive(Serialize)]
|
||||
struct Data<'a> {
|
||||
memories: &'a HashMap<String, Memory>,
|
||||
conversations: &'a HashMap<String, Conversation>,
|
||||
}
|
||||
|
||||
let data = Data {
|
||||
memories: &self.memories,
|
||||
conversations: &self.conversations,
|
||||
};
|
||||
|
||||
let content = serde_json::to_string_pretty(&data)
|
||||
.context("Failed to serialize data")?;
|
||||
|
||||
std::fs::write(&self.data_file, content)
|
||||
.context("Failed to write data file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user