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()
|
gate := g.withDefaults()
|
||||||
req := state.Input.Request
|
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)
|
configuredKnowledgeIDs := utils.SplitInt64s(req.AIAgent.KnowledgeIDs)
|
||||||
retriever := gate.newRetriever(req.AIAgent)
|
retriever := gate.newRetriever(req.AIAgent)
|
||||||
if retriever == nil {
|
if retriever == nil {
|
||||||
@@ -232,6 +237,72 @@ func (g *KnowledgeAnswerabilityGate) retrieveKnowledge(ctx context.Context, stat
|
|||||||
return state, nil
|
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) {
|
func (g *KnowledgeAnswerabilityGate) gradeAnswerability(ctx context.Context, state *answerabilityGateState) (*answerabilityGateState, error) {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return &answerabilityGateState{}, 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 {
|
func newTestKnowledgeAnswerabilityGate(retriever knowledgeContextRetriever, chatModel model.BaseChatModel) *KnowledgeAnswerabilityGate {
|
||||||
return &KnowledgeAnswerabilityGate{
|
return &KnowledgeAnswerabilityGate{
|
||||||
newRetriever: func(aiAgent models.AIAgent) knowledgeContextRetriever {
|
newRetriever: func(aiAgent models.AIAgent) knowledgeContextRetriever {
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ func resolveKnowledgeHumanSupportFallback(aiAgent models.AIAgent) string {
|
|||||||
if strs.IsBlank(base) {
|
if strs.IsBlank(base) {
|
||||||
base = "当前知识库暂无明确信息。"
|
base = "当前知识库暂无明确信息。"
|
||||||
}
|
}
|
||||||
return base
|
humanSupport := "建议你联系人工客服进一步确认。"
|
||||||
|
if strings.Contains(base, humanSupport) {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return base + " " + humanSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKnowledgeRuntimeInstruction(answerMode enums.KnowledgeAnswerMode, fallbackReply string) string {
|
func buildKnowledgeRuntimeInstruction(answerMode enums.KnowledgeAnswerMode, fallbackReply string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user