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: 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: 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 }