feat(rag): pgvector knowledge base with API and LLM tools
- 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:
1 parent
ac0c2b2c73
commit
9c8d5a9e8d
6 files changed
+432
-2
No files matched your search
@@ -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)})
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in new issue
Block a user