memoryweave/go/internal/api/routes/graph.go

121 lines
3.5 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 — 知识图谱 API
package routes
import (
"encoding/json"
"net/http"
"strings"
"unicode"
"github.com/xiaoxue/memoryweave/internal/governance"
)
type GraphAPI struct {
Graph governance.GraphStore
}
func NewGraphAPI(g governance.GraphStore) *GraphAPI {
return &GraphAPI{Graph: g}
}
// GET /api/v1/graph/stats
func (ga *GraphAPI) Stats(w http.ResponseWriter, r *http.Request) {
nodeCount, edgeCount, density := ga.Graph.Stats()
respond(w, 200, map[string]interface{}{
"node_count": nodeCount,
"edge_count": edgeCount,
"density": density,
})
}
// POST /api/v1/graph/query
// 设计文档 §2.5.4: match 格式兼容; 同时保留 entity+relation 简洁格式
func (ga *GraphAPI) Query(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
Relation string `json:"relation"`
Namespace string `json:"namespace"`
// 设计文档兼容格式: {"match": {"type": "entity", "label": "..."}}
Match *struct {
Type string `json:"type"`
Label string `json:"label"`
} `json:"match"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
// 兼容设计文档 match 格式
if req.Match != nil && req.Entity == "" {
// match.type 过滤 node type, match.label 做 name 模糊搜索
var nodes []map[string]interface{}
if req.Match.Label != "" {
nodes = ga.Graph.SearchNodes(req.Match.Label, req.Namespace)
} else if req.Match.Type != "" {
nodes = ga.Graph.ListNodesByType(req.Match.Type, req.Namespace)
} else {
nodes = ga.Graph.ListNodes(req.Namespace)
}
respond(w, 200, map[string]interface{}{"nodes": nodes, "count": len(nodes)})
return
}
// 默认跨 namespace 搜索(空 = 匹配所有§2.5.6
results := ga.Graph.Query(normalizeEntity(req.Entity), req.Relation, req.Namespace)
respond(w, 200, map[string]interface{}{"results": results, "count": len(results)})
}
// POST /api/v1/graph/navigate
func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
MaxHops int `json:"max_hops"`
Namespace string `json:"namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Entity == "" {
respondError(w, 400, "entity required")
return
}
if req.MaxHops <= 0 {
req.MaxHops = 2
}
entity := normalizeEntity(req.Entity)
paths, err := ga.Graph.Navigate(entity, req.MaxHops, req.Namespace)
if err != nil {
respondError(w, 500, "navigate failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{"paths": paths, "entity": req.Entity, "count": len(paths)})
}
// normalizeEntity 规整实体名:去特殊字符 + n_前缀保留中文和 Unicode 字符
func normalizeEntity(entity string) string {
if strings.HasPrefix(entity, "n_") {
entity = entity[2:]
}
// 过滤特殊字符,保留 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(entity)))
// 合并连续下划线
for strings.Contains(clean, "__") {
clean = strings.ReplaceAll(clean, "__", "_")
}
clean = strings.Trim(clean, "_ ")
if clean == "" {
return "n_unknown"
}
return "n_" + clean
}