diff --git a/Cargo.toml b/Cargo.toml index 0400ebd..9d6bea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ seahorse = "*" rusqlite = { version = "0.29", features = ["serde_json"] } shellexpand = "*" fs_extra = "1.3" +rand = "0.9.1" diff --git a/example.json b/example.json index 6df5aef..0876118 100644 --- a/example.json +++ b/example.json @@ -26,5 +26,17 @@ "おはよう!今日もがんばろう!", "ねえ、話したいことがあるの。" ] + }, + "last_interaction": "2025-05-21T23:15:00Z", + "memory": { + "recent_messages": [], + "long_term_notes": [] + }, + "metrics": { + "trust": 0.5, + "intimacy": 0.5, + "energy": 0.5, + "can_send": true, + "last_updated": "2025-05-21T15:52:06.590981Z" } -} \ No newline at end of file +} diff --git a/src/chat.rs b/src/chat.rs index d6bb18b..72844e2 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4,7 +4,7 @@ use std::process::Command; use serde::Deserialize; use seahorse::Context; use crate::config::ConfigPaths; -use crate::metrics::{load_metrics, save_metrics, update_metrics_decay}; +use crate::metrics::{load_user_data, save_user_data, update_metrics_decay}; #[derive(Debug, Clone, PartialEq)] pub enum Provider { @@ -46,33 +46,28 @@ pub fn ask_chat(c: &Context, question: &str) -> Option { let config = ConfigPaths::new(); let base_dir = config.base_dir.join("mcp"); let script_path = base_dir.join("scripts/ask.py"); - let metrics_path = config.base_dir.join("metrics.json"); - let mut metrics = load_metrics(&metrics_path); + let user_path = config.base_dir.join("user.json"); - update_metrics_decay(&mut metrics); - - if !metrics.can_send { - println!("❌ 送信条件を満たしていないため、AIメッセージは送信されません。"); - return None; - } + let mut user = load_user_data(&user_path); + user.metrics = update_metrics_decay(); + // Python 実行パス let python_path = if cfg!(target_os = "windows") { base_dir.join(".venv/Scripts/python.exe") } else { base_dir.join(".venv/bin/python") }; + // 各種オプション let ollama_host = c.string_flag("host").ok(); let ollama_model = c.string_flag("model").ok(); let provider_str = c.string_flag("provider").unwrap_or_else(|_| "ollama".to_string()); let provider = Provider::from_str(&provider_str).unwrap_or(Provider::Ollama); - //let api_key = c.string_flag("api-key").ok().or_else(|| crate::metrics::load_openai_api_key()); - let api_key = c.string_flag("api-key") - .ok() - .or_else(|| load_openai_api_key()); + let api_key = c.string_flag("api-key").ok().or_else(load_openai_api_key); println!("🔍 使用プロバイダー: {}", provider.as_str()); + // Python コマンド準備 let mut command = Command::new(python_path); command.arg(script_path).arg(question); @@ -93,12 +88,10 @@ pub fn ask_chat(c: &Context, question: &str) -> Option { if output.status.success() { let response = String::from_utf8_lossy(&output.stdout).to_string(); - println!("💬 {}", response); + user.metrics.intimacy += 0.01; + user.metrics.last_updated = chrono::Utc::now(); + save_user_data(&user_path, &user); - // 応答後のメトリクス更新 - metrics.intimacy += 0.02; - metrics.last_updated = chrono::Utc::now(); - save_metrics(&metrics, &metrics_path); Some(response) } else { eprintln!( diff --git a/src/commands/mcp.rs b/src/commands/mcp.rs index 46338c8..db03704 100644 --- a/src/commands/mcp.rs +++ b/src/commands/mcp.rs @@ -9,6 +9,7 @@ use crate::chat::ask_chat; use crate::git::{git_init, git_status}; use crate::config::ConfigPaths; use crate::commands::git_repo::read_all_git_files; +use crate::metrics::{load_user_data, save_user_data}; pub fn mcp_setup() { let config = ConfigPaths::new(); @@ -160,7 +161,8 @@ fn chat_cmd() -> Command { ) .action(|c: &Context| { let config = ConfigPaths::new(); - + let user_path = config.data_file("json"); + let mut user = load_user_data(&user_path); // repoがある場合は、コードベース読み込みモード if let Ok(repo_url) = c.string_flag("repo") { let repo_base = config.base_dir.join("repos"); @@ -188,14 +190,24 @@ fn chat_cmd() -> Command { } else { eprintln!("❗ 提案が取得できませんでした"); } + return; } // 通常のチャット処理(repoが指定されていない場合) match c.args.get(0) { Some(question) => { - if let Some(response) = ask_chat(c, question) { - println!("💬 応答:\n{}", response); + let response = ask_chat(c, question); + + if let Some(ref text) = response { + println!("💬 応答:\n{}", text); + // 返答内容に基づいて増減(返答の感情解析) + if text.contains("thank") || text.contains("great") { + user.metrics.trust += 0.05; + } else if text.contains("hate") || text.contains("bad") { + user.metrics.trust -= 0.05; + } + save_user_data(&user_path, &user); } else { eprintln!("❗ 応答が取得できませんでした"); } diff --git a/src/commands/scheduler.rs b/src/commands/scheduler.rs index fe185f0..78cccaa 100644 --- a/src/commands/scheduler.rs +++ b/src/commands/scheduler.rs @@ -1,29 +1,105 @@ // src/commands/scheduler.rs - use seahorse::{Command, Context}; use std::thread; use std::time::Duration; -use chrono::Local; +use chrono::{Local, Utc, Timelike}; +use crate::metrics::{load_user_data, save_user_data}; +use crate::config::ConfigPaths; +use crate::chat::ask_chat; +use rand::prelude::*; +use rand::rng; +fn send_scheduled_message() { + let config = ConfigPaths::new(); + let user_path = config.data_file("json"); + let mut user = load_user_data(&user_path); + + if !user.metrics.can_send { + println!("🚫 送信条件を満たしていないため、スケジュール送信スキップ"); + return; + } + + if let Some(schedule_str) = &user.messaging.schedule_time { + let now = Local::now(); + let target: Vec<&str> = schedule_str.split(':').collect(); + + if target.len() != 2 { + println!("⚠️ schedule_time形式が無効です: {}", schedule_str); + return; + } + + let (sh, sm) = (target[0].parse::(), target[1].parse::()); + if let (Ok(sh), Ok(sm)) = (sh, sm) { + if now.hour() == sh && now.minute() == sm { + if let Some(msg) = user.messaging.templates.choose(&mut rng()) { + println!("💬 自動送信メッセージ: {}", msg); + let dummy_context = Context::new(vec![], None, "".to_string()); + ask_chat(&dummy_context, msg); + user.metrics.intimacy += 0.03; + save_user_data(&user_path, &user); + } + } + } + } +} pub fn scheduler_cmd() -> Command { Command::new("scheduler") .usage("scheduler [interval_sec]") .alias("s") + .description("定期的に送信条件をチェックし、自発的なメッセージ送信を試みる") .action(|c: &Context| { let interval = c.args.get(0) .and_then(|s| s.parse::().ok()) - .unwrap_or(60); // デフォルト: 60秒ごと + .unwrap_or(3600); // デフォルト: 1時間(テストしやすく) - println!("⏳ スケジューラー開始({interval}秒ごと)..."); + println!("⏳ スケジューラー開始({}秒ごと)...", interval); loop { - let now = Local::now(); - println!("🔁 タスク実行中: {}", now.format("%Y-%m-%d %H:%M:%S")); - - // ここで talk_cmd や save_cmd の内部処理を呼ぶ感じ - // たとえば load_config → AI更新 → print とか + let config = ConfigPaths::new(); + let user_path = config.data_file("json"); + let mut user = load_user_data(&user_path); + let now = Utc::now(); + let elapsed = now.signed_duration_since(user.metrics.last_updated); + let hours = elapsed.num_minutes() as f32 / 60.0; + + let speed_factor = if hours > 48.0 { + 2.0 + } else if hours > 24.0 { + 1.5 + } else { + 1.0 + }; + + user.metrics.trust = (user.metrics.trust - 0.01 * speed_factor).clamp(0.0, 1.0); + user.metrics.intimacy = (user.metrics.intimacy - 0.01 * speed_factor).clamp(0.0, 1.0); + user.metrics.energy = (user.metrics.energy - 0.01 * speed_factor).clamp(0.0, 1.0); + + user.metrics.can_send = + user.metrics.trust >= 0.5 && + user.metrics.intimacy >= 0.5 && + user.metrics.energy >= 0.5; + + user.metrics.last_updated = now; + + if user.metrics.can_send { + println!("💡 AIメッセージ送信条件を満たしています(信頼:{:.2}, 親密:{:.2}, エネルギー:{:.2})", + user.metrics.trust, + user.metrics.intimacy, + user.metrics.energy + ); + send_scheduled_message(); + } else { + println!("🤫 条件未達成のため送信スキップ: trust={:.2}, intimacy={:.2}, energy={:.2}", + user.metrics.trust, + user.metrics.intimacy, + user.metrics.energy + ); + } + + save_user_data(&user_path, &user); thread::sleep(Duration::from_secs(interval)); } }) } + diff --git a/src/metrics.rs b/src/metrics.rs index a2f67b7..54c743b 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -1,10 +1,12 @@ // src/metrics.rs -use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; -#[derive(Serialize, Deserialize, Debug)] +use crate::config::ConfigPaths; + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Metrics { pub trust: f32, pub intimacy: f32, @@ -13,86 +15,129 @@ pub struct Metrics { pub last_updated: DateTime, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Personality { + pub kind: String, + pub strength: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Relationship { + pub trust: f32, + pub intimacy: f32, + pub curiosity: f32, + pub threshold: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Environment { + pub luck_today: f32, + pub luck_history: Vec, + pub level: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Messaging { + pub enabled: bool, + pub schedule_time: Option, + pub decay_rate: f32, + pub templates: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Memory { + pub recent_messages: Vec, + pub long_term_notes: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserData { + pub personality: Personality, + pub relationship: Relationship, + pub environment: Environment, + pub messaging: Messaging, + pub last_interaction: DateTime, + pub memory: Memory, + pub metrics: Metrics, +} + impl Metrics { - fn default() -> Self { - Self { - trust: 0.5, - intimacy: 0.5, - energy: 0.5, - last_updated: chrono::Utc::now(), - can_send: true, - } - } - /// パラメータの減衰処理を行い、can_sendを更新する pub fn decay(&mut self) { let now = Utc::now(); - let elapsed = now.signed_duration_since(self.last_updated); - let hours = elapsed.num_minutes() as f32 / 60.0; - + let hours = (now - self.last_updated).num_minutes() as f32 / 60.0; self.trust = decay_param(self.trust, hours); self.intimacy = decay_param(self.intimacy, hours); self.energy = decay_param(self.energy, hours); - - self.last_updated = now; self.can_send = self.trust >= 0.5 && self.intimacy >= 0.5 && self.energy >= 0.5; - } - - /// JSONからMetricsを読み込み、減衰し、保存して返す - pub fn load_and_decay(path: &Path) -> Self { - let mut metrics = if path.exists() { - let content = fs::read_to_string(path).expect("metrics.jsonの読み込みに失敗しました"); - serde_json::from_str(&content).expect("JSONパース失敗") - } else { - println!("⚠️ metrics.json が存在しないため、新しく作成します。"); - Metrics::default() - }; - - metrics.decay(); - metrics.save(path); - metrics - } - - /// Metricsを保存する - pub fn save(&self, path: &Path) { - let data = serde_json::to_string_pretty(self).expect("JSON変換失敗"); - fs::write(path, data).expect("metrics.jsonの書き込みに失敗しました"); + self.last_updated = now; } } -/// 単一のパラメータを減衰させる +pub fn load_user_data(path: &Path) -> UserData { + let config = ConfigPaths::new(); + let example_path = Path::new("example.json"); + config.ensure_file_exists("json", example_path); + + if !path.exists() { + return UserData { + personality: Personality { + kind: "positive".into(), + strength: 0.8, + }, + relationship: Relationship { + trust: 0.2, + intimacy: 0.6, + curiosity: 0.5, + threshold: 1.5, + }, + environment: Environment { + luck_today: 0.9, + luck_history: vec![0.9, 0.9, 0.9], + level: 1, + }, + messaging: Messaging { + enabled: true, + schedule_time: Some("08:00".to_string()), + decay_rate: 0.1, + templates: vec![ + "おはよう!今日もがんばろう!".to_string(), + "ねえ、話したいことがあるの。".to_string(), + ], + }, + last_interaction: Utc::now(), + memory: Memory { + recent_messages: vec![], + long_term_notes: vec![], + }, + metrics: Metrics { + trust: 0.5, + intimacy: 0.5, + energy: 0.5, + can_send: true, + last_updated: Utc::now(), + }, + }; + } + + let content = fs::read_to_string(path).expect("user.json の読み込みに失敗しました"); + serde_json::from_str(&content).expect("user.json のパースに失敗しました") +} + +pub fn save_user_data(path: &Path, data: &UserData) { + let content = serde_json::to_string_pretty(data).expect("user.json のシリアライズ失敗"); + fs::write(path, content).expect("user.json の書き込みに失敗しました"); +} + +pub fn update_metrics_decay() -> Metrics { + let config = ConfigPaths::new(); + let path = config.base_dir.join("user.json"); + let mut data = load_user_data(&path); + data.metrics.decay(); + save_user_data(&path, &data); + data.metrics +} + fn decay_param(value: f32, hours: f32) -> f32 { - let decay_rate = 0.01; // 時間ごとの減衰率 + let decay_rate = 0.05; (value * (1.0f32 - decay_rate).powf(hours)).clamp(0.0, 1.0) } - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - - #[test] - fn test_decay_behavior() { - let mut metrics = Metrics { - trust: 1.0, - intimacy: 1.0, - energy: 1.0, - can_send: true, - last_updated: Utc::now() - Duration::hours(12), - }; - metrics.decay(); - assert!(metrics.trust < 1.0); - assert!(metrics.can_send); // 減衰後でも0.5以上あるならtrue - } -} - -pub fn load_metrics(path: &Path) -> Metrics { - Metrics::load_and_decay(path) -} - -pub fn save_metrics(metrics: &Metrics, path: &Path) { - metrics.save(path) -} - -pub fn update_metrics_decay(metrics: &mut Metrics) { - metrics.decay() -}