191 lines
4.6 KiB
Go
191 lines
4.6 KiB
Go
// 织忆 MemoryWeave — 图谱扩展器(供 Recall 管线调用)
|
||
package governance
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/xiaoxue/memoryweave/internal/models"
|
||
)
|
||
|
||
// ExpandFromResults 从 recall 结果出发,双向 BFS 扩展图谱邻接节点
|
||
// 实现 GraphStore 接口
|
||
func (g *InMemoryGraph) ExpandFromResults(results []models.RecallResult, namespace string, maxHops int) []models.RecallResult {
|
||
var expanded []models.RecallResult
|
||
seen := make(map[string]bool)
|
||
|
||
// 收集已有结果 ID
|
||
for _, r := range results {
|
||
seen[r.ID] = true
|
||
}
|
||
|
||
// 从每个结果出发扩展
|
||
for _, r := range results {
|
||
paths, err := g.Navigate(r.Category, maxHops, namespace, nil)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, p := range paths {
|
||
to, _ := p["to"].(string)
|
||
from, _ := p["from"].(string)
|
||
|
||
for _, id := range []string{to, from} {
|
||
if id != "" && !seen[id] {
|
||
seen[id] = true
|
||
expanded = append(expanded, models.RecallResult{
|
||
ID: id,
|
||
Category: "graph_expanded",
|
||
Score: 0.5, // 图谱扩展降权
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return expanded
|
||
}
|
||
|
||
// ExpandWithSummary BFS 扩展 + 生成汇总语句 — E1 图谱导航增强
|
||
func (g *InMemoryGraph) ExpandWithSummary(results []models.RecallResult, namespace string, maxHops int) models.GraphBFSResult {
|
||
if maxHops <= 0 {
|
||
maxHops = 2
|
||
}
|
||
|
||
seenEntities := make(map[string]bool)
|
||
var relations []models.ExpandedRelation
|
||
|
||
// 从 recall 结果提取实体
|
||
for _, r := range results {
|
||
entities := extractPotentialEntitiesFromContent(r.Content)
|
||
for _, entity := range entities {
|
||
if seenEntities[entity] {
|
||
continue
|
||
}
|
||
seenEntities[entity] = true
|
||
|
||
nodeID := normalizeEntityID(entity)
|
||
paths, _ := g.Navigate(nodeID, maxHops, namespace, nil)
|
||
for _, p := range paths {
|
||
from, _ := p["source"].(string)
|
||
to, _ := p["target"].(string)
|
||
rel, _ := p["relation"].(string)
|
||
weight, _ := p["weight"].(float64)
|
||
hop, _ := p["hop"].(int)
|
||
|
||
fromName := strings.TrimPrefix(from, "n_")
|
||
toName := strings.TrimPrefix(to, "n_")
|
||
|
||
rel = strings.TrimSpace(rel)
|
||
if rel == "" {
|
||
rel = "RELATED_TO"
|
||
}
|
||
|
||
relations = append(relations, models.ExpandedRelation{
|
||
From: fromName,
|
||
To: toName,
|
||
Relation: rel,
|
||
Hops: hop,
|
||
Weight: weight,
|
||
Score: r.Score * weight,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
summary := buildBFSSummary(relations)
|
||
return models.GraphBFSResult{
|
||
ExpandedRelations: relations,
|
||
Summary: summary,
|
||
}
|
||
}
|
||
|
||
// extractPotentialEntitiesFromContent 从文本提取实体(InMemoryGraph 用)
|
||
func extractPotentialEntitiesFromContent(text string) []string {
|
||
var entities []string
|
||
seen := make(map[string]bool)
|
||
runes := []rune(text)
|
||
for i := 0; i < len(runes); {
|
||
r := runes[i]
|
||
// 中文字符
|
||
if r >= 0x4E00 && r <= 0x9FFF {
|
||
start := i
|
||
i++
|
||
for i < len(runes) && runes[i] >= 0x4E00 && runes[i] <= 0x9FFF {
|
||
i++
|
||
}
|
||
chinese := string(runes[start:i])
|
||
if len(chinese) >= 2 && len(chinese) <= 8 && !seen[chinese] {
|
||
seen[chinese] = true
|
||
entities = append(entities, chinese)
|
||
}
|
||
continue
|
||
}
|
||
// 英文/其他
|
||
start := i
|
||
for i < len(runes) {
|
||
r2 := runes[i]
|
||
if r2 >= 0x4E00 && r2 <= 0x9FFF {
|
||
break
|
||
}
|
||
i++
|
||
}
|
||
if i-start < 2 {
|
||
continue
|
||
}
|
||
w := string(runes[start:i])
|
||
w = strings.Trim(w, ",.;:!?,。;:!?、\"'()()[]【】")
|
||
if len(w) < 2 {
|
||
continue
|
||
}
|
||
first := []rune(w)
|
||
if len(first) > 0 && first[0] >= 'A' && first[0] <= 'Z' {
|
||
lower := strings.ToLower(w)
|
||
if !seen[lower] {
|
||
seen[lower] = true
|
||
entities = append(entities, w)
|
||
}
|
||
}
|
||
}
|
||
return entities
|
||
}
|
||
|
||
// buildBFSSummary 从扩展关系列表生成一句话汇总
|
||
func buildBFSSummary(relations []models.ExpandedRelation) string {
|
||
if len(relations) == 0 {
|
||
return "未发现图谱关联"
|
||
}
|
||
if len(relations) == 1 {
|
||
r := relations[0]
|
||
return fmt.Sprintf("%s --[%s]--> %s(%d跳,权重%.2f)", r.From, r.Relation, r.To, r.Hops, r.Weight)
|
||
}
|
||
|
||
relCounts := make(map[string]int)
|
||
var totalWeight float64
|
||
maxHops := 0
|
||
for _, r := range relations {
|
||
relCounts[r.Relation]++
|
||
totalWeight += r.Weight
|
||
if r.Hops > maxHops {
|
||
maxHops = r.Hops
|
||
}
|
||
}
|
||
|
||
var topRel string
|
||
topCount := 0
|
||
for rel, cnt := range relCounts {
|
||
if cnt > topCount {
|
||
topCount = cnt
|
||
topRel = rel
|
||
}
|
||
}
|
||
|
||
avgWeight := totalWeight / float64(len(relations))
|
||
uniqueEntities := make(map[string]bool)
|
||
for _, r := range relations {
|
||
uniqueEntities[r.From] = true
|
||
uniqueEntities[r.To] = true
|
||
}
|
||
|
||
return fmt.Sprintf("发现 %d 条关联(跨越 %d 个实体,最深 %d 跳),关系以 [%s] 为主(%d 条),平均权重 %.2f",
|
||
len(relations), len(uniqueEntities), maxHops, topRel, topCount, avgWeight)
|
||
}
|