Files
goclaw/internal/http/knowledge_graph_handlers.go
T
viettranx 28fab9507a feat(storage): add lazy folder loading, SSE size endpoint, and enhanced file viewer
- Backend: depth-limited WalkDir (max 3 levels default) with on-demand subtree loading
- Backend: new GET /v1/storage/size SSE endpoint with 60min in-memory cache
- Backend: raw binary file serving (?raw=true) with MIME detection and download support
- Frontend: lazy tree expansion with loading spinners for deep folders
- Frontend: streaming size display with cache info tooltip
- Frontend: image viewer (blob URL), unsupported file UI, download button, colored size badges
- Frontend: file-type icons for 13 categories (md, json, yaml, images, video, etc.)
- Fix sidebar connection status text overflow on collapse
- Apply go fix modernization (interface{} → any) across http handlers
2026-03-14 18:13:52 +07:00

265 lines
8.0 KiB
Go

package http
import (
"encoding/json"
"log/slog"
"net/http"
"strconv"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
func (h *KnowledgeGraphHandler) handleListEntities(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agentID")
userID := r.URL.Query().Get("user_id")
entityType := r.URL.Query().Get("type")
query := r.URL.Query().Get("q")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit <= 0 {
limit = 50
}
// If query is provided, use search
if query != "" {
entities, err := h.store.SearchEntities(r.Context(), agentID, userID, query, limit)
if err != nil {
slog.Warn("kg.search_entities failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if entities == nil {
entities = []store.Entity{}
}
writeJSON(w, http.StatusOK, entities)
return
}
entities, err := h.store.ListEntities(r.Context(), agentID, userID, store.EntityListOptions{
EntityType: entityType,
Limit: limit,
Offset: offset,
})
if err != nil {
slog.Warn("kg.list_entities failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if entities == nil {
entities = []store.Entity{}
}
writeJSON(w, http.StatusOK, entities)
}
func (h *KnowledgeGraphHandler) handleGetEntity(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
agentID := r.PathValue("agentID")
entityID := r.PathValue("entityID")
userID := r.URL.Query().Get("user_id")
entity, err := h.store.GetEntity(r.Context(), agentID, userID, entityID)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "entity", entityID)})
return
}
relations, err := h.store.ListRelations(r.Context(), agentID, userID, entityID)
if err != nil {
relations = []store.Relation{}
}
if relations == nil {
relations = []store.Relation{}
}
writeJSON(w, http.StatusOK, map[string]any{
"entity": entity,
"relations": relations,
})
}
func (h *KnowledgeGraphHandler) handleUpsertEntity(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
agentID := r.PathValue("agentID")
var entity store.Entity
if err := json.NewDecoder(r.Body).Decode(&entity); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
return
}
entity.AgentID = agentID
if entity.ExternalID == "" || entity.Name == "" || entity.EntityType == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityFieldsRequired)})
return
}
if entity.Confidence <= 0 {
entity.Confidence = 1.0
}
if err := h.store.UpsertEntity(r.Context(), &entity); err != nil {
slog.Warn("kg.upsert_entity failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *KnowledgeGraphHandler) handleDeleteEntity(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agentID")
entityID := r.PathValue("entityID")
userID := r.URL.Query().Get("user_id")
if err := h.store.DeleteEntity(r.Context(), agentID, userID, entityID); err != nil {
slog.Warn("kg.delete_entity failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
func (h *KnowledgeGraphHandler) handleTraverse(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
agentID := r.PathValue("agentID")
var body struct {
EntityID string `json:"entity_id"`
UserID string `json:"user_id"`
MaxDepth int `json:"max_depth"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
return
}
if body.EntityID == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgEntityIDRequired)})
return
}
if body.MaxDepth <= 0 {
body.MaxDepth = 2
}
if body.MaxDepth > 3 {
body.MaxDepth = 3
}
results, err := h.store.Traverse(r.Context(), agentID, body.UserID, body.EntityID, body.MaxDepth)
if err != nil {
slog.Warn("kg.traverse failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if results == nil {
results = []store.TraversalResult{}
}
writeJSON(w, http.StatusOK, results)
}
func (h *KnowledgeGraphHandler) handleExtract(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
agentID := r.PathValue("agentID")
var body struct {
Text string `json:"text"`
UserID string `json:"user_id"`
Provider string `json:"provider"`
Model string `json:"model"`
MinConf float64 `json:"min_confidence"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
return
}
if body.Text == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgTextRequired)})
return
}
if body.Provider == "" || body.Model == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgProviderModelRequired)})
return
}
extractor := h.NewExtractor(body.Provider, body.Model, body.MinConf)
if extractor == nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidProviderOrModel)})
return
}
result, err := extractor.Extract(r.Context(), body.Text)
if err != nil {
slog.Warn("kg.extract failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
// Ingest extracted entities and relations into the store
for i := range result.Entities {
result.Entities[i].AgentID = agentID
result.Entities[i].UserID = body.UserID
}
for i := range result.Relations {
result.Relations[i].AgentID = agentID
result.Relations[i].UserID = body.UserID
}
if err := h.store.IngestExtraction(r.Context(), agentID, body.UserID, result.Entities, result.Relations); err != nil {
slog.Warn("kg.ingest_extraction failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"entities": len(result.Entities),
"relations": len(result.Relations),
})
}
func (h *KnowledgeGraphHandler) handleStats(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agentID")
userID := r.URL.Query().Get("user_id")
stats, err := h.store.Stats(r.Context(), agentID, userID)
if err != nil {
slog.Warn("kg.stats failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, stats)
}
// handleGraph returns all entities + relations for graph visualization.
func (h *KnowledgeGraphHandler) handleGraph(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("agentID")
userID := r.URL.Query().Get("user_id")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 200
}
entities, err := h.store.ListEntities(r.Context(), agentID, userID, store.EntityListOptions{Limit: limit})
if err != nil {
slog.Warn("kg.graph entities failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if entities == nil {
entities = []store.Entity{}
}
relations, err := h.store.ListAllRelations(r.Context(), agentID, userID, limit*3)
if err != nil {
slog.Warn("kg.graph relations failed", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if relations == nil {
relations = []store.Relation{}
}
writeJSON(w, http.StatusOK, map[string]any{
"entities": entities,
"relations": relations,
})
}