feat(rag): pgvector knowledge base with API and LLM tools
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled

- services/rag: knowledge_docs store — vector search via the shared
  embedder (HNSW index, graceful ILIKE fallback), list/delete scoped to
  the user, search intentionally global for the single-user deployment
- gateway: GET/POST /knowledge/docs, DELETE /knowledge/docs/:id,
  POST /knowledge/search
- plugins/knowledge: knowledge_search and knowledge_ingest tools so the
  persona can save and recall information mid-conversation
- main: migrations, store wiring, plugin registration
This commit is contained in:
Aska committed 2026-08-21 18:40:01 +08:00
1 parent ac0c2b2c73
commit 9c8d5a9e8d
6 files changed
+432 -2

No files matched your search

+56 -2
View File
@@ -258,8 +258,7 @@ func (r *Router) sessionExportHandler(w http.ResponseWriter, req *http.Request)
}
}
func (r *Router) imageAnalyzeHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
func (r *Router) imageAnalyzeHandler(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
if r.deps.Vision == nil { writeJSON(w, 503, M{"error": "vision not available"}); return }
var body struct {
ImageURL string `json:"image_url"`
@@ -305,3 +304,58 @@ func (r *Router) hostExecHandler(w http.ResponseWriter, req *http.Request) {
"command": body.Command,
})
}
// ===== Knowledge Base =====
func (r *Router) knowledgeDocsHandler(w http.ResponseWriter, req *http.Request) {
if r.deps.Knowledge == nil { writeJSON(w, 503, M{"error": "knowledge base unavailable"}); return }
uid := r.userID(req)
switch req.Method {
case "GET":
docs, err := r.deps.Knowledge.List(req.Context(), uid, 50)
if err != nil { writeJSON(w, 500, M{"error": err.Error()}); return }
writeJSON(w, 200, M{"docs": docs})
case "POST":
var body struct {
Title string `json:"title"`
Content string `json:"content"`
}
json.NewDecoder(req.Body).Decode(&body)
if body.Content == "" { writeJSON(w, 400, M{"error": "missing content"}); return }
doc, err := r.deps.Knowledge.Ingest(req.Context(), uid, body.Title, body.Content)
if err != nil { writeJSON(w, 500, M{"error": err.Error()}); return }
writeJSON(w, 201, M{"doc": doc})
default:
writeJSON(w, 405, M{"error": "method not allowed"})
}
}
func (r *Router) knowledgeDocItemHandler(w http.ResponseWriter, req *http.Request) {
if r.deps.Knowledge == nil { writeJSON(w, 503, M{"error": "knowledge base unavailable"}); return }
if req.Method != "DELETE" { writeJSON(w, 405, M{"error": "method not allowed"}); return }
idStr := strings.TrimPrefix(req.URL.Path, "/api/v1/knowledge/docs/")
var id int64
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil || id <= 0 {
writeJSON(w, 400, M{"error": "invalid id"})
return
}
if err := r.deps.Knowledge.Delete(req.Context(), r.userID(req), id); err != nil {
writeJSON(w, 404, M{"error": err.Error()})
return
}
writeJSON(w, 204, nil)
}
func (r *Router) knowledgeSearchHandler(w http.ResponseWriter, req *http.Request) {
if r.deps.Knowledge == nil { writeJSON(w, 503, M{"error": "knowledge base unavailable"}); return }
if req.Method != "POST" { writeJSON(w, 405, M{"error": "POST required"}); return }
var body struct {
Query string `json:"query"`
TopK int `json:"top_k"`
}
json.NewDecoder(req.Body).Decode(&body)
if body.Query == "" { writeJSON(w, 400, M{"error": "missing query"}); return }
docs, err := r.deps.Knowledge.Search(req.Context(), r.userID(req), body.Query, body.TopK)
if err != nil { writeJSON(w, 500, M{"error": err.Error()}); return }
writeJSON(w, 200, M{"docs": docs, "total": len(docs)})
}
+15
View File
@@ -13,6 +13,7 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/store"
"git.yeij.top/AskaEth/Cyrene_v2/services/rag"
)
// --- Interfaces for injected services ---
@@ -96,6 +97,14 @@ type VisionAnalyzer interface {
AnalyzeImage(ctx context.Context, prompt, imageRef string) (string, error)
}
// KnowledgeStorer manages the knowledge base.
type KnowledgeStorer interface {
Ingest(ctx context.Context, userID, title, content string) (*rag.Document, error)
Search(ctx context.Context, userID, query string, topK int) ([]rag.Document, error)
List(ctx context.Context, userID string, limit int) ([]rag.Document, error)
Delete(ctx context.Context, userID string, id int64) error
}
// ChatStorer persists chat sessions and messages.
type ChatStorer interface {
CreateSession(ctx context.Context, id, userID, title string) (*store.Session, error)
@@ -123,6 +132,7 @@ type Dependencies struct {
AutomationEng AutomationEngine
Reminders *ReminderStore
Vision VisionAnalyzer
Knowledge KnowledgeStorer
PluginList []map[string]string
}
@@ -184,6 +194,11 @@ func (r *Router) setupRoutes() {
// Message search & session export
r.mux.HandleFunc("/api/v1/messages/search", r.authMW(r.messageSearchHandler))
// Knowledge base
r.mux.HandleFunc("/api/v1/knowledge/docs", r.authMW(r.knowledgeDocsHandler))
r.mux.HandleFunc("/api/v1/knowledge/docs/", r.authMW(r.knowledgeDocItemHandler))
r.mux.HandleFunc("/api/v1/knowledge/search", r.authMW(r.knowledgeSearchHandler))
// Image analysis
r.mux.HandleFunc("/api/v1/images/analyze", r.authMW(r.imageAnalyzeHandler))