from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from src.api.deps import get_db from src.api.schemas.ingest import BookInfo, StatsResponse from src.core.llm import list_available_models from src.core.vector_store import get_collection_count from src.db.repository import delete_book, get_all_books, get_book_by_id, get_book_chunks, get_stats router = APIRouter(prefix="/admin", tags=["Admin"]) @router.get("/stats", response_model=StatsResponse) async def collection_stats(session: AsyncSession = Depends(get_db)): """Get overall collection statistics.""" stats = await get_stats(session) stats["vector_store_count"] = get_collection_count() return StatsResponse(**stats) @router.get("/books", response_model=list[BookInfo]) async def list_books(session: AsyncSession = Depends(get_db)): """List all books in the collection.""" books = await get_all_books(session) return [ BookInfo( id=b.id, title=b.title, author=b.author, publication_year=b.publication_year, edition=b.edition, ingestion_status=b.ingestion_status, total_chunks=b.total_chunks, source_file=b.source_file, notes=b.notes, ) for b in books ] @router.get("/books/{book_id}", response_model=BookInfo) async def get_book(book_id: str, session: AsyncSession = Depends(get_db)): """Get details of a specific book.""" book = await get_book_by_id(session, book_id) if not book: raise HTTPException(status_code=404, detail="Book not found") return BookInfo( id=book.id, title=book.title, author=book.author, publication_year=book.publication_year, edition=book.edition, ingestion_status=book.ingestion_status, total_chunks=book.total_chunks, source_file=book.source_file, ) @router.get("/books/{book_id}/fulltext") async def get_book_fulltext(book_id: str, session: AsyncSession = Depends(get_db)): """Get the full reconstructed text of a book, organized by chapter.""" book = await get_book_by_id(session, book_id) if not book: raise HTTPException(status_code=404, detail="Book not found") chunks = await get_book_chunks(session, book_id) # Organize chunks into chapters chapters: dict[str, list] = {} for chunk in chunks: chapter_key = chunk.chapter_title or ( f"Chapter {chunk.chapter_number}" if chunk.chapter_number else "Full Text" ) if chapter_key not in chapters: chapters[chapter_key] = [] chapters[chapter_key].append({ "text": chunk.content, "page": chunk.page_start, "chunk_index": chunk.chunk_index, }) return { "id": book.id, "title": book.title, "author": book.author, "publication_year": book.publication_year, "total_chunks": book.total_chunks, "chapters": [ { "title": ch_title, "passages": passages, } for ch_title, passages in chapters.items() ], } @router.delete("/books/{book_id}") async def remove_book(book_id: str, session: AsyncSession = Depends(get_db)): """Delete a book and all its chunks.""" from src.core.vector_store import delete_by_book_id book = await get_book_by_id(session, book_id) if not book: raise HTTPException(status_code=404, detail="Book not found") # Remove from vector store delete_by_book_id(book_id) # Remove from SQLite await delete_book(session, book_id) return {"message": f"Book '{book.title}' deleted successfully"} @router.get("/models", response_model=list[str]) async def available_models(): """List available LLM models.""" return list_available_models()