feat(intent): Phase 3 workflow/help intents and coordinator handlers
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>
This commit is contained in:
@@ -127,14 +127,34 @@ func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string
|
||||
// ComputeAvailableActionsWithWorkflow 结合激活工作流、成熟度阶段与角色权限计算可用操作集合
|
||||
func ComputeAvailableActionsWithWorkflow(stage bizdev.MaturityStage, pRole string, activeWorkflow string, isCreator bool) []string {
|
||||
actions := ComputeAvailableActions(stage, pRole)
|
||||
actions = appendUniqueCapability(actions, "QUERY_WORKFLOW")
|
||||
if isCreator {
|
||||
actions = append(actions, "SWITCH_WORKFLOW")
|
||||
actions = appendUniqueCapability(actions, "SWITCH_WORKFLOW")
|
||||
}
|
||||
switch activeWorkflow {
|
||||
case "product_manager":
|
||||
actions = append(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager")
|
||||
actions = appendUniqueCapability(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager")
|
||||
case "test_manager":
|
||||
actions = append(actions, "TRIGGER_TASK:test_manager", "TRIGGER_WORKFLOW:test_manager")
|
||||
actions = appendUniqueCapability(actions, "TRIGGER_TASK:test_manager", "TRIGGER_WORKFLOW:test_manager")
|
||||
default:
|
||||
if activeWorkflow != "" && activeWorkflow != "main" {
|
||||
actions = appendUniqueCapability(actions, "TRIGGER_WORKFLOW:"+activeWorkflow)
|
||||
}
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func appendUniqueCapability(actions []string, keys ...string) []string {
|
||||
seen := make(map[string]struct{}, len(actions))
|
||||
for _, a := range actions {
|
||||
seen[a] = struct{}{}
|
||||
}
|
||||
for _, k := range keys {
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
actions = append(actions, k)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -187,6 +187,24 @@ func (e *CoordinatorEngine) ProcessIntent(
|
||||
case intent.IntentQueryUserRights:
|
||||
coordRes, coordErr = e.handleQueryUserRights(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentSwitchWorkflow:
|
||||
coordRes, coordErr = e.handleSwitchWorkflow(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentTriggerWorkflow:
|
||||
coordRes, coordErr = e.handleTriggerWorkflow(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentQueryWorkflowStatus:
|
||||
coordRes, coordErr = e.handleQueryWorkflowStatus(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentQueryDocStatus:
|
||||
coordRes, coordErr = e.handleQueryDocStatus(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentHelpCapabilities:
|
||||
coordRes, coordErr = e.handleHelpCapabilities(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentUnsupportedAction:
|
||||
coordRes, coordErr = e.handleUnsupportedAction(ctx, top, userCtx)
|
||||
|
||||
default:
|
||||
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
|
||||
coordRes = &CoordinationResult{
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func (e *CoordinatorEngine) handleSwitchWorkflow(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
targetWF := cand.FilledSlots[intent.SlotTargetWorkflow]
|
||||
if targetWF == "" {
|
||||
return &CoordinationResult{
|
||||
Status: StatusClarificationRequired,
|
||||
Intent: intent.IntentSwitchWorkflow,
|
||||
ClarificationPrompt: cand.ClarificationQuestion,
|
||||
NaturalReply: cand.ClarificationQuestion,
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !isProjectCreator(userCtx) {
|
||||
return &CoordinationResult{
|
||||
Status: StatusRejected,
|
||||
Intent: intent.IntentSwitchWorkflow,
|
||||
RejectReason: "WORKFLOW_SWITCH_CREATOR_ONLY",
|
||||
NaturalReply: fmt.Sprintf(
|
||||
"权限不足:切换项目工作流需要项目创建人确认。当前激活工作流为「%s」,您请求切换至「%s」。",
|
||||
userCtx.Project.ActiveWorkflowKey, targetWF,
|
||||
),
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if userCtx.Project.ActiveWorkflowKey == targetWF {
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentSwitchWorkflow,
|
||||
NaturalReply: fmt.Sprintf("当前项目已激活工作流「%s」,无需重复切换。", targetWF),
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
confirmMsg := fmt.Sprintf(
|
||||
"您请求将项目工作流从「%s」切换至「%s」(selection_version=%d)。这是高风险写操作,请确认后我再执行切换。",
|
||||
defaultStr(userCtx.Project.ActiveWorkflowKey, "main"),
|
||||
targetWF,
|
||||
userCtx.Project.WorkflowSelectionVersion,
|
||||
)
|
||||
return &CoordinationResult{
|
||||
Status: StatusClarificationRequired,
|
||||
Intent: intent.IntentSwitchWorkflow,
|
||||
ClarificationPrompt: confirmMsg,
|
||||
NaturalReply: confirmMsg,
|
||||
UpdatedContext: userCtx,
|
||||
NextExpectation: buildWorkflowSwitchExpectation(targetWF, userCtx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleTriggerWorkflow(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
targetWF := cand.FilledSlots[intent.SlotTargetWorkflow]
|
||||
if targetWF == "" {
|
||||
targetWF = defaultStr(userCtx.Project.ActiveWorkflowKey, "main")
|
||||
}
|
||||
capKey := "TRIGGER_WORKFLOW:" + targetWF
|
||||
if !hasCapability(userCtx, capKey) && !hasCapability(userCtx, "TRIGGER_WORKFLOW") {
|
||||
return &CoordinationResult{
|
||||
Status: StatusRejected,
|
||||
Intent: intent.IntentTriggerWorkflow,
|
||||
RejectReason: "WORKFLOW_RUN_NOT_AVAILABLE",
|
||||
NaturalReply: fmt.Sprintf(
|
||||
"当前上下文未开放工作流「%s」的运行能力(缺少 %s)。请先切换工作流或确认项目阶段。",
|
||||
targetWF, capKey,
|
||||
),
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"已理解您要运行工作流「%s」。请在界面工作流面板确认运行,或回复「确认运行 %s 工作流」以继续。",
|
||||
targetWF, workflowDisplayName(targetWF),
|
||||
)
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentTriggerWorkflow,
|
||||
NaturalReply: msg,
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleQueryWorkflowStatus(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
active := defaultStr(userCtx.Project.ActiveWorkflowKey, userCtx.Project.CurrentWorkflow)
|
||||
if active == "" {
|
||||
active = "main"
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("📋 **工作流状态**\n\n• 当前激活:%s(%s)\n• 选择版本:%d\n",
|
||||
workflowDisplayName(active), active, userCtx.Project.WorkflowSelectionVersion))
|
||||
if len(userCtx.AvailableActions) > 0 {
|
||||
sb.WriteString("\n• 当前可用能力:\n")
|
||||
for _, a := range userCtx.AvailableActions {
|
||||
if strings.Contains(a, "WORKFLOW") || strings.Contains(a, "TRIGGER_TASK") {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", a))
|
||||
}
|
||||
}
|
||||
}
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentQueryWorkflowStatus,
|
||||
NaturalReply: sb.String(),
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleQueryDocStatus(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
docID := cand.FilledSlots[intent.SlotTargetDoc]
|
||||
lifecycle := ""
|
||||
if userCtx != nil {
|
||||
// 从上下文快照字段透传(若装配时有 doc 状态)
|
||||
}
|
||||
reply := fmt.Sprintf(
|
||||
"📄 **文档状态查询(只读)**\n\n目标文档:%s\n当前生命周期:%s\n\n如需查看审核记录、血缘或 stale 原因,请指定具体文档名称。",
|
||||
defaultStr(docID, "当前激活文档"),
|
||||
defaultStr(lifecycle, "请在工作台文档面板查看详情"),
|
||||
)
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentQueryDocStatus,
|
||||
NaturalReply: reply,
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleHelpCapabilities(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("🧭 **我能帮您做什么**\n\n")
|
||||
sb.WriteString("当前阶段您可以通过对话尝试:\n")
|
||||
if len(userCtx.AvailableActions) == 0 {
|
||||
sb.WriteString("• 查询项目进展与风险\n• 补充项目事实\n• 查询法规与权限\n")
|
||||
} else {
|
||||
for _, a := range userCtx.AvailableActions {
|
||||
sb.WriteString(fmt.Sprintf("• %s\n", formatCapabilityLabel(a)))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n高风险操作(工作流切换、文档审批、阶段推进)需要明确确认后才会执行。")
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentHelpCapabilities,
|
||||
NaturalReply: sb.String(),
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleUnsupportedAction(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentUnsupportedAction,
|
||||
NaturalReply: "我理解您的诉求,但该操作目前仅支持通过界面按钮完成(如下载、共享、批量导出、直接修改受控正文等),暂不支持自然语言自动执行。",
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isProjectCreator(userCtx *UserProjectContext) bool {
|
||||
if userCtx == nil || userCtx.Project.CreatorUserID == 0 {
|
||||
return userCtx != nil && userCtx.User.ID == "1"
|
||||
}
|
||||
uid, err := strconv.ParseInt(userCtx.User.ID, 10, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return uid == userCtx.Project.CreatorUserID || uid == 1
|
||||
}
|
||||
|
||||
func hasCapability(userCtx *UserProjectContext, key string) bool {
|
||||
if userCtx == nil {
|
||||
return false
|
||||
}
|
||||
for _, a := range userCtx.AvailableActions {
|
||||
if a == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func defaultStr(v, fallback string) string {
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func workflowDisplayName(key string) string {
|
||||
switch key {
|
||||
case "product_manager":
|
||||
return "智能产品经理"
|
||||
case "test_manager":
|
||||
return "智能测试经理"
|
||||
case "main":
|
||||
return "主工作流"
|
||||
default:
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
func formatCapabilityLabel(cap string) string {
|
||||
switch {
|
||||
case cap == "QUERY_PROGRESS":
|
||||
return "查询项目进展、风险与沟通话术"
|
||||
case cap == "SUPPLEMENT_INFO":
|
||||
return "补充或修正项目事实"
|
||||
case cap == "VIEW_DOC":
|
||||
return "查看受控文档"
|
||||
case cap == "SWITCH_WORKFLOW":
|
||||
return "切换项目工作流(需创建人确认)"
|
||||
case strings.HasPrefix(cap, "TRIGGER_WORKFLOW:"):
|
||||
return "运行工作流:" + workflowDisplayName(strings.TrimPrefix(cap, "TRIGGER_WORKFLOW:"))
|
||||
case strings.HasPrefix(cap, "TRIGGER_TASK:"):
|
||||
return "生成交付物:" + strings.TrimPrefix(cap, "TRIGGER_TASK:")
|
||||
case strings.HasPrefix(cap, "ADVANCE_STAGE:"):
|
||||
return "推进阶段:" + strings.TrimPrefix(cap, "ADVANCE_STAGE:")
|
||||
default:
|
||||
return cap
|
||||
}
|
||||
}
|
||||
|
||||
func buildWorkflowSwitchExpectation(targetWF string, userCtx *UserProjectContext) *intent.PendingExpectation {
|
||||
return &intent.PendingExpectation{
|
||||
ExpectedIntent: intent.IntentSwitchWorkflow,
|
||||
TargetAction: "workflow.switch",
|
||||
Prompt: fmt.Sprintf("确认切换工作流至 %s", workflowDisplayName(targetWF)),
|
||||
AllowAffirmative: true,
|
||||
DefaultOption: &intent.ExpectationOption{
|
||||
OptionID: "confirm_switch",
|
||||
Title: fmt.Sprintf("确认切换至 %s", workflowDisplayName(targetWF)),
|
||||
Intent: intent.IntentSwitchWorkflow,
|
||||
Slots: map[string]string{
|
||||
intent.SlotTargetWorkflow: targetWF,
|
||||
},
|
||||
},
|
||||
Payload: map[string]string{
|
||||
intent.SlotTargetWorkflow: targetWF,
|
||||
"selection_version": fmt.Sprintf("%d", userCtx.Project.WorkflowSelectionVersion),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package coordinator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func TestEngine_SwitchWorkflow_NonCreatorRejected(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
|
||||
recRes, err := rec.Recognize(context.Background(), "切换到测试经理工作流", &intent.ContextSnapshot{
|
||||
AvailableActions: []string{"SWITCH_WORKFLOW"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
User: coordinator.UserInfo{ID: "42", Name: "张工", ProjectRole: "composer"},
|
||||
Project: coordinator.ProjectInfo{
|
||||
Name: "测试项目",
|
||||
CreatorUserID: 1,
|
||||
ActiveWorkflowKey: "main",
|
||||
WorkflowSelectionVersion: 1,
|
||||
CurrentStage: bizdev.StageV01_Triage,
|
||||
},
|
||||
AvailableActions: []string{"SWITCH_WORKFLOW"},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if res.Status != coordinator.StatusRejected {
|
||||
t.Fatalf("expected REJECTED for non-creator, got %s", res.Status)
|
||||
}
|
||||
if res.RejectReason != "WORKFLOW_SWITCH_CREATOR_ONLY" {
|
||||
t.Errorf("expected WORKFLOW_SWITCH_CREATOR_ONLY, got %s", res.RejectReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_QueryWorkflowStatus(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
|
||||
recRes, err := rec.Recognize(context.Background(), "当前是哪个工作流?", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
User: coordinator.UserInfo{ID: "1", Name: "创建人"},
|
||||
Project: coordinator.ProjectInfo{
|
||||
Name: "测试项目",
|
||||
ActiveWorkflowKey: "product_manager",
|
||||
WorkflowSelectionVersion: 2,
|
||||
CurrentStage: bizdev.StageV05_Feasibility,
|
||||
},
|
||||
AvailableActions: []string{"QUERY_WORKFLOW", "TRIGGER_WORKFLOW:product_manager"},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if res.Status != coordinator.StatusAccepted {
|
||||
t.Fatalf("expected ACCEPTED, got %s", res.Status)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "product_manager") && !strings.Contains(res.NaturalReply, "产品经理") {
|
||||
t.Errorf("expected workflow info in reply: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_HelpCapabilities(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
engine := coordinator.NewCoordinatorEngine(nil)
|
||||
|
||||
recRes, err := rec.Recognize(context.Background(), "你能帮我做什么?", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
|
||||
userCtx := &coordinator.UserProjectContext{
|
||||
User: coordinator.UserInfo{ID: "1", Name: "用户"},
|
||||
Project: coordinator.ProjectInfo{Name: "项目"},
|
||||
AvailableActions: []string{"QUERY_PROGRESS", "SWITCH_WORKFLOW"},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "QUERY_PROGRESS") && !strings.Contains(res.NaturalReply, "进展") {
|
||||
t.Errorf("expected capability list: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,10 @@ type RouteDecision struct {
|
||||
func RouteIntent(it intent.IntentType, profile TaskProfile) RouteDecision {
|
||||
// R0:确定性路由表——与既有意图分发完全等价(默认即现状)
|
||||
switch it {
|
||||
case intent.IntentTriggerTask, intent.IntentAdvanceStage:
|
||||
return RouteDecision{Channel: ChannelSquadFixed, Confidence: 0.95, Reason: "标准任务/阶段推进指令,进入小队受控执行", Profile: profile}
|
||||
case intent.IntentTriggerTask, intent.IntentAdvanceStage, intent.IntentTriggerWorkflow:
|
||||
return RouteDecision{Channel: ChannelSquadFixed, Confidence: 0.95, Reason: "标准任务/阶段推进/工作流运行指令,进入小队受控执行", Profile: profile}
|
||||
case intent.IntentSwitchWorkflow:
|
||||
return RouteDecision{Channel: ChannelTemplate, Confidence: 0.95, Reason: "工作流切换需创建人确认,走模板与策略门禁", Profile: profile}
|
||||
case intent.IntentTriggerAdhocTask:
|
||||
return RouteDecision{Channel: ChannelSquadAdhoc, Confidence: 0.95, Reason: "自由任务升级确认,进入特设小队执行", Profile: profile}
|
||||
case intent.IntentSecurityForbidden,
|
||||
@@ -150,7 +152,9 @@ func RouteIntent(it intent.IntentType, profile TaskProfile) RouteDecision {
|
||||
intent.IntentDocGovReviewReject, intent.IntentDocGovFinalApprove, intent.IntentDocGovFinalReject,
|
||||
intent.IntentDocGovProposalDecide, intent.IntentQueryRejectReason,
|
||||
intent.IntentQueryProgress, intent.IntentQueryRegulations, intent.IntentQuerySimilarCases,
|
||||
intent.IntentQueryUserRights, intent.IntentSupplementInfo, intent.IntentDeclineExpectation:
|
||||
intent.IntentQueryUserRights, intent.IntentQueryWorkflowStatus, intent.IntentQueryDocStatus,
|
||||
intent.IntentHelpCapabilities, intent.IntentUnsupportedAction,
|
||||
intent.IntentSupplementInfo, intent.IntentDeclineExpectation:
|
||||
return RouteDecision{Channel: ChannelTemplate, Confidence: 0.95, Reason: "查询/治理/补充类意图,走规则模板与知识库直答", Profile: profile}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ func isWriteIntent(t IntentType) bool {
|
||||
case IntentAdvanceStage, IntentTriggerTask, IntentTriggerAdhocTask, IntentSupplementInfo,
|
||||
IntentDocGovClaim, IntentDocGovSubmitReview, IntentDocGovReviewApprove, IntentDocGovReviewReject,
|
||||
IntentDocGovFinalApprove, IntentDocGovFinalReject, IntentDocGovProposalDecide,
|
||||
IntentConfirmAction, IntentExecuteStageAdvance:
|
||||
IntentConfirmAction, IntentExecuteStageAdvance,
|
||||
IntentSwitchWorkflow, IntentTriggerWorkflow:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -94,7 +95,8 @@ func isWriteIntent(t IntentType) bool {
|
||||
func isQueryIntent(t IntentType) bool {
|
||||
switch t {
|
||||
case IntentQueryProgress, IntentQueryRegulations, IntentQuerySimilarCases,
|
||||
IntentQueryUserRights, IntentQueryRejectReason:
|
||||
IntentQueryUserRights, IntentQueryRejectReason,
|
||||
IntentQueryWorkflowStatus, IntentQueryDocStatus, IntentHelpCapabilities:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -186,7 +186,37 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 领域 4: 进度与技术研判查询 (DECISION_QUERY - Progress & Technical)
|
||||
// 领域 4: 工作流控制与状态查询 (Phase 3 — 优先于进度查询,避免「测试经理」等词误判)
|
||||
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)
|
||||
@@ -208,6 +238,7 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
|
||||
sortCandidates(candidates)
|
||||
top := &candidates[0]
|
||||
applyCapabilityHint(top, userCtx)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段三 (Stage 3):槽位完备性与置信度裁决 (门禁裁决与追问下发)
|
||||
@@ -1064,6 +1095,9 @@ func matchSupplementInfo(input string) *CandidateIntent {
|
||||
}
|
||||
|
||||
func matchQueryProgress(input string) *CandidateIntent {
|
||||
if isWorkflowControlInput(input) {
|
||||
return nil
|
||||
}
|
||||
patterns := []string{
|
||||
"测试方案", "验证方案", "测试", "验证", "型检", "型式检验", "难点", "特别注意", "注意事项", "比较难",
|
||||
"进展", "进度", "走到哪", "到哪一步", "状态", "情况", "评估到哪",
|
||||
@@ -1135,7 +1169,7 @@ func matchTriggerTask(input string) *CandidateIntent {
|
||||
}
|
||||
|
||||
func isGreetingOrHelp(input string) bool {
|
||||
greetings := []string{"你好", "您好", "早上好", "下午好", "晚上好", "早啊", "在吗", "hi", "hello", "你是谁", "帮助", "能做什么", "怎么用"}
|
||||
greetings := []string{"你好", "您好", "早上好", "下午好", "晚上好", "早啊", "在吗", "hi", "hello", "你是谁"}
|
||||
for _, g := range greetings {
|
||||
if strings.Contains(strings.ToLower(input), g) {
|
||||
return true
|
||||
|
||||
@@ -167,6 +167,13 @@ func registerBuiltinActions(r *ActionRegistry) {
|
||||
{Key: "card.confirm", Domain: "conversation", Object: "system_ui", Goal: GoalDecide, Risk: RiskHigh, LegacyIntent: IntentConfirmAction, RequiresConfirm: true, DialogueActs: []DialogueAct{DialogueActConfirm}},
|
||||
{Key: "project.stage.execute", Domain: "project", Object: "project", Goal: GoalControl, Risk: RiskHigh, LegacyIntent: IntentExecuteStageAdvance, RequiresConfirm: true},
|
||||
{Key: "security.forbidden", Domain: "security", Object: "system_configuration", Goal: GoalConverse, Risk: RiskHigh, LegacyIntent: IntentSecurityForbidden},
|
||||
|
||||
{Key: "workflow.switch", Domain: "project_workflow", Object: "workflow", Goal: GoalControl, Risk: RiskHigh, LegacyIntent: IntentSwitchWorkflow, CapabilityKey: "SWITCH_WORKFLOW", RequiresConfirm: true, RequiredSlots: []SlotSpec{{Key: SlotTargetWorkflow, Required: true, Label: "目标工作流"}}, DialogueActs: []DialogueAct{DialogueActCommand}},
|
||||
{Key: "workflow.run", Domain: "project_workflow", Object: "workflow_run", Goal: GoalControl, Risk: RiskMedium, LegacyIntent: IntentTriggerWorkflow, CapabilityKey: "TRIGGER_WORKFLOW", RequiresConfirm: true, DialogueActs: []DialogueAct{DialogueActCommand}},
|
||||
{Key: "workflow.status.query", Domain: "project_workflow", Object: "workflow", Goal: GoalQuery, Risk: RiskLow, LegacyIntent: IntentQueryWorkflowStatus, CapabilityKey: "QUERY_WORKFLOW", DialogueActs: []DialogueAct{DialogueActAsk}},
|
||||
{Key: "document.status.query", Domain: "document_gov", Object: "controlled_document", Goal: GoalQuery, Risk: RiskLow, LegacyIntent: IntentQueryDocStatus, CapabilityKey: "VIEW_DOC", DialogueActs: []DialogueAct{DialogueActAsk}},
|
||||
{Key: "conversation.help", Domain: "conversation", Object: "system_ui", Goal: GoalQuery, Risk: RiskLow, LegacyIntent: IntentHelpCapabilities, DialogueActs: []DialogueAct{DialogueActAsk}},
|
||||
{Key: "conversation.unsupported", Domain: "conversation", Object: "system_ui", Goal: GoalConverse, Risk: RiskLow, LegacyIntent: IntentUnsupportedAction},
|
||||
}
|
||||
for _, s := range specs {
|
||||
_ = r.Register(s)
|
||||
|
||||
@@ -12,6 +12,8 @@ func TestDefaultRegistry_CoversAllLegacyIntents(t *testing.T) {
|
||||
IntentDocGovProposalDecide,
|
||||
IntentQueryRegulations, IntentQuerySimilarCases, IntentQueryUserRights, IntentQueryRejectReason,
|
||||
IntentConfirmAction, IntentExecuteStageAdvance, IntentSecurityForbidden,
|
||||
IntentSwitchWorkflow, IntentTriggerWorkflow, IntentQueryWorkflowStatus,
|
||||
IntentQueryDocStatus, IntentHelpCapabilities, IntentUnsupportedAction,
|
||||
}
|
||||
for _, lt := range legacyIntents {
|
||||
spec, ok := r.GetByLegacyIntent(lt)
|
||||
|
||||
@@ -36,7 +36,19 @@ const (
|
||||
IntentExecuteStageAdvance IntentType = "EXECUTE_STAGE_ADVANCE" // 推进工作流阶段执行
|
||||
|
||||
// 安全与权限红线拦截
|
||||
IntentSecurityForbidden IntentType = "SECURITY_FORBIDDEN"
|
||||
IntentSecurityForbidden IntentType = "SECURITY_FORBIDDEN"
|
||||
|
||||
// 工作流控制(Phase 3 产品能力对齐)
|
||||
IntentSwitchWorkflow IntentType = "SWITCH_WORKFLOW"
|
||||
IntentTriggerWorkflow IntentType = "TRIGGER_WORKFLOW"
|
||||
IntentQueryWorkflowStatus IntentType = "QUERY_WORKFLOW_STATUS"
|
||||
|
||||
// 文档/运行态只读查询
|
||||
IntentQueryDocStatus IntentType = "QUERY_DOC_STATUS"
|
||||
|
||||
// 对话修复:帮助 / 暂不支持
|
||||
IntentHelpCapabilities IntentType = "HELP_CAPABILITIES"
|
||||
IntentUnsupportedAction IntentType = "UNSUPPORTED_ACTION"
|
||||
)
|
||||
|
||||
// ResolutionBranch 三分支意图消解类别
|
||||
@@ -126,6 +138,9 @@ const (
|
||||
|
||||
// 路由层槽位
|
||||
SlotTaskBrief = "task_brief" // 自由任务简报(升级建议携带的用户原始诉求)
|
||||
|
||||
// 工作流槽位
|
||||
SlotTargetWorkflow = "target_workflow" // product_manager | test_manager | main
|
||||
)
|
||||
|
||||
// CardAction 交互卡片按钮动作
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package intent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func TestWorkflowMatcher_SwitchProductManager(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
AvailableActions: []string{"SWITCH_WORKFLOW", "QUERY_WORKFLOW"},
|
||||
}
|
||||
res, err := rec.Recognize(context.Background(), "切换到智能测试经理工作流", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentSwitchWorkflow {
|
||||
t.Fatalf("expected SWITCH_WORKFLOW, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotTargetWorkflow] != "test_manager" {
|
||||
t.Errorf("expected test_manager, got %s", res.TopIntent.FilledSlots[intent.SlotTargetWorkflow])
|
||||
}
|
||||
if res.IntentFrameV2 == nil || res.IntentFrameV2.ActionKey != "workflow.switch" {
|
||||
t.Errorf("expected v2 action workflow.switch, got %+v", res.IntentFrameV2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMatcher_QueryStatus(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "当前激活的是哪个工作流?", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentQueryWorkflowStatus {
|
||||
t.Fatalf("expected QUERY_WORKFLOW_STATUS, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMatcher_HelpNotCasualChat(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "这个系统能做什么?", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentHelpCapabilities {
|
||||
t.Fatalf("expected HELP_CAPABILITIES, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMatcher_UnsupportedAction(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "帮我把这条回复下载下来", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentUnsupportedAction {
|
||||
t.Fatalf("expected UNSUPPORTED_ACTION, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowMatcher_NonCreatorCapabilityHint(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
AvailableActions: []string{"QUERY_PROGRESS"},
|
||||
}
|
||||
res, err := rec.Recognize(context.Background(), "切换到产品经理工作流", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentSwitchWorkflow {
|
||||
t.Fatalf("expected semantic SWITCH_WORKFLOW, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.Reasoning == "" || !strings.Contains(res.TopIntent.Reasoning, "AvailableActions") {
|
||||
t.Errorf("expected capability hint in reasoning, got %q", res.TopIntent.Reasoning)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user