- templates/web/pricing.py: pricing table with source and as-of date; cost_for_call
(input rate + (output + thinking) at output rate, long-context above threshold;
unknown model raises naming the known models).
- Cost tab: coverage pill, total/this-month/tokens tiles, month-by-month table,
per-run tokens and cost ("not priced", never $0), by-hand panel with editable
assumptions labelled as such.
- Contract: RoiRun and GET /roi; optional GET /models and POST /runs/{id}/retry.
- SKILL.md: cost page wiring, model picker and retry-on-other-model as optional sections.
- Tests: test_pricing.py self-check, page_check_ai/test_ai_cost.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.1 KiB
4.1 KiB
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 }
summarycounts the run's rows per band. It isnulluntil the run isdone.scoreis the field's worst word score, ornullwhen it has none.corrected_valueisnulluntil a reviewer saves a value different fromvalue.created_atis ISO 8601 in UTC. Months on the cost page are UTC months.RoiRuntoken counts andtotal_cost(USD) arenullwhen 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.pyand nowhere else: green ≥0.95, amber 0.90–0.95, red <0.90. A field takes its worst word score. A blank mandatory field isblank, whatever its score. No scores isunscored. - 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
queuedorrunning, the page pollsGET /runs/{job_id}every 1.5s and shows "page X of Y". It stops when the run settles. - Cost comes from
templates/web/pricing.pyand 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 /roireturns 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.