| """API endpoints registered on the Gradio Server. |
| |
| Every endpoint uses ``@app.api(...)`` so it runs through Gradio's queue |
| (concurrency-managed, ZeroGPU-safe) and is reachable from ``gradio_client`` |
| and the frontend JS client. |
| |
| All endpoints are placeholders: they call the placeholder backend functions |
| and return canned data. No real parsing/embedding/RAG is wired yet. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from gradio import Server |
| from gradio.data_classes import FileData |
|
|
| from ..indexing.store import BACKEND |
| from ..parsing.directive import parse_directive |
| from ..parsing.lonstatistik import parse_lonstatistik |
| from ..rag import answer |
| from ..schemas import ( |
| ChatMessage, |
| Chunk, |
| IndexStatus, |
| ParseResult, |
| Profile, |
| ) |
|
|
|
|
| def register(app: Server) -> None: |
| """Register all backend endpoints on the given Gradio Server.""" |
|
|
| @app.api(name="parse_directive") |
| def parse_directive_endpoint(lang: str = "en") -> list[dict]: |
| """Parse Directive 2023/970 into article-level chunks (placeholder).""" |
| chunks: list[Chunk] = parse_directive(lang=lang) |
| return [c.model_dump() for c in chunks] |
|
|
| @app.api(name="parse_lonstatistik") |
| def parse_lonstatistik_endpoint( |
| file: FileData, source: str = "unknown" |
| ) -> dict: |
| """OCR/table-parse an uploaded lønstatistik PDF (placeholder).""" |
| result: ParseResult = parse_lonstatistik( |
| file_path=file["path"] if file else None, source=source |
| ) |
| return result.model_dump() |
|
|
| @app.api(name="index_documents") |
| def index_documents_endpoint(source: str = "directive") -> dict: |
| """Report the loaded vector index status. |
| |
| The index is built offline (``scripts/build_index.py``); this endpoint |
| reports how many chunks are loaded, not a live (re)build. |
| """ |
| from ..indexing.store import VectorStore |
|
|
| store = VectorStore() |
| persisted = store.load() |
| return IndexStatus( |
| source=source, |
| indexed_chunks=len(store), |
| backend=BACKEND, |
| persisted=persisted, |
| ).model_dump() |
|
|
| @app.api(name="warmup") |
| def warmup_endpoint() -> dict: |
| """Pre-load the chat models (embedder + LLM) on GPU. |
| |
| Fired by the frontend when onboarding starts, so the model is ready by |
| the time the user reaches the chat. Best-effort: returns a status dict and |
| never errors the UI. |
| """ |
| from ..rag import warmup |
|
|
| return warmup() |
|
|
| @app.api(name="chat") |
| def chat_endpoint(messages: list[dict], lang: str = "en", context: str = "") -> dict: |
| """Chat with the RAG assistant: retrieve grounded chunks, cite sources. |
| |
| Returns the full ``{reply, citations, disclaimer}`` in one response. |
| ``context`` is the user's compact salary-dashboard summary, injected so |
| the answer can be specific to their situation. |
| """ |
| msgs = [ChatMessage(**m) for m in messages] |
| return answer(msgs, lang=lang, context=context or None).model_dump() |
|
|
| @app.api(name="dashboard") |
| def dashboard_endpoint(profile: dict) -> dict: |
| """Build the profile-driven salary dashboard (structured IDA retrieval). |
| |
| Takes the onboarding answers (a ``Profile``) and returns the fused |
| ``Dashboard`` — per-axis comparison cells, reliability-gated, projected |
| to 2026, with the user's percentile and gap. CPU-only (no embedding). |
| """ |
| from ..fusion import build_dashboard |
|
|
| return build_dashboard(Profile(**profile)).model_dump() |
|
|