Move bizdev/freedoc/intent/projectworkflow/squad giant files into ≤800-line use-case siblings without behavior change; short tests green.
362 lines
12 KiB
Go
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{},
|
|
}
|
|
}
|