"""On-demand actions and export endpoints. These let the frontend trigger individual functions (re-extract with a chosen schema, re-summarize) and download results — independent of the chat agent. """ from __future__ import annotations from fastapi import APIRouter, HTTPException, Query from fastapi.responses import Response from app.core.logging import get_logger from app.schemas.documents import Classification, ExtractionResult from app.schemas.templates import all_template_names from app.services import ( anomaly, classification, export, extraction, storage, summary, vectorstore, ) log = get_logger(__name__) router = APIRouter(prefix="/api/documents", tags=["actions"]) @router.post("/{doc_id}/extract", response_model=ExtractionResult) async def re_extract(doc_id: str, schema: str | None = Query(None), provider: str | None = Query(None)): detail = storage.get(doc_id) if not detail: raise HTTPException(404, "Document not found") chunks = await vectorstore.query(doc_id, "key fields", k=40) doc_type = detail.classification.doc_type if detail.classification else None # `schema` is an OPTIONAL user-chosen framing; "auto" (or unset) keeps the # extractor fully adaptive. hint = schema if (schema and schema.lower() != "auto") else None result = await extraction.extract(detail.markdown or "", doc_type, chunks, provider, schema_hint=hint) detail.extraction = result detail.anomalies = await anomaly.detect(detail.markdown or "", result, doc_type, provider) storage.save(detail) return result @router.post("/{doc_id}/classify", response_model=Classification) async def re_classify(doc_id: str, provider: str | None = Query(None)): detail = storage.get(doc_id) if not detail: raise HTTPException(404, "Document not found") c = await classification.classify(detail.markdown or "", provider) detail.classification = c storage.save(detail) return c @router.post("/{doc_id}/summarize") async def re_summarize(doc_id: str, provider: str | None = Query(None)): detail = storage.get(doc_id) if not detail: raise HTTPException(404, "Document not found") s = await summary.summarize(detail.markdown or "", provider) detail.summary = s storage.save(detail) return {"summary": s} @router.get("/{doc_id}/export") async def export_document(doc_id: str, format: str = Query("json")): detail = storage.get(doc_id) if not detail: raise HTTPException(404, "Document not found") fmt = format.lower() if fmt not in export.EXPORTERS: raise HTTPException(400, f"Unsupported format. Use one of {list(export.EXPORTERS)}") fn, media, ext = export.EXPORTERS[fmt] data = fn(detail) safe = (detail.filename or doc_id).rsplit(".", 1)[0] return Response( content=data, media_type=media, headers={"Content-Disposition": f'attachment; filename="{safe}.{ext}"'}, ) @router.get("/-/schemas") async def list_schemas(): return {"schemas": all_template_names()}