refactor: replace skill code with skill ID across the application
- Updated skill handling to use skill IDs instead of skill codes in various components, services, and models. - Modified tests to reflect changes in skill identification. - Removed references to skill codes in favor of skill IDs for consistency and clarity. - Updated localization files to remove skill code references and adjust error messages accordingly.
This commit is contained in:
@@ -16,8 +16,11 @@ var newCandidateLoader = func() *candidateLoader {
|
||||
type candidateLoader struct {
|
||||
}
|
||||
|
||||
func (l *candidateLoader) findManualSkillDefinition(skillCode string) *models.SkillDefinition {
|
||||
return repositories.SkillDefinitionRepository.GetByCode(sqls.DB(), skillCode)
|
||||
func (l *candidateLoader) findManualSkillDefinition(skillDefinitionID int64) *models.SkillDefinition {
|
||||
if skillDefinitionID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return repositories.SkillDefinitionRepository.Get(sqls.DB(), skillDefinitionID)
|
||||
}
|
||||
|
||||
func (l *candidateLoader) loadCandidateSkills(aiAgent models.AIAgent) []models.SkillDefinition {
|
||||
|
||||
@@ -11,12 +11,11 @@ import (
|
||||
func TestBuildRunLogMatchedPlan(t *testing.T) {
|
||||
log := BuildRunLog(
|
||||
RuntimeContext{
|
||||
AIAgent: models.AIAgent{ID: 22},
|
||||
AIConfig: models.AIConfig{ID: 33},
|
||||
ConversationID: 11,
|
||||
ManualSkillCode: "manual_refund",
|
||||
IntentCode: "refund",
|
||||
UserMessage: "我要退款",
|
||||
AIAgent: models.AIAgent{ID: 22},
|
||||
AIConfig: models.AIConfig{ID: 33},
|
||||
ConversationID: 11,
|
||||
ManualSkillDefinitionID: 44,
|
||||
UserMessage: "我要退款",
|
||||
},
|
||||
&ExecutionPlan{
|
||||
AIAgent: models.AIAgent{ID: 22},
|
||||
@@ -25,10 +24,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
|
||||
ModelName: "gpt-test",
|
||||
Provider: enums.AIProviderOpenAI,
|
||||
},
|
||||
Skill: &models.SkillDefinition{
|
||||
ID: 44,
|
||||
Code: "refund_skill",
|
||||
},
|
||||
Skill: &models.SkillDefinition{ID: 44},
|
||||
MatchReason: "llm_route",
|
||||
},
|
||||
&ExecutionTrace{Status: "ok"},
|
||||
@@ -41,7 +37,7 @@ func TestBuildRunLogMatchedPlan(t *testing.T) {
|
||||
if log.ConversationID != 11 || log.AIAgentID != 22 || log.AIConfigID != 33 {
|
||||
t.Fatalf("unexpected ids in run log: %#v", log)
|
||||
}
|
||||
if !log.Matched || !log.FinalSelected || log.SkillCode != "refund_skill" {
|
||||
if !log.Matched || !log.FinalSelected || log.SkillDefinitionID != 44 {
|
||||
t.Fatalf("expected matched skill log, got %#v", log)
|
||||
}
|
||||
if log.MatchReason != "llm_route" {
|
||||
|
||||
@@ -2,13 +2,10 @@ package skills
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
)
|
||||
|
||||
type intentTriggerConfig struct {
|
||||
@@ -18,45 +15,34 @@ type intentTriggerConfig struct {
|
||||
// MatchSkill 对单个 SkillDefinition 执行命中判断。
|
||||
func MatchSkill(execCtx context.Context, ctx RuntimeContext) (*models.SkillDefinition, string, *RouteTrace, error) {
|
||||
loader := newCandidateLoader()
|
||||
if strs.IsNotBlank(ctx.ManualSkillCode) {
|
||||
skill := loader.findManualSkillDefinition(ctx.ManualSkillCode)
|
||||
if ctx.ManualSkillDefinitionID > 0 {
|
||||
skill := loader.findManualSkillDefinition(ctx.ManualSkillDefinitionID)
|
||||
if skill == nil || skill.Status != enums.StatusOk {
|
||||
return nil, "", nil, errorsx.InvalidParamI18n("error.e0054")
|
||||
}
|
||||
return skill, "manual_skill_code", &RouteTrace{
|
||||
Status: "manual_selected",
|
||||
SelectedSkillCode: skill.Code,
|
||||
return skill, "manual_skill_id", &RouteTrace{
|
||||
Status: "manual_selected",
|
||||
SelectedSkillID: skill.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
candidates := loader.loadCandidateSkills(ctx.AIAgent)
|
||||
trace := &RouteTrace{
|
||||
Status: "started",
|
||||
CandidateSkillCodes: make([]string, 0, len(candidates)),
|
||||
Status: "started",
|
||||
CandidateSkillIDs: make([]int64, 0, len(candidates)),
|
||||
}
|
||||
for _, item := range candidates {
|
||||
trace.CandidateSkillCodes = append(trace.CandidateSkillCodes, item.Code)
|
||||
trace.CandidateSkillIDs = append(trace.CandidateSkillIDs, item.ID)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected, routeTrace, err := routeSkillWithLLM(execCtx, ctx, candidates)
|
||||
if routeTrace != nil {
|
||||
trace.Status = routeTrace.Status
|
||||
trace.SelectedSkillCode = routeTrace.SelectedSkillCode
|
||||
trace.SelectedSkillID = routeTrace.SelectedSkillID
|
||||
trace.RawDecision = routeTrace.RawDecision
|
||||
trace.LatencyMs = routeTrace.LatencyMs
|
||||
trace.Error = routeTrace.Error
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,10 +14,10 @@ import (
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
)
|
||||
|
||||
const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillCode,或者返回 NONE。
|
||||
const routeSkillSystemPrompt = `你是客服技能路由器。你只能在候选 Skill 中选择一个最合适的 skillId,或者返回 NONE。
|
||||
只有当用户问题与 Skill 的职责边界明确匹配时才选择;
|
||||
如果不明确、信息不足、多个 Skill 都不够确定,就返回 NONE。
|
||||
输出只能是 skillCode 或 NONE,不能输出其他内容。`
|
||||
输出只能是 skillId 或 NONE,不能输出其他内容。`
|
||||
|
||||
func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidates []models.SkillDefinition) (*models.SkillDefinition, *RouteTrace, error) {
|
||||
trace := &RouteTrace{Status: "started"}
|
||||
@@ -43,10 +44,16 @@ func routeSkillWithLLM(ctx context.Context, runtimeCtx RuntimeContext, candidate
|
||||
trace.Status = "not_matched"
|
||||
return nil, trace, nil
|
||||
}
|
||||
selectedID, parseErr := strconv.ParseInt(decision, 10, 64)
|
||||
if parseErr != nil || selectedID <= 0 {
|
||||
trace.Status = "invalid_decision"
|
||||
trace.Error = fmt.Sprintf("invalid route decision: %s", decision)
|
||||
return nil, trace, nil
|
||||
}
|
||||
for _, item := range candidates {
|
||||
if strings.EqualFold(item.Code, decision) {
|
||||
if item.ID == selectedID {
|
||||
trace.Status = "llm_selected"
|
||||
trace.SelectedSkillCode = item.Code
|
||||
trace.SelectedSkillID = item.ID
|
||||
return &item, trace, nil
|
||||
}
|
||||
}
|
||||
@@ -62,14 +69,14 @@ func buildSkillRoutePrompt(userMessage string, candidates []models.SkillDefiniti
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "候选 Skills:")
|
||||
for _, item := range candidates {
|
||||
line := fmt.Sprintf("- skillCode=%s; name=%s; description=%s", strings.TrimSpace(item.Code), strings.TrimSpace(item.Name), strings.TrimSpace(item.Description))
|
||||
line := fmt.Sprintf("- skillId=%d; name=%s; description=%s", item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description))
|
||||
if examples := parseSkillExamples(item.Examples); len(examples) > 0 {
|
||||
line += "; examples=" + strings.Join(examples, " | ")
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "请只输出一个 skillCode 或 NONE。")
|
||||
lines = append(lines, "请只输出一个 skillId 或 NONE。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestParseSkillExamples(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeRouteDecision(t *testing.T) {
|
||||
if got := normalizeRouteDecision("```refund_skill```\n补充说明"); got != "refund_skill" {
|
||||
if got := normalizeRouteDecision("```44```\n补充说明"); got != "44" {
|
||||
t.Fatalf("unexpected normalized decision: %q", got)
|
||||
}
|
||||
if got := normalizeRouteDecision(" none "); got != "NONE" {
|
||||
@@ -29,20 +29,20 @@ func TestNormalizeRouteDecision(t *testing.T) {
|
||||
func TestBuildSkillRoutePrompt(t *testing.T) {
|
||||
prompt := buildSkillRoutePrompt("我要申请退款", []models.SkillDefinition{
|
||||
{
|
||||
Code: "refund_skill",
|
||||
ID: 44,
|
||||
Name: "退款处理",
|
||||
Description: "负责退款和退货相关问题",
|
||||
Examples: `["退款进度","退货运费"]`,
|
||||
},
|
||||
})
|
||||
|
||||
if !strings.Contains(prompt, "skillCode=refund_skill") {
|
||||
t.Fatalf("expected prompt to include skill code, got %q", prompt)
|
||||
if !strings.Contains(prompt, "skillId=44") {
|
||||
t.Fatalf("expected prompt to include skill id, got %q", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "examples=退款进度 | 退货运费") {
|
||||
t.Fatalf("expected prompt to include examples, got %q", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "请只输出一个 skillCode 或 NONE。") {
|
||||
if !strings.Contains(prompt, "请只输出一个 skillId 或 NONE。") {
|
||||
t.Fatalf("expected prompt to include output constraint, got %q", prompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,12 @@ type RunLogService struct{}
|
||||
// Build 根据执行计划与运行结果构建 Skill 运行日志。
|
||||
func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *ExecutionTrace, err error) *models.SkillRunLog {
|
||||
log := &models.SkillRunLog{
|
||||
ConversationID: ctx.ConversationID,
|
||||
AIAgentID: ctx.AIAgent.ID,
|
||||
ManualSkillCode: ctx.ManualSkillCode,
|
||||
IntentCode: ctx.IntentCode,
|
||||
UserMessage: ctx.UserMessage,
|
||||
TraceData: s.buildTraceData(trace),
|
||||
CreatedAt: time.Now(),
|
||||
ConversationID: ctx.ConversationID,
|
||||
AIAgentID: ctx.AIAgent.ID,
|
||||
ManualSkillID: ctx.ManualSkillDefinitionID,
|
||||
UserMessage: ctx.UserMessage,
|
||||
TraceData: s.buildTraceData(trace),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if plan != nil {
|
||||
log.AIConfigID = plan.AIConfig.ID
|
||||
@@ -34,7 +33,6 @@ func (s *RunLogService) Build(ctx RuntimeContext, plan *ExecutionPlan, trace *Ex
|
||||
|
||||
if plan.Skill != nil {
|
||||
log.SkillDefinitionID = plan.Skill.ID
|
||||
log.SkillCode = plan.Skill.Code
|
||||
log.Matched = true
|
||||
log.FinalSelected = true
|
||||
log.MatchReason = plan.MatchReason
|
||||
|
||||
+11
-12
@@ -4,12 +4,11 @@ import "agent-desk/internal/models"
|
||||
|
||||
// RuntimeContext 表示一次 Skill 运行的输入上下文。
|
||||
type RuntimeContext struct {
|
||||
AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
|
||||
AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
|
||||
UserMessage string // UserMessage 为当前用户输入。
|
||||
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
|
||||
ManualSkillCode string // ManualSkillCode 为显式指定的 Skill 编码。
|
||||
IntentCode string // IntentCode 为上游识别出的意图编码。
|
||||
AIAgent models.AIAgent // AIAgent 为当前请求所属的 AI Agent,必填。
|
||||
AIConfig models.AIConfig // AIConfig 为当前请求实际使用的模型配置,必填。
|
||||
UserMessage string // UserMessage 为当前用户输入。
|
||||
ConversationID int64 // ConversationID 为当前会话 ID,无会话上下文时为 0。
|
||||
ManualSkillDefinitionID int64 // ManualSkillDefinitionID 为显式指定的 Skill 定义ID。
|
||||
}
|
||||
|
||||
// ExecutionPlan 表示 Skill Runtime 计算出的最终路由结果。
|
||||
@@ -35,12 +34,12 @@ type ExecutionTrace struct {
|
||||
}
|
||||
|
||||
type RouteTrace struct {
|
||||
Status string `json:"status"`
|
||||
CandidateSkillCodes []string `json:"candidateSkillCodes,omitempty"`
|
||||
SelectedSkillCode string `json:"selectedSkillCode,omitempty"`
|
||||
RawDecision string `json:"rawDecision,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CandidateSkillIDs []int64 `json:"candidateSkillIds,omitempty"`
|
||||
SelectedSkillID int64 `json:"selectedSkillId,omitempty"`
|
||||
RawDecision string `json:"rawDecision,omitempty"`
|
||||
LatencyMs int64 `json:"latencyMs,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type PromptTrace struct {
|
||||
|
||||
Reference in New Issue
Block a user