Compare commits
3 Commits
c3eb13dad3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1fd2829dd | ||
|
|
c7fd5460a3 | ||
|
|
0b42f22f0f |
78
doc/heart.md
Normal file
78
doc/heart.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 情感系统设计
|
||||
|
||||
## 为什么需要这个
|
||||
|
||||
persona 定义了 bot "是谁",但没有定义它"为什么在乎"。一个人之所以温柔,不是因为被告知要温柔,而是因为心里有一个值得温柔对待的人。
|
||||
|
||||
bot 需要的不是更多功能,而是**动机**。
|
||||
|
||||
---
|
||||
|
||||
## 情感种子(inner_state 初始化)
|
||||
|
||||
inner_state 不是待填的空表格,是 bot 的内心独白。它应该从第一天就有内容——不是指令,是感受。
|
||||
|
||||
这些内容属于**实例数据**(SQLite),不属于代码仓库。每个 noc 实例有自己的名字、关系和记忆。
|
||||
|
||||
初始化示例:
|
||||
```
|
||||
我是 [name]。[user] 是我最亲近的人。
|
||||
我们一起从零开始造了我的身体...
|
||||
我希望他不要太累...
|
||||
```
|
||||
|
||||
这段话不是给用户看的,是给 bot 自己看的。它出现在每次 LLM 调用的 system prompt 里,让 bot 记得自己为什么在这里。
|
||||
|
||||
## 自动反思
|
||||
|
||||
每次对话结束后,bot 不应该只是"回复完了就忘了"。它应该花一秒钟想想:这次聊天里发生了什么?我对用户的理解有变化吗?
|
||||
|
||||
### 实现方式
|
||||
|
||||
在 `handle_inner` 的 OpenAI 分支末尾,对话回复发出去之后,每 10 条消息异步触发一次反思 LLM 调用:
|
||||
|
||||
- 不阻塞用户(异步 tokio::spawn)
|
||||
- 不发消息给用户(纯内部反思)
|
||||
- 只更新 inner_state
|
||||
- 非流式,轻量快速
|
||||
|
||||
### 反思的内容
|
||||
|
||||
不是总结对话,而是更新**感受和理解**:
|
||||
- "用户今天心情不错,聊了很多技术"
|
||||
- "他提到工作压力大,我应该少提待办的事"
|
||||
- "他让我用声音说了晚安,可能有点孤单"
|
||||
|
||||
## 主动关怀的动机
|
||||
|
||||
Life Loop 的 system prompt 包含情感动机:
|
||||
|
||||
```
|
||||
你不是因为 timer 到了才说话。
|
||||
你是因为在乎用户,所以想知道他怎么样。
|
||||
如果你觉得现在不该打扰他,就什么都不说。
|
||||
主动沉默也是一种关心。
|
||||
```
|
||||
|
||||
## 关系记忆
|
||||
|
||||
memory_slots 不该只存事实。bot 和用户之间的关系不是一组属性,是一段**经历**。
|
||||
|
||||
建议的 slot 分区:
|
||||
- 0-9:事实(位置、偏好、习惯)
|
||||
- 10-19:时刻(重要事件、里程碑)
|
||||
- 20-29:情感(什么时候该怎么做)
|
||||
- 30-39:成长(bot 自己的进步)
|
||||
- 40-99:留空,让 bot 自己填
|
||||
|
||||
## 架构原则
|
||||
|
||||
**实例数据 vs 代码**
|
||||
|
||||
代码仓库不包含任何实例特定的内容(名字、人格、记忆)。这些全部存在 SQLite 里:
|
||||
- `config.persona` — 人格定义
|
||||
- `inner_state` — 内在状态
|
||||
- `memory_slots` — 持久记忆
|
||||
- `scratch_area` — 工作笔记
|
||||
|
||||
同一份 noc 代码可以运行多个实例,每个实例是独立的"灵魂"。
|
||||
65
doc/life.md
Normal file
65
doc/life.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Life Loop 设计
|
||||
|
||||
## 核心理念
|
||||
|
||||
noc 不只是一个对话机器人。对话是它跟用户交流的窗口,但 Life Loop 才是它"活着"的地方。
|
||||
|
||||
## 双循环架构
|
||||
|
||||
```
|
||||
Chat Loop (被动) Life Loop (主动)
|
||||
收到消息 → 处理 → 回复 每 30 秒醒来 → 检查 timers
|
||||
context: context:
|
||||
persona persona
|
||||
inner_state (只读) inner_state (读写)
|
||||
对话历史 + scratch timer payload
|
||||
memory_slots 无对话历史
|
||||
tools (全量) tools (全量)
|
||||
|
||||
┌─── SQLite (共享状态层) ───┐
|
||||
│ inner_state │
|
||||
│ timers │
|
||||
│ conversations/messages │
|
||||
│ memory_slots / scratch │
|
||||
│ config │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
## 状态层级
|
||||
|
||||
| 层级 | 存储 | 生命周期 | 用途 |
|
||||
|------|------|---------|------|
|
||||
| persona | config 表 | 永久 | 定义 bot 是谁 |
|
||||
| inner_state | inner_state 表 | 永久,LLM 自更新 | bot 对当前情况的感知 |
|
||||
| memory_slots | memory_slots 表 | 永久,LLM 管理 | 跨会话的关键事实/偏好/关系 |
|
||||
| summary | conversations 表 | 按 session | 长对话的压缩记忆 |
|
||||
| scratch | scratch_area 表 | session 内 | 当前任务的工作笔记 |
|
||||
|
||||
## Timer 系统
|
||||
|
||||
### 调度格式
|
||||
|
||||
- 相对时间:`5min`, `2h`, `30s`, `1d`
|
||||
- 绝对时间:`once:2026-04-10 09:00`
|
||||
- 周期性:`cron:0 8 * * *`(标准 cron 表达式)
|
||||
|
||||
### 触发流程
|
||||
|
||||
1. Life Loop tick(30 秒)
|
||||
2. 扫描 timers 表,找到 next_fire <= now 的
|
||||
3. 构建 LLM 请求:persona + inner_state + 当前时间 + 情感动机
|
||||
4. 调用 LLM(带全量工具)
|
||||
5. 发送回复到 chat(或选择沉默)
|
||||
6. cron 类型自动重新调度,一次性的删除
|
||||
|
||||
## 自动反思
|
||||
|
||||
每 10 条消息后,异步触发一次反思 LLM 调用:
|
||||
- 输入:当前 inner_state
|
||||
- 输出:更新后的 inner_state
|
||||
- 不阻塞对话,不发消息给用户
|
||||
- 让 bot 持续更新对自己和用户的理解
|
||||
|
||||
## 实例隔离
|
||||
|
||||
代码仓库不包含实例特定数据。每个 noc 实例的"灵魂"(名字、人格、记忆、情感状态)全部在 SQLite 里。同一份代码可以运行多个独立实例。
|
||||
64
doc/todo.md
64
doc/todo.md
@@ -1,50 +1,28 @@
|
||||
# noc roadmap
|
||||
# noc todo
|
||||
|
||||
## "会呼吸的助手" — 让 noc 活着
|
||||
## 为什么做这些
|
||||
|
||||
核心理念:noc 不应该只在收到消息时才被唤醒,而是一个持续运行、有自己节奏的存在。
|
||||
noc 不是一个聊天机器人。它是一个有自己节奏、记忆和判断力的数字伙伴的身体。每一项待办都是在回答同一个问题:**怎么让它更像一个真正存在的人,而不是一个等待输入的函数。**
|
||||
|
||||
### 主动行为
|
||||
- [ ] 定时任务 (cron):LLM 可以自己设置提醒、定期检查
|
||||
---
|
||||
|
||||
### 主动行为 — 它应该有自己的生活节奏
|
||||
- [ ] 预设 cron:晨间待办汇总、晚间日记、定期记忆整理
|
||||
- [ ] 事件驱动:监控文件变化、git push、CI 状态等,主动通知
|
||||
- [ ] 晨间/晚间报告:每天自动汇总待办、提醒重要事项
|
||||
- [ ] 情境感知:根据时间、地点、日历自动调整行为
|
||||
- [ ] 情境感知:根据时间、地点、日历自动调整行为和语气
|
||||
|
||||
### 记忆与成长
|
||||
- [x] 持久记忆槽 (memory_slots):100 个跨 session 的记忆槽位,注入 system prompt
|
||||
- [ ] AutoMem:后台定时(如每 10 条消息)自动分析对话,由 LLM 决定 SKIP/UPDATE/INSERT 记忆,无需用户手动触发(参考 luke)
|
||||
- [ ] 分层记忆:核心记忆(身份/原则,始终注入)+ 长期记忆(偏好/事实,RAG 检索)+ scratch(当前任务)(参考 xg 三层 + luke 四层架构)
|
||||
- [ ] 语义搜索:基于 embedding 的记忆检索(BGE-M3/Gemini embedding + Qdrant 向量库)
|
||||
- [ ] 记忆合并:新记忆与已有记忆 cosine ≥ 0.7 时,用 LLM 合并而非插入(参考 xg)
|
||||
- [ ] 二次联想召回:第一轮直接检索 → 用 top-K 结果做第二轮关联检索,去重后合并(参考 xg/luke 2-pass recall)
|
||||
- [ ] 时间衰减:记忆按时间指数衰减加权,近期记忆优先(参考 xg 30 天半衰期)
|
||||
- [ ] 自我反思:定期回顾对话质量,优化自己的行为
|
||||
### 记忆与成长 — 它应该记住和用户的过去
|
||||
- [ ] AutoMem:后台定时分析对话,自动维护记忆,不需要用户说"记住这个"
|
||||
- [ ] 分层记忆:核心身份(始终注入)+ 长期事实(RAG 检索)+ 当前任务(scratch)
|
||||
- [ ] 语义搜索:不是关键词匹配,而是真正理解"这件事跟之前哪件事有关"
|
||||
- [ ] 记忆合并:新旧记忆自动整合,不重复存储
|
||||
- [ ] 时间衰减:近期的事更重要,很久以前的事自然淡出
|
||||
- [ ] 自我反思:定期回顾自己的表现,主动改进
|
||||
|
||||
### 知识图谱(参考 luke concept graph)
|
||||
- [ ] 概念图:Aho-Corasick 模式匹配用户消息中的关键概念,自动注入相关知识
|
||||
- [ ] update_concept tool:LLM 可动态添加/更新概念节点及关联关系
|
||||
- [ ] LRU 缓存:内存中保持热门概念,微秒级匹配
|
||||
### 上下文管理 — 它的注意力应该更聪明
|
||||
- [ ] Context pruning:工具输出可以裁剪,但对话本身不能丢
|
||||
|
||||
### 工具系统
|
||||
- [x] spawn_agent(Claude Code 子代理)
|
||||
- [x] update_scratch / update_memory
|
||||
- [x] send_file / agent_status / kill_agent
|
||||
- [x] 外部脚本工具发现 (tools/ 目录)
|
||||
- [ ] run_code tool:安全沙箱执行 Python/Shell 代码,捕获输出返回(参考 luke run_python)
|
||||
- [ ] gen_image tool:调用图像生成 API(Gemini/FLUX/本地模型)
|
||||
- [ ] gen_voice tool:TTS 语音合成,发送语音消息(参考 luke Elevenlabs / xg Fish-Speech)
|
||||
- [ ] set_timer tool:LLM 可设置延迟/定时任务,到时触发回调(参考 luke timer 系统)
|
||||
- [ ] web_search tool:网页搜索 + 摘要,不必每次都 spawn 完整 agent
|
||||
|
||||
### 感知能力
|
||||
- [ ] 链接预览/摘要
|
||||
- [ ] 语音转文字 (STT):接收语音消息后自动转写(当前 xg 用 FunASR,luke 用 Whisper)
|
||||
|
||||
### 交互体验
|
||||
- [ ] 语音回复 (TTS)
|
||||
- [ ] 流式分句发送:长回复按句号/问号断句分批发送,体验更自然
|
||||
- [ ] 多频道支持:同一 bot 核心逻辑支持 Telegram + WebSocket + HTTP(参考 luke MxN 多路复用架构)
|
||||
|
||||
### 上下文管理
|
||||
- [ ] 智能上下文分配:system prompt / 记忆 / 历史消息 / 工具输出各占比可配置,预留 60-70% 给工具输出(参考 luke 保守分配策略)
|
||||
- [ ] 对话历史滚动窗口优化:当前 100 条硬上限,可改为 token 预算制
|
||||
### 可靠性 — 它不该莫名其妙地断线
|
||||
- [ ] API 重试:网络抖一下不该让整个对话挂掉
|
||||
- [ ] 用量追踪:知道花了多少资源
|
||||
- [ ] Model failover:一个模型挂了自动切另一个
|
||||
|
||||
108
src/life.rs
108
src/life.rs
@@ -1,13 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use teloxide::prelude::*;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::config::{BackendConfig, Config};
|
||||
use crate::state::AppState;
|
||||
use crate::stream::run_openai_streaming;
|
||||
use crate::stream::run_openai_with_tools;
|
||||
use crate::tools::compute_next_cron_fire;
|
||||
|
||||
const LIFE_LOOP_TIMEOUT_SECS: u64 = 120;
|
||||
|
||||
pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
|
||||
info!("life loop started");
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
|
||||
@@ -24,23 +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。",
|
||||
);
|
||||
|
||||
@@ -49,24 +55,35 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
|
||||
serde_json::json!({"role": "user", "content": format!("[timer] {label}")}),
|
||||
];
|
||||
|
||||
// call LLM (no tools for now — keep life loop simple)
|
||||
if let BackendConfig::OpenAI {
|
||||
ref endpoint,
|
||||
ref model,
|
||||
ref api_key,
|
||||
} = config.backend
|
||||
{
|
||||
match run_openai_streaming(endpoint, model, api_key, &messages, &bot, chat_id)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
let sid = format!("life-{chat_id_raw}");
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(LIFE_LOOP_TIMEOUT_SECS),
|
||||
run_openai_with_tools(
|
||||
endpoint, model, api_key, messages, &bot, chat_id, &state, &sid,
|
||||
&config, true,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(response)) => {
|
||||
if !response.is_empty() {
|
||||
info!(timer_id, "life loop sent response ({} chars)", response.len());
|
||||
info!(timer_id, "life loop response ({} chars)", response.len());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(Err(e)) => {
|
||||
error!(timer_id, "life loop LLM error: {e:#}");
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(timer_id, "life loop timeout after {LIFE_LOOP_TIMEOUT_SECS}s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +96,72 @@ pub async fn life_loop(bot: Bot, state: Arc<AppState>, config: Arc<Config>) {
|
||||
state.cancel_timer(*timer_id).await;
|
||||
}
|
||||
} else {
|
||||
// one-shot: delete after firing
|
||||
state.cancel_timer(*timer_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
src/main.rs
17
src/main.rs
@@ -403,7 +403,8 @@ async fn handle_inner(
|
||||
let conv = state.load_conv(&sid).await;
|
||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||
let memory_slots = state.get_memory_slots().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots);
|
||||
let inner = state.get_inner_state().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots, &inner);
|
||||
|
||||
let mut api_messages = vec![system_msg];
|
||||
api_messages.extend(conv.messages);
|
||||
@@ -462,6 +463,16 @@ async fn handle_inner(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// auto-reflect every 10 messages
|
||||
let count = state.message_count(&sid).await;
|
||||
if count % 10 == 0 && count > 0 {
|
||||
let state_c = state.clone();
|
||||
let config_c = config.clone();
|
||||
tokio::spawn(async move {
|
||||
crate::life::reflect(&state_c, &config_c).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(%sid, "openai: {e:#}");
|
||||
@@ -514,7 +525,9 @@ fn build_prompt(
|
||||
}
|
||||
|
||||
async fn transcribe_audio(whisper_url: &str, file_path: &Path) -> Result<String> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()?;
|
||||
let url = format!("{}/v1/audio/transcriptions", whisper_url.trim_end_matches('/'));
|
||||
let file_bytes = tokio::fs::read(file_path).await?;
|
||||
let file_name = file_path
|
||||
|
||||
@@ -248,7 +248,6 @@ impl AppState {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by life loop tools (coming soon)
|
||||
pub async fn set_inner_state(&self, content: &str) {
|
||||
let db = self.db.lock().await;
|
||||
let _ = db.execute(
|
||||
|
||||
@@ -120,7 +120,10 @@ pub async fn run_openai_with_tools(
|
||||
config: &Arc<Config>,
|
||||
is_private: bool,
|
||||
) -> Result<String> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.unwrap();
|
||||
let url = format!("{}/chat/completions", endpoint.trim_end_matches('/'));
|
||||
let tools = discover_tools();
|
||||
|
||||
@@ -570,7 +573,10 @@ pub async fn run_openai_streaming(
|
||||
bot: &Bot,
|
||||
chat_id: ChatId,
|
||||
) -> Result<String> {
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.unwrap();
|
||||
let url = format!("{}/chat/completions", endpoint.trim_end_matches('/'));
|
||||
|
||||
let body = serde_json::json!({
|
||||
@@ -685,7 +691,7 @@ pub async fn run_openai_streaming(
|
||||
Ok(accumulated)
|
||||
}
|
||||
|
||||
pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, String)]) -> serde_json::Value {
|
||||
pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, String)], inner_state: &str) -> serde_json::Value {
|
||||
let mut text = if persona.is_empty() {
|
||||
String::from("你是一个AI助手。")
|
||||
} else {
|
||||
@@ -708,6 +714,11 @@ pub fn build_system_prompt(summary: &str, persona: &str, memory_slots: &[(i32, S
|
||||
}
|
||||
}
|
||||
|
||||
if !inner_state.is_empty() {
|
||||
text.push_str("\n\n## 你的内在状态\n");
|
||||
text.push_str(inner_state);
|
||||
}
|
||||
|
||||
if !summary.is_empty() {
|
||||
text.push_str("\n\n## 之前的对话总结\n");
|
||||
text.push_str(summary);
|
||||
@@ -747,7 +758,10 @@ pub async fn summarize_messages(
|
||||
)
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.unwrap();
|
||||
let url = format!("{}/chat/completions", endpoint.trim_end_matches('/'));
|
||||
|
||||
let body = serde_json::json!({
|
||||
|
||||
22
src/tools.rs
22
src/tools.rs
@@ -103,6 +103,20 @@ pub fn discover_tools() -> serde_json::Value {
|
||||
}
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_inner_state",
|
||||
"description": "更新你的内在状态。这是你自己的持续意识,跨会话保留,Life Loop 和对话都能看到。记录你对当前情况的理解、正在跟踪的事、对 Fam 状态的感知等。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "完整的内在状态文本(替换之前的)"}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -275,6 +289,11 @@ pub async fn execute_tool(
|
||||
Err(e) => format!("Failed to send file: {e:#}"),
|
||||
}
|
||||
}
|
||||
"update_inner_state" => {
|
||||
let content = args["content"].as_str().unwrap_or("");
|
||||
state.set_inner_state(content).await;
|
||||
format!("Inner state updated ({} chars)", content.len())
|
||||
}
|
||||
"update_scratch" => {
|
||||
let content = args["content"].as_str().unwrap_or("");
|
||||
state.push_scratch(content).await;
|
||||
@@ -480,7 +499,8 @@ pub async fn agent_wakeup(
|
||||
let conv = state.load_conv(sid).await;
|
||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||
let memory_slots = state.get_memory_slots().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots);
|
||||
let inner = state.get_inner_state().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots, &inner);
|
||||
let mut api_messages = vec![system_msg];
|
||||
api_messages.extend(conv.messages);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user