Files
Agent-System/internal/coordinator/context.go
T
weichen dc29b84cdf feat(ctx): inject cleaned project dialogue into squad and assistant
Add dialoguectx packaging, conflictresolve adjudication with HITL routing,
workspace context/dialogue/ injection (default on), and goose 00071.
2026-09-24 17:04:04 +00:00

537 lines
20 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"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/maturity"
"chat-agent-system/internal/squad"
"chat-agent-system/internal/workspace"
)
// UserInfo 用户在系统与项目内的身份
type UserInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"` // 系统角色 (如 engineer, pm, director)
ProjectRole string `json:"project_role"` // 项目内角色 (如 编制人 composer, 审核人 reviewer, 批准人 approver)
}
// ProjectInfo 项目与工作流当前基准
type ProjectInfo struct {
ID string `json:"id"`
Name string `json:"name"`
CustomerName string `json:"customer_name"`
DeviceType string `json:"device_type"`
IntendedUse string `json:"intended_use"`
TargetMarket string `json:"target_market"`
CreatorUserID int64 `json:"creator_user_id,omitempty"`
ActiveWorkflowKey string `json:"active_workflow_key,omitempty"`
WorkflowSelectionVersion int `json:"workflow_selection_version,omitempty"`
CurrentWorkflow string `json:"current_workflow"`
CurrentStage maturity.MaturityStage `json:"current_stage"`
StageRules []string `json:"stage_rules,omitempty"`
}
// UserDocsInfo 用户相关文档清单与认领状态
type UserDocsInfo struct {
Composed []string `json:"composed"` // 负责编制的文档
Reviewed []string `json:"reviewed"` // 负责审核的文档
Approved []string `json:"approved"` // 负责批准的文档
Claimed []string `json:"claimed"` // 已认领/已打开查阅的文档
}
// PendingItems 待用户处理或决策的待办项
type PendingItems struct {
ConfirmModifications []string `json:"confirm_modifications,omitempty"`
PendingReviews []string `json:"pending_reviews,omitempty"`
PendingApprovals []string `json:"pending_approvals,omitempty"`
MyDraftsInProgress []string `json:"my_drafts_in_progress,omitempty"`
UnreadSupplements []string `json:"unread_supplements,omitempty"`
}
// ReadableDocument 是已通过项目与阶段权限过滤后的只读文档快照。
// Markdown 只用于当轮总结,不写入上下文快照或对话元数据。
type ReadableDocument struct {
DocID string
Title string
TaskKey string
LifecycleState string
Version int
Markdown string
IsTemplateOnly bool
// WorkflowKey 是文档归属工作流。CrossWorkflow 为真时正文来自非激活流,仅只读。
WorkflowKey string
ActiveWorkflow string
CrossWorkflow bool
}
// DocumentSummaryReader 由 HTTP 服务端按当前登录用户和项目绑定。
// 协调层不得直接枚举文档,以免绕过阶段可见性规则。
type DocumentSummaryReader func(ctx context.Context, target string) (*ReadableDocument, error)
// ErrWrongWorkflow 表示目标文档存在但不属于当前激活工作流。
// 服务端文档读取器按激活流收窄匹配池后,用该错误携带「请先切换工作流」引导话术,
// 协调层识别后原样回复,不执行任何读/写动作。
type ErrWrongWorkflow struct {
Message string
}
func (e *ErrWrongWorkflow) Error() string { return e.Message }
// WrongWorkflowGuidance 若错误链上是跨工作流拦截,返回引导话术;否则空串。
func WrongWorkflowGuidance(err error) string {
var target *ErrWrongWorkflow
if errors.As(err, &target) && target != nil && strings.TrimSpace(target.Message) != "" {
return target.Message
}
return ""
}
// ErrDocViewDenied 表示跨工作流文档存在,但当前用户无权查看。
type ErrDocViewDenied struct {
Message string
}
func (e *ErrDocViewDenied) Error() string {
if e == nil {
return "document view denied"
}
return e.Message
}
// DocViewDeniedGuidance 若错误链是跨流查看拒绝,返回话术。
func DocViewDeniedGuidance(err error) string {
var target *ErrDocViewDenied
if errors.As(err, &target) && target != nil && strings.TrimSpace(target.Message) != "" {
return target.Message
}
return ""
}
// ErrNoUploadAsset 表示项目内尚无可总结的上传附件。
type ErrNoUploadAsset struct {
Message string
}
func (e *ErrNoUploadAsset) Error() string {
if e == nil || strings.TrimSpace(e.Message) == "" {
return "no upload asset in project"
}
return e.Message
}
// NoUploadAssetGuidance 返回无上传附件时的话术。
func NoUploadAssetGuidance(err error) string {
var target *ErrNoUploadAsset
if errors.As(err, &target) && target != nil {
if msg := strings.TrimSpace(target.Message); msg != "" {
return msg
}
return "当前项目还没有上传过附件。请先在对话中上传文件后再试总结。"
}
return ""
}
// ErrUploadParseState 表示最近上传附件仍在解析中或解析失败,尚无可总结正文。
type ErrUploadParseState struct {
State string
Filename string
Message string
}
func (e *ErrUploadParseState) Error() string {
if e == nil {
return "upload parse state unavailable"
}
if msg := strings.TrimSpace(e.Message); msg != "" {
return msg
}
name := strings.TrimSpace(e.Filename)
if name == "" {
name = "该附件"
}
switch strings.TrimSpace(e.State) {
case "pending", "running":
return fmt.Sprintf("《%s》仍在解析中,请稍后再试总结。", name)
case "failed":
return fmt.Sprintf("《%s》解析失败。可在附件卡片点击重新解析后再试总结。", name)
case "slow":
return fmt.Sprintf("《%s》解析耗时较长(仍在后台进行),请稍后再让我总结。", name)
default:
return fmt.Sprintf("《%s》暂时无法读取正文,请稍后再试。", name)
}
}
// UploadParseStateGuidance 返回上传解析状态提示话术。
func UploadParseStateGuidance(err error) string {
var target *ErrUploadParseState
if errors.As(err, &target) && target != nil {
return target.Error()
}
return ""
}
// UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费)
type UserProjectContext struct {
mu sync.Mutex
Version int64 `json:"version"`
SnapshotID string `json:"snapshot_id"`
AssembledAt time.Time `json:"assembled_at"`
User UserInfo `json:"user"`
Project ProjectInfo `json:"project"`
UserDocs UserDocsInfo `json:"user_docs"`
Pending PendingItems `json:"pending"`
BlackboardFacts map[string]string `json:"blackboard_facts"`
LatestAdvisorBrief *squad.AdvisorBriefing `json:"latest_advisor_brief,omitempty"`
PendingExpectation *intent.PendingExpectation `json:"pending_expectation,omitempty"`
InteractiveCard *intent.InteractiveCardPayload `json:"interactive_card,omitempty"`
AvailableActions []string `json:"available_actions"`
RecentEvents []string `json:"recent_events,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
// RecentDialogue 近轮对话(供 Phase A 对话 LLM 使用,不序列化到快照)
RecentDialogue []DialogueTurn `json:"-"`
// DialogueSummary 早期对话滚动压缩摘要(由 ctxmgr 维护生成,不序列化到快照)
DialogueSummary string `json:"-"`
// ProjectDialogueSnippet 项目成员协同近况(跨会话共享对话摘要,权威度低于黑板)
ProjectDialogueSnippet string `json:"-"`
// TaskState 会话级任务状态(DST,记录 active_task, topic_anchor, collected_slots 等)
TaskState *ctxmgr.ConversationTaskState `json:"task_state,omitempty"`
// PersonalizedGuidance 用户工作习惯/偏好注入语(由 ctxmgr 编译,供对话 LLM 使用)
PersonalizedGuidance string `json:"-"`
// RecalledMemories 按需语义召回的相关历史片段(TopK ≤ 3)
RecalledMemories []ctxmgr.RecalledMemoryItem `json:"-"`
// RecalledMemorySnippet 格式化后的相关历史片段 Prompt 文本
RecalledMemorySnippet string `json:"-"`
// RewrittenQuery 规则改写补全后的完整独立问句(用于检索与 Prompt 双呈现,不进意图识别)
RewrittenQuery string `json:"-"`
// RewriteRules 本轮命中的改写规则列表(如 ellipsis_completion, pronoun_resolution)
RewriteRules []string `json:"-"`
// RegulationHits 知识库法规与强标检索命中项(带 CiteKey 引用键,供对话层精准引用)
RegulationHits []kb.HitItem `json:"-"`
// UpgradeOffered 路由升级建议冷却标记(每会话至多主动建议一次,防打扰——对齐"参谋不强推"人设)
UpgradeOffered bool `json:"-"`
// DocumentSummaryReader 仅用于服务端鉴权后的只读正文访问,不序列化。
DocumentSummaryReader DocumentSummaryReader `json:"-"`
// RealtimeSnippet 本轮实时检索(天气/新闻等)成功结果,供 Prompt 注入;不序列化。
RealtimeSnippet string `json:"-"`
// RealtimeUnavailable 本轮命中实时查询但检索失败/空结果,强制拒答约束;不序列化。
RealtimeUnavailable bool `json:"-"`
}
// ContextAssembler 上下文组装服务
type ContextAssembler struct{}
func NewContextAssembler() *ContextAssembler {
return &ContextAssembler{}
}
// ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合
func ComputeAvailableActions(stage maturity.MaturityStage, pRole string) []string {
actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}
switch stage {
case maturity.StageV01_Triage:
actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility")
case maturity.StageV05_Feasibility:
actions = append(actions,
"TRIGGER_TASK:research",
"TRIGGER_TASK:feasibility_reg",
"TRIGGER_TASK:prelim_design",
"ADVANCE_STAGE:V1.0_quoting",
)
case maturity.StageV10_Quoting:
actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL")
default:
actions = append(actions, "ADVANCE_STAGE:V0.1_triage")
}
return actions
}
// ComputeAvailableActionsWithWorkflow 结合激活工作流、成熟度阶段与角色权限计算可用操作集合
func ComputeAvailableActionsWithWorkflow(stage maturity.MaturityStage, pRole string, activeWorkflow string, isCreator bool) []string {
actions := ComputeAvailableActions(stage, pRole)
actions = appendUniqueCapability(actions, "QUERY_WORKFLOW")
if isCreator {
actions = appendUniqueCapability(actions, "SWITCH_WORKFLOW")
}
switch activeWorkflow {
case "product_manager":
actions = appendUniqueCapability(actions, "TRIGGER_TASK:system_reqs", "TRIGGER_WORKFLOW:product_manager")
case "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
}
// ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照
func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot {
if ctx == nil {
return nil
}
snap := &intent.ContextSnapshot{
SnapshotID: ctx.SnapshotID,
ProjectName: ctx.Project.Name,
CurrentStage: string(ctx.Project.CurrentStage),
ActiveWorkflowKey: ctx.Project.ActiveWorkflowKey,
AvailableActions: ctx.AvailableActions,
PendingExpectation: ctx.PendingExpectation,
}
if ctx.User.ProjectRole != "" {
snap.UserDocRole = ctx.User.ProjectRole
} else if ctx.User.Role != "" {
snap.UserDocRole = ctx.User.Role
}
if ctx.InteractiveCard != nil && ctx.InteractiveCard.CurrentStatus == "active" {
snap.ActiveDocID = ctx.InteractiveCard.TargetDocID
if ctx.InteractiveCard.TargetDocID != "" {
snap.DocLifecycleState = "reviewing"
}
}
return snap
}
// BuildNextExpectation 根据当前成熟度阶段与项目名构造参谋下一步建议选项草稿
func BuildNextExpectation(stage maturity.MaturityStage, projectName string) *intent.PendingExpectation {
if projectName == "" {
projectName = "本项目"
}
var options []intent.ExpectationOption
var snippet string
switch stage {
case maturity.StageV01_Triage:
snippet = fmt.Sprintf("项目「%s」当前处于初始摸底期,基准产品画像已梳理完成。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "进入 V0.5 方案成型与法规对齐",
Description: "自动比选技术路线,并梳理分类路径与型检标准",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV05_Feasibility),
},
},
{
OptionID: "2",
Title: "补充或微调事实信息",
Description: "随时告知我客户的最新的预算、目标市场或使用场景",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "查阅当前立项成熟度与对外沟通建议",
Description: "获取进展研判与对外商务沟通话术",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "all",
},
},
}
case maturity.StageV05_Feasibility:
snippet = fmt.Sprintf("项目「%s」已完成方案成型与法规对齐,具备进入商务核算的条件。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "推进到 V1.0 商业锁定阶段",
Description: "生成精细化 CDO 工时与成本核算表,锁定立项商务方案",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV10_Quoting),
},
},
{
OptionID: "2",
Title: "微调设计方案或技术参数",
Description: "进一步指定元器件品牌、特殊工艺或交付节点",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "审阅初步系统架构图与法规分类论证说明",
Description: "查看技术路线比选报告与检验标准分析",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "progress",
},
},
}
case maturity.StageV10_Quoting:
snippet = fmt.Sprintf("项目「%s」已完成商务锁定,报价核算表已就绪。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "重新核算或调整报价单参数",
Description: "调整费率、工时模型或元器件采购预算",
Intent: intent.IntentTriggerTask,
Slots: map[string]string{
intent.SlotTargetTask: "cdo_quote",
},
},
{
OptionID: "2",
Title: "生成面向客户负责人的正式沟通函件",
Description: "草拟发给客户的商务方案总揽与下一步推进排期",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "talk_script",
},
},
}
default:
snippet = fmt.Sprintf("项目「%s」当前处于初始准备阶段。", projectName)
options = []intent.ExpectationOption{
{
OptionID: "1",
Title: "启动 V0.1 闪电摸底",
Description: "快速沉淀第一版立项画像并为您准备沟通话术",
Intent: intent.IntentAdvanceStage,
Slots: map[string]string{
intent.SlotTargetStage: string(maturity.StageV01_Triage),
},
},
}
}
var defaultOpt *intent.ExpectationOption
if len(options) > 0 {
defaultOpt = &options[0]
}
return &intent.PendingExpectation{
ContextSnippet: snippet,
DefaultOption: defaultOpt,
Options: options,
AllowAffirmative: true,
CreatedAt: time.Now(),
}
}
// AssembleFromWorkspace 基于现有工作区快速组装 UserProjectContext
func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo) (*UserProjectContext, error) {
snapID := fmt.Sprintf("ctx_%d", time.Now().UnixNano())
ctx := &UserProjectContext{
SnapshotID: snapID,
AssembledAt: time.Now(),
User: user,
WorkDir: workDir,
BlackboardFacts: make(map[string]string),
AvailableActions: []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"},
}
// 1. 尝试读取 workspace.json
wsMetaPath := filepath.Join(workDir, "workspace.json")
if data, err := os.ReadFile(wsMetaPath); err == nil {
var wsMeta squad.WorkspaceMetadata
if json.Unmarshal(data, &wsMeta) == nil {
ctx.Project.Name = wsMeta.ProjectName
ctx.Project.CustomerName = wsMeta.CustomerName
ctx.Project.DeviceType = wsMeta.DeviceType
ctx.Project.IntendedUse = wsMeta.IntendedUse
ctx.Project.TargetMarket = wsMeta.TargetMarket
ctx.Project.CurrentWorkflow = wsMeta.TaskKey
}
} else {
// 备选:从目录名推断
baseName := filepath.Base(workDir)
if strings.HasPrefix(baseName, "intake_") {
ctx.Project.CurrentWorkflow = "intake"
}
}
// 2. 尝试读取 blackboard.json
bbPath := filepath.Join(workDir, "blackboard.json")
if data, err := os.ReadFile(bbPath); err == nil {
var bb squad.BlackboardState
if json.Unmarshal(data, &bb) == nil {
for k, v := range bb.Fields {
ctx.BlackboardFacts[k] = v
}
for _, g := range bb.Gaps {
if g.Blocking && !g.Resolved {
ctx.Pending.ConfirmModifications = append(ctx.Pending.ConfirmModifications, g.Item)
}
}
}
}
// 3. 尝试提取阶段与文档
if ctx.Project.ActiveWorkflowKey != "" {
ctx.Project.CurrentWorkflow = ctx.Project.ActiveWorkflowKey
}
if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.1") || ctx.Project.CurrentWorkflow == "intake" {
ctx.Project.CurrentStage = maturity.StageV01_Triage
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") {
ctx.Project.CurrentStage = maturity.StageV05_Feasibility
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") {
ctx.Project.CurrentStage = maturity.StageV10_Quoting
}
if ctx.Project.CurrentStage == "" {
ctx.Project.CurrentStage = maturity.StageV01_Triage
}
isCreator := false
if user.ID != "" && ctx.Project.CreatorUserID > 0 {
uid, _ := strconv.ParseInt(user.ID, 10, 64)
isCreator = (uid == ctx.Project.CreatorUserID)
}
ctx.AvailableActions = ComputeAvailableActionsWithWorkflow(ctx.Project.CurrentStage, user.ProjectRole, ctx.Project.ActiveWorkflowKey, isCreator)
// 4. 优先尝试直接读取持久化的 advisor_brief.json
briefPath := filepath.Join(workDir, "advisor_brief.json")
if data, err := os.ReadFile(briefPath); err == nil {
var brief squad.AdvisorBriefing
if json.Unmarshal(data, &brief) == nil {
ctx.LatestAdvisorBrief = &brief
}
}
// 5. 若无 advisor_brief.json 但有交付正文,尝试从文本抽取
if ctx.LatestAdvisorBrief == nil {
outPath := filepath.Join(workDir, workspace.DeliverableKey)
if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 {
ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts)
ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow)
}
}
return ctx, nil
}