Files
library-desk/src/routers/vector.py
T
2025-12-11 17:28:23 +01:00

174 lines
5.4 KiB
Python

"""
Vector router for Library Desk API.
Endpoints for semantic search and vector operations.
"""
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import Optional
import logging
from src.models.vector import (
SearchRequest, SearchResponse,
VectorUpdateRequest, VectorUpdateSummary,
CollectionListResponse,
DeletePageChunksRequest, DeletePageChunksResponse
)
from src.services.vector_service import VectorService
from src.clients.qdrant_client import QdrantClientWrapper
from src.clients.wikijs_client import WikiJSClient
from src.clients.ollama_client import OllamaClient
from src.core.dependencies import QdrantDep, WikiJSDep, OllamaDep, verify_api_key
from src.core.multi_tenancy import DEFAULT_USER
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/vector", tags=["Vector"])
# Dependency to get vector service
def get_vector_service(
qdrant_client: QdrantDep,
wiki_client: WikiJSDep,
ollama_client: OllamaDep
) -> VectorService:
"""Get vector service instance."""
return VectorService(qdrant_client, wiki_client, ollama_client)
@router.post("/search", response_model=SearchResponse)
async def semantic_search(
request: SearchRequest,
vector_service: VectorService = Depends(get_vector_service),
api_key: str = Depends(verify_api_key)
):
"""
Perform semantic search across user's documents.
Uses Ollama to generate query embedding, then searches Qdrant
for similar document chunks.
**Example Request:**
```json
{
"query": "how to configure docker",
"user": "jpmschweitzer",
"limit": 10,
"score_threshold": 0.5
}
```
**Returns:** List of matching chunks with similarity scores
"""
try:
return await vector_service.search(
query=request.query,
user=request.user,
limit=request.limit,
score_threshold=request.score_threshold
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Semantic search failed: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Search failed")
@router.post("/update-from-page/{page_id}", response_model=VectorUpdateSummary)
async def update_vectors_from_page(
page_id: int,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
force_refresh: bool = Query(default=False, description="Force re-embedding"),
vector_service: VectorService = Depends(get_vector_service),
api_key: str = Depends(verify_api_key)
):
"""
Update vector embeddings from a wiki page.
This endpoint:
1. Fetches the page from Wiki.js
2. Chunks the content (500 tokens with 50 token overlap)
3. Generates embeddings via Ollama
4. Upserts chunks to Qdrant with metadata
**Use Cases:**
- Called automatically after page creation/update (via BackgroundTasks)
- Called manually by user/Librarian to refresh vectors
- Called by Scheduler for batch processing
**Example:** `POST /vector/update-from-page/5?user=jpmschweitzer`
**Returns:** Summary with chunks created and processing time
"""
try:
summary = await vector_service.update_from_page(
page_id=page_id,
user=user,
force_refresh=force_refresh
)
if not summary.success:
raise HTTPException(
status_code=500,
detail=f"Vector update failed: {summary.error_message}"
)
return summary
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update vectors from page {page_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Vector update failed")
@router.delete("/pages/{page_id}", response_model=DeletePageChunksResponse)
async def delete_page_chunks(
page_id: int,
user: str = Query(default=DEFAULT_USER, description="User identifier"),
vector_service: VectorService = Depends(get_vector_service),
api_key: str = Depends(verify_api_key)
):
"""
Delete all vector chunks for a wiki page.
This is automatically called when a page is deleted from the wiki.
**Example:** `DELETE /vector/pages/5?user=jpmschweitzer`
"""
try:
deleted_count = await vector_service.delete_page_chunks(
page_id=page_id,
user=user
)
return DeletePageChunksResponse(
page_id=page_id,
chunks_deleted=deleted_count,
success=deleted_count > 0,
message=f"Deleted {deleted_count} chunks for page {page_id}"
)
except Exception as e:
logger.error(f"Failed to delete chunks for page {page_id}: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to delete chunks")
@router.get("/collections", response_model=CollectionListResponse)
async def list_collections(
vector_service: VectorService = Depends(get_vector_service),
api_key: str = Depends(verify_api_key)
):
"""
List all Qdrant collections with statistics.
Returns collection names, vector counts, and point counts.
**Example:** `GET /vector/collections`
"""
try:
return await vector_service.list_collections()
except Exception as e:
logger.error(f"Failed to list collections: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to list collections")