264 lines
8.8 KiB
Go
264 lines
8.8 KiB
Go
package intent
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"strings"
|
||
)
|
||
|
||
// DialogueAct 轻量对话行为标签(Phase 1 安全门控;完整枚举留给 V2)
|
||
type DialogueAct string
|
||
|
||
const (
|
||
DialogueActAsk DialogueAct = "ask"
|
||
DialogueActCommand DialogueAct = "command"
|
||
DialogueActConfirm DialogueAct = "confirm"
|
||
DialogueActDecline DialogueAct = "decline"
|
||
DialogueActOther DialogueAct = "other"
|
||
)
|
||
|
||
// detectDialogueAct 识别查询/命令/确认/拒绝语气(规则轻量版)
|
||
func detectDialogueAct(input string) DialogueAct {
|
||
trimmed := strings.TrimSpace(input)
|
||
if trimmed == "" {
|
||
return DialogueActOther
|
||
}
|
||
|
||
declineMarkers := []string{"不要", "别", "先别", "先不", "取消", "算了", "暂缓", "放弃"}
|
||
for _, m := range declineMarkers {
|
||
if strings.HasPrefix(trimmed, m) || strings.Contains(trimmed, m) {
|
||
// 「不要修改…只查询…」整体仍是只读询问,优先 ask
|
||
if isNegatedWriteWithReadonlyIntent(trimmed) {
|
||
return DialogueActAsk
|
||
}
|
||
if strings.HasPrefix(trimmed, m) {
|
||
return DialogueActDecline
|
||
}
|
||
}
|
||
}
|
||
|
||
confirmExact := []string{"确认", "确定", "确认推进", "确认切换", "确认执行", "确认驳回", "确认批准", "就按这个执行", "就按这个办"}
|
||
for _, c := range confirmExact {
|
||
if trimmed == c || strings.HasPrefix(trimmed, c+",") || strings.HasPrefix(trimmed, c+",") {
|
||
return DialogueActConfirm
|
||
}
|
||
}
|
||
|
||
askMarkers := []string{
|
||
"吗", "呢", "谁", "是否", "有没有", "通过了吗", "了吗", "了没",
|
||
"什么状态", "当前状态", "查询", "看看", "问一下", "了解一下",
|
||
"怎么样", "怎么", "什么是", "为什么", "哪些", "多少", "好吗",
|
||
}
|
||
hasAskPunct := strings.Contains(trimmed, "?") || strings.Contains(trimmed, "?")
|
||
for _, m := range askMarkers {
|
||
if strings.Contains(trimmed, m) {
|
||
return DialogueActAsk
|
||
}
|
||
}
|
||
if hasAskPunct {
|
||
return DialogueActAsk
|
||
}
|
||
|
||
commandMarkers := []string{"请", "把", "帮我", "立即", "马上", "执行"}
|
||
for _, m := range commandMarkers {
|
||
if strings.Contains(trimmed, m) {
|
||
return DialogueActCommand
|
||
}
|
||
}
|
||
|
||
return DialogueActCommand
|
||
}
|
||
|
||
// isNegatedWriteWithReadonlyIntent 「不要修改X,只查询Y」类否定写 + 只读意图
|
||
func isNegatedWriteWithReadonlyIntent(input string) bool {
|
||
hasNeg := strings.Contains(input, "不要") || strings.Contains(input, "别") || strings.Contains(input, "先别")
|
||
hasModify := strings.Contains(input, "修改配置") || strings.Contains(input, "修改设置") ||
|
||
strings.Contains(input, "改配置") || strings.Contains(input, "改设置")
|
||
hasQuery := strings.Contains(input, "查询") || strings.Contains(input, "只查") ||
|
||
strings.Contains(input, "看看") || strings.Contains(input, "查看")
|
||
return hasNeg && hasModify && hasQuery
|
||
}
|
||
|
||
// isWriteIntent 高风险/写操作意图(用于候选冲突门禁)
|
||
func isWriteIntent(t IntentType) bool {
|
||
switch t {
|
||
case IntentAdvanceStage, IntentTriggerTask, IntentTriggerAdhocTask, IntentSupplementInfo,
|
||
IntentDocGovClaim, IntentDocGovSubmitReview, IntentDocGovReviewApprove, IntentDocGovReviewReject,
|
||
IntentDocGovFinalApprove, IntentDocGovFinalReject, IntentDocGovProposalDecide,
|
||
IntentConfirmAction, IntentExecuteStageAdvance,
|
||
IntentSwitchWorkflow, IntentTriggerWorkflow,
|
||
IntentRunResume, IntentRunRestart,
|
||
IntentRevisePropose:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// IsQueryLegacyIntent 导出只读查询意图判定(供 Coordinator IntentPlan 使用)
|
||
func IsQueryLegacyIntent(t IntentType) bool {
|
||
return isQueryIntent(t)
|
||
}
|
||
|
||
// isQueryIntent 只读查询意图
|
||
func isQueryIntent(t IntentType) bool {
|
||
switch t {
|
||
case IntentQueryProgress, IntentQueryRegulations, IntentQuerySimilarCases,
|
||
IntentQueryUserRights, IntentQueryRejectReason,
|
||
IntentQueryWorkflowStatus, IntentQueryDocStatus, IntentHelpCapabilities,
|
||
IntentRunStatusQuery, IntentImpactAnalyze:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// hasIndependentNewPredicate 肯定前缀后是否仍含独立新请求谓词(防挂起预期吞请求)
|
||
func hasIndependentNewPredicate(input string) bool {
|
||
rest := stripAffirmativePrefix(input)
|
||
if rest == "" || rest == input {
|
||
// 无肯定前缀剥离时,仍检查整句中「好,」后的部分
|
||
rest = input
|
||
}
|
||
markers := []string{
|
||
"查法规", "查一下法规", "请先查", "先查法规", "查询法规", "查法规",
|
||
"查进度", "查询进度", "问进度",
|
||
"认领", "送审", "提交审核", "提审",
|
||
"切换工作流", "切换到", "运行工作流",
|
||
"查标准", "查强标", "查指导原则",
|
||
}
|
||
for _, m := range markers {
|
||
if strings.Contains(rest, m) {
|
||
return true
|
||
}
|
||
}
|
||
// 「查」+「法规/标准」
|
||
if (strings.Contains(rest, "查") || strings.Contains(rest, "请先")) &&
|
||
(strings.Contains(rest, "法规") || strings.Contains(rest, "标准") || strings.Contains(rest, "进度")) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func stripAffirmativePrefix(input string) string {
|
||
prefixes := []string{"好的", "好", "行啊", "行", "可以", "没问题", "同意", "就按这个办", "就按这个", "按这个", "听你的", "ok", "yes", "就这么定", "确认", "确定", "批准"}
|
||
trimmed := strings.TrimSpace(input)
|
||
lower := strings.ToLower(trimmed)
|
||
for _, p := range prefixes {
|
||
pl := strings.ToLower(p)
|
||
if lower == pl {
|
||
return ""
|
||
}
|
||
for _, sep := range []string{",", ",", " ", "!", "!", "但"} {
|
||
if strings.HasPrefix(lower, pl+sep) || strings.HasPrefix(trimmed, p+sep) {
|
||
return strings.TrimSpace(trimmed[len(p)+len(sep):])
|
||
}
|
||
}
|
||
}
|
||
return trimmed
|
||
}
|
||
|
||
// isDocGovStatusQuery 文档治理关键词 + 状态/结果询问
|
||
func isDocGovStatusQuery(input, kind string) bool {
|
||
statusMarkers := []string{"了吗", "了吗?", "通过了吗", "了吗?", "状态", "有没有", "是否", "完成了吗", "好了吗"}
|
||
hasStatus := false
|
||
for _, m := range statusMarkers {
|
||
if strings.Contains(input, m) {
|
||
hasStatus = true
|
||
break
|
||
}
|
||
}
|
||
if detectDialogueAct(input) == DialogueActAsk {
|
||
hasStatus = true
|
||
}
|
||
switch kind {
|
||
case "claim":
|
||
return hasStatus && strings.Contains(input, "认领")
|
||
case "submit":
|
||
return hasStatus && (strings.Contains(input, "送审") || strings.Contains(input, "提审") || strings.Contains(input, "审核"))
|
||
default:
|
||
return hasStatus
|
||
}
|
||
}
|
||
|
||
// applyCandidateConflictGate 写/读候选分差不足时触发澄清,禁止直接执行 Top1
|
||
func applyCandidateConflictGate(candidates []CandidateIntent, top *CandidateIntent) bool {
|
||
if len(candidates) < 2 || top == nil {
|
||
return false
|
||
}
|
||
margin := top.Confidence - candidates[1].Confidence
|
||
if margin >= 0.05 {
|
||
return false
|
||
}
|
||
topWrite := isWriteIntent(top.Intent)
|
||
secWrite := isWriteIntent(candidates[1].Intent)
|
||
topQuery := isQueryIntent(top.Intent)
|
||
secQuery := isQueryIntent(candidates[1].Intent)
|
||
if (topWrite && secQuery) || (topQuery && secWrite) {
|
||
writeLabel := string(top.Intent)
|
||
queryLabel := string(candidates[1].Intent)
|
||
if topQuery {
|
||
writeLabel = string(candidates[1].Intent)
|
||
queryLabel = string(top.Intent)
|
||
}
|
||
top.ClarificationQuestion = fmt.Sprintf(
|
||
"检测到您的表述可能有两种理解:查询「%s」相关信息,或执行「%s」写操作。请明确说明您希望查询还是执行。",
|
||
queryLabel, writeLabel,
|
||
)
|
||
top.Reasoning = fmt.Sprintf("写/读候选冲突:top=%s(%.2f) vs second=%s(%.2f) margin=%.2f",
|
||
top.Intent, top.Confidence, candidates[1].Intent, candidates[1].Confidence, margin)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// enrichShadowFields 填充观测字段(不参与路由)
|
||
func enrichShadowFields(res *IntentRecognitionResult) {
|
||
if res == nil || res.TopIntent == nil {
|
||
return
|
||
}
|
||
res.ShadowScore = res.TopIntent.Confidence
|
||
if len(res.Candidates) >= 2 {
|
||
res.ShadowMargin = res.Candidates[0].Confidence - res.Candidates[1].Confidence
|
||
}
|
||
res.ShadowDialogueAct = string(detectDialogueAct(res.RawInput))
|
||
switch {
|
||
case len(res.TopIntent.MissingSlots) > 0:
|
||
res.ShadowDecisionHint = "needs_slots"
|
||
case res.Branch == BranchAmbiguousFallback:
|
||
res.ShadowDecisionHint = "ambiguous"
|
||
case res.RequiresClarifying:
|
||
res.ShadowDecisionHint = "needs_clarification"
|
||
default:
|
||
res.ShadowDecisionHint = "allowed"
|
||
}
|
||
}
|
||
|
||
// finalizeRecognitionResult 填充 shadow + V2 双写(不改变 TopIntent 路由语义)
|
||
func finalizeRecognitionResult(res *IntentRecognitionResult) {
|
||
enrichShadowFields(res)
|
||
AttachIntentFrameV2(res)
|
||
ApplyV2ShadowObservability(res)
|
||
if res != nil && res.TopIntent != nil {
|
||
slog.Info("intent_recognition",
|
||
"intent", string(res.TopIntent.Intent),
|
||
"branch", string(res.Branch),
|
||
"confidence", res.TopIntent.Confidence,
|
||
)
|
||
}
|
||
}
|
||
|
||
func isExplicitConfirmPhrase(input string) bool {
|
||
trimmed := strings.TrimSpace(input)
|
||
phrases := []string{
|
||
"确认推进", "确认切换", "确认执行", "确认驳回", "确认批准",
|
||
"就按这个执行", "就按这个办", "就这么定",
|
||
}
|
||
for _, p := range phrases {
|
||
if trimmed == p || strings.HasPrefix(trimmed, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|