feat: introduce prepare and plan services for improved skill execution handling

This commit is contained in:
mlogclub
2026-04-13 19:15:12 +08:00
parent e5448e00b3
commit 102271ac84
8 changed files with 384 additions and 323 deletions
+175
View File
@@ -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
}
}
+15 -204
View File
@@ -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
}
}
@@ -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
}
-90
View File
@@ -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 ""
@@ -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
}
+20
View File
@@ -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
)
+46
View File
@@ -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
}
+3 -29
View File
@@ -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 路由日志。