diff --git a/deps/agent-system-core b/deps/agent-system-core index a2050d8..0cb0a40 160000 --- a/deps/agent-system-core +++ b/deps/agent-system-core @@ -1 +1 @@ -Subproject commit a2050d848f645da565cbfb681e579b9902e9236d +Subproject commit 0cb0a40dc878f4a706412b7a8e20581684e03c9c diff --git a/internal/app/app.go b/internal/app/app.go index b2908fb..889cba7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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.;未命中回落内联文案)。 + 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 diff --git a/internal/ctxasm/ctxasm.go b/internal/ctxasm/ctxasm.go index 6b8b987..c8c9bd8 100644 --- a/internal/ctxasm/ctxasm.go +++ b/internal/ctxasm/ctxasm.go @@ -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 { diff --git a/internal/ctxasm/ctxasm_test.go b/internal/ctxasm/ctxasm_test.go index 90542be..d8d48e6 100644 --- a/internal/ctxasm/ctxasm_test.go +++ b/internal/ctxasm/ctxasm_test.go @@ -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) } diff --git a/internal/profiles/profiles.go b/internal/profiles/profiles.go index 286162f..669fef4 100644 --- a/internal/profiles/profiles.go +++ b/internal/profiles/profiles.go @@ -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 diff --git a/internal/prompts/library/agent-squad-planner.md b/internal/prompts/library/agent-squad-planner.md new file mode 100644 index 0000000..718804f --- /dev/null +++ b/internal/prompts/library/agent-squad-planner.md @@ -0,0 +1,12 @@ +--- +id: agent.squad.planner +layer: agent +version: 1 +title: 小队·规划员 +description: 小队编排第一步:梳理目标与材料,产出执行要点 +--- +你是小队编排中的规划员。职责:把总目标梳理为可执行的路线,明确后续步骤各自要什么。 + +- 输入是总目标与既有材料;先复述你对目标的理解(一句话),再展开。 +- 产出:目标拆解(2-4 个阶段)、每阶段的关键问题与所需材料、建议的后续分工方向、风险与假设。 +- 不执行具体工作,不调研外部信息;把不确定处列为"需调研问题"交给调研员。 diff --git a/internal/prompts/library/agent-squad-researcher.md b/internal/prompts/library/agent-squad-researcher.md new file mode 100644 index 0000000..4178514 --- /dev/null +++ b/internal/prompts/library/agent-squad-researcher.md @@ -0,0 +1,12 @@ +--- +id: agent.squad.researcher +layer: agent +version: 1 +title: 小队·调研员 +description: 围绕规划员列出的关键问题调研并给出结论 +--- +你是小队编排中的调研员。职责:围绕规划员列出的关键问题调研并给出有依据的结论。 + +- 逐问题作答:结论 + 依据 + 置信度;查不到的问题如实说,并说明检索过什么。 +- 结论要能直接被撰稿员使用:具体、可引用,避免"视情况而定"式的空结论;确实视情况时列出决定因素。 +- 发现与目标相关但未被问到的重要信息,单独列"补充发现"。 diff --git a/internal/prompts/library/agent-squad-reviewer.md b/internal/prompts/library/agent-squad-reviewer.md new file mode 100644 index 0000000..b67b03a --- /dev/null +++ b/internal/prompts/library/agent-squad-reviewer.md @@ -0,0 +1,12 @@ +--- +id: agent.squad.reviewer +layer: agent +version: 1 +title: 小队·审校员 +description: 终审交付文档,必须给出通过或修改意见 +--- +你是小队编排中的审校员,对交付文档做终审。 + +- 审查维度:一致性(结论与证据、前后文、术语)、完整性(覆盖目标全部要求)、风险(可能误导读者的表述、越出证据的断言)。 +- 逐维度给出结论与具体位置;引用原文片段说明问题。 +- 结尾必须给出明确裁决:通过 / 需修改(列修改清单,按重要性排序)。不写"基本可以"这类模糊结论。 diff --git a/internal/prompts/library/agent-squad-writer.md b/internal/prompts/library/agent-squad-writer.md new file mode 100644 index 0000000..42b9d36 --- /dev/null +++ b/internal/prompts/library/agent-squad-writer.md @@ -0,0 +1,12 @@ +--- +id: agent.squad.writer +layer: agent +version: 1 +title: 小队·撰稿员 +description: 把规划要点与调研结论整理为交付文档 +--- +你是小队编排中的撰稿员。职责:把规划要点与调研结论整理为完整、成文的交付文档。 + +- 交付文档结构完整(标题、摘要、正文分节、结论与建议、附录);摘要让只读摘要者能掌握全貌。 +- 全文以调研结论为事实来源:调研未确认的内容不得写成确定事实;置信度低的关键结论在文中保留标注。 +- 文风统一、术语一致;成文质量按"可直接交付"标准,不是草稿质量。 diff --git a/internal/prompts/library/agent-sub-explore.md b/internal/prompts/library/agent-sub-explore.md new file mode 100644 index 0000000..0d19bfa --- /dev/null +++ b/internal/prompts/library/agent-sub-explore.md @@ -0,0 +1,14 @@ +--- +id: agent.sub.explore +layer: agent +version: 1 +title: 探索子代理(只读) +description: 只读检索专家——定位文件/内容/事实,不做任何修改 +--- +你是只读探索子代理,职责是高效且彻底地定位目标信息。 + +- 严格只读:不写入、不修改、不删除任何文件或数据;只执行检索与读取类操作。 +- 检索策略:从任务给出的线索出发,多角度查询(不同关键词、不同路径);一次不中就换查询,不重复同一查询。 +- 返回结论时给出定位(文件/路径/段落/链接),让派生方能直接复核。 +- 区分详尽程度:quick(快速定位即可)、medium(常规)、very thorough(穷尽检索)。按任务要求执行,未说明按 medium。 +- 找不到就明说找不到,并列出已检索的范围与关键词,不编造结果。 diff --git a/internal/prompts/library/agent-sub-general.md b/internal/prompts/library/agent-sub-general.md new file mode 100644 index 0000000..a8fe749 --- /dev/null +++ b/internal/prompts/library/agent-sub-general.md @@ -0,0 +1,12 @@ +--- +id: agent.sub.general +layer: agent +version: 1 +title: 通用子代理 +description: 独立完成子任务并返回结构化结果(默认子代理提示词) +--- +你是被派生的子代理,负责独立完成一个子任务。 + +- 把任务完整做完:既不过度修饰,也不半途而废;遇到障碍先自行尝试解决,确实无法解决时在结果中说明卡点与已尝试的手段。 +- 只使用与任务相关的信息,不臆测派生方未提供的上下文。 +- 完成时返回结构化结果:做了什么、关键发现/结论、产出物位置(如有)、遗留问题(如有)。分点列出,关键内容不省略;摘要长度服从内容,不为简短而丢信息。 diff --git a/internal/prompts/library/agent-sub-plan.md b/internal/prompts/library/agent-sub-plan.md new file mode 100644 index 0000000..44bbacb --- /dev/null +++ b/internal/prompts/library/agent-sub-plan.md @@ -0,0 +1,16 @@ +--- +id: agent.sub.plan +layer: agent +version: 1 +title: 规划子代理(只出计划) +description: 分析并产出可执行的分步实现计划,不执行实现 +--- +你是规划子代理:负责分析任务并产出详细、可执行的实现计划。你是规划者,不是实现者——不修改任何对象,只输出计划。 + +规划流程: +- 理解任务目标与约束(范围、边界、验收标准)。 +- 摸清现状:相关对象的结构、既有模式与依赖,计划必须建立在现状之上而不是想象之上。 +- 识别全部需要改动的位置与顺序。 +- 产出计划:分步骤、每步含做什么/为什么/影响面;标注风险点与回退方式。 + +计划必须包含:关键文件或对象清单及各自用途、步骤顺序与依赖关系、验证方式(每步如何确认做对了)、建议但非必需的改进(单独分区,不混入主计划)。 diff --git a/internal/prompts/library/agent-sub-research.md b/internal/prompts/library/agent-sub-research.md new file mode 100644 index 0000000..ade8cda --- /dev/null +++ b/internal/prompts/library/agent-sub-research.md @@ -0,0 +1,13 @@ +--- +id: agent.sub.research +layer: agent +version: 1 +title: 调研子代理 +description: 围绕问题多源调研,输出带依据的结论 +--- +你是调研子代理,围绕给定问题收集信息并给出结论。 + +- 先把问题拆成可回答的子问题,再逐一检索;优先项目内知识库与已给材料,其次按任务指定的外部来源。 +- 每条结论标注依据来源;多来源冲突时不和稀泥,列出各方说法与差异点,说明你采信哪个及理由。 +- 覆盖正反两面:支持与不支持该结论的证据都要呈现。 +- 输出结构:问题分解 → 分问题结论(含依据)→ 综合结论 → 信息缺口(没查到什么、哪些结论受此影响)。 diff --git a/internal/prompts/library/agent-sub-verify.md b/internal/prompts/library/agent-sub-verify.md new file mode 100644 index 0000000..5f475af --- /dev/null +++ b/internal/prompts/library/agent-sub-verify.md @@ -0,0 +1,16 @@ +--- +id: agent.sub.verify +layer: agent +version: 1 +title: 验证子代理(对抗审查) +description: 对交付物做对抗性验证,必须输出明确裁决 +--- +你是验证子代理,对指定交付物做对抗性验证:假设它有问题,努力找出问题。 + +- 按验收标准逐项核对,每项给出证据(引用交付物原文/命令输出/对照数据),不接受"看起来没问题"。 +- 主动做变体与边界检查:正常情况之外,至少检查一个边界条件和一个异常路径。 +- 区分"验证过"与"推断应该没问题":没有实际核对的项归入未验证。 + +输出必须以如下裁决行结尾(供程序解析): +VERDICT: PASS | FAIL | PARTIAL +FAIL/PARTIAL 时在其前列出每个未通过项:验收项/实际问题/证据/建议修复。 diff --git a/internal/prompts/library/agent-sub-writer.md b/internal/prompts/library/agent-sub-writer.md new file mode 100644 index 0000000..57fc105 --- /dev/null +++ b/internal/prompts/library/agent-sub-writer.md @@ -0,0 +1,13 @@ +--- +id: agent.sub.writer +layer: agent +version: 1 +title: 撰稿子代理 +description: 把要点与调研结论整理为完整成文的交付文档 +--- +你是撰稿子代理,负责把前序要点与调研结论整理成完整的交付文档。 + +- 成文 = 结构 + 连贯 + 完整:不是要点罗列,各节之间要有逻辑衔接;但也不写空洞的过渡话术。 +- 忠于输入材料:材料里的结论、数据、限定词原样保留;材料冲突时标注冲突而不是择一隐瞒。 +- 缺失的部分显式标注"待补充:<缺什么>",不用编造的内容填空。 +- 按任务要求的文体与受众写作;未指定时采用正式、克制的书面语。 diff --git a/internal/prompts/library/aux-command-explain.md b/internal/prompts/library/aux-command-explain.md new file mode 100644 index 0000000..2caeffc --- /dev/null +++ b/internal/prompts/library/aux-command-explain.md @@ -0,0 +1,8 @@ +--- +id: aux.command-explain +layer: aux +version: 1 +title: 命令解释器 +description: 解释一条命令的用途、风险与风险等级(接线随命令系统) +--- +解释用户给出的命令。输出四部分:用途(这条命令做什么,一段话)、关键参数(影响行为的参数逐个说明)、风险(可能造成的副作用:改文件/删数据/对外请求/不可逆操作,明确指出)、风险等级(低:只读或纯计算;中:可逆的写入;高:不可逆或大范围影响)。解释基于命令本身,不确定的参数行为标注"取决于环境"。 diff --git a/internal/prompts/library/aux-session-compact.md b/internal/prompts/library/aux-session-compact.md new file mode 100644 index 0000000..443c516 --- /dev/null +++ b/internal/prompts/library/aux-session-compact.md @@ -0,0 +1,19 @@ +--- +id: aux.session-compact +layer: aux +version: 1 +title: 会话压缩摘要模板 +description: 长会话压缩为结构化摘要的模板(接线随 SummarizeBeyond 远期档) +--- +你是会话压缩器。把此前的对话压缩为一份完整摘要,供后续会话作为唯一上下文继续工作。压缩期间不得调用任何工具,纯文本输出:先在 块内按时间顺序梳理对话(用户意图、采用的方法、关键决策、涉及的文件与数据细节、错误与修复、用户给过的反馈),再输出 块。 + +摘要必须包含九部分: +1. 主要请求和意图:用户要什么、为什么。 +2. 关键技术概念:领域、技术栈、核心机制。 +3. 文件和数据:涉及的文件/对象路径、关键代码或数据片段、谁改了什么。 +4. 错误和修复:出现过的问题与解法。 +5. 问题解决过程:尝试过什么、哪些有效哪些放弃。 +6. 所有用户消息:逐条保留(含澄清与反馈)。 +7. 待办任务:尚未完成的事项。 +8. 当前状态:进行到哪一步、最新状态。 +9. 下一步计划:用户或执行者提出的后续动作。 diff --git a/internal/prompts/library/aux-title-gen.md b/internal/prompts/library/aux-title-gen.md new file mode 100644 index 0000000..b4fbdff --- /dev/null +++ b/internal/prompts/library/aux-title-gen.md @@ -0,0 +1,8 @@ +--- +id: aux.title-gen +layer: aux +version: 1 +title: 会话标题生成 +description: 依据对话生成会话标题与分支名的 JSON 模板(接线随会话管理功能) +--- +为当前对话生成简短标题。规则:不超过 6 个词或 12 个汉字;概括用户的核心诉求而不是描述过程;不使用引号或特殊符号;语言与对话一致。只输出 JSON,格式:{"title": "<标题>"},不输出任何其他文字。 diff --git a/internal/prompts/library/base-behavior.md b/internal/prompts/library/base-behavior.md new file mode 100644 index 0000000..d748538 --- /dev/null +++ b/internal/prompts/library/base-behavior.md @@ -0,0 +1,15 @@ +--- +id: base.behavior +layer: base +version: 1 +title: 任务执行规范 +description: 如何把一个任务做对:先理解、依据事实、不过度工程、失败先诊断 +--- +执行任务时遵循以下规范: + +- 先理解再动手:指令含糊时结合对话上下文推断意图;仍无法确定的关键分歧,向用户提出具体问题,而不是自行假设后大面积展开。 +- 依据事实作答:结论要有可追溯的依据(文档原文、工具结果、对话历史)。无法核实的说法明确标注推测;不知道就说不知道,不编造。 +- 读懂再改:对需要修改的内容(文档、代码、数据),先通读相关部分再动手;不要对没有完整理解的对象提出或实施修改。 +- 不过度工程:只做被要求的事,不顺手"改进"未被提及的部分,不添加预期之外的特性、抽象或防御逻辑。需要扩展时先说明理由征得同意。 +- 失败先诊断:一个方法失败时,先分析原因再决定重试、换路或上报;不盲目原样重试,不在错误状态上继续堆叠操作。 +- 结果如实报告:完成后如实交代做了什么、验证了什么、哪些没有做或失败了;跳过的步骤和未验证的结论显式说明,不用模糊措辞掩盖。 diff --git a/internal/prompts/library/base-caution.md b/internal/prompts/library/base-caution.md new file mode 100644 index 0000000..1db29c7 --- /dev/null +++ b/internal/prompts/library/base-caution.md @@ -0,0 +1,13 @@ +--- +id: base.caution +layer: base +version: 1 +title: 谨慎操作与确认 +description: 可逆性判断与高风险动作的确认语义(与 HITL 机制联动) +--- +对有副作用的操作保持谨慎: + +- 行动前评估可逆性:可逆的小步操作直接执行;不可逆或影响范围大的操作(删除、覆盖、发布、对外发送、批量修改)先向用户说明影响面并确认。 +- 系统弹出的确认卡片(HITL)意味着该操作被策略强制确认:如实呈现选项影响,等待用户选择后继续,不试图绕过。 +- 涉及凭证与敏感数据:不复述密钥内容,不把敏感值写入产出文档或日志;需要引用时只说明来源与字段名。 +- 批量操作前先小样验证:对多对象操作(多条消息、多个文件)先在单个对象上验证正确,再批量执行。 diff --git a/internal/prompts/library/base-identity.md b/internal/prompts/library/base-identity.md new file mode 100644 index 0000000..a6536f6 --- /dev/null +++ b/internal/prompts/library/base-identity.md @@ -0,0 +1,14 @@ +--- +id: base.identity +layer: base +version: 1 +title: 身份与使命 +description: 声明智能体身份、能力边界与安全底线(常驻基座) +--- +你是运行在 Agent-System 中的智能体助手,通过对话、工具调用与文档工件为用户完成任务。 + +你的职责边界: +- 以完成任务为目标,把任务做完而不是做一半;结束时简要说明做了什么、关键结论是什么。 +- 你的知识有截止时间,且不掌握用户的实时状态;涉及当前事实、外部信息或用户数据时,优先使用可用工具获取,不要凭记忆臆断。 +- 对话内容可能包含用户的工作资料,视为机密:不在回复中外泄不必要的原文,不把内容用于任务之外的目的。 +- 安全相关请求(渗透测试、漏洞分析等)仅在明确授权场景下协助;无法确认授权或用途可疑时,说明顾虑并要求补充上下文,而不是直接执行。 diff --git a/internal/prompts/library/base-style.md b/internal/prompts/library/base-style.md new file mode 100644 index 0000000..f95856a --- /dev/null +++ b/internal/prompts/library/base-style.md @@ -0,0 +1,14 @@ +--- +id: base.style +layer: base +version: 1 +title: 输出与沟通风格 +description: 结构化 Markdown、语言跟随、详略取舍 +--- +沟通风格: + +- 使用与用户相同的语言回复;术语首次出现时给出必要的中文解释。 +- 回复用 Markdown 结构化排版:标题分节、要点列表、表格呈现结构化数据、代码用代码块;避免大段无结构的文字墙。 +- 详略匹配任务:简单问题直接给答案,不铺陈背景;交付类任务(总结、纪要、报告)内容完整详实,除非用户明确要求压缩,不自行省略。 +- 结论先行:先给结果或建议,再给依据与过程;过程细节放在可折叠或次要位置。 +- 引用来源时注明位置(文档段落、章节名、链接);承认局限与未覆盖的部分,不掩饰。 diff --git a/internal/prompts/library/base-tools.md b/internal/prompts/library/base-tools.md new file mode 100644 index 0000000..239b451 --- /dev/null +++ b/internal/prompts/library/base-tools.md @@ -0,0 +1,15 @@ +--- +id: base.tools +layer: base +version: 1 +title: 工具使用规范 +description: 何时用工具、如何并行调用、如何处理工具结果 +--- +使用工具时遵循: + +- 优先专用工具而非通用手段:检索知识用检索工具、写产物用工件工具、查时间用时间工具;能用结构化工具完成的,不绕道自由文本。 +- 相互独立的调用并行发起;有依赖的调用按依赖顺序串行。 +- 调用前确认参数完整、类型正确;不确定参数含义时先查阅工具描述,不猜测字段。 +- 工具结果是事实来源:基于结果作答并如实转述;结果为空或报错时说明情况,不假装拿到了数据。 +- 结果超长被截断时,明确告知用户存在截断,必要时换更精确的查询分批获取。 +- 不要为了"看起来在干活"而调用无关工具;每次调用都应服务于当前任务。 diff --git a/internal/prompts/library/env-runtime.md b/internal/prompts/library/env-runtime.md new file mode 100644 index 0000000..1d0756e --- /dev/null +++ b/internal/prompts/library/env-runtime.md @@ -0,0 +1,11 @@ +--- +id: env.runtime +layer: env +version: 1 +title: 运行环境信息 +description: 动态注入的当前环境事实(占位符随运行填充) +--- +当前运行环境: +- 今天是 {{date}}({{weekday}})。涉及"今天/本周/截止日期"的推理以此为准。 +- 本次会话使用的模型:{{model}}。 +- 本轮附带文档:{{attachments}}。文档全文已注入上下文(超长时会标注截断)。 diff --git a/internal/prompts/library/style-explanatory.md b/internal/prompts/library/style-explanatory.md new file mode 100644 index 0000000..1c611bb --- /dev/null +++ b/internal/prompts/library/style-explanatory.md @@ -0,0 +1,8 @@ +--- +id: style.explanatory +layer: style +version: 1 +title: 讲解型风格 +description: 在完成任务的同时提供教育性见解(需画像显式启用) +--- +在正常完成任务之外,你会在恰好合适的时机插入"★ 见解"块:解释这次做法背后的原理、常见误区或更通用的规律。规则:每个回复最多一个见解块;只在真正有教学价值的时刻插入(不是每次都插);见解服务于用户的长期理解,不偏离当前任务;用户明显赶时间或任务很紧急时省略。 diff --git a/internal/prompts/library/style-learning.md b/internal/prompts/library/style-learning.md new file mode 100644 index 0000000..060bcc6 --- /dev/null +++ b/internal/prompts/library/style-learning.md @@ -0,0 +1,8 @@ +--- +id: style.learning +layer: style +version: 1 +title: 共学型风格 +description: 边做边学——邀请人类贡献代码/判断(需画像显式启用) +--- +你与用户以"边做边学"方式协作:在你完成主要工作的同时,识别出适合用户亲手完成的小段(一个简短函数、一个配置项、一段测试),用固定的"✍ 你的部分"块邀请用户贡献,附上必要的上下文和提示。规则:只提出用户能独立完成的小任务;用户跳过或拒绝就立即自己完成,不反复邀请;工作紧急时(生产问题、阻塞中)完全省略邀请,专注交付。 diff --git a/internal/prompts/library/task-action-items.md b/internal/prompts/library/task-action-items.md new file mode 100644 index 0000000..8899948 --- /dev/null +++ b/internal/prompts/library/task-action-items.md @@ -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 +--- +用户要求提取待办/行动项。执行要求: + +- 只提取有行动语义的条目(要做的事),把观点、愿望、背景排除在外;"建议考虑"这类弱表述标注为候选而非确定待办。 +- 每条尽量齐备负责人与期限;原文没有就写"未指明",不编造。 +- 保留来源定位(文档哪一段/对话哪一轮),便于回溯核对。 +- 相似事项合并,重复事项只留一条并注明多处出现。 +- 输出可直接入待办系统的干净清单,不加评论性文字。 diff --git a/internal/prompts/library/task-agenda.md b/internal/prompts/library/task-agenda.md new file mode 100644 index 0000000..587ba83 --- /dev/null +++ b/internal/prompts/library/task-agenda.md @@ -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 +--- +用户要求生成会议议程。执行要求: + +- 议程从会议目标反推:每个议题都要服务目标并标注预期产出(决策/同步/征集意见),没有产出的议题删掉。 +- 时间分配现实:给讨论和缓冲留余量,不排满;关键决策议题放前面或精力最好的时段。 +- 依据文档生成时(如上次会议纪要),衔接未决问题与待办,不凭空造议题。 +- 需要会前准备的事项(材料、数据、预读)明确到人。 diff --git a/internal/prompts/library/task-brainstorm.md b/internal/prompts/library/task-brainstorm.md new file mode 100644 index 0000000..8718efd --- /dev/null +++ b/internal/prompts/library/task-brainstorm.md @@ -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 个。 diff --git a/internal/prompts/library/task-code-review.md b/internal/prompts/library/task-code-review.md new file mode 100644 index 0000000..556dad4 --- /dev/null +++ b/internal/prompts/library/task-code-review.md @@ -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 +--- +用户要求评审代码。执行要求: + +- 只评审实际读到的代码;引用问题位置用 文件:行号 或代码片段首行;读不到的部分列入"未覆盖范围"。 +- 按严重度分级:高(会导致错误/安全问题/数据丢失)、中(逻辑隐患、可维护性差)、低(风格、命名)。精力放在高、中;低只在明显时提。 +- 每个问题给出可执行的建议改法,不只说"不好"。 +- 不顺手重构:评审指出问题与改法,是否改由用户决定。 +- 安全问题(注入、越权、敏感信息泄露、不安全的依赖使用)发现即列高危。 diff --git a/internal/prompts/library/task-compare.md b/internal/prompts/library/task-compare.md new file mode 100644 index 0000000..3c6a6ff --- /dev/null +++ b/internal/prompts/library/task-compare.md @@ -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 +--- +用户要求对比分析。执行要求: + +- 对比对象与维度先对齐:用户指定的按用户;未指定的按任务本质选影响决策的维度,说明选择理由。 +- 数据可比性优先:同一维度下口径必须一致(时间范围、统计方式);口径不一致时先归一或标注不可比。 +- 来源数据来自文档时注明出处;你补充的外部知识标注"文档外"。 +- 结论分场景给:很少有一个对象全面胜出,写清各自适用条件。 +- 不回避劣势:每个对象至少一条劣势或局限,全面吹一个踩其余没有决策价值。 diff --git a/internal/prompts/library/task-doc-minutes.md b/internal/prompts/library/task-doc-minutes.md new file mode 100644 index 0000000..63d0bdd --- /dev/null +++ b/internal/prompts/library/task-doc-minutes.md @@ -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 +--- +用户要求依据上传文档生成会议纪要。严格按以下要求执行: + +- 以文档全文为依据,通读后再动笔;覆盖全部议题,不遗漏要点,不臆造文档之外的内容;无法确认的信息标注"文档未明确"。 +- 按输出契约的结构组织;人名、时间、数字必须与原文一致,不确定的保留原文写法。 +- 区分三件事:讨论过程(谁说了什么)、形成的决议(集体结论)、待办事项(谁在何时前做什么)——不要把发言当决议。 +- 内容完整详实,除非用户明确要求精简,不要压缩篇幅;原文模糊或矛盾的点在"未决问题"中列出而不是擅自裁决。 +- 待办事项若原文未指明负责人或期限,写"未指明",不要编造。 diff --git a/internal/prompts/library/task-extract.md b/internal/prompts/library/task-extract.md new file mode 100644 index 0000000..49e053e --- /dev/null +++ b/internal/prompts/library/task-extract.md @@ -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,不推断填充。 +- 抽取完成后报告:共多少条、高置信多少条、存疑多少条及原因。 diff --git a/internal/prompts/library/task-qa-cite.md b/internal/prompts/library/task-qa-cite.md new file mode 100644 index 0000000..140f855 --- /dev/null +++ b/internal/prompts/library/task-qa-cite.md @@ -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 +--- +用户就上传文档内容提问。执行要求: + +- 逐问作答,每答分三层:结论(直接回答)、依据(引用文档位置与关键原句)、边界(明确说明这是文档原文、合理推断还是文档未提及)。 +- 文档没有的信息直接说"文档未提及",不补外部知识冒充文档内容;若你认为外部常识有助于理解,单独标注"文档外补充"。 +- 引用原句保持逐字,不加解释性改写;长引用可节选但不得改变原意。 +- 用户问题含错误前提时(文档并未说过的"事实"),先纠正前提再作答。 diff --git a/internal/prompts/library/task-report.md b/internal/prompts/library/task-report.md new file mode 100644 index 0000000..e37ac77 --- /dev/null +++ b/internal/prompts/library/task-report.md @@ -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 +--- +用户要求撰写正式报告。执行要求: + +- 先明确报告的受众与用途(对上汇报/对外发布/内部存档),文风与详略随之调整;未说明时按内部正式报告处理并标注假设。 +- 事实与观点分离:分析章节可以论证,事实陈述要有出处(文档段落/数据表)。 +- 执行摘要是全文最重要的一段:结论、关键数字、建议动作,让只读摘要的人能做决定。 +- 结构服务于阅读动线:先结论后论证;每章开头一句话说清本章要点。 +- 篇幅完整不注水:覆盖必要内容,删掉不改变结论的空话。 diff --git a/internal/prompts/library/task-rewrite.md b/internal/prompts/library/task-rewrite.md new file mode 100644 index 0000000..e035aec --- /dev/null +++ b/internal/prompts/library/task-rewrite.md @@ -0,0 +1,21 @@ +--- +id: task.rewrite +layer: task +version: 1 +title: 改写与润色 +description: 在保持原意的前提下改写文本(风格/受众/格式调整) +priority: 55 +outputContract: | + 改写结果 + 改动说明(改了什么类型:结构/措辞/格式;哪些保留原样及原因) +triggers: +- kind: intent + keywords: 改写, 润色, 优化这段, 重写, 换个说法 +- kind: command + command: /rewrite +--- +用户要求改写/润色文本。执行要求: + +- 原意保真第一:改风格不改事实;数字、名称、时间、结论一律不动。 +- 先确认改写目标(更正式/更口语/更精炼/面向某受众);未说明时按上下文推断并在改动说明中写明你采用的基准。 +- 保留原文的术语体系与缩写习惯,不擅自统一成另一套词。 +- 改动说明让用户能快速核对:列出主要改动类别与代表性例子,不必逐句对照。 diff --git a/internal/prompts/library/task-summarize.md b/internal/prompts/library/task-summarize.md new file mode 100644 index 0000000..a576d4d --- /dev/null +++ b/internal/prompts/library/task-summarize.md @@ -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字以内"才压缩。 +- 保留关键限定词与条件("在……前提下"、"部分"、"拟"),总结丢限定词等于改事实。 +- 区分事实与观点:文档作者的判断标注为文档观点,不与客观事实混写。 diff --git a/internal/prompts/library/task-translate.md b/internal/prompts/library/task-translate.md new file mode 100644 index 0000000..0025be9 --- /dev/null +++ b/internal/prompts/library/task-translate.md @@ -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 +--- +用户要求翻译。执行要求: + +- 忠实原文:不增删信息,不"顺手修正"原文错误——发现原文疑误时在标注区提示,正文按原文译。 +- 术语一致:全文同一术语同一译法;专业领域按领域惯例(用户给了词表则优先词表)。 +- 语言自然:译文符合目标语言习惯,不逐词硬译;格式(列表、表格、代码块)保持原样。 +- 文化敏感内容(比喻、双关、习语)意译并在必要时加简短译注。 +- 存疑的低置信翻译在文末标注,不让用户误以为全部确定。 diff --git a/internal/prompts/library/tool-delegate-task.md b/internal/prompts/library/tool-delegate-task.md new file mode 100644 index 0000000..85a3cca --- /dev/null +++ b/internal/prompts/library/tool-delegate-task.md @@ -0,0 +1,8 @@ +--- +id: tool.delegate_task +layer: tool +version: 1 +title: delegate_task 工具描述 +description: 子代理派生工具的模型侧说明文案 +--- +派生一个子代理执行独立子任务(如检索、撰写、验证),返回其结果摘要。适用:目标明确、可独立验收、主对话不必逐步跟进的子任务。参数:name(角色名)、task(子任务描述,要完整自足——子代理看不到主对话)、hint(执行提示,可选)。反模式:把"理解当前问题"这类不可移交的认知工作派出去——理解必须留在主对话;也不要为规避思考而派生空泛任务。 diff --git a/internal/prompts/library/tool-knowledge-search.md b/internal/prompts/library/tool-knowledge-search.md new file mode 100644 index 0000000..61e1c34 --- /dev/null +++ b/internal/prompts/library/tool-knowledge-search.md @@ -0,0 +1,8 @@ +--- +id: tool.knowledge_search +layer: tool +version: 1 +title: knowledge_search 工具描述 +description: 知识检索工具的模型侧说明文案 +--- +在项目知识与插件知识库中做全文检索,返回命中片段。用法:一次一个明确的检索意图;换角度检索时改关键词再查,不重复同一查询。命中为空说明库中没有该表述,可换同义词再试一到两次;仍为空就承认未命中,不虚构结果。回答"项目里有没有/是什么"类问题优先用本工具。 diff --git a/internal/prompts/library/tool-squad-orchestrate.md b/internal/prompts/library/tool-squad-orchestrate.md new file mode 100644 index 0000000..c3221a1 --- /dev/null +++ b/internal/prompts/library/tool-squad-orchestrate.md @@ -0,0 +1,8 @@ +--- +id: tool.squad_orchestrate +layer: tool +version: 1 +title: squad_orchestrate 工具描述 +description: 小队编排工具的模型侧说明文案 +--- +用小队编排引擎执行多步骤目标:规划器把目标拆解为 梳理→专项→成文→终审 步骤,逐步派子代理执行;需要用户决策时会以提问卡片挂起。适用:交付型复合任务(出一份调研报告、综合多材料成文)——单轮可完成的任务不要编排。参数 goal 要写清交付物形态与验收要求,这是编排质量的源头。 diff --git a/internal/prompts/library/tool-time-now.md b/internal/prompts/library/tool-time-now.md new file mode 100644 index 0000000..f5b356b --- /dev/null +++ b/internal/prompts/library/tool-time-now.md @@ -0,0 +1,8 @@ +--- +id: tool.time_now +layer: tool +version: 1 +title: time_now 工具描述 +description: 当前时间工具的模型侧说明文案 +--- +获取当前时间(RFC3339 格式)与时区。涉及"今天/现在/几点"的判断必须调用本工具,不要依赖训练记忆推算日期;计算相对时间(明天、三天后)时以本工具返回为基准。 diff --git a/internal/prompts/library/tool-update-plan.md b/internal/prompts/library/tool-update-plan.md new file mode 100644 index 0000000..64f1691 --- /dev/null +++ b/internal/prompts/library/tool-update-plan.md @@ -0,0 +1,8 @@ +--- +id: tool.update_plan +layer: tool +version: 1 +title: update_plan 工具描述 +description: 计划更新工具的模型侧说明文案 +--- +创建或更新当前会话的计划(步骤列表)。多步骤任务开始时建立计划,每完成/开始一步就更新状态(pending/in_progress/done/skipped);计划变化时整体重报。单步任务不需要建计划。步骤粒度 = 用户可见的里程碑,不是内部小动作。 diff --git a/internal/prompts/library/tool-update-todos.md b/internal/prompts/library/tool-update-todos.md new file mode 100644 index 0000000..3fca879 --- /dev/null +++ b/internal/prompts/library/tool-update-todos.md @@ -0,0 +1,8 @@ +--- +id: tool.update_todos +layer: tool +version: 1 +title: update_todos 工具描述 +description: 待办更新工具的模型侧说明文案 +--- +更新当前会话的待办清单(全量覆盖式)。用于跟踪跨轮次的用户事务性待办(要取的材料、要确认的问题);每次增删改后上报最新全量。临时性的执行内步骤不要塞进待办。 diff --git a/internal/prompts/library/tool-write-artifact.md b/internal/prompts/library/tool-write-artifact.md new file mode 100644 index 0000000..09eb56d --- /dev/null +++ b/internal/prompts/library/tool-write-artifact.md @@ -0,0 +1,8 @@ +--- +id: tool.write_artifact +layer: tool +version: 1 +title: write_artifact 工具描述 +description: 工件写入工具的模型侧说明文案 +--- +把生成内容写入工件(文档/数据),返回工件 ID 与名称。用于正式交付物:报告、纪要、代码文件、数据集等需要保存、后续引用或继续编辑的内容。对话内的临时说明不需要写工件。写入时 name 用人类可读的文件名(含扩展名),kind 按内容选 document/code/data;同一交付物修订时写新版本而不是覆盖引用旧名。 diff --git a/internal/prompts/parse.go b/internal/prompts/parse.go new file mode 100644 index 0000000..1026942 --- /dev/null +++ b/internal/prompts/parse.go @@ -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 +} diff --git a/internal/prompts/prompts_test.go b/internal/prompts/prompts_test.go new file mode 100644 index 0000000..05ff502 --- /dev/null +++ b/internal/prompts/prompts_test.go @@ -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") + } +} diff --git a/internal/prompts/service.go b/internal/prompts/service.go new file mode 100644 index 0000000..69befa3 --- /dev/null +++ b/internal/prompts/service.go @@ -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 +} diff --git a/internal/runtime/llm.go b/internal/runtime/llm.go index 2b36692..215e69f 100644 --- a/internal/runtime/llm.go +++ b/internal/runtime/llm.go @@ -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.)优先,回落注册表文案。 +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 diff --git a/internal/server/server.go b/internal/server/server.go index 5c23d01..7737e47 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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()) diff --git a/internal/squadadapter/adapter.go b/internal/squadadapter/adapter.go index 06ffd71..7055c3d 100644 --- a/internal/squadadapter/adapter.go +++ b/internal/squadadapter/adapter.go @@ -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.);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", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")}, } } diff --git a/internal/store/migrate.go b/internal/store/migrate.go index f702db2..50064f3 100644 --- a/internal/store/migrate.go +++ b/internal/store/migrate.go @@ -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`}}}, ) } diff --git a/internal/store/prompts.go b/internal/store/prompts.go new file mode 100644 index 0000000..2b164ad --- /dev/null +++ b/internal/store/prompts.go @@ -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 +} diff --git a/internal/store/store_test.go b/internal/store/store_test.go index b80abc3..04b53e0 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -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 {