add life_log, memory_slots updated_at, enhanced reflection and system prompt

This commit is contained in:
Fam Zheng
2026-04-10 21:09:04 +01:00
parent c1fd2829dd
commit b093b96a46
4 changed files with 68 additions and 18 deletions

View File

@@ -74,14 +74,22 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
match result { match result {
Ok(Ok(response)) => { 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() { if !response.is_empty() {
info!(timer_id, "life loop response ({} chars)", response.len()); info!(timer_id, "life loop response ({} chars)", response.len());
} }
} }
Ok(Err(e)) => { Ok(Err(e)) => {
state.log_life("timer_error", &format!("{label}: {e:#}")).await;
error!(timer_id, "life loop LLM error: {e:#}"); error!(timer_id, "life loop LLM error: {e:#}");
} }
Err(_) => { Err(_) => {
state.log_life("timer_timeout", label).await;
warn!(timer_id, "life loop timeout after {LIFE_LOOP_TIMEOUT_SECS}s"); 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 inner = state.get_inner_state().await;
let persona = state.get_config("persona").await.unwrap_or_default();
let messages = vec![ let messages = vec![
serde_json::json!({ serde_json::json!({
"role": "system", "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!({ serde_json::json!({
"role": "user", "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 let Some(new_state) = json["choices"][0]["message"]["content"].as_str() {
if !new_state.is_empty() { if !new_state.is_empty() {
state.set_inner_state(new_state).await; 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()); info!("reflected, inner_state updated ({} chars)", new_state.len());
} }
} }

View File

@@ -344,8 +344,8 @@ async fn handle_inner(
if memory_slots.is_empty() { if memory_slots.is_empty() {
diag.push_str("(empty)\n\n"); diag.push_str("(empty)\n\n");
} else { } else {
for (nr, content) in &memory_slots { for (nr, content, updated_at) in &memory_slots {
diag.push_str(&format!("- `[{nr}]` {content}\n")); diag.push_str(&format!("- `[{nr}]` {content} ({updated_at})\n"));
} }
diag.push('\n'); diag.push('\n');
} }

View File

@@ -80,7 +80,8 @@ impl AppState {
); );
CREATE TABLE IF NOT EXISTS memory_slots ( CREATE TABLE IF NOT EXISTS memory_slots (
slot_nr INTEGER PRIMARY KEY CHECK(slot_nr BETWEEN 0 AND 99), 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 ( CREATE TABLE IF NOT EXISTS timers (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -95,7 +96,13 @@ impl AppState {
id INTEGER PRIMARY KEY CHECK(id = 1), id INTEGER PRIMARY KEY CHECK(id = 1),
content TEXT NOT NULL DEFAULT '' 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"); .expect("init db schema");
@@ -104,6 +111,10 @@ impl AppState {
"ALTER TABLE messages ADD COLUMN created_at TEXT NOT NULL DEFAULT ''", "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()); 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 { pub async fn add_timer(&self, chat_id: i64, label: &str, schedule: &str, next_fire: &str) -> i64 {
let db = self.db.lock().await; let db = self.db.lock().await;
db.execute( 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 db = self.db.lock().await;
let mut stmt = db 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(); .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() .unwrap()
.filter_map(|r| r.ok()) .filter_map(|r| r.ok())
.collect() .collect()
@@ -348,8 +367,8 @@ impl AppState {
} }
let db = self.db.lock().await; let db = self.db.lock().await;
db.execute( db.execute(
"INSERT INTO memory_slots (slot_nr, content) VALUES (?1, ?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", ON CONFLICT(slot_nr) DO UPDATE SET content = ?2, updated_at = datetime('now', 'localtime')",
rusqlite::params![slot_nr, content], rusqlite::params![slot_nr, content],
)?; )?;
Ok(()) Ok(())

View File

@@ -691,7 +691,7 @@ pub async fn run_openai_streaming(
Ok(accumulated) 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() { let mut text = if persona.is_empty() {
String::from("你是一个AI助手。") String::from("你是一个AI助手。")
} else { } else {
@@ -708,14 +708,26 @@ pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, S
); );
if !memory_slots.is_empty() { if !memory_slots.is_empty() {
text.push_str("\n\n## 持久记忆(跨会话保留)\n"); text.push_str(
for (nr, content) in memory_slots { "\n\n## 持久记忆(跨会话保留,你可以用 update_memory 工具管理)\n\
text.push_str(&format!("[{nr}] {content}\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() { 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); text.push_str(inner_state);
} }