Initial commit: library-desk service extraction from portainer-core
Build and Push / build (release) Successful in 36s

This commit is contained in:
2025-12-11 17:28:23 +01:00
commit 95852190ba
59 changed files with 17146 additions and 0 deletions
+173
View File
@@ -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")