Files
weichen b5a782bb38 feat(chat): dialogue triggers for comments/reopen/pending-supplements with mistouch guards
Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
2026-09-25 20:14:10 +00:00

265 lines
8.9 KiB
Go
Raw Permalink 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"
"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,
IntentDocGovCommentAdd, IntentDocGovCommentResolve, IntentDocGovReopenRequest, IntentDocGovReopenApprove,
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, IntentQueryPendingSupplements,
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
}