Files
ai-agent/internal/ai/skills/matcher.go
T
mlogclub 3c0abaaedc Refactor AI skill routing and introduce reply handling services
- Moved the skill routing logic from matcher.go to a new router.go file for better organization.
- Implemented replyCommitService to handle sending AI replies and managing reply rounds.
- Added replyInterruptService to manage conversation interrupts and resume handling.
- Created replyRunLogService to log AI reply actions and their outcomes.
- Introduced helper functions for building conversation interrupts and resolving prompts.
- Added unit tests for the new services and functions to ensure correctness.
- Removed unused code and optimized imports in matcher.go.
2026-04-13 17:17:13 +08:00

105 lines
2.9 KiB
Go

package skills
import (
"context"
"strings"
"cs-agent/internal/models"
"cs-agent/internal/pkg/enums"
"cs-agent/internal/pkg/errorsx"
"cs-agent/internal/pkg/utils"
"cs-agent/internal/repositories"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
)
type intentTriggerConfig struct {
Intents []string `json:"intents"`
}
// MatchSkill 对单个 SkillDefinition 执行命中判断。
func MatchSkill(execCtx context.Context, ctx RuntimeContext, aiAgent *models.AIAgent, aiConfig *models.AIConfig) (*models.SkillDefinition, string, *RouteTrace, error) {
if strs.IsNotBlank(ctx.ManualSkillCode) {
skill := repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), ctx.ManualSkillCode)
if skill == nil || skill.Status != enums.StatusOk {
return nil, "", nil, errorsx.InvalidParam("Skill 不存在或未启用")
}
return skill, "manual_skill_code", &RouteTrace{
Status: "manual_selected",
SelectedSkillCode: skill.Code,
}, nil
}
candidates := loadCandidateSkills(aiAgent)
trace := &RouteTrace{
Status: "started",
CandidateSkillCodes: make([]string, 0, len(candidates)),
}
for _, item := range candidates {
trace.CandidateSkillCodes = append(trace.CandidateSkillCodes, item.Code)
}
if len(candidates) == 0 {
trace.Status = "no_candidate"
return nil, "no_enabled_skill_bound", trace, nil
}
intentCode := strings.TrimSpace(ctx.IntentCode)
if intentCode != "" {
for _, item := range candidates {
if strings.EqualFold(strings.TrimSpace(item.Code), intentCode) {
trace.Status = "intent_selected"
trace.SelectedSkillCode = item.Code
return &item, "intent_code", trace, nil
}
}
}
if len(candidates) == 1 {
trace.Status = "single_candidate"
trace.SelectedSkillCode = candidates[0].Code
return &candidates[0], "single_candidate", trace, nil
}
selected, routeTrace, err := routeSkillWithLLM(execCtx, aiConfig, ctx.UserMessage, candidates)
if routeTrace != nil {
trace.Status = routeTrace.Status
trace.SelectedSkillCode = routeTrace.SelectedSkillCode
trace.RawDecision = routeTrace.RawDecision
trace.LatencyMs = routeTrace.LatencyMs
trace.Error = routeTrace.Error
}
if err != nil {
if trace.Error == "" {
trace.Error = err.Error()
}
return nil, "route_error", trace, err
}
if selected == nil {
if trace.Status == "started" {
trace.Status = "not_matched"
}
return nil, "route_none", trace, nil
}
return selected, "llm_route", trace, nil
}
func loadCandidateSkills(aiAgent *models.AIAgent) []models.SkillDefinition {
if aiAgent == nil {
return nil
}
skillIDs := utils.SplitInt64s(aiAgent.SkillIDs)
if len(skillIDs) == 0 {
return nil
}
ret := make([]models.SkillDefinition, 0, len(skillIDs))
for _, id := range skillIDs {
skill := repositories.SkillDefinitionRepository.Get(sqls.DB(), id)
if skill == nil || skill.Status != enums.StatusOk {
continue
}
ret = append(ret, *skill)
}
return ret
}