Files
Agent-System/internal/intent/recognizer_matchers_task.go
T
WeiCHEN dca33c705d refactor(boundaries): split five oversized packages by use-case
Move bizdev/freedoc/intent/projectworkflow/squad giant files into
≤800-line use-case siblings without behavior change; short tests green.
2026-09-23 09:52:16 +08:00

362 lines
12 KiB
Go

package intent
import (
"fmt"
"regexp"
"sort"
"strings"
"unicode/utf8"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/squad"
)
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) != 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
}
// hasWeakTaskWriteVerb 弱写动词:倾向整理/梳理,默认需确认后再启动正式任务。
func hasWeakTaskWriteVerb(input string) bool {
for _, verb := range []string{"整理", "梳理", "汇总", "归纳", "收集"} {
if strings.Contains(input, verb) {
return true
}
}
return false
}
func isRequirementsTopic(input string) bool {
if strings.Contains(input, "需求") || strings.Contains(input, "摸底") {
return true
}
return strings.Contains(input, "用户") && (hasTaskWriteVerb(input) || hasWeakTaskWriteVerb(input))
}
// hasURSStrongMarker URS / 用户需求规格书级强标记(非「初始/摸底」时才算)。
func hasURSStrongMarker(input string) bool {
if hasIntakeSemanticMarker(input) {
return false
}
return strings.Contains(input, "URS") ||
strings.Contains(input, "用户需求规格") ||
strings.Contains(input, "用户需求表")
}
// stageAwareRequirementsTask 按当前阶段消解需求类任务键,与前端 resolveRequirementsTaskKey 对齐:
// V0.1 / 空阶段且无 URS 强标记 → intake;含 URS 强标记 → user_requirements。
func stageAwareRequirementsTask(input string, userCtx *ContextSnapshot) string {
if hasIntakeSemanticMarker(input) {
return "intake"
}
stage := ""
if userCtx != nil {
stage = userCtx.CurrentStage
}
if hasURSStrongMarker(input) {
return "user_requirements"
}
// 含糊「需求」在初期默认摸底表,避免误触 V0.5 门禁失败。
if stage == "" || stage == string(maturity.StageV01_Triage) {
return "intake"
}
if strings.Contains(input, "用户需求") || strings.Contains(input, "需求表") {
return "user_requirements"
}
return "intake"
}
func requirementsTriggerPhrase(taskKey string) string {
switch taskKey {
case "user_requirements":
return "用户需求表"
default:
return "用户初始需求收集表"
}
}
func matchTriggerTask(input string, userCtx *ContextSnapshot) *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
})
strong := hasTaskWriteVerb(input)
weak := hasWeakTaskWriteVerb(input)
if !strong && !weak {
return nil
}
// 需求类话题优先阶段感知消解,避免研发短别名「用户需求」截走商务流初期表述。
if isRequirementsTopic(input) {
taskKey := stageAwareRequirementsTask(input, userCtx)
phrase := requirementsTriggerPhrase(taskKey)
if cand := scopedTriggerTaskCandidate(input, phrase, taskKey, userCtx); cand != nil {
return cand
}
cand := triggerTaskCandidate(phrase, taskKey)
if !strong && weak {
cand.Confidence = 0.78
cand.FilledSlots[SlotNeedsConfirmation] = "true"
cand.Reasoning = fmt.Sprintf("弱写动词触发需求整理,阶段感知消解为 %s,需用户确认后执行", taskKey)
}
return cand
}
if !strong {
// 弱写动词仅覆盖需求类宾语;其他话题仍走对话兜底。
return nil
}
if workflowScopeUsesWeights() {
flat := make([]struct{ text, key string }, 0, len(phrases))
for _, item := range phrases {
flat = append(flat, struct{ text, key string }{text: item.text, key: item.key})
}
return weightedTriggerTask(input, flat, userCtx)
}
for _, item := range phrases {
if strings.Contains(input, item.text) {
if cand := scopedTriggerTaskCandidate(input, item.text, item.key, userCtx); cand != nil {
return cand
}
return triggerTaskCandidate(item.text, item.key)
}
}
return nil
}
// scopedTriggerTaskCandidate 若目标任务键不属于当前激活工作流,返回跨流拦截候选;
// 无激活工作流(存量单流 / planning 态)或归属未知时返回 nil,维持原路由。
func scopedTriggerTaskCandidate(input, phrase, taskKey string, userCtx *ContextSnapshot) *CandidateIntent {
active := ""
if userCtx != nil {
active = userCtx.ActiveWorkflowKey
}
owners, ok := docScopeForPhrase(phrase)
if !ok {
if owner := taskKeyWorkflowOwner(taskKey); owner != "" {
owners = []string{owner}
ok = true
}
}
if !ok || active == "" || containsWorkflow(owners, active) {
return nil
}
return workflowScopeCandidate(phrase, owners, active)
}
func workflowScopeCandidate(phrase string, owners []string, activeWorkflow string) *CandidateIntent {
slots := make(map[string]string)
markWorkflowScopeMismatch(slots, phrase, owners)
return &CandidateIntent{
Intent: IntentTriggerTask,
Confidence: 0.86,
Branch: BranchNewIntentPreempt,
FilledSlots: slots,
Reasoning: fmt.Sprintf("用户要求生成交付任务 %s,但该文档属于非激活工作流,前置剪枝", phrase),
ClarificationQuestion: workflowScopeGuidanceQuestion(phrase, owners, activeWorkflow),
}
}
func containsWorkflow(workflows []string, key string) bool {
for _, wf := range workflows {
if wf == key {
return true
}
}
return false
}
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 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{},
}
}