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