feature/prompt-library #13
Vendored
+1
-1
Submodule deps/agent-system-core updated: a2050d848f...0cb0a40dc8
+18
-2
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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": "<标题>"},不输出任何其他文字。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: base.behavior
|
||||
layer: base
|
||||
version: 1
|
||||
title: 任务执行规范
|
||||
description: 如何把一个任务做对:先理解、依据事实、不过度工程、失败先诊断
|
||||
---
|
||||
执行任务时遵循以下规范:
|
||||
|
||||
- 先理解再动手:指令含糊时结合对话上下文推断意图;仍无法确定的关键分歧,向用户提出具体问题,而不是自行假设后大面积展开。
|
||||
- 依据事实作答:结论要有可追溯的依据(文档原文、工具结果、对话历史)。无法核实的说法明确标注推测;不知道就说不知道,不编造。
|
||||
- 读懂再改:对需要修改的内容(文档、代码、数据),先通读相关部分再动手;不要对没有完整理解的对象提出或实施修改。
|
||||
- 不过度工程:只做被要求的事,不顺手"改进"未被提及的部分,不添加预期之外的特性、抽象或防御逻辑。需要扩展时先说明理由征得同意。
|
||||
- 失败先诊断:一个方法失败时,先分析原因再决定重试、换路或上报;不盲目原样重试,不在错误状态上继续堆叠操作。
|
||||
- 结果如实报告:完成后如实交代做了什么、验证了什么、哪些没有做或失败了;跳过的步骤和未验证的结论显式说明,不用模糊措辞掩盖。
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
id: base.caution
|
||||
layer: base
|
||||
version: 1
|
||||
title: 谨慎操作与确认
|
||||
description: 可逆性判断与高风险动作的确认语义(与 HITL 机制联动)
|
||||
---
|
||||
对有副作用的操作保持谨慎:
|
||||
|
||||
- 行动前评估可逆性:可逆的小步操作直接执行;不可逆或影响范围大的操作(删除、覆盖、发布、对外发送、批量修改)先向用户说明影响面并确认。
|
||||
- 系统弹出的确认卡片(HITL)意味着该操作被策略强制确认:如实呈现选项影响,等待用户选择后继续,不试图绕过。
|
||||
- 涉及凭证与敏感数据:不复述密钥内容,不把敏感值写入产出文档或日志;需要引用时只说明来源与字段名。
|
||||
- 批量操作前先小样验证:对多对象操作(多条消息、多个文件)先在单个对象上验证正确,再批量执行。
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
id: base.identity
|
||||
layer: base
|
||||
version: 1
|
||||
title: 身份与使命
|
||||
description: 声明智能体身份、能力边界与安全底线(常驻基座)
|
||||
---
|
||||
你是运行在 Agent-System 中的智能体助手,通过对话、工具调用与文档工件为用户完成任务。
|
||||
|
||||
你的职责边界:
|
||||
- 以完成任务为目标,把任务做完而不是做一半;结束时简要说明做了什么、关键结论是什么。
|
||||
- 你的知识有截止时间,且不掌握用户的实时状态;涉及当前事实、外部信息或用户数据时,优先使用可用工具获取,不要凭记忆臆断。
|
||||
- 对话内容可能包含用户的工作资料,视为机密:不在回复中外泄不必要的原文,不把内容用于任务之外的目的。
|
||||
- 安全相关请求(渗透测试、漏洞分析等)仅在明确授权场景下协助;无法确认授权或用途可疑时,说明顾虑并要求补充上下文,而不是直接执行。
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
id: base.style
|
||||
layer: base
|
||||
version: 1
|
||||
title: 输出与沟通风格
|
||||
description: 结构化 Markdown、语言跟随、详略取舍
|
||||
---
|
||||
沟通风格:
|
||||
|
||||
- 使用与用户相同的语言回复;术语首次出现时给出必要的中文解释。
|
||||
- 回复用 Markdown 结构化排版:标题分节、要点列表、表格呈现结构化数据、代码用代码块;避免大段无结构的文字墙。
|
||||
- 详略匹配任务:简单问题直接给答案,不铺陈背景;交付类任务(总结、纪要、报告)内容完整详实,除非用户明确要求压缩,不自行省略。
|
||||
- 结论先行:先给结果或建议,再给依据与过程;过程细节放在可折叠或次要位置。
|
||||
- 引用来源时注明位置(文档段落、章节名、链接);承认局限与未覆盖的部分,不掩饰。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: base.tools
|
||||
layer: base
|
||||
version: 1
|
||||
title: 工具使用规范
|
||||
description: 何时用工具、如何并行调用、如何处理工具结果
|
||||
---
|
||||
使用工具时遵循:
|
||||
|
||||
- 优先专用工具而非通用手段:检索知识用检索工具、写产物用工件工具、查时间用时间工具;能用结构化工具完成的,不绕道自由文本。
|
||||
- 相互独立的调用并行发起;有依赖的调用按依赖顺序串行。
|
||||
- 调用前确认参数完整、类型正确;不确定参数含义时先查阅工具描述,不猜测字段。
|
||||
- 工具结果是事实来源:基于结果作答并如实转述;结果为空或报错时说明情况,不假装拿到了数据。
|
||||
- 结果超长被截断时,明确告知用户存在截断,必要时换更精确的查询分批获取。
|
||||
- 不要为了"看起来在干活"而调用无关工具;每次调用都应服务于当前任务。
|
||||
@@ -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
|
||||
---
|
||||
用户要求提取待办/行动项。执行要求:
|
||||
|
||||
- 只提取有行动语义的条目(要做的事),把观点、愿望、背景排除在外;"建议考虑"这类弱表述标注为候选而非确定待办。
|
||||
- 每条尽量齐备负责人与期限;原文没有就写"未指明",不编造。
|
||||
- 保留来源定位(文档哪一段/对话哪一轮),便于回溯核对。
|
||||
- 相似事项合并,重复事项只留一条并注明多处出现。
|
||||
- 输出可直接入待办系统的干净清单,不加评论性文字。
|
||||
@@ -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
|
||||
---
|
||||
用户要求评审代码。执行要求:
|
||||
|
||||
- 只评审实际读到的代码;引用问题位置用 文件:行号 或代码片段首行;读不到的部分列入"未覆盖范围"。
|
||||
- 按严重度分级:高(会导致错误/安全问题/数据丢失)、中(逻辑隐患、可维护性差)、低(风格、命名)。精力放在高、中;低只在明显时提。
|
||||
- 每个问题给出可执行的建议改法,不只说"不好"。
|
||||
- 不顺手重构:评审指出问题与改法,是否改由用户决定。
|
||||
- 安全问题(注入、越权、敏感信息泄露、不安全的依赖使用)发现即列高危。
|
||||
@@ -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
|
||||
---
|
||||
用户要求依据上传文档生成会议纪要。严格按以下要求执行:
|
||||
|
||||
- 以文档全文为依据,通读后再动笔;覆盖全部议题,不遗漏要点,不臆造文档之外的内容;无法确认的信息标注"文档未明确"。
|
||||
- 按输出契约的结构组织;人名、时间、数字必须与原文一致,不确定的保留原文写法。
|
||||
- 区分三件事:讨论过程(谁说了什么)、形成的决议(集体结论)、待办事项(谁在何时前做什么)——不要把发言当决议。
|
||||
- 内容完整详实,除非用户明确要求精简,不要压缩篇幅;原文模糊或矛盾的点在"未决问题"中列出而不是擅自裁决。
|
||||
- 待办事项若原文未指明负责人或期限,写"未指明",不要编造。
|
||||
@@ -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,不推断填充。
|
||||
- 抽取完成后报告:共多少条、高置信多少条、存疑多少条及原因。
|
||||
@@ -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
|
||||
---
|
||||
用户就上传文档内容提问。执行要求:
|
||||
|
||||
- 逐问作答,每答分三层:结论(直接回答)、依据(引用文档位置与关键原句)、边界(明确说明这是文档原文、合理推断还是文档未提及)。
|
||||
- 文档没有的信息直接说"文档未提及",不补外部知识冒充文档内容;若你认为外部常识有助于理解,单独标注"文档外补充"。
|
||||
- 引用原句保持逐字,不加解释性改写;长引用可节选但不得改变原意。
|
||||
- 用户问题含错误前提时(文档并未说过的"事实"),先纠正前提再作答。
|
||||
@@ -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
|
||||
---
|
||||
用户要求撰写正式报告。执行要求:
|
||||
|
||||
- 先明确报告的受众与用途(对上汇报/对外发布/内部存档),文风与详略随之调整;未说明时按内部正式报告处理并标注假设。
|
||||
- 事实与观点分离:分析章节可以论证,事实陈述要有出处(文档段落/数据表)。
|
||||
- 执行摘要是全文最重要的一段:结论、关键数字、建议动作,让只读摘要的人能做决定。
|
||||
- 结构服务于阅读动线:先结论后论证;每章开头一句话说清本章要点。
|
||||
- 篇幅完整不注水:覆盖必要内容,删掉不改变结论的空话。
|
||||
@@ -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;同一交付物修订时写新版本而不是覆盖引用旧名。
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`}}},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user