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:
parent
525b5f2bb2
commit
df2b648973
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue