From 28d18ab6c00df05d47b88bca447ad07b76bff5dd Mon Sep 17 00:00:00 2001 From: mlogclub Date: Thu, 9 Apr 2026 11:58:04 +0800 Subject: [PATCH] Implement skill debugging functionality and refactor skill execution flow --- internal/ai/runtime/debug_run.go | 83 ++++++++++++++++++ .../ai/runtime/internal/engine/service.go | 53 ++++++++++- internal/ai/runtime/internal/engine/types.go | 20 +++-- .../internal/impl/factory/agent_factory.go | 71 ++++++++++++++- internal/ai/runtime/service.go | 87 ++++++++++++------- internal/ai/runtime/types.go | 19 ++-- internal/ai/skills/execution.go | 62 ------------- internal/ai/skills/runtime.go | 37 ++++---- internal/ai/skills/types.go | 34 ++------ internal/pkg/dto/response/skill_response.go | 20 +++-- internal/pkg/enums/skill.go | 17 ---- internal/services/skill_runtime_service.go | 51 ++--------- 12 files changed, 321 insertions(+), 233 deletions(-) create mode 100644 internal/ai/runtime/debug_run.go delete mode 100644 internal/ai/skills/execution.go delete mode 100644 internal/pkg/enums/skill.go diff --git a/internal/ai/runtime/debug_run.go b/internal/ai/runtime/debug_run.go new file mode 100644 index 0000000..94db644 --- /dev/null +++ b/internal/ai/runtime/debug_run.go @@ -0,0 +1,83 @@ +package runtime + +import ( + "context" + "strings" + + "cs-agent/internal/models" + "cs-agent/internal/pkg/dto/request" + "cs-agent/internal/pkg/dto/response" + "cs-agent/internal/pkg/enums" + "cs-agent/internal/pkg/errorsx" + svc "cs-agent/internal/services" +) + +func init() { + svc.SkillDebugRunHook = DebugRunSkill +} + +func DebugRunSkill(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) { + aiAgent := svc.AIAgentService.Get(req.AIAgentID) + if aiAgent == nil || aiAgent.Status != enums.StatusOk { + return nil, errorsx.InvalidParam("AI Agent不存在或未启用") + } + aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) + if aiConfig == nil { + return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") + } + conversation := &models.Conversation{ID: req.ConversationID, AIAgentID: req.AIAgentID} + if req.ConversationID > 0 { + conversation = svc.ConversationService.Get(req.ConversationID) + if conversation == nil { + return nil, errorsx.InvalidParam("会话不存在") + } + } + message := &models.Message{ + ConversationID: req.ConversationID, + SenderType: enums.IMSenderTypeCustomer, + MessageType: enums.IMMessageTypeText, + Content: strings.TrimSpace(req.UserMessage), + } + summary, err := Service.Run(ctx, Request{ + Conversation: conversation, + UserMessage: message, + AIAgent: aiAgent, + AIConfig: aiConfig, + ManualSkillCode: strings.TrimSpace(req.SkillCode), + }) + if err != nil { + return buildSkillDebugRunResponse(req, summary, nil), err + } + selectedSkill := svc.SkillDefinitionService.GetByCode(strings.TrimSpace(req.SkillCode)) + if summary == nil || strings.TrimSpace(summary.PlannedSkillCode) == "" { + return nil, errorsx.InvalidParam("Skill 未命中") + } + return buildSkillDebugRunResponse(req, summary, selectedSkill), nil +} + +func buildSkillDebugRunResponse(req request.SkillDebugRunRequest, summary *Summary, skill *models.SkillDefinition) *response.SkillDebugRunResponse { + resp := &response.SkillDebugRunResponse{ + ConversationID: req.ConversationID, + AIAgentID: req.AIAgentID, + } + if skill != nil { + resp.SkillCode = skill.Code + resp.SkillName = skill.Name + } + if summary == nil { + return resp + } + if resp.SkillCode == "" { + resp.SkillCode = strings.TrimSpace(summary.PlannedSkillCode) + } + resp.ReplyText = summary.ReplyText + resp.PlanReason = summary.PlanReason + resp.SkillRouteTrace = summary.SkillRouteTrace + resp.ToolCodes = append([]string(nil), summary.ToolCodes...) + resp.InvokedToolCodes = append([]string(nil), summary.InvokedToolCodes...) + resp.CheckPointID = summary.CheckPointID + resp.Interrupted = summary.Interrupted + resp.TraceData = summary.TraceData + resp.ErrorMessage = summary.ErrorMessage + return resp +} diff --git a/internal/ai/runtime/internal/engine/service.go b/internal/ai/runtime/internal/engine/service.go index fe68e4d..01356ae 100644 --- a/internal/ai/runtime/internal/engine/service.go +++ b/internal/ai/runtime/internal/engine/service.go @@ -11,6 +11,7 @@ import ( "cs-agent/internal/ai/runtime/internal/impl/callbacks" "cs-agent/internal/ai/runtime/internal/impl/factory" "cs-agent/internal/ai/runtime/internal/impl/retrievers" + "cs-agent/internal/models" "cs-agent/internal/pkg/utils" "github.com/cloudwego/eino/adk" @@ -74,8 +75,9 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { summary.TraceData = collector.Marshal() return summary, err } - toolDefsByModelName := make(map[string]string, len(toolDefs)) - for _, item := range toolDefs { + filteredToolDefs := filterToolDefinitionsBySkill(toolDefs, req.SelectedSkill) + toolDefsByModelName := make(map[string]string, len(filteredToolDefs)) + for _, item := range filteredToolDefs { summary.ToolCodes = append(summary.ToolCodes, item.ToolCode) toolDefsByModelName[item.ModelName] = item.ToolCode } @@ -92,8 +94,14 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { collector.Data.Model.Provider = string(req.AIConfig.Provider) collector.Data.Model.Name = req.AIConfig.ModelName + summary.SelectedSkillCode = "" + summary.SkillRouteReason = strings.TrimSpace(req.SkillRouteReason) + summary.SkillRouteTrace = strings.TrimSpace(req.SkillRouteTrace) + if req.SelectedSkill != nil { + summary.SelectedSkillCode = strings.TrimSpace(req.SelectedSkill.Code) + } - agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, toolDefs, req.ExtraTools, req.ExtraToolCodes, collector) + agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, req.SelectedSkill, filteredToolDefs, req.ExtraTools, req.ExtraToolCodes, collector) if err != nil { summary.Status = "error" summary.ErrorMessage = err.Error() @@ -217,7 +225,7 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro collector.Data.Input.ToolCodes = append(collector.Data.Input.ToolCodes, summary.ToolCodes...) collector.Data.Model.Provider = string(req.AIConfig.Provider) collector.Data.Model.Name = req.AIConfig.ModelName - agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, toolDefs, req.ExtraTools, req.ExtraToolCodes, collector) + agent, err := s.agentFactory.BuildCustomerServiceAgent(ctx, req.AIAgent, req.AIConfig, nil, toolDefs, req.ExtraTools, req.ExtraToolCodes, collector) if err != nil { summary.Status = "error" summary.ErrorMessage = err.Error() @@ -261,6 +269,43 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro return summary, nil } +func filterToolDefinitionsBySkill(definitions []adapter.MCPToolDefinition, skill *models.SkillDefinition) []adapter.MCPToolDefinition { + if len(definitions) == 0 || skill == nil { + return definitions + } + allowed := parseJSONArraySet(skill.AllowedToolCodes) + if len(allowed) == 0 { + return definitions + } + ret := make([]adapter.MCPToolDefinition, 0, len(definitions)) + for _, item := range definitions { + if _, ok := allowed[strings.TrimSpace(item.ToolCode)]; ok { + ret = append(ret, item) + } + } + return ret +} + +func parseJSONArraySet(raw string) map[string]struct{} { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + var items []string + if err := json.Unmarshal([]byte(raw), &items); err != nil { + return nil + } + ret := make(map[string]struct{}, len(items)) + for _, item := range items { + item = strings.TrimSpace(item) + if item == "" { + continue + } + ret[item] = struct{}{} + } + return ret +} + func buildRunOptions(checkPointID string) []adk.AgentRunOption { if strings.TrimSpace(checkPointID) == "" { return nil diff --git a/internal/ai/runtime/internal/engine/types.go b/internal/ai/runtime/internal/engine/types.go index 83db43c..5fafea1 100644 --- a/internal/ai/runtime/internal/engine/types.go +++ b/internal/ai/runtime/internal/engine/types.go @@ -7,13 +7,16 @@ import ( ) type Request struct { - Conversation *models.Conversation - UserMessage *models.Message - AIAgent *models.AIAgent - AIConfig *models.AIConfig - CheckPointID string - ExtraTools []einotool.BaseTool - ExtraToolCodes map[string]string + Conversation *models.Conversation + UserMessage *models.Message + AIAgent *models.AIAgent + AIConfig *models.AIConfig + SelectedSkill *models.SkillDefinition + SkillRouteReason string + SkillRouteTrace string + CheckPointID string + ExtraTools []einotool.BaseTool + ExtraToolCodes map[string]string } type ResumeRequest struct { @@ -36,6 +39,9 @@ type Summary struct { RunID string Status string ReplyText string + SelectedSkillCode string + SkillRouteReason string + SkillRouteTrace string ModelName string PromptTokens int CompletionTokens int diff --git a/internal/ai/runtime/internal/impl/factory/agent_factory.go b/internal/ai/runtime/internal/impl/factory/agent_factory.go index 834272c..1ce0af8 100644 --- a/internal/ai/runtime/internal/impl/factory/agent_factory.go +++ b/internal/ai/runtime/internal/impl/factory/agent_factory.go @@ -2,6 +2,8 @@ package factory import ( "context" + "encoding/json" + "fmt" "strings" einoadapter "cs-agent/internal/ai/runtime/internal/impl/adapter" @@ -27,7 +29,7 @@ func NewAgentFactory() *AgentFactory { } func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *models.AIAgent, aiConfig *models.AIConfig, - toolDefinitions []einoadapter.MCPToolDefinition, extraTools []einobasetool.BaseTool, extraToolCodes map[string]string, + selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition, extraTools []einobasetool.BaseTool, extraToolCodes map[string]string, collector *einocallbacks.RuntimeTraceCollector) (*einoagents.CustomerServiceAgent, error) { if aiAgent == nil || aiConfig == nil { return nil, nil @@ -58,7 +60,7 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m inner, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ Name: strings.TrimSpace(aiAgent.Name), Description: strings.TrimSpace(aiAgent.Description), - Instruction: buildAgentInstruction(aiAgent, extraToolCodes), + Instruction: buildAgentInstruction(aiAgent, selectedSkill, toolDefinitions, extraToolCodes), Model: chatModel, ToolsConfig: adk.ToolsConfig{ ToolsNodeConfig: compose.ToolsNodeConfig{ @@ -73,12 +75,15 @@ func (f *AgentFactory) BuildCustomerServiceAgent(ctx context.Context, aiAgent *m return &einoagents.CustomerServiceAgent{Inner: inner}, nil } -func buildAgentInstruction(aiAgent *models.AIAgent, extraToolCodes map[string]string) string { +func buildAgentInstruction(aiAgent *models.AIAgent, selectedSkill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition, extraToolCodes map[string]string) string { baseInstruction := "" if aiAgent != nil { baseInstruction = strings.TrimSpace(aiAgent.SystemPrompt) } - appendixParts := make([]string, 0, 1) + appendixParts := make([]string, 0, 2) + if skillInstruction := buildSelectedSkillInstruction(selectedSkill, toolDefinitions); skillInstruction != "" { + appendixParts = append(appendixParts, skillInstruction) + } if hasToolCode(extraToolCodes, "builtin/create_ticket_with_confirmation") { appendixParts = append(appendixParts, strings.TrimSpace(` 你可以在确认信息充分后调用 create_ticket_with_confirmation 工具来创建工单,但必须遵守以下规则: @@ -98,6 +103,44 @@ func buildAgentInstruction(aiAgent *models.AIAgent, extraToolCodes map[string]st return baseInstruction + "\n\n" + strings.Join(appendixParts, "\n\n") } +func buildSelectedSkillInstruction(skill *models.SkillDefinition, toolDefinitions []einoadapter.MCPToolDefinition) 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)) + } + if content := strings.TrimSpace(skill.Content); content != "" { + lines = append(lines, "", "技能说明:", content) + } + if examples := parseJSONStringArray(skill.Examples); len(examples) > 0 { + lines = append(lines, "", "典型示例问法:") + for _, item := range examples { + lines = append(lines, "- "+item) + } + } + if len(toolDefinitions) > 0 { + lines = append(lines, "", "当前技能允许使用的工具:") + for _, item := range toolDefinitions { + if strings.TrimSpace(item.ToolCode) == "" { + continue + } + line := "- " + strings.TrimSpace(item.ToolCode) + if title := strings.TrimSpace(item.Title); title != "" { + line += " | " + title + } + lines = append(lines, line) + } + } + lines = append(lines, "", "执行要求:", "- 优先遵循该技能说明完成任务。", "- 如果关键信息不足,先向用户追问。", "- 不得调用当前技能未授权的工具。") + return strings.TrimSpace(strings.Join(lines, "\n")) +} + func hasToolCode(toolCodes map[string]string, target string) bool { target = strings.TrimSpace(target) if target == "" { @@ -110,3 +153,23 @@ func hasToolCode(toolCodes map[string]string, target string) bool { } return false } + +func parseJSONStringArray(raw string) []string { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + var ret []string + if err := json.Unmarshal([]byte(raw), &ret); err != nil { + return nil + } + out := make([]string, 0, len(ret)) + for _, item := range ret { + item = strings.TrimSpace(item) + if item == "" { + continue + } + out = append(out, item) + } + return out +} diff --git a/internal/ai/runtime/service.go b/internal/ai/runtime/service.go index fecce98..dc8d18b 100644 --- a/internal/ai/runtime/service.go +++ b/internal/ai/runtime/service.go @@ -2,12 +2,14 @@ package runtime import ( "context" + "encoding/json" "strings" "cs-agent/internal/ai/runtime/internal/engine" "cs-agent/internal/ai/runtime/registry" "cs-agent/internal/ai/runtime/tools" "cs-agent/internal/ai/skills" + "cs-agent/internal/models" ) var Service = newService() @@ -27,21 +29,27 @@ type service struct { } func (s *service) Run(ctx context.Context, req Request) (*Summary, error) { - skillSummary, skillErr := s.tryRunSkill(ctx, req) - if skillSummary != nil && strings.TrimSpace(skillSummary.ReplyText) != "" { - return skillSummary, nil + selectedSkill, skillReason, skillTrace, skillErr := s.selectSkill(ctx, req) + req.SelectedSkill = selectedSkill + req.SkillRouteReason = skillReason + req.SkillRouteTrace = skillTrace + if req.SelectedSkill != nil { + req.SelectedSkill = cloneSkillDefinition(req.SelectedSkill) } if err := s.prepareToolsForRun(&req); err != nil { return nil, err } summary, err := s.runtime.Run(ctx, engine.Request{ - Conversation: req.Conversation, - UserMessage: req.UserMessage, - AIAgent: req.AIAgent, - AIConfig: req.AIConfig, - CheckPointID: req.CheckPointID, - ExtraTools: req.ExtraTools, - ExtraToolCodes: req.ExtraToolCodes, + Conversation: req.Conversation, + UserMessage: req.UserMessage, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + SelectedSkill: req.SelectedSkill, + SkillRouteReason: req.SkillRouteReason, + SkillRouteTrace: req.SkillRouteTrace, + CheckPointID: req.CheckPointID, + ExtraTools: req.ExtraTools, + ExtraToolCodes: req.ExtraToolCodes, }) if err != nil { ret := toSummary(summary) @@ -119,8 +127,9 @@ func toSummary(summary *engine.Summary) *Summary { RunID: summary.RunID, Status: summary.Status, ReplyText: summary.ReplyText, - PlannedSkillCode: "", - PlanReason: "", + PlannedSkillCode: strings.TrimSpace(summary.SelectedSkillCode), + PlanReason: strings.TrimSpace(summary.SkillRouteReason), + SkillRouteTrace: strings.TrimSpace(summary.SkillRouteTrace), ModelName: summary.ModelName, PromptTokens: summary.PromptTokens, CompletionTokens: summary.CompletionTokens, @@ -147,31 +156,45 @@ func toSummary(summary *engine.Summary) *Summary { return ret } -func (s *service) tryRunSkill(ctx context.Context, req Request) (*Summary, error) { +func (s *service) selectSkill(ctx context.Context, req Request) (*models.SkillDefinition, string, string, error) { if req.AIAgent == nil || req.AIConfig == nil || req.UserMessage == nil || req.Conversation == nil { - return nil, nil + return nil, "", "", nil } - result, err := skills.Execute(ctx, skills.RuntimeContext{ - AIAgentID: req.AIAgent.ID, - UserMessage: strings.TrimSpace(req.UserMessage.Content), - ConversationID: req.Conversation.ID, + result, err := skills.Select(ctx, skills.RuntimeContext{ + AIAgentID: req.AIAgent.ID, + UserMessage: strings.TrimSpace(req.UserMessage.Content), + ConversationID: req.Conversation.ID, + ManualSkillCode: strings.TrimSpace(req.ManualSkillCode), }) if err != nil { - return nil, err + return nil, "", "", err } if result == nil || result.Plan == nil || result.Plan.Skill == nil { - return nil, nil + traceData := marshalSkillRouteTrace(result) + reason := "" + if result != nil && result.Plan != nil { + reason = strings.TrimSpace(result.Plan.MatchReason) + } + return nil, reason, traceData, nil } - traceData := "" - if result.RunLog != nil { - traceData = result.RunLog.TraceData - } - return &Summary{ - Status: "completed", - ReplyText: strings.TrimSpace(result.ReplyText), - PlannedSkillCode: strings.TrimSpace(result.Plan.Skill.Code), - PlanReason: strings.TrimSpace(result.Plan.MatchReason), - ModelName: req.AIConfig.ModelName, - TraceData: traceData, - }, nil + return result.Plan.Skill, strings.TrimSpace(result.Plan.MatchReason), marshalSkillRouteTrace(result), nil +} + +func marshalSkillRouteTrace(result *skills.ExecutionResult) string { + if result == nil || result.Plan == nil || result.Plan.RouteTrace == nil { + return "" + } + buf, err := json.Marshal(result.Plan.RouteTrace) + if err != nil { + return "" + } + return string(buf) +} + +func cloneSkillDefinition(item *models.SkillDefinition) *models.SkillDefinition { + if item == nil { + return nil + } + clone := *item + return &clone } diff --git a/internal/ai/runtime/types.go b/internal/ai/runtime/types.go index 8eadd1b..cd2467e 100644 --- a/internal/ai/runtime/types.go +++ b/internal/ai/runtime/types.go @@ -7,13 +7,17 @@ import ( ) type Request struct { - Conversation *models.Conversation - UserMessage *models.Message - AIAgent *models.AIAgent - AIConfig *models.AIConfig - CheckPointID string - ExtraTools []einotool.BaseTool - ExtraToolCodes map[string]string + Conversation *models.Conversation + UserMessage *models.Message + AIAgent *models.AIAgent + AIConfig *models.AIConfig + ManualSkillCode string + SelectedSkill *models.SkillDefinition + SkillRouteReason string + SkillRouteTrace string + CheckPointID string + ExtraTools []einotool.BaseTool + ExtraToolCodes map[string]string } type ResumeRequest struct { @@ -38,6 +42,7 @@ type Summary struct { ReplyText string PlannedSkillCode string PlanReason string + SkillRouteTrace string ModelName string PromptTokens int CompletionTokens int diff --git a/internal/ai/skills/execution.go b/internal/ai/skills/execution.go deleted file mode 100644 index 61c7eae..0000000 --- a/internal/ai/skills/execution.go +++ /dev/null @@ -1,62 +0,0 @@ -package skills - -import ( - "context" - "strings" - "time" - - "cs-agent/internal/ai" - "cs-agent/internal/pkg/errorsx" -) - -func executeByPlan(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext) (string, *ExecutionTrace, error) { - if plan == nil || plan.Skill == nil { - return "", nil, nil - } - trace := &ExecutionTrace{ - Status: "started", - ExecutionMode: "content", - } - replyText, err := executeContent(ctx, plan, runtimeCtx, trace) - return replyText, trace, err -} - -func executeContent(ctx context.Context, plan *ExecutionPlan, runtimeCtx RuntimeContext, trace *ExecutionTrace) (string, error) { - if plan == nil || plan.Skill == nil { - return "", nil - } - if plan.AIConfig == nil { - return "", errorsx.InvalidParam("Skill 关联的 AI 配置不可用") - } - systemPrompt := strings.TrimSpace(plan.Skill.Content) - if systemPrompt == "" { - return "", errorsx.InvalidParam("Skill Content 不能为空") - } - userPrompt := strings.TrimSpace(runtimeCtx.UserMessage) - if userPrompt == "" { - return "", errorsx.InvalidParam("用户消息不能为空") - } - promptTrace := &PromptTrace{Status: "started"} - if trace != nil { - trace.Prompt = promptTrace - } - startedAt := time.Now() - result, err := ai.LLM.ChatWithConfig(ctx, plan.AIConfig, systemPrompt, userPrompt) - promptTrace.LatencyMs = time.Since(startedAt).Milliseconds() - if err != nil { - promptTrace.Status = "error" - promptTrace.Error = err.Error() - if trace != nil { - trace.Status = "error" - } - return "", err - } - promptTrace.Status = "ok" - promptTrace.ModelName = result.ModelName - promptTrace.PromptTokens = result.PromptTokens - promptTrace.CompletionTokens = result.CompletionTokens - if trace != nil { - trace.Status = "ok" - } - return strings.TrimSpace(result.Content), nil -} diff --git a/internal/ai/skills/runtime.go b/internal/ai/skills/runtime.go index c1e9f12..89c020a 100644 --- a/internal/ai/skills/runtime.go +++ b/internal/ai/skills/runtime.go @@ -40,7 +40,7 @@ func BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*Execution }, nil } -// WriteRunLog 写入 Skill 运行日志。 +// WriteRunLog 写入 Skill 路由日志。 func WriteRunLog(log *models.SkillRunLog) error { if log == nil { return nil @@ -48,37 +48,33 @@ func WriteRunLog(log *models.SkillRunLog) error { return repositories.SkillRunLogRepository.Create(sqls.DB(), log) } -// Execute 执行一次 Skill 运行,当前阶段仅支持 prompt_only 风格的手动 Skill。 -func Execute(ctx context.Context, runtimeCtx RuntimeContext) (*ExecutionResult, error) { +// Select 执行一次 Skill 路由并记录路由日志。 +func Select(ctx context.Context, runtimeCtx RuntimeContext) (*ExecutionResult, error) { plan, err := BuildExecutionPlan(ctx, runtimeCtx) if err != nil { - trace := &ExecutionTrace{Status: "plan_error"} + trace := &ExecutionTrace{Status: "route_error"} log := BuildRunLog(runtimeCtx, nil, trace, err) _ = WriteRunLog(log) return nil, err } + trace := &ExecutionTrace{Status: "ok"} if plan == nil || plan.Skill == nil { - trace := &ExecutionTrace{Status: "noop"} if plan != nil { + trace.Status = "not_matched" trace.MatchReason = strings.TrimSpace(plan.MatchReason) trace.Route = plan.RouteTrace } log := BuildRunLog(runtimeCtx, plan, trace, nil) _ = WriteRunLog(log) - return nil, nil - } - - replyText, trace, err := executeByPlan(ctx, plan, runtimeCtx) - if trace != nil { - trace.MatchReason = strings.TrimSpace(plan.MatchReason) - if trace.Route == nil { - trace.Route = plan.RouteTrace - } + return &ExecutionResult{ + Plan: plan, + RunLog: log, + Trace: trace, + }, nil } + trace.MatchReason = strings.TrimSpace(plan.MatchReason) + trace.Route = plan.RouteTrace log := BuildRunLog(runtimeCtx, plan, trace, err) - if strings.TrimSpace(replyText) != "" && strings.TrimSpace(log.MatchReason) == "" { - log.MatchReason = "content" - } if writeErr := WriteRunLog(log); writeErr != nil && err == nil { err = writeErr } @@ -86,9 +82,8 @@ func Execute(ctx context.Context, runtimeCtx RuntimeContext) (*ExecutionResult, return nil, err } return &ExecutionResult{ - Plan: plan, - ReplyText: strings.TrimSpace(replyText), - RunLog: log, - Trace: trace, + Plan: plan, + RunLog: log, + Trace: trace, }, nil } diff --git a/internal/ai/skills/types.go b/internal/ai/skills/types.go index d897650..1f9d247 100644 --- a/internal/ai/skills/types.go +++ b/internal/ai/skills/types.go @@ -11,7 +11,7 @@ type RuntimeContext struct { IntentCode string // IntentCode 为上游识别出的意图编码。 } -// ExecutionPlan 表示 Skill Runtime 计算出的最终执行计划。 +// ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。 type ExecutionPlan struct { AIAgent *models.AIAgent // AIAgent 为本次请求所属的 AI Agent。 AIConfig *models.AIConfig // AIConfig 为本次请求实际使用的模型配置。 @@ -20,21 +20,17 @@ type ExecutionPlan struct { RouteTrace *RouteTrace // RouteTrace 为匹配阶段的路由追踪。 } -// ExecutionResult 表示一次 Skill 执行的最终结果。 +// ExecutionResult 表示一次 Skill 路由的最终结果。 type ExecutionResult struct { - Plan *ExecutionPlan - ReplyText string - RunLog *models.SkillRunLog - Trace *ExecutionTrace + Plan *ExecutionPlan + RunLog *models.SkillRunLog + Trace *ExecutionTrace } type ExecutionTrace struct { - Status string `json:"status"` - MatchReason string `json:"matchReason,omitempty"` - Route *RouteTrace `json:"route,omitempty"` - ExecutionMode string `json:"executionMode,omitempty"` - Prompt *PromptTrace `json:"prompt,omitempty"` - MCP *MCPExecutionTrace `json:"mcp,omitempty"` + Status string `json:"status"` + MatchReason string `json:"matchReason,omitempty"` + Route *RouteTrace `json:"route,omitempty"` } type RouteTrace struct { @@ -54,17 +50,3 @@ type PromptTrace struct { CompletionTokens int `json:"completionTokens,omitempty"` Error string `json:"error,omitempty"` } - -type MCPExecutionTrace struct { - Status string `json:"status"` - ServerCode string `json:"serverCode,omitempty"` - ToolName string `json:"toolName,omitempty"` - Arguments map[string]any `json:"arguments,omitempty"` - LatencyMs int64 `json:"latencyMs,omitempty"` - IsError bool `json:"isError,omitempty"` - ContentItemCount int `json:"contentItemCount,omitempty"` - HasStructuredContent bool `json:"hasStructuredContent,omitempty"` - ResultPreview string `json:"resultPreview,omitempty"` - Error string `json:"error,omitempty"` - SummaryPrompt *PromptTrace `json:"summaryPrompt,omitempty"` -} diff --git a/internal/pkg/dto/response/skill_response.go b/internal/pkg/dto/response/skill_response.go index 45b6a5a..e014704 100644 --- a/internal/pkg/dto/response/skill_response.go +++ b/internal/pkg/dto/response/skill_response.go @@ -21,13 +21,19 @@ type SkillDefinitionResponse struct { } type SkillDebugRunResponse struct { - SkillCode string `json:"skillCode"` - SkillName string `json:"skillName"` - ReplyText string `json:"replyText"` - RunLogID int64 `json:"runLogId"` - TraceData string `json:"traceData"` - ConversationID int64 `json:"conversationId"` - AIAgentID int64 `json:"aiAgentId"` + SkillCode string `json:"skillCode"` + SkillName string `json:"skillName"` + ReplyText string `json:"replyText"` + PlanReason string `json:"planReason"` + SkillRouteTrace string `json:"skillRouteTrace"` + ToolCodes []string `json:"toolCodes"` + InvokedToolCodes []string `json:"invokedToolCodes"` + CheckPointID string `json:"checkPointId"` + Interrupted bool `json:"interrupted"` + TraceData string `json:"traceData"` + ErrorMessage string `json:"errorMessage"` + ConversationID int64 `json:"conversationId"` + AIAgentID int64 `json:"aiAgentId"` } type AgentRunLogResponse struct { diff --git a/internal/pkg/enums/skill.go b/internal/pkg/enums/skill.go deleted file mode 100644 index c66fe11..0000000 --- a/internal/pkg/enums/skill.go +++ /dev/null @@ -1,17 +0,0 @@ -package enums - -type SkillExecutionMode string - -const ( - SkillExecutionModePromptOnly SkillExecutionMode = "prompt_only" - SkillExecutionModeMCPTool SkillExecutionMode = "mcp_tool" -) - -var skillExecutionModeLabelMap = map[SkillExecutionMode]string{ - SkillExecutionModePromptOnly: "Prompt直出", - SkillExecutionModeMCPTool: "MCP工具", -} - -func GetSkillExecutionModeLabel(mode SkillExecutionMode) string { - return skillExecutionModeLabelMap[mode] -} diff --git a/internal/services/skill_runtime_service.go b/internal/services/skill_runtime_service.go index 7f0acd3..6fdf5eb 100644 --- a/internal/services/skill_runtime_service.go +++ b/internal/services/skill_runtime_service.go @@ -2,16 +2,16 @@ package services import ( "context" + "fmt" "strings" - "cs-agent/internal/ai/skills" "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/dto/response" - "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" ) var SkillRuntimeService = newSkillRuntimeService() +var SkillDebugRunHook func(ctx context.Context, req request.SkillDebugRunRequest) (*response.SkillDebugRunResponse, error) func newSkillRuntimeService() *skillRuntimeService { return &skillRuntimeService{} @@ -29,49 +29,8 @@ func (s *skillRuntimeService) DebugRun(ctx context.Context, req request.SkillDeb if strings.TrimSpace(req.UserMessage) == "" { return nil, errorsx.InvalidParam("userMessage不能为空") } - - aiAgent := AIAgentService.Get(req.AIAgentID) - if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return nil, errorsx.InvalidParam("AI Agent不存在或未启用") + if SkillDebugRunHook == nil { + return nil, fmt.Errorf("skill debug runner is not initialized") } - if AIConfigService.Get(aiAgent.AIConfigID) == nil { - return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") - } - if req.ConversationID > 0 { - conversation := ConversationService.Get(req.ConversationID) - if conversation == nil { - return nil, errorsx.InvalidParam("会话不存在") - } - } - - result, err := skills.Execute(ctx, skills.RuntimeContext{ - AIAgentID: req.AIAgentID, - UserMessage: strings.TrimSpace(req.UserMessage), - ConversationID: req.ConversationID, - ManualSkillCode: strings.TrimSpace(req.SkillCode), - }) - if err != nil { - return nil, err - } - if result == nil || result.Plan == nil || result.Plan.Skill == nil { - return nil, errorsx.InvalidParam("Skill 未命中") - } - return buildSkillDebugRunResponse(req, result), nil -} - -func buildSkillDebugRunResponse(req request.SkillDebugRunRequest, result *skills.ExecutionResult) *response.SkillDebugRunResponse { - resp := &response.SkillDebugRunResponse{ - ConversationID: req.ConversationID, - AIAgentID: req.AIAgentID, - ReplyText: result.ReplyText, - } - if result.Plan != nil && result.Plan.Skill != nil { - resp.SkillCode = result.Plan.Skill.Code - resp.SkillName = result.Plan.Skill.Name - } - if result.RunLog != nil { - resp.RunLogID = result.RunLog.ID - resp.TraceData = result.RunLog.TraceData - } - return resp + return SkillDebugRunHook(ctx, req) }