From 102271ac8431380e67f48148f59ffdc8f03b4dfa Mon Sep 17 00:00:00 2001 From: mlogclub Date: Mon, 13 Apr 2026 19:15:12 +0800 Subject: [PATCH] feat: introduce prepare and plan services for improved skill execution handling --- internal/ai/runtime/app/prepare_service.go | 175 +++++++++++++++ internal/ai/runtime/app/service.go | 219 ++----------------- internal/ai/runtime/app/summary_builder.go | 46 ++++ internal/ai/runtime/reply_service.go | 90 -------- internal/ai/runtime/reply_trigger_service.go | 79 +++++++ internal/ai/runtime/reply_types.go | 20 ++ internal/ai/skills/plan_service.go | 46 ++++ internal/ai/skills/service.go | 32 +-- 8 files changed, 384 insertions(+), 323 deletions(-) create mode 100644 internal/ai/runtime/app/prepare_service.go create mode 100644 internal/ai/runtime/app/summary_builder.go create mode 100644 internal/ai/runtime/reply_trigger_service.go create mode 100644 internal/ai/runtime/reply_types.go create mode 100644 internal/ai/skills/plan_service.go diff --git a/internal/ai/runtime/app/prepare_service.go b/internal/ai/runtime/app/prepare_service.go new file mode 100644 index 0000000..b8f31d8 --- /dev/null +++ b/internal/ai/runtime/app/prepare_service.go @@ -0,0 +1,175 @@ +package app + +import ( + "context" + "encoding/json" + "strings" + + "cs-agent/internal/ai/runtime/registry" + "cs-agent/internal/ai/skills" + "cs-agent/internal/models" + "cs-agent/internal/pkg/toolx" +) + +func newPrepareService(registry *registry.Registry) *prepareService { + return &prepareService{registry: registry} +} + +type prepareService struct { + registry *registry.Registry +} + +func (s *prepareService) 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 + } + 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 + } + if result == nil || result.Plan == nil || result.Plan.Skill == nil { + traceData := marshalSkillRouteTrace(result) + reason := "" + if result != nil && result.Plan != nil { + reason = strings.TrimSpace(result.Plan.MatchReason) + } + return nil, reason, traceData, nil + } + return result.Plan.Skill, strings.TrimSpace(result.Plan.MatchReason), marshalSkillRouteTrace(result), nil +} + +func (s *prepareService) prepareToolsForRun(req *Request) error { + if req == nil || req.ToolSet != nil || s.registry == nil { + return nil + } + toolSet, err := s.registry.Resolve(registry.Context{ + Conversation: req.Conversation, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + UserMessage: req.UserMessage, + AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill), + }) + if err != nil { + return err + } + req.ToolSet = toolSet + return nil +} + +func (s *prepareService) prepareToolsForResume(req *ResumeRequest) error { + if req == nil || req.ToolSet != nil || s.registry == nil { + return nil + } + toolSet, err := s.registry.Resolve(registry.Context{ + Conversation: req.Conversation, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + AllowedToolCodes: parseAgentAllowedToolCodes(req.AIAgent), + }) + if err != nil { + return err + } + req.ToolSet = toolSet + return 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 +} + +func parseSkillAllowedToolCodes(skill *models.SkillDefinition) []string { + if skill == nil { + return nil + } + raw := strings.TrimSpace(skill.ToolWhitelist) + if raw == "" { + return nil + } + var items []string + if err := json.Unmarshal([]byte(raw), &items); err != nil { + return nil + } + ret := make([]string, 0, len(items)) + for _, item := range items { + item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) + if item == "" { + continue + } + ret = append(ret, item) + } + return ret +} + +func parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string { + if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" { + return nil + } + items, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools) + if err != nil { + return nil + } + ret := make([]string, 0, len(items)) + for _, item := range items { + toolCode := strings.TrimSpace(item.ToolCode) + toolCode = toolx.NormalizeToolCodeAlias(toolCode) + if toolCode == "" { + continue + } + ret = append(ret, toolCode) + } + return ret +} + +func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string { + agentAllowed := parseAgentAllowedToolCodes(aiAgent) + skillAllowed := parseSkillAllowedToolCodes(skill) + switch { + case len(agentAllowed) == 0: + return skillAllowed + case len(skillAllowed) == 0: + return agentAllowed + default: + skillSet := make(map[string]struct{}, len(skillAllowed)) + for _, item := range skillAllowed { + item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) + if item == "" { + continue + } + skillSet[item] = struct{}{} + } + ret := make([]string, 0, len(agentAllowed)) + for _, item := range agentAllowed { + item = strings.TrimSpace(item) + item = toolx.NormalizeToolCodeAlias(item) + if item == "" { + continue + } + if _, ok := skillSet[item]; ok { + ret = append(ret, item) + } + } + return ret + } +} diff --git a/internal/ai/runtime/app/service.go b/internal/ai/runtime/app/service.go index 639ebaf..b5bf44d 100644 --- a/internal/ai/runtime/app/service.go +++ b/internal/ai/runtime/app/service.go @@ -2,20 +2,16 @@ package app import ( "context" - "encoding/json" - "strings" "cs-agent/internal/ai/runtime/internal/executor" "cs-agent/internal/ai/runtime/registry" "cs-agent/internal/ai/runtime/tools" - "cs-agent/internal/ai/skills" - "cs-agent/internal/models" - "cs-agent/internal/pkg/toolx" ) type Service struct { runtime *executor.Service registry *registry.Registry + prepare *prepareService } func NewService() *Service { @@ -31,15 +27,23 @@ func NewService() *Service { } } +// TODO 这个方法真的要这样吗? 不能直接在NewService中直接初始化吗? +func (s *Service) initPrepareService() { + if s.prepare == nil { + s.prepare = newPrepareService(s.registry) + } +} + func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { - selectedSkill, skillReason, skillTrace, skillErr := s.selectSkill(ctx, req) + s.initPrepareService() + selectedSkill, skillReason, skillTrace, skillErr := s.prepare.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 { + if err := s.prepare.prepareToolsForRun(&req); err != nil { return nil, err } summary, err := s.runtime.ExecuteRun(ctx, executor.RunInput{ @@ -55,20 +59,21 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { }) if err != nil { ret := toSummary(summary) - if ret != nil && skillErr != nil && strings.TrimSpace(ret.PlanReason) == "" { + if ret != nil && skillErr != nil && ret.PlanReason == "" { ret.PlanReason = "skill_failed_fallback_runtime" } return ret, err } ret := toSummary(summary) - if ret != nil && skillErr != nil && strings.TrimSpace(ret.PlanReason) == "" { + if ret != nil && skillErr != nil && ret.PlanReason == "" { ret.PlanReason = "skill_failed_fallback_runtime" } return ret, nil } func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) { - if err := s.prepareToolsForResume(&req); err != nil { + s.initPrepareService() + if err := s.prepare.prepareToolsForResume(&req); err != nil { return nil, err } summary, err := s.runtime.ExecuteResume(ctx, executor.ResumeInput{ @@ -84,197 +89,3 @@ func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, erro } return toSummary(summary), nil } - -func (s *Service) prepareToolsForRun(req *Request) error { - if req == nil || req.ToolSet != nil || s.registry == nil { - return nil - } - toolSet, err := s.registry.Resolve(registry.Context{ - Conversation: req.Conversation, - AIAgent: req.AIAgent, - AIConfig: req.AIConfig, - UserMessage: req.UserMessage, - AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill), - }) - if err != nil { - return err - } - req.ToolSet = toolSet - return nil -} - -func (s *Service) prepareToolsForResume(req *ResumeRequest) error { - if req == nil || req.ToolSet != nil || s.registry == nil { - return nil - } - toolSet, err := s.registry.Resolve(registry.Context{ - Conversation: req.Conversation, - AIAgent: req.AIAgent, - AIConfig: req.AIConfig, - AllowedToolCodes: parseAgentAllowedToolCodes(req.AIAgent), - }) - if err != nil { - return err - } - req.ToolSet = toolSet - return nil -} - -func toSummary(summary *executor.RunResult) *Summary { - if summary == nil { - return nil - } - ret := &Summary{ - RunID: summary.RunID, - Status: summary.Status, - ReplyText: summary.ReplyText, - PlannedSkillCode: strings.TrimSpace(summary.SelectedSkillCode), - PlannedSkillName: strings.TrimSpace(summary.SelectedSkillName), - PlanReason: strings.TrimSpace(summary.SkillRouteReason), - SkillRouteTrace: strings.TrimSpace(summary.SkillRouteTrace), - SkillAllowedToolCodes: append([]string(nil), summary.SkillAllowedToolCodes...), - ModelName: summary.ModelName, - PromptTokens: summary.PromptTokens, - CompletionTokens: summary.CompletionTokens, - HistoryMessageCount: summary.HistoryMessageCount, - RetrieverCount: summary.RetrieverCount, - ToolCallCount: summary.ToolCallCount, - ToolCodes: append([]string(nil), summary.ToolCodes...), - InvokedToolCodes: append([]string(nil), summary.InvokedToolCodes...), - CheckPointID: summary.CheckPointID, - Interrupted: summary.Interrupted, - TraceData: summary.TraceData, - ErrorMessage: summary.ErrorMessage, - } - if len(summary.Interrupts) > 0 { - ret.Interrupts = make([]InterruptContextSummary, 0, len(summary.Interrupts)) - for _, item := range summary.Interrupts { - ret.Interrupts = append(ret.Interrupts, InterruptContextSummary{ - Type: item.Type, - ID: item.ID, - InfoPreview: item.InfoPreview, - }) - } - } - return ret -} - -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 - } - 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 - } - if result == nil || result.Plan == nil || result.Plan.Skill == nil { - traceData := marshalSkillRouteTrace(result) - reason := "" - if result != nil && result.Plan != nil { - reason = strings.TrimSpace(result.Plan.MatchReason) - } - return nil, reason, 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 -} - -func parseSkillAllowedToolCodes(skill *models.SkillDefinition) []string { - if skill == nil { - return nil - } - raw := strings.TrimSpace(skill.ToolWhitelist) - if raw == "" { - return nil - } - var items []string - if err := json.Unmarshal([]byte(raw), &items); err != nil { - return nil - } - ret := make([]string, 0, len(items)) - for _, item := range items { - item = strings.TrimSpace(item) - item = toolx.NormalizeToolCodeAlias(item) - if item == "" { - continue - } - ret = append(ret, item) - } - return ret -} - -func parseAgentAllowedToolCodes(aiAgent *models.AIAgent) []string { - if aiAgent == nil || strings.TrimSpace(aiAgent.AllowedMCPTools) == "" { - return nil - } - items, err := toolx.ParseAgentMCPToolsJSON(aiAgent.AllowedMCPTools) - if err != nil { - return nil - } - ret := make([]string, 0, len(items)) - for _, item := range items { - toolCode := strings.TrimSpace(item.ToolCode) - toolCode = toolx.NormalizeToolCodeAlias(toolCode) - if toolCode == "" { - continue - } - ret = append(ret, toolCode) - } - return ret -} - -func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinition) []string { - agentAllowed := parseAgentAllowedToolCodes(aiAgent) - skillAllowed := parseSkillAllowedToolCodes(skill) - switch { - case len(agentAllowed) == 0: - return skillAllowed - case len(skillAllowed) == 0: - return agentAllowed - default: - skillSet := make(map[string]struct{}, len(skillAllowed)) - for _, item := range skillAllowed { - item = strings.TrimSpace(item) - item = toolx.NormalizeToolCodeAlias(item) - if item == "" { - continue - } - skillSet[item] = struct{}{} - } - ret := make([]string, 0, len(agentAllowed)) - for _, item := range agentAllowed { - item = strings.TrimSpace(item) - item = toolx.NormalizeToolCodeAlias(item) - if item == "" { - continue - } - if _, ok := skillSet[item]; ok { - ret = append(ret, item) - } - } - return ret - } -} diff --git a/internal/ai/runtime/app/summary_builder.go b/internal/ai/runtime/app/summary_builder.go new file mode 100644 index 0000000..ae2658a --- /dev/null +++ b/internal/ai/runtime/app/summary_builder.go @@ -0,0 +1,46 @@ +package app + +import ( + "strings" + + "cs-agent/internal/ai/runtime/internal/executor" +) + +func toSummary(summary *executor.RunResult) *Summary { + if summary == nil { + return nil + } + ret := &Summary{ + RunID: summary.RunID, + Status: summary.Status, + ReplyText: summary.ReplyText, + PlannedSkillCode: strings.TrimSpace(summary.SelectedSkillCode), + PlannedSkillName: strings.TrimSpace(summary.SelectedSkillName), + PlanReason: strings.TrimSpace(summary.SkillRouteReason), + SkillRouteTrace: strings.TrimSpace(summary.SkillRouteTrace), + SkillAllowedToolCodes: append([]string(nil), summary.SkillAllowedToolCodes...), + ModelName: summary.ModelName, + PromptTokens: summary.PromptTokens, + CompletionTokens: summary.CompletionTokens, + HistoryMessageCount: summary.HistoryMessageCount, + RetrieverCount: summary.RetrieverCount, + ToolCallCount: summary.ToolCallCount, + ToolCodes: append([]string(nil), summary.ToolCodes...), + InvokedToolCodes: append([]string(nil), summary.InvokedToolCodes...), + CheckPointID: summary.CheckPointID, + Interrupted: summary.Interrupted, + TraceData: summary.TraceData, + ErrorMessage: summary.ErrorMessage, + } + if len(summary.Interrupts) > 0 { + ret.Interrupts = make([]InterruptContextSummary, 0, len(summary.Interrupts)) + for _, item := range summary.Interrupts { + ret.Interrupts = append(ret.Interrupts, InterruptContextSummary{ + Type: item.Type, + ID: item.ID, + InfoPreview: item.InfoPreview, + }) + } + } + return ret +} diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index 4210260..033b3ee 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -1,14 +1,8 @@ package runtime import ( - "context" - "encoding/json" - "log/slog" "strings" - "time" - "cs-agent/internal/models" - "cs-agent/internal/pkg/enums" svc "cs-agent/internal/services" ) @@ -36,90 +30,6 @@ type aiReplyService struct { runlog *replyRunLogService } -type aiReplyTraceData struct { - Status string `json:"status"` - RuntimeLatencyMs int64 `json:"runtimeLatencyMs,omitempty"` - RecheckMs int64 `json:"recheckMs,omitempty"` - CommitMs int64 `json:"commitMs,omitempty"` - FinalAction string `json:"finalAction,omitempty"` - ResumeSource string `json:"resumeSource,omitempty"` - ReplySent bool `json:"replySent,omitempty"` - ReplyMessageID int64 `json:"replyMessageId,omitempty"` - Runtime json.RawMessage `json:"runtime,omitempty"` -} - -const ( - defaultAIReplyAsyncTimeoutSeconds = 180 - maxAIReplyAsyncTimeoutSeconds = 600 -) - -func (s *aiReplyService) resolveReplyTimeout(aiAgent models.AIAgent) time.Duration { - if aiAgent.ReplyTimeoutSeconds <= 0 { - return time.Duration(defaultAIReplyAsyncTimeoutSeconds) * time.Second - } - if aiAgent.ReplyTimeoutSeconds > maxAIReplyAsyncTimeoutSeconds { - return time.Duration(maxAIReplyAsyncTimeoutSeconds) * time.Second - } - return time.Duration(aiAgent.ReplyTimeoutSeconds) * time.Second -} - -func (s *aiReplyService) TriggerReplyAsync(conversation models.Conversation, message models.Message) { - go func() { - aiAgent := svc.AIAgentService.Get(conversation.AIAgentID) - if aiAgent == nil || aiAgent.Status != enums.StatusOk { - return - } - startedAt := time.Now() - timeout := s.resolveReplyTimeout(*aiAgent) - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - if err := s.TriggerReply(ctx, conversation, message, *aiAgent); err != nil { - slog.Error("failed to trigger ai reply", - "message_id", message.ID, - "timeout_ms", timeout.Milliseconds(), - "elapsed_ms", time.Since(startedAt).Milliseconds(), - "error", err) - } - }() -} - -func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.Conversation, message models.Message, aiAgent models.AIAgent) (retErr error) { - startedAt := time.Now() - trace := &aiReplyTraceData{Status: "started"} - var summary *Summary - if err := ctx.Err(); err != nil { - return err - } - if s.eligibility != nil && !s.eligibility.CanReply(conversation, message, aiAgent) { - return nil - } - defer func() { - s.runlog.Write(startedAt, message, conversation, aiAgent, message.Content, retErr, trace, summary) - }() - if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil { - return s.interrupts.ResumePendingInterrupt(ctx, s, conversation, message, aiAgent, pendingInterrupt, trace, &summary) - } - var err error - summary, err = s.executor.Run(ctx, conversation, message, aiAgent, trace) - if err != nil { - return err - } - if summary != nil && summary.Interrupted { - return s.interrupts.HandleInterruptedSummary(s, conversation, message, aiAgent, summary, trace) - } - if summary != nil && strings.TrimSpace(summary.ReplyText) != "" { - replyMessage, err := s.commit.SendAIReply(conversation, message, aiAgent, summary.ReplyText, trace, "ai_reply") - if err != nil { - return err - } - if err := s.commit.IncrementAIReplyRounds(conversation.ID, conversation.AIReplyRounds+1, aiAgent.Name); err != nil { - return err - } - trace.ReplySent = replyMessage != nil - } - return nil -} - func firstInvokedToolCode(summary *Summary) string { if summary == nil { return "" diff --git a/internal/ai/runtime/reply_trigger_service.go b/internal/ai/runtime/reply_trigger_service.go new file mode 100644 index 0000000..652b0ed --- /dev/null +++ b/internal/ai/runtime/reply_trigger_service.go @@ -0,0 +1,79 @@ +package runtime + +import ( + "context" + "log/slog" + "strings" + "time" + + "cs-agent/internal/models" + "cs-agent/internal/pkg/enums" + svc "cs-agent/internal/services" +) + +func (s *aiReplyService) resolveReplyTimeout(aiAgent models.AIAgent) time.Duration { + if aiAgent.ReplyTimeoutSeconds <= 0 { + return time.Duration(defaultAIReplyAsyncTimeoutSeconds) * time.Second + } + if aiAgent.ReplyTimeoutSeconds > maxAIReplyAsyncTimeoutSeconds { + return time.Duration(maxAIReplyAsyncTimeoutSeconds) * time.Second + } + return time.Duration(aiAgent.ReplyTimeoutSeconds) * time.Second +} + +func (s *aiReplyService) TriggerReplyAsync(conversation models.Conversation, message models.Message) { + go func() { + aiAgent := svc.AIAgentService.Get(conversation.AIAgentID) + if aiAgent == nil || aiAgent.Status != enums.StatusOk { + return + } + startedAt := time.Now() + timeout := s.resolveReplyTimeout(*aiAgent) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := s.TriggerReply(ctx, conversation, message, *aiAgent); err != nil { + slog.Error("failed to trigger ai reply", + "message_id", message.ID, + "timeout_ms", timeout.Milliseconds(), + "elapsed_ms", time.Since(startedAt).Milliseconds(), + "error", err) + } + }() +} + +func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.Conversation, message models.Message, aiAgent models.AIAgent) (retErr error) { + startedAt := time.Now() + trace := &aiReplyTraceData{Status: "started"} + var summary *Summary + if err := ctx.Err(); err != nil { + return err + } + if s.eligibility != nil && !s.eligibility.CanReply(conversation, message, aiAgent) { + return nil + } + defer func() { + s.runlog.Write(startedAt, message, conversation, aiAgent, message.Content, retErr, trace, summary) + }() + if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil { + return s.interrupts.ResumePendingInterrupt(ctx, s, conversation, message, aiAgent, pendingInterrupt, trace, &summary) + } + var err error + summary, err = s.executor.Run(ctx, conversation, message, aiAgent, trace) + if err != nil { + return err + } + if summary != nil && summary.Interrupted { + return s.interrupts.HandleInterruptedSummary(s, conversation, message, aiAgent, summary, trace) + } + if summary != nil && strings.TrimSpace(summary.ReplyText) != "" { + replyMessage, err := s.commit.SendAIReply(conversation, message, aiAgent, summary.ReplyText, trace, "ai_reply") + if err != nil { + return err + } + if err := s.commit.IncrementAIReplyRounds(conversation.ID, conversation.AIReplyRounds+1, aiAgent.Name); err != nil { + return err + } + trace.ReplySent = replyMessage != nil + } + return nil +} diff --git a/internal/ai/runtime/reply_types.go b/internal/ai/runtime/reply_types.go new file mode 100644 index 0000000..425fc66 --- /dev/null +++ b/internal/ai/runtime/reply_types.go @@ -0,0 +1,20 @@ +package runtime + +import "encoding/json" + +type aiReplyTraceData struct { + Status string `json:"status"` + RuntimeLatencyMs int64 `json:"runtimeLatencyMs,omitempty"` + RecheckMs int64 `json:"recheckMs,omitempty"` + CommitMs int64 `json:"commitMs,omitempty"` + FinalAction string `json:"finalAction,omitempty"` + ResumeSource string `json:"resumeSource,omitempty"` + ReplySent bool `json:"replySent,omitempty"` + ReplyMessageID int64 `json:"replyMessageId,omitempty"` + Runtime json.RawMessage `json:"runtime,omitempty"` +} + +const ( + defaultAIReplyAsyncTimeoutSeconds = 180 + maxAIReplyAsyncTimeoutSeconds = 600 +) diff --git a/internal/ai/skills/plan_service.go b/internal/ai/skills/plan_service.go new file mode 100644 index 0000000..6c5a83d --- /dev/null +++ b/internal/ai/skills/plan_service.go @@ -0,0 +1,46 @@ +package skills + +import ( + "context" + "strings" + + "cs-agent/internal/pkg/errorsx" + "cs-agent/internal/repositories" + + "github.com/mlogclub/simple/sqls" +) + +func newPlanService() *planService { + return &planService{} +} + +type planService struct{} + +// BuildExecutionPlan 构建当前请求的 Skill 执行计划。 +func (s *planService) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) { + if ctx.AIAgentID <= 0 { + return nil, errorsx.InvalidParam("AIAgentID不能为空") + } + + aiAgent := repositories.AIAgentRepository.Get(sqls.DB(), ctx.AIAgentID) + if aiAgent == nil { + return nil, errorsx.InvalidParam("AI Agent不存在") + } + aiConfig := repositories.AIConfigRepository.Get(sqls.DB(), aiAgent.AIConfigID) + if aiConfig == nil { + return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") + } + + skill, matchReason, routeTrace, err := MatchSkill(execCtx, ctx, aiAgent, aiConfig) + if err != nil { + return nil, err + } + + return &ExecutionPlan{ + AIAgent: aiAgent, + AIConfig: aiConfig, + Skill: skill, + MatchReason: strings.TrimSpace(matchReason), + RouteTrace: routeTrace, + }, nil +} diff --git a/internal/ai/skills/service.go b/internal/ai/skills/service.go index b3bdff8..20b705f 100644 --- a/internal/ai/skills/service.go +++ b/internal/ai/skills/service.go @@ -5,51 +5,25 @@ import ( "strings" "cs-agent/internal/models" - "cs-agent/internal/pkg/errorsx" - "cs-agent/internal/repositories" - - "github.com/mlogclub/simple/sqls" ) var RuntimeService = newService() func newService() *Service { return &Service{ + plan: newPlanService(), runlog: newRunLogService(), } } type Service struct { + plan *planService runlog *RunLogService } // BuildExecutionPlan 构建当前请求的 Skill 执行计划。 func (s *Service) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) { - if ctx.AIAgentID <= 0 { - return nil, errorsx.InvalidParam("AIAgentID不能为空") - } - - aiAgent := repositories.AIAgentRepository.Get(sqls.DB(), ctx.AIAgentID) - if aiAgent == nil { - return nil, errorsx.InvalidParam("AI Agent不存在") - } - aiConfig := repositories.AIConfigRepository.Get(sqls.DB(), aiAgent.AIConfigID) - if aiConfig == nil { - return nil, errorsx.InvalidParam("AI Agent关联的AI配置不存在") - } - - skill, matchReason, routeTrace, err := MatchSkill(execCtx, ctx, aiAgent, aiConfig) - if err != nil { - return nil, err - } - - return &ExecutionPlan{ - AIAgent: aiAgent, - AIConfig: aiConfig, - Skill: skill, - MatchReason: strings.TrimSpace(matchReason), - RouteTrace: routeTrace, - }, nil + return s.plan.BuildExecutionPlan(execCtx, ctx) } // WriteRunLog 写入 Skill 路由日志。