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)})
|
||||
}
|
||||
Reference in new issue
Block a user