Add typed jevdecide HTTP client (circuit, metrics) and coordinator DecideAssist wiring behind JEV_DECIDE_MODE (default off). Offline eval and synthetic soak evidence gate realtime/profile before assist rollout.
346 lines
12 KiB
Go
346 lines
12 KiB
Go
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.review_approve | docgov.approver_approve | docgov.reject | taskrun.resume | taskrun.restart
|
||
DocID string
|
||
RunID string
|
||
WorkDir string
|
||
Reason string
|
||
Comment string
|
||
Rollback vocabulary.DocumentState
|
||
TaskKey string
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
// 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.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.IntentImpactAnalyze:
|
||
coordRes, coordErr = e.handleImpactAnalyze(ctx, top, userCtx, recRes.RawInput)
|
||
|
||
case intent.IntentRevisePropose:
|
||
coordRes, coordErr = e.handleRevisePropose(ctx, top, userCtx, recRes.RawInput)
|
||
|
||
default:
|
||
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
|
||
coordRes = &CoordinationResult{
|
||
Status: StatusAccepted,
|
||
Intent: top.Intent,
|
||
Branch: recRes.Branch,
|
||
NaturalReply: "我已收到您的指示。针对当前项目,请问需要我为您查询进展、补充技术事实,还是推进方案论证?随时吩咐我。",
|
||
UpdatedContext: userCtx,
|
||
NextExpectation: nextExp,
|
||
}
|
||
}
|
||
|
||
if coordErr == nil && coordRes != nil {
|
||
e.enrichAdvisorBriefWithCases(ctx, coordRes.AdvisorBrief, userCtx, top.FilledSlots)
|
||
}
|
||
|
||
return coordRes, coordErr
|
||
}
|