203 lines
7.6 KiB
Python
203 lines
7.6 KiB
Python
"""Admin routes for the EPUB search web UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from python.ebook_search.api.bm25_tasks import schedule_bm25_refresh
|
|
from python.ebook_search.api.dependencies import (
|
|
AppConfig, # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
|
)
|
|
from python.ebook_search.api.web import templates
|
|
from python.ebook_search.embeddings import embed_missing_chunks, embedding_model_stats
|
|
from python.ebook_search.ingest import ingest_configured_paths
|
|
from python.ebook_search.protected_phrases.lib import (
|
|
build_missing_protected_phrases,
|
|
generate_candidate_phrases_for_books,
|
|
judge_candidate_phrases_for_books,
|
|
)
|
|
from python.fastapi_tools import DbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/admin")
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
def admin(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Render the admin page."""
|
|
stats = embedding_model_stats(session)
|
|
logger.info("ebook_admin_page_loaded models=%s", len(stats))
|
|
return templates.TemplateResponse(request, "admin.html", {"config": config, "stats": stats})
|
|
|
|
|
|
@router.post("/scan", response_class=HTMLResponse)
|
|
def scan_library(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Scan configured library paths for EPUB changes."""
|
|
try:
|
|
count = ingest_configured_paths(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
logger.exception("ebook_admin_scan_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info("ebook_admin_scan_complete changed_files=%s", count)
|
|
if count > 0:
|
|
schedule_bm25_refresh(request.app)
|
|
return templates.TemplateResponse(request, "partials/admin_status.html", {"message": f"Indexed {count} EPUBs"})
|
|
|
|
|
|
@router.post("/build-phrases", response_class=HTMLResponse)
|
|
def build_phrases(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Build protected phrases for indexed books that are missing them."""
|
|
try:
|
|
result = build_missing_protected_phrases(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
session.rollback()
|
|
logger.exception("ebook_admin_build_phrases_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info(
|
|
"ebook_admin_build_phrases_complete books_seen=%s books_built=%s protected=%s mentions=%s",
|
|
result.books_seen,
|
|
result.books_built,
|
|
result.protected_phrases,
|
|
result.phrase_mentions,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{
|
|
"message": (
|
|
f"Built phrases for {result.books_built} of {result.books_seen} books; "
|
|
f"{result.protected_phrases} protected phrases, {result.phrase_mentions} mentions"
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/generate-ngrams", response_class=HTMLResponse)
|
|
def generate_ngrams(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Generate candidate n-grams for indexed books without LLM judging."""
|
|
try:
|
|
result = generate_candidate_phrases_for_books(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
session.rollback()
|
|
logger.exception("ebook_admin_generate_ngrams_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info(
|
|
"ebook_admin_generate_ngrams_complete books_seen=%s books_built=%s candidates=%s",
|
|
result.books_seen,
|
|
result.books_built,
|
|
result.candidate_phrases,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{
|
|
"message": (
|
|
f"Generated n-grams for {result.books_built} of {result.books_seen} books; "
|
|
f"{result.candidate_phrases} candidates stored"
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/judge-ngrams", response_class=HTMLResponse)
|
|
def judge_ngrams(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Judge stored candidate n-grams and promote accepted protected phrases."""
|
|
try:
|
|
result = judge_candidate_phrases_for_books(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
session.rollback()
|
|
logger.exception("ebook_admin_judge_ngrams_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info(
|
|
"ebook_admin_judge_ngrams_complete books_seen=%s books_judged=%s books_failed=%s candidates_judged=%s "
|
|
"protected=%s mentions=%s",
|
|
result.books_seen,
|
|
result.books_judged,
|
|
result.books_failed,
|
|
result.candidates_judged,
|
|
result.protected_phrases,
|
|
result.phrase_mentions,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{
|
|
"message": (
|
|
f"Judged {result.candidates_judged} candidates across {result.books_judged} of "
|
|
f"{result.books_seen} books; {result.protected_phrases} protected phrases, "
|
|
f"{result.phrase_mentions} mentions"
|
|
+ (f"; {result.books_failed} books failed" if result.books_failed else "")
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/embed-missing", response_class=HTMLResponse)
|
|
def embed_missing(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Embed chunks missing vectors for the configured model."""
|
|
try:
|
|
count = embed_missing_chunks(session, config)
|
|
session.commit()
|
|
except Exception as error:
|
|
logger.exception("ebook_admin_embed_missing_failed")
|
|
return templates.TemplateResponse(request, "partials/error.html", {"message": str(error)}, status_code=500)
|
|
|
|
logger.info("ebook_admin_embed_missing_complete chunks=%s", count)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{"message": f"Embedded {count} chunks"},
|
|
)
|
|
|
|
|
|
@router.post("/embed-all", response_class=HTMLResponse)
|
|
def embed_all(request: Request, config: AppConfig, session: DbSession) -> HTMLResponse:
|
|
"""Embed all chunks missing vectors in fixed-size batches."""
|
|
total = 0
|
|
batches = 0
|
|
try:
|
|
while True:
|
|
count = embed_missing_chunks(session, config)
|
|
if count == 0:
|
|
break
|
|
session.commit()
|
|
total += count
|
|
batches += 1
|
|
logger.info(
|
|
"ebook_admin_embed_all_batch_complete batch=%s chunks=%s total_chunks=%s",
|
|
batches,
|
|
count,
|
|
total,
|
|
)
|
|
except Exception as error:
|
|
logger.exception(
|
|
"ebook_admin_embed_all_failed batches=%s chunks=%s",
|
|
batches,
|
|
total,
|
|
)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/error.html",
|
|
{"message": f"Embed all failed after {total} chunks in {batches} batches: {error}"},
|
|
status_code=500,
|
|
)
|
|
|
|
logger.info("ebook_admin_embed_all_complete batches=%s chunks=%s", batches, total)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"partials/admin_status.html",
|
|
{"message": f"Embedded {total} chunks in {batches} batches of {config.embedding_batch_size}"},
|
|
)
|