Files
Agent-System/internal/coordinator/context.go
T

317 lines
11 KiB
Go

package coordinator
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/squad"
)
// 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"`
CurrentWorkflow string `json:"current_workflow"`
CurrentStage bizdev.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"`
}
// UserProjectContext 用户项目级全量统一上下文 (对话系统与执行系统共同消费)
type UserProjectContext struct {
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"`
AvailableActions []string `json:"available_actions"`
RecentEvents []string `json:"recent_events,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
}
// ContextAssembler 上下文组装服务
type ContextAssembler struct{}
func NewContextAssembler() *ContextAssembler {
return &ContextAssembler{}
}
// ComputeAvailableActions 根据当前成熟度阶段与角色计算允许执行的操作集合
func ComputeAvailableActions(stage bizdev.MaturityStage, pRole string) []string {
actions := []string{"QUERY_PROGRESS", "SUPPLEMENT_INFO", "VIEW_DOC"}
switch stage {
case bizdev.StageV01_Triage:
actions = append(actions, "TRIGGER_TASK:intake", "ADVANCE_STAGE:V0.5_feasibility")
case bizdev.StageV05_Feasibility:
actions = append(actions,
"TRIGGER_TASK:research",
"TRIGGER_TASK:feasibility_reg",
"TRIGGER_TASK:prelim_design",
"ADVANCE_STAGE:V1.0_quoting",
)
case bizdev.StageV10_Quoting:
actions = append(actions, "TRIGGER_TASK:cdo_quote", "LOCK_COMMERCIAL")
default:
actions = append(actions, "ADVANCE_STAGE:V0.1_triage")
}
return actions
}
// ToSnapshot 转换为 L1 意图识别器所需的轻量上下文快照
func (ctx *UserProjectContext) ToSnapshot() *intent.ContextSnapshot {
if ctx == nil {
return nil
}
return &intent.ContextSnapshot{
SnapshotID: ctx.SnapshotID,
ProjectName: ctx.Project.Name,
CurrentStage: string(ctx.Project.CurrentStage),
AvailableActions: ctx.AvailableActions,
PendingExpectation: ctx.PendingExpectation,
}
}
// BuildNextExpectation 根据当前成熟度阶段与项目名构造参谋下一步建议选项草稿
func BuildNextExpectation(stage bizdev.MaturityStage, projectName string) *intent.PendingExpectation {
if projectName == "" {
projectName = "本项目"
}
var options []intent.ExpectationOption
var snippet string
switch stage {
case bizdev.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(bizdev.StageV05_Feasibility),
},
},
{
OptionID: "2",
Title: "补充或微调事实信息",
Description: "随时告知我客户的最新的预算、目标市场或使用场景",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "查阅当前立项成熟度与对外沟通建议",
Description: "获取参谋深度研判与对外商务沟通话术",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "all",
},
},
}
case bizdev.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(bizdev.StageV10_Quoting),
},
},
{
OptionID: "2",
Title: "微调设计方案或技术参数",
Description: "进一步指定元器件品牌、特殊工艺或交付节点",
Intent: intent.IntentSupplementInfo,
},
{
OptionID: "3",
Title: "审阅初步系统架构图与法规分类论证说明",
Description: "查看技术路线比选报告与检验标准分析",
Intent: intent.IntentQueryProgress,
Slots: map[string]string{
intent.SlotQueryTopic: "progress",
},
},
}
case bizdev.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(bizdev.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 strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.1") || ctx.Project.CurrentWorkflow == "intake" {
ctx.Project.CurrentStage = bizdev.StageV01_Triage
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V0.5") {
ctx.Project.CurrentStage = bizdev.StageV05_Feasibility
} else if strings.HasPrefix(ctx.Project.CurrentWorkflow, "V1.0") {
ctx.Project.CurrentStage = bizdev.StageV10_Quoting
}
if ctx.Project.CurrentStage == "" {
ctx.Project.CurrentStage = bizdev.StageV01_Triage
}
ctx.AvailableActions = ComputeAvailableActions(ctx.Project.CurrentStage, user.ProjectRole)
// 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 但有 output.md,尝试从文本抽取
if ctx.LatestAdvisorBrief == nil {
outPath := filepath.Join(workDir, "output.md")
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
}