209 lines
6.3 KiB
Go
209 lines
6.3 KiB
Go
// 织忆 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
|
||
}
|