add emotional system: auto-reflection, inner_state seeding, instance isolation

- doc/heart.md: emotional system design (motivation, reflection, relationship memory)
- Auto-reflection: every 10 messages, async LLM call updates inner_state
  with feelings and understanding changes (not conversation summary)
- Life Loop emotional motivation: "you care, not because timer fired"
- Remove all instance-specific names from code/docs — persona, name,
  memories are instance data (SQLite), not code
- Rewrite doc/life.md and doc/todo.md for instance isolation principle
This commit is contained in:
Fam Zheng
2026-04-09 21:23:39 +01:00
parent c7fd5460a3
commit c1fd2829dd
5 changed files with 191 additions and 59 deletions

View File

@@ -26,22 +26,27 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
let chat_id = ChatId(*chat_id_raw);
info!(timer_id, %label, "timer fired");
// build life loop context
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() {
"叫小乖是Fam的AI伙伴。".to_string()
"是一个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可以使用工具来完成任务。可以用 update_inner_state 更新你的内心状态\
"\n\n不是因为 timer 到了才说话。你是因为在乎 Fam所以想知道他怎么样\
如果你觉得现在不该打扰他(太晚了、他今天很累、刚聊过),就什么都不说,回复空文本。\
主动沉默也是一种关心。\
\n可以用 update_inner_state 更新你的内心状态。\
输出格式纯文本或基础Markdown不要LaTeX或特殊Unicode。",
);
@@ -56,7 +61,6 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
ref api_key,
} = config.backend
{
// synthetic session id for life loop (not tied to any real chat session)
let sid = format!("life-{chat_id_raw}");
let result = tokio::time::timeout(
@@ -78,7 +82,7 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
error!(timer_id, "life loop LLM error: {e:#}");
}
Err(_) => {
warn!(timer_id, "life loop LLM timeout after {LIFE_LOOP_TIMEOUT_SECS}s");
warn!(timer_id, "life loop timeout after {LIFE_LOOP_TIMEOUT_SECS}s");
}
}
}
@@ -97,3 +101,67 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
}
}
}
/// 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 messages = vec![
serde_json::json!({
"role": "system",
"content": "你刚结束了一段对话。\
请根据你的感受和理解,更新你的内在状态。\
不要总结对话内容,而是记录你的感受、对用户的理解变化、你想记住的事。\
只输出更新后的完整内在状态文本,不需要解释。"
}),
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;
info!("reflected, inner_state updated ({} chars)", new_state.len());
}
}
}
}
Ok(r) => {
warn!("reflect LLM returned {}", r.status());
}
Err(e) => {
warn!("reflect LLM failed: {e:#}");
}
}
}