feat: M9 功能补全 — 30+ 新端点 + 自调参闭环
新增路由层 (10 files): - graph.go: GET /graph/stats, POST /graph/query, POST /graph/navigate - feedback.go: POST /feedback/useful|not-useful|deprecate|correct - gaps.go: GET /gaps, POST /gaps/detect, POST /gaps/close - conflicts.go: GET /conflicts, POST /conflicts/resolve - agent.go: POST /agents/register, GET /agents - admin.go: DELETE /distilled, GET /memory/versions, POST /admin/forget|backup, GET /admin/audit - l3.go: GET|POST /l3/worldmodel - triggers.go: GET /triggers, POST /triggers/fire, GET /skills, POST /skills/trial - eval.go: POST /eval/run, GET /eval/history, POST /eval/generate - tuning.go: GET /tuning/status, POST /tuning/run, POST /tuning/analytics - obsidian.go: POST /obsidian/push|pull, GET /obsidian/status 引擎增强: - governance: InMemoryGraph (零外部依赖) + ListActive/Resolve/DB - selfoptimize: RecordDeprecation/Correction/GapClosed/ConflictResolved - storage: IncrementUseful/NotUseful, UpdateMemoryContent, GetVersionHistory, GetCandidatesForForgetting, Backup, GetAuditLog 总计: 17 files, 1915+ lines, 35 endpoints, go build 0 errors
This commit is contained in:
parent
02eef1024d
commit
ef2ef5069c
|
|
@ -0,0 +1,142 @@
|
|||
// 织忆 MemoryWeave — 管理端点
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/governance"
|
||||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||||
)
|
||||
|
||||
type AdminAPI struct {
|
||||
LanceDB *storage.LanceDB
|
||||
Forgetter *governance.Forgetter
|
||||
}
|
||||
|
||||
func NewAdminAPI(ldb *storage.LanceDB, f *governance.Forgetter) *AdminAPI {
|
||||
return &AdminAPI{LanceDB: ldb, Forgetter: f}
|
||||
}
|
||||
|
||||
// DELETE /api/v1/distilled/{id}
|
||||
func (aa *AdminAPI) DeleteDistilled(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if id == "" {
|
||||
respondError(w, 400, "id required")
|
||||
return
|
||||
}
|
||||
if err := aa.LanceDB.SoftDelete(id, "manual_delete"); err != nil {
|
||||
respondError(w, 500, "delete failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]string{"status": "deleted", "id": id})
|
||||
}
|
||||
|
||||
// GET /api/v1/memory/{id}/versions
|
||||
func (aa *AdminAPI) Versions(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if id == "" {
|
||||
respondError(w, 400, "id required")
|
||||
return
|
||||
}
|
||||
versions, err := aa.LanceDB.GetVersionHistory(id)
|
||||
if err != nil {
|
||||
respondError(w, 500, "get versions failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"memory_id": id, "versions": versions, "count": len(versions),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/admin/forget
|
||||
func (aa *AdminAPI) Forget(w http.ResponseWriter, r *http.Request) {
|
||||
results, err := aa.LanceDB.GetCandidatesForForgetting()
|
||||
if err != nil {
|
||||
respondError(w, 500, "list failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
forgotten := 0
|
||||
for _, mem := range results {
|
||||
lastAccess := parseTimeStr(mem["last_recalled_at"])
|
||||
recallCnt := intVal(mem["recall_count"])
|
||||
tier := strVal(mem["tier"])
|
||||
|
||||
if aa.Forgetter.ShouldForget(lastAccess, recallCnt, tier) {
|
||||
aa.LanceDB.SoftDelete(strVal(mem["id"]), "auto_forget")
|
||||
forgotten++
|
||||
}
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"status": "ok", "scanned": len(results), "forgotten": forgotten,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/admin/backup
|
||||
func (aa *AdminAPI) Backup(w http.ResponseWriter, r *http.Request) {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
backupPath := fmt.Sprintf("/home/muc/backups/memoryweave/%s", timestamp)
|
||||
os.MkdirAll(backupPath, 0755)
|
||||
|
||||
if err := aa.LanceDB.Backup(backupPath); err != nil {
|
||||
respondError(w, 500, "backup failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]string{
|
||||
"status": "ok", "path": backupPath, "timestamp": timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/admin/audit
|
||||
func (aa *AdminAPI) Audit(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 100
|
||||
logs, err := aa.LanceDB.GetAuditLog(limit)
|
||||
if err != nil {
|
||||
respondError(w, 500, "audit failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{"audit_logs": logs, "count": len(logs)})
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func parseTimeStr(s interface{}) time.Time {
|
||||
if s == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
switch v := s.(type) {
|
||||
case string:
|
||||
t, _ := time.Parse(time.RFC3339, v)
|
||||
return t
|
||||
case time.Time:
|
||||
return v
|
||||
}
|
||||
return 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 json.Number:
|
||||
i, _ := n.Int64()
|
||||
return int(i)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func strVal(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch s := v.(type) {
|
||||
case string: return s
|
||||
case json.Number: return s.String()
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// 织忆 MemoryWeave — Agent 注册 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/distributed"
|
||||
)
|
||||
|
||||
type AgentRegistry struct {
|
||||
agents map[string]AgentInfo
|
||||
limiter *distributed.RateLimiter
|
||||
}
|
||||
|
||||
type AgentInfo struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
Quota Quota `json:"quota"`
|
||||
Registered string `json:"registered"`
|
||||
}
|
||||
|
||||
type Quota struct {
|
||||
RecallQPS int `json:"recall_qps"`
|
||||
CommitQPS int `json:"commit_qps"`
|
||||
Burst int `json:"burst"`
|
||||
}
|
||||
|
||||
func NewAgentRegistry(limiter *distributed.RateLimiter) *AgentRegistry {
|
||||
return &AgentRegistry{
|
||||
agents: make(map[string]AgentInfo),
|
||||
limiter: limiter,
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/v1/agents/register
|
||||
func (ar *AgentRegistry) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.AgentID == "" {
|
||||
respondError(w, 400, "agent_id required")
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := ar.agents[req.AgentID]; exists {
|
||||
respondError(w, 409, "agent already registered")
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := generateKey(32)
|
||||
info := AgentInfo{
|
||||
AgentID: req.AgentID,
|
||||
APIKey: apiKey,
|
||||
Quota: Quota{RecallQPS: 10, CommitQPS: 2, Burst: 20},
|
||||
Registered: "now",
|
||||
}
|
||||
ar.agents[req.AgentID] = info
|
||||
respond(w, 201, info)
|
||||
}
|
||||
|
||||
// GET /api/v1/agents — 已注册 Agent 列表
|
||||
func (ar *AgentRegistry) List(w http.ResponseWriter, r *http.Request) {
|
||||
var list []AgentInfo
|
||||
for _, info := range ar.agents {
|
||||
info.APIKey = "" // 不泄漏 key
|
||||
list = append(list, info)
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{"agents": list, "count": len(list)})
|
||||
}
|
||||
|
||||
// Auth 验证 API Key
|
||||
func (ar *AgentRegistry) Auth(apiKey string) (*AgentInfo, bool) {
|
||||
for _, info := range ar.agents {
|
||||
if info.APIKey == apiKey {
|
||||
return &info, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func generateKey(length int) string {
|
||||
b := make([]byte, length)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("mw-%x", b)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// 织忆 MemoryWeave — 冲突管理 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/governance"
|
||||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||||
)
|
||||
|
||||
type ConflictAPI struct {
|
||||
Detector *governance.ConflictDetector
|
||||
}
|
||||
|
||||
func NewConflictAPI(d *governance.ConflictDetector) *ConflictAPI {
|
||||
return &ConflictAPI{Detector: d}
|
||||
}
|
||||
|
||||
// GET /api/v1/conflicts
|
||||
func (ca *ConflictAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||
list := ca.Detector.ListActive()
|
||||
pending := 0
|
||||
for _, c := range list {
|
||||
if c.Status == "pending" {
|
||||
pending++
|
||||
}
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"conflicts": list, "count": len(list), "pending": pending,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/conflicts/resolve
|
||||
func (ca *ConflictAPI) Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ConflictID string `json:"conflict_id"`
|
||||
Resolution string `json:"resolution"` // keep_left / keep_right / merge / dismiss
|
||||
Winner string `json:"winner"` // id of the winning memory
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.ConflictID == "" || req.Resolution == "" {
|
||||
respondError(w, 400, "conflict_id and resolution required")
|
||||
return
|
||||
}
|
||||
|
||||
err := ca.Detector.Resolve(req.ConflictID, req.Resolution, req.Winner)
|
||||
if err != nil {
|
||||
respondError(w, 500, "resolve failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
selfoptimize.Dash.RecordConflictResolved(true)
|
||||
PushConflictResolved(req.ConflictID, req.Resolution)
|
||||
respond(w, 200, map[string]string{"status": "resolved", "conflict_id": req.ConflictID})
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
// 织忆 MemoryWeave — 评估系统 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||||
)
|
||||
|
||||
type EvalAPI struct {
|
||||
Pipeline *storage.RecallPipeline
|
||||
LanceDB *storage.LanceDB
|
||||
}
|
||||
|
||||
type EvalRun struct {
|
||||
ID string `json:"id"`
|
||||
Model string `json:"model"`
|
||||
Precision float64 `json:"precision"`
|
||||
Recall float64 `json:"recall_k"`
|
||||
MRR float64 `json:"mrr"`
|
||||
NDCG float64 `json:"ndcg"`
|
||||
Queries int `json:"queries"`
|
||||
RanAt time.Time `json:"ran_at"`
|
||||
}
|
||||
|
||||
type evalStore struct {
|
||||
mu sync.RWMutex
|
||||
runs []*EvalRun
|
||||
}
|
||||
|
||||
var evals = &evalStore{}
|
||||
|
||||
func NewEvalAPI(p *storage.RecallPipeline, ldb *storage.LanceDB) *EvalAPI {
|
||||
return &EvalAPI{Pipeline: p, LanceDB: ldb}
|
||||
}
|
||||
|
||||
// POST /api/v1/eval/run
|
||||
func (ea *EvalAPI) Run(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Queries []struct {
|
||||
Query string `json:"query"`
|
||||
ExpectedIDs []string `json:"expected_ids"`
|
||||
} `json:"queries"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if len(req.Queries) == 0 {
|
||||
respondError(w, 400, "at least one query required")
|
||||
return
|
||||
}
|
||||
|
||||
var totalPrecision, totalRecall, totalMRR, totalNDCG float64
|
||||
totalQueries := 0
|
||||
|
||||
for _, q := range req.Queries {
|
||||
results, err := ea.Pipeline.Recall(q.Query, "shared", 10, 0.5)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Precision@k
|
||||
hits := 0
|
||||
expectedSet := makeSet(q.ExpectedIDs)
|
||||
for i, res := range results {
|
||||
if expectedSet[res.ID] && i < len(q.ExpectedIDs) {
|
||||
hits++
|
||||
}
|
||||
}
|
||||
if len(results) > 0 {
|
||||
totalPrecision += float64(hits) / float64(len(results))
|
||||
}
|
||||
|
||||
// Recall@k
|
||||
if len(q.ExpectedIDs) > 0 {
|
||||
totalRecall += float64(hits) / float64(len(q.ExpectedIDs))
|
||||
}
|
||||
|
||||
// MRR (First correct position)
|
||||
for i, res := range results {
|
||||
if expectedSet[res.ID] {
|
||||
totalMRR += 1.0 / float64(i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// NDCG (binary relevance)
|
||||
dcg := 0.0
|
||||
idcg := 0.0
|
||||
for i, res := range results {
|
||||
rel := 0.0
|
||||
if expectedSet[res.ID] {
|
||||
rel = 1.0
|
||||
}
|
||||
dcg += rel / log2(float64(i+2))
|
||||
if i < len(q.ExpectedIDs) {
|
||||
idcg += 1.0 / log2(float64(i+2))
|
||||
}
|
||||
}
|
||||
if idcg > 0 {
|
||||
totalNDCG += dcg / idcg
|
||||
}
|
||||
totalQueries++
|
||||
}
|
||||
|
||||
if totalQueries == 0 {
|
||||
respondError(w, 500, "all queries failed")
|
||||
return
|
||||
}
|
||||
|
||||
run := &EvalRun{
|
||||
ID: time.Now().Format("20060102-150405"),
|
||||
Model: req.Model,
|
||||
Precision: totalPrecision / float64(totalQueries),
|
||||
Recall: totalRecall / float64(totalQueries),
|
||||
MRR: totalMRR / float64(totalQueries),
|
||||
NDCG: totalNDCG / float64(totalQueries),
|
||||
Queries: totalQueries,
|
||||
RanAt: time.Now(),
|
||||
}
|
||||
|
||||
evals.mu.Lock()
|
||||
evals.runs = append(evals.runs, run)
|
||||
evals.mu.Unlock()
|
||||
|
||||
respond(w, 200, run)
|
||||
}
|
||||
|
||||
// GET /api/v1/eval/history
|
||||
func (ea *EvalAPI) History(w http.ResponseWriter, r *http.Request) {
|
||||
evals.mu.RLock()
|
||||
defer evals.mu.RUnlock()
|
||||
|
||||
if len(evals.runs) == 0 {
|
||||
respond(w, 200, map[string]interface{}{"runs": []EvalRun{}, "count": 0})
|
||||
return
|
||||
}
|
||||
|
||||
// 计算趋势(最近 2 次对比)
|
||||
trend := "stable"
|
||||
if len(evals.runs) >= 2 {
|
||||
curr := evals.runs[len(evals.runs)-1].MRR
|
||||
prev := evals.runs[len(evals.runs)-2].MRR
|
||||
if curr > prev*1.05 {
|
||||
trend = "improving"
|
||||
} else if curr < prev*0.95 {
|
||||
trend = "degrading"
|
||||
}
|
||||
}
|
||||
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"runs": evals.runs, "count": len(evals.runs), "trend": trend,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/eval/generate — 自动生成金标查询集
|
||||
func (ea *EvalAPI) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.Namespace == "" {
|
||||
req.Namespace = "shared"
|
||||
}
|
||||
if req.Count <= 0 {
|
||||
req.Count = 10
|
||||
}
|
||||
|
||||
// 获取高质量记忆作为金标基础
|
||||
memories, err := ea.LanceDB.GetTopByQuality("", req.Count)
|
||||
if err != nil {
|
||||
respondError(w, 500, "generate failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var queries []map[string]interface{}
|
||||
for _, mem := range memories {
|
||||
query := mem.Content
|
||||
if len(query) > 50 {
|
||||
query = query[:50]
|
||||
}
|
||||
queries = append(queries, map[string]interface{}{
|
||||
"query": query,
|
||||
"expected_ids": []string{mem.ID},
|
||||
})
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"queries": queries, "count": len(queries),
|
||||
})
|
||||
}
|
||||
|
||||
// 辅助
|
||||
func makeSet(ids []string) map[string]bool {
|
||||
s := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
s[id] = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func log2(x float64) float64 {
|
||||
// log2(x) ≈ ln(x)/ln(2) 但不用 math 包避免类型问题
|
||||
result := 0.0
|
||||
for x > 2 {
|
||||
x /= 2
|
||||
result += 1
|
||||
}
|
||||
if x > 1 {
|
||||
result += (x - 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// 织忆 MemoryWeave — 反馈 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||||
)
|
||||
|
||||
type FeedbackAPI struct {
|
||||
LanceDB *storage.LanceDB
|
||||
}
|
||||
|
||||
func NewFeedbackAPI(ldb *storage.LanceDB) *FeedbackAPI {
|
||||
return &FeedbackAPI{LanceDB: ldb}
|
||||
}
|
||||
|
||||
// POST /api/v1/feedback/useful
|
||||
func (fa *FeedbackAPI) MarkUseful(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
MemoryID string `json:"memory_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.MemoryID == "" {
|
||||
respondError(w, 400, "memory_id required")
|
||||
return
|
||||
}
|
||||
|
||||
selfoptimize.Dash.RecordFeedback(true)
|
||||
fa.LanceDB.IncrementUseful(req.MemoryID)
|
||||
respond(w, 200, map[string]string{"status": "ok", "memory_id": req.MemoryID})
|
||||
}
|
||||
|
||||
// POST /api/v1/feedback/not-useful
|
||||
func (fa *FeedbackAPI) MarkNotUseful(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
MemoryID string `json:"memory_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.MemoryID == "" {
|
||||
respondError(w, 400, "memory_id required")
|
||||
return
|
||||
}
|
||||
|
||||
selfoptimize.Dash.RecordFeedback(false)
|
||||
fa.LanceDB.IncrementNotUseful(req.MemoryID)
|
||||
respond(w, 200, map[string]string{"status": "ok", "memory_id": req.MemoryID})
|
||||
}
|
||||
|
||||
// POST /api/v1/feedback/deprecate
|
||||
func (fa *FeedbackAPI) Deprecate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
MemoryID string `json:"memory_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.MemoryID == "" {
|
||||
respondError(w, 400, "memory_id required")
|
||||
return
|
||||
}
|
||||
|
||||
selfoptimize.Dash.RecordDeprecation()
|
||||
fa.LanceDB.SoftDelete(req.MemoryID, req.Reason)
|
||||
respond(w, 200, map[string]string{"status": "deprecated", "memory_id": req.MemoryID})
|
||||
}
|
||||
|
||||
// POST /api/v1/feedback/correct
|
||||
func (fa *FeedbackAPI) Correct(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
MemoryID string `json:"memory_id"`
|
||||
NewContent string `json:"new_content"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.MemoryID == "" || req.NewContent == "" {
|
||||
respondError(w, 400, "memory_id and new_content required")
|
||||
return
|
||||
}
|
||||
if req.Source == "" {
|
||||
req.Source = "muchen_correction"
|
||||
}
|
||||
|
||||
selfoptimize.Dash.RecordCorrection(req.Source)
|
||||
err := fa.LanceDB.UpdateMemoryContent(req.MemoryID, req.NewContent, req.Source)
|
||||
if err != nil {
|
||||
respondError(w, 500, "correct failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]string{"status": "corrected", "memory_id": req.MemoryID})
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// 织忆 MemoryWeave — 知识缺口 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||||
)
|
||||
|
||||
type GapAPI struct {
|
||||
Detector *selfoptimize.GapDetector
|
||||
}
|
||||
|
||||
func NewGapAPI(d *selfoptimize.GapDetector) *GapAPI {
|
||||
return &GapAPI{Detector: d}
|
||||
}
|
||||
|
||||
// GET /api/v1/gaps
|
||||
func (ga *GapAPI) List(w http.ResponseWriter, r *http.Request) {
|
||||
gaps := ga.Detector.List()
|
||||
openCount := 0
|
||||
for _, g := range gaps {
|
||||
if !g.Closed {
|
||||
openCount++
|
||||
}
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"gaps": gaps,
|
||||
"count": len(gaps),
|
||||
"open": openCount,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/gaps/{topic}/close
|
||||
func (ga *GapAPI) Close(w http.ResponseWriter, r *http.Request) {
|
||||
topic := r.PathValue("topic")
|
||||
if topic == "" {
|
||||
respondError(w, 400, "topic required")
|
||||
return
|
||||
}
|
||||
|
||||
ga.Detector.Close(topic)
|
||||
selfoptimize.Dash.RecordGapClosed()
|
||||
respond(w, 200, map[string]string{"status": "closed", "topic": topic})
|
||||
}
|
||||
|
||||
// POST /api/v1/gaps/detect — 手动触发缺口检测(miss 计数)
|
||||
func (ga *GapAPI) Detect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.Topic == "" {
|
||||
respondError(w, 400, "topic required")
|
||||
return
|
||||
}
|
||||
|
||||
gap := ga.Detector.RecordMiss(req.Topic)
|
||||
if gap != nil {
|
||||
PushGapFound(req.Topic, string(gap.Type))
|
||||
respond(w, 201, map[string]interface{}{
|
||||
"status": "gap_detected", "gap": gap,
|
||||
})
|
||||
} else {
|
||||
respond(w, 200, map[string]string{"status": "tracking", "topic": req.Topic})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// 织忆 MemoryWeave — 知识图谱 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/governance"
|
||||
)
|
||||
|
||||
type GraphAPI struct {
|
||||
Graph *governance.InMemoryGraph
|
||||
}
|
||||
|
||||
func NewGraphAPI(g *governance.InMemoryGraph) *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
|
||||
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"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.Namespace == "" {
|
||||
req.Namespace = "shared"
|
||||
}
|
||||
|
||||
results := ga.Graph.Query(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
|
||||
}
|
||||
if req.Namespace == "" {
|
||||
req.Namespace = "shared"
|
||||
}
|
||||
|
||||
paths, err := ga.Graph.Navigate(req.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)})
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// 织忆 MemoryWeave — L3 世界模型 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// L3 世界模型:ℰ (Environment) + ℐ (Implicit Rules) + C (Capabilities)
|
||||
type WorldModel struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
Environment struct {
|
||||
OS string `json:"os"`
|
||||
GPU string `json:"gpu"`
|
||||
VRAM string `json:"vram"`
|
||||
RAM string `json:"ram"`
|
||||
CPU string `json:"cpu"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
HermesVer string `json:"hermes_ver"`
|
||||
} `json:"environment"`
|
||||
|
||||
ImplicitRules []string `json:"implicit_rules"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
var WM = &WorldModel{}
|
||||
|
||||
func init() {
|
||||
WM.Environment.OS = "Linux"
|
||||
WM.Environment.GPU = "RTX 3050 Laptop"
|
||||
WM.Environment.VRAM = "4GB"
|
||||
WM.Environment.RAM = "16GB"
|
||||
WM.Environment.CPU = "Intel"
|
||||
WM.Environment.HermesVer = "0.14.0"
|
||||
WM.ImplicitRules = []string{
|
||||
"牧尘话少直接,结论先行",
|
||||
"性能可以多余,不能短缺",
|
||||
"方案最大化完善,不精简",
|
||||
"先修性能再推进分布式",
|
||||
}
|
||||
WM.Capabilities = []string{
|
||||
"飞书消息收发",
|
||||
"ComfyUI 图像生成 (SD 1.5)",
|
||||
"语音合成 TTS",
|
||||
"语音识别 STT",
|
||||
}
|
||||
WM.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// GET /api/v1/l3/worldmodel
|
||||
func (wm *WorldModel) GetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
wm.mu.RLock()
|
||||
defer wm.mu.RUnlock()
|
||||
respond(w, 200, wm)
|
||||
}
|
||||
|
||||
// POST /api/v1/l3/worldmodel — 更新
|
||||
func (wm *WorldModel) UpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Environment *struct {
|
||||
OS string `json:"os"`
|
||||
GPU string `json:"gpu"`
|
||||
VRAM string `json:"vram"`
|
||||
RAM string `json:"ram"`
|
||||
} `json:"environment"`
|
||||
AddRules []string `json:"add_rules"`
|
||||
AddCapabilities []string `json:"add_capabilities"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
if req.Environment != nil {
|
||||
if req.Environment.OS != "" {
|
||||
wm.Environment.OS = req.Environment.OS
|
||||
}
|
||||
if req.Environment.GPU != "" {
|
||||
wm.Environment.GPU = req.Environment.GPU
|
||||
}
|
||||
if req.Environment.VRAM != "" {
|
||||
wm.Environment.VRAM = req.Environment.VRAM
|
||||
}
|
||||
if req.Environment.RAM != "" {
|
||||
wm.Environment.RAM = req.Environment.RAM
|
||||
}
|
||||
}
|
||||
for _, rule := range req.AddRules {
|
||||
wm.ImplicitRules = append(wm.ImplicitRules, rule)
|
||||
}
|
||||
for _, cap := range req.AddCapabilities {
|
||||
wm.Capabilities = append(wm.Capabilities, cap)
|
||||
}
|
||||
wm.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
respond(w, 200, wm)
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
// 织忆 MemoryWeave — Obsidian 双向同步
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||||
)
|
||||
|
||||
// ObsidianSyncer 双向同步织忆与 Obsidian 知识库
|
||||
type ObsidianSyncer struct {
|
||||
vaultPath string
|
||||
ldb *storage.LanceDB
|
||||
mu sync.Mutex
|
||||
lastSync time.Time
|
||||
}
|
||||
|
||||
var Obsidian *ObsidianSyncer
|
||||
|
||||
func NewObsidianSyncer(vaultPath string, ldb *storage.LanceDB) *ObsidianSyncer {
|
||||
s := &ObsidianSyncer{
|
||||
vaultPath: vaultPath,
|
||||
ldb: ldb,
|
||||
}
|
||||
Obsidian = s
|
||||
return s
|
||||
}
|
||||
|
||||
// PushToObsidian 推送织忆记忆到 Obsidian(默认 vault)
|
||||
func (s *ObsidianSyncer) PushToObsidian(memories []map[string]interface{}, folder string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.vaultPath == "" {
|
||||
s.vaultPath = os.Getenv("OBSIDIAN_VAULT")
|
||||
if s.vaultPath == "" {
|
||||
s.vaultPath = fmt.Sprintf("/home/%s/mc/小唯", os.Getenv("USER"))
|
||||
}
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(s.vaultPath, folder)
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", targetDir, err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, mem := range memories {
|
||||
content := strVal(mem["content"])
|
||||
if len(content) < 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := sanitizeFilename(content[:minz(len(content), 30)]) + ".md"
|
||||
filepath := filepath.Join(targetDir, filename)
|
||||
|
||||
mdContent := fmt.Sprintf(`---
|
||||
id: %s
|
||||
category: %s
|
||||
quality_score: %.2f
|
||||
sync_time: %s
|
||||
---
|
||||
|
||||
# %s
|
||||
|
||||
%s
|
||||
|
||||
---
|
||||
*由织忆 MemoryWeave 同步*
|
||||
`, strVal(mem["id"]), strVal(mem["category"]),
|
||||
floatVal(mem["quality_score"]),
|
||||
time.Now().Format(time.RFC3339),
|
||||
truncate(content, 60),
|
||||
content)
|
||||
|
||||
if err := os.WriteFile(filepath, []byte(mdContent), 0644); err != nil {
|
||||
log.Printf("[obsidian] 写入失败 %s: %v", filename, err)
|
||||
continue
|
||||
}
|
||||
count++
|
||||
}
|
||||
s.lastSync = time.Now()
|
||||
log.Printf("[obsidian] 推送 %d 条记忆到 %s", count, targetDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PullFromObsidian 从 Obsidian 拉取笔记并 commit 到织忆
|
||||
func (s *ObsidianSyncer) PullFromObsidian(folder string) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.vaultPath == "" {
|
||||
s.vaultPath = os.Getenv("OBSIDIAN_VAULT")
|
||||
if s.vaultPath == "" {
|
||||
s.vaultPath = fmt.Sprintf("/home/%s/mc/小唯", os.Getenv("USER"))
|
||||
}
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(s.vaultPath, folder)
|
||||
entries, err := os.ReadDir(targetDir)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("readdir %s: %w", targetDir, err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(targetDir, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
// 跳过已经同步过的(检查 FRONT MATTER)
|
||||
if strings.Contains(content, "*由织忆 MemoryWeave 同步*") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 简单提取:去掉 YAML 头,取正文
|
||||
body := content
|
||||
if idx := strings.Index(content, "---\n"); idx >= 0 {
|
||||
if endIdx := strings.Index(content[4:], "---\n"); endIdx >= 0 {
|
||||
body = content[endIdx+8:]
|
||||
}
|
||||
}
|
||||
|
||||
s.ldb.InsertEpisode(
|
||||
"obsidian-sync",
|
||||
"shared",
|
||||
strings.TrimSpace(body),
|
||||
"obsidian_import",
|
||||
)
|
||||
count++
|
||||
}
|
||||
s.lastSync = time.Now()
|
||||
log.Printf("[obsidian] 拉取 %d 条笔记", count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ─── API ──────────────────────────────────────────────────
|
||||
|
||||
// POST /api/v1/obsidian/push
|
||||
func (s *ObsidianSyncer) PushHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Memories []map[string]interface{} `json:"memories"`
|
||||
Folder string `json:"folder"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.Folder == "" {
|
||||
req.Folder = "02-Memory"
|
||||
}
|
||||
|
||||
if err := s.PushToObsidian(req.Memories, req.Folder); err != nil {
|
||||
respondError(w, 500, "push failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"status": "pushed", "count": len(req.Memories),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/obsidian/pull
|
||||
func (s *ObsidianSyncer) PullHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Folder string `json:"folder"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
if req.Folder == "" {
|
||||
req.Folder = "02-Memory"
|
||||
}
|
||||
|
||||
count, err := s.PullFromObsidian(req.Folder)
|
||||
if err != nil {
|
||||
respondError(w, 500, "pull failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"status": "pulled", "count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/obsidian/status
|
||||
func (s *ObsidianSyncer) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"vault_path": s.vaultPath,
|
||||
"last_sync": s.lastSync.Format(time.RFC3339),
|
||||
"vault_exists": dirExists(s.vaultPath),
|
||||
})
|
||||
}
|
||||
|
||||
// ─── 辅助 ─────────────────────────────────────────────────
|
||||
|
||||
func sanitizeFilename(s string) string {
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') || r == '_' || r == '-' || r == ' ' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, s)
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func minz(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func floatVal(v interface{}) float64 {
|
||||
switch f := v.(type) {
|
||||
case float64:
|
||||
return f
|
||||
case int:
|
||||
return float64(f)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func dirExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
// 织忆 MemoryWeave — 触发器 & Skill 管理 API
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ─── 触发器 ──────────────────────────────────────────────
|
||||
|
||||
type TriggerType string
|
||||
|
||||
const (
|
||||
TriggerCommitCount TriggerType = "commit_count" // 新增 N 条后触发
|
||||
TriggerTimeSince TriggerType = "time_since" // 距上次操作 N 小时后触发
|
||||
TriggerRecallMiss TriggerType = "recall_miss" // 连续 N 次 miss
|
||||
TriggerQualityDrop TriggerType = "quality_drop" // quality < threshold
|
||||
)
|
||||
|
||||
type Trigger struct {
|
||||
ID string `json:"id"`
|
||||
Type TriggerType `json:"type"`
|
||||
Condition string `json:"condition"`
|
||||
Urgency float64 `json:"urgency"` // 0-1,越大越紧急
|
||||
Active bool `json:"active"`
|
||||
FiredAt time.Time `json:"fired_at,omitempty"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type TriggerManager struct {
|
||||
mu sync.RWMutex
|
||||
triggers []*Trigger
|
||||
}
|
||||
|
||||
var Triggers = &TriggerManager{
|
||||
triggers: []*Trigger{
|
||||
{ID: "t1", Type: TriggerCommitCount, Condition: "50 new commits", Urgency: 0.3, Active: true, Description: "蒸馏量达到 50 条触发整合"},
|
||||
{ID: "t2", Type: TriggerTimeSince, Condition: "24h since last deep consolidate", Urgency: 0.5, Active: true, Description: "距上次深度整合超 24h"},
|
||||
{ID: "t3", Type: TriggerRecallMiss, Condition: "3 consecutive misses", Urgency: 0.7, Active: true, Description: "连续 3 次召回失败 → 缺口分类"},
|
||||
{ID: "t4", Type: TriggerQualityDrop, Condition: "quality < 0.3", Urgency: 0.6, Active: true, Description: "某条记忆质量过低 → 审查"},
|
||||
},
|
||||
}
|
||||
|
||||
// GET /api/v1/triggers
|
||||
func (tm *TriggerManager) List(w http.ResponseWriter, r *http.Request) {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
// 按 urgency 降序
|
||||
sorted := make([]*Trigger, len(tm.triggers))
|
||||
copy(sorted, tm.triggers)
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"triggers": sorted, "count": len(sorted),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/triggers/fire — 手动触发
|
||||
func (tm *TriggerManager) Fire(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
TriggerID string `json:"trigger_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
for _, t := range tm.triggers {
|
||||
if t.ID == req.TriggerID {
|
||||
t.FiredAt = time.Now()
|
||||
respond(w, 200, map[string]string{
|
||||
"status": "fired", "trigger_id": req.TriggerID,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
respondError(w, 404, "trigger not found")
|
||||
}
|
||||
|
||||
// ─── Skill 结晶 ──────────────────────────────────────────
|
||||
|
||||
type Skill struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Trials int `json:"trials"`
|
||||
ETA float64 `json:"eta"` // 有效性 η = successes / trials
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type SkillManager struct {
|
||||
mu sync.RWMutex
|
||||
skills map[string]*Skill
|
||||
}
|
||||
|
||||
var Skills = &SkillManager{
|
||||
skills: make(map[string]*Skill),
|
||||
}
|
||||
|
||||
// GET /api/v1/skills
|
||||
func (sm *SkillManager) List(w http.ResponseWriter, r *http.Request) {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
|
||||
var list []*Skill
|
||||
for _, s := range sm.skills {
|
||||
list = append(list, s)
|
||||
}
|
||||
respond(w, 200, map[string]interface{}{"skills": list, "count": len(list)})
|
||||
}
|
||||
|
||||
// POST /api/v1/skills/{name}/trial
|
||||
func (sm *SkillManager) Trial(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if name == "" {
|
||||
respondError(w, 400, "name required")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, 400, "invalid body")
|
||||
return
|
||||
}
|
||||
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
skill, exists := sm.skills[name]
|
||||
if !exists {
|
||||
skill = &Skill{
|
||||
Name: name,
|
||||
Description: "自动发现的工作模式",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
sm.skills[name] = skill
|
||||
}
|
||||
skill.Trials++
|
||||
if req.Success {
|
||||
skill.ETA = float64(skill.Trials-1) / float64(skill.Trials)
|
||||
} else {
|
||||
skill.ETA = float64(skill.Trials-1) / float64(skill.Trials)
|
||||
}
|
||||
respond(w, 200, skill)
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
// 织忆 MemoryWeave — 自调参闭环(Auto-tuning)
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||||
)
|
||||
|
||||
// TuningConfig 可自动调优的参数
|
||||
type TuningConfig struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
Diversity float64 `json:"diversity"` // MMR diversity 参数 (0-1)
|
||||
DecayRate float64 `json:"decay_rate"` // 遗忘衰减率
|
||||
GapThreshold int `json:"gap_threshold"` // 缺口感测阈值 (miss 次数)
|
||||
DistillInterval int `json:"distill_interval"` // 蒸馏间隔 (分钟)
|
||||
ConsolidateAfter int `json:"consolidate_after"` // N 条蒸馏后触发整合
|
||||
|
||||
// 历史调参记录
|
||||
history []TuningEvent
|
||||
}
|
||||
|
||||
type TuningEvent struct {
|
||||
Parameter string `json:"parameter"`
|
||||
OldValue float64 `json:"old_value"`
|
||||
NewValue float64 `json:"new_value"`
|
||||
Reason string `json:"reason"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
var Tuner = &TuningConfig{
|
||||
Diversity: 0.5,
|
||||
DecayRate: 0.015,
|
||||
GapThreshold: 3,
|
||||
DistillInterval: 5,
|
||||
ConsolidateAfter: 50,
|
||||
}
|
||||
|
||||
// AutoTune 基于仪表盘指标自动调参
|
||||
func (tc *TuningConfig) AutoTune(metrics map[string]float64) []TuningEvent {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
var events []TuningEvent
|
||||
|
||||
// 1. 召回命中率 < 0.5 → 提高 diversity(扩大搜索范围)
|
||||
if hitRate, ok := metrics["recall_hit_rate"]; ok && hitRate < 0.5 {
|
||||
newDiversity := tc.Diversity + 0.1
|
||||
if newDiversity > 1.0 {
|
||||
newDiversity = 1.0
|
||||
}
|
||||
events = append(events, recordTuning(tc, "diversity", tc.Diversity, newDiversity, "recall_hit_rate < 0.5"))
|
||||
tc.Diversity = newDiversity
|
||||
}
|
||||
|
||||
// 2. 召回有用率 > 0.9 → 降低 diversity(更精准)
|
||||
if usefulRate, ok := metrics["recall_usefulness_rate"]; ok && usefulRate > 0.9 {
|
||||
newDiversity := tc.Diversity - 0.05
|
||||
if newDiversity < 0.1 {
|
||||
newDiversity = 0.1
|
||||
}
|
||||
events = append(events, recordTuning(tc, "diversity", tc.Diversity, newDiversity, "recall_usefulness_rate > 0.9"))
|
||||
tc.Diversity = newDiversity
|
||||
}
|
||||
|
||||
// 3. 废弃率 > 5/day → 提高衰减率(加快清理)
|
||||
if deprecated, ok := metrics["deprecated_per_day"]; ok && deprecated > 5 {
|
||||
newDecay := tc.DecayRate * 1.2
|
||||
if newDecay > 0.05 {
|
||||
newDecay = 0.05
|
||||
}
|
||||
events = append(events, recordTuning(tc, "decay_rate", tc.DecayRate, newDecay, "deprecated_per_day > 5"))
|
||||
tc.DecayRate = newDecay
|
||||
}
|
||||
|
||||
// 4. 缺口闭环率 < 0.5 → 降低 gap 阈值
|
||||
if gapRate, ok := metrics["gap_closure_rate"]; ok && gapRate < 0.5 {
|
||||
newThreshold := tc.GapThreshold - 1
|
||||
if newThreshold < 2 {
|
||||
newThreshold = 2
|
||||
}
|
||||
events = append(events, recordTuningInt(tc, "gap_threshold", tc.GapThreshold, newThreshold, "gap_closure_rate < 0.5"))
|
||||
tc.GapThreshold = newThreshold
|
||||
}
|
||||
|
||||
// 5. 蒸馏损失 > 0.3 → 更频繁蒸馏
|
||||
if loss, ok := metrics["avg_distill_loss"]; ok && loss > 0.3 {
|
||||
newInterval := tc.DistillInterval - 1
|
||||
if newInterval < 1 {
|
||||
newInterval = 1
|
||||
}
|
||||
events = append(events, recordTuningInt(tc, "distill_interval", tc.DistillInterval, newInterval, "avg_distill_loss > 0.3"))
|
||||
tc.DistillInterval = newInterval
|
||||
}
|
||||
|
||||
tc.history = append(tc.history, events...)
|
||||
return events
|
||||
}
|
||||
|
||||
func recordTuning(tc *TuningConfig, param string, oldVal, newVal float64, reason string) TuningEvent {
|
||||
return TuningEvent{
|
||||
Parameter: param,
|
||||
OldValue: oldVal,
|
||||
NewValue: newVal,
|
||||
Reason: reason,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func recordTuningInt(tc *TuningConfig, param string, oldVal, newVal int, reason string) TuningEvent {
|
||||
return TuningEvent{
|
||||
Parameter: param,
|
||||
OldValue: float64(oldVal),
|
||||
NewValue: float64(newVal),
|
||||
Reason: reason,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── API ──────────────────────────────────────────────────
|
||||
|
||||
// GET /api/v1/tuning/status
|
||||
func (tc *TuningConfig) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tc.mu.RLock()
|
||||
defer tc.mu.RUnlock()
|
||||
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"diversity": tc.Diversity,
|
||||
"decay_rate": tc.DecayRate,
|
||||
"gap_threshold": tc.GapThreshold,
|
||||
"distill_interval": tc.DistillInterval,
|
||||
"consolidate_after": tc.ConsolidateAfter,
|
||||
},
|
||||
"history": tc.history,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/tuning/run — 手动触发一次自动调参
|
||||
func (tc *TuningConfig) RunHandler(w http.ResponseWriter, r *http.Request) {
|
||||
metrics := selfoptimize.Dash.Metrics()
|
||||
events := tc.AutoTune(metrics)
|
||||
|
||||
if len(events) == 0 {
|
||||
respond(w, 200, map[string]string{"status": "no_changes_needed"})
|
||||
} else {
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"status": "tuned",
|
||||
"events": events,
|
||||
"count": len(events),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/v1/tuning/analytics — 运行因果分析
|
||||
func (tc *TuningConfig) AnalyticsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 基于指标间的关系给出建议
|
||||
m := selfoptimize.Dash.Metrics()
|
||||
|
||||
suggestions := []string{}
|
||||
if m["recall_hit_rate"] < 0.5 && m["avg_distill_loss"] > 0.3 {
|
||||
suggestions = append(suggestions, "蒸馏质量低导致召回率低 — 建议检查 Embedding 模型")
|
||||
}
|
||||
if m["deprecated_per_day"] > 10 {
|
||||
suggestions = append(suggestions, "废弃率过高 — 正在流失大量记忆,检查冲突检测阈值")
|
||||
}
|
||||
if m["gap_closure_rate"] < 0.3 {
|
||||
suggestions = append(suggestions, "缺口闭环率过低 — Agent 未回应缺口学习请求")
|
||||
}
|
||||
if m["auto_resolve_rate"] < 0.5 {
|
||||
suggestions = append(suggestions, "自动裁决率低 — 规则覆盖不全,考虑扩展策略")
|
||||
}
|
||||
|
||||
respond(w, 200, map[string]interface{}{
|
||||
"metrics": m,
|
||||
"suggestions": suggestions,
|
||||
})
|
||||
}
|
||||
|
|
@ -62,11 +62,9 @@ func (s *SSEManager) SSEHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
log.Printf("[sse] agent %s connected", agentID)
|
||||
|
||||
// 欢迎消息
|
||||
fmt.Fprintf(w, "data: {\"type\":\"connected\",\"agent_id\":\"%s\"}\n\n", agentID)
|
||||
flusher.Flush()
|
||||
|
||||
// 心跳
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
|
@ -89,7 +87,8 @@ func (s *SSEManager) SSEHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// 便捷推送方法
|
||||
// ─── 便捷推送方法 ──────────────────────────────────────────
|
||||
|
||||
func PushMemoryCommitted(agentID, namespace, memoryID string) {
|
||||
SSEBus.Push(SSEMessage{
|
||||
Type: "memory_committed",
|
||||
|
|
@ -117,3 +116,10 @@ func PushConsolidationDone(summary string) {
|
|||
Payload: summary,
|
||||
})
|
||||
}
|
||||
|
||||
func PushConflictResolved(conflictID, resolution string) {
|
||||
SSEBus.Push(SSEMessage{
|
||||
Type: "conflict_resolved",
|
||||
Payload: map[string]string{"conflict_id": conflictID, "resolution": resolution},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/xiaoxue/memoryweave/internal/api/middleware"
|
||||
"github.com/xiaoxue/memoryweave/internal/api/routes"
|
||||
"github.com/xiaoxue/memoryweave/internal/governance"
|
||||
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
|
||||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||||
)
|
||||
|
||||
|
|
@ -15,13 +18,44 @@ import (
|
|||
func NewServer() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 初始化存储层
|
||||
// ─── 初始化依赖 ──────────────────────────────
|
||||
ldb := storage.NewLanceClient()
|
||||
emb := storage.NewEmbedder(os.Getenv("VLLM_ENDPOINT"))
|
||||
rerank := storage.NewReranker(os.Getenv("RERANK_ENDPOINT"))
|
||||
|
||||
// 核心 API
|
||||
api := routes.NewAPI(ldb, emb, rerank)
|
||||
|
||||
// 知识图谱(零外部依赖 = 内存实现)
|
||||
graphStore := governance.NewInMemoryGraph()
|
||||
graphAPI := routes.NewGraphAPI(graphStore)
|
||||
|
||||
// 冲突检测器
|
||||
conflictDetector := governance.NewConflictDetector()
|
||||
conflictAPI := routes.NewConflictAPI(conflictDetector)
|
||||
|
||||
// 自优化
|
||||
gapDetector := selfoptimize.NewGapDetector()
|
||||
gapAPI := routes.NewGapAPI(gapDetector)
|
||||
|
||||
// 反馈
|
||||
feedbackAPI := routes.NewFeedbackAPI(ldb)
|
||||
|
||||
// 管理
|
||||
forgetter := governance.NewForgetter()
|
||||
adminAPI := routes.NewAdminAPI(ldb, forgetter)
|
||||
|
||||
// Agent 注册
|
||||
agentRegistry := routes.NewAgentRegistry(nil)
|
||||
|
||||
// 评估
|
||||
evalAPI := routes.NewEvalAPI(api.Pipeline, ldb)
|
||||
|
||||
// L3
|
||||
l3API := routes.WM
|
||||
|
||||
// ─── 路由注册 ──────────────────────────────
|
||||
|
||||
// 公共路由
|
||||
mux.HandleFunc("/health", routes.HandleHealth)
|
||||
|
||||
|
|
@ -32,12 +66,104 @@ func NewServer() http.Handler {
|
|||
mux.HandleFunc("/api/v1/stats", api.Stats)
|
||||
mux.HandleFunc("/api/v1/batch-commit", api.BatchCommit)
|
||||
|
||||
// M3: SSE 实时推送
|
||||
// SSE 实时推送
|
||||
mux.HandleFunc("/api/v1/ws/", routes.SSEBus.SSEHandler)
|
||||
|
||||
// M8: 深度整合
|
||||
mux.HandleFunc("/api/v1/admin/consolidate", routes.HandleConsolidate)
|
||||
// 知识图谱
|
||||
mux.HandleFunc("/api/v1/graph/stats", graphAPI.Stats)
|
||||
mux.HandleFunc("/api/v1/graph/query", graphAPI.Query)
|
||||
mux.HandleFunc("/api/v1/graph/navigate", graphAPI.Navigate)
|
||||
|
||||
log.Println("[zhiyid] 路由注册: /health /commit /recall /bootstrap /stats /batch-commit /ws/ /admin/consolidate")
|
||||
// 冲突管理
|
||||
mux.HandleFunc("/api/v1/conflicts", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
conflictAPI.List(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/v1/conflicts/resolve", conflictAPI.Resolve)
|
||||
|
||||
// 反馈
|
||||
mux.HandleFunc("/api/v1/feedback/useful", feedbackAPI.MarkUseful)
|
||||
mux.HandleFunc("/api/v1/feedback/not-useful", feedbackAPI.MarkNotUseful)
|
||||
mux.HandleFunc("/api/v1/feedback/deprecate", feedbackAPI.Deprecate)
|
||||
mux.HandleFunc("/api/v1/feedback/correct", feedbackAPI.Correct)
|
||||
|
||||
// 知识缺口
|
||||
mux.HandleFunc("/api/v1/gaps", gapAPI.List)
|
||||
mux.HandleFunc("/api/v1/gaps/detect", gapAPI.Detect)
|
||||
mux.HandleFunc("/api/v1/gaps/close/", gapAPI.Close)
|
||||
|
||||
// Agent 注册
|
||||
mux.HandleFunc("/api/v1/agents/register", agentRegistry.Register)
|
||||
mux.HandleFunc("/api/v1/agents", agentRegistry.List)
|
||||
|
||||
// 管理端点
|
||||
mux.HandleFunc("/api/v1/admin/consolidate", routes.HandleConsolidate)
|
||||
mux.HandleFunc("/api/v1/admin/forget", adminAPI.Forget)
|
||||
mux.HandleFunc("/api/v1/admin/backup", adminAPI.Backup)
|
||||
mux.HandleFunc("/api/v1/admin/audit", adminAPI.Audit)
|
||||
mux.HandleFunc("/api/v1/distilled/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "DELETE" {
|
||||
adminAPI.DeleteDistilled(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/v1/memory/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
adminAPI.Versions(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
// L3 世界模型
|
||||
mux.HandleFunc("/api/v1/l3/worldmodel", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
l3API.GetHandler(w, r)
|
||||
} else if r.Method == "POST" {
|
||||
l3API.UpdateHandler(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
// 触发器
|
||||
mux.HandleFunc("/api/v1/triggers", routes.Triggers.List)
|
||||
mux.HandleFunc("/api/v1/triggers/fire", routes.Triggers.Fire)
|
||||
|
||||
// Skill 结晶
|
||||
mux.HandleFunc("/api/v1/skills", routes.Skills.List)
|
||||
mux.HandleFunc("/api/v1/skills/", func(w http.ResponseWriter, r *http.Request) {
|
||||
routes.Skills.Trial(w, r)
|
||||
})
|
||||
|
||||
// 评估
|
||||
mux.HandleFunc("/api/v1/eval/run", evalAPI.Run)
|
||||
mux.HandleFunc("/api/v1/eval/history", evalAPI.History)
|
||||
mux.HandleFunc("/api/v1/eval/generate", evalAPI.Generate)
|
||||
|
||||
// 自调参
|
||||
mux.HandleFunc("/api/v1/tuning/status", routes.Tuner.StatusHandler)
|
||||
mux.HandleFunc("/api/v1/tuning/run", routes.Tuner.RunHandler)
|
||||
mux.HandleFunc("/api/v1/tuning/analytics", routes.Tuner.AnalyticsHandler)
|
||||
|
||||
// 自优化仪表盘
|
||||
mux.HandleFunc("/api/v1/metrics/self", func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics := selfoptimize.Dash.Metrics()
|
||||
data, _ := json.Marshal(metrics)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
// Obsidian 同步
|
||||
obsidian := routes.NewObsidianSyncer("", ldb)
|
||||
mux.HandleFunc("/api/v1/obsidian/push", obsidian.PushHandler)
|
||||
mux.HandleFunc("/api/v1/obsidian/pull", obsidian.PullHandler)
|
||||
mux.HandleFunc("/api/v1/obsidian/status", obsidian.StatusHandler)
|
||||
|
||||
log.Println("[zhiyid] 路由注册: /health /commit /recall /bootstrap /stats /batch-commit /ws/* /graph/* /conflicts/* /feedback/* /gaps/* /agents/* /admin/* /l3/* /triggers/* /skills/* /eval/* /tuning/* /obsidian/*")
|
||||
return middleware.Auth(mux)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,35 @@ func (gs *GraphStore) Navigate(entity string, maxHops int, namespace string) ([]
|
|||
return paths, nil
|
||||
}
|
||||
|
||||
// DB 返回底层 sql.DB(供路由层直接查询)
|
||||
func (gs *GraphStore) DB() *sql.DB {
|
||||
return gs.db
|
||||
}
|
||||
|
||||
// ListActive 返回所有活跃冲突
|
||||
func (cd *ConflictDetector) ListActive() []*Conflict {
|
||||
cd.mu.RLock()
|
||||
defer cd.mu.RUnlock()
|
||||
var list []*Conflict
|
||||
for _, c := range cd.active {
|
||||
if c.Status == "pending" {
|
||||
list = append(list, c)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Resolve 解决冲突
|
||||
func (cd *ConflictDetector) Resolve(id, resolution, winner string) error {
|
||||
cd.mu.Lock()
|
||||
defer cd.mu.Unlock()
|
||||
if c, ok := cd.active[id]; ok {
|
||||
c.Status = "resolved"
|
||||
c.Strategy = resolution
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prune 修剪图谱(删除孤立节点、低权重边)
|
||||
func (gs *GraphStore) Prune(minWeight float64) error {
|
||||
_, err := gs.db.Exec("DELETE FROM graph_edges WHERE weight < ?", minWeight)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
// 织忆 MemoryWeave — 内存知识图谱(零外部依赖)
|
||||
package governance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// InMemoryGraph 纯内存知识图谱,替代 SQLite 依赖
|
||||
type InMemoryGraph struct {
|
||||
mu sync.RWMutex
|
||||
nodes map[string]*GraphNode
|
||||
edges []*GraphEdge
|
||||
}
|
||||
|
||||
type GraphNode struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type GraphEdge struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Relation string `json:"relation"`
|
||||
Weight float64 `json:"weight"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
func NewInMemoryGraph() *InMemoryGraph {
|
||||
return &InMemoryGraph{
|
||||
nodes: make(map[string]*GraphNode),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *InMemoryGraph) AddNode(id, name, nodeType, namespace string) error {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.nodes[id] = &GraphNode{ID: id, Name: name, Type: nodeType, Namespace: namespace}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *InMemoryGraph) AddEdge(id, source, target, relation, namespace string, weight float64) error {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.edges = append(g.edges, &GraphEdge{
|
||||
ID: id, Source: source, Target: target,
|
||||
Relation: relation, Weight: weight, Namespace: namespace,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Navigate 多跳 BFS 导航
|
||||
func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
visited := map[string]bool{entity: true}
|
||||
queue := []string{entity}
|
||||
var paths []map[string]interface{}
|
||||
|
||||
for hop := 1; hop <= maxHops && len(queue) > 0; hop++ {
|
||||
var nextQueue []string
|
||||
for _, current := range queue {
|
||||
for _, e := range g.edges {
|
||||
if e.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
neighbor := ""
|
||||
if e.Source == current {
|
||||
neighbor = e.Target
|
||||
} else if e.Target == current {
|
||||
neighbor = e.Source
|
||||
}
|
||||
if neighbor == "" || visited[neighbor] {
|
||||
continue
|
||||
}
|
||||
visited[neighbor] = true
|
||||
nextQueue = append(nextQueue, neighbor)
|
||||
paths = append(paths, map[string]interface{}{
|
||||
"edge_id": e.ID,
|
||||
"source": current,
|
||||
"target": neighbor,
|
||||
"relation": e.Relation,
|
||||
"weight": e.Weight,
|
||||
"hop": hop,
|
||||
})
|
||||
}
|
||||
}
|
||||
queue = nextQueue
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// Stats 返回图谱统计
|
||||
func (g *InMemoryGraph) Stats() (nodeCount, edgeCount int, density float64) {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
nodeCount = len(g.nodes)
|
||||
edgeCount = len(g.edges)
|
||||
if nodeCount > 1 {
|
||||
density = float64(edgeCount) / float64(nodeCount*(nodeCount-1))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Prune 删除低权重边和孤立节点
|
||||
func (g *InMemoryGraph) Prune(minWeight float64) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
// 删除低权重边
|
||||
var kept []*GraphEdge
|
||||
for _, e := range g.edges {
|
||||
if e.Weight >= minWeight {
|
||||
kept = append(kept, e)
|
||||
}
|
||||
}
|
||||
g.edges = kept
|
||||
|
||||
// 删除孤立节点
|
||||
connected := make(map[string]bool)
|
||||
for _, e := range g.edges {
|
||||
connected[e.Source] = true
|
||||
connected[e.Target] = true
|
||||
}
|
||||
for id := range g.nodes {
|
||||
if !connected[id] {
|
||||
delete(g.nodes, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Query 按实体和关系查询
|
||||
func (g *InMemoryGraph) Query(entity, relation, namespace string) []map[string]interface{} {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
var results []map[string]interface{}
|
||||
for _, e := range g.edges {
|
||||
if e.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
if e.Source == entity || e.Target == entity {
|
||||
if relation == "" || containsRelation(e.Relation, relation) {
|
||||
results = append(results, map[string]interface{}{
|
||||
"edge_id": e.ID,
|
||||
"source": e.Source,
|
||||
"target": e.Target,
|
||||
"relation": e.Relation,
|
||||
"weight": e.Weight,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func containsRelation(rel, substr string) bool {
|
||||
return len(substr) == 0 || fmt.Sprintf("%s", rel) != ""
|
||||
// 简化实现:总是匹配
|
||||
}
|
||||
|
|
@ -292,6 +292,40 @@ func (pg *PrefetchGraph) RecordCoAccess(a, b string) {
|
|||
}
|
||||
|
||||
// GetPrefetch 获取某个 query 的预取候选项
|
||||
// RecordDeprecation 记录废弃操作
|
||||
func (d *Dashboard) RecordDeprecation() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.DeprecatedToday++
|
||||
}
|
||||
|
||||
// RecordCorrection 记录用户修正
|
||||
func (d *Dashboard) RecordCorrection(source string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.TotalFixes++
|
||||
if source == "muchen_correction" {
|
||||
d.CascadeFixedTotal++
|
||||
}
|
||||
}
|
||||
|
||||
// RecordGapClosed 记录缺口关闭
|
||||
func (d *Dashboard) RecordGapClosed() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.ClosedGaps++
|
||||
}
|
||||
|
||||
// RecordConflictResolved 记录冲突解决
|
||||
func (d *Dashboard) RecordConflictResolved(auto bool) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.TotalConflicts++
|
||||
if auto {
|
||||
d.AutoResolvedConflicts++
|
||||
}
|
||||
}
|
||||
|
||||
func (pg *PrefetchGraph) GetPrefetch(query string) []string {
|
||||
pg.mu.RLock()
|
||||
defer pg.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -218,5 +218,104 @@ func (c *LanceClient) InsertMemory(m models.MemoryRecord) error {
|
|||
return c.Insert("memories", m)
|
||||
}
|
||||
|
||||
// IncrementUseful 增加 useful 计数
|
||||
func (c *LanceClient) IncrementUseful(id string) {
|
||||
c.Update("memories", id, map[string]any{
|
||||
"useful_count": "useful_count + 1",
|
||||
"recall_count": "recall_count + 1",
|
||||
})
|
||||
}
|
||||
|
||||
// IncrementNotUseful 增加 not-useful 计数
|
||||
func (c *LanceClient) IncrementNotUseful(id string) {
|
||||
c.Update("memories", id, map[string]any{
|
||||
"not_useful_count": "not_useful_count + 1",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateMemoryContent 更新记忆内容并记录版本历史
|
||||
func (c *LanceClient) UpdateMemoryContent(id, newContent, source string) error {
|
||||
return c.Update("memories", id, map[string]any{
|
||||
"content": newContent,
|
||||
"source": source,
|
||||
"version": "version + 1",
|
||||
})
|
||||
}
|
||||
|
||||
// GetVersionHistory 获取记忆版本历史
|
||||
func (c *LanceClient) GetVersionHistory(id string) ([]map[string]interface{}, error) {
|
||||
// 从 memory 的 version_history JSON 字段读取
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/v1/table/memories/query", c.baseURL), nil)
|
||||
q := req.URL.Query()
|
||||
q.Set("filter", fmt.Sprintf("id = '%s'", id))
|
||||
q.Set("columns", "id,version_history")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetCandidatesForForgetting 获取可遗忘的候选记忆
|
||||
func (c *LanceClient) GetCandidatesForForgetting() ([]map[string]interface{}, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"filter": "is_deleted = false AND tier != 'core'",
|
||||
"order": "last_recalled_at ASC",
|
||||
"top_k": 100,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/table/memories/query", c.baseURL), bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Backup 备份数据到指定目录
|
||||
func (c *LanceClient) Backup(path string) error {
|
||||
// 调用 LanceDB 原生备份接口
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/backup", c.baseURL),
|
||||
bytes.NewReader([]byte(fmt.Sprintf(`{"path":"%s"}`, path))))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAuditLog 获取最近 N 条审计日志
|
||||
func (c *LanceClient) GetAuditLog(limit int) ([]map[string]interface{}, error) {
|
||||
// 从 audits 表或 tombstones 表取
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/v1/table/tombstones/query", c.baseURL), nil)
|
||||
q := req.URL.Query()
|
||||
q.Set("top_k", fmt.Sprintf("%d", limit))
|
||||
q.Set("order", "deleted_at DESC")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// LanceDB 类型别名,兼容路由层引用
|
||||
type LanceDB = LanceClient
|
||||
|
|
|
|||
Loading…
Reference in New Issue