fix: bypass answerability gate for runtime actions
This commit is contained in:
@@ -181,6 +181,11 @@ func (g *KnowledgeAnswerabilityGate) retrieveKnowledge(ctx context.Context, stat
|
||||
}
|
||||
gate := g.withDefaults()
|
||||
req := state.Input.Request
|
||||
if isRuntimeActionIntent(req.UserMessage.Content) {
|
||||
state.SkipGate = true
|
||||
state.recordAnswerability(answerabilityStatusSkipped, "runtime action intent", nil)
|
||||
return state, nil
|
||||
}
|
||||
configuredKnowledgeIDs := utils.SplitInt64s(req.AIAgent.KnowledgeIDs)
|
||||
retriever := gate.newRetriever(req.AIAgent)
|
||||
if retriever == nil {
|
||||
@@ -232,6 +237,72 @@ func (g *KnowledgeAnswerabilityGate) retrieveKnowledge(ctx context.Context, stat
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func isRuntimeActionIntent(content string) bool {
|
||||
text := strings.ToLower(strings.TrimSpace(content))
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
compact := strings.NewReplacer(" ", "", "\t", "", "\n", "", "\r", "").Replace(text)
|
||||
handoffPhrases := []string{
|
||||
"我要转人工",
|
||||
"帮我转人工",
|
||||
"转人工",
|
||||
"接人工",
|
||||
"找人工",
|
||||
"真人客服",
|
||||
"humanagent",
|
||||
"liveagent",
|
||||
}
|
||||
for _, phrase := range handoffPhrases {
|
||||
if strings.Contains(compact, phrase) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if containsAny(compact, []string{"人工客服", "人工服务", "人工处理"}) &&
|
||||
!containsAny(compact, []string{"是什么", "怎么", "如何", "多少", "几", "吗", "?"}) &&
|
||||
(isShortActionPhrase(compact) || containsAny(compact, []string{"我要", "帮我", "请", "联系", "需要"})) {
|
||||
return true
|
||||
}
|
||||
ticketPhrases := []string{
|
||||
"创建工单",
|
||||
"新建工单",
|
||||
"提交工单",
|
||||
"发起工单",
|
||||
"建工单",
|
||||
"开工单",
|
||||
"我要建单",
|
||||
"帮我建单",
|
||||
"创建ticket",
|
||||
"createticket",
|
||||
}
|
||||
for _, phrase := range ticketPhrases {
|
||||
if strings.Contains(compact, phrase) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.Contains(compact, "工单") {
|
||||
for _, action := range []string{"创建", "新建", "提交", "发起", "建", "开", "帮我", "我要", "请"} {
|
||||
if strings.Contains(compact, action) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAny(text string, values []string) bool {
|
||||
for _, value := range values {
|
||||
if strings.Contains(text, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isShortActionPhrase(text string) bool {
|
||||
return len([]rune(text)) <= 8
|
||||
}
|
||||
|
||||
func (g *KnowledgeAnswerabilityGate) gradeAnswerability(ctx context.Context, state *answerabilityGateState) (*answerabilityGateState, error) {
|
||||
if state == nil {
|
||||
return &answerabilityGateState{}, nil
|
||||
|
||||
@@ -349,6 +349,64 @@ func TestBuildRunMessagesInjectsKnowledgeWhenGateAllows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRunMessagesSkipsAnswerabilityGateForExplicitHandoffIntent(t *testing.T) {
|
||||
summary := &RunResult{}
|
||||
retriever := &fakeKnowledgeContextRetriever{
|
||||
knowledgeBaseIDs: []int64{1},
|
||||
err: errors.New("retriever should not be called for handoff intent"),
|
||||
}
|
||||
gate := newTestKnowledgeAnswerabilityGate(retriever, nil)
|
||||
|
||||
messages := buildRunMessages(context.Background(), newAnswerabilityGateRunInput("我要转人工", "1"), summary, nil, gate)
|
||||
|
||||
if summary.ReplyText != "" {
|
||||
t.Fatalf("expected handoff intent to continue to agent/tool routing, got fallback %q", summary.ReplyText)
|
||||
}
|
||||
if retriever.lastQuery != "" {
|
||||
t.Fatalf("expected retriever to be skipped for handoff intent, got query %q", retriever.lastQuery)
|
||||
}
|
||||
if !messagesContainContent(messages, "我要转人工") {
|
||||
t.Fatalf("expected current user message in messages, got %#v", messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRunMessagesSkipsAnswerabilityGateForExplicitTicketIntent(t *testing.T) {
|
||||
summary := &RunResult{}
|
||||
retriever := &fakeKnowledgeContextRetriever{
|
||||
knowledgeBaseIDs: []int64{1},
|
||||
err: errors.New("retriever should not be called for ticket intent"),
|
||||
}
|
||||
gate := newTestKnowledgeAnswerabilityGate(retriever, nil)
|
||||
|
||||
messages := buildRunMessages(context.Background(), newAnswerabilityGateRunInput("帮我创建一个工单", "1"), summary, nil, gate)
|
||||
|
||||
if summary.ReplyText != "" {
|
||||
t.Fatalf("expected ticket intent to continue to agent/tool routing, got fallback %q", summary.ReplyText)
|
||||
}
|
||||
if retriever.lastQuery != "" {
|
||||
t.Fatalf("expected retriever to be skipped for ticket intent, got query %q", retriever.lastQuery)
|
||||
}
|
||||
if !messagesContainContent(messages, "帮我创建一个工单") {
|
||||
t.Fatalf("expected current user message in messages, got %#v", messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeActionIntentDoesNotMatchKnowledgeQuestions(t *testing.T) {
|
||||
cases := []string{
|
||||
"人工客服服务时间是什么?",
|
||||
"工单状态怎么查询?",
|
||||
"需要查询工单状态怎么操作?",
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
if isRuntimeActionIntent(tc) {
|
||||
t.Fatalf("expected %q to stay in knowledge answerability flow", tc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestKnowledgeAnswerabilityGate(retriever knowledgeContextRetriever, chatModel model.BaseChatModel) *KnowledgeAnswerabilityGate {
|
||||
return &KnowledgeAnswerabilityGate{
|
||||
newRetriever: func(aiAgent models.AIAgent) knowledgeContextRetriever {
|
||||
|
||||
@@ -57,7 +57,11 @@ func resolveKnowledgeHumanSupportFallback(aiAgent models.AIAgent) string {
|
||||
if strs.IsBlank(base) {
|
||||
base = "当前知识库暂无明确信息。"
|
||||
}
|
||||
return base
|
||||
humanSupport := "建议你联系人工客服进一步确认。"
|
||||
if strings.Contains(base, humanSupport) {
|
||||
return base
|
||||
}
|
||||
return base + " " + humanSupport
|
||||
}
|
||||
|
||||
func buildKnowledgeRuntimeInstruction(answerMode enums.KnowledgeAnswerMode, fallbackReply string) string {
|
||||
|
||||
Reference in New Issue
Block a user