feat(intent): Phase 4 run control, subtypes, and limited IntentPlan
Add RUN_STATUS_QUERY/RUN_RESUME/RUN_RESTART with matcher and coordinator handlers; write QUERY_PROGRESS/SUPPLEMENT_INFO subtypes to V2 frame; support safe query+query multi-intent plans via IntentPlan dual-write. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
dda6a06e4a
commit
9cb129d3e8
@@ -205,6 +205,15 @@ func (e *CoordinatorEngine) ProcessIntent(
|
||||
case intent.IntentUnsupportedAction:
|
||||
coordRes, coordErr = e.handleUnsupportedAction(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentRunStatusQuery:
|
||||
coordRes, coordErr = e.handleRunStatusQuery(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentRunResume:
|
||||
coordRes, coordErr = e.handleRunResume(ctx, top, userCtx)
|
||||
|
||||
case intent.IntentRunRestart:
|
||||
coordRes, coordErr = e.handleRunRestart(ctx, top, userCtx)
|
||||
|
||||
default:
|
||||
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
|
||||
coordRes = &CoordinationResult{
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func (e *CoordinatorEngine) handleRunStatusQuery(_ context.Context, _ *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
workDir := userCtx.WorkDir
|
||||
reply := "📊 **运行状态查询(只读)**\n\n"
|
||||
if workDir != "" {
|
||||
reply += fmt.Sprintf("• 最近工作区:`%s`\n• 您可以在任务面板查看该运行的阶段、失败原因与产物摘要。\n", workDir)
|
||||
} else {
|
||||
reply += "• 当前会话未绑定具体工作区。请指定要查询的运行任务,或先在界面打开失败任务详情。\n"
|
||||
}
|
||||
return &CoordinationResult{
|
||||
Status: StatusAccepted,
|
||||
Intent: intent.IntentRunStatusQuery,
|
||||
NaturalReply: reply,
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleRunResume(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
workDir := cand.FilledSlots[intent.SlotWorkDir]
|
||||
if workDir == "" {
|
||||
workDir = userCtx.WorkDir
|
||||
}
|
||||
if workDir == "" {
|
||||
msg := cand.ClarificationQuestion
|
||||
if msg == "" {
|
||||
msg = "续跑需要绑定目标工作区。请说明要恢复哪一次失败运行,或在工作区面板选择「继续」。"
|
||||
}
|
||||
return &CoordinationResult{
|
||||
Status: StatusClarificationRequired,
|
||||
Intent: intent.IntentRunResume,
|
||||
ClarificationPrompt: msg,
|
||||
NaturalReply: msg,
|
||||
UpdatedContext: userCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"已理解您要续跑工作区 `%s`(复用已有上下文)。请在任务面板确认「继续」,或回复「确认续跑」后我将为您发起 resume 请求。",
|
||||
workDir,
|
||||
)
|
||||
return &CoordinationResult{
|
||||
Status: StatusClarificationRequired,
|
||||
Intent: intent.IntentRunResume,
|
||||
NaturalReply: msg,
|
||||
ClarificationPrompt: msg,
|
||||
UpdatedContext: userCtx,
|
||||
NextExpectation: &intent.PendingExpectation{
|
||||
ExpectedIntent: intent.IntentRunResume,
|
||||
TargetAction: "run.resume",
|
||||
Prompt: "确认续跑长任务",
|
||||
AllowAffirmative: true,
|
||||
Payload: map[string]string{
|
||||
intent.SlotWorkDir: workDir,
|
||||
},
|
||||
DefaultOption: &intent.ExpectationOption{
|
||||
OptionID: "confirm_resume",
|
||||
Title: "确认续跑",
|
||||
Intent: intent.IntentRunResume,
|
||||
Slots: map[string]string{
|
||||
intent.SlotWorkDir: workDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *CoordinatorEngine) handleRunRestart(_ context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||||
msg := "已理解您希望重新运行任务(将创建新工作区,保留原始意图但不复用失败现场)。这是高风险操作,请确认后再执行。"
|
||||
if cand.ClarificationQuestion != "" {
|
||||
msg = cand.ClarificationQuestion
|
||||
}
|
||||
return &CoordinationResult{
|
||||
Status: StatusClarificationRequired,
|
||||
Intent: intent.IntentRunRestart,
|
||||
NaturalReply: msg,
|
||||
ClarificationPrompt: msg,
|
||||
UpdatedContext: userCtx,
|
||||
NextExpectation: &intent.PendingExpectation{
|
||||
ExpectedIntent: intent.IntentRunRestart,
|
||||
TargetAction: "run.restart",
|
||||
Prompt: "确认重新运行长任务",
|
||||
AllowAffirmative: false,
|
||||
DefaultOption: &intent.ExpectationOption{
|
||||
OptionID: "confirm_restart",
|
||||
Title: "确认重新运行",
|
||||
Intent: intent.IntentRunRestart,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package coordinator_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/coordinator"
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func TestEngine_RunStatusQuery_WithWorkDir(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: "项目"},
|
||||
WorkDir: "/data/dev/ws-123",
|
||||
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, "ws-123") {
|
||||
t.Errorf("expected workdir in reply: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngine_RunResume_MissingWorkDirClarify(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: "项目"},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
|
||||
res, err := engine.ProcessIntent(context.Background(), recRes, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if res.Status != coordinator.StatusClarificationRequired {
|
||||
t.Fatalf("expected CLARIFICATION_REQUIRED, got %s", res.Status)
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,9 @@ type RouteDecision struct {
|
||||
func RouteIntent(it intent.IntentType, profile TaskProfile) RouteDecision {
|
||||
// R0:确定性路由表——与既有意图分发完全等价(默认即现状)
|
||||
switch it {
|
||||
case intent.IntentTriggerTask, intent.IntentAdvanceStage, intent.IntentTriggerWorkflow:
|
||||
return RouteDecision{Channel: ChannelSquadFixed, Confidence: 0.95, Reason: "标准任务/阶段推进/工作流运行指令,进入小队受控执行", Profile: profile}
|
||||
case intent.IntentTriggerTask, intent.IntentAdvanceStage, intent.IntentTriggerWorkflow,
|
||||
intent.IntentRunResume, intent.IntentRunRestart:
|
||||
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:
|
||||
@@ -153,7 +154,7 @@ func RouteIntent(it intent.IntentType, profile TaskProfile) RouteDecision {
|
||||
intent.IntentDocGovProposalDecide, intent.IntentQueryRejectReason,
|
||||
intent.IntentQueryProgress, intent.IntentQueryRegulations, intent.IntentQuerySimilarCases,
|
||||
intent.IntentQueryUserRights, intent.IntentQueryWorkflowStatus, intent.IntentQueryDocStatus,
|
||||
intent.IntentHelpCapabilities, intent.IntentUnsupportedAction,
|
||||
intent.IntentHelpCapabilities, intent.IntentUnsupportedAction, intent.IntentRunStatusQuery,
|
||||
intent.IntentSupplementInfo, intent.IntentDeclineExpectation:
|
||||
return RouteDecision{Channel: ChannelTemplate, Confidence: 0.95, Reason: "查询/治理/补充类意图,走规则模板与知识库直答", Profile: profile}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,21 @@ package intent
|
||||
import "strings"
|
||||
|
||||
// branchToRelation 将三分支映射为 ExpectationRelation
|
||||
func mapQueryProgressSubtype(topic string) string {
|
||||
switch topic {
|
||||
case "risk":
|
||||
return "risk"
|
||||
case "testing_analysis", "technical_design":
|
||||
return "technical"
|
||||
case "talk_script":
|
||||
return "communication"
|
||||
case "progress":
|
||||
return "status"
|
||||
default:
|
||||
return topic
|
||||
}
|
||||
}
|
||||
|
||||
func branchToRelation(branch ResolutionBranch, cand *CandidateIntent) ExpectationRelation {
|
||||
switch branch {
|
||||
case BranchExpectationHit:
|
||||
@@ -60,8 +75,15 @@ func BuildIntentFrameV2(res *IntentRecognitionResult, registry *ActionRegistry)
|
||||
}
|
||||
}
|
||||
if top.FilledSlots != nil {
|
||||
if topic, ok := top.FilledSlots[SlotQueryTopic]; ok && top.Intent == IntentQueryProgress {
|
||||
frame.Subtype = topic
|
||||
switch top.Intent {
|
||||
case IntentQueryProgress:
|
||||
if topic, ok := top.FilledSlots[SlotQueryTopic]; ok {
|
||||
frame.Subtype = mapQueryProgressSubtype(topic)
|
||||
}
|
||||
case IntentSupplementInfo:
|
||||
if action, ok := top.FilledSlots[SlotSupplementAction]; ok {
|
||||
frame.Subtype = action
|
||||
}
|
||||
}
|
||||
}
|
||||
frame.Evidence = buildEvidenceFromCandidate(top, res)
|
||||
|
||||
@@ -84,7 +84,8 @@ func isWriteIntent(t IntentType) bool {
|
||||
IntentDocGovClaim, IntentDocGovSubmitReview, IntentDocGovReviewApprove, IntentDocGovReviewReject,
|
||||
IntentDocGovFinalApprove, IntentDocGovFinalReject, IntentDocGovProposalDecide,
|
||||
IntentConfirmAction, IntentExecuteStageAdvance,
|
||||
IntentSwitchWorkflow, IntentTriggerWorkflow:
|
||||
IntentSwitchWorkflow, IntentTriggerWorkflow,
|
||||
IntentRunResume, IntentRunRestart:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -96,7 +97,8 @@ func isQueryIntent(t IntentType) bool {
|
||||
switch t {
|
||||
case IntentQueryProgress, IntentQueryRegulations, IntentQuerySimilarCases,
|
||||
IntentQueryUserRights, IntentQueryRejectReason,
|
||||
IntentQueryWorkflowStatus, IntentQueryDocStatus, IntentHelpCapabilities:
|
||||
IntentQueryWorkflowStatus, IntentQueryDocStatus, IntentHelpCapabilities,
|
||||
IntentRunStatusQuery:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package intent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var planConnectors = []string{",然后", ",然后", ",并", ",并", ",接着", ",接着", ";再", ";再"}
|
||||
|
||||
// splitOnPlanConnectors 按有限连接词拆分多意图输入
|
||||
func splitOnPlanConnectors(input string) []string {
|
||||
for _, sep := range planConnectors {
|
||||
if idx := strings.Index(input, sep); idx > 0 {
|
||||
left := strings.TrimSpace(input[:idx])
|
||||
right := strings.TrimSpace(input[idx+len(sep):])
|
||||
if left != "" && right != "" {
|
||||
return []string{left, right}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildLimitedIntentPlan 构建有限多意图计划(仅 query+query / query+generate 等安全组合)
|
||||
func buildLimitedIntentPlan(ctx context.Context, r *RuleBasedRecognizer, input string, snap *ContextSnapshot, primary *IntentRecognitionResult) *IntentPlan {
|
||||
parts := splitOnPlanConnectors(input)
|
||||
if len(parts) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var steps []IntentPlanStep
|
||||
for i, part := range parts {
|
||||
subRes, err := r.Recognize(ctx, part, snap)
|
||||
if err != nil || subRes == nil || subRes.TopIntent == nil {
|
||||
return nil
|
||||
}
|
||||
frame := BuildIntentFrameV2(subRes, DefaultRegistry())
|
||||
if frame == nil {
|
||||
return nil
|
||||
}
|
||||
// 高风险写动作组合初期不支持
|
||||
if isWriteIntent(subRes.TopIntent.Intent) {
|
||||
return nil
|
||||
}
|
||||
stepID := fmt.Sprintf("%d", i+1)
|
||||
var depends []string
|
||||
if i > 0 {
|
||||
depends = []string{fmt.Sprintf("%d", i)}
|
||||
}
|
||||
steps = append(steps, IntentPlanStep{
|
||||
StepID: stepID,
|
||||
Frame: *frame,
|
||||
DependsOn: depends,
|
||||
OnFailure: "abort",
|
||||
})
|
||||
}
|
||||
|
||||
if len(steps) < 2 {
|
||||
return nil
|
||||
}
|
||||
// 至少一个查询步骤
|
||||
hasQuery := false
|
||||
for _, s := range steps {
|
||||
if s.Frame.Goal == GoalQuery || isQueryIntent(s.Frame.LegacyIntent) {
|
||||
hasQuery = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasQuery {
|
||||
return nil
|
||||
}
|
||||
|
||||
if primary != nil && primary.IntentFrameV2 != nil {
|
||||
primary.IntentFrameV2.Relation = RelationExpectationPlusNew
|
||||
}
|
||||
return &IntentPlan{
|
||||
Steps: steps,
|
||||
Atomicity: "sequential",
|
||||
NeedsConfirm: false,
|
||||
FailureMode: "stop_on_first_failure",
|
||||
}
|
||||
}
|
||||
|
||||
func inferSupplementSubtype(input string, slots map[string]string) string {
|
||||
if strings.Contains(input, "附件") || strings.Contains(input, "材料") || strings.Contains(input, "上传") {
|
||||
return "attach"
|
||||
}
|
||||
if strings.Contains(input, "纠正") || strings.Contains(input, "更正") ||
|
||||
(strings.Contains(input, "改为") && strings.Contains(input, "原来")) {
|
||||
return "correct"
|
||||
}
|
||||
if strings.Contains(input, "修改为") || strings.Contains(input, "改为") || strings.Contains(input, "改成") {
|
||||
if slots[SlotFactKey] != "" {
|
||||
return "correct"
|
||||
}
|
||||
}
|
||||
return "add"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package intent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func TestLimitedIntentPlan_QueryThenQuery(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "查一下项目风险,然后查相关法规", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.IntentPlan == nil {
|
||||
t.Fatal("expected intent_plan for query+query combination")
|
||||
}
|
||||
if len(res.IntentPlan.Steps) != 2 {
|
||||
t.Fatalf("expected 2 steps, got %d", len(res.IntentPlan.Steps))
|
||||
}
|
||||
if res.IntentPlan.Steps[0].Frame.Goal != intent.GoalQuery {
|
||||
t.Errorf("step1 goal want query got %s", res.IntentPlan.Steps[0].Frame.Goal)
|
||||
}
|
||||
if res.IntentPlan.Steps[1].Frame.Goal != intent.GoalQuery {
|
||||
t.Errorf("step2 goal want query got %s", res.IntentPlan.Steps[1].Frame.Goal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupplementSubtype_InV2Frame(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
res, err := rec.Recognize(context.Background(), "客户反馈附件已上传,请入库", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize: %v", err)
|
||||
}
|
||||
if res.IntentFrameV2 == nil || res.IntentFrameV2.Subtype != "attach" {
|
||||
t.Errorf("expected subtype attach, got %+v", res.IntentFrameV2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMatcher_ResumeRequiresClarification(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.IntentRunResume {
|
||||
t.Fatalf("expected RUN_RESUME, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if !res.RequiresClarifying && res.TopIntent.ClarificationQuestion == "" {
|
||||
t.Error("expected clarification when work_dir missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunMatcher_StatusQuery(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.IntentRunStatusQuery {
|
||||
t.Fatalf("expected RUN_STATUS_QUERY, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,13 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 领域 4: 工作流控制与状态查询 (Phase 3 — 优先于进度查询,避免「测试经理」等词误判)
|
||||
// 领域 4: 长任务运行控制 (Phase 4)
|
||||
if cand := matchRunControl(trimmed, userCtx); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 领域 5: 工作流控制与状态查询 (Phase 3 — 优先于进度查询,避免「测试经理」等词误判)
|
||||
if cand := matchQueryWorkflowStatus(trimmed, userCtx); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
@@ -291,6 +297,9 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
RequiresClarifying: requiresClarifying,
|
||||
}
|
||||
finalizeRecognitionResult(result)
|
||||
if plan := buildLimitedIntentPlan(ctx, r, trimmed, userCtx, result); plan != nil {
|
||||
result.IntentPlan = plan
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1085,6 +1094,7 @@ func matchSupplementInfo(input string) *CandidateIntent {
|
||||
slots = make(map[string]string)
|
||||
}
|
||||
slots[SlotSupplementDoc] = input
|
||||
slots[SlotSupplementAction] = inferSupplementSubtype(input, slots)
|
||||
|
||||
return &CandidateIntent{
|
||||
Intent: IntentSupplementInfo,
|
||||
@@ -1095,7 +1105,7 @@ func matchSupplementInfo(input string) *CandidateIntent {
|
||||
}
|
||||
|
||||
func matchQueryProgress(input string) *CandidateIntent {
|
||||
if isWorkflowControlInput(input) {
|
||||
if isWorkflowControlInput(input) || isRunControlInput(input) {
|
||||
return nil
|
||||
}
|
||||
patterns := []string{
|
||||
|
||||
@@ -174,6 +174,10 @@ func registerBuiltinActions(r *ActionRegistry) {
|
||||
{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},
|
||||
|
||||
{Key: "run.status.query", Domain: "project", Object: "workflow_run", Goal: GoalQuery, Risk: RiskLow, LegacyIntent: IntentRunStatusQuery, DialogueActs: []DialogueAct{DialogueActAsk}},
|
||||
{Key: "run.resume", Domain: "project", Object: "workflow_run", Goal: GoalControl, Risk: RiskMedium, LegacyIntent: IntentRunResume, RequiresConfirm: true, DialogueActs: []DialogueAct{DialogueActCommand}},
|
||||
{Key: "run.restart", Domain: "project", Object: "workflow_run", Goal: GoalControl, Risk: RiskHigh, LegacyIntent: IntentRunRestart, RequiresConfirm: true, DialogueActs: []DialogueAct{DialogueActCommand}},
|
||||
}
|
||||
for _, s := range specs {
|
||||
_ = r.Register(s)
|
||||
|
||||
@@ -14,6 +14,7 @@ func TestDefaultRegistry_CoversAllLegacyIntents(t *testing.T) {
|
||||
IntentConfirmAction, IntentExecuteStageAdvance, IntentSecurityForbidden,
|
||||
IntentSwitchWorkflow, IntentTriggerWorkflow, IntentQueryWorkflowStatus,
|
||||
IntentQueryDocStatus, IntentHelpCapabilities, IntentUnsupportedAction,
|
||||
IntentRunStatusQuery, IntentRunResume, IntentRunRestart,
|
||||
}
|
||||
for _, lt := range legacyIntents {
|
||||
spec, ok := r.GetByLegacyIntent(lt)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package intent
|
||||
|
||||
import "strings"
|
||||
|
||||
func isRunControlInput(input string) bool {
|
||||
runMarkers := []string{
|
||||
"继续", "续跑", "接着跑", "继续上次", "继续任务", "恢复运行", "恢复任务",
|
||||
"重来", "重新运行", "重启", "从头开始", "重新跑",
|
||||
"运行状态", "任务状态", "执行状态", "跑到哪", "失败了吗", "完成了吗", "任务进度",
|
||||
"上次任务", "这次运行", "工作区",
|
||||
}
|
||||
for _, m := range runMarkers {
|
||||
if strings.Contains(input, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchRunControl(input string, snap *ContextSnapshot) *CandidateIntent {
|
||||
if !isRunControlInput(input) {
|
||||
return nil
|
||||
}
|
||||
// 避免与挂起预期裸肯定词冲突(「继续」单独出现且 pending 存在时由 expectation 处理)
|
||||
if strings.TrimSpace(input) == "继续" || strings.TrimSpace(input) == "接着" {
|
||||
return nil
|
||||
}
|
||||
|
||||
slots := map[string]string{}
|
||||
if snap != nil && snap.SnapshotID != "" {
|
||||
_ = snap.SnapshotID
|
||||
}
|
||||
|
||||
restartMarkers := []string{"重来", "重新运行", "重启", "从头开始", "重新跑", "重新执行"}
|
||||
for _, m := range restartMarkers {
|
||||
if strings.Contains(input, m) {
|
||||
slots[SlotRunAction] = "restart"
|
||||
return &CandidateIntent{
|
||||
Intent: IntentRunRestart,
|
||||
Confidence: 0.91,
|
||||
FilledSlots: slots,
|
||||
Reasoning: "用户请求重新启动长任务运行(将创建新工作区,保留原意图)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeMarkers := []string{"继续", "续跑", "接着跑", "继续上次", "继续任务", "恢复运行", "恢复任务"}
|
||||
for _, m := range resumeMarkers {
|
||||
if strings.Contains(input, m) {
|
||||
slots[SlotRunAction] = "resume"
|
||||
cand := &CandidateIntent{
|
||||
Intent: IntentRunResume,
|
||||
Confidence: 0.90,
|
||||
FilledSlots: slots,
|
||||
Reasoning: "用户请求续跑/恢复已有长任务工作区",
|
||||
}
|
||||
if snap == nil || (len(slots[SlotWorkDir]) == 0) {
|
||||
cand.ClarificationQuestion = "请说明要续跑的具体任务或失败阶段;若您指的是最近一次失败运行,请确认后我将基于该工作区继续。"
|
||||
}
|
||||
return cand
|
||||
}
|
||||
}
|
||||
|
||||
if detectDialogueAct(input) == DialogueActAsk ||
|
||||
strings.Contains(input, "状态") || strings.Contains(input, "失败了吗") ||
|
||||
strings.Contains(input, "完成了吗") || strings.Contains(input, "跑到哪") {
|
||||
slots[SlotRunAction] = "status"
|
||||
return &CandidateIntent{
|
||||
Intent: IntentRunStatusQuery,
|
||||
Confidence: 0.89,
|
||||
FilledSlots: map[string]string{
|
||||
SlotRunAction: "status",
|
||||
SlotQueryTopic: "run_status",
|
||||
},
|
||||
Reasoning: "用户查询长任务/工作流运行状态(只读)",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -49,6 +49,11 @@ const (
|
||||
// 对话修复:帮助 / 暂不支持
|
||||
IntentHelpCapabilities IntentType = "HELP_CAPABILITIES"
|
||||
IntentUnsupportedAction IntentType = "UNSUPPORTED_ACTION"
|
||||
|
||||
// 长任务运行控制(Phase 4)
|
||||
IntentRunStatusQuery IntentType = "RUN_STATUS_QUERY"
|
||||
IntentRunResume IntentType = "RUN_RESUME"
|
||||
IntentRunRestart IntentType = "RUN_RESTART"
|
||||
)
|
||||
|
||||
// ResolutionBranch 三分支意图消解类别
|
||||
@@ -118,6 +123,7 @@ type IntentRecognitionResult struct {
|
||||
// V2 双写:迁移期与 TopIntent 并存,Router/Coordinator 默认仍消费 TopIntent。
|
||||
IntentFrameV2 *IntentFrameV2 `json:"intent_frame_v2,omitempty"`
|
||||
RankedFrames *RankedIntentSet `json:"ranked_frames,omitempty"`
|
||||
IntentPlan *IntentPlan `json:"intent_plan,omitempty"`
|
||||
}
|
||||
|
||||
// 常见槽位 Key 常量定义
|
||||
@@ -141,6 +147,11 @@ const (
|
||||
|
||||
// 工作流槽位
|
||||
SlotTargetWorkflow = "target_workflow" // product_manager | test_manager | main
|
||||
|
||||
// 长任务/运行槽位
|
||||
SlotWorkDir = "work_dir"
|
||||
SlotRunAction = "run_action" // status | resume | restart
|
||||
SlotSupplementAction = "supplement_action" // add | correct | attach
|
||||
)
|
||||
|
||||
// CardAction 交互卡片按钮动作
|
||||
|
||||
@@ -75,3 +75,19 @@ type CandidateConflict struct {
|
||||
}
|
||||
|
||||
const IntentFrameV2Version = "v2.0"
|
||||
|
||||
// IntentPlan 有限多意图执行计划(Phase 4:仅支持可验证组合)
|
||||
type IntentPlan struct {
|
||||
Steps []IntentPlanStep `json:"steps"`
|
||||
Atomicity string `json:"atomicity"` // sequential
|
||||
NeedsConfirm bool `json:"needs_confirm,omitempty"`
|
||||
FailureMode string `json:"failure_mode,omitempty"` // stop_on_first_failure
|
||||
}
|
||||
|
||||
// IntentPlanStep 计划步骤
|
||||
type IntentPlanStep struct {
|
||||
StepID string `json:"step_id"`
|
||||
Frame IntentFrameV2 `json:"frame"`
|
||||
DependsOn []string `json:"depends_on,omitempty"`
|
||||
OnFailure string `json:"on_failure,omitempty"` // abort | skip
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user