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

9.6 KiB

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