diff --git a/internal/ai/runtime/app/service.go b/internal/ai/runtime/app/service.go new file mode 100644 index 0000000..9ebf644 --- /dev/null +++ b/internal/ai/runtime/app/service.go @@ -0,0 +1,280 @@ +package app + +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" + "cs-agent/internal/pkg/toolx" +) + +type Service struct { + runtime *engine.Service + registry *registry.Registry +} + +func NewService() *Service { + return &Service{ + runtime: engine.NewService(), + registry: registry.NewRegistry( + tools.NewTriageServiceRequestTool(), + tools.NewAnalyzeConversationTool(), + tools.NewPrepareTicketDraftTool(), + tools.NewCreateTicketGraphTool(), + tools.NewHandoffGraphTool(), + ), + } +} + +func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { + 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.ExecuteRun(ctx, engine.RunInput{ + Conversation: req.Conversation, + UserMessage: req.UserMessage, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + SelectedSkill: req.SelectedSkill, + SkillRouteReason: req.SkillRouteReason, + SkillRouteTrace: req.SkillRouteTrace, + CheckPointID: req.CheckPointID, + ToolSet: req.ToolSet, + }) + if err != nil { + ret := toSummary(summary) + if ret != nil && skillErr != nil && strings.TrimSpace(ret.PlanReason) == "" { + ret.PlanReason = "skill_failed_fallback_runtime" + } + return ret, err + } + ret := toSummary(summary) + if ret != nil && skillErr != nil && strings.TrimSpace(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 { + return nil, err + } + summary, err := s.runtime.ExecuteResume(ctx, engine.ResumeInput{ + Conversation: req.Conversation, + AIAgent: req.AIAgent, + AIConfig: req.AIConfig, + CheckPointID: req.CheckPointID, + ResumeData: req.ResumeData, + ToolSet: req.ToolSet, + }) + if err != nil { + return toSummary(summary), err + } + 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 *engine.Summary) *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/types.go b/internal/ai/runtime/app/types.go new file mode 100644 index 0000000..86b0d05 --- /dev/null +++ b/internal/ai/runtime/app/types.go @@ -0,0 +1,58 @@ +package app + +import ( + "cs-agent/internal/ai/runtime/registry" + "cs-agent/internal/models" +) + +type Request struct { + Conversation *models.Conversation + UserMessage *models.Message + AIAgent *models.AIAgent + AIConfig *models.AIConfig + ManualSkillCode string + SelectedSkill *models.SkillDefinition + SkillRouteReason string + SkillRouteTrace string + CheckPointID string + ToolSet *registry.ToolSet +} + +type ResumeRequest struct { + Conversation *models.Conversation + AIAgent *models.AIAgent + AIConfig *models.AIConfig + CheckPointID string + ResumeData map[string]any + ToolSet *registry.ToolSet +} + +type InterruptContextSummary struct { + Type string `json:"type,omitempty"` + ID string `json:"id"` + InfoPreview string `json:"infoPreview,omitempty"` +} + +type Summary struct { + RunID string + Status string + ReplyText string + PlannedSkillCode string + PlannedSkillName string + PlanReason string + SkillRouteTrace string + SkillAllowedToolCodes []string + ModelName string + PromptTokens int + CompletionTokens int + HistoryMessageCount int + RetrieverCount int + ToolCallCount int + ToolCodes []string + InvokedToolCodes []string + CheckPointID string + Interrupted bool + Interrupts []InterruptContextSummary + TraceData string + ErrorMessage string +} diff --git a/internal/ai/runtime/internal/engine/service.go b/internal/ai/runtime/internal/engine/service.go index 2ca5c0d..e1c1c20 100644 --- a/internal/ai/runtime/internal/engine/service.go +++ b/internal/ai/runtime/internal/engine/service.go @@ -34,6 +34,10 @@ func NewService() *Service { } func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { + return s.ExecuteRun(ctx, req) +} + +func (s *Service) ExecuteRun(ctx context.Context, req RunInput) (*RunResult, error) { summary := &Summary{ RunID: uuid.NewString(), Status: "started", @@ -182,6 +186,10 @@ func (s *Service) Run(ctx context.Context, req Request) (*Summary, error) { } func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) { + return s.ExecuteResume(ctx, req) +} + +func (s *Service) ExecuteResume(ctx context.Context, req ResumeInput) (*RunResult, error) { summary := &Summary{ RunID: uuid.NewString(), Status: "started", diff --git a/internal/ai/runtime/internal/engine/types.go b/internal/ai/runtime/internal/engine/types.go index 529a769..2945bec 100644 --- a/internal/ai/runtime/internal/engine/types.go +++ b/internal/ai/runtime/internal/engine/types.go @@ -5,7 +5,7 @@ import ( "cs-agent/internal/models" ) -type Request struct { +type RunInput struct { Conversation *models.Conversation UserMessage *models.Message AIAgent *models.AIAgent @@ -17,7 +17,7 @@ type Request struct { ToolSet *registry.ToolSet } -type ResumeRequest struct { +type ResumeInput struct { Conversation *models.Conversation AIAgent *models.AIAgent AIConfig *models.AIConfig @@ -32,7 +32,7 @@ type InterruptContextSummary struct { InfoPreview string `json:"infoPreview,omitempty"` } -type Summary struct { +type RunResult struct { RunID string Status string ReplyText string @@ -55,3 +55,7 @@ type Summary struct { TraceData string ErrorMessage string } + +type Request = RunInput +type ResumeRequest = ResumeInput +type Summary = RunResult diff --git a/internal/ai/runtime/reply_eligibility.go b/internal/ai/runtime/reply_eligibility.go new file mode 100644 index 0000000..bff628e --- /dev/null +++ b/internal/ai/runtime/reply_eligibility.go @@ -0,0 +1,30 @@ +package runtime + +import ( + "cs-agent/internal/models" + "cs-agent/internal/pkg/enums" + + "github.com/mlogclub/simple/common/strs" +) + +type replyEligibility struct{} + +func newReplyEligibility() *replyEligibility { + return &replyEligibility{} +} + +func (e *replyEligibility) CanReply(conversation models.Conversation, message models.Message, aiAgent models.AIAgent) bool { + if message.SenderType != enums.IMSenderTypeCustomer { + return false + } + if conversation.HandoffAt != nil || conversation.CurrentAssigneeID > 0 { + return false + } + if aiAgent.ServiceMode == enums.IMConversationServiceModeHumanOnly { + return false + } + if strs.IsBlank(message.Content) { + return false + } + return true +} diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index ca76205..ed493b5 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -16,7 +16,6 @@ import ( "cs-agent/internal/repositories" svc "cs-agent/internal/services" - "github.com/mlogclub/simple/common/strs" "github.com/mlogclub/simple/sqls" ) @@ -27,10 +26,16 @@ func init() { } func newAIReplyService() *aiReplyService { - return &aiReplyService{} + return &aiReplyService{ + eligibility: newReplyEligibility(), + executor: newRuntimeReplyExecutor(), + } } -type aiReplyService struct{} +type aiReplyService struct { + eligibility *replyEligibility + executor *runtimeReplyExecutor +} type aiReplyTraceData struct { Status string `json:"status"` @@ -86,16 +91,7 @@ func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.C if err := ctx.Err(); err != nil { return err } - if message.SenderType != enums.IMSenderTypeCustomer { - return nil - } - if conversation.HandoffAt != nil || conversation.CurrentAssigneeID > 0 { - return nil - } - if aiAgent.ServiceMode == enums.IMConversationServiceModeHumanOnly { - return nil - } - if strs.IsBlank(message.Content) { + if s.eligibility != nil && !s.eligibility.CanReply(conversation, message, aiAgent) { return nil } defer func() { @@ -104,33 +100,11 @@ func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.C if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil { return s.resumePendingInterrupt(ctx, conversation, message, aiAgent, pendingInterrupt, trace, &summary) } - aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) - if aiConfig == nil { - return fmt.Errorf("ai config is nil") - } - - runtimeStartedAt := time.Now() var err error - summary, err = Service.Run(ctx, Request{ - Conversation: &conversation, - UserMessage: &message, - AIAgent: &aiAgent, - AIConfig: aiConfig, - }) - trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds() + summary, err = s.executor.Run(ctx, conversation, message, aiAgent, trace) if err != nil { - trace.Status = "runtime_error" - trace.FinalAction = "error" - if summary != nil { - trace.Runtime = json.RawMessage(summary.TraceData) - } return err } - trace.Status = "runtime_prepared" - trace.FinalAction = toRunLogFinalAction(summary) - if summary != nil && strings.TrimSpace(summary.TraceData) != "" { - trace.Runtime = json.RawMessage(summary.TraceData) - } if summary != nil && summary.Interrupted { return s.handleInterruptedSummary(conversation, message, aiAgent, summary, trace) } @@ -152,29 +126,11 @@ func (s *aiReplyService) resumePendingInterrupt(ctx context.Context, conversatio if pendingInterrupt == nil { return nil } - aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) - if aiConfig == nil { - return fmt.Errorf("ai config is nil") - } - runtimeStartedAt := time.Now() - trace.ResumeSource = "pending_interrupt" - summary, err := Service.Resume(ctx, ResumeRequest{ - Conversation: &conversation, - AIAgent: &aiAgent, - AIConfig: aiConfig, - CheckPointID: strings.TrimSpace(pendingInterrupt.CheckPointID), - ResumeData: map[string]any{ - strings.TrimSpace(pendingInterrupt.InterruptID): strings.TrimSpace(message.Content), - }, - }) - trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds() + summary, err := s.executor.ResumePendingInterrupt(ctx, conversation, message, aiAgent, pendingInterrupt, trace) *summaryRef = summary if err != nil { if isCheckpointMissingError(err) { - summary = &Summary{ - Status: "expired", - ReplyText: graphs.ConfirmationExpiredReply, - } + summary = expiredInterruptSummary() *summaryRef = summary trace.Status = "interrupt_expired" trace.FinalAction = "expired" @@ -194,18 +150,8 @@ func (s *aiReplyService) resumePendingInterrupt(ctx context.Context, conversatio } return nil } - trace.Status = "runtime_error" - trace.FinalAction = "error" - if summary != nil { - trace.Runtime = json.RawMessage(summary.TraceData) - } return err } - trace.Status = "runtime_prepared" - trace.FinalAction = toRunLogFinalAction(summary) - if summary != nil && strings.TrimSpace(summary.TraceData) != "" { - trace.Runtime = json.RawMessage(summary.TraceData) - } if summary != nil && summary.Interrupted { return s.handleInterruptedResume(conversation, message, aiAgent, pendingInterrupt, summary, trace) } diff --git a/internal/ai/runtime/runtime_reply_executor.go b/internal/ai/runtime/runtime_reply_executor.go new file mode 100644 index 0000000..0eccc3a --- /dev/null +++ b/internal/ai/runtime/runtime_reply_executor.go @@ -0,0 +1,92 @@ +package runtime + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "cs-agent/internal/ai/runtime/graphs" + "cs-agent/internal/models" + svc "cs-agent/internal/services" +) + +type runtimeReplyExecutor struct{} + +func newRuntimeReplyExecutor() *runtimeReplyExecutor { + return &runtimeReplyExecutor{} +} + +func (e *runtimeReplyExecutor) Run(ctx context.Context, conversation models.Conversation, message models.Message, aiAgent models.AIAgent, trace *aiReplyTraceData) (*Summary, error) { + aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) + if aiConfig == nil { + return nil, fmt.Errorf("ai config is nil") + } + runtimeStartedAt := time.Now() + summary, err := Service.Run(ctx, Request{ + Conversation: &conversation, + UserMessage: &message, + AIAgent: &aiAgent, + AIConfig: aiConfig, + }) + if trace != nil { + trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds() + e.fillTraceFromSummary(trace, summary, err) + } + return summary, err +} + +func (e *runtimeReplyExecutor) ResumePendingInterrupt(ctx context.Context, conversation models.Conversation, message models.Message, aiAgent models.AIAgent, pendingInterrupt *models.ConversationInterrupt, trace *aiReplyTraceData) (*Summary, error) { + if pendingInterrupt == nil { + return nil, nil + } + aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) + if aiConfig == nil { + return nil, fmt.Errorf("ai config is nil") + } + runtimeStartedAt := time.Now() + if trace != nil { + trace.ResumeSource = "pending_interrupt" + } + summary, err := Service.Resume(ctx, ResumeRequest{ + Conversation: &conversation, + AIAgent: &aiAgent, + AIConfig: aiConfig, + CheckPointID: strings.TrimSpace(pendingInterrupt.CheckPointID), + ResumeData: map[string]any{ + strings.TrimSpace(pendingInterrupt.InterruptID): strings.TrimSpace(message.Content), + }, + }) + if trace != nil { + trace.RuntimeLatencyMs = time.Since(runtimeStartedAt).Milliseconds() + e.fillTraceFromSummary(trace, summary, err) + } + return summary, err +} + +func (e *runtimeReplyExecutor) fillTraceFromSummary(trace *aiReplyTraceData, summary *Summary, runErr error) { + if trace == nil { + return + } + if runErr != nil { + trace.Status = "runtime_error" + trace.FinalAction = "error" + if summary != nil { + trace.Runtime = json.RawMessage(summary.TraceData) + } + return + } + trace.Status = "runtime_prepared" + trace.FinalAction = toRunLogFinalAction(summary) + if summary != nil && strings.TrimSpace(summary.TraceData) != "" { + trace.Runtime = json.RawMessage(summary.TraceData) + } +} + +func expiredInterruptSummary() *Summary { + return &Summary{ + Status: "expired", + ReplyText: graphs.ConfirmationExpiredReply, + } +} diff --git a/internal/ai/runtime/service.go b/internal/ai/runtime/service.go index 60d2bb9..2d9da56 100644 --- a/internal/ai/runtime/service.go +++ b/internal/ai/runtime/service.go @@ -2,281 +2,32 @@ 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" - "cs-agent/internal/pkg/toolx" + runtimeapp "cs-agent/internal/ai/runtime/app" ) var Service = newService() func newService() *service { return &service{ - runtime: engine.NewService(), - registry: registry.NewRegistry( - tools.NewTriageServiceRequestTool(), - tools.NewAnalyzeConversationTool(), - tools.NewPrepareTicketDraftTool(), - tools.NewCreateTicketGraphTool(), - tools.NewHandoffGraphTool(), - ), + app: runtimeapp.NewService(), } } type service struct { - runtime *engine.Service - registry *registry.Registry + app *runtimeapp.Service } func (s *service) Run(ctx context.Context, req Request) (*Summary, error) { - 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 s == nil || s.app == nil { + return nil, nil } - 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, - SelectedSkill: req.SelectedSkill, - SkillRouteReason: req.SkillRouteReason, - SkillRouteTrace: req.SkillRouteTrace, - CheckPointID: req.CheckPointID, - ToolSet: req.ToolSet, - }) - if err != nil { - ret := toSummary(summary) - if ret != nil && skillErr != nil && strings.TrimSpace(ret.PlanReason) == "" { - ret.PlanReason = "skill_failed_fallback_runtime" - } - return ret, err - } - ret := toSummary(summary) - if ret != nil && skillErr != nil && strings.TrimSpace(ret.PlanReason) == "" { - ret.PlanReason = "skill_failed_fallback_runtime" - } - return ret, nil + return s.app.Run(ctx, req) } func (s *service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) { - if err := s.prepareToolsForResume(&req); err != nil { - return nil, err - } - summary, err := s.runtime.Resume(ctx, engine.ResumeRequest{ - Conversation: req.Conversation, - AIAgent: req.AIAgent, - AIConfig: req.AIConfig, - CheckPointID: req.CheckPointID, - ResumeData: req.ResumeData, - ToolSet: req.ToolSet, - }) - if err != nil { - return toSummary(summary), err - } - 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 *engine.Summary) *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 + if s == nil || s.app == nil { + return nil, nil } + return s.app.Resume(ctx, req) } diff --git a/internal/ai/runtime/types.go b/internal/ai/runtime/types.go index faff57a..cc9ea9f 100644 --- a/internal/ai/runtime/types.go +++ b/internal/ai/runtime/types.go @@ -1,58 +1,9 @@ package runtime -import ( - "cs-agent/internal/ai/runtime/registry" - "cs-agent/internal/models" -) +import runtimeapp "cs-agent/internal/ai/runtime/app" -type Request struct { - Conversation *models.Conversation - UserMessage *models.Message - AIAgent *models.AIAgent - AIConfig *models.AIConfig - ManualSkillCode string - SelectedSkill *models.SkillDefinition - SkillRouteReason string - SkillRouteTrace string - CheckPointID string - ToolSet *registry.ToolSet -} - -type ResumeRequest struct { - Conversation *models.Conversation - AIAgent *models.AIAgent - AIConfig *models.AIConfig - CheckPointID string - ResumeData map[string]any - ToolSet *registry.ToolSet -} - -type InterruptContextSummary struct { - Type string `json:"type,omitempty"` - ID string `json:"id"` - InfoPreview string `json:"infoPreview,omitempty"` -} - -type Summary struct { - RunID string - Status string - ReplyText string - PlannedSkillCode string - PlannedSkillName string - PlanReason string - SkillRouteTrace string - SkillAllowedToolCodes []string - ModelName string - PromptTokens int - CompletionTokens int - HistoryMessageCount int - RetrieverCount int - ToolCallCount int - ToolCodes []string - InvokedToolCodes []string - CheckPointID string - Interrupted bool - Interrupts []InterruptContextSummary - TraceData string - ErrorMessage string -} +// TODO 这里为什么再设置一下类型别名,不能直接在外部使用 runtimeapp.Request 之类的类型呢? +type Request = runtimeapp.Request +type ResumeRequest = runtimeapp.ResumeRequest +type InterruptContextSummary = runtimeapp.InterruptContextSummary +type Summary = runtimeapp.Summary