174 lines
5.4 KiB
Python
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")
|