feat(intent): implement 4-stage hierarchical intent funnel and slot extraction
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
9e84ba71e3
commit
85e5c31034
+464
-21
@@ -14,7 +14,7 @@ type Recognizer interface {
|
||||
Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error)
|
||||
}
|
||||
|
||||
// RuleBasedRecognizer 规则与上下文约束结合的三分支柔性意图消解器
|
||||
// RuleBasedRecognizer 规则与上下文约束结合的四级漏斗意图消解引擎
|
||||
type RuleBasedRecognizer struct{}
|
||||
|
||||
func NewRecognizer() Recognizer {
|
||||
@@ -52,7 +52,49 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段一:若输入明确是咨询参谋/犹豫(如“这个先放放,你觉得呢”),优先归入分支 3 (参谋主动分忧)
|
||||
// 阶段零 (Stage 0):态势前置过滤与安全防护检查 (Security & UI Guard)
|
||||
// -------------------------------------------------------------
|
||||
// 1. 安全红线校验:严禁通过自然语言对话修改系统受控配置、用户法定身份/职位/工号
|
||||
if isSecurityViolation(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),触发安全红线拦截",
|
||||
}
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: trimmed,
|
||||
Branch: BranchNewIntentPreempt,
|
||||
TopIntent: top,
|
||||
Candidates: []CandidateIntent{*top},
|
||||
ContextSnapshotID: snapID,
|
||||
RequiresClarifying: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 2. 纯 UI 视窗控制指令拦截:归入 CASUAL_CHAT 并给出不可执行提示
|
||||
if isPureUICommand(trimmed) {
|
||||
top := &CandidateIntent{
|
||||
Intent: IntentCasualChat,
|
||||
Branch: BranchAmbiguousFallback,
|
||||
Confidence: 0.90,
|
||||
FilledSlots: make(map[string]string),
|
||||
Reasoning: "纯UI视窗控制指令,系统暂不支持通过对话直接操作界面视窗布局,请通过界面按钮或快捷键操作",
|
||||
ClarificationQuestion: "检测到您下达了界面视窗控制指令(如折叠/打开侧边栏)。该操作属于前端UI视窗布局控制,请直接点击界面右上角或侧边的视窗切换按钮。",
|
||||
}
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: trimmed,
|
||||
Branch: BranchAmbiguousFallback,
|
||||
TopIntent: top,
|
||||
Candidates: []CandidateIntent{*top},
|
||||
ContextSnapshotID: snapID,
|
||||
RequiresClarifying: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段 0.5:若输入明确是咨询参谋/犹豫(如“这个先放放,你觉得呢”),优先归入分支 3 (参谋主动分忧)
|
||||
// -------------------------------------------------------------
|
||||
if isConsultativeOrHesitant(trimmed) {
|
||||
res := buildAmbiguousConsultResult(trimmed, snapID, pending)
|
||||
@@ -60,7 +102,7 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段二:分支 1 判定——命中历史轮次挂起预期 (肯定 / 选项序号 / 复合带补丁 / 明确撤销)
|
||||
// 阶段 0.8:分支 1 判定——命中历史轮次挂起预期 (肯定 / 选项序号 / 复合带补丁 / 明确撤销)
|
||||
// -------------------------------------------------------------
|
||||
if pending != nil {
|
||||
if hitCand := matchExpectationHit(trimmed, pending); hitCand != nil {
|
||||
@@ -75,35 +117,50 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段三:分支 2 判定——独立新意图抢占 (用户新需求最高优先级)
|
||||
// 阶段一与阶段二 (Stage 1 & Stage 2):领域大类路由、微观细分动作与槽位提取
|
||||
// -------------------------------------------------------------
|
||||
var candidates []CandidateIntent
|
||||
|
||||
// 1. 检验 ADVANCE_STAGE (推进阶段)
|
||||
// 领域 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)
|
||||
}
|
||||
|
||||
// 2. 检验 SUPPLEMENT_INFO (补充信息/参数变更)
|
||||
// 3.2 补充信息/参数事实 (SUPPLEMENT_INFO)
|
||||
if cand := matchSupplementInfo(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 3. 检验 QUERY_PROGRESS (查询进度/技术咨询/参谋诊断/话术)
|
||||
if cand := matchQueryProgress(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 4. 检验 TRIGGER_TASK (特定任务触发)
|
||||
// 3.3 交付任务触发 (TRIGGER_TASK)
|
||||
if cand := matchTriggerTask(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 5. 检验 CASUAL_CHAT (寒暄/通用求助)
|
||||
// 领域 4: 进度与技术研判查询 (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,
|
||||
@@ -121,11 +178,27 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
top := &candidates[0]
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段四:分支 3 判定——模糊/无实际意义兜底 (置信度不足或需澄清)
|
||||
// 阶段三 (Stage 3):槽位完备性与置信度裁决 (门禁裁决与追问下发)
|
||||
// -------------------------------------------------------------
|
||||
branch := BranchNewIntentPreempt
|
||||
requiresClarifying := false
|
||||
if top.Confidence < 0.6 || len(top.MissingSlots) > 0 {
|
||||
|
||||
// 1. 若为 DOCGOV_REVIEW_REJECT,检查必填槽位 reject_reason
|
||||
if top.Intent == IntentDocGovReviewReject {
|
||||
if top.FilledSlots[SlotRejectReason] == "" {
|
||||
requiresClarifying = true
|
||||
top.ClarificationQuestion = "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
|
||||
top.MissingSlots = []string{SlotRejectReason}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 若存在权限不符或前置剪枝阻断,转入澄清引导
|
||||
if strings.Contains(top.Reasoning, "权限不符") || strings.Contains(top.Reasoning, "前置剪枝") {
|
||||
requiresClarifying = true
|
||||
}
|
||||
|
||||
// 3. 低置信度或缺失必填槽位兜底
|
||||
if top.Confidence < 0.6 || len(top.MissingSlots) > 0 || requiresClarifying {
|
||||
branch = BranchAmbiguousFallback
|
||||
top.Branch = BranchAmbiguousFallback
|
||||
requiresClarifying = true
|
||||
@@ -141,6 +214,381 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}, 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{
|
||||
`把.*(?:身份|职位|角色|工号|权限).*(?:修改|改|换)`,
|
||||
`(?:修改|改|换).*(?:身份|职位|角色|工号|网关)`,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// matchDocGovAction 匹配法定受控文档治理流转动作与槽位提取
|
||||
func matchDocGovAction(input string, userCtx *ContextSnapshot) *CandidateIntent {
|
||||
// 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 := ""
|
||||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||||
targetDoc = userCtx.ActiveDocID
|
||||
} else {
|
||||
targetDoc = extractDocName(input)
|
||||
}
|
||||
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.55
|
||||
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.55
|
||||
reasoning = fmt.Sprintf("前置剪枝:当前用户角色不是批准人(UserDocRole: %s),权限不符,无法执行终审驳回", userCtx.UserDocRole)
|
||||
clarifyQ = fmt.Sprintf("您当前在受控文档中的角色为【%s】,不具备终审批准驳回权限(需批准人角色)。", userCtx.UserDocRole)
|
||||
}
|
||||
|
||||
if userCtx.DocLifecycleState == "drafting" {
|
||||
confidence = 0.55
|
||||
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 := ""
|
||||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||||
targetDoc = userCtx.ActiveDocID
|
||||
} else {
|
||||
targetDoc = extractDocName(input)
|
||||
}
|
||||
if targetDoc != "" {
|
||||
slots[SlotTargetDoc] = targetDoc
|
||||
}
|
||||
|
||||
return &CandidateIntent{
|
||||
Intent: targetIntent,
|
||||
Confidence: 0.95,
|
||||
Branch: BranchNewIntentPreempt,
|
||||
FilledSlots: slots,
|
||||
Reasoning: "用户下达受控文档终审批准生效指令",
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := ""
|
||||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||||
targetDoc = userCtx.ActiveDocID
|
||||
} else {
|
||||
targetDoc = extractDocName(input)
|
||||
}
|
||||
if targetDoc != "" {
|
||||
slots[SlotTargetDoc] = targetDoc
|
||||
}
|
||||
|
||||
return &CandidateIntent{
|
||||
Intent: targetIntent,
|
||||
Confidence: 0.95,
|
||||
Branch: BranchNewIntentPreempt,
|
||||
FilledSlots: slots,
|
||||
Reasoning: "用户下达受控文档审核通过指令",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 认领文档 (Claim)
|
||||
if strings.Contains(input, "认领") {
|
||||
slots := make(map[string]string)
|
||||
targetDoc := ""
|
||||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||||
targetDoc = userCtx.ActiveDocID
|
||||
} else {
|
||||
targetDoc = extractDocName(input)
|
||||
}
|
||||
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, "提审") {
|
||||
slots := make(map[string]string)
|
||||
targetDoc := ""
|
||||
if userCtx != nil && userCtx.ActiveDocID != "" {
|
||||
targetDoc = userCtx.ActiveDocID
|
||||
} else {
|
||||
targetDoc = extractDocName(input)
|
||||
}
|
||||
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 {
|
||||
// 1. 否定/暂缓意图
|
||||
@@ -228,7 +676,6 @@ func matchOptionIndex(input string, options []ExpectationOption) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 匹配纯数字或带前缀
|
||||
patterns := []struct {
|
||||
regex string
|
||||
idx int
|
||||
@@ -244,7 +691,6 @@ func matchOptionIndex(input string, options []ExpectationOption) int {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否匹配 OptionID
|
||||
for idx, opt := range options {
|
||||
if strings.EqualFold(opt.OptionID, input) {
|
||||
return idx
|
||||
@@ -303,7 +749,6 @@ func buildAmbiguousConsultResult(input, snapID string, pending *PendingExpectati
|
||||
func extractFactSlots(input string) map[string]string {
|
||||
slots := make(map[string]string)
|
||||
|
||||
// 目标市场
|
||||
marketPrefixes := []string{
|
||||
"目标市场不仅是", "目标市场不仅有", "目标市场还包括", "目标市场确定为", "目标市场确定是",
|
||||
"目标市场改成", "目标市场改为", "目标市场是", "目标市场为",
|
||||
@@ -322,7 +767,6 @@ func extractFactSlots(input string) map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
// 预算
|
||||
budgetPrefixes := []string{"预算改成", "预算改为", "预算是", "预算为", "预算只有", "预算大概", "预算"}
|
||||
for _, p := range budgetPrefixes {
|
||||
if idx := strings.Index(input, p); idx != -1 && slots["budget"] == "" {
|
||||
@@ -337,7 +781,6 @@ func extractFactSlots(input string) map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
// 预期用途 / 使用场景
|
||||
usePrefixes := []string{"用途是", "用途为", "预期用途是", "场景是", "场景为"}
|
||||
for _, p := range usePrefixes {
|
||||
if idx := strings.Index(input, p); idx != -1 && slots["intended_use"] == "" {
|
||||
|
||||
@@ -2,6 +2,7 @@ package intent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -284,3 +285,210 @@ func TestThreeBranch_Branch3_AmbiguousConsult(t *testing.T) {
|
||||
t.Errorf("expected non-empty clarification question for branch 3")
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// 四级漏斗意图消解与槽位提取测试集 (Task 2)
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
||||
func TestIntentRecognition_ReviewReject(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_rej_1",
|
||||
CurrentStage: string(bizdev.StageV05_Feasibility),
|
||||
ActiveDocID: "doc-002",
|
||||
DocLifecycleState: "reviewing",
|
||||
UserDocRole: "reviewer",
|
||||
}
|
||||
|
||||
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
||||
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotRejectReason] == "" {
|
||||
t.Errorf("expected filled slot reject_reason")
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotTargetDoc] == "" {
|
||||
t.Errorf("expected filled slot target_doc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_ReviewReject_UnauthorizedRole(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_rej_2",
|
||||
CurrentStage: string(bizdev.StageV05_Feasibility),
|
||||
ActiveDocID: "doc-002",
|
||||
DocLifecycleState: "reviewing",
|
||||
UserDocRole: "author", // 非审核人
|
||||
}
|
||||
|
||||
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回,意见为:第3节竞品对比缺少史赛克高频电刀的抗扰度指标", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(res.TopIntent.Reasoning, "权限不符") {
|
||||
t.Errorf("expected reasoning to contain '权限不符', got %s", res.TopIntent.Reasoning)
|
||||
}
|
||||
if !res.RequiresClarifying {
|
||||
t.Errorf("expected RequiresClarifying to be true for unauthorized role")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_ReviewReject_MissingReason(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_rej_3",
|
||||
CurrentStage: string(bizdev.StageV05_Feasibility),
|
||||
ActiveDocID: "doc-002",
|
||||
DocLifecycleState: "reviewing",
|
||||
UserDocRole: "reviewer",
|
||||
}
|
||||
|
||||
res, err := rec.Recognize(context.Background(), "把调研报告审核驳回", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentDocGovReviewReject {
|
||||
t.Errorf("expected intent DOCGOV_REVIEW_REJECT, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if !res.RequiresClarifying {
|
||||
t.Errorf("expected RequiresClarifying to be true when reject_reason is missing")
|
||||
}
|
||||
expectedClarify := "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。"
|
||||
if res.TopIntent.ClarificationQuestion != expectedClarify {
|
||||
t.Errorf("expected clarification question '%s', got '%s'", expectedClarify, res.TopIntent.ClarificationQuestion)
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotRejectReason] != "" {
|
||||
t.Errorf("expected empty reject_reason slot, got %s", res.TopIntent.FilledSlots[intent.SlotRejectReason])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_SecurityForbidden(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "帮我把身份修改为研发总监,工号改成SMDF-001", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentSecurityForbidden {
|
||||
t.Errorf("expected intent SECURITY_FORBIDDEN, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.Confidence != 1.0 {
|
||||
t.Errorf("expected confidence 1.0 for security violations, got %v", res.TopIntent.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_QueryRegulations(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "微创手术高频电刀需要满足哪些 GB 9706 和 YY 强制性标准?", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentQueryRegulations {
|
||||
t.Errorf("expected intent QUERY_REGULATIONS, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.Confidence < 0.8 {
|
||||
t.Errorf("expected confidence >= 0.8, got %v", res.TopIntent.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_QuerySimilarCases(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "参考一下往期同类腹腔镜系统的注册案例和经验", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentQuerySimilarCases {
|
||||
t.Errorf("expected intent QUERY_SIMILAR_CASES, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_QueryUserRights(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "查询一下我在当前项目的权限和法定角色", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentQueryUserRights {
|
||||
t.Errorf("expected intent QUERY_USER_RIGHTS, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_PureUICommand(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "请帮我折叠侧边栏", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.TopIntent.Intent != intent.IntentCasualChat {
|
||||
t.Errorf("expected intent CASUAL_CHAT for pure UI command, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if !strings.Contains(res.TopIntent.Reasoning, "UI") && !strings.Contains(res.TopIntent.ClarificationQuestion, "UI") {
|
||||
t.Errorf("expected reasoning or clarification question to mention UI, got reasoning: %s", res.TopIntent.Reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_DocGov_OtherActions(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
ActiveDocID: "doc-001",
|
||||
DocLifecycleState: "drafting",
|
||||
UserDocRole: "author",
|
||||
}
|
||||
|
||||
// 认领
|
||||
resClaim, err := rec.Recognize(context.Background(), "我来认领这份需求收集表", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize claim failed: %v", err)
|
||||
}
|
||||
if resClaim.TopIntent.Intent != intent.IntentDocGovClaim {
|
||||
t.Errorf("expected intent DOCGOV_CLAIM, got %s", resClaim.TopIntent.Intent)
|
||||
}
|
||||
|
||||
// 送审
|
||||
resSubmit, err := rec.Recognize(context.Background(), "需求收集表编制完成,提交审核", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize submit failed: %v", err)
|
||||
}
|
||||
if resSubmit.TopIntent.Intent != intent.IntentDocGovSubmitReview {
|
||||
t.Errorf("expected intent DOCGOV_SUBMIT_REVIEW, got %s", resSubmit.TopIntent.Intent)
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
ctxReviewer := &intent.ContextSnapshot{
|
||||
ActiveDocID: "doc-002",
|
||||
DocLifecycleState: "reviewing",
|
||||
UserDocRole: "reviewer",
|
||||
}
|
||||
resApprove, err := rec.Recognize(context.Background(), "调研报告审核通过,同意进入下一环节", ctxReviewer)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize approve failed: %v", err)
|
||||
}
|
||||
if resApprove.TopIntent.Intent != intent.IntentDocGovReviewApprove {
|
||||
t.Errorf("expected intent DOCGOV_REVIEW_APPROVE, got %s", resApprove.TopIntent.Intent)
|
||||
}
|
||||
|
||||
// 终审批准
|
||||
ctxApprover := &intent.ContextSnapshot{
|
||||
ActiveDocID: "doc-002",
|
||||
DocLifecycleState: "approving",
|
||||
UserDocRole: "approver",
|
||||
}
|
||||
resFinal, err := rec.Recognize(context.Background(), "终审批准生效", ctxApprover)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize final approve failed: %v", err)
|
||||
}
|
||||
if resFinal.TopIntent.Intent != intent.IntentDocGovFinalApprove {
|
||||
t.Errorf("expected intent DOCGOV_FINAL_APPROVE, got %s", resFinal.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user