""" Admin API — /api/v1/collections, /api/v1/delete Management endpoints for tenant collections. """ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from loguru import logger from app.core.qdrant_client import qdrant_manager from app.core.security import verify_api_key router = APIRouter() # ─── List All Collections ───────────────────────────────────────────────────── @router.get("/collections") async def list_collections(_: str = Depends(verify_api_key)): """ List all Qdrant collections currently in the instance. Returns both kb_* tenant collections and any other collections. **Required Header:** `X-RAG-API-Key` """ try: collections = await qdrant_manager.list_all_collections() tenant_collections = [c for c in collections if c.startswith("kb_")] return { "total": len(collections), "tenant_collections": tenant_collections, "all_collections": collections, } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ─── Collection Info ────────────────────────────────────────────────────────── @router.get("/collections/{company_id}") async def get_collection_info( company_id: str, _: str = Depends(verify_api_key), ): """ Get stats for a specific tenant's collection. Returns vector count, point count, and status. **Required Header:** `X-RAG-API-Key` """ try: info = await qdrant_manager.get_collection_info(company_id) return info except Exception as e: raise HTTPException(status_code=404, detail=f"Collection not found: {str(e)}") # ─── Delete Tenant Collection ───────────────────────────────────────────────── class DeleteRequest(BaseModel): company_id: str = Field(..., description="Tenant to permanently remove.") confirm: bool = Field( ..., description="Must be `true` to confirm deletion. Irreversible.", ) @router.post("/delete") async def delete_tenant( body: DeleteRequest, _: str = Depends(verify_api_key), ): """ Permanently delete a tenant's entire knowledge base collection. **This is irreversible.** All embedded documents are lost. Requires `confirm: true` in the request body as a safety check. Uses POST instead of DELETE to avoid request body stripping by proxies/CDNs. **Required Header:** `X-RAG-API-Key` """ if not body.confirm: raise HTTPException( status_code=400, detail="Deletion not confirmed. Set 'confirm': true to proceed.", ) try: result = await qdrant_manager.delete_tenant_collection(body.company_id) logger.info(f"Admin deleted collection for '{body.company_id}'") return { "status": "deleted", "company_id": body.company_id, "collection": result["collection"], "message": f"Collection '{result['collection']}' permanently deleted.", } except Exception as e: logger.error(f"Delete failed for '{body.company_id}': {e}") raise HTTPException(status_code=500, detail=str(e)) # ─── Delete Specific Source from a Collection ───────────────────────────────── class DeleteSourceRequest(BaseModel): company_id: str = Field(...) source: str = Field(..., description="Filename or URL to remove from the collection.") @router.post("/delete/source") async def delete_source( body: DeleteSourceRequest, _: str = Depends(verify_api_key), ): """ Delete all chunks belonging to a specific source (file or URL) from a tenant's collection, without deleting the entire collection. Use this when a company updates a document — delete old version, re-ingest new. Uses POST instead of DELETE to avoid request body stripping by proxies/CDNs. **Required Header:** `X-RAG-API-Key` """ try: await qdrant_manager.delete_chunks_by_source(body.company_id, body.source) return { "status": "deleted", "company_id": body.company_id, "source": body.source, "message": f"All chunks for source '{body.source}' deleted from 'kb_{body.company_id}'.", } except Exception as e: raise HTTPException(status_code=500, detail=str(e))