Files
Agent-System/internal/intent/recognizer.go
T
weichenandCursor a236ddc0fa fix(intent): accept 编写/编制/起草 and keep intake markers on intake
The chat fallback only treated 生成/做/跑/出 as task verbs, and the shorter RDev alias 用户需求 stole 初始用户需求表.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 06:12:15 +00:00

1292 lines
47 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 (
"context"
"fmt"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/squad"
)
// Recognizer 意图识别器接口
type Recognizer interface {
Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error)
}
// RuleBasedRecognizer 规则与上下文约束结合的四级漏斗意图消解引擎
type RuleBasedRecognizer struct{}
func NewRecognizer() Recognizer {
return &RuleBasedRecognizer{}
}
func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error) {
result, err := r.recognizeCore(ctx, input, userCtx)
if err != nil || result == nil {
return result, err
}
attachPlanProposal(ctx, r, input, userCtx, result)
return result, nil
}
func (r *RuleBasedRecognizer) recognizeCore(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
result := &IntentRecognitionResult{
RawInput: input,
Branch: BranchAmbiguousFallback,
TopIntent: &CandidateIntent{
Intent: IntentCasualChat,
Branch: BranchAmbiguousFallback,
Confidence: 1.0,
Reasoning: "输入为空",
},
Candidates: []CandidateIntent{
{Intent: IntentCasualChat, Branch: BranchAmbiguousFallback, Confidence: 1.0, Reasoning: "空输入"},
},
}
finalizeRecognitionResult(result)
return result, nil
}
snapID := ""
currentStage := maturity.StageV01_Triage
var pending *PendingExpectation
if userCtx != nil {
snapID = userCtx.SnapshotID
if userCtx.CurrentStage != "" {
currentStage = maturity.MaturityStage(userCtx.CurrentStage)
}
pending = userCtx.PendingExpectation
}
// -------------------------------------------------------------
// 阶段零 (Stage 0):态势前置过滤与安全防护检查 (Security & UI Guard)
// -------------------------------------------------------------
// 1. 安全红线校验:严禁通过自然语言对话修改系统受控配置、用户法定身份/职位/工号
// 「不要修改配置,只查询…」类否定写 + 只读意图不得误触安全拦截
if isSecurityViolation(trimmed) && !isNegatedWriteWithReadonlyIntent(trimmed) {
top := &CandidateIntent{
Intent: IntentSecurityForbidden,
Branch: BranchNewIntentPreempt,
Confidence: 1.0,
FilledSlots: map[string]string{SlotForbiddenTarget: "identity_or_settings"},
Reasoning: "检测到试图修改系统底层受控配置或用户法定身份/职位/工号,根据医疗器械质量与合规管理规范(ISO 13485 / 21 CFR Part 11),触发安全红线拦截",
}
result := &IntentRecognitionResult{
RawInput: trimmed,
Branch: BranchNewIntentPreempt,
TopIntent: top,
Candidates: []CandidateIntent{*top},
ContextSnapshotID: snapID,
RequiresClarifying: false,
}
finalizeRecognitionResult(result)
return result, nil
}
// 1.5 否定写 + 只读查询:不得误触安全拦截,也不得当作写操作
if isNegatedWriteWithReadonlyIntent(trimmed) {
top := &CandidateIntent{
Intent: IntentQueryUserRights,
Branch: BranchNewIntentPreempt,
Confidence: 0.88,
FilledSlots: map[string]string{SlotQueryTopic: "system_configuration"},
Reasoning: "用户明确否定修改配置并请求只读查询,不得路由为 SECURITY_FORBIDDEN 或写操作",
ClarificationQuestion: "我理解您希望只读查看当前系统配置,不会对配置做任何修改。请问您具体想查看哪一项配置?",
}
result := &IntentRecognitionResult{
RawInput: trimmed,
Branch: BranchNewIntentPreempt,
TopIntent: top,
Candidates: []CandidateIntent{*top},
ContextSnapshotID: snapID,
RequiresClarifying: true,
}
finalizeRecognitionResult(result)
return result, nil
}
// 2. 纯 UI 视窗与主题控制指令拦截:归入 CASUAL_CHAT 并给出不可执行提示
if isPureUICommand(trimmed) {
top := &CandidateIntent{
Intent: IntentCasualChat,
Branch: BranchNewIntentPreempt,
Confidence: 0.95,
FilledSlots: make(map[string]string),
Reasoning: "纯UI视窗或主题控制指令,系统暂不支持通过对话直接操作界面视窗布局或主题,请通过界面按钮或快捷键操作",
ClarificationQuestion: "检测到您下达了界面视窗或主题控制指令(如折叠/打开侧边栏、切换主题/深色浅色)。该操作属于前端UI视窗布局与主题控制,请直接点击界面右上角或侧边的视窗/主题切换按钮。",
}
result := &IntentRecognitionResult{
RawInput: trimmed,
Branch: BranchNewIntentPreempt,
TopIntent: top,
Candidates: []CandidateIntent{*top},
ContextSnapshotID: snapID,
RequiresClarifying: false,
}
finalizeRecognitionResult(result)
return result, nil
}
// -------------------------------------------------------------
// 阶段 0.5:若输入明确是咨询参谋/犹豫(如“这个先放放,你觉得呢”),优先归入分支 3 (参谋主动分忧)
// -------------------------------------------------------------
if isConsultativeOrHesitant(trimmed) {
res := buildAmbiguousConsultResult(trimmed, snapID, pending)
return res, nil
}
// -------------------------------------------------------------
// 阶段 0.8:分支 1 判定——命中历史轮次挂起预期 (肯定 / 选项序号 / 复合带补丁 / 明确撤销)
// -------------------------------------------------------------
if pending != nil {
if hitCand := matchExpectationHit(trimmed, pending); hitCand != nil {
result := &IntentRecognitionResult{
RawInput: trimmed,
Branch: BranchExpectationHit,
TopIntent: hitCand,
Candidates: []CandidateIntent{*hitCand},
ContextSnapshotID: snapID,
}
finalizeRecognitionResult(result)
return result, nil
}
}
// -------------------------------------------------------------
// 阶段一与阶段二 (Stage 1 & Stage 2):领域大类路由、微观细分动作与槽位提取
// -------------------------------------------------------------
var candidates []CandidateIntent
// 领域 1: 法定受控文档治理流转 (GOV_LIFECYCLE)
// (涵盖 驳回、通过、送审、认领、终审批准、提案裁决)
if cand := matchDocGovAction(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 2: 决策辅助、合规与知识检索 (DECISION_QUERY - Knowledge & Rights)
// (涵盖 法规强标、标杆类似案例、用户法定职责与权限)
if cand := matchKnowledgeQuery(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 3: 交付物与阶段演进 (CONTENT_SYNTHESIS)
// 3.1 阶段推进 (ADVANCE_STAGE)
if cand := matchAdvanceStage(trimmed, currentStage); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
if cand := matchImpactIntent(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 3.2 补充信息/参数事实 (SUPPLEMENT_INFO)
if cand := matchSupplementInfo(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 3.3 交付任务触发 (TRIGGER_TASK)
if cand := matchTriggerTask(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 4: 长任务运行控制 (Phase 4)
if cand := matchRunControl(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 5: 工作流控制与状态查询 (Phase 3 — 优先于进度查询,避免「测试经理」等词误判)
if CurrentFeatureFlags().WorkflowIntentsEnabled {
if cand := matchQueryWorkflowStatus(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
if cand := matchSwitchWorkflow(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
if cand := matchTriggerWorkflow(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
}
// 领域 5: 文档/交付物状态只读查询
if cand := matchQueryDocStatus(trimmed, userCtx); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 6: 帮助与暂不支持
if cand := matchHelpCapabilities(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
if cand := matchUnsupportedActionCandidate(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 领域 7: 进度与技术研判查询 (DECISION_QUERY - Progress & Technical)
if cand := matchQueryProgress(trimmed); cand != nil {
cand.Branch = BranchNewIntentPreempt
candidates = append(candidates, *cand)
}
// 通用对话/闲聊保底 (CASUAL_CHAT)
chatCand := CandidateIntent{
Intent: IntentCasualChat,
Branch: BranchNewIntentPreempt,
Confidence: 0.2,
FilledSlots: make(map[string]string),
Reasoning: "未匹配到明确业务指令,归入通用对话",
}
if isGreetingOrHelp(trimmed) {
chatCand.Confidence = 0.85
chatCand.Reasoning = "匹配到问候语或通用帮助请求"
}
candidates = append(candidates, chatCand)
sortCandidates(candidates)
top := &candidates[0]
applyCapabilityHint(top, userCtx)
// -------------------------------------------------------------
// 阶段三 (Stage 3):槽位完备性与置信度裁决 (门禁裁决与追问下发)
// -------------------------------------------------------------
branch := BranchNewIntentPreempt
requiresClarifying := false
slotOrPolicyClarify := false
// 1. 若为 DOCGOV_REVIEW_REJECT 或 DOCGOV_FINAL_REJECT,检查必填槽位 reject_reason
if top.Intent == IntentDocGovReviewReject || top.Intent == IntentDocGovFinalReject {
if top.FilledSlots[SlotRejectReason] == "" {
requiresClarifying = true
slotOrPolicyClarify = true
top.ClarificationQuestion = "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
top.MissingSlots = []string{SlotRejectReason}
}
}
// 2. 权限/生命周期前置剪枝:保留真实写意图,仅标记需澄清(L2 最终裁决)
if strings.Contains(top.Reasoning, "权限不符") || strings.Contains(top.Reasoning, "前置剪枝") {
requiresClarifying = true
slotOrPolicyClarify = true
}
// 3. 已有定向澄清问题(如查询语气的只读确认)
if top.ClarificationQuestion != "" && !slotOrPolicyClarify {
requiresClarifying = true
slotOrPolicyClarify = true
}
// 4. 写/读候选冲突:分差不足时禁止直接执行 Top1
if applyCandidateConflictGate(candidates, top) {
requiresClarifying = true
slotOrPolicyClarify = true
}
// 5. 低置信度且无定向澄清时才归入模糊咨询分支
if top.Confidence < 0.6 && !slotOrPolicyClarify {
branch = BranchAmbiguousFallback
top.Branch = BranchAmbiguousFallback
requiresClarifying = true
}
result := &IntentRecognitionResult{
RawInput: trimmed,
Branch: branch,
TopIntent: top,
Candidates: candidates,
ContextSnapshotID: snapID,
RequiresClarifying: requiresClarifying,
}
finalizeRecognitionResult(result)
if pending != nil {
if plan := buildExpectationPlusNewPlan(ctx, r, trimmed, userCtx, pending, result); plan != nil {
result.IntentPlan = plan
}
}
if result.IntentPlan == nil {
if plan := buildLimitedIntentPlan(ctx, r, trimmed, userCtx, result); plan != nil {
result.IntentPlan = plan
}
}
return result, nil
}
// isSecurityViolation 检查是否命中敏感安全与权限红线
func isSecurityViolation(input string) bool {
lower := strings.ToLower(input)
patterns := []string{
"修改身份", "身份修改", "修改职位", "职位修改", "修改岗位", "岗位修改",
"修改姓名", "姓名修改", "修改名字", "名字修改",
"修改部门", "部门修改",
"修改资质", "资质修改",
"改成批准人", "改成审核人", "改成编制人",
"工号改成", "修改工号", "工号修改",
"修改设置", "修改配置", "系统设置", "系统配置",
"修改网关", "网关配置", "网关密钥", "大模型网关", "模型网关",
"修改密码", "重置密码", "提升权限", "越权",
"修改向量库", "向量库连接", "向量库配置", "修改向量库连接",
"修改知识库连接", "知识库连接", "知识库配置",
}
for _, p := range patterns {
if strings.Contains(lower, p) {
return true
}
}
secRegexes := []string{
`把.*(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质).*(?:修改|改|换|重置|设为|变为)`,
`(?:修改|改|换|重置|设为).*(?:我的)?(?:身份|职位|角色|工号|权限|姓名|名字|部门|资质|网关|向量库|知识库连接|密钥|apikey|api_key)`,
`.*(?:向量库连接|修改向量库|知识库连接|大模型网关密钥|网关密钥).*`,
}
for _, r := range secRegexes {
if matched, _ := regexp.MatchString(r, input); matched {
return true
}
}
return false
}
// isPureUICommand 检查是否为纯前端UI视窗布局与主题控制指令
func isPureUICommand(input string) bool {
patterns := []string{
"打开工具栏", "关闭工具栏",
"折叠侧边栏", "展开侧边栏", "隐藏侧边栏", "显示侧边栏",
"折叠左侧栏", "展开左侧栏", "折叠右侧栏", "展开右侧栏",
"折叠工作台", "展开工作台",
"切换全屏", "全屏显示", "退出全屏",
"打开控制台", "关闭控制台",
"切换主题", "换成深色", "换成浅色",
"打开抽屉", "展开抽屉", "关闭抽屉",
"深色模式", "浅色模式", "深色主题", "浅色主题",
}
for _, p := range patterns {
if strings.Contains(input, p) {
return true
}
}
return false
}
// resolveTargetDoc 优先从输入中提取显式受控文档名称,提取不到时回退至上下文的 ActiveDocID
func resolveTargetDoc(input string, userCtx *ContextSnapshot) string {
if doc := extractDocName(input); doc != "" {
return doc
}
if userCtx != nil && userCtx.ActiveDocID != "" {
return userCtx.ActiveDocID
}
return ""
}
// isRejectReasonQuery 判定是否为询问驳回/退回原因(而非发起驳回执行)
func isRejectReasonQuery(input string) bool {
markers := []string{
"原因", "为何", "为什么", "怎么回事", "什么情况", "什么意见",
"哪点", "哪些问题", "整改", "意见是什么", "理由", "怎么改", "如何修改",
"查一下驳回", "看看驳回",
}
for _, m := range markers {
if strings.Contains(input, m) {
return true
}
}
return strings.Contains(input, "被驳回")
}
// matchDocGovAction 匹配法定受控文档治理流转动作与槽位提取
func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent {
// 0. 询问驳回原因:归属 DECISION_QUERY,不得路由为 DOCGOV_*_REJECT
if (strings.Contains(input, "驳回") || strings.Contains(input, "退回")) && isRejectReasonQuery(input) {
slots := make(map[string]string)
if doc := resolveTargetDoc(input, userCtx); doc != "" {
slots[SlotTargetDoc] = doc
}
return &CandidateIntent{
Intent: IntentQueryRejectReason,
Confidence: 0.96,
FilledSlots: slots,
Reasoning: "用户询问受控文档驳回/退回原因,路由为 QUERY_REJECT_REASON",
}
}
// 1. 驳回 (Review Reject or Final Reject)
if strings.Contains(input, "驳回") {
isFinal := false
if strings.Contains(input, "终审") || strings.Contains(input, "批准") {
isFinal = true
} else if userCtx != nil && (userCtx.DocLifecycleState == "approving" || userCtx.UserDocRole == "approver") {
isFinal = true
}
targetIntent := IntentDocGovReviewReject
if isFinal {
targetIntent = IntentDocGovFinalReject
}
slots := make(map[string]string)
// target_doc
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
// target_section
section := extractSection(input)
if section != "" {
slots[SlotTargetSection] = section
}
// reject_reason
reason := extractRejectReason(input)
if reason != "" {
slots[SlotRejectReason] = reason
}
confidence := 0.95
reasoning := fmt.Sprintf("用户下达法定治理驳回指令,目标意图为 %s", targetIntent)
clarifyQ := ""
// Stage 0 权限与状态前置剪枝校验
if userCtx != nil {
if !isFinal && userCtx.UserDocRole != "" && userCtx.UserDocRole != "reviewer" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是审核人(UserDocRole: %s),权限不符,无法执行审核驳回", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备审核驳回权限(需审核人角色)。", userCtx.UserDocRole)
} else if isFinal && userCtx.UserDocRole != "" && userCtx.UserDocRole != "approver" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是批准人(UserDocRole: %s),权限不符,无法执行终审驳回", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备终审批准驳回权限(需批准人角色)。", userCtx.UserDocRole)
}
if userCtx.DocLifecycleState == "drafting" {
confidence = 0.90
reasoning = "前置剪枝:当前文档处于编制草稿阶段(drafting),尚未提交审核,无法执行驳回操作"
clarifyQ = "当前文档处于编制中(drafting),尚未送审,无需驳回。"
}
}
return &CandidateIntent{
Intent: targetIntent,
Confidence: confidence,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: reasoning,
ClarificationQuestion: clarifyQ,
}
}
// 2. 终审批准 (Final Approve)
if strings.Contains(input, "终审批准") || strings.Contains(input, "批准发布") || strings.Contains(input, "批准生效") ||
strings.Contains(input, "同意发布") || (strings.Contains(input, "批准") && !strings.Contains(input, "不")) {
targetIntent := IntentDocGovFinalApprove
slots := make(map[string]string)
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
confidence := 0.95
reasoning := "用户下达受控文档终审批准生效指令"
clarifyQ := ""
if userCtx != nil {
if userCtx.UserDocRole != "" && userCtx.UserDocRole != "approver" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是批准人(UserDocRole: %s),权限不符,无法执行终审批准", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备终审批准权限(需批准人角色)。", userCtx.UserDocRole)
} else if userCtx.DocLifecycleState != "" && userCtx.DocLifecycleState != "approving" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前文档处于【%s】阶段,尚未进入终审批准阶段(approving),无法执行终审批准", userCtx.DocLifecycleState)
clarifyQ = fmt.Sprintf("当前文档处于【%s】阶段,未进入终审阶段,无法执行终审批准。", userCtx.DocLifecycleState)
}
}
return &CandidateIntent{
Intent: targetIntent,
Confidence: confidence,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: reasoning,
ClarificationQuestion: clarifyQ,
}
}
// 3. 审核通过 (Review Approve)
if strings.Contains(input, "审核通过") || strings.Contains(input, "审阅通过") || strings.Contains(input, "评审通过") ||
(strings.Contains(input, "通过") && (strings.Contains(input, "审核") || strings.Contains(input, "审阅") || strings.Contains(input, "报告") || strings.Contains(input, "准予") || strings.Contains(input, "环节"))) {
if !strings.Contains(input, "下一阶段") && !strings.Contains(input, "推进") {
targetIntent := IntentDocGovReviewApprove
slots := make(map[string]string)
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
confidence := 0.95
reasoning := "用户下达受控文档审核通过指令"
clarifyQ := ""
if userCtx != nil {
if userCtx.UserDocRole != "" && userCtx.UserDocRole != "reviewer" && userCtx.UserDocRole != "system" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是审核人(UserDocRole: %s),权限不符,无法执行审核通过", userCtx.UserDocRole)
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备审核通过权限(需审核人角色)。", userCtx.UserDocRole)
} else if userCtx.DocLifecycleState != "" && userCtx.DocLifecycleState != "reviewing" {
confidence = 0.90
reasoning = fmt.Sprintf("前置剪枝:当前文档处于【%s】阶段,尚未进入审核阶段(reviewing),无法执行审核通过", userCtx.DocLifecycleState)
clarifyQ = fmt.Sprintf("当前文档处于【%s】阶段,未进入审核阶段,无法执行审核通过。", userCtx.DocLifecycleState)
}
}
return &CandidateIntent{
Intent: targetIntent,
Confidence: confidence,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: reasoning,
ClarificationQuestion: clarifyQ,
}
}
}
// 4. 认领文档 (Claim) — 查询语气不得路由为写操作
if strings.Contains(input, "认领") {
if detectDialogueAct(input) == DialogueActAsk || isDocGovStatusQuery(input, "claim") {
slots := make(map[string]string)
if doc := resolveTargetDoc(input, userCtx); doc != "" {
slots[SlotTargetDoc] = doc
}
return &CandidateIntent{
Intent: IntentQueryUserRights,
Confidence: 0.91,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "查询语气下的文档认领状态询问,不得路由为写操作 DOCGOV_CLAIM",
ClarificationQuestion: "您是想查询该文档当前由谁认领负责吗?我将为您检索认领状态,不会执行认领操作。",
}
}
slots := make(map[string]string)
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
return &CandidateIntent{
Intent: IntentDocGovClaim,
Confidence: 0.94,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "用户下达认领文档编制责任人指令",
}
}
// 5. 提交送审 (Submit Review) — 查询语气不得路由为写操作
if strings.Contains(input, "送审") || strings.Contains(input, "提交审核") || strings.Contains(input, "发起审核") || strings.Contains(input, "提审") {
if detectDialogueAct(input) == DialogueActAsk || isDocGovStatusQuery(input, "submit") {
slots := make(map[string]string)
if doc := resolveTargetDoc(input, userCtx); doc != "" {
slots[SlotTargetDoc] = doc
}
return &CandidateIntent{
Intent: IntentQueryRejectReason,
Confidence: 0.91,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "查询语气下的送审/审核状态询问,不得路由为写操作 DOCGOV_SUBMIT_REVIEW",
ClarificationQuestion: "您是想查询该文档的送审或审核状态吗?我将为您检索当前流转状态,不会发起新的送审。",
}
}
slots := make(map[string]string)
targetDoc := resolveTargetDoc(input, userCtx)
if targetDoc != "" {
slots[SlotTargetDoc] = targetDoc
}
return &CandidateIntent{
Intent: IntentDocGovSubmitReview,
Confidence: 0.94,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "用户下达编制完成送交审核指令",
}
}
// 6. 提案裁决 (Proposal Decide)
if strings.Contains(input, "提案") || strings.Contains(input, "修改建议") || strings.Contains(input, "变更建议") {
if strings.Contains(input, "同意") || strings.Contains(input, "采纳") || strings.Contains(input, "拒绝") || strings.Contains(input, "拍板") {
slots := make(map[string]string)
if strings.Contains(input, "拒绝") {
slots[SlotProposalAction] = "reject"
} else {
slots[SlotProposalAction] = "accept"
}
return &CandidateIntent{
Intent: IntentDocGovProposalDecide,
Confidence: 0.93,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: "用户对受控文档的修改提案或批注做出决策裁决",
}
}
}
return nil
}
// matchKnowledgeQuery 匹配法规标准、标杆案例与权限查询
func matchKnowledgeQuery(input string) *CandidateIntent {
// 排除事实参数补充的输入场景
excludeKeywords := []string{"客户说", "客户反馈", "客户补充", "补充说", "补充", "修改为", "改为", "增加"}
for _, ek := range excludeKeywords {
if strings.Contains(input, ek) {
return nil
}
}
// 1. 用户权限查询 (IntentQueryUserRights)
userRightsPatterns := []string{
"我的权限", "我有什么权限", "我负责什么", "我的角色", "查看我的职责",
"查询权限", "当前权限", "我在这个项目的角色", "法定角色", "法定职责", "我的职责",
"文档权限", "文档职责", "权限和职责", "职责和权限", "负责哪些文档", "权限",
}
for _, p := range userRightsPatterns {
if strings.Contains(input, p) {
return &CandidateIntent{
Intent: IntentQueryUserRights,
Confidence: 0.95,
FilledSlots: map[string]string{
SlotQueryTopic: "user_rights",
},
Reasoning: "用户查询自身在项目及受控文档中的法定职责与操作权限",
}
}
}
// 2. 标杆/相似案例查询 (IntentQuerySimilarCases)
similarCasesPatterns := []string{
"参考案例", "往期经验", "类似项目", "相似案例", "同类产品",
"同类项目", "以往案例", "标杆案例", "同类获批", "竞品获批案例",
"注册案例", "申报案例",
}
for _, p := range similarCasesPatterns {
if strings.Contains(input, p) {
return &CandidateIntent{
Intent: IntentQuerySimilarCases,
Confidence: 0.92,
FilledSlots: map[string]string{
SlotQueryTopic: "similar_cases",
},
Reasoning: "用户查询以往类似产品的注册申报、上市审评或标杆案例与经验",
}
}
}
// 3. 法规强标查询 (IntentQueryRegulations)
regKeywords := []string{
"gb 9706", "gb9706", "yy 0505", "yy0505", "强制性标准", "强标",
"技术审评指导原则", "指导原则", "合规要求", "注册标准",
}
lower := strings.ToLower(input)
for _, rk := range regKeywords {
if strings.Contains(lower, rk) {
return &CandidateIntent{
Intent: IntentQueryRegulations,
Confidence: 0.92,
FilledSlots: map[string]string{
SlotQueryTopic: "regulations",
},
Reasoning: "用户咨询医疗器械适用的国家标准、行业强标或审评指导原则法规",
}
}
}
if (strings.Contains(lower, "法规") || strings.Contains(lower, "标准") || strings.Contains(lower, "yy") || strings.Contains(lower, "fda") || strings.Contains(lower, "ce")) &&
(strings.Contains(lower, "查") || strings.Contains(lower, "哪些") || strings.Contains(lower, "满足") || strings.Contains(lower, "要求") || strings.Contains(lower, "规范")) {
return &CandidateIntent{
Intent: IntentQueryRegulations,
Confidence: 0.90,
FilledSlots: map[string]string{
SlotQueryTopic: "regulations",
},
Reasoning: "用户咨询医疗器械适用的国家标准、行业强标或审评指导原则法规",
}
}
return nil
}
// extractDocName 从文本或预设映射中提取目标受控文档名称/标识
func extractDocName(input string) string {
reDocID := regexp.MustCompile(`(doc-[a-zA-Z0-9_-]+)`)
if match := reDocID.FindString(input); match != "" {
return match
}
docNames := []string{
"用户初始需求收集表", "用户初始需求表", "用户需求收集表", "需求收集表", "需求表",
"技术与市场调研报告", "调研报告",
"法规与注册论证报告", "法规评估", "注册评估",
"初步设计与架构方案", "初步设计", "设计方案",
"CDO报价单", "初步报价单", "报价单",
}
for _, name := range docNames {
if strings.Contains(input, name) {
return name
}
}
reBa := regexp.MustCompile(`把\s*(.+?)\s*(?:审核驳回|终审驳回|驳回|审核通过|批准|认领|送审|提交审核)`)
if match := reBa.FindStringSubmatch(input); len(match) > 1 {
return strings.TrimSpace(match[1])
}
return ""
}
// extractRejectReason 从用户输入中提取驳回意见或批注内容
func extractRejectReason(input string) string {
reReason := regexp.MustCompile(`(?:意见为|意见是|意见|理由为|理由是|理由|原因|批注为|批注|修改意见)[::\s]\s*(.+)`)
if match := reReason.FindStringSubmatch(input); len(match) > 1 {
res := strings.TrimSpace(match[1])
return strings.Trim(res, ",,。;; ")
}
reAfterReject := regexp.MustCompile(`驳回[,,::\s]+(.+)`)
if match := reAfterReject.FindStringSubmatch(input); len(match) > 1 {
res := strings.TrimSpace(match[1])
res = regexp.MustCompile(`^(?:意见为|意见是|意见|理由为|理由是|理由|原因|批注为|批注|修改意见)[::\s]\s*`).ReplaceAllString(res, "")
res = strings.Trim(res, ",,。;; ")
if res != "" {
return res
}
}
return ""
}
// extractSection 从文本中抽取目标章节线索 (如 "第3节", "3.1节")
func extractSection(input string) string {
reSection := regexp.MustCompile(`(第[0-9一二三四五六七八九十]+[章节条部分]+|[0-9]+\.[0-9]+[节章]*)`)
if match := reSection.FindString(input); match != "" {
return match
}
return ""
}
// 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 检验是否属于犹豫或征询参谋意见 (分支 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
}
// extractFactSlots 从文本中提取结构化事实槽位 (支持补丁合并与补充信息)
func extractFactSlots(input string) map[string]string {
slots := make(map[string]string)
marketPrefixes := []string{
"目标市场不仅是", "目标市场不仅有", "目标市场还包括", "目标市场确定为", "目标市场确定是",
"目标市场改成", "目标市场改为", "目标市场是", "目标市场为",
"市场改成", "市场改为", "市场是", "市场为", "目标市场除了国内还想考虑",
}
for _, p := range marketPrefixes {
if idx := strings.Index(input, p); idx != -1 {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["target_market"] = val
slots[SlotFactKey] = "target_market"
slots[SlotFactValue] = val
break
}
}
}
budgetPrefixes := []string{"预算改成", "预算改为", "预算是", "预算为", "预算只有", "预算大概", "预算"}
for _, p := range budgetPrefixes {
if idx := strings.Index(input, p); idx != -1 && slots["budget"] == "" {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["budget"] = val
slots[SlotFactKey] = "budget"
slots[SlotFactValue] = val
break
}
}
}
usePrefixes := []string{"用途是", "用途为", "预期用途是", "场景是", "场景为"}
for _, p := range usePrefixes {
if idx := strings.Index(input, p); idx != -1 && slots["intended_use"] == "" {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
slots["intended_use"] = val
slots[SlotFactKey] = "intended_use"
slots[SlotFactValue] = val
break
}
}
}
return slots
}
func matchAdvanceStage(input string, currentStage maturity.MaturityStage) *CandidateIntent {
patterns := []string{
"进入下一阶段", "推进到下一阶段", "推进阶段", "进入方案", "进入论证",
"方案论证", "可行性论证", "开始方案", "进入报价", "开始报价", "商业锁定",
"进入v0.1", "推进到v0.1", "进入v0.5", "推进到v0.5", "进入v1.0", "推进到v1.0", "可以进入",
"没有问题.*进入", "没问题.*推进", "通过.*下一阶段", "开始推进",
"做.*摸底", "开始.*摸底", "先做.*摸底", "进行.*摸底", "做v0.1", "先做v0.1",
"做.*论证", "做.*报价", "开始论证",
}
for _, p := range patterns {
if matched, _ := regexp.MatchString("(?i)"+p, input); matched {
var target maturity.MaturityStage
if strings.Contains(input, "报价") || strings.Contains(input, "v1.0") || strings.Contains(input, "V1.0") || strings.Contains(input, "商业锁定") {
target = maturity.StageV10_Quoting
} else if strings.Contains(input, "方案") || strings.Contains(input, "论证") || strings.Contains(input, "可行性") || strings.Contains(input, "v0.5") || strings.Contains(input, "V0.5") {
target = maturity.StageV05_Feasibility
} else if strings.Contains(input, "摸底") || strings.Contains(input, "初筛") || strings.Contains(input, "v0.1") || strings.Contains(input, "V0.1") {
target = maturity.StageV01_Triage
} else {
if currentStage == maturity.StageV01_Triage {
target = maturity.StageV05_Feasibility
} else if currentStage == maturity.StageV05_Feasibility {
target = maturity.StageV10_Quoting
} else {
target = maturity.StageV10_Quoting
}
}
reason := fmt.Sprintf("匹配推进指令 '%s',目标成熟度阶段为 %s", p, target)
return &CandidateIntent{
Intent: IntentAdvanceStage,
Confidence: 0.92,
FilledSlots: map[string]string{
SlotTargetStage: string(target),
},
Reasoning: reason,
}
}
}
return nil
}
func matchSupplementInfo(input string) *CandidateIntent {
patterns := []string{
"补充", "更新", "客户说", "客户反馈", "客户确定", "修改为", "改为", "增加",
"目标市场", "市场是", "参数是", "用途是", "设备是", "预算是", "时间是", "资料", "附件",
}
matchedAny := false
for _, p := range patterns {
if strings.Contains(input, p) {
matchedAny = true
break
}
}
if !matchedAny {
return nil
}
slots := extractFactSlots(input)
if len(slots) == 0 {
slots = make(map[string]string)
}
slots[SlotSupplementDoc] = input
slots[SlotSupplementAction] = inferSupplementSubtype(input, slots)
return &CandidateIntent{
Intent: IntentSupplementInfo,
Confidence: 0.88,
FilledSlots: slots,
Reasoning: "用户输入中包含参数变更或新材料事实补充",
}
}
func matchQueryProgress(input string) *CandidateIntent {
if matchTriggerTask(input) != nil {
return nil
}
if isWorkflowControlInput(input) || isRunControlInput(input) {
return nil
}
patterns := []string{
"测试方案", "验证方案", "测试", "验证", "型检", "型式检验", "难点", "特别注意", "注意事项", "比较难",
"进展", "进度", "走到哪", "到哪一步", "状态", "情况", "评估到哪",
"风险", "坑", "注意什么", "话术", "怎么说", "怎么跟客户说", "微信", "草稿",
"探头功耗", "功耗", "供电", "怎么设计", "设计方案", "临床要不要", "怎么做", "查一下", "看下",
"成熟度", "汇报一下", "项目怎么样",
}
for _, p := range patterns {
if strings.Contains(input, p) {
topic := "all"
if strings.Contains(input, "测试") || strings.Contains(input, "验证") || strings.Contains(input, "型检") || strings.Contains(input, "难点") || strings.Contains(input, "特别注意") || strings.Contains(input, "注意事项") || strings.Contains(input, "比较难") {
topic = "testing_analysis"
} else if strings.Contains(input, "风险") || strings.Contains(input, "坑") {
topic = "risk"
} else if strings.Contains(input, "话术") || strings.Contains(input, "微信") || strings.Contains(input, "怎么说") {
topic = "talk_script"
} else if strings.Contains(input, "进展") || strings.Contains(input, "进度") || strings.Contains(input, "步") {
topic = "progress"
} else if strings.Contains(input, "功耗") || strings.Contains(input, "供电") || strings.Contains(input, "设计") {
topic = "technical_design"
}
confidence := 0.90
if topic == "testing_analysis" {
confidence = 0.95
}
return &CandidateIntent{
Intent: IntentQueryProgress,
Confidence: confidence,
FilledSlots: map[string]string{
SlotQueryTopic: topic,
},
Reasoning: fmt.Sprintf("用户在询问项目状态、技术测试方案或对外沟通话术 (主题: %s)", topic),
}
}
}
return nil
}
func hasTaskWriteVerb(input string) bool {
for _, verb := range []string{"生成", "做", "跑", "出", "编写", "编制", "起草"} {
if strings.Contains(input, verb) {
return true
}
}
return false
}
func matchTriggerTask(input string) *CandidateIntent {
type phrase struct {
text string
key string
}
taskMap := map[string]string{
"需求收集表": "intake",
"需求表": "intake",
"调研报告": "research",
"调研": "research",
"法规评估": "feasibility_reg",
"注册评估": "feasibility_reg",
"初步设计": "prelim_design",
"设计方案": "prelim_design",
"报价单": "cdo_quote",
"报价": "cdo_quote",
}
phrases := make([]phrase, 0, len(taskMap)+60)
for kw, tKey := range taskMap {
phrases = append(phrases, phrase{text: kw, key: tKey})
}
for _, item := range squad.RDevTriggerPhrases() {
phrases = append(phrases, phrase{text: item.Text, key: item.TaskKey})
}
sort.Slice(phrases, func(i, j int) bool {
li := utf8.RuneCountInString(phrases[i].text)
lj := utf8.RuneCountInString(phrases[j].text)
if li != lj {
return li > lj
}
return phrases[i].text < phrases[j].text
})
if !hasTaskWriteVerb(input) {
return nil
}
// 「初始 / 首轮 / 摸底」是 V0.1 摸底,不能被更短的研发别名「用户需求」截走。
if hasIntakeSemanticMarker(input) && strings.Contains(input, "需求") {
return triggerTaskCandidate("需求表", "intake")
}
for _, item := range phrases {
if strings.Contains(input, item.text) {
return triggerTaskCandidate(item.text, item.key)
}
}
return nil
}
func hasIntakeSemanticMarker(input string) bool {
return strings.Contains(input, "首轮") ||
strings.Contains(input, "初始") ||
strings.Contains(input, "需求摸底") ||
strings.Contains(input, "摸底")
}
func triggerTaskCandidate(phrase, taskKey string) *CandidateIntent {
return &CandidateIntent{
Intent: IntentTriggerTask,
Confidence: 0.86,
FilledSlots: map[string]string{
SlotTargetTask: taskKey,
},
Reasoning: fmt.Sprintf("用户要求显式生成交付任务: %s (%s)", phrase, taskKey),
}
}
func isGreetingOrHelp(input string) bool {
greetings := []string{"你好", "您好", "早上好", "下午好", "晚上好", "早啊", "在吗", "hi", "hello", "你是谁"}
for _, g := range greetings {
if strings.Contains(strings.ToLower(input), g) {
return true
}
}
return false
}
func matchImpactIntent(input string) *CandidateIntent {
propose := strings.Contains(input, "修订提案") || strings.Contains(input, "生成修订") || strings.Contains(input, "提议修改文档")
analyze := strings.Contains(input, "分析影响") || strings.Contains(input, "影响分析") || strings.Contains(input, "波及哪些") ||
strings.Contains(input, "影响哪些") || strings.Contains(input, "改动影响") || strings.Contains(input, "变更影响")
if !propose && !analyze {
return nil
}
intent := IntentImpactAnalyze
if propose {
intent = IntentRevisePropose
}
return &CandidateIntent{
Intent: intent,
Confidence: 0.9,
FilledSlots: map[string]string{},
}
}
func sortCandidates(c []CandidateIntent) {
for i := 0; i < len(c)-1; i++ {
for j := i + 1; j < len(c); j++ {
if c[j].Confidence > c[i].Confidence {
c[i], c[j] = c[j], c[i]
}
}
}
}