accounts/server/api/rag.go

102 lines
2.5 KiB
Go
Raw Normal View History

2025-08-07 08:19:34 +08:00
package api
import (
2025-08-10 10:32:07 +08:00
"context"
2025-08-14 16:32:03 +08:00
"errors"
2025-08-07 08:19:34 +08:00
"net/http"
2025-08-14 13:06:49 +08:00
"sync"
2025-08-07 08:19:34 +08:00
"github.com/gin-gonic/gin"
2025-08-09 18:42:19 +08:00
"xcontrol/internal/rag"
rconfig "xcontrol/internal/rag/config"
2025-08-14 16:32:03 +08:00
ragembed "xcontrol/internal/rag/embed"
2025-08-09 18:42:19 +08:00
"xcontrol/internal/rag/store"
2025-08-09 15:54:27 +08:00
"xcontrol/server/proxy"
2025-08-07 08:19:34 +08:00
)
2025-08-10 10:32:07 +08:00
// ragService defines methods used by the RAG API. It allows tests to supply a
// mock implementation without touching the real vector database or embedding
// service.
type ragService interface {
Upsert(ctx context.Context, rows []store.DocRow) (int, error)
Query(ctx context.Context, question string, limit int) ([]rag.Document, error)
}
2025-08-14 13:06:49 +08:00
// ragSvc handles RAG document storage and retrieval. It is initialized lazily
// on demand. ragMu guards concurrent initialization attempts.
var (
ragSvc ragService
ragMu sync.Mutex
)
2025-08-07 08:19:34 +08:00
// initRAG attempts to construct a RAG service from server configuration.
2025-08-10 10:32:07 +08:00
func initRAG() ragService {
2025-08-07 08:19:34 +08:00
cfg, err := rconfig.LoadServer()
if err != nil {
return nil
}
2025-08-09 15:54:27 +08:00
proxy.Set(cfg.Proxy)
2025-08-09 17:43:23 +08:00
return rag.New(cfg.ToConfig())
2025-08-07 08:19:34 +08:00
}
2025-08-14 13:06:49 +08:00
// getRAG returns an initialized ragService, creating it if necessary.
func getRAG() ragService {
ragMu.Lock()
defer ragMu.Unlock()
if ragSvc == nil {
ragSvc = initRAG()
}
return ragSvc
}
2025-08-09 17:23:21 +08:00
// registerRAGRoutes wires the /api/rag upsert and query endpoints.
2025-08-07 08:19:34 +08:00
func registerRAGRoutes(r *gin.RouterGroup) {
2025-08-09 17:23:21 +08:00
r.POST("/rag/upsert", func(c *gin.Context) {
2025-08-14 13:06:49 +08:00
svc := getRAG()
if svc == nil {
2025-08-09 17:23:21 +08:00
c.JSON(http.StatusOK, gin.H{"rows": 0})
2025-08-07 08:19:34 +08:00
return
}
2025-08-09 17:23:21 +08:00
var req struct {
Docs []store.DocRow `json:"docs"`
}
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
2025-08-14 13:06:49 +08:00
n, err := svc.Upsert(c.Request.Context(), req.Docs)
2025-08-09 14:55:22 +08:00
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"rows": 0, "error": err.Error()})
2025-08-07 08:19:34 +08:00
return
}
2025-08-09 17:23:21 +08:00
c.JSON(http.StatusOK, gin.H{"rows": n})
2025-08-07 08:19:34 +08:00
})
r.POST("/rag/query", func(c *gin.Context) {
var req struct {
Question string `json:"question"`
}
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
2025-08-14 13:06:49 +08:00
svc := getRAG()
if svc == nil {
2025-08-07 08:19:34 +08:00
c.JSON(http.StatusOK, gin.H{"chunks": nil})
return
}
2025-08-14 13:06:49 +08:00
docs, err := svc.Query(c.Request.Context(), req.Question, 5)
2025-08-07 08:19:34 +08:00
if err != nil {
2025-08-14 16:32:03 +08:00
var httpErr *ragembed.HTTPError
if errors.As(err, &httpErr) {
c.JSON(httpErr.Code, gin.H{"error": httpErr.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
2025-08-07 08:19:34 +08:00
return
}
c.JSON(http.StatusOK, gin.H{"chunks": docs})
})
}