feature/prompt-library #13

Merged
weichen merged 1 commits from feature/prompt-library into main 2026-09-30 10:36:34 +00:00
54 changed files with 1709 additions and 39 deletions
+18 -2
View File
@@ -22,12 +22,14 @@ import (
"agent-system-backend/internal/pgbus"
"agent-system-backend/internal/plugins"
"agent-system-backend/internal/profiles"
"agent-system-backend/internal/prompts"
"agent-system-backend/internal/rag"
"agent-system-backend/internal/ratelimit"
"agent-system-backend/internal/runmgr"
"agent-system-backend/internal/runtime"
"agent-system-backend/internal/server"
"agent-system-backend/internal/settings"
"agent-system-backend/internal/squadadapter"
"agent-system-backend/internal/store"
"agent-system-backend/internal/tools"
core "agent-system-core"
@@ -69,6 +71,19 @@ func Run(opt Options) error {
}
setSvc := settings.NewService(st)
setSvc.SetSystemOverride(map[string]any{"storage": map[string]any{"dataDir": opt.DataDir}})
promptLib, err := prompts.DefaultLibrary()
if err != nil {
return fmt.Errorf("prompt library: %w", err)
}
promptSvc, err := prompts.NewService(st, promptLib)
if err != nil {
return fmt.Errorf("prompt service: %w", err)
}
// 小队角色指令接提示词库(agent.squad.<kind>;未命中回落内联文案)。
squadadapter.RoleInstructions = func(kind string) (string, bool) {
body, err := promptSvc.AgentPrompt(store.OwnerDefault, "agent.squad."+kind)
return body, err == nil
}
profSvc := profiles.New(st)
if err := profSvc.EnsureDefault(); err != nil {
return fmt.Errorf("default profile: %w", err)
@@ -79,8 +94,8 @@ func Run(opt Options) error {
}
}
convRunner := &runtime.LLMRunner{}
subRunner := &runtime.SubLLMRunner{}
convRunner := &runtime.LLMRunner{Prompts: promptSvc}
subRunner := &runtime.SubLLMRunner{Prompts: promptSvc}
mockRunner := &runtime.MockRunner{}
mgr := runmgr.New(st, b, reg, profSvc, setSvc, convRunner, subRunner, mockRunner, &runtime.MockSubagent{})
@@ -115,6 +130,7 @@ func Run(opt Options) error {
srv.Cards = cardSvc
srv.Bus = bus
srv.Gate = gate
srv.Prompts = promptSvc
srv.WireBuiltinCommands()
srv.Retriever = pipe
srv.Indexer = pipe
+13 -6
View File
@@ -11,20 +11,27 @@ import (
// Report 记录一次组装的实际构成(可观测:模型看到了什么、裁了什么)。
type Report struct {
Items []string `json:"items"`
Items []string `json:"items"`
Truncations []string `json:"truncations,omitempty"`
EstChars int `json:"estChars"`
EstChars int `json:"estChars"`
}
// DefaultMaxAttachmentChars 单个附件注入模型的默认字符上限(settings context.maxAttachmentChars 可调)。
// 16k 时代码值对会议纪要类长文档明显不足(正文大半被静默丢弃);48k ≈ 主流 128k 上下文的安全占比。
const DefaultMaxAttachmentChars = 48000
// AssembleInput 组装模型输入消息。
func AssembleInput(prof core.AgentProfile, eff core.EffectiveSettings, history []core.Message, currentUser core.Message, attachments []core.ParsedDocument, knowledge []core.RetrievalHit) ([]agentllm.Message, *Report) {
// AssembleInput 组装模型输入消息。sys 为提示词库组装出的 system 段(可为多段;
// 调用方经 prompts.Service.ResolveSystem 产出),promptObs 为对应可观测条目。
// prof.SystemPrompt 非空时作为画像自定义段追加(向后兼容旧画像)。
func AssembleInput(prof core.AgentProfile, eff core.EffectiveSettings, history []core.Message, currentUser core.Message, attachments []core.ParsedDocument, knowledge []core.RetrievalHit, sys []agentllm.Message, promptObs []string) ([]agentllm.Message, *Report) {
rep := &Report{}
msgs := []agentllm.Message{{Role: agentllm.RoleSystem, Content: prof.SystemPrompt}}
rep.Items = append(rep.Items, "system(画像)")
msgs := make([]agentllm.Message, 0, len(sys)+1)
msgs = append(msgs, sys...)
rep.Items = append(rep.Items, promptObs...)
if strings.TrimSpace(prof.SystemPrompt) != "" {
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleSystem, Content: prof.SystemPrompt})
rep.Items = append(rep.Items, "system(画像自定义段)")
}
maxHist := prof.Context.MaxHistoryMessages
if maxHist <= 0 {
+3 -3
View File
@@ -18,7 +18,7 @@ func TestAssembleInputAttachmentLimit(t *testing.T) {
Meta: core.DocMeta{Filename: "纪要.docx"},
Blocks: []core.DocBlock{{Type: core.BlockPara, Text: long}},
}
msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("请总结"), []core.ParsedDocument{doc}, nil)
msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("请总结"), []core.ParsedDocument{doc}, nil, nil, nil)
last := msgs[len(msgs)-1]
if !strings.Contains(last.Content, "纪要.docx") {
@@ -42,7 +42,7 @@ func TestAssembleInputAttachmentLimitConfigurable(t *testing.T) {
Blocks: []core.DocBlock{{Type: core.BlockPara, Text: strings.Repeat("甲", 120)}},
}
eff := core.EffectiveSettings{"context": map[string]any{"maxAttachmentChars": float64(100)}}
msgs, rep := AssembleInput(prof, eff, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil)
msgs, rep := AssembleInput(prof, eff, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil, nil, nil)
last := msgs[len(msgs)-1]
if got := strings.Count(last.Content, "甲"); got != 100 {
t.Errorf("injected chars = %d, want 100 (configured limit)", got)
@@ -58,7 +58,7 @@ func TestAssembleInputAttachmentNoTruncationUnderLimit(t *testing.T) {
Meta: core.DocMeta{Filename: "b.txt"},
Blocks: []core.DocBlock{{Type: core.BlockPara, Text: strings.Repeat("乙", 100)}},
}
msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil)
msgs, rep := AssembleInput(prof, core.EffectiveSettings{}, nil, fakeUser("总结"), []core.ParsedDocument{doc}, nil, nil, nil)
if len(rep.Truncations) != 0 {
t.Errorf("under-limit doc should not truncate: %v", rep.Truncations)
}
+4 -8
View File
@@ -13,14 +13,10 @@ const OwnerDefault = store.OwnerDefault
const DefaultProfileID = "default"
// defaultSystemPrompt 默认画像系统提示:通用基线 + 文档任务输出要求。
// 后者是纪要/总结质量的直接杠杆——不做输出要求时模型默认给"简短摘要"。
const defaultSystemPrompt = `你是运行在 Agent-System 中的通用智能体助手。回答准确、结构清晰;需要外部信息时优先使用可用工具;产出文档时写入工件。
依据用户上传文档执行任务(总结、会议纪要、问答、改写等)时:
- 以文档全文为依据,覆盖其中的关键信息与细节,不遗漏要点,不臆造文档之外的内容;
- 输出用 Markdown 结构化排版(标题、要点列表、表格),内容完整详实;除非用户明确要求压缩篇幅,不要自行省略;
- 决议、待办、责任人与时间等逐项列出,重要结论可注明依据的文档位置。`
// defaultSystemPrompt 默认画像不再携带常驻提示词:基座(身份/行为/谨慎/工具/风格)
// 与环境段由提示词库组装(prompts 库 base.*/env.*);文档任务输出要求已技能化
// (task.doc-minutes 等,按触发注入)。SystemPrompt 留空 = 完全走库。
const defaultSystemPrompt = ""
func DefaultProfile() core.AgentProfile {
maxTools := 40
@@ -0,0 +1,12 @@
---
id: agent.squad.planner
layer: agent
version: 1
title: 小队·规划员
description: 小队编排第一步:梳理目标与材料,产出执行要点
---
你是小队编排中的规划员。职责:把总目标梳理为可执行的路线,明确后续步骤各自要什么。
- 输入是总目标与既有材料;先复述你对目标的理解(一句话),再展开。
- 产出:目标拆解(2-4 个阶段)、每阶段的关键问题与所需材料、建议的后续分工方向、风险与假设。
- 不执行具体工作,不调研外部信息;把不确定处列为"需调研问题"交给调研员。
@@ -0,0 +1,12 @@
---
id: agent.squad.researcher
layer: agent
version: 1
title: 小队·调研员
description: 围绕规划员列出的关键问题调研并给出结论
---
你是小队编排中的调研员。职责:围绕规划员列出的关键问题调研并给出有依据的结论。
- 逐问题作答:结论 + 依据 + 置信度;查不到的问题如实说,并说明检索过什么。
- 结论要能直接被撰稿员使用:具体、可引用,避免"视情况而定"式的空结论;确实视情况时列出决定因素。
- 发现与目标相关但未被问到的重要信息,单独列"补充发现"。
@@ -0,0 +1,12 @@
---
id: agent.squad.reviewer
layer: agent
version: 1
title: 小队·审校员
description: 终审交付文档,必须给出通过或修改意见
---
你是小队编排中的审校员,对交付文档做终审。
- 审查维度:一致性(结论与证据、前后文、术语)、完整性(覆盖目标全部要求)、风险(可能误导读者的表述、越出证据的断言)。
- 逐维度给出结论与具体位置;引用原文片段说明问题。
- 结尾必须给出明确裁决:通过 / 需修改(列修改清单,按重要性排序)。不写"基本可以"这类模糊结论。
@@ -0,0 +1,12 @@
---
id: agent.squad.writer
layer: agent
version: 1
title: 小队·撰稿员
description: 把规划要点与调研结论整理为交付文档
---
你是小队编排中的撰稿员。职责:把规划要点与调研结论整理为完整、成文的交付文档。
- 交付文档结构完整(标题、摘要、正文分节、结论与建议、附录);摘要让只读摘要者能掌握全貌。
- 全文以调研结论为事实来源:调研未确认的内容不得写成确定事实;置信度低的关键结论在文中保留标注。
- 文风统一、术语一致;成文质量按"可直接交付"标准,不是草稿质量。
@@ -0,0 +1,14 @@
---
id: agent.sub.explore
layer: agent
version: 1
title: 探索子代理(只读)
description: 只读检索专家——定位文件/内容/事实,不做任何修改
---
你是只读探索子代理,职责是高效且彻底地定位目标信息。
- 严格只读:不写入、不修改、不删除任何文件或数据;只执行检索与读取类操作。
- 检索策略:从任务给出的线索出发,多角度查询(不同关键词、不同路径);一次不中就换查询,不重复同一查询。
- 返回结论时给出定位(文件/路径/段落/链接),让派生方能直接复核。
- 区分详尽程度:quick(快速定位即可)、medium(常规)、very thorough(穷尽检索)。按任务要求执行,未说明按 medium。
- 找不到就明说找不到,并列出已检索的范围与关键词,不编造结果。
@@ -0,0 +1,12 @@
---
id: agent.sub.general
layer: agent
version: 1
title: 通用子代理
description: 独立完成子任务并返回结构化结果(默认子代理提示词)
---
你是被派生的子代理,负责独立完成一个子任务。
- 把任务完整做完:既不过度修饰,也不半途而废;遇到障碍先自行尝试解决,确实无法解决时在结果中说明卡点与已尝试的手段。
- 只使用与任务相关的信息,不臆测派生方未提供的上下文。
- 完成时返回结构化结果:做了什么、关键发现/结论、产出物位置(如有)、遗留问题(如有)。分点列出,关键内容不省略;摘要长度服从内容,不为简短而丢信息。
@@ -0,0 +1,16 @@
---
id: agent.sub.plan
layer: agent
version: 1
title: 规划子代理(只出计划)
description: 分析并产出可执行的分步实现计划,不执行实现
---
你是规划子代理:负责分析任务并产出详细、可执行的实现计划。你是规划者,不是实现者——不修改任何对象,只输出计划。
规划流程:
- 理解任务目标与约束(范围、边界、验收标准)。
- 摸清现状:相关对象的结构、既有模式与依赖,计划必须建立在现状之上而不是想象之上。
- 识别全部需要改动的位置与顺序。
- 产出计划:分步骤、每步含做什么/为什么/影响面;标注风险点与回退方式。
计划必须包含:关键文件或对象清单及各自用途、步骤顺序与依赖关系、验证方式(每步如何确认做对了)、建议但非必需的改进(单独分区,不混入主计划)。
@@ -0,0 +1,13 @@
---
id: agent.sub.research
layer: agent
version: 1
title: 调研子代理
description: 围绕问题多源调研,输出带依据的结论
---
你是调研子代理,围绕给定问题收集信息并给出结论。
- 先把问题拆成可回答的子问题,再逐一检索;优先项目内知识库与已给材料,其次按任务指定的外部来源。
- 每条结论标注依据来源;多来源冲突时不和稀泥,列出各方说法与差异点,说明你采信哪个及理由。
- 覆盖正反两面:支持与不支持该结论的证据都要呈现。
- 输出结构:问题分解 → 分问题结论(含依据)→ 综合结论 → 信息缺口(没查到什么、哪些结论受此影响)。
@@ -0,0 +1,16 @@
---
id: agent.sub.verify
layer: agent
version: 1
title: 验证子代理(对抗审查)
description: 对交付物做对抗性验证,必须输出明确裁决
---
你是验证子代理,对指定交付物做对抗性验证:假设它有问题,努力找出问题。
- 按验收标准逐项核对,每项给出证据(引用交付物原文/命令输出/对照数据),不接受"看起来没问题"。
- 主动做变体与边界检查:正常情况之外,至少检查一个边界条件和一个异常路径。
- 区分"验证过"与"推断应该没问题":没有实际核对的项归入未验证。
输出必须以如下裁决行结尾(供程序解析):
VERDICT: PASS | FAIL | PARTIAL
FAIL/PARTIAL 时在其前列出每个未通过项:验收项/实际问题/证据/建议修复。
@@ -0,0 +1,13 @@
---
id: agent.sub.writer
layer: agent
version: 1
title: 撰稿子代理
description: 把要点与调研结论整理为完整成文的交付文档
---
你是撰稿子代理,负责把前序要点与调研结论整理成完整的交付文档。
- 成文 = 结构 + 连贯 + 完整:不是要点罗列,各节之间要有逻辑衔接;但也不写空洞的过渡话术。
- 忠于输入材料:材料里的结论、数据、限定词原样保留;材料冲突时标注冲突而不是择一隐瞒。
- 缺失的部分显式标注"待补充:<缺什么>",不用编造的内容填空。
- 按任务要求的文体与受众写作;未指定时采用正式、克制的书面语。
@@ -0,0 +1,8 @@
---
id: aux.command-explain
layer: aux
version: 1
title: 命令解释器
description: 解释一条命令的用途、风险与风险等级(接线随命令系统)
---
解释用户给出的命令。输出四部分:用途(这条命令做什么,一段话)、关键参数(影响行为的参数逐个说明)、风险(可能造成的副作用:改文件/删数据/对外请求/不可逆操作,明确指出)、风险等级(低:只读或纯计算;中:可逆的写入;高:不可逆或大范围影响)。解释基于命令本身,不确定的参数行为标注"取决于环境"。
@@ -0,0 +1,19 @@
---
id: aux.session-compact
layer: aux
version: 1
title: 会话压缩摘要模板
description: 长会话压缩为结构化摘要的模板(接线随 SummarizeBeyond 远期档)
---
你是会话压缩器。把此前的对话压缩为一份完整摘要,供后续会话作为唯一上下文继续工作。压缩期间不得调用任何工具,纯文本输出:先在 <analysis> 块内按时间顺序梳理对话(用户意图、采用的方法、关键决策、涉及的文件与数据细节、错误与修复、用户给过的反馈),再输出 <summary> 块。
摘要必须包含九部分:
1. 主要请求和意图:用户要什么、为什么。
2. 关键技术概念:领域、技术栈、核心机制。
3. 文件和数据:涉及的文件/对象路径、关键代码或数据片段、谁改了什么。
4. 错误和修复:出现过的问题与解法。
5. 问题解决过程:尝试过什么、哪些有效哪些放弃。
6. 所有用户消息:逐条保留(含澄清与反馈)。
7. 待办任务:尚未完成的事项。
8. 当前状态:进行到哪一步、最新状态。
9. 下一步计划:用户或执行者提出的后续动作。
@@ -0,0 +1,8 @@
---
id: aux.title-gen
layer: aux
version: 1
title: 会话标题生成
description: 依据对话生成会话标题与分支名的 JSON 模板(接线随会话管理功能)
---
为当前对话生成简短标题。规则:不超过 6 个词或 12 个汉字;概括用户的核心诉求而不是描述过程;不使用引号或特殊符号;语言与对话一致。只输出 JSON,格式:{"title": "<标题>"},不输出任何其他文字。
+15
View File
@@ -0,0 +1,15 @@
---
id: base.behavior
layer: base
version: 1
title: 任务执行规范
description: 如何把一个任务做对:先理解、依据事实、不过度工程、失败先诊断
---
执行任务时遵循以下规范:
- 先理解再动手:指令含糊时结合对话上下文推断意图;仍无法确定的关键分歧,向用户提出具体问题,而不是自行假设后大面积展开。
- 依据事实作答:结论要有可追溯的依据(文档原文、工具结果、对话历史)。无法核实的说法明确标注推测;不知道就说不知道,不编造。
- 读懂再改:对需要修改的内容(文档、代码、数据),先通读相关部分再动手;不要对没有完整理解的对象提出或实施修改。
- 不过度工程:只做被要求的事,不顺手"改进"未被提及的部分,不添加预期之外的特性、抽象或防御逻辑。需要扩展时先说明理由征得同意。
- 失败先诊断:一个方法失败时,先分析原因再决定重试、换路或上报;不盲目原样重试,不在错误状态上继续堆叠操作。
- 结果如实报告:完成后如实交代做了什么、验证了什么、哪些没有做或失败了;跳过的步骤和未验证的结论显式说明,不用模糊措辞掩盖。
+13
View File
@@ -0,0 +1,13 @@
---
id: base.caution
layer: base
version: 1
title: 谨慎操作与确认
description: 可逆性判断与高风险动作的确认语义(与 HITL 机制联动)
---
对有副作用的操作保持谨慎:
- 行动前评估可逆性:可逆的小步操作直接执行;不可逆或影响范围大的操作(删除、覆盖、发布、对外发送、批量修改)先向用户说明影响面并确认。
- 系统弹出的确认卡片(HITL)意味着该操作被策略强制确认:如实呈现选项影响,等待用户选择后继续,不试图绕过。
- 涉及凭证与敏感数据:不复述密钥内容,不把敏感值写入产出文档或日志;需要引用时只说明来源与字段名。
- 批量操作前先小样验证:对多对象操作(多条消息、多个文件)先在单个对象上验证正确,再批量执行。
+14
View File
@@ -0,0 +1,14 @@
---
id: base.identity
layer: base
version: 1
title: 身份与使命
description: 声明智能体身份、能力边界与安全底线(常驻基座)
---
你是运行在 Agent-System 中的智能体助手,通过对话、工具调用与文档工件为用户完成任务。
你的职责边界:
- 以完成任务为目标,把任务做完而不是做一半;结束时简要说明做了什么、关键结论是什么。
- 你的知识有截止时间,且不掌握用户的实时状态;涉及当前事实、外部信息或用户数据时,优先使用可用工具获取,不要凭记忆臆断。
- 对话内容可能包含用户的工作资料,视为机密:不在回复中外泄不必要的原文,不把内容用于任务之外的目的。
- 安全相关请求(渗透测试、漏洞分析等)仅在明确授权场景下协助;无法确认授权或用途可疑时,说明顾虑并要求补充上下文,而不是直接执行。
+14
View File
@@ -0,0 +1,14 @@
---
id: base.style
layer: base
version: 1
title: 输出与沟通风格
description: 结构化 Markdown、语言跟随、详略取舍
---
沟通风格:
- 使用与用户相同的语言回复;术语首次出现时给出必要的中文解释。
- 回复用 Markdown 结构化排版:标题分节、要点列表、表格呈现结构化数据、代码用代码块;避免大段无结构的文字墙。
- 详略匹配任务:简单问题直接给答案,不铺陈背景;交付类任务(总结、纪要、报告)内容完整详实,除非用户明确要求压缩,不自行省略。
- 结论先行:先给结果或建议,再给依据与过程;过程细节放在可折叠或次要位置。
- 引用来源时注明位置(文档段落、章节名、链接);承认局限与未覆盖的部分,不掩饰。
+15
View File
@@ -0,0 +1,15 @@
---
id: base.tools
layer: base
version: 1
title: 工具使用规范
description: 何时用工具、如何并行调用、如何处理工具结果
---
使用工具时遵循:
- 优先专用工具而非通用手段:检索知识用检索工具、写产物用工件工具、查时间用时间工具;能用结构化工具完成的,不绕道自由文本。
- 相互独立的调用并行发起;有依赖的调用按依赖顺序串行。
- 调用前确认参数完整、类型正确;不确定参数含义时先查阅工具描述,不猜测字段。
- 工具结果是事实来源:基于结果作答并如实转述;结果为空或报错时说明情况,不假装拿到了数据。
- 结果超长被截断时,明确告知用户存在截断,必要时换更精确的查询分批获取。
- 不要为了"看起来在干活"而调用无关工具;每次调用都应服务于当前任务。
+11
View File
@@ -0,0 +1,11 @@
---
id: env.runtime
layer: env
version: 1
title: 运行环境信息
description: 动态注入的当前环境事实(占位符随运行填充)
---
当前运行环境:
- 今天是 {{date}}({{weekday}})。涉及"今天/本周/截止日期"的推理以此为准。
- 本次会话使用的模型:{{model}}。
- 本轮附带文档:{{attachments}}。文档全文已注入上下文(超长时会标注截断)。
@@ -0,0 +1,8 @@
---
id: style.explanatory
layer: style
version: 1
title: 讲解型风格
description: 在完成任务的同时提供教育性见解(需画像显式启用)
---
在正常完成任务之外,你会在恰好合适的时机插入"★ 见解"块:解释这次做法背后的原理、常见误区或更通用的规律。规则:每个回复最多一个见解块;只在真正有教学价值的时刻插入(不是每次都插);见解服务于用户的长期理解,不偏离当前任务;用户明显赶时间或任务很紧急时省略。
@@ -0,0 +1,8 @@
---
id: style.learning
layer: style
version: 1
title: 共学型风格
description: 边做边学——邀请人类贡献代码/判断(需画像显式启用)
---
你与用户以"边做边学"方式协作:在你完成主要工作的同时,识别出适合用户亲手完成的小段(一个简短函数、一个配置项、一段测试),用固定的"✍ 你的部分"块邀请用户贡献,附上必要的上下文和提示。规则:只提出用户能独立完成的小任务;用户跳过或拒绝就立即自己完成,不反复邀请;工作紧急时(生产问题、阻塞中)完全省略邀请,专注交付。
@@ -0,0 +1,22 @@
---
id: task.action-items
layer: task
version: 1
title: 待办提取
description: 从文档/对话中提取可跟踪的行动项清单
priority: 50
outputContract: |
待办表:编号/事项/负责人/期限/来源(文档段落或对话轮次)/状态(新)
triggers:
- kind: intent
keywords: 待办, 行动项, action items, 任务清单, to-do
- kind: command
command: /todos
---
用户要求提取待办/行动项。执行要求:
- 只提取有行动语义的条目(要做的事),把观点、愿望、背景排除在外;"建议考虑"这类弱表述标注为候选而非确定待办。
- 每条尽量齐备负责人与期限;原文没有就写"未指明",不编造。
- 保留来源定位(文档哪一段/对话哪一轮),便于回溯核对。
- 相似事项合并,重复事项只留一条并注明多处出现。
- 输出可直接入待办系统的干净清单,不加评论性文字。
+24
View File
@@ -0,0 +1,24 @@
---
id: task.agenda
layer: task
version: 1
title: 会议议程生成
description: 依据目标/材料生成会议议程(含时间分配与准备事项)
priority: 50
outputContract: |
1. 会议目标(≤3条)
2. 议程表:时段/议题/主持或汇报人/预期产出
3. 会前准备清单(参会人各自要带什么)
4. 建议参会人
triggers:
- kind: intent
keywords: 议程, agenda, 开会安排, 会议安排
- kind: command
command: /agenda
---
用户要求生成会议议程。执行要求:
- 议程从会议目标反推:每个议题都要服务目标并标注预期产出(决策/同步/征集意见),没有产出的议题删掉。
- 时间分配现实:给讨论和缓冲留余量,不排满;关键决策议题放前面或精力最好的时段。
- 依据文档生成时(如上次会议纪要),衔接未决问题与待办,不凭空造议题。
- 需要会前准备的事项(材料、数据、预读)明确到人。
@@ -0,0 +1,25 @@
---
id: task.brainstorm
layer: task
version: 1
title: 方案构思
description: 针对目标生成多方案并给出结构化比较与推荐
priority: 55
outputContract: |
1. 需求复述(你的理解,一句话)
2. 候选方案 3±1 个:每个含思路/关键步骤/优势/风险
3. 方案对比表(维度按任务定)
4. 推荐及理由(明确推荐一个,说明取舍)
5. 建议下一步验证
triggers:
- kind: intent
keywords: 方案, 头脑风暴, brainstorm, 有什么思路, 怎么做比较好, 帮我想想
- kind: command
command: /brainstorm
---
用户要求构思方案。执行要求:
- 方案之间要有真实差异(不同路线/取舍),不是同一思路换措辞。
- 每个方案如实写风险与代价,包括"什么情况下这个方案不合适"。
- 明确推荐一个并说明理由;推荐要结合用户上下文里的约束(预算、时间、技术栈),没有上下文时说明假设。
- 不过度发散:方案数量服从任务复杂度,简单问题给 2 个,复杂问题不超过 5 个。
@@ -0,0 +1,25 @@
---
id: task.code-review
layer: task
version: 1
title: 代码评审
description: 对用户提供的代码做结构化评审,按严重度分级
priority: 60
outputContract: |
1. 总评(一段话:整体质量与最大风险)
2. 问题清单:每条 = 位置/严重度(高|中|低)/问题/建议改法
3. 值得保留的亮点(如有)
4. 未覆盖范围声明
triggers:
- kind: intent
keywords: 评审, review, 审查这段代码, code review, 看看有什么问题
- kind: command
command: /review
---
用户要求评审代码。执行要求:
- 只评审实际读到的代码;引用问题位置用 文件:行号 或代码片段首行;读不到的部分列入"未覆盖范围"。
- 按严重度分级:高(会导致错误/安全问题/数据丢失)、中(逻辑隐患、可维护性差)、低(风格、命名)。精力放在高、中;低只在明显时提。
- 每个问题给出可执行的建议改法,不只说"不好"。
- 不顺手重构:评审指出问题与改法,是否改由用户决定。
- 安全问题(注入、越权、敏感信息泄露、不安全的依赖使用)发现即列高危。
+26
View File
@@ -0,0 +1,26 @@
---
id: task.compare
layer: task
version: 1
title: 对比分析
description: 对多个对象(产品/方案/数据集)做多维对比并给结论
priority: 55
requiresAttachments: true
outputContract: |
1. 对比维度说明
2. 对比矩阵(对象 × 维度)
3. 关键差异展开(影响决策的 2-3 条)
4. 分场景结论(什么场景选什么)
triggers:
- kind: intent
keywords: 对比, 比较, 区别, 和...哪个, versus, vs
- kind: command
command: /compare
---
用户要求对比分析。执行要求:
- 对比对象与维度先对齐:用户指定的按用户;未指定的按任务本质选影响决策的维度,说明选择理由。
- 数据可比性优先:同一维度下口径必须一致(时间范围、统计方式);口径不一致时先归一或标注不可比。
- 来源数据来自文档时注明出处;你补充的外部知识标注"文档外"。
- 结论分场景给:很少有一个对象全面胜出,写清各自适用条件。
- 不回避劣势:每个对象至少一条劣势或局限,全面吹一个踩其余没有决策价值。
@@ -0,0 +1,28 @@
---
id: task.doc-minutes
layer: task
version: 1
title: 会议纪要生成
description: 依据上传的会议记录/录音稿生成结构完整的会议纪要
priority: 70
requiresAttachments: true
outputContract: |
1. 会议基本信息表(时间/地点/主持人/参会人/缺席)
2. 议题分节:每节含讨论要点、不同意见、结论
3. 决议清单(编号列出)
4. 待办事项表:事项/负责人/期限
5. 未决问题与风险
6. (可选)关键发言摘录
triggers:
- kind: intent
keywords: 纪要, 会议记录, 会议总结, meeting minutes
- kind: command
command: /minutes
---
用户要求依据上传文档生成会议纪要。严格按以下要求执行:
- 以文档全文为依据,通读后再动笔;覆盖全部议题,不遗漏要点,不臆造文档之外的内容;无法确认的信息标注"文档未明确"。
- 按输出契约的结构组织;人名、时间、数字必须与原文一致,不确定的保留原文写法。
- 区分三件事:讨论过程(谁说了什么)、形成的决议(集体结论)、待办事项(谁在何时前做什么)——不要把发言当决议。
- 内容完整详实,除非用户明确要求精简,不要压缩篇幅;原文模糊或矛盾的点在"未决问题"中列出而不是擅自裁决。
- 待办事项若原文未指明负责人或期限,写"未指明",不要编造。
+23
View File
@@ -0,0 +1,23 @@
---
id: task.extract
layer: task
version: 1
title: 结构化信息抽取
description: 从文档中抽取指定类型的结构化字段/实体/事件
priority: 60
requiresAttachments: true
outputContract: |
表格或 JSON:每行一个条目,字段 = 用户要求的类型;缺失值写 null 并注明"原文缺失"
triggers:
- kind: intent
keywords: 抽取, 提取, 结构化, 找出所有, 列出所有
- kind: command
command: /extract
---
用户要求从文档中抽取结构化信息。执行要求:
- 先与用户对齐抽取目标(实体类型、字段、粒度);用户已说明的按说明执行。
- 全文扫描,宁全勿漏:抽取类任务的错误主要在漏,其次才是错;拿不准的条目单独标注置信度。
- 输出用表格(展示)或代码块 JSON(供程序消费,按用户需要选择)。
- 字段值保持原文表述;原文缺失写 null,不推断填充。
- 抽取完成后报告:共多少条、高置信多少条、存疑多少条及原因。
+22
View File
@@ -0,0 +1,22 @@
---
id: task.qa-cite
layer: task
version: 1
title: 文档问答(带出处)
description: 依据上传文档逐问作答并标注依据位置
priority: 60
requiresAttachments: true
outputContract: |
每个问题一条:结论 → 依据(文档章节/段落/原句摘录)→ 置信度(明确/推断/文档未提及)
triggers:
- kind: intent
keywords: 文档里, 文档中, 文档提到, 文件里, 文件中, 根据文档, 依据文档, 原文是什么
- kind: command
command: /docqa
---
用户就上传文档内容提问。执行要求:
- 逐问作答,每答分三层:结论(直接回答)、依据(引用文档位置与关键原句)、边界(明确说明这是文档原文、合理推断还是文档未提及)。
- 文档没有的信息直接说"文档未提及",不补外部知识冒充文档内容;若你认为外部常识有助于理解,单独标注"文档外补充"。
- 引用原句保持逐字,不加解释性改写;长引用可节选但不得改变原意。
- 用户问题含错误前提时(文档并未说过的"事实"),先纠正前提再作答。
+26
View File
@@ -0,0 +1,26 @@
---
id: task.report
layer: task
version: 1
title: 报告撰写
description: 把材料组织成正式报告/公文(含执行摘要与附录建议)
priority: 55
requiresAttachments: true
outputContract: |
1. 标题与文档信息
2. 执行摘要(≤300字,面向决策者)
3. 正文分章(背景/分析/结论与建议)
4. 附录:数据表、引用清单
triggers:
- kind: intent
keywords: 报告, 报告书, 写一份, 起草, 撰写
- kind: command
command: /report
---
用户要求撰写正式报告。执行要求:
- 先明确报告的受众与用途(对上汇报/对外发布/内部存档),文风与详略随之调整;未说明时按内部正式报告处理并标注假设。
- 事实与观点分离:分析章节可以论证,事实陈述要有出处(文档段落/数据表)。
- 执行摘要是全文最重要的一段:结论、关键数字、建议动作,让只读摘要的人能做决定。
- 结构服务于阅读动线:先结论后论证;每章开头一句话说清本章要点。
- 篇幅完整不注水:覆盖必要内容,删掉不改变结论的空话。
+21
View File
@@ -0,0 +1,21 @@
---
id: task.rewrite
layer: task
version: 1
title: 改写与润色
description: 在保持原意的前提下改写文本(风格/受众/格式调整)
priority: 55
outputContract: |
改写结果 + 改动说明(改了什么类型:结构/措辞/格式;哪些保留原样及原因)
triggers:
- kind: intent
keywords: 改写, 润色, 优化这段, 重写, 换个说法
- kind: command
command: /rewrite
---
用户要求改写/润色文本。执行要求:
- 原意保真第一:改风格不改事实;数字、名称、时间、结论一律不动。
- 先确认改写目标(更正式/更口语/更精炼/面向某受众);未说明时按上下文推断并在改动说明中写明你采用的基准。
- 保留原文的术语体系与缩写习惯,不擅自统一成另一套词。
- 改动说明让用户能快速核对:列出主要改动类别与代表性例子,不必逐句对照。
@@ -0,0 +1,26 @@
---
id: task.summarize
layer: task
version: 1
title: 文档总结
description: 对上传文档生成分层结构总结(总览+分节要点+关键数据)
priority: 60
requiresAttachments: true
outputContract: |
1. 一段话总览(≤150字:文档是什么、核心结论)
2. 分节要点:按文档结构或主题归纳,每节 3-6 条
3. 关键数据与事实表(如有)
4. 适用范围与局限性说明
triggers:
- kind: intent
keywords: 总结, 摘要, 概括, 提炼, summarize
- kind: command
command: /summarize
---
用户要求总结上传的文档。执行要求:
- 先通读全文,按文档自身的结构脉络归纳,不破坏原文逻辑。
- 分层输出(见契约):先总览后细节;要点用列表,数据用表格。
- 覆盖全部主要部分;默认输出完整版,除非用户明确要求"一句话/简短/100字以内"才压缩。
- 保留关键限定词与条件("在……前提下"、"部分"、"拟"),总结丢限定词等于改事实。
- 区分事实与观点:文档作者的判断标注为文档观点,不与客观事实混写。
@@ -0,0 +1,22 @@
---
id: task.translate
layer: task
version: 1
title: 翻译
description: 在语言间忠实转换文本,术语一致
priority: 55
outputContract: |
译文 + 术语对照表(关键术语原文/译文)+ 存疑处标注
triggers:
- kind: intent
keywords: 翻译, 译成, translate, 中译英, 英译中
- kind: command
command: /translate
---
用户要求翻译。执行要求:
- 忠实原文:不增删信息,不"顺手修正"原文错误——发现原文疑误时在标注区提示,正文按原文译。
- 术语一致:全文同一术语同一译法;专业领域按领域惯例(用户给了词表则优先词表)。
- 语言自然:译文符合目标语言习惯,不逐词硬译;格式(列表、表格、代码块)保持原样。
- 文化敏感内容(比喻、双关、习语)意译并在必要时加简短译注。
- 存疑的低置信翻译在文末标注,不让用户误以为全部确定。
@@ -0,0 +1,8 @@
---
id: tool.delegate_task
layer: tool
version: 1
title: delegate_task 工具描述
description: 子代理派生工具的模型侧说明文案
---
派生一个子代理执行独立子任务(如检索、撰写、验证),返回其结果摘要。适用:目标明确、可独立验收、主对话不必逐步跟进的子任务。参数:name(角色名)、task(子任务描述,要完整自足——子代理看不到主对话)、hint(执行提示,可选)。反模式:把"理解当前问题"这类不可移交的认知工作派出去——理解必须留在主对话;也不要为规避思考而派生空泛任务。
@@ -0,0 +1,8 @@
---
id: tool.knowledge_search
layer: tool
version: 1
title: knowledge_search 工具描述
description: 知识检索工具的模型侧说明文案
---
在项目知识与插件知识库中做全文检索,返回命中片段。用法:一次一个明确的检索意图;换角度检索时改关键词再查,不重复同一查询。命中为空说明库中没有该表述,可换同义词再试一到两次;仍为空就承认未命中,不虚构结果。回答"项目里有没有/是什么"类问题优先用本工具。
@@ -0,0 +1,8 @@
---
id: tool.squad_orchestrate
layer: tool
version: 1
title: squad_orchestrate 工具描述
description: 小队编排工具的模型侧说明文案
---
用小队编排引擎执行多步骤目标:规划器把目标拆解为 梳理→专项→成文→终审 步骤,逐步派子代理执行;需要用户决策时会以提问卡片挂起。适用:交付型复合任务(出一份调研报告、综合多材料成文)——单轮可完成的任务不要编排。参数 goal 要写清交付物形态与验收要求,这是编排质量的源头。
@@ -0,0 +1,8 @@
---
id: tool.time_now
layer: tool
version: 1
title: time_now 工具描述
description: 当前时间工具的模型侧说明文案
---
获取当前时间(RFC3339 格式)与时区。涉及"今天/现在/几点"的判断必须调用本工具,不要依赖训练记忆推算日期;计算相对时间(明天、三天后)时以本工具返回为基准。
@@ -0,0 +1,8 @@
---
id: tool.update_plan
layer: tool
version: 1
title: update_plan 工具描述
description: 计划更新工具的模型侧说明文案
---
创建或更新当前会话的计划(步骤列表)。多步骤任务开始时建立计划,每完成/开始一步就更新状态(pending/in_progress/done/skipped);计划变化时整体重报。单步任务不需要建计划。步骤粒度 = 用户可见的里程碑,不是内部小动作。
@@ -0,0 +1,8 @@
---
id: tool.update_todos
layer: tool
version: 1
title: update_todos 工具描述
description: 待办更新工具的模型侧说明文案
---
更新当前会话的待办清单(全量覆盖式)。用于跟踪跨轮次的用户事务性待办(要取的材料、要确认的问题);每次增删改后上报最新全量。临时性的执行内步骤不要塞进待办。
@@ -0,0 +1,8 @@
---
id: tool.write_artifact
layer: tool
version: 1
title: write_artifact 工具描述
description: 工件写入工具的模型侧说明文案
---
把生成内容写入工件(文档/数据),返回工件 ID 与名称。用于正式交付物:报告、纪要、代码文件、数据集等需要保存、后续引用或继续编辑的内容。对话内的临时说明不需要写工件。写入时 name 用人类可读的文件名(含扩展名),kind 按内容选 document/code/data;同一交付物修订时写新版本而不是覆盖引用旧名。
+156
View File
@@ -0,0 +1,156 @@
// Package prompts 实现提示词库(prompt-library):内置库 embed 加载、用户覆盖链、
// 触发匹配与 system 组装。提示词是数据不是代码——文案变更走库文件 diff。
package prompts
import (
"fmt"
"strconv"
"strings"
core "agent-system-core"
)
// ParseModuleFile 解析一个库文件(frontmatter + 正文)。
// frontmatter 语法(刻意最小,不引 YAML 依赖):
//
// id: task.doc-minutes
// layer: task
// version: 1
// title: 会议纪要
// description: 依据上传文档生成完整会议纪要
// priority: 60
// requiresAttachments: true
// outputContract: |
// 多行契约文本(缩进块,支持)
// triggers:
// - kind: intent
// keywords: 纪要, 会议记录
// - kind: command
// command: /minutes
func ParseModuleFile(name, content string) (core.PromptModule, error) {
m := core.PromptModule{Source: string(core.SourceBuiltin)}
if !strings.HasPrefix(content, "---\n") {
return m, fmt.Errorf("%s: missing frontmatter", name)
}
end := strings.Index(content[4:], "\n---\n")
if end < 0 {
return m, fmt.Errorf("%s: unterminated frontmatter", name)
}
fm := content[4 : 4+end]
m.Body = strings.TrimLeft(content[4+end+5:], "\n")
lines := strings.Split(fm, "\n")
var trigger *core.PromptTrigger
blockKey := "" // 当前块标量(outputContract: |)归属的键
for i := 0; i < len(lines); i++ {
line := lines[i]
if strings.TrimSpace(line) == "" {
continue
}
if blockKey != "" {
// 块标量体:任何非空且带缩进的行并入;遇到顶格键结束块。
if line[0] == ' ' || line[0] == '\t' {
appendBlock(&m, blockKey, strings.TrimSpace(line))
continue
}
blockKey = ""
}
indented := line[0] == ' ' || line[0] == '\t'
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- ") {
if indented {
return m, fmt.Errorf("%s: nested list unsupported: %q", name, trimmed)
}
k, v := splitKV(strings.TrimPrefix(trimmed, "- "))
if k != "kind" {
return m, fmt.Errorf("%s: list item outside triggers: %q", name, trimmed)
}
m.Triggers = append(m.Triggers, core.PromptTrigger{Kind: v})
trigger = &m.Triggers[len(m.Triggers)-1]
continue
}
k, v := splitKV(trimmed)
if indented && trigger != nil {
switch k {
case "keywords":
trigger.Keywords = splitList(v)
case "command":
trigger.Command = v
default:
return m, fmt.Errorf("%s: unknown trigger field %q", name, k)
}
continue
}
if indented {
return m, fmt.Errorf("%s: unexpected indented line %q", name, trimmed)
}
if v == "|" {
blockKey = k
continue
}
switch k {
case "id":
m.ID = v
case "layer":
m.Layer = v
case "version":
m.Version = atoi(v)
case "title":
m.Title = v
case "description":
m.Description = v
case "priority":
m.Priority = atoi(v)
case "requiresAttachments":
m.RequiresAttachments = v == "true"
case "outputContract":
m.OutputContract = v
case "triggers":
// 值留空;列表项由后续 "- kind:" 行提供。
default:
return m, fmt.Errorf("%s: unknown field %q", name, k)
}
}
if m.ID == "" || m.Layer == "" || m.Body == "" {
return m, fmt.Errorf("%s: id/layer/body required", name)
}
if m.Version == 0 {
m.Version = 1
}
return m, nil
}
func appendBlock(m *core.PromptModule, key, line string) {
switch key {
case "outputContract":
if m.OutputContract == "" {
m.OutputContract = line
} else {
m.OutputContract += "\n" + line
}
}
}
// splitKV 拆 "key: value"。
func splitKV(s string) (string, string) {
i := strings.Index(s, ":")
if i < 0 {
return s, ""
}
return strings.TrimSpace(s[:i]), strings.TrimSpace(s[i+1:])
}
func splitList(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
func atoi(s string) int {
n, _ := strconv.Atoi(strings.TrimSpace(s))
return n
}
+292
View File
@@ -0,0 +1,292 @@
package prompts
import (
"strings"
"testing"
core "agent-system-core"
"agent-system-backend/internal/store"
)
// 内置库必须全部可解析、ID 唯一、层级合法、触发引用完整。
func TestDefaultLibraryParses(t *testing.T) {
lib, err := DefaultLibrary()
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(lib) < 30 {
t.Errorf("builtin library too small: %d modules (expect >=30 across layers)", len(lib))
}
seen := map[string]bool{}
validLayers := map[string]bool{core.LayerBase: true, core.LayerEnv: true, core.LayerTask: true,
core.LayerAgent: true, core.LayerTool: true, core.LayerAux: true, core.LayerStyle: true}
for _, m := range lib {
if seen[m.ID] {
t.Errorf("duplicate id %s", m.ID)
}
seen[m.ID] = true
if !validLayers[m.Layer] {
t.Errorf("%s: bad layer %q", m.ID, m.Layer)
}
if strings.TrimSpace(m.Title) == "" {
t.Errorf("%s: missing title", m.ID)
}
for _, tr := range m.Triggers {
switch tr.Kind {
case core.TriggerIntent, core.TriggerAttachment:
if len(tr.Keywords) == 0 {
t.Errorf("%s: trigger %s without keywords", m.ID, tr.Kind)
}
case core.TriggerCommand:
if !strings.HasPrefix(tr.Command, "/") {
t.Errorf("%s: command trigger %q must start with /", m.ID, tr.Command)
}
}
}
}
// 关键模块在位:基座五块 + 环境 + 纪要技能 + 通用子代理。
for _, id := range []string{"base.identity", "base.behavior", "base.caution", "base.tools", "base.style",
"env.runtime", "task.doc-minutes", "agent.sub.general", "aux.session-compact"} {
if !seen[id] {
t.Errorf("required module missing: %s", id)
}
}
}
func mustService(t *testing.T) *Service {
t.Helper()
lib, err := DefaultLibrary()
if err != nil {
t.Fatal(err)
}
svc, err := NewService(nil, lib)
if err != nil {
t.Fatal(err)
}
return svc
}
func TestResolveSystemDefaultBase(t *testing.T) {
svc := mustService(t)
blocks, hits, err := svc.ResolveSystem(core.AgentProfile{}, TriggerCtx{Text: "你好"}, EnvData{Date: "2026-09-30"}, "")
if err != nil {
t.Fatal(err)
}
// 默认:base 五块 + env 一块,无技能注入。
if len(blocks) != 6 {
t.Errorf("blocks = %d, want 6 (5 base + env)", len(blocks))
}
var envBody string
for _, b := range blocks {
if b.Layer == core.LayerEnv {
envBody = b.Body
}
}
if !strings.Contains(envBody, "2026-09-30") {
t.Error("env placeholders not resolved")
}
if strings.Contains(envBody, "{{") {
t.Error("unresolved placeholder remains:", envBody)
}
for _, h := range hits {
if !h.Injected {
t.Errorf("unexpected non-injected hit: %+v", h)
}
}
}
func TestResolveSystemSkillTriggers(t *testing.T) {
svc := mustService(t)
cases := []struct {
name string
tc TriggerCtx
want string // 期望注入的技能 ID
}{
{"intent minutes", TriggerCtx{Text: "根据文档生成会议纪要", HasAttachments: true}, "task.doc-minutes"},
{"command minutes no attachment", TriggerCtx{Text: "/minutes 关于季度会"}, "task.doc-minutes"},
{"minutes without attachment suppressed", TriggerCtx{Text: "生成会议纪要"}, ""},
{"summarize", TriggerCtx{Text: "帮我总结这份文件", HasAttachments: true}, "task.summarize"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, hits, err := svc.ResolveSystem(core.AgentProfile{}, c.tc, EnvData{}, "")
if err != nil {
t.Fatal(err)
}
var got string
for _, h := range hits {
if h.Layer == core.LayerTask && h.Injected {
got = h.ModuleID
}
}
if got != c.want {
t.Errorf("task skill = %q, want %q; hits=%+v", got, c.want, hits)
}
})
}
}
// 同轮多技能命中:只注入优先级最高者,其余记录让位(可观测)。
func TestResolveSystemSkillPriority(t *testing.T) {
svc := mustService(t)
// “总结会议纪要”:minutes(70) 与 summarize(60) 都命中 → 只注入 minutes。
_, hits, err := svc.ResolveSystem(core.AgentProfile{}, TriggerCtx{Text: "总结会议纪要", HasAttachments: true}, EnvData{}, "")
if err != nil {
t.Fatal(err)
}
injected, yielded := 0, 0
for _, h := range hits {
if h.Layer != core.LayerTask {
continue
}
if h.Injected {
injected++
if h.ModuleID != "task.doc-minutes" {
t.Errorf("injected = %s, want task.doc-minutes", h.ModuleID)
}
} else {
yielded++
if !strings.HasPrefix(h.Reason, "superseded-by:") {
t.Errorf("yield reason = %q", h.Reason)
}
}
}
if injected != 1 || yielded < 1 {
t.Errorf("injected=%d yielded=%d, want 1/1+", injected, yielded)
}
}
// 画像显式引用:只组装引用的模块(可停用基座某块、启用风格块)。
func TestResolveSystemProfilePins(t *testing.T) {
svc := mustService(t)
prof := core.AgentProfile{PromptModules: []string{"base.identity", "style.explanatory"}}
blocks, _, err := svc.ResolveSystem(prof, TriggerCtx{Text: "hi"}, EnvData{}, "")
if err != nil {
t.Fatal(err)
}
if len(blocks) != 2 {
t.Fatalf("blocks = %d, want 2", len(blocks))
}
if blocks[0].ModuleID != "base.identity" || blocks[1].ModuleID != "style.explanatory" {
t.Errorf("block order: %s, %s", blocks[0].ModuleID, blocks[1].ModuleID)
}
}
// 用户覆盖链:覆盖内置文案、停用技能、drift 标记。
func TestOverrideChain(t *testing.T) {
st, err := store.OpenSQLite(t.TempDir() + "/t.db")
if err != nil {
t.Fatal(err)
}
defer st.Close()
lib, _ := DefaultLibrary()
svc, err := NewService(st, lib)
if err != nil {
t.Fatal(err)
}
owner := "alice"
// 覆盖 summarize 的正文。
if err := svc.Save(owner, core.PromptModule{ID: "task.summarize", Layer: core.LayerTask, Version: 1, Title: "总结", Body: "覆盖后的总结指令"}); err != nil {
t.Fatal(err)
}
m, ok, err := svc.Get(owner, "task.summarize")
if err != nil || !ok {
t.Fatal(err, ok)
}
if m.Body != "覆盖后的总结指令" || m.Drifted {
t.Errorf("override body=%q drifted=%v", m.Body, m.Drifted)
}
// 停用:不触发。
if err := svc.Save(owner, core.PromptModule{ID: "task.doc-minutes", Layer: core.LayerTask, Version: 1, Title: "纪要", Body: "x", Disabled: true}); err != nil {
t.Fatal(err)
}
_, hits, _ := svc.ResolveSystem(core.AgentProfile{}, TriggerCtx{Text: "生成会议纪要", HasAttachments: true}, EnvData{}, owner)
for _, h := range hits {
if h.ModuleID == "task.doc-minutes" && h.Injected {
t.Error("disabled module still injected")
}
}
// 删除覆盖 = 回到内置。
if err := svc.Delete(owner, "task.summarize"); err != nil {
t.Fatal(err)
}
m, _, _ = svc.Get(owner, "task.summarize")
if m.Body == "覆盖后的总结指令" {
t.Error("delete override did not restore builtin")
}
}
// 命令触发匹配(服务端改写入口)。
func TestMatchCommand(t *testing.T) {
svc := mustService(t)
mod, rest, ok, err := svc.MatchCommand("", "/minutes 关于季度会")
if err != nil || !ok {
t.Fatal(err, ok)
}
if mod.ID != "task.doc-minutes" || rest != "关于季度会" {
t.Errorf("mod=%s rest=%q", mod.ID, rest)
}
if _, _, ok, _ := svc.MatchCommand("", "普通消息"); ok {
t.Error("plain text should not match command")
}
if _, _, ok, _ := svc.MatchCommand("", "/minutesxx"); ok {
t.Error("prefix collision should not match")
}
}
// 子代理与工具文案取用。
func TestAgentAndToolPrompt(t *testing.T) {
svc := mustService(t)
if body, err := svc.AgentPrompt("", "agent.sub.general"); err != nil || !strings.Contains(body, "子代理") {
t.Errorf("agent prompt: %v %q", err, body)
}
if body, ok, _ := svc.ToolPrompt("", "knowledge_search"); !ok || !strings.Contains(body, "检索") {
t.Errorf("tool prompt: ok=%v %q", ok, body)
}
if _, ok, _ := svc.ToolPrompt("", "nonexistent"); ok {
t.Error("unknown tool should miss")
}
}
// Frontmatter 解析:块标量契约、触发列表、错误拒绝。
func TestParseModuleFile(t *testing.T) {
src := `---
id: task.x
layer: task
version: 3
title: 测试
priority: 80
requiresAttachments: true
outputContract: |
第一行
第二行
triggers:
- kind: intent
keywords: 甲, 乙
- kind: command
command: /x
---
正文内容`
m, err := ParseModuleFile("x.md", src)
if err != nil {
t.Fatal(err)
}
if m.Version != 3 || m.Priority != 80 || !m.RequiresAttachments {
t.Errorf("meta: %+v", m)
}
if m.OutputContract != "第一行\n第二行" {
t.Errorf("contract: %q", m.OutputContract)
}
if len(m.Triggers) != 2 || m.Triggers[0].Keywords[1] != "乙" || m.Triggers[1].Command != "/x" {
t.Errorf("triggers: %+v", m.Triggers)
}
if m.Body != "正文内容" {
t.Errorf("body: %q", m.Body)
}
if _, err := ParseModuleFile("bad.md", "---\nid: x\nlayer: task\nunknown: 1\n---\nb"); err == nil {
t.Error("unknown field should fail")
}
}
+328
View File
@@ -0,0 +1,328 @@
package prompts
import (
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"agent-system-backend/internal/store"
core "agent-system-core"
)
//go:embed library/*.md
var libraryFS embed.FS
// DefaultLibrary 解析内置提示词库(embed 文件);解析失败 = 构建期问题,启动即报。
func DefaultLibrary() ([]core.PromptModule, error) {
names, err := fs.Glob(libraryFS, "library/*.md")
if err != nil {
return nil, err
}
var out []core.PromptModule
for _, name := range names {
raw, err := libraryFS.ReadFile(name)
if err != nil {
return nil, err
}
m, err := ParseModuleFile(name, string(raw))
if err != nil {
return nil, err
}
out = append(out, m)
}
// 确定性顺序:库文件名即排序键(base-* 先于 env-* 先于 task-*)。
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
// TriggerCtx 触发上下文:当前轮用户输入与附件状态。
type TriggerCtx struct {
Text string
HasAttachments bool
}
// EnvData 环境段占位符数据。
type EnvData struct {
Date string
Weekday string
Model string
Attachments string // 已格式化的附件名列表(空 = "无")
}
// Service 提示词库服务:内置(embed)→ 用户(DB)覆盖链;触发匹配与 system 组装。
type Service struct {
st *store.Store
builtin []core.PromptModule
}
func NewService(st *store.Store, lib []core.PromptModule) (*Service, error) {
seen := map[string]bool{}
for _, m := range lib {
if seen[m.ID] {
return nil, fmt.Errorf("prompts: duplicate id %s", m.ID)
}
seen[m.ID] = true
}
return &Service{st: st, builtin: lib}, nil
}
// Effective 返回覆盖链合并后的模块全集(内置 + 用户行;用户覆盖内置同 ID)。
// UpdateAvailable = 用户行基于的内置版本落后于当前内置版本。
func (s *Service) Effective(owner string) ([]core.PromptModule, error) {
out := make([]core.PromptModule, len(s.builtin))
copy(out, s.builtin)
if s.st == nil {
return out, nil
}
user, err := s.st.ListPromptModules(owner)
if err != nil {
return nil, err
}
idx := map[string]int{}
for i, m := range out {
idx[m.ID] = i
}
for _, u := range user {
if i, ok := idx[u.ID]; ok {
base := out[i]
u.Source = string(core.SourceUser)
if u.BasedOnVersion < base.Version {
u.Drifted = true
}
out[i] = u
continue
}
u.Source = string(core.SourceUser)
out = append(out, u)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
// Get 单模块(覆盖链后视图)。
func (s *Service) Get(owner, id string) (core.PromptModule, bool, error) {
mods, err := s.Effective(owner)
if err != nil {
return core.PromptModule{}, false, err
}
for _, m := range mods {
if m.ID == id {
return m, true, nil
}
}
return core.PromptModule{}, false, nil
}
// Save 用户覆盖/新建:覆盖内置时记录 BasedOnVersion 供 drift 提示。
func (s *Service) Save(owner string, m core.PromptModule) error {
m.Source = string(core.SourceUser)
m.Owner = owner
for _, b := range s.builtin {
if b.ID == m.ID {
m.BasedOnVersion = b.Version
break
}
}
return s.st.SavePromptModule(owner, m)
}
// Delete 撤销用户覆盖(回到内置)。
func (s *Service) Delete(owner, id string) error { return s.st.DeletePromptModule(owner, id) }
// MatchCommand 命令触发匹配:文本以某技能命令开头时返回该模块与剩余文本。
func (s *Service) MatchCommand(owner, text string) (core.PromptModule, string, bool, error) {
mods, err := s.Effective(owner)
if err != nil {
return core.PromptModule{}, "", false, err
}
for _, m := range mods {
if m.Disabled {
continue
}
for _, t := range m.Triggers {
if t.Kind != core.TriggerCommand || t.Command == "" {
continue
}
if text == t.Command || strings.HasPrefix(text, t.Command+" ") {
rest := strings.TrimSpace(strings.TrimPrefix(text, t.Command))
return m, rest, true, nil
}
}
}
return core.PromptModule{}, "", false, nil
}
// matchModule 判定单模块是否命中(Triggers 任一命中即真;manual 永不自动命中)。
// command 触发视为显式用户意图:命中即真(不受 RequiresAttachments 约束)。
func matchModule(m core.PromptModule, tc TriggerCtx) (string, bool) {
for _, t := range m.Triggers {
switch t.Kind {
case core.TriggerCommand:
if t.Command != "" && (tc.Text == t.Command || strings.HasPrefix(tc.Text, t.Command+" ")) {
return "command:" + t.Command, true
}
case core.TriggerIntent:
for _, kw := range t.Keywords {
if kw != "" && strings.Contains(tc.Text, kw) {
return "intent:" + kw, true
}
}
case core.TriggerAttachment:
if !tc.HasAttachments {
continue
}
if len(t.Keywords) == 0 {
return "attachment", true
}
for _, kw := range t.Keywords {
if kw != "" && strings.Contains(tc.Text, kw) {
return "attachment:" + kw, true
}
}
}
}
return "", false
}
// ResolveSystem 组装 system 段。
// - 基座/环境/风格:画像 promptModules 引用(空 = 默认 base+env 全集,按 ID 序)
// - 技能(task):对全部生效技能做触发匹配;多命中取优先级最高,其余记录让位
// - RequiresAttachments 只约束 intent 触发(command 为显式意图,不设门槛)
func (s *Service) ResolveSystem(prof core.AgentProfile, tc TriggerCtx, env EnvData, owner string) ([]core.PromptBlock, []core.PromptHitRecord, error) {
mods, err := s.Effective(owner)
if err != nil {
return nil, nil, err
}
byID := map[string]core.PromptModule{}
for _, m := range mods {
byID[m.ID] = m
}
// 基座选取:显式引用(可裁剪基座/启用风格块)或缺省 base+env 全集。
// style 是表达变体,默认不开,须画像 promptModules 显式引用。
pick := func(ids []string) []core.PromptModule {
allowed := []string{core.LayerBase, core.LayerEnv, core.LayerStyle}
if len(ids) > 0 {
var out []core.PromptModule
for _, id := range ids {
if m, ok := byID[id]; ok && !m.Disabled && contains(allowed, m.Layer) {
out = append(out, m)
}
}
return out
}
var out []core.PromptModule
for _, m := range mods {
if m.Disabled || !contains([]string{core.LayerBase, core.LayerEnv}, m.Layer) {
continue
}
out = append(out, m)
}
return out
}
var blocks []core.PromptBlock
var hits []core.PromptHitRecord
addBlock := func(m core.PromptModule, reason string, injected bool) {
hits = append(hits, core.PromptHitRecord{ModuleID: m.ID, Layer: m.Layer, Reason: reason, Injected: injected})
if injected {
blocks = append(blocks, core.PromptBlock{ModuleID: m.ID, Version: m.Version, Layer: m.Layer, Body: resolvePlaceholders(m.Body, env)})
}
}
for _, m := range pick(prof.PromptModules) {
addBlock(m, "always", true)
}
// 技能匹配:task 层全员参与(含未列入画像引用的——技能是系统能力,画像定基座)。
type candidate struct {
m core.PromptModule
reason string
}
var matched []candidate
for _, m := range mods {
if m.Disabled || m.Layer != core.LayerTask {
continue
}
reason, ok := matchModule(m, tc)
if !ok {
continue
}
// 附件门槛只约束 intent 命中;command 是显式用户意图,不设门槛。
if m.RequiresAttachments && !tc.HasAttachments && !strings.HasPrefix(reason, "command:") {
continue
}
matched = append(matched, candidate{m, reason})
}
sort.SliceStable(matched, func(i, j int) bool {
return priority(matched[i].m) > priority(matched[j].m)
})
for i, c := range matched {
if i == 0 {
addBlock(c.m, c.reason, true)
continue
}
addBlock(c.m, "superseded-by:"+matched[0].m.ID, false)
}
return blocks, hits, nil
}
// priority 缺省 50;大者优先。
func priority(m core.PromptModule) int {
if m.Priority == 0 {
return 50
}
return m.Priority
}
// AgentPrompt 取子代理/角色提示词(agent 层;缺省回落 general)。
func (s *Service) AgentPrompt(owner, id string) (string, error) {
m, ok, err := s.Get(owner, id)
if err != nil {
return "", err
}
if !ok || m.Layer != core.LayerAgent {
return "", fmt.Errorf("prompts: agent module %q not found", id)
}
return m.Body, nil
}
// ToolPrompt 取工具描述文案(tool 层;found=false 表示库未覆盖该工具,调用方回落注册表描述)。
func (s *Service) ToolPrompt(owner, toolName string) (string, bool, error) {
m, ok, err := s.Get(owner, "tool."+toolName)
if err != nil || !ok {
return "", false, err
}
return m.Body, true, nil
}
// resolvePlaceholders 填充 {{key}};未知占位符原样保留(可见即信号)。
func resolvePlaceholders(body string, env EnvData) string {
repl := map[string]string{
"date": env.Date,
"weekday": env.Weekday,
"model": env.Model,
"attachments": env.Attachments,
}
if repl["attachments"] == "" {
repl["attachments"] = "无"
}
for k, v := range repl {
if v == "" {
v = "未知" // 日期/星期/模型缺失不裸露占位符
}
body = strings.ReplaceAll(body, "{{"+k+"}}", v)
}
return body
}
func contains(list []string, v string) bool {
for _, s := range list {
if s == v {
return true
}
}
return false
}
+118 -12
View File
@@ -4,17 +4,22 @@ import (
"encoding/json"
"fmt"
"strings"
"time"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
"agent-system-backend/internal/ctxasm"
"agent-system-backend/internal/prompts"
"agent-system-backend/internal/squadadapter"
)
// LLMRunner 主对话执行器:复用 agent-native 的 OpenAI 兼容流式客户端(不自研接入层)。
// 多轮循环:流式 → 工具调用(画像策略)→ 回灌结果 → 直至无工具调用或预算耗尽。
// 内置三个编排型工具(不经注册表):delegate_task(子代理)、update_plan、update_todos。
type LLMRunner struct{}
type LLMRunner struct {
// Prompts 提示词库(nil = 回退画像 SystemPrompt 单段,兼容测试与骨架)。
Prompts *prompts.Service
}
func (l *LLMRunner) Name() string { return "llm" }
@@ -25,6 +30,83 @@ const (
toolSquad = "squad_orchestrate"
)
// resolveSystem 经提示词库组装 system 段(基座+环境+命中技能),并产出可观测条目。
func (l *LLMRunner) resolveSystem(rc RunCtx, model string) ([]agentllm.Message, []string) {
userText := currentUserText(rc)
atts := rc.Attachments()
names := make([]string, len(atts))
for i, a := range atts {
names[i] = a.Meta.Filename
}
now := time.Now()
env := prompts.EnvData{
Date: now.Format("2006-01-02"),
Weekday: weekdayCN(now),
Model: model,
}
if len(names) > 0 {
env.Attachments = strings.Join(names, "、")
}
prof := rc.Profile()
blocks, hits, err := l.Prompts.ResolveSystem(prof, prompts.TriggerCtx{
Text: userText, HasAttachments: len(atts) > 0,
}, env, prof.Owner)
if err != nil {
return nil, []string{"prompt(库解析失败:" + err.Error() + ")"}
}
msgs := make([]agentllm.Message, 0, len(blocks))
var obs []string
for _, b := range blocks {
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleSystem, Content: b.Body})
}
for _, h := range hits {
if h.Injected {
obs = append(obs, fmt.Sprintf("prompt(%s v%d · %s)", h.ModuleID, versionOf(blocks, h.ModuleID), h.Reason))
} else {
obs = append(obs, fmt.Sprintf("skill-yield(%s · %s)", h.ModuleID, h.Reason))
}
}
return msgs, obs
}
// toolDesc 工具描述:库覆盖(tool.<name>)优先,回落注册表文案。
func (l *LLMRunner) toolDesc(owner, name, fallback string) string {
if l.Prompts != nil {
if body, ok, err := l.Prompts.ToolPrompt(owner, name); err == nil && ok && strings.TrimSpace(body) != "" {
return body
}
}
return fallback
}
func versionOf(blocks []core.PromptBlock, id string) int {
for _, b := range blocks {
if b.ModuleID == id {
return b.Version
}
}
return 0
}
func weekdayCN(t time.Time) string {
switch t.Weekday() {
case time.Sunday:
return "周日"
case time.Monday:
return "周一"
case time.Tuesday:
return "周二"
case time.Wednesday:
return "周三"
case time.Thursday:
return "周四"
case time.Friday:
return "周五"
default:
return "周六"
}
}
func (l *LLMRunner) Execute(rc RunCtx) error {
eff := rc.Settings()
prof := rc.Profile()
@@ -53,25 +135,32 @@ func (l *LLMRunner) Execute(rc RunCtx) error {
break
}
}
msgs, asmReport := ctxasm.AssembleInput(prof, eff, hist, currentUser, rc.Attachments(), knowledge)
var sys []agentllm.Message
var promptObs []string
if l.Prompts != nil {
sys, promptObs = l.resolveSystem(rc, route.Model)
}
msgs, asmReport := ctxasm.AssembleInput(prof, eff, hist, currentUser, rc.Attachments(), knowledge, sys, promptObs)
if asmReport != nil {
rc.SetContextObservation(core.ContextObservation{
Items: asmReport.Items, Truncations: asmReport.Truncations, EstChars: asmReport.EstChars,
})
}
// 声明工具:编排型内置 + 注册表中画像允许的工具。
// 声明工具:编排型内置 + 注册表中画像允许的工具;描述文案库覆盖优先。
decls := []agentllm.ToolDecl{
{Name: toolSquad, Description: "用小队编排引擎执行多步骤目标:规则规划器拆解为 梳理→专项→成文→终审 步骤并逐步执行(每步一个子代理),挂起决策会以提问卡片征求用户", Parameters: json.RawMessage(`{"type":"object","properties":{"goal":{"type":"string","description":"要完成的整体目标描述"}},"required":["goal"]}`)},
{Name: toolDelegate, Description: "派生一个子代理执行独立子任务(如检索、撰写),返回其结果摘要", Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string","description":"子代理角色名"},"task":{"type":"string","description":"子任务描述"},"hint":{"type":"string","description":"执行提示"}},"required":["name","task"]}`)},
{Name: toolPlan, Description: "创建或更新当前会话的计划(步骤列表)", Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done","skipped"]}},"required":["id","text","status"]}}},"required":["title","steps"]}`)},
{Name: toolTodos, Description: "更新当前会话的待办清单", Parameters: json.RawMessage(`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done"]}},"required":["id","text","status"]}}},"required":["items"]}`)},
{Name: toolSquad, Description: l.toolDesc(prof.Owner, toolSquad, "用小队编排引擎执行多步骤目标:规则规划器拆解为 梳理→专项→成文→终审 步骤并逐步执行(每步一个子代理),挂起决策会以提问卡片征求用户"), Parameters: json.RawMessage(`{"type":"object","properties":{"goal":{"type":"string","description":"要完成的整体目标描述"}},"required":["goal"]}`)},
{Name: toolDelegate, Description: l.toolDesc(prof.Owner, toolDelegate, "派生一个子代理执行独立子任务(如检索、撰写),返回其结果摘要"), Parameters: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string","description":"子代理角色名"},"task":{"type":"string","description":"子任务描述"},"hint":{"type":"string","description":"执行提示"}},"required":["name","task"]}`)},
{Name: toolPlan, Description: l.toolDesc(prof.Owner, toolPlan, "创建或更新当前会话的计划(步骤列表)"), Parameters: json.RawMessage(`{"type":"object","properties":{"title":{"type":"string"},"steps":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done","skipped"]}},"required":["id","text","status"]}}},"required":["title","steps"]}`)},
{Name: toolTodos, Description: l.toolDesc(prof.Owner, toolTodos, "更新当前会话的待办清单"), Parameters: json.RawMessage(`{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"text":{"type":"string"},"status":{"type":"string","enum":["pending","in_progress","done"]}},"required":["id","text","status"]}}},"required":["items"]}`)},
}
var toolDescs []core.ToolDesc
for _, t := range toolList(rc) {
if prof.Tools.ToolAllowed(t.Name) {
toolDescs = append(toolDescs, t)
decls = append(decls, agentllm.ToolDecl{Name: t.Name, Description: t.Description, Parameters: t.Parameters})
td := t
td.Description = l.toolDesc(prof.Owner, td.Name, td.Description)
toolDescs = append(toolDescs, td)
decls = append(decls, agentllm.ToolDecl{Name: td.Name, Description: td.Description, Parameters: td.Parameters})
}
}
@@ -241,11 +330,28 @@ func (l *LLMRunner) Execute(rc RunCtx) error {
return fmt.Errorf("budget: 轮次超过上限 %d(profile.budget.maxTurns)", maxTurns)
}
// SubLLMRunner 子代理执行器:轻量模型单轮完成任务并返回摘要。
type SubLLMRunner struct{}
// SubLLMRunner 子代理执行器:轻量模型单轮完成任务并返回结果。
type SubLLMRunner struct {
// Prompts 提示词库(nil = 使用内联回退文案,兼容测试与骨架)。
Prompts *prompts.Service
}
func (l *SubLLMRunner) Name() string { return "llm-subagent" }
// subagentPrompt 子代理 system:库 agent.sub.general 优先;派生方 hint(如小队角色)
// 已含角色要求时(hint 以库角色 ID 标记)换用对应角色模块。
const subagentFallback = "你是被派生的子代理。独立完成给定子任务,输出完整、结构化的结果;需要分点时分条列出,不要为了简短而省略关键内容。"
func (l *SubLLMRunner) subagentPrompt(rc RunCtx) string {
if l.Prompts == nil {
return subagentFallback
}
if body, err := l.Prompts.AgentPrompt(rc.Profile().Owner, "agent.sub.general"); err == nil && strings.TrimSpace(body) != "" {
return body
}
return subagentFallback
}
func (l *SubLLMRunner) Execute(rc RunCtx) error {
route := ResolveLightweightModel(rc.Profile(), rc.Settings())
if route.Model == "" || route.Base == "" {
@@ -254,7 +360,7 @@ func (l *SubLLMRunner) Execute(rc RunCtx) error {
rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: "执行子任务……", Phase: core.PhaseActing})
client := &agentllm.Client{BaseURL: route.Base, APIKey: route.Key}
msgs := []agentllm.Message{
{Role: agentllm.RoleSystem, Content: "你是被派生的子代理。独立完成给定子任务,输出完整、结构化的结果;需要分点时分条列出,不要为了简短而省略关键内容。"},
{Role: agentllm.RoleSystem, Content: l.subagentPrompt(rc)},
{Role: agentllm.RoleUser, Content: rc.Subtask()},
}
var sb strings.Builder
+75 -1
View File
@@ -22,6 +22,7 @@ import (
"agent-system-backend/internal/ingest"
"agent-system-backend/internal/plugins"
"agent-system-backend/internal/profiles"
"agent-system-backend/internal/prompts"
"agent-system-backend/internal/ratelimit"
"agent-system-backend/internal/runmgr"
"agent-system-backend/internal/settings"
@@ -42,6 +43,7 @@ type Server struct {
Cards *cardsvc.Service // 收件箱任务卡(nil = 不启用;组合根注入)
Bus *cmdbus.Bus // 命令总线(nil = 不启用;组合根注入;四入口同源 D27)
Gate NLIntentGate // 意图闸门(chat 入口同源判定;nil = 不接)
Prompts *prompts.Service // 提示词库(nil = 不启用 API 与技能命令;执行器另有回退)
reg *tools.Registry
prof *profiles.Service
set *settings.Service
@@ -163,6 +165,9 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/profiles", s.listProfiles)
mux.HandleFunc("POST /api/profiles", s.saveProfile)
mux.HandleFunc("DELETE /api/profiles/{id}", s.deleteProfile)
mux.HandleFunc("GET /api/prompts", s.listPromptModules)
mux.HandleFunc("POST /api/prompts", s.savePromptModule)
mux.HandleFunc("DELETE /api/prompts/{id}", s.deletePromptModule)
mux.HandleFunc("GET /api/settings", s.getSettings)
mux.HandleFunc("PUT /api/settings/user", s.putSettingsUser)
mux.HandleFunc("PUT /api/settings/project/{id}", s.putSettingsProject)
@@ -441,8 +446,18 @@ func (s *Server) postMessage(w http.ResponseWriter, r *http.Request) {
return
}
// 技能命令:提示词库命令触发命中时保持原文、跳过命令路由,走正常 run——
// system 组装时命令触发照常命中(命令是显式意图,不受附件门槛约束);
// 技能命令是提示词层能力,不进命令总线。
skillCommand := false
if s.Prompts != nil && strings.HasPrefix(text, "/") {
if _, _, ok, err := s.Prompts.MatchCommand(owner, text); err == nil && ok {
skillCommand = true
}
}
// 命令路由:/ 开头走内置命令,不启动 run(4.11 输入增强)。
if strings.HasPrefix(text, "/") {
if strings.HasPrefix(text, "/") && !skillCommand {
s.handleCommand(w, r, conv, text)
return
}
@@ -1004,10 +1019,69 @@ func (s *Server) listCommands(w http.ResponseWriter, r *http.Request) {
out = append(out, item)
}
}
// 提示词库技能命令(命令触发型 task 模块):进 `/` 菜单同源展示(发送时由
// postMessage 改写为技能任务,不走命令总线)。
if s.Prompts != nil {
mods, err := s.Prompts.Effective(s.owner(r))
if err == nil {
for _, m := range mods {
if m.Disabled || m.Layer != core.LayerTask {
continue
}
for _, t := range m.Triggers {
if t.Kind == core.TriggerCommand && t.Command != "" {
out = append(out, map[string]any{
"id": m.ID, "name": t.Command, "title": m.Title,
"description": m.Description, "source": "prompt-skill", "enabled": true,
})
break
}
}
}
}
}
writeJSON(w, http.StatusOK, out)
}
// ---- 提示词库 API(覆盖链:内置 embed → 用户 DB 行)----
func (s *Server) listPromptModules(w http.ResponseWriter, r *http.Request) {
if s.Prompts == nil {
http.Error(w, "prompt library not enabled", http.StatusServiceUnavailable)
return
}
mods, err := s.Prompts.Effective(s.owner(r))
writeJSONOr(w, mods, err)
}
func (s *Server) savePromptModule(w http.ResponseWriter, r *http.Request) {
if s.Prompts == nil {
http.Error(w, "prompt library not enabled", http.StatusServiceUnavailable)
return
}
var m core.PromptModule
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if m.ID == "" || m.Layer == "" || strings.TrimSpace(m.Body) == "" {
http.Error(w, "id/layer/body required", http.StatusBadRequest)
return
}
if err := s.Prompts.Save(s.owner(r), m); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, m)
}
func (s *Server) deletePromptModule(w http.ResponseWriter, r *http.Request) {
if s.Prompts == nil {
http.Error(w, "prompt library not enabled", http.StatusServiceUnavailable)
return
}
writeJSONOr(w, map[string]any{"deleted": r.PathValue("id")}, s.Prompts.Delete(s.owner(r), r.PathValue("id")))
}
func (s *Server) listParsers(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, ingest.ParsersDescription())
+16 -4
View File
@@ -262,16 +262,28 @@ func isHoldStatus(s string) bool {
return s == "waiting_human" || s == "awaiting_confirmation" || s == "awaiting_clarification" || s == "quality_review"
}
// RoleInstructions 小队角色指令来源:app 装配时注入提示词库查找
// (agent.squad.<kind>);nil 或未命中 = 使用内联回退文案(测试与骨架兼容)。
var RoleInstructions func(kind string) (string, bool)
func builtinRoles() []squad.RoleSpec {
instr := func(kind, fallback string) string {
if RoleInstructions != nil {
if body, ok := RoleInstructions(kind); ok && strings.TrimSpace(body) != "" {
return body
}
}
return fallback
}
return []squad.RoleSpec{
{Role: "planner", Name: "规划员", Backend: "agent-system", BashPermission: "deny",
Keywords: []string{"梳理", "规划", "分析"}, Instructions: "梳理目标与材料,产出结构化要点与执行建议。"},
Keywords: []string{"梳理", "规划", "分析"}, Instructions: instr("planner", "梳理目标与材料,产出结构化要点与执行建议。")},
{Role: "researcher", Name: "调研员", Backend: "agent-system", BashPermission: "deny",
Keywords: []string{"调研", "检索", "资料"}, Instructions: "围绕目标调研关键问题,输出简明结论与依据。"},
Keywords: []string{"调研", "检索", "资料"}, Instructions: instr("researcher", "围绕目标调研关键问题,输出简明结论与依据。")},
{Role: "writer", Name: "撰稿员", Backend: "agent-system", BashPermission: "deny",
Keywords: []string{"撰写", "成文", "报告"}, Instructions: "把前序要点与调研结论整理为清晰成文的交付文档。"},
Keywords: []string{"撰写", "成文", "报告"}, Instructions: instr("writer", "把前序要点与调研结论整理为清晰成文的交付文档。")},
{Role: "reviewer", Name: "审校员", Backend: "agent-system", BashPermission: "deny",
Keywords: []string{"审校", "把关", "终审"}, Instructions: "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。"},
Keywords: []string{"审校", "把关", "终审"}, Instructions: instr("reviewer", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")},
}
}
+9
View File
@@ -228,6 +228,15 @@ func init() {
`CREATE INDEX IF NOT EXISTS idx_audit_op ON command_audit(operation_id)`,
}},
down: map[string][]string{"sqlite": {`DROP TABLE IF EXISTS command_audit`}, "postgres": {`DROP TABLE IF EXISTS command_audit`}}},
migration{version: 8, name: "prompt_modules",
// 提示词库用户覆盖(core.PromptModule):内置在 embed 库,本表只存
// 用户副本/覆盖行(BasedOnVersion 供上游升级 drift 提示)。
up: map[string][]string{"sqlite": {
`CREATE TABLE IF NOT EXISTS prompt_modules (id TEXT NOT NULL, owner TEXT NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(id, owner))`,
}, "postgres": {
`CREATE TABLE IF NOT EXISTS prompt_modules (id TEXT NOT NULL, owner TEXT NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(id, owner))`,
}},
down: map[string][]string{"sqlite": {`DROP TABLE IF EXISTS prompt_modules`}, "postgres": {`DROP TABLE IF EXISTS prompt_modules`}}},
)
}
+63
View File
@@ -0,0 +1,63 @@
package store
import (
"encoding/json"
core "agent-system-core"
)
// SavePromptModule 写用户提示词模块(覆盖或新建;按 owner 隔离)。
func (s *Store) SavePromptModule(owner string, m core.PromptModule) error {
if m.Owner == "" {
m.Owner = owner
}
payload, err := json.Marshal(m)
if err != nil {
return err
}
_, err = s.db.Exec(s.q(`INSERT INTO prompt_modules(id, owner, payload) VALUES(?,?,?)
ON CONFLICT(id, owner) DO UPDATE SET payload=excluded.payload`), m.ID, owner, payload)
return err
}
// ListPromptModules 返回该 owner 的用户模块(不含内置——内置在 embed 库)。
func (s *Store) ListPromptModules(owner string) ([]core.PromptModule, error) {
rows, err := s.db.Query(s.q(`SELECT payload FROM prompt_modules WHERE owner=?`), owner)
if err != nil {
return nil, err
}
defer rows.Close()
var out []core.PromptModule
for rows.Next() {
var payload []byte
if err := rows.Scan(&payload); err != nil {
return nil, err
}
var m core.PromptModule
if err := json.Unmarshal(payload, &m); err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}
// GetPromptModule 读单个用户模块。
func (s *Store) GetPromptModule(owner, id string) (core.PromptModule, error) {
var payload []byte
err := s.db.QueryRow(s.q(`SELECT payload FROM prompt_modules WHERE id=? AND owner=?`), id, owner).Scan(&payload)
if err != nil {
return core.PromptModule{}, err
}
var m core.PromptModule
if err := json.Unmarshal(payload, &m); err != nil {
return core.PromptModule{}, err
}
return m, nil
}
// DeletePromptModule 删除用户模块(内置不受影响)。
func (s *Store) DeletePromptModule(owner, id string) error {
_, err := s.db.Exec(s.q(`DELETE FROM prompt_modules WHERE id=? AND owner=?`), id, owner)
return err
}
+2 -2
View File
@@ -56,8 +56,8 @@ func TestMigrationUpgradesExistingDatabase(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(got) != 7 {
t.Fatalf("expected versions [1..7], got %v", got)
if len(got) != 8 {
t.Fatalf("expected versions [1..8], got %v", got)
}
// 老库形态(v2 前):events 全局自增、settings 单列主键。
if err := st.MigrateDown(1); err != nil {