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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"cs-agent/internal/ai/runtime/internal/executor"
|
"cs-agent/internal/ai/runtime/internal/executor"
|
||||||
"cs-agent/internal/ai/runtime/registry"
|
"cs-agent/internal/ai/runtime/registry"
|
||||||
"cs-agent/internal/ai/runtime/tools"
|
"cs-agent/internal/ai/runtime/tools"
|
||||||
"cs-agent/internal/ai/skills"
|
|
||||||
"cs-agent/internal/models"
|
|
||||||
"cs-agent/internal/pkg/toolx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
runtime *executor.Service
|
runtime *executor.Service
|
||||||
registry *registry.Registry
|
registry *registry.Registry
|
||||||
|
prepare *prepareService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService() *Service {
|
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) {
|
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.SelectedSkill = selectedSkill
|
||||||
req.SkillRouteReason = skillReason
|
req.SkillRouteReason = skillReason
|
||||||
req.SkillRouteTrace = skillTrace
|
req.SkillRouteTrace = skillTrace
|
||||||
if req.SelectedSkill != nil {
|
if req.SelectedSkill != nil {
|
||||||
req.SelectedSkill = cloneSkillDefinition(req.SelectedSkill)
|
req.SelectedSkill = cloneSkillDefinition(req.SelectedSkill)
|
||||||
}
|
}
|
||||||
if err := s.prepareToolsForRun(&req); err != nil {
|
if err := s.prepare.prepareToolsForRun(&req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
summary, err := s.runtime.ExecuteRun(ctx, executor.RunInput{
|
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 {
|
if err != nil {
|
||||||
ret := toSummary(summary)
|
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"
|
ret.PlanReason = "skill_failed_fallback_runtime"
|
||||||
}
|
}
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
ret := toSummary(summary)
|
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"
|
ret.PlanReason = "skill_failed_fallback_runtime"
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Resume(ctx context.Context, req ResumeRequest) (*Summary, error) {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
summary, err := s.runtime.ExecuteResume(ctx, executor.ResumeInput{
|
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
|
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
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
|
||||||
"cs-agent/internal/pkg/enums"
|
|
||||||
svc "cs-agent/internal/services"
|
svc "cs-agent/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,90 +30,6 @@ type aiReplyService struct {
|
|||||||
runlog *replyRunLogService
|
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 {
|
func firstInvokedToolCode(summary *Summary) string {
|
||||||
if summary == nil {
|
if summary == nil {
|
||||||
return ""
|
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"
|
"strings"
|
||||||
|
|
||||||
"cs-agent/internal/models"
|
"cs-agent/internal/models"
|
||||||
"cs-agent/internal/pkg/errorsx"
|
|
||||||
"cs-agent/internal/repositories"
|
|
||||||
|
|
||||||
"github.com/mlogclub/simple/sqls"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RuntimeService = newService()
|
var RuntimeService = newService()
|
||||||
|
|
||||||
func newService() *Service {
|
func newService() *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
|
plan: newPlanService(),
|
||||||
runlog: newRunLogService(),
|
runlog: newRunLogService(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
plan *planService
|
||||||
runlog *RunLogService
|
runlog *RunLogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildExecutionPlan 构建当前请求的 Skill 执行计划。
|
// BuildExecutionPlan 构建当前请求的 Skill 执行计划。
|
||||||
func (s *Service) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) {
|
func (s *Service) BuildExecutionPlan(execCtx context.Context, ctx RuntimeContext) (*ExecutionPlan, error) {
|
||||||
if ctx.AIAgentID <= 0 {
|
return s.plan.BuildExecutionPlan(execCtx, ctx)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteRunLog 写入 Skill 路由日志。
|
// WriteRunLog 写入 Skill 路由日志。
|
||||||
|
|||||||
Reference in New Issue
Block a user