Files
app/docs/superpowers/specs/2026-06-08-ai-assistant-phase1-design.md
BOHA 4bf7bf4ed6 docs(ai): spec for AI assistant Phase 1 (chat + invoice import + budget)
Admin-only Claude (Sonnet 4.6) chat embedded on the dashboard under the
welcome text. Attach received-invoice PDFs → extract (vision + structured
output) → confirm/edit → save via the existing received-invoices endpoint.
$50/month admin-editable budget enforced via an ai_usage table + pre-call
guard. New @anthropic-ai/sdk dep, ANTHROPIC_API_KEY, ai.use permission;
one migration. No system-data tools (Phase 2). Ships as v2.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:09:47 +02:00

191 lines
9.6 KiB
Markdown

# Design — AI assistant, Phase 1 (chat + invoice import + budget cap)
**Date:** 2026-06-08
**Status:** Approved (brainstorming) — ready for implementation plan
**Phase 1 of 2.** Phase 2 (the AI querying system data via read-tools) is a separate later cycle — see "Deferred: Phase 2" at the end.
---
## Goal
Add the app's first AI capability: an **admin-only chat assistant embedded on the
dashboard** where you can talk to Claude and, crucially, **attach received-invoice
PDFs that the AI reads and imports into Přijaté faktury** (extract → you confirm →
save). All AI usage is bounded by an **admin-editable monthly budget (default
$50)** that hard-stops calls when exhausted.
## Decisions (locked in brainstorming)
- **Scope (Phase 1):** chat + invoice import + budget cap. **No system-data
access** (the AI cannot query your invoices/projects/attendance — that's Phase 2).
- **Placement:** a `DashAssistant` widget on the **dashboard, at the top under the
welcome text** — not a separate page, not a floating bubble.
- **Access:** **admins only**, via a new `ai.use` permission.
- **Model:** **Claude Sonnet 4.6** (strong vision, low cost).
- **Save flow:** extract → **you confirm/edit → save** (never silent auto-save).
- **Budget:** **$50/month**, resets each calendar month, **admin-editable in
Settings**; AI calls are **blocked** once the month's spend reaches the budget.
- **Chat:** **non-streaming** (request → full reply) and **in-browser history
only** (not persisted server-side) for v1 — both are easy later upgrades.
## Non-goals / out of scope (Phase 1)
- No system-data query tools / function-calling (Phase 2).
- No server-side conversation persistence, no streaming.
- No new invoice storage path — reuses `POST /received-invoices` verbatim.
---
## Architecture
Greenfield AI integration; the rest of the app is unaffected and works unchanged
if `ANTHROPIC_API_KEY` is absent (the assistant widget simply doesn't render and
the AI routes return a clear "not configured" error).
- **Dependency:** `@anthropic-ai/sdk`.
- **Env:** `ANTHROPIC_API_KEY` (required for the feature). Optional
`AI_MONTHLY_BUDGET_USD` default seed (the live value lives in the DB, below).
- **`src/services/ai.service.ts`** — wraps the SDK, owns the model choice + system
prompt + cost accounting. Two operations:
- `chat(messages)``client.messages.create({ model: "claude-sonnet-4-6", … })`,
non-streaming, returns `{ reply: string, usage }`.
- `extractInvoice(pdfBuffer, fileName)``client.messages.create` with the PDF as
a `document` content block + `output_config.format` JSON-schema for the
received-invoice fields, returns `{ fields, usage }`.
- Both call the spend tracker to record usage **and** are gated by the budget
guard (below) BEFORE the API call.
- Isolated so tests can stub the Anthropic call (no real API in CI).
## Spend cap (the $50/month budget)
- **New table `ai_usage`** (migration): `id`, `user_id?`, `kind` ("chat" |
"extract"), `model`, `input_tokens`, `output_tokens`, `cost_usd`
`@db.Decimal(10,6)`, `created_at @db.Timestamp(0)`, with an index on
`created_at`.
- **Cost map** (in `ai.service.ts`): model → `{ inputPerToken, outputPerToken }`.
Sonnet 4.6 = $3 / $15 per 1M → `0.000003` / `0.000015` per token. `cost_usd =
in*inP + out*outP`.
- **Budget value:** a new `ai_monthly_budget_usd Decimal @default(50)` column on
`company_settings` (the existing app-config singleton). Admin-editable in
Settings.
- **Budget guard `assertBudgetAvailable()`:** sum `cost_usd` for rows where
`created_at >= startOfMonth(now)`; if `>= ai_monthly_budget_usd`, return
`{ error: "Měsíční rozpočet AI byl vyčerpán", status: 402 }`. Called at the top
of every AI route BEFORE the Claude call.
- **Usage read:** `GET /api/admin/ai/usage` → `{ month_spend_usd, budget_usd,
remaining_usd }` for the chat's budget indicator and the Settings page.
## Backend routes — `src/routes/admin/ai.ts`
All guarded by `requirePermission("ai.use")`.
- `POST /api/admin/ai/chat` — body `{ messages: {role, content}[] }`. Budget guard →
`ai.service.chat` → log usage → `success(reply: string, remaining_usd)`.
- `POST /api/admin/ai/extract-invoices` — multipart PDF files (≤ `MAX_UPLOAD_SIZE`,
PDF/image only). Budget guard → `extractInvoice` per file → returns the extracted
fields per file (NOT saved) + usage. The files are **not** persisted here.
- `GET /api/admin/ai/usage` — current-month spend + budget + remaining.
If `ANTHROPIC_API_KEY` is unset, the POST routes return
`{ error: "AI není nakonfigurováno", status: 503 }`.
## Invoice import flow (extract → confirm → save)
1. Admin attaches one or more invoice PDFs in the dashboard chat (optionally with a
text note).
2. Frontend posts the files to `POST /ai/extract-invoices`; the AI returns the
extracted fields for each.
3. The chat renders an **editable review card per invoice** (dodavatel, číslo
faktury, částka, měna, sazba DPH, datum vystavení/splatnosti, poznámka),
pre-filled with the AI's values, plus a duplicate hint if `invoice_number` +
`supplier_name` already exists.
4. Admin glances/edits → **"Uložit"** (per card) or **"Uložit vše"**.
5. The frontend submits **the original File + the confirmed metadata** to the
**existing `POST /api/admin/received-invoices`** (multipart) — same NAS storage,
same server-side VAT recompute, same audit. The AI never writes invoices
directly. (The PDF is uploaded twice — once to extract, once to save — which is
fine for invoice-sized files and avoids any server-side temp-file handling.)
## Frontend
- **`src/admin/components/dashboard/DashAssistant.tsx`** — the chat widget:
message list, input box, attach-file button, and a small budget indicator
("Rozpočet: $X.XX / $50"). Non-streaming; conversation in component state only.
Renders the invoice review cards inline (reusing the received-invoice field
inputs / kit components).
- **`src/admin/pages/Dashboard.tsx`** — render `<DashAssistant />` at the top,
directly under the welcome text, **only when** the user is admin / has `ai.use`
(and only when AI is configured — a `GET /ai/usage` 503 hides it gracefully).
- **`src/admin/lib/queries/ai.ts`** — React Query options/mutations for chat,
extract, and usage. On a successful invoice save, invalidate `["received-invoices"]`.
- **Settings** — an admin field to set the monthly budget (writes
`company_settings.ai_monthly_budget_usd`).
## Permissions & migration
One Prisma migration:
- Create `ai_usage` table.
- Add `ai_monthly_budget_usd` to `company_settings` (default 50).
- Insert the `ai.use` permission and grant it to the **admin** role (explicit
`INSERT`s in `migration.sql`, per the project's migration policy).
(The admin role bypasses permission checks via `roleName === "admin"`, but the
explicit `ai.use` permission lets the access be widened later without code changes
and documents intent.)
## Security / privacy
- **Phase 1 has no tools**, so the AI cannot take actions on your data — its only
effect is the human-confirmed invoice save. A malicious/auto-generated invoice
PDF therefore can't trigger anything (the injection-to-action surface arrives in
Phase 2 and is designed there).
- Attached PDFs are sent to the Anthropic API to be read. The API key lives in
`.env` (server-side only), never exposed to the browser.
- Standard sanitization already applies on the received-invoice render/PDF paths;
AI-extracted text flows through the same validated `received_invoices` create.
## Testing
Server-side, real test DB, **Anthropic call stubbed** (no real API in CI):
- **Spend tracker:** `cost_usd` computed correctly from tokens for Sonnet 4.6;
monthly sum only counts the current month.
- **Budget guard:** under budget → allowed; at/over budget → `402` with the Czech
message; the guard reads the DB budget value.
- **Permissions:** all `/ai/*` routes return `403` without `ai.use`.
- **Not-configured:** with no `ANTHROPIC_API_KEY`, POST routes return `503`.
- **extract field-mapping:** given a stubbed AI response, the route returns the
expected `received_invoices` field shape (and computes nothing that the existing
create doesn't already).
Frontend has no component-test harness → gated by `tsc -b` + `build` + a manual
check (the dashboard widget renders for admins, hidden otherwise).
Gates: `npx tsc -b --noEmit`, `npm run build`, `npx vitest run`.
## Rollout
Ships as **v2.1.0** (notable new capability) via the standard process. **Requires a
migration**, so the dev server must be stopped for `prisma migrate dev` (ask first),
and the release runs `prisma migrate deploy` on prod. `ANTHROPIC_API_KEY` must be
set in the production `.env` before the feature works.
---
## Deferred: Phase 2 (system-data access) — captured
A later, separate spec + cycle. The AI gains **read-only tools** (function calling)
to answer questions about your data — e.g. "co je po splatnosti?", "kolik jsme
fakturovali v květnu?". Key design work for Phase 2:
- A curated set of **read tools** (search invoices, overdue list, project lookup,
attendance summary, …), each **scoped to the calling user's permissions** so the
AI never returns data the user can't see.
- The **agentic tool-loop** (Claude requests a tool → app executes against Prisma →
returns results → Claude continues), with a per-conversation tool-call cap to
bound cost (within the same monthly budget).
- **Prompt-injection hardening:** because the AI will both read untrusted invoice
content _and_ hold tools, tools stay **read-only**, results are treated as data,
and any state-changing action remains human-confirmed.