Files
zeekayandHanzo Dev df58dde0e9 One address per vector operation
The control plane served two writes and two deletes for one vector. The
second delete read its scope from the query string only, so a scope sent in
its body was ignored. The REST pair remains, POST /v1/memory/vector and
DELETE /v1/memory/vector/:key, and every SDK calls it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 04:10:34 -07:00

251 lines
6.6 KiB
Go

package handlers
import (
"net/http"
"github.com/hanzoai/agents/control-plane/internal/logger"
"github.com/hanzoai/agents/control-plane/pkg/types"
"github.com/gin-gonic/gin"
)
// SetVectorRequest captures inputs for storing a vector embedding.
type SetVectorRequest struct {
Key string `json:"key" binding:"required"`
Embedding []float32 `json:"embedding" binding:"required"`
Metadata map[string]interface{} `json:"metadata"`
Scope *string `json:"scope,omitempty"`
}
// DeleteNamespaceRequest removes all vectors by namespace prefix.
type DeleteNamespaceRequest struct {
Namespace string `json:"namespace" binding:"required"`
Scope *string `json:"scope,omitempty"`
}
// VectorSearchRequest describes a similarity search query.
type VectorSearchRequest struct {
QueryEmbedding []float32 `json:"query_embedding" binding:"required"`
TopK int `json:"top_k"`
Filters map[string]interface{} `json:"filters"`
Scope *string `json:"scope,omitempty"`
}
// SetVectorHandler stores or updates a vector embedding.
func SetVectorHandler(storage MemoryStorage) gin.HandlerFunc {
return func(c *gin.Context) {
var req SetVectorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: err.Error(),
Code: http.StatusBadRequest,
})
return
}
if len(req.Embedding) == 0 {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: "embedding cannot be empty",
Code: http.StatusBadRequest,
})
return
}
scope, scopeID := resolveScope(c, req.Scope)
record := &types.VectorRecord{
Scope: scope,
ScopeID: scopeID,
Key: req.Key,
Embedding: req.Embedding,
Metadata: req.Metadata,
}
if err := storage.SetVector(c.Request.Context(), record); err != nil {
logger.Logger.Error().Err(err).Msg("failed to set vector")
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "storage_error",
Message: err.Error(),
Code: http.StatusInternalServerError,
})
return
}
c.JSON(http.StatusOK, gin.H{
"key": record.Key,
"scope": record.Scope,
"scope_id": record.ScopeID,
"metadata": record.Metadata,
})
}
}
// GetVectorHandler retrieves a vector by key.
func GetVectorHandler(storage MemoryStorage) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.Param("key")
if key == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: "key is required",
Code: http.StatusBadRequest,
})
return
}
scopeParam := c.Query("scope")
var scopePtr *string
if scopeParam != "" {
scopePtr = &scopeParam
}
scope, scopeID := resolveScope(c, scopePtr)
record, err := storage.GetVector(c.Request.Context(), scope, scopeID, key)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to get vector")
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "storage_error",
Message: err.Error(),
Code: http.StatusInternalServerError,
})
return
}
if record == nil {
c.JSON(http.StatusNotFound, ErrorResponse{
Error: "not_found",
Message: "vector not found",
Code: http.StatusNotFound,
})
return
}
c.JSON(http.StatusOK, record)
}
}
// DeleteVectorHandler removes a vector by key.
func DeleteVectorHandler(storage MemoryStorage) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.Param("key")
if key == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: "key is required",
Code: http.StatusBadRequest,
})
return
}
scopeParam := c.Query("scope")
var scopePtr *string
if scopeParam != "" {
scopePtr = &scopeParam
}
scope, scopeID := resolveScope(c, scopePtr)
if err := storage.DeleteVector(c.Request.Context(), scope, scopeID, key); err != nil {
logger.Logger.Error().Err(err).Msg("failed to delete vector")
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "storage_error",
Message: err.Error(),
Code: http.StatusInternalServerError,
})
return
}
c.Status(http.StatusNoContent)
}
}
// DeleteNamespaceVectorsHandler removes all vectors whose keys start with the namespace prefix.
func DeleteNamespaceVectorsHandler(storage MemoryStorage) gin.HandlerFunc {
return func(c *gin.Context) {
var req DeleteNamespaceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: err.Error(),
Code: http.StatusBadRequest,
})
return
}
if req.Namespace == "" {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: "namespace is required",
Code: http.StatusBadRequest,
})
return
}
scope, scopeID := resolveScope(c, req.Scope)
deleted, err := storage.DeleteVectorsByPrefix(c.Request.Context(), scope, scopeID, req.Namespace)
if err != nil {
logger.Logger.Error().Err(err).Msg("failed to delete namespace vectors")
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "storage_error",
Message: err.Error(),
Code: http.StatusInternalServerError,
})
return
}
c.JSON(http.StatusOK, gin.H{
"namespace": req.Namespace,
"deleted": deleted,
"scope": scope,
"scope_id": scopeID,
"status": "deleted",
})
}
}
// SimilaritySearchHandler performs a similarity search.
func SimilaritySearchHandler(storage MemoryStorage) gin.HandlerFunc {
return func(c *gin.Context) {
var req VectorSearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: err.Error(),
Code: http.StatusBadRequest,
})
return
}
if len(req.QueryEmbedding) == 0 {
c.JSON(http.StatusBadRequest, ErrorResponse{
Error: "invalid_request",
Message: "query_embedding cannot be empty",
Code: http.StatusBadRequest,
})
return
}
if req.TopK <= 0 {
req.TopK = 10
}
scope, scopeID := resolveScope(c, req.Scope)
results, err := storage.SimilaritySearch(
c.Request.Context(),
scope,
scopeID,
req.QueryEmbedding,
req.TopK,
req.Filters,
)
if err != nil {
logger.Logger.Error().Err(err).Msg("vector search failed")
c.JSON(http.StatusInternalServerError, ErrorResponse{
Error: "storage_error",
Message: err.Error(),
Code: http.StatusInternalServerError,
})
return
}
c.JSON(http.StatusOK, results)
}
}