memoryweave/go/internal/api/routes/consolidation_pipe.go

273 lines
8.0 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.

// 织忆 MemoryWeave — Consolidation 流水线 (Step 1-4)
package routes
import (
"fmt"
"log"
"strings"
"time"
"github.com/xiaoxue/memoryweave/internal/consolidate"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// ConsolidationPipeline 完整整合流水线
// 优先调用 Rust sidecarDBSCAN + 衰减校准 + 质量回溯)
// Rust 不可用时降级为 Go 启发式
type ConsolidationPipeline struct {
ldb storage.LanceDB
graph governance.GraphStore
graphUpdater *governance.AutoGraphUpdater
conflicts *governance.ConflictDetector
// Rust IPC 路径
dataDir string
sqlitePath string
}
func NewConsolidationPipeline(ldb storage.LanceDB, g governance.GraphStore, cd *governance.ConflictDetector) *ConsolidationPipeline {
return &ConsolidationPipeline{
ldb: ldb,
graph: g,
graphUpdater: governance.NewAutoGraphUpdater(g),
conflicts: cd,
}
}
// SetDataDir 设置 Rust IPC 所需的路径(不设置则只走 Go 启发式)
func (cp *ConsolidationPipeline) SetDataDir(dataDir, sqlitePath string) {
cp.dataDir = dataDir
cp.sqlitePath = sqlitePath
}
// Run 执行全流程
// 优先调 Rust zhiyi-consolidateDBSCAN + 衰减校准 + 质量回溯)
// Rust 不可用 → 降级为 Go 启发式
func (cp *ConsolidationPipeline) Run() (*ConsolidationReport, error) {
// ─── 尝试 Rust sidecar ──────────────────────────────
if cp.dataDir != "" && cp.sqlitePath != "" {
if rustReport, err := consolidate.Run(cp.dataDir, cp.sqlitePath, "full"); err == nil {
report := &ConsolidationReport{
StartedAt: time.Now(),
FinishedAt: time.Now(),
Duration: "rust_sidecar",
Merged: rustReport.Clusters,
ConflictsFound: 0,
Patterns: []string{fmt.Sprintf("decay_rates=%v", rustReport.DecayRates)},
GraphPruned: rustReport.Noise,
}
if rustReport.Quality != nil {
report.Patterns = append(report.Patterns,
fmt.Sprintf("quality_score=%.2f low_info=%d hallucinations=%d",
rustReport.Quality.Score, rustReport.Quality.LowInfo, rustReport.Quality.Hallucinations))
}
log.Printf("[consolidation] Rust sidecar 完成: clusters=%d noise=%d", rustReport.Clusters, rustReport.Noise)
PushConsolidationDone(fmt.Sprintf("rust: merged=%d conflicts=%d patterns=%d pruned=%d",
report.Merged, report.ConflictsFound, len(report.Patterns), report.GraphPruned))
return report, nil
}
log.Printf("[consolidation] Rust sidecar 不可用,降级为 Go 启发式")
}
return cp.runGoFallback()
}
// runGoFallback Go 启发式整合Rust 不可用时的降级方案)
func (cp *ConsolidationPipeline) runGoFallback() (*ConsolidationReport, error) {
report := &ConsolidationReport{StartedAt: time.Now()}
// Step 1: 合并相似记忆
merged, err := cp.mergeSimilar()
if err != nil {
report.Errors = append(report.Errors, "merge: "+err.Error())
} else {
report.Merged = merged
}
// Step 2: 扫描冲突
found, err := cp.scanConflicts()
if err != nil {
report.Errors = append(report.Errors, "conflicts: "+err.Error())
} else {
report.ConflictsFound = found
}
// Step 3: 模式挖掘
patterns, err := cp.minePatterns()
if err != nil {
report.Errors = append(report.Errors, "patterns: "+err.Error())
} else {
report.Patterns = patterns
}
// Step 4: 图谱更新
pruned, err := cp.updateGraph()
if err != nil {
report.Errors = append(report.Errors, "graph: "+err.Error())
} else {
report.GraphPruned = pruned
}
report.FinishedAt = time.Now()
report.Duration = report.FinishedAt.Sub(report.StartedAt).String()
// 推送完成事件
PushConsolidationDone(fmt.Sprintf("merged=%d conflicts=%d patterns=%d pruned=%d",
report.Merged, report.ConflictsFound, len(report.Patterns), report.GraphPruned))
return report, nil
}
// RunMerge 公开合并步骤(供触发器 merge 单独使用)
func (cp *ConsolidationPipeline) RunMerge() (int, error) {
return cp.mergeSimilar()
}
// mergeSimilar 合并相似记忆(向量相似度 > 0.8 → 保留最新)
func (cp *ConsolidationPipeline) mergeSimilar() (int, error) {
// 获取全部 distilled 记忆
memories, err := cp.ldb.GetCandidatesForForgetting()
if err != nil {
return 0, err
}
merged := 0
// 简单启发式:相同 category + 高内容重叠 → 合并
for i := 0; i < len(memories); i++ {
for j := i + 1; j < len(memories); j++ {
catI := strVal(memories[i]["category"])
catJ := strVal(memories[j]["category"])
if catI != catJ {
continue
}
contentI := strVal(memories[i]["content"])
contentJ := strVal(memories[j]["content"])
if overlap := contentOverlap(contentI, contentJ); overlap > 0.8 {
// 保留较新的
idI := strVal(memories[i]["id"])
idJ := strVal(memories[j]["id"])
_ = cp.ldb.SoftDelete(idJ, fmt.Sprintf("merged_into_%s", idI))
merged++
}
}
}
return merged, nil
}
// Step 2: 扫描所有待解决冲突
func (cp *ConsolidationPipeline) scanConflicts() (int, error) {
// 获取最近 100 条记忆
memories, err := cp.ldb.GetCandidatesForForgetting()
if err != nil {
return 0, err
}
count := 0
for i := 0; i < len(memories); i++ {
content := strVal(memories[i]["content"])
entities := extractEntities(content)
if len(entities) == 0 {
continue
}
// 对比已有内容
existing := make([]map[string]interface{}, 0)
for j := 0; j < len(memories) && j < 50; j++ {
if i != j {
existing = append(existing, memories[j])
}
}
conflicts := cp.conflicts.Scan(content, entities, existing)
if len(conflicts) > 0 {
count += len(conflicts)
// 自动裁决:来源信任差异 > 0.5
for _, c := range conflicts {
if c.Strategy == "latest_wins" {
cp.conflicts.AutoResolve(c)
}
}
}
}
return count, nil
}
// Step 3: 模式挖掘(连续 3+ 同 category 记忆 → 提取 pattern
func (cp *ConsolidationPipeline) minePatterns() ([]string, error) {
memories, err := cp.ldb.GetCandidatesForForgetting()
if err != nil {
return nil, err
}
catCount := make(map[string]int)
for _, mem := range memories {
cat := strVal(mem["category"])
catCount[cat]++
}
var patterns []string
for cat, count := range catCount {
if count >= 3 {
patterns = append(patterns, fmt.Sprintf("pattern:%s (count=%d)", cat, count))
}
}
return patterns, nil
}
// Step 4: 图谱修剪
func (cp *ConsolidationPipeline) updateGraph() (int, error) {
before, _, _ := cp.graph.Stats()
cp.graph.Prune(0.1) // 删除权重 < 0.1 的边
after, _, _ := cp.graph.Stats()
return before - after, nil
}
// ─── 报告 ─────────────────────────────────────────────
type ConsolidationReport struct {
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
Duration string `json:"duration"`
Merged int `json:"merged"`
ConflictsFound int `json:"conflicts_found"`
Patterns []string `json:"patterns"`
GraphPruned int `json:"graph_pruned"`
Errors []string `json:"errors,omitempty"`
}
// ─── 辅助 ─────────────────────────────────────────────
func contentOverlap(a, b string) float64 {
wordsA := strings.Fields(strings.ToLower(a))
wordsB := strings.Fields(strings.ToLower(b))
setA := make(map[string]bool, len(wordsA))
for _, w := range wordsA {
setA[w] = true
}
overlap := 0
for _, w := range wordsB {
if setA[w] {
overlap++
}
}
if len(wordsA) == 0 || len(wordsB) == 0 {
return 0
}
return float64(overlap) / float64(minInt2(len(wordsA), len(wordsB)))
}
func extractEntities(text string) []string {
words := strings.Fields(text)
var entities []string
for _, w := range words {
if len(w) > 1 && w[0] >= 'A' && w[0] <= 'Z' {
entities = append(entities, w)
}
}
return entities
}
func minInt2(a, b int) int {
if a < b { return a }
return b
}