80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
"""Vectors routes: upsert, search, delete cloud vector store entries, and embed text."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from pydantic import BaseModel
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.core.llm import embed
|
|
from app.schemas import (
|
|
UserProfile,
|
|
VectorSearchRequest,
|
|
VectorSearchResponse,
|
|
VectorUpsertRequest,
|
|
)
|
|
from app.storage.encryption import reject_if_tampered
|
|
from app.storage.vector_store import VectorStore
|
|
|
|
router = APIRouter(prefix="/storage", tags=["vectors"])
|
|
|
|
_vector_store = VectorStore()
|
|
|
|
|
|
class _VectorDeleteRequest(BaseModel):
|
|
ids: list[str]
|
|
|
|
|
|
class _EmbedRequest(BaseModel):
|
|
text: str
|
|
|
|
|
|
class _EmbedResponse(BaseModel):
|
|
vector: list[float]
|
|
|
|
|
|
@router.post("/vectors/upsert", response_model=dict)
|
|
async def upsert_vectors(
|
|
body: VectorUpsertRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> dict[str, int]:
|
|
"""Verify checksums and store encrypted vectors in the user-scoped namespace."""
|
|
for item in body.vectors:
|
|
reject_if_tampered(item.blob, item.checksum)
|
|
await _vector_store.upsert(current_user.id, body.vectors)
|
|
return {"upserted": len(body.vectors)}
|
|
|
|
|
|
@router.post("/vectors/search", response_model=VectorSearchResponse)
|
|
async def search_vectors(
|
|
body: VectorSearchRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> VectorSearchResponse:
|
|
"""Search the user-scoped vector namespace with an encrypted query blob."""
|
|
results = await _vector_store.search(current_user.id, body.query_blob, body.top_k)
|
|
return VectorSearchResponse(results=results)
|
|
|
|
|
|
@router.delete("/vectors", response_model=dict)
|
|
async def delete_vectors(
|
|
body: _VectorDeleteRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> dict[str, bool]:
|
|
"""Delete vectors by ID, scoped to the authenticated user."""
|
|
await _vector_store.delete(current_user.id, body.ids)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/vectors/embed", response_model=_EmbedResponse)
|
|
async def embed_text(
|
|
body: _EmbedRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> _EmbedResponse:
|
|
"""Generate a 1536-dim embedding vector for the given text.
|
|
|
|
Uses ``text-embedding-3-small`` via OpenAI. Auth required (JWT).
|
|
Used by backend tools (note_agent) and Electron (vectordb.ts) alike.
|
|
"""
|
|
vector = await embed(body.text)
|
|
return _EmbedResponse(vector=vector)
|