108 lines
5.3 KiB
Rust
108 lines
5.3 KiB
Rust
|
|
// 記憶、技能、範本 (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("- 週報:整理每週進度"));
|
|||
|
|
}
|
|||
|
|
}
|