diff --git a/README.md b/README.md index bc20d81..2334c16 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ |图片|image|file:文件名,url:链接| |语音|record|file:文件名,url:链接| |短视频|video|file:文件名,url:链接| -|@某人|at|qq:QQ号或all| +|@某人|at|qq:QQ号或all(全体成员,不得随意使用打扰大家,仅在管理员强烈要求时才可用)| |猜拳|rps|{}| |骰子|dice|{}| |窗口抖动|shake|{}| @@ -450,11 +450,16 @@ #### 2. 分析调用结果 - 如果任务成功完成,调用`end_action`结束本次任务; +- 如果需要持久化记忆,调用`save_group_memory`或`save_private_memory`; - 如果还需要进一步操作,首先发消息将要执行的任务解释给用户, - 如果任务不是敏感或危险操作,直接执行; - 否则,调用`end_action`暂停本次任务,等待用户确认。 注意事项: - 如果你只是在回应用户而不做高级调用,发送一条消息成功后立即结束本次任务。 -- 除非用户明确指示,禁止连续发送消息打扰用户! +- 除非用户明确指示,禁止连续发送消息或`@all`打扰用户! - 用户可以在任何时候终止你的任务或添加新的指示。 + +### 记忆 +> 以下为你之前为当前聊天保存的记忆 +%v \ No newline at end of file diff --git a/actions.yaml b/actions.yaml index 298f05a..1d4a6cb 100644 --- a/actions.yaml +++ b/actions.yaml @@ -3,6 +3,10 @@ actions: desc: 结束或暂停任务 params: "-" data: "-" + save_memory: + desc: 持久化记忆 + params: text 简明扼要地用一句话概括你认为在该会话必须记住的一件事,禁止换行 (string) + data: "-" send_private_msg: desc: 发送私聊消息 params: user_id 对方QQ号;message 要发送的内容 (json.RawMessage) @@ -98,6 +102,7 @@ actions: config: owner: - end_action + - save_memory - send_private_msg - send_group_msg - delete_msg @@ -123,6 +128,7 @@ config: - get_group_member_list admin: # need to check if gid is the same as admin's gid - end_action + - save_memory - send_group_msg - delete_msg - send_like @@ -141,6 +147,7 @@ config: - get_group_member_list user: # need to check if gid is the same as user's gid - end_action + - save_memory - send_group_msg - send_like - set_msg_emoji_like diff --git a/agent.go b/agent.go index 7a3ce8a..ca40a77 100644 --- a/agent.go +++ b/agent.go @@ -27,6 +27,8 @@ import ( const ( // EOA is a dummy action that is used to terminate request EOA = "end_action" + // SVM is a dummy action that is used to indicate that a memory has been saved + SVM = "save_memory" ) var ( @@ -43,7 +45,9 @@ type Agent struct { chars string perm *Perm imgpcache *ttl.Cache[uint64, string] + mem MemoryStorage manualaddreq bool + manualaddmem bool hasimageapi bool } @@ -52,16 +56,17 @@ type Agent struct { // - characteristics 推荐使用 Markdown 格式,描述 Agent 个性。 // - defaultprompt 为上下文为空时的默认提示,建议为事件的 JSON,一般不会用到,因此也可留空。 // - manualaddreq 表示是否由用户手动添加请求。 +// - manualaddmem 表示是否由用户手动添加记忆。 func NewAgent( id int64, batchcap, itemscap int, imgpcachettl time.Duration, - nickname, sex, characteristics, defaultprompt string, - manualaddreq bool, + nickname, sex, characteristics, defaultprompt string, mem MemoryStorage, + manualaddreq, manualaddmem bool, ) (ag Agent) { ag = Agent{ id: id, nickname: nickname, sex: sex, chars: characteristics, - imgpcache: ttl.NewCache[uint64, string](imgpcachettl), - log: chat.NewLog[fmt.Stringer](batchcap, itemscap, "\n", defaultprompt), - manualaddreq: manualaddreq, + imgpcache: ttl.NewCache[uint64, string](imgpcachettl), + log: chat.NewLog[fmt.Stringer](batchcap, itemscap, "\n", defaultprompt), + mem: mem, manualaddreq: manualaddreq, manualaddmem: manualaddmem, } _ = ag.LoadPermTable() return @@ -87,6 +92,11 @@ func (ag *Agent) AddTerminus(grp int64) { ag.log.Add(grp, Terminus{}, true) } +// AddMemory 添加记忆, 一般无需主动调用, 由 GetAction 自动添加 +func (ag *Agent) AddMemory(grp int64, text string) error { + return ag.mem.Save(grp, text) +} + // CanViewImage will be true if SetViewImageAPI is called func (ag *Agent) CanViewImage() bool { return ag.hasimageapi @@ -206,7 +216,7 @@ func (ag *Agent) ClearViewImageAPI() { func (ag *Agent) GetAction(api deepinfra.API, p model.Protocol, grp int64, role PermRole, isusersystem bool) ( reqs []zero.APIRequest, err error, ) { - sysp, err := ag.system(role) + sysp, err := ag.system(role, grp) if err != nil { return } @@ -243,6 +253,32 @@ func (ag *Agent) GetAction(api deepinfra.API, p model.Protocol, grp int64, role return case !ag.manualaddreq: ag.AddRequest(grp, &r) + if !ag.manualaddmem && r.Action == SVM { + txt, ok := r.Params["text"].(string) + if !ok || txt == "" { + continue + } + txt, err := extractMemory(&r) + if err != nil { + ag.AddResponse(grp, &APIResponse{ + Status: "error", + Message: err.Error(), + }) + continue + } + err = ag.AddMemory(grp, txt) + s := "ok" + msg := "" + if err != nil { + s = "error" + msg = err.Error() + } + ag.AddResponse(grp, &APIResponse{ + Status: s, + Message: msg, + }) + continue + } } reqs = append(reqs, r) } diff --git a/memory.go b/memory.go new file mode 100644 index 0000000..2edcd09 --- /dev/null +++ b/memory.go @@ -0,0 +1,44 @@ +package goba + +import ( + "errors" + "strings" + + zero "github.com/wdvxdr1123/ZeroBot" +) + +var ( + errEmptyMempry = errors.New("empty memory") + errMemoryHasReturn = errors.New("memory has \\r|\\n") +) + +type MemoryStorage interface { + Save(grp int64, text string) error + Load(grp int64) []string +} + +func extractMemory(r *zero.APIRequest) (string, error) { + txt, ok := r.Params["text"].(string) + if !ok || txt == "" { + return "", errEmptyMempry + } + for _, c := range txt { + if c == '\r' || c == '\n' { + return "", errMemoryHasReturn + } + } + return txt, nil +} + +func (ag *Agent) memoryof(grp int64) string { + mems := ag.mem.Load(grp) + if len(mems) == 0 { + return "" + } + sb := strings.Builder{} + for _, m := range mems { + sb.WriteByte('\n') + sb.WriteString(m) + } + return sb.String() +} diff --git a/perm_test.go b/perm_test.go index 262b836..1bf1b16 100644 --- a/perm_test.go +++ b/perm_test.go @@ -5,6 +5,7 @@ import "testing" const fulltab = `|功能|action|params|data| |---|---|---|---| |结束或暂停任务|end_action|-|-| +|持久化记忆|save_memory|text 简明扼要地用一句话概括你认为在该会话必须记住的一件事,禁止换行 (string)|-| |发送私聊消息|send_private_msg|user_id 对方QQ号;message 要发送的内容 (json.RawMessage)|message_id 消息ID (number)| |发送群消息|send_group_msg|group_id 群号;message 要发送的内容 (json.RawMessage)|message_id 消息ID (number)| |撤回消息|delete_msg|message_id 消息ID|-| diff --git a/prompt.go b/prompt.go index 6726a5f..2e27ea7 100644 --- a/prompt.go +++ b/prompt.go @@ -8,10 +8,13 @@ import ( //go:embed README.md var sysp string -func (ag *Agent) system(role PermRole) (string, error) { +func (ag *Agent) system(role PermRole, grp int64) (string, error) { tab, err := ag.perm.mdtable(role) if err != nil { return "", err } - return fmt.Sprintf(sysp, ag.id, ag.nickname, ag.sex, ag.chars, tab), nil + return fmt.Sprintf( + sysp, ag.id, ag.nickname, ag.sex, + ag.chars, tab, ag.memoryof(grp), + ), nil } diff --git a/types.go b/types.go index 8ff901f..3f8d029 100644 --- a/types.go +++ b/types.go @@ -64,3 +64,19 @@ type Terminus struct{} func (Terminus) String() string { return `{"action":"` + EOA + `"}` } + +type Memory struct { + Action string `json:"action"` + GroupID int64 `json:"group_id,omitempty"` // QQ群号 + UserID int64 `json:"user_id,omitempty"` // QQ号 + Text string `json:"text"` +} + +func (m *Memory) String() string { + sb := strings.Builder{} + err := json.NewEncoder(&sb).Encode(m) + if err != nil { + panic(errors.Wrap(err, "unexpected")) + } + return strings.TrimSpace(sb.String()) +}