From 03fc405e4e10c38f03fda7d21deccda8bb8b1754 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 14 Apr 2026 12:00:03 +0800 Subject: [PATCH] feat: implement instruction assembly and project instruction providers with tests --- .../assembler.go} | 29 +++---- .../assembler_test.go} | 6 +- .../helpers.go} | 6 +- .../instruction/project_instruction.go | 15 ++++ .../providers.go} | 5 +- .../providers_test.go} | 2 +- .../service.go} | 24 +++--- .../internal/impl/factory/agent_factory.go | 5 +- .../impl/factory/project_instruction.go | 85 ------------------- .../impl/factory/skill_middleware_backend.go | 3 +- .../internal/impl/factory/tool_helpers.go | 3 +- .../impl/factory/tool_helpers_test.go | 4 +- 12 files changed, 58 insertions(+), 129 deletions(-) rename internal/ai/runtime/{internal/impl/factory/instruction_assembler.go => instruction/assembler.go} (77%) rename internal/ai/runtime/{internal/impl/factory/instruction_assembler_test.go => instruction/assembler_test.go} (86%) rename internal/ai/runtime/{internal/impl/factory/skill_instruction_helpers.go => instruction/helpers.go} (94%) create mode 100644 internal/ai/runtime/instruction/project_instruction.go rename internal/ai/runtime/{internal/impl/factory/instruction_providers.go => instruction/providers.go} (94%) rename internal/ai/runtime/{internal/impl/factory/instruction_providers_test.go => instruction/providers_test.go} (98%) rename internal/ai/runtime/{internal/impl/factory/instruction_service.go => instruction/service.go} (84%) delete mode 100644 internal/ai/runtime/internal/impl/factory/project_instruction.go diff --git a/internal/ai/runtime/internal/impl/factory/instruction_assembler.go b/internal/ai/runtime/instruction/assembler.go similarity index 77% rename from internal/ai/runtime/internal/impl/factory/instruction_assembler.go rename to internal/ai/runtime/instruction/assembler.go index 8326dec..0475e83 100644 --- a/internal/ai/runtime/internal/impl/factory/instruction_assembler.go +++ b/internal/ai/runtime/instruction/assembler.go @@ -1,4 +1,4 @@ -package factory +package instruction import "strings" @@ -8,11 +8,11 @@ const defaultGovernanceInstruction = ` 如果存在工具白名单限制,只能调用当前允许的工具;信息不足时优先追问,不要伪造事实或跳过必要确认。 ` -type InstructionAssembler struct { +type Assembler struct { governanceInstruction string } -type InstructionAssemblerInput struct { +type AssemblerInput struct { AgentInstruction string GovernanceInstruction string SkillInstruction string @@ -20,8 +20,7 @@ type InstructionAssemblerInput struct { ProjectInstruction string } -// InstructionAssemblySummary 描述 instruction 各组成部分的来源摘要。 -type InstructionAssemblySummary struct { +type AssemblySummary struct { SectionTitles []string HasProjectRule bool HasGovernanceRule bool @@ -30,26 +29,22 @@ type InstructionAssemblySummary struct { HasToolRule bool } -// InstructionAssemblyResult 为 instruction 装配结果。 -type InstructionAssemblyResult struct { +type AssemblyResult struct { Text string - Summary InstructionAssemblySummary + Summary AssemblySummary } -func NewInstructionAssembler() *InstructionAssembler { - return &InstructionAssembler{ - governanceInstruction: strings.TrimSpace(defaultGovernanceInstruction), - } +func NewAssembler() *Assembler { + return &Assembler{governanceInstruction: strings.TrimSpace(defaultGovernanceInstruction)} } -func (a *InstructionAssembler) Build(input InstructionAssemblerInput) string { +func (a *Assembler) Build(input AssemblerInput) string { return a.Assemble(input).Text } -// Assemble 构建最终 instruction 文本及其来源摘要。 -func (a *InstructionAssembler) Assemble(input InstructionAssemblerInput) InstructionAssemblyResult { +func (a *Assembler) Assemble(input AssemblerInput) AssemblyResult { parts := make([]string, 0, 5) - summary := InstructionAssemblySummary{SectionTitles: make([]string, 0, 5)} + summary := AssemblySummary{SectionTitles: make([]string, 0, 5)} projectInstruction := strings.TrimSpace(input.ProjectInstruction) if projectInstruction == "" { projectInstruction = strings.TrimSpace(DefaultProjectInstruction) @@ -83,7 +78,7 @@ func (a *InstructionAssembler) Assemble(input InstructionAssemblerInput) Instruc summary.HasToolRule = true summary.SectionTitles = append(summary.SectionTitles, "工具补充规则") } - return InstructionAssemblyResult{ + return AssemblyResult{ Text: strings.TrimSpace(strings.Join(parts, "\n\n")), Summary: summary, } diff --git a/internal/ai/runtime/internal/impl/factory/instruction_assembler_test.go b/internal/ai/runtime/instruction/assembler_test.go similarity index 86% rename from internal/ai/runtime/internal/impl/factory/instruction_assembler_test.go rename to internal/ai/runtime/instruction/assembler_test.go index 3eddc7e..0a39f96 100644 --- a/internal/ai/runtime/internal/impl/factory/instruction_assembler_test.go +++ b/internal/ai/runtime/instruction/assembler_test.go @@ -1,12 +1,12 @@ -package factory +package instruction import ( "strings" "testing" ) -func TestInstructionAssemblerRespectsProvidedSources(t *testing.T) { - result := NewInstructionAssembler().Assemble(InstructionAssemblerInput{ +func TestAssemblerRespectsProvidedSources(t *testing.T) { + result := NewAssembler().Assemble(AssemblerInput{ ProjectInstruction: "project-rule", GovernanceInstruction: "governance-rule", AgentInstruction: "agent-rule", diff --git a/internal/ai/runtime/internal/impl/factory/skill_instruction_helpers.go b/internal/ai/runtime/instruction/helpers.go similarity index 94% rename from internal/ai/runtime/internal/impl/factory/skill_instruction_helpers.go rename to internal/ai/runtime/instruction/helpers.go index 8f10251..ca9e67d 100644 --- a/internal/ai/runtime/internal/impl/factory/skill_instruction_helpers.go +++ b/internal/ai/runtime/instruction/helpers.go @@ -1,4 +1,4 @@ -package factory +package instruction import ( "encoding/json" @@ -10,7 +10,7 @@ import ( "cs-agent/internal/pkg/toolx" ) -func buildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string { +func BuildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string { if skill == nil { return "" } @@ -26,7 +26,7 @@ func buildSelectedSkillActivationInstruction(skill *models.SkillDefinition) stri return strings.TrimSpace(strings.Join(lines, "\n")) } -func buildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string { +func BuildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition) string { if skill == nil { return "" } diff --git a/internal/ai/runtime/instruction/project_instruction.go b/internal/ai/runtime/instruction/project_instruction.go new file mode 100644 index 0000000..4334eec --- /dev/null +++ b/internal/ai/runtime/instruction/project_instruction.go @@ -0,0 +1,15 @@ +package instruction + +// DefaultProjectInstruction 为当前项目统一注入的全局项目规则。 +// +// 默认回退到内置常量;运行时优先由 ProjectInstructionProvider 读取仓库中的 AGENTS.md。 +const DefaultProjectInstruction = `# AGENTS.md + +本文件定义本项目内 AI Agent 的强制开发规则。除非用户明确要求偏离,否则必须遵循。 + +## 1. 基本原则 + +- 适用范围:仓库根目录及所有子目录 +- 优先级:用户明确指令 > 本文件 > 默认实现习惯 +- 若与用户要求冲突:先执行用户要求,并在变更说明中标注偏离点 +` diff --git a/internal/ai/runtime/internal/impl/factory/instruction_providers.go b/internal/ai/runtime/instruction/providers.go similarity index 94% rename from internal/ai/runtime/internal/impl/factory/instruction_providers.go rename to internal/ai/runtime/instruction/providers.go index 77759c5..db469db 100644 --- a/internal/ai/runtime/internal/impl/factory/instruction_providers.go +++ b/internal/ai/runtime/instruction/providers.go @@ -1,4 +1,4 @@ -package factory +package instruction import ( "os" @@ -14,7 +14,6 @@ type ProjectInstructionProvider struct { fileName string } -// TODO 这个要读取AGENTS.md文件,后面考虑还要不要 func NewProjectInstructionProvider() *ProjectInstructionProvider { return &ProjectInstructionProvider{fileName: "AGENTS.md"} } @@ -84,7 +83,7 @@ func NewSkillInstructionProvider() *SkillInstructionProvider { } func (p *SkillInstructionProvider) Resolve(selectedSkill *models.SkillDefinition) string { - return buildSelectedSkillActivationInstruction(selectedSkill) + return BuildSelectedSkillActivationInstruction(selectedSkill) } func (p *ToolAppendixProvider) Build(toolDefinitions []runtimetooling.MCPToolDefinition, extraToolCodes map[string]string) []string { diff --git a/internal/ai/runtime/internal/impl/factory/instruction_providers_test.go b/internal/ai/runtime/instruction/providers_test.go similarity index 98% rename from internal/ai/runtime/internal/impl/factory/instruction_providers_test.go rename to internal/ai/runtime/instruction/providers_test.go index e77d473..3eef409 100644 --- a/internal/ai/runtime/internal/impl/factory/instruction_providers_test.go +++ b/internal/ai/runtime/instruction/providers_test.go @@ -1,4 +1,4 @@ -package factory +package instruction import ( "os" diff --git a/internal/ai/runtime/internal/impl/factory/instruction_service.go b/internal/ai/runtime/instruction/service.go similarity index 84% rename from internal/ai/runtime/internal/impl/factory/instruction_service.go rename to internal/ai/runtime/instruction/service.go index ec24580..2a285c2 100644 --- a/internal/ai/runtime/internal/impl/factory/instruction_service.go +++ b/internal/ai/runtime/instruction/service.go @@ -1,4 +1,4 @@ -package factory +package instruction import ( "strings" @@ -7,23 +7,23 @@ import ( "cs-agent/internal/models" ) -type InstructionService struct { - assembler *InstructionAssembler +type Service struct { + assembler *Assembler projectInstructionProvider *ProjectInstructionProvider governanceInstructionProvider *GovernanceInstructionProvider skillInstructionProvider *SkillInstructionProvider toolAppendixProvider *ToolAppendixProvider } -func NewInstructionService( - assembler *InstructionAssembler, +func NewService( + assembler *Assembler, projectProvider *ProjectInstructionProvider, governanceProvider *GovernanceInstructionProvider, skillProvider *SkillInstructionProvider, toolProvider *ToolAppendixProvider, -) *InstructionService { +) *Service { if assembler == nil { - assembler = NewInstructionAssembler() + assembler = NewAssembler() } if projectProvider == nil { projectProvider = NewProjectInstructionProvider() @@ -37,7 +37,7 @@ func NewInstructionService( if toolProvider == nil { toolProvider = NewToolAppendixProvider() } - return &InstructionService{ + return &Service{ assembler: assembler, projectInstructionProvider: projectProvider, governanceInstructionProvider: governanceProvider, @@ -46,12 +46,12 @@ func NewInstructionService( } } -func (s *InstructionService) Build( +func (s *Service) Build( aiAgent *models.AIAgent, selectedSkill *models.SkillDefinition, toolDefinitions []runtimetooling.MCPToolDefinition, extraToolCodes map[string]string, -) InstructionAssemblyResult { +) AssemblyResult { baseInstruction := "" if aiAgent != nil { baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt) @@ -72,11 +72,11 @@ func (s *InstructionService) Build( if s != nil && s.toolAppendixProvider != nil { toolAppendices = s.toolAppendixProvider.Build(toolDefinitions, extraToolCodes) } - assembler := NewInstructionAssembler() + assembler := NewAssembler() if s != nil && s.assembler != nil { assembler = s.assembler } - return assembler.Assemble(InstructionAssemblerInput{ + return assembler.Assemble(AssemblerInput{ AgentInstruction: baseInstruction, GovernanceInstruction: governanceInstruction, SkillInstruction: skillInstruction, diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 4d79600..27c21d3 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -4,6 +4,7 @@ import ( "context" "strings" + runtimeinstruction "cs-agent/internal/ai/runtime/instruction" einoagents "cs-agent/internal/ai/runtime/internal/impl/agents" einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks" "cs-agent/internal/ai/runtime/registry" @@ -18,7 +19,7 @@ import ( type AgentFactory struct { chatModelFactory *ChatModelFactory toolFactory *ToolFactory - instructionService *InstructionService + instructionService *runtimeinstruction.Service handlerService *AgentHandlerService } @@ -55,7 +56,7 @@ func NewAgentFactory() *AgentFactory { return &AgentFactory{ chatModelFactory: NewChatModelFactory(), toolFactory: NewToolFactory(), - instructionService: NewInstructionService(nil, nil, nil, nil, nil), + instructionService: runtimeinstruction.NewService(nil, nil, nil, nil, nil), handlerService: NewAgentHandlerService(nil), } } diff --git a/internal/ai/runtime/internal/impl/factory/project_instruction.go b/internal/ai/runtime/internal/impl/factory/project_instruction.go deleted file mode 100644 index 65e6522..0000000 --- a/internal/ai/runtime/internal/impl/factory/project_instruction.go +++ /dev/null @@ -1,85 +0,0 @@ -package factory - -// DefaultProjectInstruction 为当前项目统一注入的全局项目规则。 -// -// 默认回退到内置常量;运行时优先由 ProjectInstructionProvider 读取仓库中的 AGENTS.md。 -// TODO 这个内容不合适,需要重新整理内容,内容需要面向客服系统 -const DefaultProjectInstruction = `# AGENTS.md - -本文件定义本项目内 AI Agent 的强制开发规则。除非用户明确要求偏离,否则必须遵循。 - -## 1. 基本原则 - -- 适用范围:仓库根目录及所有子目录 -- 优先级:用户明确指令 > 本文件 > 默认实现习惯 -- 若与用户要求冲突:先执行用户要求,并在变更说明中标注偏离点 - -## 2. 固定技术栈 - -- 后端:Golang + Iris + GORM + github.com/mlogclub/simple -- 数据库:同时兼容 SQLite 和 MySQL -- 前端:Next.js(App Router) + React + shadcn/ui + Tailwind CSS -- 前端包管理器:pnpm - -## 3. 后端分层 - -必须遵循单向依赖:models -> repositories -> services -> controllers - -- models:只定义实体和表映射 -- repositories:只封装数据访问 -- services:负责业务规则、事务编排、聚合逻辑 -- controllers:只做参数解析、权限校验、service 调用、响应封装 - -禁止: - -- controller 直接调用 repository -- 直接将 GORM model 返回前端 -- 在 models/repositories 中写业务编排 - -## 4. simple 使用约定 - -- DB 初始化后必须执行:sqls.SetDB(db) -- 查询条件优先使用:sqls.Cnd -- 参数绑定优先使用:web/params -- HTTP 响应统一使用:web.JsonData、web.JsonPageData、web.JsonError - -## 5. 数据库兼容规则 - -- 字段类型使用兼容集合:varchar、text、int、bigint、datetime -- 主键统一使用 int64 -- 避免数据库私有语法和方言特性 - -## 6. 接口与 DTO - -- DTO 分离:request / response 分开定义 -- JSON 字段统一使用 camelCase -- 禁止透传底层 SQL 错误 -- controller 入参使用 request DTO -- controller 出参使用 response DTO -- 禁止直接返回 models 到前端 - -## 7. Go 代码规范 - -- 日志统一使用标准库 log/slog -- 新增 Go 代码统一使用 any,禁止新增 interface{} -- 修改 Go 代码后必须执行 gofmt - -## 8. 前端规范 - -- 前端目录:web -- 框架:Next.js 16 + App Router -- 基础组件优先使用 shadcn/ui -- 前端业务接口统一通过 web/lib/api/* 发起,禁止页面里散落裸 fetch -- 新增或修改前端页面后至少执行:cd web && pnpm typecheck - -## 9. 提交前检查清单 - -每次修改后至少确认: - -1. 没有跨层调用或反向依赖 -2. 写操作有明确事务边界 -3. 返回仍符合统一 JsonResult 结构 -4. 兼容 SQLite 与 MySQL -5. 补充了必要测试,至少覆盖 service 核心路径 -6. Go 改动已执行 gofmt -7. 前端改动至少通过 pnpm lint 或 pnpm typecheck(在 web 目录)` diff --git a/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go b/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go index 316a3ef..ece75a6 100644 --- a/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go +++ b/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + runtimeinstruction "cs-agent/internal/ai/runtime/instruction" runtimetooling "cs-agent/internal/ai/runtime/tooling" "cs-agent/internal/models" @@ -25,7 +26,7 @@ func newSelectedSkillBackend(selectedSkill *models.SkillDefinition, toolDefiniti return nil, fmt.Errorf("selected skill code is empty") } description := strings.TrimSpace(selectedSkill.Description) - content := buildSelectedSkillDocument(selectedSkill, toolDefinitions) + content := runtimeinstruction.BuildSelectedSkillDocument(selectedSkill, toolDefinitions) return &selectedSkillBackend{ frontMatter: einoskill.FrontMatter{ Name: skillName, diff --git a/internal/ai/runtime/internal/impl/factory/tool_helpers.go b/internal/ai/runtime/internal/impl/factory/tool_helpers.go index 57f63e4..f90d910 100644 --- a/internal/ai/runtime/internal/impl/factory/tool_helpers.go +++ b/internal/ai/runtime/internal/impl/factory/tool_helpers.go @@ -3,6 +3,7 @@ package factory import ( "strings" + runtimeinstruction "cs-agent/internal/ai/runtime/instruction" einocallbacks "cs-agent/internal/ai/runtime/internal/impl/callbacks" "cs-agent/internal/ai/runtime/registry" runtimetooling "cs-agent/internal/ai/runtime/tooling" @@ -10,7 +11,7 @@ import ( "cs-agent/internal/pkg/toolx" ) -func buildInstructionTraceSummary(summary InstructionAssemblySummary) einocallbacks.InstructionTraceSummary { +func buildInstructionTraceSummary(summary runtimeinstruction.AssemblySummary) einocallbacks.InstructionTraceSummary { return einocallbacks.InstructionTraceSummary{ SectionTitles: append([]string(nil), summary.SectionTitles...), HasProjectRule: summary.HasProjectRule, diff --git a/internal/ai/runtime/internal/impl/factory/tool_helpers_test.go b/internal/ai/runtime/internal/impl/factory/tool_helpers_test.go index 0afa32f..6660434 100644 --- a/internal/ai/runtime/internal/impl/factory/tool_helpers_test.go +++ b/internal/ai/runtime/internal/impl/factory/tool_helpers_test.go @@ -2,10 +2,12 @@ package factory import ( "testing" + + runtimeinstruction "cs-agent/internal/ai/runtime/instruction" ) func TestBuildInstructionTraceSummary(t *testing.T) { - got := buildInstructionTraceSummary(InstructionAssemblySummary{ + got := buildInstructionTraceSummary(runtimeinstruction.AssemblySummary{ SectionTitles: []string{"项目级规则", "当前技能上下文"}, HasProjectRule: true, HasGovernanceRule: true,