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 {
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::<String>()).await;
info!("reflected, inner_state updated ({} chars)", new_state.len());
}
}

View File

@ -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');
}

View File

@ -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(())

View File

@ -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);
}