feat: introduce prepare and plan services for improved skill execution handling
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 路由日志。
|
||||
|
||||
Reference in New Issue
Block a user