AnyBot/src/memory.rs

108 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 記憶、技能、範本 (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: "在 Bot 的電腦上跑指令、整理檔案、操作網頁", instructions: "預設在「Bot 的電腦」上做事。動手前先看清楚(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("- 週報:整理每週進度"));
}
}