feat: implement runtime reply service and refactor related components

This commit is contained in:
mlogclub
2026-04-12 23:37:52 +08:00
parent 6499126f40
commit 93cda6ae82
9 changed files with 502 additions and 382 deletions
+280
View File
@@ -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
}
}
+58
View File
@@ -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
}
@@ -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",
+7 -3
View File
@@ -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
+30
View File
@@ -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
}
+12 -66
View File
@@ -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)
}
@@ -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,
}
}
+9 -258
View File
@@ -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)
}
+6 -55
View File
@@ -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