diff --git a/go.mod b/go.mod index 53505ba..a6085d1 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect diff --git a/go.sum b/go.sum index feb83b6..7f98c36 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw= github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= diff --git a/internal/ai/runtime/internal/engine/service.go b/internal/ai/runtime/internal/engine/service.go index 02fe0f8..c7195cf 100644 --- a/internal/ai/runtime/internal/engine/service.go +++ b/internal/ai/runtime/internal/engine/service.go @@ -13,6 +13,7 @@ import ( "cs-agent/internal/ai/runtime/internal/impl/retrievers" "cs-agent/internal/ai/runtime/registry" "cs-agent/internal/models" + "cs-agent/internal/pkg/toolx" "cs-agent/internal/pkg/utils" "github.com/cloudwego/eino/adk" @@ -84,8 +85,12 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { toolDefsByModelName[item.ModelName] = item.ToolCode } if len(filteredToolDefs) > 0 { - summary.ToolCodes = appendIfMissing(summary.ToolCodes, "builtin/tool_search") - toolDefsByModelName["tool_search"] = "builtin/tool_search" + summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinToolSearchToolCode) + toolDefsByModelName[toolx.BuiltinToolSearchToolName] = toolx.BuiltinToolSearchToolCode + } + if req.SelectedSkill != nil { + summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinSkillToolCode) + toolDefsByModelName[toolx.BuiltinSkillToolName] = toolx.BuiltinSkillToolCode } for modelName, toolCode := range toolSetStaticToolCodes(req.ToolSet) { toolCode = strings.TrimSpace(toolCode) @@ -238,8 +243,8 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro toolDefsByModelName[item.ModelName] = item.ToolCode } if len(toolDefs) > 0 { - summary.ToolCodes = appendIfMissing(summary.ToolCodes, "builtin/tool_search") - toolDefsByModelName["tool_search"] = "builtin/tool_search" + summary.ToolCodes = appendIfMissing(summary.ToolCodes, toolx.BuiltinToolSearchToolCode) + toolDefsByModelName[toolx.BuiltinToolSearchToolName] = toolx.BuiltinToolSearchToolCode } for modelName, toolCode := range toolSetStaticToolCodes(req.ToolSet) { toolCode = strings.TrimSpace(toolCode) diff --git a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go index a9d179a..03efdd9 100644 --- a/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go +++ b/internal/ai/runtime/internal/impl/callbacks/agent_trace_handler.go @@ -93,6 +93,14 @@ func (h *RuntimeTraceHandler) resolveToolMetadata(modelToolName string) (ToolMet SourceType: toolx.BuiltinToolCatalogServerCode, }, true } + if modelToolName == toolx.BuiltinSkillToolName { + return ToolMetadata{ + ToolCode: toolx.BuiltinSkillToolCode, + ServerCode: toolx.BuiltinToolCatalogServerCode, + ToolName: toolx.BuiltinSkillToolName, + SourceType: toolx.BuiltinToolCatalogServerCode, + }, true + } metadata, ok := h.toolMetadataBy[modelToolName] return metadata, ok } diff --git a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go index 02d0743..9fb1090 100644 --- a/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go +++ b/internal/ai/runtime/internal/impl/callbacks/runlog_callback.go @@ -55,6 +55,16 @@ func (c *RuntimeTraceCollector) SetInstructionSummary(summary InstructionTraceSu c.Data.Instruction.HasToolRule = summary.HasToolRule } +func (c *RuntimeTraceCollector) SetSkillMiddleware(enabled bool, toolName string) { + if c == nil { + return + } + c.mu.Lock() + defer c.mu.Unlock() + c.Data.Skill.MiddlewareEnabled = enabled + c.Data.Skill.MiddlewareToolName = toolName +} + func (c *RuntimeTraceCollector) AddToolItem(item ToolTraceItem) { if c == nil { return diff --git a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go index 5966e2c..df80b62 100644 --- a/internal/ai/runtime/internal/impl/callbacks/trace_callback.go +++ b/internal/ai/runtime/internal/impl/callbacks/trace_callback.go @@ -107,11 +107,13 @@ type RuntimeTraceData struct { } type SkillTraceData struct { - Code string `json:"code,omitempty"` - Name string `json:"name,omitempty"` - RouteReason string `json:"routeReason,omitempty"` - RouteTrace string `json:"routeTrace,omitempty"` - AllowedToolCodes []string `json:"allowedToolCodes,omitempty"` + Code string `json:"code,omitempty"` + Name string `json:"name,omitempty"` + RouteReason string `json:"routeReason,omitempty"` + RouteTrace string `json:"routeTrace,omitempty"` + AllowedToolCodes []string `json:"allowedToolCodes,omitempty"` + MiddlewareEnabled bool `json:"middlewareEnabled,omitempty"` + MiddlewareToolName string `json:"middlewareToolName,omitempty"` } type InterruptTraceContext struct { diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 93fb223..d6d5bf3 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -14,6 +14,7 @@ import ( "github.com/cloudwego/eino/adk" einotoolsearch "github.com/cloudwego/eino/adk/middlewares/dynamictool/toolsearch" + einoskill "github.com/cloudwego/eino/adk/middlewares/skill" einobasetool "github.com/cloudwego/eino/components/tool" "github.com/cloudwego/eino/compose" ) @@ -74,7 +75,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input Buil } allTools := make([]einobasetool.BaseTool, 0, len(input.StaticTools)) allTools = append(allTools, input.StaticTools...) - handlers := make([]adk.ChatModelAgentMiddleware, 0, 1) + handlers := make([]adk.ChatModelAgentMiddleware, 0, 2) if len(dynamicTools) > 0 { toolSearchHandler, toolSearchErr := einotoolsearch.New(ctx, &einotoolsearch.Config{ DynamicTools: dynamicTools, @@ -84,6 +85,13 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input Buil } handlers = append(handlers, toolSearchHandler) } + if input.SelectedSkill != nil { + skillHandler, skillErr := f.buildSelectedSkillMiddleware(ctx, input.SelectedSkill, input.InstructionToolDefinitions) + if skillErr != nil { + return nil, skillErr + } + handlers = append(handlers, skillHandler) + } if input.Collector != nil { toolMetadataBy := make(map[string]einocallbacks.ToolMetadata, len(input.DynamicMCPToolDefinitions)+len(input.StaticToolCodes)) for _, item := range input.DynamicMCPToolDefinitions { @@ -118,6 +126,15 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input Buil SourceType: resolveToolSourceType(toolCode), } } + if input.SelectedSkill != nil { + toolMetadataBy[toolx.BuiltinSkillToolName] = einocallbacks.ToolMetadata{ + ToolCode: toolx.BuiltinSkillToolCode, + ServerCode: toolx.BuiltinToolCatalogServerCode, + ToolName: toolx.BuiltinSkillToolName, + SourceType: toolx.BuiltinToolCatalogServerCode, + } + input.Collector.SetSkillMiddleware(true, toolx.BuiltinSkillToolName) + } handlers = append(handlers, einocallbacks.NewRuntimeTraceHandler(input.Collector, toolMetadataBy)) } instructionResult := assembleAgentInstruction(input.AIAgent, input.SelectedSkill, input.InstructionToolDefinitions, input.StaticToolCodes) @@ -149,11 +166,26 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, input Buil return &einoagents.CustomerServiceAgent{Inner: inner}, nil } +func (f *AgentFactory) buildSelectedSkillMiddleware(ctx context.Context, selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) (adk.ChatModelAgentMiddleware, error) { + backend, err := newSelectedSkillBackend(selectedSkill, toolDefinitions) + if err != nil { + return nil, err + } + toolName := toolx.BuiltinSkillToolName + return einoskill.NewMiddleware(ctx, &einoskill.Config{ + Backend: backend, + SkillToolName: &toolName, + UseChinese: true, + }) +} + func resolveToolSourceType(toolCode string) string { toolCode = strings.TrimSpace(toolCode) switch { case toolCode == toolx.BuiltinToolSearchToolCode: return toolx.BuiltinToolCatalogServerCode + case toolCode == toolx.BuiltinSkillToolCode: + return toolx.BuiltinToolCatalogServerCode case strings.HasPrefix(toolCode, toolx.GraphToolCatalogServerCode+"/"): return toolx.GraphToolCatalogServerCode case strings.HasPrefix(toolCode, toolx.BuiltinToolCatalogServerCode+"/"): @@ -169,7 +201,7 @@ func assembleAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.Ski baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt) } appendixParts := make([]string, 0, 2) - if skillInstruction := buildSelectedSkillInstruction(selectedSkill, toolDefinitions); skillInstruction != "" { + if skillInstruction := buildSelectedSkillActivationInstruction(selectedSkill); skillInstruction != "" { appendixParts = append(appendixParts, skillInstruction) } if len(toolDefinitions) > 0 { @@ -229,7 +261,23 @@ func remainingAppendixParts(parts []string) []string { return ret } -func buildSelectedSkillInstruction(skill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) string { +func buildSelectedSkillActivationInstruction(skill *models.SkillDefinition) string { + if skill == nil { + return "" + } + lines := []string{ + "当前命中的专项技能:", + fmt.Sprintf("- code: %s", strings.TrimSpace(skill.Code)), + fmt.Sprintf("- name: %s", strings.TrimSpace(skill.Name)), + } + if desc := strings.TrimSpace(skill.Description); desc != "" { + lines = append(lines, fmt.Sprintf("- description: %s", desc)) + } + lines = append(lines, "", "执行要求:", "- 本轮优先处理该技能范围内的问题。", fmt.Sprintf("- 需要专项处理细节时,优先调用 %s 工具加载该技能说明后再继续。", toolx.BuiltinSkillToolName), "- 如果关键信息不足,先向用户追问。", "- 不得调用当前技能未授权的工具。") + return strings.TrimSpace(strings.Join(lines, "\n")) +} + +func buildSelectedSkillDocument(skill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) string { if skill == nil { return "" } diff --git a/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go b/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go new file mode 100644 index 0000000..31b96fc --- /dev/null +++ b/internal/ai/runtime/internal/impl/factory/skill_middleware_backend.go @@ -0,0 +1,60 @@ +package factory + +import ( + "context" + "fmt" + "strings" + + einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter" + "cs-agent/internal/models" + + einoskill "github.com/cloudwego/eino/adk/middlewares/skill" +) + +type selectedSkillBackend struct { + frontMatter einoskill.FrontMatter + skill einoskill.Skill +} + +func newSelectedSkillBackend(selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) (*selectedSkillBackend, error) { + if selectedSkill == nil { + return nil, fmt.Errorf("selected skill is nil") + } + skillName := strings.TrimSpace(selectedSkill.Code) + if skillName == "" { + return nil, fmt.Errorf("selected skill code is empty") + } + description := strings.TrimSpace(selectedSkill.Description) + content := buildSelectedSkillDocument(selectedSkill, toolDefinitions) + return &selectedSkillBackend{ + frontMatter: einoskill.FrontMatter{ + Name: skillName, + Description: description, + }, + skill: einoskill.Skill{ + FrontMatter: einoskill.FrontMatter{ + Name: skillName, + Description: description, + }, + Content: content, + }, + }, nil +} + +func (b *selectedSkillBackend) List(_ context.Context) ([]einoskill.FrontMatter, error) { + if b == nil { + return nil, nil + } + return []einoskill.FrontMatter{b.frontMatter}, nil +} + +func (b *selectedSkillBackend) Get(_ context.Context, name string) (einoskill.Skill, error) { + if b == nil { + return einoskill.Skill{}, fmt.Errorf("selected skill backend is nil") + } + name = strings.TrimSpace(name) + if name == "" || strings.EqualFold(name, b.frontMatter.Name) { + return b.skill, nil + } + return einoskill.Skill{}, fmt.Errorf("skill %q not found", name) +} diff --git a/internal/pkg/toolx/builtin_tools.go b/internal/pkg/toolx/builtin_tools.go index 4b51385..7b0c90e 100644 --- a/internal/pkg/toolx/builtin_tools.go +++ b/internal/pkg/toolx/builtin_tools.go @@ -6,6 +6,10 @@ const ( BuiltinToolSearchToolName = "tool_search" BuiltinToolSearchToolTitle = "搜索并调用动态工具" BuiltinToolSearchToolDescription = "用于搜索当前允许使用的 MCP 工具,并在确认目标 toolCode 后动态调用该工具。适合处理长尾工具,不应替代固定内置流程工具。" + BuiltinSkillToolCode = "builtin/skill" + BuiltinSkillToolName = "skill" + BuiltinSkillToolTitle = "加载专项技能说明" + BuiltinSkillToolDescription = "用于加载当前命中的专项技能说明文档。仅在本轮已命中 Skill 时可用,适合将专项处理规则按需注入上下文。" GraphToolCatalogServerCode = "graph" GraphCreateTicketConfirmToolCode = "graph/create_ticket_with_confirmation" GraphCreateTicketConfirmToolName = "create_ticket_with_confirmation" @@ -22,7 +26,7 @@ const ( ) func IsAutoInjectedToolCode(toolCode string) bool { - return toolCode == BuiltinToolSearchToolCode + return toolCode == BuiltinToolSearchToolCode || toolCode == BuiltinSkillToolCode } func NormalizeToolCodeAlias(toolCode string) string {