From b093b96a462f82efcf6c7f7546f02938e07a6b1f Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Fri, 10 Apr 2026 21:09:04 +0100 Subject: [PATCH] add life_log, memory_slots updated_at, enhanced reflection and system prompt --- src/life.rs | 27 +++++++++++++++++++++++---- src/main.rs | 4 ++-- src/state.rs | 33 ++++++++++++++++++++++++++------- src/stream.rs | 22 +++++++++++++++++----- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/life.rs b/src/life.rs index 4004eff..f4a4b2d 100644 --- a/src/life.rs +++ b/src/life.rs @@ -74,14 +74,22 @@ pub async fn life_loop(bot: Bot, state: Arc, config: Arc) { 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"); } } @@ -116,13 +124,23 @@ pub async fn reflect(state: &AppState, config: &Config) { 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": "你刚结束了一段对话。\ - 请根据你的感受和理解,更新你的内在状态。\ - 不要总结对话内容,而是记录你的感受、对用户的理解变化、你想记住的事。\ - 只输出更新后的完整内在状态文本,不需要解释。" + "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", @@ -152,6 +170,7 @@ pub async fn reflect(state: &AppState, config: &Config) { 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()); } } diff --git a/src/main.rs b/src/main.rs index 6d5f979..6a26f21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -344,8 +344,8 @@ async fn handle_inner( if memory_slots.is_empty() { diag.push_str("(empty)\n\n"); } else { - for (nr, content) in &memory_slots { - diag.push_str(&format!("- `[{nr}]` {content}\n")); + for (nr, content, updated_at) in &memory_slots { + diag.push_str(&format!("- `[{nr}]` {content} ({updated_at})\n")); } diag.push('\n'); } diff --git a/src/state.rs b/src/state.rs index 685e8e5..53a90e7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -80,7 +80,8 @@ impl AppState { ); CREATE TABLE IF NOT EXISTS memory_slots ( slot_nr INTEGER PRIMARY KEY CHECK(slot_nr BETWEEN 0 AND 99), - content TEXT NOT NULL DEFAULT '' + content TEXT NOT NULL DEFAULT '', + updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS timers ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -95,7 +96,13 @@ impl AppState { id INTEGER PRIMARY KEY CHECK(id = 1), content TEXT NOT NULL DEFAULT '' ); - INSERT OR IGNORE INTO inner_state (id, content) VALUES (1, '');", + INSERT OR IGNORE INTO inner_state (id, content) VALUES (1, ''); + CREATE TABLE IF NOT EXISTS life_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event TEXT NOT NULL, + detail TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) + );", ) .expect("init db schema"); @@ -104,6 +111,10 @@ impl AppState { "ALTER TABLE messages ADD COLUMN created_at TEXT NOT NULL DEFAULT ''", [], ); + let _ = conn.execute( + "ALTER TABLE memory_slots ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''", + [], + ); info!("opened db {}", db_path.display()); @@ -256,6 +267,14 @@ impl AppState { ); } + pub async fn log_life(&self, event: &str, detail: &str) { + let db = self.db.lock().await; + let _ = db.execute( + "INSERT INTO life_log (event, detail) VALUES (?1, ?2)", + rusqlite::params![event, detail], + ); + } + pub async fn add_timer(&self, chat_id: i64, label: &str, schedule: &str, next_fire: &str) -> i64 { let db = self.db.lock().await; db.execute( @@ -328,12 +347,12 @@ impl AppState { ); } - pub async fn get_memory_slots(&self) -> Vec<(i32, String)> { + pub async fn get_memory_slots(&self) -> Vec<(i32, String, String)> { let db = self.db.lock().await; let mut stmt = db - .prepare("SELECT slot_nr, content FROM memory_slots WHERE content != '' ORDER BY slot_nr") + .prepare("SELECT slot_nr, content, updated_at FROM memory_slots WHERE content != '' ORDER BY slot_nr") .unwrap(); - stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?))) + stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))) .unwrap() .filter_map(|r| r.ok()) .collect() @@ -348,8 +367,8 @@ impl AppState { } let db = self.db.lock().await; db.execute( - "INSERT INTO memory_slots (slot_nr, content) VALUES (?1, ?2) \ - ON CONFLICT(slot_nr) DO UPDATE SET content = ?2", + "INSERT INTO memory_slots (slot_nr, content, updated_at) VALUES (?1, ?2, datetime('now', 'localtime')) \ + ON CONFLICT(slot_nr) DO UPDATE SET content = ?2, updated_at = datetime('now', 'localtime')", rusqlite::params![slot_nr, content], )?; Ok(()) diff --git a/src/stream.rs b/src/stream.rs index a4e9214..d47bc55 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -691,7 +691,7 @@ pub async fn run_openai_streaming( Ok(accumulated) } -pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, String)], inner_state: &str) -> serde_json::Value { +pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, String, String)], inner_state: &str) -> serde_json::Value { let mut text = if persona.is_empty() { String::from("你是一个AI助手。") } else { @@ -708,14 +708,26 @@ pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, S ); if !memory_slots.is_empty() { - text.push_str("\n\n## 持久记忆(跨会话保留)\n"); - for (nr, content) in memory_slots { - text.push_str(&format!("[{nr}] {content}\n")); + text.push_str( + "\n\n## 持久记忆(跨会话保留,你可以用 update_memory 工具管理)\n\ + 槽位 0-9: 事实(位置/偏好/习惯)\n\ + 槽位 10-19: 重要时刻\n\ + 槽位 20-29: 情感经验\n\ + 槽位 30-39: 你自己的成长\n\ + 槽位 40-99: 自由使用\n\ + 发现重要信息时主动更新,过时的要清理。\n\n", + ); + for (nr, content, updated_at) in memory_slots { + if updated_at.is_empty() { + text.push_str(&format!("[{nr}] {content}\n")); + } else { + text.push_str(&format!("[{nr}] {content} ({updated_at})\n")); + } } } if !inner_state.is_empty() { - text.push_str("\n\n## 你的内在状态\n"); + text.push_str("\n\n## 你的内在状态(你可以用 update_inner_state 工具更新)\n"); text.push_str(inner_state); }