Add dialoguectx packaging, conflictresolve adjudication with HITL routing, workspace context/dialogue/ injection (default on), and goose 00071.
289 lines
12 KiB
Go
289 lines
12 KiB
Go
package coordinator
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/ctxmgr"
|
||
)
|
||
|
||
// dialogueCST 与 ctxmgr day_key 约定一致:固定 UTC+8,不依赖容器 tzdata。
|
||
var dialogueCST = time.FixedZone("CST", 8*3600)
|
||
|
||
var weekdayCN = map[time.Weekday]string{
|
||
time.Sunday: "周日",
|
||
time.Monday: "周一",
|
||
time.Tuesday: "周二",
|
||
time.Wednesday: "周三",
|
||
time.Thursday: "周四",
|
||
time.Friday: "周五",
|
||
time.Saturday: "周六",
|
||
}
|
||
|
||
// MemoryComposer 分层记忆编排器:遵循严格的十段式模板结构自上而下组装对话 Prompt
|
||
type MemoryComposer struct {
|
||
MaxWindowTurns int
|
||
// NowFn 可注入时钟;nil 时使用 time.Now(测试可固定)。
|
||
NowFn func() time.Time
|
||
}
|
||
|
||
// NewMemoryComposer 创建分层记忆编排器
|
||
func NewMemoryComposer() *MemoryComposer {
|
||
return &MemoryComposer{
|
||
MaxWindowTurns: dialogueHistoryMax,
|
||
NowFn: time.Now,
|
||
}
|
||
}
|
||
|
||
func (mc *MemoryComposer) now() time.Time {
|
||
if mc != nil && mc.NowFn != nil {
|
||
return mc.NowFn()
|
||
}
|
||
return time.Now()
|
||
}
|
||
|
||
// formatDialogueClock 渲染提示词用当前时间行(Asia/Shanghai / UTC+8)。
|
||
func formatDialogueClock(t time.Time) string {
|
||
local := t.In(dialogueCST)
|
||
wd := weekdayCN[local.Weekday()]
|
||
return fmt.Sprintf("%s (%s) %s (UTC+8)",
|
||
local.Format("2006-01-02"), wd, local.Format("15:04"))
|
||
}
|
||
|
||
// ComposeDialoguePrompt 依据结构化规范编排完整提示词
|
||
// 布局结构 (自上而下):
|
||
// 1. 【工作记忆】 task_state (DST) + PendingExpectation 挂起建议 + InteractiveCard 状态
|
||
// 2. 【项目基准与协同上下文】 项目/用户/器械/阶段/待办/黑板事实(最高优先级声明)
|
||
// 3. 【早期对话摘要】 压缩记忆(仅长会话出现)
|
||
// 4. 【近期对话】 短期记忆(滑动窗口近 M 轮,逐轮编号 + 范围标注)
|
||
// 5. 【相关历史片段】 按需语义检索 TopK≤3
|
||
// 6. 【参考知识】 法规 KB 命中(带 CiteKey 引用键)/ 标杆案例参考
|
||
// 7. 【用户偏好画像】 PersonalizedGuidance(沟通风格与重点关注)
|
||
// 8. 【本轮用户】 原文 + (结合上下文理解完整诉求: …)双呈现
|
||
// 9. 【答复要求】 优先级裁决、引用规范、隐私条款与输出格式
|
||
func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserProjectContext) string {
|
||
if userCtx == nil {
|
||
return fmt.Sprintf("【本轮用户】\n%s\n\n【答复要求】\n请结合上下文,以温和助理口吻作答,并写入 reply.md。", userInput)
|
||
}
|
||
|
||
var sb strings.Builder
|
||
|
||
// -------------------------------------------------------------
|
||
// 1. 【工作记忆】 (~200 字)
|
||
// -------------------------------------------------------------
|
||
var wmBuilder strings.Builder
|
||
hasWorkingMemory := false
|
||
if userCtx.TaskState != nil {
|
||
if tsSnippet := userCtx.TaskState.RenderPromptSnippet(); tsSnippet != "" {
|
||
wmBuilder.WriteString(tsSnippet)
|
||
wmBuilder.WriteString("\n")
|
||
hasWorkingMemory = true
|
||
}
|
||
}
|
||
if userCtx.PendingExpectation != nil && userCtx.PendingExpectation.ContextSnippet != "" {
|
||
wmBuilder.WriteString(fmt.Sprintf("- 上一轮挂起建议摘要: %s\n", userCtx.PendingExpectation.ContextSnippet))
|
||
hasWorkingMemory = true
|
||
}
|
||
if userCtx.InteractiveCard != nil && userCtx.InteractiveCard.CurrentStatus == "active" {
|
||
wmBuilder.WriteString(fmt.Sprintf("- 当前待响应交互卡片: %s (剩余%d轮)\n", userCtx.InteractiveCard.Title, userCtx.InteractiveCard.TTLTurns))
|
||
hasWorkingMemory = true
|
||
}
|
||
if hasWorkingMemory {
|
||
sb.WriteString("【工作记忆】\n")
|
||
sb.WriteString(wmBuilder.String())
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 2. 【项目基准与协同上下文】 项目基准信息、待办事项、黑板事实 (声明最高优先级)
|
||
// -------------------------------------------------------------
|
||
sb.WriteString("【项目基准与协同上下文】\n")
|
||
sb.WriteString(fmt.Sprintf("- 当前时间: %s\n", formatDialogueClock(mc.now())))
|
||
sb.WriteString(fmt.Sprintf("- 当前项目: %s (ID: %s)\n", userCtx.Project.Name, userCtx.Project.ID))
|
||
sb.WriteString(fmt.Sprintf("- 阶段: %s | 当前工作流: %s\n", userCtx.Project.CurrentStage, userCtx.Project.CurrentWorkflow))
|
||
if userCtx.Project.DeviceType != "" || userCtx.Project.TargetMarket != "" {
|
||
sb.WriteString(fmt.Sprintf("- 器械与市场: %s | %s\n", userCtx.Project.DeviceType, userCtx.Project.TargetMarket))
|
||
}
|
||
if userCtx.User.Name != "" {
|
||
sb.WriteString(fmt.Sprintf("- 当前协同用户: %s (角色: %s, 项目角色: %s)\n", userCtx.User.Name, userCtx.User.Role, userCtx.User.ProjectRole))
|
||
}
|
||
|
||
if len(userCtx.Pending.ConfirmModifications) > 0 ||
|
||
len(userCtx.Pending.PendingReviews) > 0 ||
|
||
len(userCtx.Pending.PendingApprovals) > 0 ||
|
||
len(userCtx.Pending.MyDraftsInProgress) > 0 {
|
||
sb.WriteString("- 用户侧待办线索:\n")
|
||
for _, x := range userCtx.Pending.ConfirmModifications {
|
||
sb.WriteString(fmt.Sprintf(" - 待确认修改: %s\n", x))
|
||
}
|
||
for _, x := range userCtx.Pending.PendingReviews {
|
||
sb.WriteString(fmt.Sprintf(" - 待审核: %s\n", x))
|
||
}
|
||
for _, x := range userCtx.Pending.PendingApprovals {
|
||
sb.WriteString(fmt.Sprintf(" - 待批准: %s\n", x))
|
||
}
|
||
for _, x := range userCtx.Pending.MyDraftsInProgress {
|
||
sb.WriteString(fmt.Sprintf(" - 编制中: %s\n", x))
|
||
}
|
||
}
|
||
|
||
if len(userCtx.BlackboardFacts) > 0 {
|
||
sb.WriteString("- 项目黑板核心事实(最高优先级,与早期摘要或历史片段冲突时以此为准):\n")
|
||
n := 0
|
||
for k, v := range userCtx.BlackboardFacts {
|
||
sb.WriteString(fmt.Sprintf(" - %s = %s\n", k, v))
|
||
n++
|
||
if n >= 12 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if s := strings.TrimSpace(userCtx.ProjectDialogueSnippet); s != "" {
|
||
sb.WriteString(s)
|
||
if !strings.HasSuffix(s, "\n") {
|
||
sb.WriteString("\n")
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
|
||
// -------------------------------------------------------------
|
||
// 3. 【早期对话摘要】 压缩记忆 (~300 字)
|
||
// -------------------------------------------------------------
|
||
if s := strings.TrimSpace(userCtx.DialogueSummary); s != "" {
|
||
sb.WriteString(s)
|
||
if !strings.HasSuffix(s, "\n") {
|
||
sb.WriteString("\n")
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 4. 【近期对话】 滑动窗口近 M 轮 (~3000 字,含范围标注)
|
||
// -------------------------------------------------------------
|
||
if len(userCtx.RecentDialogue) > 0 {
|
||
totalTurns := len(userCtx.RecentDialogue)
|
||
turns := userCtx.RecentDialogue
|
||
startIdx := 1
|
||
if totalTurns > mc.MaxWindowTurns {
|
||
turns = turns[totalTurns-mc.MaxWindowTurns:]
|
||
startIdx = totalTurns - mc.MaxWindowTurns + 1
|
||
sb.WriteString(fmt.Sprintf("【近期对话】(第 %d~%d 轮,更早对话已压缩至摘要)\n", startIdx, totalTurns))
|
||
} else {
|
||
sb.WriteString("【近期对话】\n")
|
||
}
|
||
|
||
for i, t := range turns {
|
||
roleName := "用户"
|
||
if t.Role == "assistant" {
|
||
roleName = "助手"
|
||
} else if t.Role == "system" {
|
||
roleName = "系统"
|
||
}
|
||
sb.WriteString(fmt.Sprintf("[%d] %s: %s\n", startIdx+i, roleName, t.Content))
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 5. 【相关历史片段】 语义检索切片 TopK≤3 (~600 字)
|
||
// -------------------------------------------------------------
|
||
recalledText := strings.TrimSpace(userCtx.RecalledMemorySnippet)
|
||
if recalledText == "" && len(userCtx.RecalledMemories) > 0 {
|
||
recalledText = strings.TrimSpace(ctxmgr.FormatRecalledMemorySnippet(userCtx.RecalledMemories, userCtx.SnapshotID))
|
||
}
|
||
if recalledText != "" {
|
||
sb.WriteString(recalledText)
|
||
if !strings.HasSuffix(recalledText, "\n") {
|
||
sb.WriteString("\n")
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 6. 【参考知识】 法规 KB 命中(带 CiteKey)/ 往期标杆案例
|
||
// -------------------------------------------------------------
|
||
hasKnowledge := len(userCtx.RegulationHits) > 0 ||
|
||
(userCtx.LatestAdvisorBrief != nil && len(userCtx.LatestAdvisorBrief.RecommendedCases) > 0)
|
||
if hasKnowledge {
|
||
sb.WriteString("【参考知识】(权威法规与标准依据,涉及结论时请沿用〔引用键〕引用)\n")
|
||
for _, h := range userCtx.RegulationHits {
|
||
cite := h.CiteKey
|
||
if cite == "" {
|
||
cite = fmt.Sprintf("〔%s〕", h.DocCode)
|
||
}
|
||
title := h.Title
|
||
if title == "" {
|
||
title = h.DocCode
|
||
}
|
||
sb.WriteString(fmt.Sprintf("• %s %s\n", cite, title))
|
||
if h.HeadingPath != "" {
|
||
sb.WriteString(fmt.Sprintf(" - 条款章节: %s\n", h.HeadingPath))
|
||
}
|
||
if h.Content != "" {
|
||
cText := strings.TrimSpace(h.Content)
|
||
if len([]rune(cText)) > 150 {
|
||
cText = string([]rune(cText)[:150]) + "..."
|
||
}
|
||
sb.WriteString(fmt.Sprintf(" - 条款要点: %s\n", cText))
|
||
}
|
||
}
|
||
if userCtx.LatestAdvisorBrief != nil && len(userCtx.LatestAdvisorBrief.RecommendedCases) > 0 {
|
||
for _, c := range userCtx.LatestAdvisorBrief.RecommendedCases {
|
||
sb.WriteString(fmt.Sprintf("• 《%s》(%s): %s\n", c.ArtifactTitle, c.TaskKey, c.SummarySnippet))
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 6b. 【实时检索结果】 天气/新闻等权威实时数据(强制锚定)
|
||
// -------------------------------------------------------------
|
||
if snip := strings.TrimSpace(userCtx.RealtimeSnippet); snip != "" {
|
||
sb.WriteString("【实时检索结果】(权威实时数据,回答天气/新闻等只准以此为准)\n")
|
||
sb.WriteString(snip)
|
||
if !strings.HasSuffix(snip, "\n") {
|
||
sb.WriteString("\n")
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 7. 【用户偏好画像】 PersonalizedGuidance (~300 字)
|
||
// -------------------------------------------------------------
|
||
if g := strings.TrimSpace(userCtx.PersonalizedGuidance); g != "" {
|
||
sb.WriteString(g)
|
||
if !strings.HasSuffix(g, "\n") {
|
||
sb.WriteString("\n")
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// 8. 【本轮用户】
|
||
// -------------------------------------------------------------
|
||
sb.WriteString("【本轮用户】\n")
|
||
if userCtx.RewrittenQuery != "" && userCtx.RewrittenQuery != userInput {
|
||
sb.WriteString(fmt.Sprintf("用户原话: %s\n(结合上下文理解完整诉求: %s)", userInput, userCtx.RewrittenQuery))
|
||
} else {
|
||
sb.WriteString(userInput)
|
||
}
|
||
sb.WriteString("\n\n")
|
||
|
||
// -------------------------------------------------------------
|
||
// 9. 【答复要求】
|
||
// -------------------------------------------------------------
|
||
sb.WriteString("【答复要求】\n")
|
||
sb.WriteString("1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。\n")
|
||
sb.WriteString("2. 事实与引用:涉及法规条款与标准时,优先依据【参考知识】与【项目基准与协同上下文】并沿用〔引用键〕格式;无把握时明确提示依据不足,严禁生造标准号。\n")
|
||
sb.WriteString("3. 隐私与跨用户边界:其他成员姓名与私人习惯仅限理解语境,禁止外泄复述。\n")
|
||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。\n")
|
||
if userCtx.RealtimeUnavailable {
|
||
sb.WriteString("5. 本轮实时检索失败:必须明确告知无法获取实时数据(建议查看天气 App),严禁编造任何具体天气/新闻数值。\n")
|
||
} else if strings.TrimSpace(userCtx.RealtimeSnippet) != "" {
|
||
sb.WriteString("5. 实时数据:涉及天气/新闻等时效问题,必须严格依据【实时检索结果】作答,不得使用训练记忆中的过期数值。\n")
|
||
}
|
||
|
||
return sb.String()
|
||
}
|