Files
Agent-System/internal/coordinator/engine.go
T
weichen 457e870330 fix(chat): 只上传文档时给出梳理或追问,不再裸报 HTTP 400
空消息加附件可以进入回合。无文字时按解析状态引导或回退审阅,有文字但未处理文档时先回答再追问。4xx 信封不再被前端丢弃。
2026-09-28 02:00:24 +00:00

422 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package coordinator
import (
"context"
"fmt"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/app/stageflow"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/stream"
)
// CoordinationStatus 协调裁决状态
type CoordinationStatus string
const (
StatusAccepted CoordinationStatus = "ACCEPTED"
StatusRejected CoordinationStatus = "REJECTED"
StatusClarificationRequired CoordinationStatus = "CLARIFICATION_REQUIRED"
)
// PendingAction 声明对话层需执行的真实动作(由 Session 持有服务后执行)。
type PendingAction struct {
Kind string // docgov.claim | docgov.submit_review | docgov.review_approve | docgov.approver_approve | docgov.reject | docgov.comment_add | docgov.comment_resolve | docgov.reopen_request | docgov.reopen_approve | workflow.run | taskrun.resume | taskrun.restart
DocID string
WorkflowKey string
RunID string
WorkDir string
Reason string
Comment string
Rollback vocabulary.DocumentState
TaskKey string
Role string
Section string
RequestID int64
}
// CoordinationResult 协调层给对话层的回执
type CoordinationResult struct {
Status CoordinationStatus `json:"status"`
Intent intent.IntentType `json:"intent"`
Branch intent.ResolutionBranch `json:"branch,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
ClarificationPrompt string `json:"clarification_prompt,omitempty"`
NaturalReply string `json:"natural_reply"`
AdvisorBrief *squad.AdvisorBriefing `json:"advisor_brief,omitempty"`
ArtifactPath string `json:"artifact_path,omitempty"`
UpdatedContext *UserProjectContext `json:"updated_context"`
InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"`
NextExpectation *intent.PendingExpectation `json:"next_expectation,omitempty"`
ClearExpectation bool `json:"clear_expectation,omitempty"`
ReplySource string `json:"reply_source,omitempty"` // pi-agent | template
PendingAction *PendingAction `json:"-"`
// Task projection for clickable document entry (sync TRIGGER_TASK / adhoc).
TaskStatus string `json:"task_status,omitempty"`
DocID string `json:"doc_id,omitempty"`
ArtifactTitle string `json:"artifact_title,omitempty"`
TaskKey string `json:"task_key,omitempty"`
PendingAgentCandidateID int64 `json:"pending_agent_candidate_id,omitempty"`
PendingChangeReportID int64 `json:"pending_change_report_id,omitempty"`
PendingRiskLevel string `json:"pending_risk_level,omitempty"`
}
// CoordinatorEngine L2 协调引擎
type CoordinatorEngine struct {
stageTasks StageTaskRunner
instSvc *projectinstance.Service
assembler *ContextAssembler
dialogueBackend agent.Backend
realtimeSearcher RealtimeSearcher
decideAssist DecideAssist
dataDir string
ctxMgr *ctxmgr.ContextManager
kbClient *kb.Client
impact ImpactActions
projectAccess ProjectAccess
docResolver DocResolver
pendingSupps PendingSupplementReader
reopenLister ReopenLister
// OnSupplement is optional; composition typically wires supplementimpact.TriggerAsync.
// Nil-safe: unset hooks are skipped.
OnSupplement func(ctx context.Context, projectID, userID int64, sessionID, summary, markdown string)
pendingSupplementCard *intent.InteractiveCardPayload
}
// StageTaskRunner is the application owner for ExecuteTask / ExecuteStage.
type StageTaskRunner interface {
ExecuteTask(ctx context.Context, req stageflow.TaskRunRequest) (*stageflow.TaskRunResult, error)
ExecuteStage(ctx context.Context, req stageflow.StageRunRequest) (*stageflow.StageRunResult, error)
}
func (e *CoordinatorEngine) SetStageTaskRunner(r StageTaskRunner) {
if e != nil {
e.stageTasks = r
}
}
func (e *CoordinatorEngine) tasks() StageTaskRunner {
if e == nil {
return nil
}
return e.stageTasks
}
func (e *CoordinatorEngine) SetContextServices(ctxMgr *ctxmgr.ContextManager, kbClient *kb.Client) {
if e != nil {
e.ctxMgr = ctxMgr
e.kbClient = kbClient
}
}
// SetInstanceService 注入项目工作流实例服务(对话推进与实例门禁合流)。
func (e *CoordinatorEngine) SetInstanceService(svc *projectinstance.Service) {
if e != nil {
e.instSvc = svc
}
}
// SetRealtimeSearcher 注入对话路径的实时检索(天气/新闻等);nil 时跳过强制检索。
func (e *CoordinatorEngine) SetRealtimeSearcher(s RealtimeSearcher) {
if e != nil {
e.realtimeSearcher = s
}
}
// SetImpactActions wires read-only impact analysis and confirmed revision proposals.
func (e *CoordinatorEngine) SetImpactActions(a ImpactActions) {
if e != nil {
e.impact = a
}
}
// SetDocResolver wires the controlled-document resolution gate.
func (e *CoordinatorEngine) SetDocResolver(r DocResolver) {
if e != nil {
e.docResolver = r
}
}
// SetPendingSupplementReader wires locked-period pending supplement queries.
func (e *CoordinatorEngine) SetPendingSupplementReader(r PendingSupplementReader) {
if e != nil {
e.pendingSupps = r
}
}
// SetReopenLister wires pending reopen listing for approve dialogue.
func (e *CoordinatorEngine) SetReopenLister(r ReopenLister) {
if e != nil {
e.reopenLister = r
}
}
// SetOnSupplement wires the optional post-blackboard supplement impact hook.
func (e *CoordinatorEngine) SetOnSupplement(fn func(ctx context.Context, projectID, userID int64, sessionID, summary, markdown string)) {
if e != nil {
e.OnSupplement = fn
}
}
// HasDocResolver reports whether a document resolution gate is already wired.
func (e *CoordinatorEngine) HasDocResolver() bool {
return e != nil && e.docResolver != nil
}
func (e *CoordinatorEngine) SetPendingSupplementCard(card *intent.InteractiveCardPayload) {
if e == nil {
return
}
e.pendingSupplementCard = card
}
func (e *CoordinatorEngine) TakePendingSupplementCard() *intent.InteractiveCardPayload {
if e == nil {
return nil
}
c := e.pendingSupplementCard
e.pendingSupplementCard = nil
return c
}
func NewCoordinatorEngine(runner StageTaskRunner) *CoordinatorEngine {
return &CoordinatorEngine{
stageTasks: runner,
assembler: NewContextAssembler(),
}
}
// NewCoordinatorEngineWithDialogue 注入对话 LLM 后端(Phase A:CASUAL / AMBIGUOUS)
func NewCoordinatorEngineWithDialogue(runner StageTaskRunner, dialogueBackend agent.Backend, dataDir string) *CoordinatorEngine {
return &CoordinatorEngine{
stageTasks: runner,
assembler: NewContextAssembler(),
dialogueBackend: dialogueBackend,
dataDir: dataDir,
}
}
// ProcessIntent 校验候选意图合法性并驱动调度与响应
func (e *CoordinatorEngine) ProcessIntent(
ctx context.Context,
recRes *intent.IntentRecognitionResult,
userCtx *UserProjectContext,
) (*CoordinationResult, error) {
if userCtx == nil {
userCtx = &UserProjectContext{
User: UserInfo{ID: "guest", Name: "用户", Role: "engineer", ProjectRole: "composer"},
BlackboardFacts: make(map[string]string),
}
}
// 交互卡片 TTL 轮次消退与生命周期清理
if userCtx.InteractiveCard != nil {
_ = ApplyContextUpdate(userCtx, ContextUpdate{ExpectedVersion: userCtx.Version, TickInteractive: true})
}
if userCtx.PendingExpectation != nil && !userCtx.PendingExpectation.ExpiresAt.IsZero() && time.Now().After(userCtx.PendingExpectation.ExpiresAt) {
userCtx.applyPending(nil)
}
top := recRes.TopIntent
if top == nil {
top = &intent.CandidateIntent{
Intent: intent.IntentCasualChat,
Confidence: 1.0,
}
}
if userCtx.AttachmentOnlyTurn && len(userCtx.PendingTurnAttachments) > 0 {
return e.handleUploadIntake(ctx, userCtx)
}
if top.FilledSlots["upload_action"] == uploadActionReviseGuidance {
return handleUploadReviseGuidance(top, userCtx), nil
}
// Phase 5:RankedFrames 写/读冲突二次门禁
if intent.RankedFrameConflictRequiresClarification(recRes) {
msg := "检测到查询与写操作意图接近,为避免误执行,请明确您是要查询状态,还是要执行变更操作?"
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: top.Intent,
Branch: recRes.Branch,
ClarificationPrompt: msg,
NaturalReply: msg,
UpdatedContext: userCtx,
}, nil
}
// Phase 5:有限多意图计划(query+query / expectation_plus_new)
if recRes.IntentPlan != nil && intent.CurrentFeatureFlags().MultiIntentEnabled {
if planRes, planErr := e.executeIntentPlan(ctx, recRes, userCtx); planRes != nil {
return planRes, planErr
}
}
// Phase 5:V2 灰度路由(intent_v2_decision 开启且档位允许时对齐 V2 帧)
if effective := intent.ResolveEffectiveIntent(recRes); effective != top.Intent {
top = &intent.CandidateIntent{
Intent: effective,
Confidence: top.Confidence,
Branch: top.Branch,
FilledSlots: top.FilledSlots,
MissingSlots: top.MissingSlots,
Reasoning: top.Reasoning + " (v2_decision)",
ClarificationQuestion: top.ClarificationQuestion,
MatchedOption: top.MatchedOption,
PatchSlots: top.PatchSlots,
IsDecline: top.IsDecline,
}
recRes.TopIntent = top
}
streamer := stream.FromContext(ctx)
if streamer != nil {
streamer.EmitStatus(string(top.Intent), string(recRes.Branch), string(userCtx.Project.CurrentStage), fmt.Sprintf("意图判定: %s (置信度: %.0f%%)", top.Intent, top.Confidence*100), top.FilledSlots)
}
logRouteDecision(ctx, recRes, RouteIntent(top.Intent, TaskProfile{}))
// 1. 若缺少核心槽位且需定向澄清(优先于模糊咨询,避免缺槽追问被参谋兜底截断)
if recRes.RequiresClarifying && top.ClarificationQuestion != "" {
return &CoordinationResult{
Status: StatusClarificationRequired,
Intent: top.Intent,
Branch: recRes.Branch,
ClarificationPrompt: top.ClarificationQuestion,
NaturalReply: top.ClarificationQuestion,
UpdatedContext: userCtx,
}, nil
}
// 2. 若为分支 3 (模糊/分忧引导),触发参谋主动分析与建议(Phase A:优先真实 LLM)
if recRes.Branch == intent.BranchAmbiguousFallback {
res, err := e.handleAmbiguousConsult(ctx, top, userCtx, recRes.RawInput)
if err == nil && res != nil {
e.enrichAdvisorBriefWithCases(ctx, res.AdvisorBrief, userCtx, top.FilledSlots)
}
return res, err
}
// 3. 根据候选意图执行分支
var coordRes *CoordinationResult
var coordErr error
switch top.Intent {
case intent.IntentDeclineExpectation:
coordRes, coordErr = e.handleDeclineExpectation(userCtx)
case intent.IntentQueryProgress:
coordRes, coordErr = e.handleQueryProgress(ctx, top, userCtx, recRes.RawInput)
case intent.IntentSupplementInfo:
coordRes, coordErr = e.handleSupplementInfo(top, userCtx)
case intent.IntentAdvanceStage:
coordRes, coordErr = e.handleAdvanceStage(ctx, top, userCtx)
case intent.IntentTriggerTask:
coordRes, coordErr = e.handleTriggerTask(ctx, top, userCtx)
case intent.IntentTriggerAdhocTask:
coordRes, coordErr = e.handleTriggerAdhocTask(ctx, top, userCtx, recRes.RawInput)
case intent.IntentCasualChat:
coordRes, coordErr = e.handleCasualChat(ctx, userCtx, recRes.RawInput)
case intent.IntentSecurityForbidden:
coordRes, coordErr = e.handleSecurityForbidden(userCtx)
case intent.IntentDocGovReviewReject, intent.IntentDocGovFinalReject:
coordRes, coordErr = e.handleDocGovReject(ctx, top, userCtx)
case intent.IntentQueryRejectReason:
coordRes, coordErr = e.handleQueryRejectReason(ctx, top, userCtx)
case intent.IntentDocGovReviewApprove, intent.IntentDocGovFinalApprove:
coordRes, coordErr = e.handleDocGovApprove(ctx, top, userCtx)
case intent.IntentDocGovClaim:
coordRes, coordErr = e.handleDocGovClaim(ctx, top, userCtx)
case intent.IntentDocGovSubmitReview:
coordRes, coordErr = e.handleDocGovSubmitReview(ctx, top, userCtx)
case intent.IntentDocGovCommentAdd:
coordRes, coordErr = e.handleDocGovCommentAdd(ctx, top, userCtx)
case intent.IntentDocGovCommentResolve:
coordRes, coordErr = e.handleDocGovCommentResolve(ctx, top, userCtx)
case intent.IntentDocGovReopenRequest:
coordRes, coordErr = e.handleDocGovReopenRequest(ctx, top, userCtx)
case intent.IntentDocGovReopenApprove:
coordRes, coordErr = e.handleDocGovReopenApprove(ctx, top, userCtx)
case intent.IntentQueryPendingSupplements:
coordRes, coordErr = e.handleQueryPendingSupplements(ctx, top, userCtx)
case intent.IntentDocGovProposalDecide:
coordRes, coordErr = e.handleDocGovProposalDecide(ctx, top, userCtx)
case intent.IntentQueryRegulations:
coordRes, coordErr = e.handleQueryRegulations(ctx, top, userCtx)
case intent.IntentQuerySimilarCases:
coordRes, coordErr = e.handleQuerySimilarCases(ctx, top, userCtx)
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)
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)
case intent.IntentConfirmAction:
coordRes, coordErr = e.handleConfirmActionHit(top, userCtx, recRes.Branch)
case intent.IntentImpactAnalyze:
coordRes, coordErr = e.handleImpactAnalyze(ctx, top, userCtx, recRes.RawInput)
case intent.IntentRevisePropose:
coordRes, coordErr = e.handleRevisePropose(ctx, top, userCtx, recRes.RawInput)
default:
coordRes = e.genericAcknowledgement(top, userCtx, recRes.Branch)
}
if coordErr == nil && coordRes != nil {
e.enrichAdvisorBriefWithCases(ctx, coordRes.AdvisorBrief, userCtx, top.FilledSlots)
}
return coordRes, coordErr
}