Initial commit: library-desk service extraction from portainer-core
Build and Push / build (release) Successful in 36s
Build and Push / build (release) Successful in 36s
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user