memoryweave/go/internal/governance/graph_auto.go

209 lines
6.3 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 — 知识图谱自动更新引擎
package governance
import (
"fmt"
"strings"
"time"
"unicode"
)
// AutoGraphUpdater 自动维护知识图谱
type AutoGraphUpdater struct {
graph GraphStore
}
func NewAutoGraphUpdater(g GraphStore) *AutoGraphUpdater {
return &AutoGraphUpdater{graph: g}
}
// UpdateFromDistill 从蒸馏产物自动更新图谱§3.4
func (agu *AutoGraphUpdater) UpdateFromDistill(distilled *DistillInput) {
// 1. 提取实体并创建节点ID 和 name 都清洗)
for _, entity := range distilled.Entities {
nodeID := entityID(entity)
cleanName := cleanEntityName(entity)
agu.graph.AddNode(nodeID, cleanName, detectNodeType(entity), distilled.Namespace)
}
// 2. 创建实体间关系 + CO_OCCURS 共访边
entityCount := len(distilled.Entities)
for i := 0; i < entityCount; i++ {
for j := i + 1; j < entityCount; j++ {
eidI := entityID(distilled.Entities[i])
eidJ := entityID(distilled.Entities[j])
// 2a. 语义关系边inferRelation
edgeID := fmt.Sprintf("e_%s_%s_%d", eidI, eidJ, time.Now().UnixNano())
relation := inferRelation(distilled.Entities[i], distilled.Entities[j], distilled.Content)
agu.graph.AddEdge(edgeID, eidI, eidJ,
relation, distilled.Namespace, 0.5)
// 2b. CO_OCCURS 共访边§2.5.2 来源 2
// 权重 = 1 / sqrt(entityCount) — 实体越多单对权重越低
coWeight := 1.0
if entityCount > 2 {
coWeight = 1.0 / sqrtFloat(float64(entityCount))
}
coEdgeID := fmt.Sprintf("co_%s_%s_%d", eidI, eidJ, time.Now().UnixNano())
agu.graph.AddEdge(coEdgeID, eidI, eidJ,
"CO_OCCURS", distilled.Namespace, coWeight)
}
}
// 3. 按内容类别创建冲突检测边 → 使用设计规范的 CONFLICTS_WITH 类型
for _, fact := range distilled.Facts {
entities := extractEntitiesFromText(fact)
for i := 0; i < len(entities); i++ {
for j := i + 1; j < len(entities); j++ {
if conflictPossible(entities[i], entities[j], fact) {
eidI := entityID(entities[i])
eidJ := entityID(entities[j])
edgeID := fmt.Sprintf("cw_%s_%s_%d", eidI, eidJ, time.Now().UnixNano())
agu.graph.AddEdge(edgeID, eidI, eidJ,
"CONFLICTS_WITH", distilled.Namespace, 0.3)
}
}
}
}
// 4. DERIVED_FROM 边:蒸馏产物 → 原始 episode
if distilled.EpisodeID != "" {
for _, entity := range distilled.Entities {
eid := entityID(entity)
derivedEdgeID := fmt.Sprintf("df_%s_%s_%d", eid, distilled.EpisodeID, time.Now().UnixNano())
agu.graph.AddEdge(derivedEdgeID, eid, distilled.EpisodeID,
"DERIVED_FROM", distilled.Namespace, 0.9)
}
for _, fact := range distilled.Facts {
factNodeID := "f_" + strings.ReplaceAll(entityID(fact), "n_", "")
derivedEdgeID := fmt.Sprintf("df_%s_%s_%d", factNodeID, distilled.EpisodeID, time.Now().UnixNano())
agu.graph.AddEdge(derivedEdgeID, factNodeID, distilled.EpisodeID,
"DERIVED_FROM", distilled.Namespace, 0.9)
}
}
}
// RecordCoOccurrence record recall 后的共访关系
func (agu *AutoGraphUpdater) RecordCoOccurrence(query, namespace string, resultIDs []string) {
// 在知识图谱中创建 CO_OCCURS 关系
for i := 0; i < len(resultIDs); i++ {
for j := i + 1; j < len(resultIDs); j++ {
edgeID := fmt.Sprintf("co_%s_%s_%s", resultIDs[i], resultIDs[j], query[:minz(len(query), 20)])
agu.graph.AddEdge(edgeID, resultIDs[i], resultIDs[j], "CO_OCCURS", namespace, 1.0)
}
}
}
// ─── 辅助 ─────────────────────────────────────────────
type DistillInput struct {
EpisodeID string
Content string
Facts []string
Decisions []string
Entities []string
Namespace string
}
func entityID(name string) string {
// 保留 Unicode 字母、数字、下划线、中横线、空格(转下划线)
clean := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == ' ' {
return r
}
// 保留 Unicode 字母(如中文)
if unicode.IsLetter(r) {
return r
}
return '_' // 特殊字符→下划线
}, strings.ToLower(strings.TrimSpace(name)))
// 空格转下划线
clean = strings.ReplaceAll(clean, " ", "_")
// 合并连续下划线
for strings.Contains(clean, "__") {
clean = strings.ReplaceAll(clean, "__", "_")
}
return "n_" + strings.Trim(clean, "_")
}
// cleanEntityName 清洗实体名用于展示(保留 Unicode 字符)
func cleanEntityName(raw string) string {
clean := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == ' ' || r == '-' || r == '_' {
return r
}
// 保留 Unicode 字母(中文、韩文、日文等)
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return r
}
return ' '
}, strings.TrimSpace(raw))
clean = strings.Join(strings.Fields(clean), " ")
if len(clean) > 80 {
clean = clean[:77] + "..."
}
if clean == "" {
clean = "unknown"
}
return clean
}
func detectNodeType(name string) string {
if strings.Contains(strings.ToLower(name), "docker") || strings.Contains(strings.ToLower(name), "nginx") {
return "software"
}
if strings.Contains(name, "牧尘") {
return "person"
}
return "concept"
}
func inferRelation(a, b, content string) string {
contentLower := strings.ToLower(content)
if strings.Contains(contentLower, "使用") || strings.Contains(contentLower, "used_by") || strings.Contains(contentLower, "用") {
return "uses"
}
if strings.Contains(contentLower, "配置") || strings.Contains(contentLower, "config") {
return "configures"
}
return "related_to"
}
func conflictPossible(a, b, fact string) bool {
return strings.Contains(strings.ToLower(fact), "not") ||
strings.Contains(strings.ToLower(fact), "不") ||
strings.Contains(strings.ToLower(fact), "false")
}
func extractEntitiesFromText(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 minz(a, b int) int {
if a < b { return a }
return b
}
func sqrtFloat(x float64) float64 {
if x <= 0 {
return 0
}
// Newton's method for sqrt
z := x / 2.0
for i := 0; i < 10; i++ {
z -= (z*z - x) / (2 * z)
}
if z < 0 {
return 0
}
return z
}