121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
// 织忆 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
|
||
}
|