Ask via PendingExpectation for weak-write verbs, mid confidence, or stage-mismatched doc tasks instead of running blindly. Co-authored-by: Cursor <cursoragent@cursor.com>
204 lines
6.6 KiB
Go
204 lines
6.6 KiB
Go
package coordinator
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/intent"
|
||
"chat-agent-system/internal/maturity"
|
||
"chat-agent-system/internal/projectinstance"
|
||
)
|
||
|
||
// taskDisplayTitle 用户可见的交付物标题(确认话术用)。
|
||
func taskDisplayTitle(taskKey string) string {
|
||
switch strings.TrimSpace(taskKey) {
|
||
case "intake", "bd_intake":
|
||
return "用户初始需求收集表"
|
||
case "user_requirements", "bd_user_requirements":
|
||
return "用户需求表"
|
||
case "research", "bd_research":
|
||
return "技术与市场调研报告"
|
||
case "feasibility_reg", "bd_feasibility_reg":
|
||
return "法规与注册论证报告"
|
||
case "feasibility_dev", "bd_feasibility_dev":
|
||
return "设计开发可行性评估"
|
||
case "prelim_design", "bd_prelim_design":
|
||
return "系统初步设计方案"
|
||
case "cdo_quote", "bd_cdo_quote":
|
||
return "CDO报价单"
|
||
default:
|
||
if taskKey == "" {
|
||
return "目标交付物"
|
||
}
|
||
return taskKey
|
||
}
|
||
}
|
||
|
||
// confirmGate 智能确认门禁:弱写动词 / 阶段不匹配 / 中等置信时先下发 PendingExpectation,不盲目执行。
|
||
// 返回非 nil 时 handleTriggerTask 应直接返回该结果。
|
||
func (e *CoordinatorEngine) confirmGate(
|
||
ctx context.Context,
|
||
cand *intent.CandidateIntent,
|
||
userCtx *UserProjectContext,
|
||
taskKey string,
|
||
) *CoordinationResult {
|
||
if cand == nil || userCtx == nil {
|
||
return nil
|
||
}
|
||
if cand.FilledSlots[intent.SlotWorkflowScopeMismatch] == "true" {
|
||
return nil
|
||
}
|
||
|
||
needsConfirm := cand.FilledSlots[intent.SlotNeedsConfirmation] == "true"
|
||
midConfidence := cand.Confidence >= 0.6 && cand.Confidence <= 0.85
|
||
|
||
stageMismatch := false
|
||
if e != nil && e.instSvc != nil && e.instSvc.Available() {
|
||
projectID := parseDecimalID(userCtx.Project.ID)
|
||
userID := parseDecimalID(userCtx.User.ID)
|
||
if projectID > 0 && userID > 0 && taskKey != "" {
|
||
isCreator := userCtx.Project.CreatorUserID > 0 && userID == userCtx.Project.CreatorUserID
|
||
d := e.instSvc.CheckDocTaskAccess(ctx, projectID, userID, taskKey, projectinstance.DocAccessActionRun, isCreator)
|
||
if !d.Allowed && d.Code == projectinstance.AccessCodeNotCurrentStage {
|
||
stageMismatch = true
|
||
}
|
||
}
|
||
}
|
||
|
||
if !needsConfirm && !stageMismatch && !midConfidence {
|
||
return nil
|
||
}
|
||
|
||
title := taskDisplayTitle(taskKey)
|
||
exp := buildTriggerConfirmExpectation(taskKey, title, userCtx, stageMismatch)
|
||
userCtx.applyPending(exp)
|
||
|
||
prompt := formatTriggerConfirmPrompt(title, needsConfirm, stageMismatch, midConfidence, userCtx)
|
||
return &CoordinationResult{
|
||
Status: StatusClarificationRequired,
|
||
Intent: cand.Intent,
|
||
Branch: cand.Branch,
|
||
ClarificationPrompt: prompt,
|
||
NaturalReply: prompt,
|
||
UpdatedContext: userCtx,
|
||
NextExpectation: exp,
|
||
}
|
||
}
|
||
|
||
func buildTriggerConfirmExpectation(
|
||
taskKey, title string,
|
||
userCtx *UserProjectContext,
|
||
stageMismatch bool,
|
||
) *intent.PendingExpectation {
|
||
optRun := intent.ExpectationOption{
|
||
OptionID: "1",
|
||
Title: fmt.Sprintf("开始整理并编写《%s》", title),
|
||
Description: "确认后启动正式文档任务,写入受控交付物",
|
||
Intent: intent.IntentTriggerTask,
|
||
Slots: map[string]string{
|
||
intent.SlotTargetTask: taskKey,
|
||
},
|
||
}
|
||
// 阶段不匹配时:默认改为当前阶段摸底表,避免再次撞门禁。
|
||
if stageMismatch {
|
||
stage := userCtx.Project.CurrentStage
|
||
if stage == "" || stage == maturity.StageV01_Triage {
|
||
optRun = intent.ExpectationOption{
|
||
OptionID: "1",
|
||
Title: "改为编制当前阶段《用户初始需求收集表》",
|
||
Description: "当前仍在 V0.1 摸底期,先完成初始需求收集更合适",
|
||
Intent: intent.IntentTriggerTask,
|
||
Slots: map[string]string{
|
||
intent.SlotTargetTask: "intake",
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
optTalk := intent.ExpectationOption{
|
||
OptionID: "2",
|
||
Title: "先在对话中梳理,暂不启动文档任务",
|
||
Description: "继续对话细化需求要点,稍后再决定是否生成正式文档",
|
||
Intent: intent.IntentCasualChat,
|
||
}
|
||
|
||
options := []intent.ExpectationOption{optRun, optTalk}
|
||
if stageMismatch {
|
||
options = append(options, intent.ExpectationOption{
|
||
OptionID: "3",
|
||
Title: "先推进到下一成熟度阶段",
|
||
Description: "确认阶段准入后再编制后续文档",
|
||
Intent: intent.IntentAdvanceStage,
|
||
Slots: map[string]string{
|
||
intent.SlotTargetStage: string(nextStageAfter(userCtx.Project.CurrentStage)),
|
||
},
|
||
})
|
||
}
|
||
|
||
return &intent.PendingExpectation{
|
||
ExpectationID: fmt.Sprintf("confirm-trigger-%d", time.Now().UnixNano()),
|
||
ExpectedIntent: intent.IntentTriggerTask,
|
||
TargetAction: "confirm_trigger_task",
|
||
Prompt: fmt.Sprintf("是否开始编写《%s》?", title),
|
||
AffirmativeWord: "好",
|
||
DeclineWord: "先不",
|
||
ExpiresAt: time.Now().Add(30 * time.Minute),
|
||
ContextSnippet: fmt.Sprintf("候选任务:%s(%s)", title, taskKey),
|
||
DefaultOption: &optRun,
|
||
Options: options,
|
||
AllowAffirmative: true,
|
||
CreatedAt: time.Now(),
|
||
}
|
||
}
|
||
|
||
func nextStageAfter(stage maturity.MaturityStage) maturity.MaturityStage {
|
||
switch stage {
|
||
case maturity.StageV01_Triage, "":
|
||
return maturity.StageV05_Feasibility
|
||
case maturity.StageV05_Feasibility:
|
||
return maturity.StageV10_Quoting
|
||
default:
|
||
return maturity.StageV05_Feasibility
|
||
}
|
||
}
|
||
|
||
func formatTriggerConfirmPrompt(
|
||
title string,
|
||
needsConfirm, stageMismatch, midConfidence bool,
|
||
userCtx *UserProjectContext,
|
||
) string {
|
||
var b strings.Builder
|
||
switch {
|
||
case stageMismatch:
|
||
stageLabel := string(userCtx.Project.CurrentStage)
|
||
if stageLabel == "" {
|
||
stageLabel = "初期"
|
||
}
|
||
b.WriteString(fmt.Sprintf(
|
||
"您提到的《%s》不在当前活动阶段(%s)。为避免启动后失败,请先确认下一步:\n\n",
|
||
title, stageLabel,
|
||
))
|
||
b.WriteString("1. 改为编制当前阶段《用户初始需求收集表》(推荐,回复「好」或「1」)\n")
|
||
b.WriteString("2. 先在对话中梳理,暂不启动文档任务\n")
|
||
b.WriteString("3. 先推进到下一成熟度阶段\n")
|
||
case needsConfirm:
|
||
b.WriteString(fmt.Sprintf(
|
||
"我理解您希望先整理需求。更稳妥的做法是开始整理并编写《%s》。请确认:\n\n",
|
||
title,
|
||
))
|
||
b.WriteString(fmt.Sprintf("1. 开始整理并编写《%s》(推荐,回复「好」或「1」)\n", title))
|
||
b.WriteString("2. 先在对话中梳理,暂不启动文档任务\n")
|
||
default: // midConfidence
|
||
b.WriteString(fmt.Sprintf(
|
||
"我倾向于为您启动《%s》的编制任务,但表述略有歧义。请确认:\n\n",
|
||
title,
|
||
))
|
||
b.WriteString(fmt.Sprintf("1. 确认启动《%s》(回复「好」或「1」)\n", title))
|
||
b.WriteString("2. 先在对话中说明,暂不启动\n")
|
||
}
|
||
_ = midConfidence
|
||
return b.String()
|
||
}
|