fix E4.3: extract entities from content instead of namespace

E4.3 bug: admin.go used mem["namespace"] which is empty (LanceDB
map keys are different from struct field names). Fixed by:
- Replace namespace lookup with content-based entity extraction
- Reuse distill/engine.go heuristic: capitalized words, Chinese
  entities (2-20 chars), tech tokens (alphanumeric/digits)
- Get max graph degree across all extracted entities
- Add isStopWord, stripNonChinese, isTechToken, isAllDigits helpers

Fixes: degree always 0 in ShouldForget call, graph integration broken.
This commit is contained in:
xiaowei 2026-06-01 22:49:50 +08:00
parent 525b5f2bb2
commit df2b648973
1 changed files with 121 additions and 13 deletions

View File

@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/xiaoxue/memoryweave/internal/governance"
@ -68,13 +69,10 @@ func (aa *AdminAPI) Forget(w http.ResponseWriter, r *http.Request) {
lastAccess := parseTimeStr(mem["last_recalled_at"])
recallCnt := intVal(mem["recall_count"])
tier := strVal(mem["tier"])
content := strVal(mem["content"])
// E4.3: 获取实体图谱度,取 namespace 和 content 中实体的最大度
entity := strVal(mem["namespace"])
degree := 0
if entity != "" {
degree = aa.GraphStore.GetEntityDegree(entity)
}
// E4.3 修复: 从 content 提取实体(而非读 namespace取图谱最大度
degree := extractTopEntityDegree(content, aa.GraphStore)
if aa.Forgetter.ShouldForget(lastAccess, recallCnt, tier, degree) {
aa.LanceDB.SoftDelete(strVal(mem["id"]), "auto_forget")
@ -141,7 +139,8 @@ func (aa *AdminAPI) Audit(w http.ResponseWriter, r *http.Request) {
respond(w, 200, map[string]interface{}{"audit_logs": logs, "count": len(logs)})
}
// 辅助函数
// ─── 辅助函数 ───────────────────────────────────────────────────────────────
func parseTimeStr(s interface{}) time.Time {
if s == nil {
return time.Time{}
@ -158,10 +157,10 @@ func parseTimeStr(s interface{}) time.Time {
func intVal(v interface{}) int {
switch n := v.(type) {
case int: return n
case int32: return int(n)
case int64: return int(n)
case float64: return int(n)
case int: return n
case int32: return int(n)
case int64: return int(n)
case float64: return int(n)
case json.Number:
i, _ := n.Int64()
return int(i)
@ -174,8 +173,117 @@ func strVal(v interface{}) string {
return ""
}
switch s := v.(type) {
case string: return s
case json.Number: return s.String()
case string: return s
case json.Number: return s.String()
}
return fmt.Sprintf("%v", v)
}
// ─── E4.3: 图谱度提取 ──────────────────────────────────────────────────────
// extractTopEntityDegree 从 content 提取实体,返回图中度数最高的实体度数
// 复用 distill/engine.go 的启发式逻辑(大写单词、中文实体、技术标记)
func extractTopEntityDegree(content string, gs governance.GraphStore) int {
if content == "" || gs == nil {
return 0
}
seen := make(map[string]bool)
var candidates []string
words := strings.Fields(content)
for _, w := range words {
w = strings.Trim(w, ",.;:!?,。;:!?、\"'()[]【】")
if len(w) < 2 {
continue
}
// 大写字母开头的英文词Hermes, ComfyUI, Redis 等)
runes := []rune(w)
if len(runes) >= 2 && runes[0] >= 'A' && runes[0] <= 'Z' {
normalized := strings.ToLower(w)
if !seen[normalized] && !isStopWord(normalized) {
seen[normalized] = true
candidates = append(candidates, w)
}
}
// 中文实体2-20 个纯中文字符)
cleanChinese := stripNonChinese(w)
if len(cleanChinese) >= 2 && len(cleanChinese) <= 20 {
if !seen[cleanChinese] {
seen[cleanChinese] = true
candidates = append(candidates, cleanChinese)
}
}
// 技术标记(数字+字母组合或纯数字)
if isTechToken(w) || isAllDigits(w) {
if !seen[w] {
seen[w] = true
candidates = append(candidates, w)
}
}
}
maxDegree := 0
for _, entity := range candidates {
d := gs.GetEntityDegree(entity)
if d > maxDegree {
maxDegree = d
}
}
return maxDegree
}
// isStopWord 停用词表(与 distill/engine.go 保持一致)
func isStopWord(w string) bool {
stops := []string{
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
"her", "was", "one", "our", "out", "this", "that", "with", "from",
"your", "what", "when", "where", "which", "their", "will", "would",
"there", "could", "other", "into", "just", "has", "have", "were",
"they", "been", "more", "than",
}
for _, s := range stops {
if w == s {
return true
}
}
return false
}
// stripNonChinese 提取纯中文字符串
func stripNonChinese(s string) string {
var result []rune
for _, r := range s {
if r >= 0x4E00 && r <= 0x9FFF {
result = append(result, r)
}
}
return string(result)
}
// isTechToken 判断是否为技术标记(数字+字母混合)
func isTechToken(s string) bool {
hasDigit := false
hasLetter := false
for _, r := range s {
if r >= '0' && r <= '9' {
hasDigit = true
}
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
hasLetter = true
}
}
return hasDigit && hasLetter
}
// isAllDigits 判断是否全为数字
func isAllDigits(s string) bool {
if len(s) == 0 {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}