# 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.
