AnyBot/src/memory.rs

108 lines
5.3 KiB
Rust
Raw Normal View History

// 記憶、技能、範本 (the original: user bot memories, agent skills and template skills/bots).
// - Memories belong to a bot: facts it was asked to keep across chats.
// - Skills are shared, named procedures; the bot sees their names and loads one when it needs it.
// - Templates start a new bot with a ready role and instructions.
use serde::{Deserialize, Serialize};
/// Most memories a bot keeps; the oldest go first (estimated).
pub const MAX_MEMORIES: usize = 200;
/// Longest memory (estimated).
pub const MAX_MEMORY_CHARS: usize = 500;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Skill {
pub name: String,
pub description: String,
pub instructions: String,
}
pub struct Template {
pub name: &'static str,
pub description: &'static str,
pub instructions: &'static str,
pub shape: &'static str,
pub color: &'static str,
}
pub const TEMPLATES: [Template; 5] = [
Template { name: "研究助理", description: "上網找資料、整理重點、附上來源", instructions: "先釐清問題,再用 web_fetch 或瀏覽器找至少兩個可靠來源。回答先給結論,再列重點,每個重點附來源網址。不確定的地方直接說不確定。", shape: "circle", color: "green" },
Template { name: "寫作夥伴", description: "幫你構思、改寫和潤稿", instructions: "先問清楚讀者和語氣。改稿時保留作者的意思,列出改了什麼和為什麼。用繁體中文、台灣用語。", shape: "cloud", color: "purple" },
Template { name: "電腦助手", description: "在自己的電腦上跑指令、整理檔案、操作網頁", instructions: "預設在你自己的電腦上做事。動手前先看清楚(list_dir、read_file、截圖),每一步確認結果。要動到使用者的 Mac 時先說明要做什麼。", shape: "gem", color: "blue" },
Template { name: "客服助理", description: "在 Slack、Discord、Telegram 或 Teams 回覆客人", instructions: "回答簡短、有禮貌。不確定的事不要編,說會請同事確認。不要透露內部資料或使用者的私人資訊。", shape: "pill", color: "orange" },
Template { name: "開發助手", description: "看程式、跑測試、追 GitHub 上的事", instructions: "改程式前先讀相關檔案和測試。每次修改後跑測試,回報結果。PR 或 CI 的事件來時,先看紀錄再下結論。", shape: "hexagon", color: "black" },
];
/// Add a memory (no duplicates; the oldest drop off past the limit).
pub fn remember(list: &mut Vec<String>, fact: &str) -> Result<String, String> {
let f: String = fact.trim().chars().take(MAX_MEMORY_CHARS).collect();
if f.is_empty() {
return Err("要記住什麼?".into());
}
if list.iter().any(|m| m == &f) {
return Ok(format!("已經記得:{f}"));
}
list.push(f.clone());
let n = list.len();
if n > MAX_MEMORIES {
list.drain(..n - MAX_MEMORIES);
}
Ok(format!("記住了:{f}"))
}
/// Forget by number (1-based) or by text it contains.
pub fn forget(list: &mut Vec<String>, what: &str) -> Result<String, String> {
let w = what.trim();
let idx = match w.parse::<usize>() {
Ok(n) if n >= 1 && n <= list.len() => Some(n - 1),
_ => list.iter().position(|m| !w.is_empty() && m.contains(w)),
};
match idx {
Some(i) => Ok(format!("忘記了:{}", list.remove(i))),
None => Err(format!("沒有記得「{w}」這件事")),
}
}
pub fn prompt_section(memories: &[String], skills: &[Skill]) -> String {
let mut s = String::new();
if !memories.is_empty() {
s.push_str("\n\n你記得的事(使用者要你記住的;用 remember 新增、forget 刪除):\n");
for (i, m) in memories.iter().enumerate() {
s.push_str(&format!("{}. {m}\n", i + 1));
}
} else {
s.push_str("\n\n使用者說「記住…」「以後都…」時,用 remember 記下來;說「忘記…」時用 forget。");
}
if !skills.is_empty() {
s.push_str("\n\n可用的技能(做法說明;需要時先用 use_skill 讀取完整內容再照做):\n");
for k in skills {
s.push_str(&format!("- {}:{}\n", k.name, k.description));
}
}
s.push_str("\n使用者教你一套可以重複用的做法時,可以用 save_skill 存成技能,所有 Bot 都能用。");
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remember_and_forget() {
let mut m = vec![];
assert!(remember(&mut m, "我喜歡靠窗的座位").unwrap().starts_with("記住了"));
assert!(remember(&mut m, "我喜歡靠窗的座位").unwrap().starts_with("已經記得"));
remember(&mut m, "週五不開會").unwrap();
assert_eq!(forget(&mut m, "1").unwrap(), "忘記了:我喜歡靠窗的座位");
assert!(forget(&mut m, "不存在").is_err());
assert_eq!(forget(&mut m, "週五").unwrap(), "忘記了:週五不開會");
for i in 0..(MAX_MEMORIES + 5) {
remember(&mut m, &format!("事 {i}")).unwrap();
}
assert_eq!(m.len(), MAX_MEMORIES);
assert_eq!(m[0], "事 5");
let p = prompt_section(&m[..1], &[Skill { name: "週報".into(), description: "整理每週進度".into(), instructions: "…".into() }]);
assert!(p.contains("1. 事 5") && p.contains("- 週報:整理每週進度"));
}
}