224 lines
7.4 KiB
Go
224 lines
7.4 KiB
Go
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
|
||
}
|