Add dialoguectx packaging, conflictresolve adjudication with HITL routing, workspace context/dialogue/ injection (default on), and goose 00071.
226 lines
6.4 KiB
Go
226 lines
6.4 KiB
Go
package dialoguectx
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
func renderTurnsMD(turns []RichDialogueTurn, from, to time.Time) string {
|
||
var sb strings.Builder
|
||
sb.WriteString("# 项目成员对话近窗(带元数据原文)\n\n")
|
||
if len(turns) == 0 {
|
||
sb.WriteString("本窗口内无授权可见的共享对话。\n")
|
||
return sb.String()
|
||
}
|
||
sb.WriteString(fmt.Sprintf("- 覆盖窗口: %s → %s\n", from.Format("2006-01-02 15:04"), to.Format("2006-01-02 15:04")))
|
||
sb.WriteString(fmt.Sprintf("- 条数: %d\n\n", len(turns)))
|
||
for _, t := range turns {
|
||
roleLabel := "用户"
|
||
if t.Role == "assistant" {
|
||
roleLabel = "助手"
|
||
}
|
||
identity := t.SpeakerName
|
||
parts := make([]string, 0, 3)
|
||
if t.SystemRole != "" {
|
||
parts = append(parts, t.SystemRole)
|
||
}
|
||
if t.ProjectRole != "" {
|
||
parts = append(parts, t.ProjectRole)
|
||
}
|
||
if t.DocDuty != "" {
|
||
parts = append(parts, "duty:"+t.DocDuty)
|
||
}
|
||
if len(parts) > 0 {
|
||
identity = fmt.Sprintf("%s(%s)", t.SpeakerName, strings.Join(parts, "/"))
|
||
}
|
||
stage := ""
|
||
if t.StageKey != "" {
|
||
stage = " stage=" + t.StageKey
|
||
}
|
||
sb.WriteString(fmt.Sprintf("[%s] %s %s:%s %s\n",
|
||
t.CreatedAt.Format("2006-01-02 15:04"), identity, roleLabel, stage, t.Content))
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
func renderDigestMD(turns []RichDialogueTurn, pkg *Package) string {
|
||
var sb strings.Builder
|
||
sb.WriteString("# 项目对话纪要\n\n")
|
||
sb.WriteString("权威度:`collab_dialogue`(低于黑板事实与已批准文档;冲突时不得静默采信对话版本)\n\n")
|
||
if pkg != nil {
|
||
sb.WriteString(fmt.Sprintf("- 装配时间: %s\n", pkg.AssembledAt.Format(time.RFC3339)))
|
||
sb.WriteString(fmt.Sprintf("- 纳入轮次: %d(私有排除 %d / 阶段不可见 %d / 噪声 %d)\n",
|
||
pkg.TurnCount, pkg.ExcludedPrivate, pkg.ExcludedNoAccess, pkg.ExcludedNoise))
|
||
if !pkg.CoveredFrom.IsZero() {
|
||
sb.WriteString(fmt.Sprintf("- 覆盖窗口: %s → %s\n",
|
||
pkg.CoveredFrom.Format("2006-01-02"), pkg.CoveredTo.Format("2006-01-02")))
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
if len(turns) == 0 {
|
||
sb.WriteString("暂无可用共享对话纪要。\n")
|
||
return sb.String()
|
||
}
|
||
|
||
// Day buckets: take first user utterance snippet per day as headline proxy.
|
||
byDay := map[string][]string{}
|
||
dayOrder := []string{}
|
||
for _, t := range turns {
|
||
day := t.DayKey
|
||
if day == "" {
|
||
day = t.CreatedAt.Format("2006-01-02")
|
||
}
|
||
if _, ok := byDay[day]; !ok {
|
||
dayOrder = append(dayOrder, day)
|
||
}
|
||
if t.Role != "user" {
|
||
continue
|
||
}
|
||
if len(byDay[day]) >= 3 {
|
||
continue
|
||
}
|
||
byDay[day] = append(byDay[day], fmt.Sprintf("%s: %s", t.SpeakerName, truncateRunes(t.Content, 64)))
|
||
}
|
||
sb.WriteString("## 按日要点\n\n")
|
||
for _, day := range dayOrder {
|
||
pts := byDay[day]
|
||
if len(pts) == 0 {
|
||
sb.WriteString(fmt.Sprintf("- **%s**: (仅助手/系统可见消息)\n", day))
|
||
continue
|
||
}
|
||
sb.WriteString(fmt.Sprintf("- **%s**: %s\n", day, strings.Join(pts, ";")))
|
||
}
|
||
out := sb.String()
|
||
if utf8.RuneCountInString(out) > MaxDigestRunes {
|
||
out = truncateRunes(out, MaxDigestRunes)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Known blackboard-style keys we try to extract from user utterances.
|
||
var factKeyPatterns = []struct {
|
||
Key string
|
||
Markers []string
|
||
}{
|
||
{"device_type", []string{"器械类型", "产品类型", "device_type", "设备类型"}},
|
||
{"target_market", []string{"目标市场", "target_market", "市场定位"}},
|
||
{"intended_use", []string{"预期用途", "intended_use", "适用场景"}},
|
||
{"customer_name", []string{"客户名称", "客户是", "customer_name"}},
|
||
{"budget", []string{"预算", "budget"}},
|
||
{"classification", []string{"注册分类", "管理类别", "分类界定"}},
|
||
}
|
||
|
||
func extractFacts(turns []RichDialogueTurn) []DialogueFact {
|
||
out := make([]DialogueFact, 0)
|
||
seen := map[string]struct{}{}
|
||
for _, t := range turns {
|
||
if t.Role != "user" {
|
||
continue
|
||
}
|
||
content := strings.TrimSpace(t.Content)
|
||
for _, pat := range factKeyPatterns {
|
||
val, ok := extractAfterMarker(content, pat.Markers)
|
||
if !ok {
|
||
continue
|
||
}
|
||
dedupe := pat.Key + "=" + val
|
||
if _, exists := seen[dedupe]; exists {
|
||
continue
|
||
}
|
||
seen[dedupe] = struct{}{}
|
||
out = append(out, DialogueFact{
|
||
Key: pat.Key,
|
||
Value: val,
|
||
SourceMessageID: t.MessageID,
|
||
SpeakerID: t.SpeakerID,
|
||
SpeakerName: t.SpeakerName,
|
||
StatedAt: t.CreatedAt,
|
||
Authority: AuthorityCollabDialogue,
|
||
ExplicitEvidence: strings.Contains(content, "材料") || strings.Contains(content, "〔") || strings.Contains(content, "决定"),
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func extractAfterMarker(content string, markers []string) (string, bool) {
|
||
for _, m := range markers {
|
||
idx := strings.Index(content, m)
|
||
if idx < 0 {
|
||
continue
|
||
}
|
||
rest := strings.TrimSpace(content[idx+len(m):])
|
||
rest = strings.TrimLeft(rest, "::=是为 ")
|
||
rest = strings.TrimSpace(rest)
|
||
if rest == "" {
|
||
continue
|
||
}
|
||
// Cut at sentence boundary.
|
||
for _, sep := range []string{"。", ";", ";", "\n", ",且", "并"} {
|
||
if i := strings.Index(rest, sep); i > 0 {
|
||
rest = rest[:i]
|
||
break
|
||
}
|
||
}
|
||
rest = truncateRunes(strings.TrimSpace(rest), 80)
|
||
if rest == "" {
|
||
continue
|
||
}
|
||
return rest, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// FactsJSON returns pretty-printed facts payload.
|
||
func FactsJSON(facts []DialogueFact) []byte {
|
||
if facts == nil {
|
||
facts = []DialogueFact{}
|
||
}
|
||
b, _ := json.MarshalIndent(facts, "", " ")
|
||
return b
|
||
}
|
||
|
||
// RenderAssistantSnippet builds a short MemoryComposer fragment (≤400 runes).
|
||
func RenderAssistantSnippet(pkg *Package) string {
|
||
if pkg == nil || pkg.TurnCount == 0 {
|
||
return ""
|
||
}
|
||
var sb strings.Builder
|
||
sb.WriteString("【项目成员协同近况】(权威度低于黑板事实;冲突时以黑板为准)\n")
|
||
// First ~3 digest day lines.
|
||
lines := strings.Split(pkg.DigestMD, "\n")
|
||
n := 0
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if !strings.HasPrefix(line, "- **") {
|
||
continue
|
||
}
|
||
sb.WriteString(line)
|
||
sb.WriteString("\n")
|
||
n++
|
||
if n >= 3 {
|
||
break
|
||
}
|
||
}
|
||
if len(pkg.Facts) > 0 {
|
||
sb.WriteString("- 对话沉淀事实候选: ")
|
||
parts := make([]string, 0, 4)
|
||
for i, f := range pkg.Facts {
|
||
if i >= 4 {
|
||
break
|
||
}
|
||
parts = append(parts, fmt.Sprintf("%s=%s(%s)", f.Key, f.Value, f.SpeakerName))
|
||
}
|
||
sb.WriteString(strings.Join(parts, "; "))
|
||
sb.WriteString("\n")
|
||
}
|
||
out := sb.String()
|
||
if utf8.RuneCountInString(out) > 400 {
|
||
out = truncateRunes(out, 400)
|
||
}
|
||
return out
|
||
}
|