diff --git a/AGENTS.md b/AGENTS.md index d668d96..0d8bf6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,6 @@ Expected early triggers here: `security-review`, `database-migration`, `ai-eval` - **Tenant context is mandatory.** Every data access runs through a module repository inside `withTenant(companyId, …)`; `companyId` comes from the session, never from client input. No raw DB client outside `src/db` and `src/features/tenancy` (ADR-0001 D7). - **The AI service is stateless.** It never gets DB or storage credentials or tenant logic; the TS worker sends bytes and persists results (D8). pg-boss is the only queue (D4). - **"Found" needs proof.** A field is `found` only if the grounding verifier confirmed its quote in the cited segment; never relax this to make evals pass (D8). -- **Gemini free tier:** local development with synthetic data only – never in the showcase or with customer data (D8). +- **Gemini free tier:** local development with synthetic data only; on the showcase only as the temporary, invite-only exception of ADR-0001 D11 amendment 2026-09-26 (#67, exceptions register) – never with customer data (D8). - **Exactly-once export** relies on the idempotency key + unique export row + row lock – keep all three (D9). - Line endings: `.gitattributes` forces LF. Git Bash on Windows tolerates CRLF (tested 2026-09-22); LF keeps scripts portable to Linux shells (CI, WSL2, containers). diff --git a/docs/decisions/ADR-0001-pilot-architecture.md b/docs/decisions/ADR-0001-pilot-architecture.md index df683f2..271fc38 100644 --- a/docs/decisions/ADR-0001-pilot-architecture.md +++ b/docs/decisions/ADR-0001-pilot-architecture.md @@ -5,6 +5,8 @@ the drafted recommendation (full TypeScript); the draft recommendation is kept as alternative 1 in D8 - **Amendment 2026-09-24 (D11, also touches D3/D5):** the showcase uses Supabase Postgres + Supabase Storage instead of Neon + R2 – see "Amendment 2026-09-24" at the end of D11 +- **Amendment 2026-09-26 (D11, touches D8):** AI service as a container on Vercel and a Gemini API free-tier + key for the invite-only showcase as a temporary exception – see "Amendment 2026-09-26" at the end of D11 - **Deciders:** Fluory (orchestrator) · drafted by a Claude session - **Inputs:** `docs/input/2026-09-22-kundenanfrage.md` (customer request), `PROJECT-START.md` (discovery) - **Facts verified:** 2026-09-22 against official docs, registries and provider terms (sources at the end). @@ -652,6 +654,33 @@ enters the code: the app still talks plain PostgreSQL (Drizzle, pg-boss) and the project as Vertex `eu`, container image already exists, no 5 GB package or 300 s limit for docling/OCR). - **Runbook:** `docs/technical/deployment-vercel.md`. +### Amendment 2026-09-26 – AI service on Vercel and a Gemini API free-tier key (decided by Fluory, orchestrator; #67) + +**Decision.** +1. On the showcase the AI service runs as a **second Vercel project** that runs the existing service + image as a **container on Vercel Functions** (beta; framework `container`, root directory + `services/ai`, `Dockerfile.vercel`, `PORT=8080`, region `fra1`), PDF pipeline `textlines`, OCR off. + Spike result (#67, 2026-09-26): the Python-function route fails – the bundle is **1386 MB** against a + **500 MB** function limit (deployment `dpl_EhZt9SnyAgsH84ayhjXWicgqanpA`); the container image builds + in about 4 minutes (image size limit 15 GB). Cold start and one extraction: recorded in the PR of #67. +2. Model calls use a **Gemini API key on the free tier** (`AI_ALLOW_GEMINI_API_DEV=true`, + `GEMINI_API_KEY` set only in the Vercel project) – a temporary exception to D8. + +**Why.** No GCP account for now; one platform for both runtimes; the code path already exists and is +fail-closed (`AI_ALLOW_GEMINI_API_DEV` together with `VERTEX_PROJECT` refuses to start; no fallback). + +**Conditions of the exception.** The Gemini API terms (verified 2026-09-22) allow human review and +product-improvement use of free-tier content and state: *"You may use only Paid Services when making +API Clients available to users in the European Economic Area, Switzerland, or the UK."* There is no EU +data-residency guarantee. Therefore: invite-only demo accounts held by the orchestrator, synthetic data +only, demo banner on; the exception expires before anyone else gets an account, at the latest +2026-10-31 → paid tier (same key with billing) or Vertex `eu`. Recorded in the exceptions register. + +**Alternatives.** Google Cloud Run + Vertex `eu` (runbook recommendation until now; needs GCP billing) · +Hugging Face Space (existing Dockerfile unchanged; new account, hosting region not verified). + +**Revisit when** the container beta ends or changes its terms, the cold start makes the 60 s AI timeout fail regularly (→ Cloud Run), or the exception expires. + --- ## Summary of the challenged decisions @@ -713,6 +742,7 @@ explicit requirements. |---|---|---| | Showcase: no unattended retries (Hobby cron once/day) | Showcase only; production runs a worker | when a production-like demo is needed | | No RLS on the `auth`/`pgboss` schemas | Not tenant business data; only server code has access | on review at M3 | +| Gemini API free-tier key on the showcase (#67) | Invite-only for the orchestrator, synthetic data only, demo banner; Google's terms require paid services for API clients offered to EEA users (D11 amendment 2026-09-26) | before anyone else gets a demo account, at the latest 2026-10-31 | | Gemini free tier for local development | Synthetic data only; never in showcase or production | when the Vertex budget is set up for development | ## Open points diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index 90c9bb7..fcb3513 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -53,6 +53,7 @@ Deliberately accepted risks – without an entry here a deviation counts as a de | Better Auth admin plugin mounted without any holder of its admin role | ADR-0001 D6 names the plugin; decided in #30: kept – its `banned` field implements deactivation (sign-in blocked by the plugin). Nobody holds `platform-admin`, so `/api/auth/admin/*` rejects every caller (tested); user management runs through `identity` | Fluory | 2026-12-31 (review at M3) | | Upload cap per person only where configured | Local and CI run without `UPLOAD_MAX_PER_HOUR`; the showcase refuses to start without it (#59); concurrent uploads may pass the check together – a cost cap, not an exact quota | Fluory | 2026-12-31 (review at M3) | | `.msg` uploads checked by OLE signature only | Structure check of Outlook messages needs a CFB parser; files are served only as attachments with `nosniff` and parsed later by the stateless AI service | Fluory | with #23 (MSG parsing) | +| Gemini API free-tier key on the showcase (#67) | Orchestrator decision 2026-09-26 (ADR-0001 D11 amendment): no GCP for now. Google's terms require paid services for API clients offered to users in the EEA and allow human review of free-tier content, so the showcase stays invite-only for the orchestrator, synthetic data only, demo banner on | Fluory | before anyone else gets a demo account, at the latest 2026-10-31 – then paid tier (same key with billing) or Vertex `eu` | | Gemini API free tier for local development | Synthetic data only; never showcase or customer data (D8) | Fluory | when a Vertex development budget exists | ## Data flow diff --git a/docs/technical/deployment-vercel.md b/docs/technical/deployment-vercel.md index 821bbf6..21bb86e 100644 --- a/docs/technical/deployment-vercel.md +++ b/docs/technical/deployment-vercel.md @@ -8,7 +8,7 @@ | Blocker | Why | |---|---| -| AI service host chosen and running | The drain calls it; recommendation: **Google Cloud Run in the EU** (same GCP project as Vertex `eu`, existing image `services/ai`, no 300 s / package limits) | +| AI service running (§3) | The drain calls it. Showcase decision 2026-09-26 (ADR-0001 D11 amendment, #67): a second **Vercel** project with a Gemini API key (temporary exception). Fallback if the Vercel spike fails: Google Cloud Run in the EU with Vertex `eu` | What runs where: Vercel (Hobby) runs the Next.js app, the drain route and the ERP mock (`/api/erp-mock`). Supabase (`eu-central-1`) holds Postgres (incl. the pg-boss queue) and the files. @@ -42,11 +42,31 @@ process list. The last output lists `app_owner` and `app_rw` with `f | f | f` (no superuser, no RLS bypass, no role creation). The script is all-or-nothing; running it twice fails on "role already exists" – that is fine. -## 3. AI service - -Deploy `services/ai` (Cloud Run EU recommended) with Vertex `eu` credentials (a service account of the -GCP project, never an API key in the repo) and a random `AI_SERVICE_TOKEN` (≥ 24 chars). The Gemini -free tier is **not** allowed for the showcase (D8). Note its HTTPS URL. +## 3. AI service (second Vercel project, container) + +1. Import the same GitHub repository as a **second Vercel project**: framework **`container`**, Root + Directory **`services/ai`**. Vercel builds `services/ai/Dockerfile.vercel` (same build as + `Dockerfile`) and runs it on Vercel Functions (container images, beta). A plain Python function does + not fit: the bundle is 1386 MB against the 500 MB function limit (ADR-0001 D11 amendment 2026-09-26). +2. Deployment Protection: **Vercel Authentication for preview deployments only** – the web app calls the + production URL server-side; the API itself is protected by `AI_SERVICE_TOKEN`. +3. Variables (**Production**): + + | Variable | Value | + |---|---| + | `PORT` | `8080` – the image runs as a non-root user, which cannot bind Vercel's default port 80 | + | `AI_SERVICE_TOKEN` | random, ≥ 24 chars – the **same** value as `AI_SERVICE_TOKEN` in the web app | + | `AI_ALLOW_GEMINI_API_DEV` | `true` | + | `GEMINI_API_KEY` | the orchestrator's key – set it through `vercel env add GEMINI_API_KEY production --sensitive` with the value on stdin, never in a chat, the repo or an issue | + | `AI_PDF_PIPELINE` / `AI_PDF_OCR` | `textlines` / `off` – no model downloads on the showcase | + + `VERTEX_PROJECT` stays **unset**: together with `AI_ALLOW_GEMINI_API_DEV=true` the service refuses to start. +4. **Exception (ADR-0001 D11 amendment 2026-09-26, exceptions register):** the Gemini API free tier is only + allowed while the showcase is invite-only for the orchestrator with synthetic data. Before anyone else + gets a demo account (at the latest 2026-10-31): enable billing for the key (paid tier) or switch to + Vertex `eu` (`VERTEX_PROJECT`, service account, `AI_ALLOW_GEMINI_API_DEV` removed). +5. `GET /healthz` → 200. The production URL → `AI_SERVICE_URL` of the web app (§4). + Instances scale to zero after 5 minutes without traffic; the first call after that pays a cold start. ## 4. Vercel project diff --git a/services/ai/.vercelignore b/services/ai/.vercelignore new file mode 100644 index 0000000..7ae0a18 --- /dev/null +++ b/services/ai/.vercelignore @@ -0,0 +1,10 @@ +# Not needed at runtime on Vercel – keeps the upload small (dependencies come from pyproject/uv.lock). +# Anchored with a leading slash: `src/requestflow_ai/evals/` is runtime code and must stay. +/tests/ +/evals/ +/scripts/ +/Dockerfile +/.venv/ +__pycache__/ +.pytest_cache/ +.ruff_cache/ diff --git a/services/ai/Dockerfile.vercel b/services/ai/Dockerfile.vercel new file mode 100644 index 0000000..878837e --- /dev/null +++ b/services/ai/Dockerfile.vercel @@ -0,0 +1,31 @@ +# RequestFlow AI service on Vercel Functions as a container image (beta; ADR-0001 D11 amendment +# 2026-09-26). Same build as ./Dockerfile - keep both in sync. Differences: no optional model +# prefetch (the showcase runs AI_PDF_PIPELINE=textlines, AI_PDF_OCR=off) and the port comes from +# $PORT: Vercel's default is 80, which the non-root user cannot bind, so the project sets PORT=8080. +# The Python-function route failed: bundle 1386 MB > 500 MB limit (deployment dpl_EhZt9SnyAgsH84ayhjXWicgqanpA). +FROM python:3.13-slim + +COPY --from=ghcr.io/astral-sh/uv:0.8.17 /uv /usr/local/bin/uv + +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +WORKDIR /app + +COPY pyproject.toml uv.lock README.md ./ +RUN uv sync --frozen --no-dev --no-install-project +COPY src ./src +RUN uv sync --frozen --no-dev + +RUN useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin app +USER 10001 + +ENV PATH=/app/.venv/bin:$PATH \ + HF_HUB_OFFLINE=1 \ + PORT=8080 + +EXPOSE 8080 +CMD ["sh", "-c", "exec uvicorn requestflow_ai.api.app:create_app --factory --host 0.0.0.0 --port \"${PORT:-8080}\" --no-access-log --no-server-header"] diff --git a/services/ai/vercel.json b/services/ai/vercel.json new file mode 100644 index 0000000..2c12ed8 --- /dev/null +++ b/services/ai/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "regions": ["fra1"] +}