add life_log, memory_slots updated_at, enhanced reflection and system prompt
This commit is contained in:
parent
c1fd2829dd
commit
b093b96a46
27
src/life.rs
27
src/life.rs
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
33
src/state.rs
33
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(())
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user