Files
Agent-System/internal/intent/recognizer_expectation.go
T

224 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"fmt"
"regexp"
"strings"
"time"
)
// matchExpectationHit 检验是否命中挂起预期 (分支 1)
func matchExpectationHit(input string, pending *PendingExpectation) *CandidateIntent {
// 0. 检验超时过期
if !pending.ExpiresAt.IsZero() && time.Now().After(pending.ExpiresAt) {
return nil
}
// 1. 否定/暂缓意图
declineKeywords := []string{"先别", "别推", "算了", "先不", "等等", "暂时不", "先别推", "不要推进", "不要", "不行", "算了吧", "先放一放", "先等等", "取消", "放弃"}
if pending.DeclineWord != "" {
declineKeywords = append(declineKeywords, pending.DeclineWord)
}
for _, dk := range declineKeywords {
if input == dk || strings.HasPrefix(input, dk) {
return &CandidateIntent{
Intent: IntentDeclineExpectation,
Confidence: 0.95,
Branch: BranchExpectationHit,
IsDecline: true,
FilledSlots: make(map[string]string),
Reasoning: fmt.Sprintf("用户明确表示暂缓或取消当前挂起建议 ('%s')", input),
}
}
}
// 2. 选择序号检验 (如 "1", "2", "选1", "选2", "第1个", "第一个", "选项1")
var matchedOpt *ExpectationOption
optIndex := matchOptionIndex(input, pending.Options)
if optIndex >= 0 && optIndex < len(pending.Options) {
matchedOpt = &pending.Options[optIndex]
}
// 肯定前缀后含独立新请求谓词时,不得吞掉新意图(如「好,请先查法规」)
if matchedOpt == nil && hasIndependentNewPredicate(input) {
return nil
}
// 3. 显式确认短语(不受 AllowAffirmative 限制)
explicitConfirm := isExplicitConfirmPhrase(input)
// 4. 肯定词检验 (如 "好", "行", "可以", "成", "没问题", "同意", "好的", "就按这个", "按你说的办", "确认")
affirmativePrefixes := []string{"好", "好的", "行", "行啊", "成", "可以", "没问题", "同意", "就按这个", "按这个", "就按这个办", "听你的", "ok", "yes", "就这么定", "确认", "确定", "批准"}
if pending.AffirmativeWord != "" {
affirmativePrefixes = append(affirmativePrefixes, pending.AffirmativeWord)
}
isAffirmative := explicitConfirm
for _, aff := range affirmativePrefixes {
if input == aff ||
strings.HasPrefix(input, aff+",") ||
strings.HasPrefix(input, aff+",") ||
strings.HasPrefix(input, aff+"但") ||
strings.HasPrefix(input, aff+" ") ||
strings.HasPrefix(input, aff+"!") ||
strings.HasPrefix(input, aff+"!") ||
(aff == "确认" && (input == "确认" || input == "确认驳回" || input == "确认批准" || input == "确认执行")) ||
(pending.AffirmativeWord != "" && strings.HasPrefix(input, pending.AffirmativeWord)) {
isAffirmative = true
break
}
}
// 裸肯定词必须 AllowAffirmative=true(显式确认短语除外)
if isAffirmative && !explicitConfirm && !pending.AllowAffirmative {
bareAffirmative := false
for _, aff := range affirmativePrefixes {
if input == aff {
bareAffirmative = true
break
}
}
if bareAffirmative {
isAffirmative = false
}
}
if matchedOpt == nil && isAffirmative {
if pending.DefaultOption != nil {
matchedOpt = pending.DefaultOption
} else if len(pending.Options) > 0 {
matchedOpt = &pending.Options[0]
} else if pending.ExpectedIntent != "" {
matchedOpt = &ExpectationOption{
OptionID: pending.TargetAction,
Title: pending.Prompt,
Intent: pending.ExpectedIntent,
Slots: pending.Payload,
}
}
}
if matchedOpt == nil {
return nil
}
// 4. 提取增量事实补丁 (Patch Merge)
// 例如: "好,但目标市场改成欧洲CE", "选1,先把预算定为15万"
patchSlots := extractFactSlots(input)
filledSlots := make(map[string]string)
for k, v := range matchedOpt.Slots {
filledSlots[k] = v
}
for k, v := range patchSlots {
filledSlots[k] = v
}
if pending.TargetAction != "" && filledSlots["target_action"] == "" {
filledSlots["target_action"] = pending.TargetAction
}
confidence := 1.0
reasoning := fmt.Sprintf("用户明确肯定上一轮参谋建议动作 [%s: %s]", matchedOpt.OptionID, matchedOpt.Title)
if len(patchSlots) > 0 {
confidence = 0.98
reasoning = fmt.Sprintf("用户肯定上一轮建议动作 [%s: %s],并附带增量事实补丁: %v", matchedOpt.OptionID, matchedOpt.Title, patchSlots)
} else if optIndex >= 0 {
confidence = 0.98
reasoning = fmt.Sprintf("用户按序号选择了备选动作 [%s: %s]", matchedOpt.OptionID, matchedOpt.Title)
}
return &CandidateIntent{
Intent: matchedOpt.Intent,
Confidence: confidence,
Branch: BranchExpectationHit,
FilledSlots: filledSlots,
PatchSlots: patchSlots,
MatchedOption: matchedOpt,
Reasoning: reasoning,
}
}
// matchOptionIndex 匹配用户输入的选项编号
func matchOptionIndex(input string, options []ExpectationOption) int {
input = strings.TrimSpace(input)
if len(options) == 0 {
return -1
}
patterns := []struct {
regex string
idx int
}{
{"^(1|选1|选第1个|第一个|第1个|选项1|选项一)$", 0},
{"^(2|选2|选第2个|第二个|第2个|选项2|选项二)$", 1},
{"^(3|选3|选第3个|第三个|第3个|选项3|选项三)$", 2},
}
for _, p := range patterns {
if matched, _ := regexp.MatchString("(?i)"+p.regex, input); matched {
return p.idx
}
}
for idx, opt := range options {
if strings.EqualFold(opt.OptionID, input) {
return idx
}
}
return -1
}
// IsConsultativeOrHesitant 报告输入是否在征询意见或表达犹豫。
// 只有这类输入才保留阶段对照话术;资料名、名词短语不得走该话术。
func IsConsultativeOrHesitant(input string) bool {
return isConsultativeOrHesitant(input)
}
// isConsultativeOrHesitant 检验是否属于犹豫或征询参谋意见 (分支 3)
func isConsultativeOrHesitant(input string) bool {
patterns := []string{
"你觉得呢", "你怎么看", "有什么建议", "怎么选", "拿不定主意",
"先放放.*你觉得", "这个先放放", "先放一放.*你觉得", "哪个好", "给点意见",
}
for _, p := range patterns {
if matched, _ := regexp.MatchString("(?i)"+p, input); matched {
return true
}
}
return false
}
// buildAmbiguousConsultResult 构建分支 3 (模糊/分忧引导) 输出
func buildAmbiguousConsultResult(input, snapID string, pending *PendingExpectation) *IntentRecognitionResult {
clarifyQ := "我为您准备了当前阶段最稳妥的推进选项。请问需要我为您详细分析技术可行性,还是先为您做方案摸底?"
if pending != nil && len(pending.Options) > 0 {
var optDesc strings.Builder
optDesc.WriteString("针对目前的决策节点,我为您梳理了以下选项供您参考:\n")
for _, o := range pending.Options {
optDesc.WriteString(fmt.Sprintf("• [%s] %s: %s\n", o.OptionID, o.Title, o.Description))
}
clarifyQ = optDesc.String()
}
top := &CandidateIntent{
Intent: IntentCasualChat,
Confidence: 0.65,
Branch: BranchAmbiguousFallback,
FilledSlots: make(map[string]string),
Reasoning: "用户表达犹豫或主动征询参谋意见,触发参谋主动分忧与引导",
ClarificationQuestion: clarifyQ,
}
result := &IntentRecognitionResult{
RawInput: input,
Branch: BranchAmbiguousFallback,
TopIntent: top,
Candidates: []CandidateIntent{*top},
ContextSnapshotID: snapID,
RequiresClarifying: true,
}
finalizeRecognitionResult(result)
return result
}