187 lines
6.8 KiB
Rust
187 lines
6.8 KiB
Rust
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<AppState>, config: Arc<Config>) {
|
||
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::<serde_json::Value>().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::<String>()).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:#}");
|
||
}
|
||
}
|
||
}
|