Add SWITCH/TRIGGER/QUERY workflow, doc status, help and unsupported intents with ActionSpec registry entries, NL matchers, L2 handlers with creator-only switch gate, router routes, and capability-aware AvailableActions. Co-authored-by: Cursor <cursoragent@cursor.com>
264 lines
7.6 KiB
Go
264 lines
7.6 KiB
Go
package intent
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
var workflowAliasMap = map[string]string{
|
||
"产品经理": "product_manager",
|
||
"智能产品经理": "product_manager",
|
||
"产品工作流": "product_manager",
|
||
"产品经理工作流": "product_manager",
|
||
"urs": "product_manager",
|
||
"需求规格": "product_manager",
|
||
"测试经理": "test_manager",
|
||
"智能测试经理": "test_manager",
|
||
"测试工作流": "test_manager",
|
||
"测试经理工作流": "test_manager",
|
||
"验证方案": "test_manager",
|
||
"主工作流": "main",
|
||
"默认工作流": "main",
|
||
"main": "main",
|
||
}
|
||
|
||
// extractWorkflowKey 从输入中解析工作流键
|
||
func extractWorkflowKey(input string) string {
|
||
lower := strings.ToLower(input)
|
||
for phrase, key := range workflowAliasMap {
|
||
if strings.Contains(lower, strings.ToLower(phrase)) || strings.Contains(input, phrase) {
|
||
return key
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func isWorkflowControlInput(input string) bool {
|
||
if strings.Contains(input, "工作流") {
|
||
return true
|
||
}
|
||
return extractWorkflowKey(input) != ""
|
||
}
|
||
|
||
func matchSwitchWorkflow(input string, snap *ContextSnapshot) *CandidateIntent {
|
||
act := detectDialogueAct(input)
|
||
if act == DialogueActAsk {
|
||
return nil
|
||
}
|
||
if !isWorkflowControlInput(input) {
|
||
return nil
|
||
}
|
||
switchMarkers := []string{"切换", "换成", "改用", "激活", "切到", "切换到", "切换至"}
|
||
hasSwitch := false
|
||
for _, m := range switchMarkers {
|
||
if strings.Contains(input, m) {
|
||
hasSwitch = true
|
||
break
|
||
}
|
||
}
|
||
if !hasSwitch {
|
||
return nil
|
||
}
|
||
wfKey := extractWorkflowKey(input)
|
||
slots := map[string]string{}
|
||
if wfKey != "" {
|
||
slots[SlotTargetWorkflow] = wfKey
|
||
}
|
||
reasoning := "用户请求切换项目激活工作流"
|
||
if wfKey == "" {
|
||
return &CandidateIntent{
|
||
Intent: IntentSwitchWorkflow,
|
||
Confidence: 0.88,
|
||
FilledSlots: slots,
|
||
Reasoning: reasoning,
|
||
ClarificationQuestion: "请说明要切换到哪个工作流:产品经理(product_manager)、测试经理(test_manager)还是主工作流(main)?",
|
||
}
|
||
}
|
||
return &CandidateIntent{
|
||
Intent: IntentSwitchWorkflow,
|
||
Confidence: 0.93,
|
||
FilledSlots: slots,
|
||
Reasoning: reasoning + " → " + wfKey,
|
||
}
|
||
}
|
||
|
||
func matchTriggerWorkflow(input string, snap *ContextSnapshot) *CandidateIntent {
|
||
if detectDialogueAct(input) == DialogueActAsk {
|
||
return nil
|
||
}
|
||
if !isWorkflowControlInput(input) {
|
||
return nil
|
||
}
|
||
runMarkers := []string{"运行", "启动", "执行", "跑一下", "跑一版", "开始跑"}
|
||
hasRun := false
|
||
for _, m := range runMarkers {
|
||
if strings.Contains(input, m) {
|
||
hasRun = true
|
||
break
|
||
}
|
||
}
|
||
wfKey := extractWorkflowKey(input)
|
||
if !hasRun && wfKey == "" {
|
||
return nil
|
||
}
|
||
if !hasRun && wfKey != "" {
|
||
// 「生成 URS」类可能走 TRIGGER_TASK,不强行 workflow run
|
||
if strings.Contains(input, "切换") {
|
||
return nil
|
||
}
|
||
}
|
||
slots := map[string]string{}
|
||
if wfKey != "" {
|
||
slots[SlotTargetWorkflow] = wfKey
|
||
} else if snap != nil && snap.AvailableActions != nil {
|
||
// 默认当前上下文工作流由 L2 解析
|
||
for _, a := range snap.AvailableActions {
|
||
if strings.HasPrefix(a, "TRIGGER_WORKFLOW:") {
|
||
slots[SlotTargetWorkflow] = strings.TrimPrefix(a, "TRIGGER_WORKFLOW:")
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if wfKey == "" && slots[SlotTargetWorkflow] == "" {
|
||
return &CandidateIntent{
|
||
Intent: IntentTriggerWorkflow,
|
||
Confidence: 0.86,
|
||
FilledSlots: slots,
|
||
Reasoning: "用户请求运行工作流但未指定目标",
|
||
ClarificationQuestion: "请说明要运行哪个工作流:产品经理(product_manager)还是测试经理(test_manager)?",
|
||
}
|
||
}
|
||
return &CandidateIntent{
|
||
Intent: IntentTriggerWorkflow,
|
||
Confidence: 0.91,
|
||
FilledSlots: slots,
|
||
Reasoning: "用户请求运行/启动项目工作流",
|
||
}
|
||
}
|
||
|
||
func matchQueryWorkflowStatus(input string, snap *ContextSnapshot) *CandidateIntent {
|
||
if detectDialogueAct(input) != DialogueActAsk {
|
||
return nil
|
||
}
|
||
markers := []string{
|
||
"当前工作流", "哪个工作流", "什么工作流", "激活的工作流", "激活的是哪个",
|
||
"有哪些工作流", "可用工作流", "工作流列表", "工作流状态",
|
||
}
|
||
hasWF := strings.Contains(input, "工作流")
|
||
matched := false
|
||
for _, m := range markers {
|
||
if strings.Contains(input, m) {
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
if !matched && !(hasWF && (strings.Contains(input, "吗") || strings.Contains(input, "?") || strings.Contains(input, "?"))) {
|
||
return nil
|
||
}
|
||
return &CandidateIntent{
|
||
Intent: IntentQueryWorkflowStatus,
|
||
Confidence: 0.92,
|
||
FilledSlots: map[string]string{
|
||
SlotQueryTopic: "workflow_status",
|
||
},
|
||
Reasoning: "用户查询当前或可用工作流状态",
|
||
}
|
||
}
|
||
|
||
func matchQueryDocStatus(input string, snap *ContextSnapshot) *CandidateIntent {
|
||
if detectDialogueAct(input) != DialogueActAsk {
|
||
return nil
|
||
}
|
||
// 已由 DOCGOV 状态查询覆盖的认领/送审不重复
|
||
if strings.Contains(input, "认领") || strings.Contains(input, "送审") {
|
||
return nil
|
||
}
|
||
markers := []string{
|
||
"文档状态", "审核状态", "什么状态", "当前状态", "版本状态",
|
||
"血缘", "stale", "过期原因", "完成了吗", "生成完了吗",
|
||
}
|
||
matched := false
|
||
for _, m := range markers {
|
||
if strings.Contains(input, m) {
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
if !matched {
|
||
return nil
|
||
}
|
||
slots := map[string]string{SlotQueryTopic: "doc_status"}
|
||
if snap != nil && snap.ActiveDocID != "" {
|
||
slots[SlotTargetDoc] = snap.ActiveDocID
|
||
}
|
||
return &CandidateIntent{
|
||
Intent: IntentQueryDocStatus,
|
||
Confidence: 0.90,
|
||
FilledSlots: slots,
|
||
Reasoning: "用户查询受控文档或交付物状态/血缘(只读)",
|
||
}
|
||
}
|
||
|
||
func matchHelpCapabilities(input string) *CandidateIntent {
|
||
helpMarkers := []string{
|
||
"能做什么", "可以做什么", "有什么功能", "怎么用", "如何使用",
|
||
"帮助", "使用说明", "能力清单", "支持哪些", "你会什么",
|
||
}
|
||
for _, m := range helpMarkers {
|
||
if strings.Contains(input, m) {
|
||
return &CandidateIntent{
|
||
Intent: IntentHelpCapabilities,
|
||
Confidence: 0.94,
|
||
FilledSlots: map[string]string{
|
||
SlotQueryTopic: "help",
|
||
},
|
||
Reasoning: "用户询问系统当前可执行能力",
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func matchUnsupportedActionCandidate(input string) *CandidateIntent {
|
||
unsupportedMarkers := []string{
|
||
"下载回复", "下载下来", "复制回复", "保存为私密文档", "共享到项目", "取消共享",
|
||
"删除私密文档", "批量导出", "修改正文", "直接改文档正文",
|
||
}
|
||
for _, m := range unsupportedMarkers {
|
||
if strings.Contains(input, m) {
|
||
return &CandidateIntent{
|
||
Intent: IntentUnsupportedAction,
|
||
Confidence: 0.93,
|
||
FilledSlots: map[string]string{
|
||
SlotQueryTopic: "unsupported",
|
||
},
|
||
Reasoning: "用户请求的能力当前仅支持显式 UI 操作,不支持自然语言自动执行",
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// applyCapabilityHint 若上下文宣告了 AvailableActions,为已识别动作附加证据(不篡改语义)
|
||
func applyCapabilityHint(top *CandidateIntent, snap *ContextSnapshot) {
|
||
if top == nil || snap == nil || len(snap.AvailableActions) == 0 {
|
||
return
|
||
}
|
||
spec, ok := DefaultRegistry().GetByLegacyIntent(top.Intent)
|
||
if !ok || spec.CapabilityKey == "" {
|
||
return
|
||
}
|
||
capKey := spec.CapabilityKey
|
||
if top.Intent == IntentTriggerWorkflow {
|
||
if wf := top.FilledSlots[SlotTargetWorkflow]; wf != "" {
|
||
capKey = "TRIGGER_WORKFLOW:" + wf
|
||
}
|
||
}
|
||
for _, a := range snap.AvailableActions {
|
||
if a == capKey || strings.HasPrefix(a, capKey) {
|
||
return
|
||
}
|
||
}
|
||
top.Reasoning += fmt.Sprintf(";注意:当前上下文 AvailableActions 未包含 %s(L2 将做权限/能力裁决)", capKey)
|
||
}
|