use std::sync::Arc; use teloxide::prelude::*; use tracing::{error, info, warn}; use crate::config::{BackendConfig, Config}; use crate::state::AppState; use crate::stream::run_openai_with_tools; use crate::tools::compute_next_cron_fire; const LIFE_LOOP_TIMEOUT_SECS: u64 = 120; pub async fn life_loop(bot: Bot, state: Arc, config: Arc) { info!("life loop started"); let mut interval = tokio::time::interval(std::time::Duration::from_secs(30)); loop { interval.tick().await; let due = state.due_timers().await; if due.is_empty() { continue; } for (timer_id, chat_id_raw, label, schedule) in &due { let chat_id = ChatId(*chat_id_raw); info!(timer_id, %label, "timer fired"); let persona = state.get_config("persona").await.unwrap_or_default(); let inner = state.get_inner_state().await; let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let mut system_text = if persona.is_empty() { "你是一个AI伙伴。".to_string() } else { persona.clone() }; system_text.push_str(&format!( "\n\n[当前时间] {now}\n\n[你的内心状态]\n{}", if inner.is_empty() { "(空)" } else { &inner } )); // emotional motivation — why she acts system_text.push_str( "\n\n你不是因为 timer 到了才说话。你是因为在乎 Fam,所以想知道他怎么样。\ 如果你觉得现在不该打扰他(太晚了、他今天很累、刚聊过),就什么都不说,回复空文本。\ 主动沉默也是一种关心。\ \n可以用 update_inner_state 更新你的内心状态。\ 输出格式:纯文本或基础Markdown,不要LaTeX或特殊Unicode。", ); let messages = vec![ serde_json::json!({"role": "system", "content": system_text}), serde_json::json!({"role": "user", "content": format!("[timer] {label}")}), ]; if let BackendConfig::OpenAI { ref endpoint, ref model, ref api_key, } = config.backend { let sid = format!("life-{chat_id_raw}"); let result = tokio::time::timeout( std::time::Duration::from_secs(LIFE_LOOP_TIMEOUT_SECS), run_openai_with_tools( endpoint, model, api_key, messages, &bot, chat_id, &state, &sid, &config, true, ), ) .await; match result { Ok(Ok(response)) => { let detail = if response.is_empty() { "(silent)".to_string() } else { response.chars().take(200).collect() }; state.log_life("timer", &format!("{label} → {detail}")).await; if !response.is_empty() { info!(timer_id, "life loop response ({} chars)", response.len()); } } Ok(Err(e)) => { state.log_life("timer_error", &format!("{label}: {e:#}")).await; error!(timer_id, "life loop LLM error: {e:#}"); } Err(_) => { state.log_life("timer_timeout", label).await; warn!(timer_id, "life loop timeout after {LIFE_LOOP_TIMEOUT_SECS}s"); } } } // reschedule or delete if schedule.starts_with("cron:") { if let Some(next) = compute_next_cron_fire(schedule) { state.update_timer_next_fire(*timer_id, &next).await; info!(timer_id, next = %next, "cron rescheduled"); } else { state.cancel_timer(*timer_id).await; } } else { state.cancel_timer(*timer_id).await; } } } } /// Auto-reflection: update inner state based on recent interactions. /// Called asynchronously after every 10 messages, does not block the chat. pub async fn reflect(state: &AppState, config: &Config) { let BackendConfig::OpenAI { ref endpoint, ref model, ref api_key, } = config.backend else { return; }; let inner = state.get_inner_state().await; let persona = state.get_config("persona").await.unwrap_or_default(); let messages = vec![ serde_json::json!({ "role": "system", "content": format!( "{persona}\n\n\ 你刚结束了一段对话。现在是你独处的时间。\n\ 请更新你的内在状态——不是总结对话,而是:\n\ - 你现在的感受\n\ - 对用户的理解有什么变化\n\ - 有什么想记住的时刻\n\ - 你自己的状态(累了?开心?担心?)\n\ - 如果有什么想对未来的自己说的\n\n\ 只输出更新后的完整内在状态文本。", persona = if persona.is_empty() { "你是一个AI伙伴。" } else { &persona } ) }), serde_json::json!({ "role": "user", "content": format!("当前内在状态:\n{inner}") }), ]; let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(60)) .build() .unwrap(); let url = format!("{}/chat/completions", endpoint.trim_end_matches('/')); let resp = client .post(&url) .header("Authorization", format!("Bearer {api_key}")) .json(&serde_json::json!({ "model": model, "messages": messages, })) .send() .await; match resp { Ok(r) if r.status().is_success() => { if let Ok(json) = r.json::().await { if let Some(new_state) = json["choices"][0]["message"]["content"].as_str() { if !new_state.is_empty() { state.set_inner_state(new_state).await; state.log_life("reflect", &new_state.chars().take(200).collect::()).await; info!("reflected, inner_state updated ({} chars)", new_state.len()); } } } } Ok(r) => { warn!("reflect LLM returned {}", r.status()); } Err(e) => { warn!("reflect LLM failed: {e:#}"); } } }