87 lines
4.9 KiB
Markdown
87 lines
4.9 KiB
Markdown
# Dashboard data contract (AI)
|
||
|
||
Everything in the no-AI contract (`../rpa-dashboard-no-ai/templates/contract.md`)
|
||
applies unchanged: the `{event, data}` envelope, its events, its actions and its rules.
|
||
The AI dashboard adds extraction runs, review rows and the approve gate. These shapes
|
||
come from the Superbill prototype, trimmed.
|
||
|
||
## Shapes
|
||
|
||
```
|
||
Run { job_id, document, status: pending|queued|running|done|failed, model,
|
||
pages_done, pages_total, summary: { green, amber, red, blank, unscored } | null,
|
||
approved, error }
|
||
ReviewRow { row_id, field, value, band: green|amber|red|blank|unscored,
|
||
score, reason, verified, corrected_value }
|
||
RoiRun { job_id, document, created_at, model, input_tokens, output_tokens,
|
||
thinking_tokens, total_cost }
|
||
```
|
||
|
||
- `summary` counts the run's rows per band. It is `null` until the run is `done`.
|
||
- `score` is the field's worst word score, or `null` when it has none.
|
||
- `corrected_value` is `null` until a reviewer saves a value different from `value`.
|
||
- `created_at` is ISO 8601 in UTC. Months on the cost page are UTC months.
|
||
- `RoiRun` token counts and `total_cost` (USD) are `null` when the run recorded no token
|
||
usage. The page shows it as "not priced" and counts it in the coverage pill, never as $0.
|
||
|
||
## Endpoints (web: `{API_BASE}` + path, JSON in and out)
|
||
|
||
| Method and path | What it does |
|
||
|---|---|
|
||
| `GET /runs` | `{runs: [Run]}`. The page opens the first one. |
|
||
| `GET /runs/{job_id}` | One `Run`. |
|
||
| `GET /runs/{job_id}/review` | `{rows: [ReviewRow]}`. Empty until the run is `done`. |
|
||
| `GET /runs/{job_id}/document` | The source document (PDF or image) shown beside the fields. |
|
||
| `PATCH /review/{row_id}` | Body `{corrected_value?, verified}`. Patches one row, returns it. |
|
||
| `POST /runs/{job_id}/verify-remaining` | Verifies every flagged row not yet verified. Returns `{verified: n}`. |
|
||
| `GET /roi` | **Fetch ROI.** `{runs: [RoiRun], pricing: {source, as_of}}`, every run, newest first. The page sums spend, groups months and computes the by-hand panel itself. |
|
||
| `POST /runs/{job_id}/approve` | **The approve gate.** `409 {detail}` while the run isn't done or any flagged row is unverified, else `200 {ok: true}`. |
|
||
|
||
Optional, only when the user chose those sections:
|
||
|
||
| Method and path | What it does |
|
||
|---|---|
|
||
| `GET /models` | `{models: [name]}`: the keys of the pricing table, so only priced models can be picked. |
|
||
| `POST /runs/{job_id}/retry` | Body `{model}`. Re-queues a `failed` run on that model, returns the `Run`. |
|
||
|
||
Errors answer with a non-2xx status and `{detail: "<why>"}`. The page shows `detail`.
|
||
|
||
## Rules
|
||
|
||
- **Bands** come from `templates/web/bands.py` and nowhere else: green ≥0.95, amber
|
||
0.90–0.95, red <0.90. A field takes its worst word score. A blank mandatory field
|
||
is `blank`, whatever its score. No scores is `unscored`.
|
||
- **Flagged** = every band but green. Each flagged row must be verified before approval.
|
||
- **The server enforces the gate.** The page disables nothing that matters for safety:
|
||
it asks, and shows the server's refusal.
|
||
- **Drafts:** the page sends nothing while a reviewer types. Save sends one PATCH per
|
||
edited row (with `verified: true`). Leaving with unsaved edits asks first.
|
||
- **Polling:** while a run is `queued` or `running`, the page polls `GET /runs/{job_id}`
|
||
every 1.5s and shows "page X of Y". It stops when the run settles.
|
||
- **Cost** comes from `templates/web/pricing.py` and nowhere else: per million tokens,
|
||
input × input rate + (output + thinking) × output rate, long-context rates above the
|
||
model's threshold. An unknown model is an error naming the known models, never $0.
|
||
The table records its source URL and as-of date; `GET /roi` returns both.
|
||
- **Assumptions** (minutes per document, hourly rate) live only on the page, typed by the
|
||
reader and labelled as assumptions. The backend never stores or guesses them.
|
||
- **Caveat:** the legend always shows that colour reflects print clarity, not
|
||
correctness.
|
||
|
||
`templates/web/server_stub.py` implements all of this on sample data, standard library
|
||
only. A real backend serves the same endpoints with the same rules.
|
||
|
||
## Desktop (PyQt, no web server)
|
||
|
||
The same endpoints and rules travel over the Qt bridge instead of HTTP:
|
||
|
||
- Page to Python: one slot, `ai_request(id, method, path, body)`. `path` is the endpoint
|
||
path above without `{API_BASE}`, `body` is a JSON string (`"{}"` when there is none).
|
||
- Python to page: one envelope, `{event: "ai_response", data: {id, status, body}}`, with the
|
||
same `status` and JSON `body` the web endpoint would answer.
|
||
- `GET /runs/{job_id}/document` answers an SVG string or a URL the page can load (for
|
||
example a `file:///` path to the PDF).
|
||
|
||
`templates/desktop/ai_bridge.py` adds that slot to the no-AI `Bridge`. It hands each
|
||
request to the review backend's `dispatch(method, path, body)` (`server_stub.py` until the
|
||
project has its own), so the approve gate is enforced in Python, never in the page.
|