diff --git a/AGENTS.md b/AGENTS.md index 7a2d7f1..7c1a418 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,3 +183,15 @@ Runbook: `docs/quote-assistant.md`. Files: `src/lib/server/quote-assistant-{core - AI writes **never set `is_processed`**, never change quote status, never send, and never touch a line with a price, `user_edited` or `is_processed`. - `pricing_source "ai_pos"` must survive human saves: route every save through `nextPricingSourceOnSave` / `withAiReview`, or a stale page turns the line back to `manual`. - Manual live steps (like select values and pb_hooks): run `npm run pb:migrate:quote-assistant` against the live PB before deploying, then activate the n8n workflow "WFDASH Quote assistant tick" (every 5 min, POSTs the tick route). See the runbook. + +# AI Collection Confirm checks — reads driver photos, ticks lines, never confirms + +Runbook: `docs/collection-ai-checks.md`. Files: `src/lib/server/collection-ai-{read,pair,findings,service,prompt}.ts` +(vision read, pairing call, document-level findings, orchestration, default prompt) and `src/lib/collection-ai-ticks.ts` +(pure, client-safe tick logic). Routes: `POST /api/v1/collection-confirm/ai/tick` (n8n, agent auth), +`.../ai/check` and `.../add-to-pos` (browser session), MCP `collection_confirm.ai_check`. Settings live in Settings → AI → Collection checks. + +- **Manual first:** the Add to POS button and the Check with AI button are manual; the tick only reads and checks, nothing else. +- The AI writes **only** `ai_check`, `collection_doc_reads` rows, and an **empty** SKU when it is sure. It never overwrites a person's SKU, confirms a collection, or changes status. +- The service refuses to run until `ai_check` exists (PB drops unknown fields silently); `ai_check` is in `IGNORED_FIELDS` of the audit hook. +- Manual live steps (like select values and pb_hooks): run `npm run pb:migrate:collection-ai` against the live PB, redeploy `pocketbase/pb_hooks/audit_log_utils.js`, deploy, activate the n8n workflow "WFDASH Collection check tick" (every 5 min), then switch Collection checks on in Settings → AI. See the runbook. diff --git a/docs/collection-ai-checks.md b/docs/collection-ai-checks.md new file mode 100644 index 0000000..e97df03 --- /dev/null +++ b/docs/collection-ai-checks.md @@ -0,0 +1,112 @@ +# Collection Confirm AI checks runbook + +Design: `docs/superpowers/specs/2026-10-17-collection-confirm-ai-checks-design.md`. + +## What it does + +On Collection Confirm, drivers upload photos of the supplier's tax invoice +or delivery note. The AI (the provider set in Settings, AI) reads each +photo, pairs the document lines with our order lines, and shows four ticks +per line (SKU, quantity, description, price), plus a POS column and an +"AI findings" box when something at document level looks wrong. + +- It runs automatically (n8n clock, every 5 minutes) on files not yet read, + and on demand with the **Check with AI** button on a card. +- **Check with AI** needs only the global AI switch. The scheduled tick + needs both the global AI switch and "Collection checks enabled". +- A file is read once (by content hash, stored in `collection_doc_reads`). + A failed photo read is retried only by the **Check with AI** button, not + by the tick. +- Lines with no AI check look exactly as before, with no ticks. +- **Add to POS** is a manual feature (per line, or "Confirm and add to + POS" on the card). It creates a `stock_items` row with zero stock. + +What the AI writes: only `ai_check` on the order/stock line, the document +reads, and an **empty SKU**, and only when it is sure. It never overwrites +a SKU a person set, never confirms a collection, and never changes status. +The SKU fill is audited as an ordinary `sku` change. + +## Going live (manual steps, in this order) + +1. **Migrate PocketBase.** Run against the live PB: + `npm run pb:migrate:collection-ai` + (creates `collection_doc_reads`, adds `ai_check` to `order_items` and + `stock_purchase_items`, and the `collection_ai_*` fields on + `ai_settings`). +2. **Redeploy the audit hook** `pocketbase/pb_hooks/audit_log_utils.js` to + the PB server's `pb_hooks/`. It now ignores `ai_check`; until then the + only cost is noisy history (a large JSON diff on every re-run). +3. **Deploy** the app. Pushing to main does not auto-deploy; it is a + manual deploy that takes 5 to 10 minutes. +4. **Activate the n8n workflow** (below). Create it inactive, activate it + after steps 1 to 3. +5. **Turn it on** in Settings, AI: the global "AI enabled" switch must be + on, then switch on "Collection checks enabled" in the Collection checks + section and click Save. + +The tick does nothing until both switches are on, so steps 4 and 5 are +safe in either order. + +## n8n workflow: "WFDASH Collection check tick" + +n8n only acts as a clock; the app decides whether anything is due. + +| Node | Setting | +|---|---| +| Name | `WFDASH Collection check tick` | +| Trigger | Schedule Trigger, every 5 minutes | +| Action | HTTP Request | +| Method / URL | `POST` `https://wfdash.apphub.co.za/api/v1/collection-confirm/ai/tick` | +| Authentication | The `WFDASH Agent API Key` credential (`Authorization: Bearer `). Do not create a new secret. | +| Body | none | +| Options | Timeout **120000 ms** (120 s); Never Error (neverError) **on** | +| Status | **inactive** until the app is deployed and migrated | + +After deploy, Execute workflow once. Expected responses: +`{"ran":false,"reason":"disabled"}` while off, `{"ran":false,"reason":"running"}` +while an earlier run is still going, `{"ran":true,"summary":{...}}` when it +ran. A 401 means the credential is wrong. + +Each run is bounded: at most 10 files, and it stops starting new files +after 90 seconds, so it normally answers inside the 120 s timeout; +leftovers carry to the next run. Runs never overlap: a run holds a lease +for up to 10 minutes, and if a run dies without writing its summary the +next tick after that takes over. + +The last run (files read, cards checked) is shown in Settings, AI, +Collection checks, and stored in +`ai_settings.collection_ai_last_run_summary`. + +## Reading the ticks + +| Colour | Meaning | +|---|---| +| Green | The document agrees with our line (SKU matches after normalising, quantity equals what was collected and what was ordered, AI is sure it is the same item, unit price within 1 cent) | +| Amber | Close but worth a look: SKU differs from the document code; collected quantity matches the document but the ordered quantity differs; the AI thinks it is the same item but is only "likely" sure | +| Red | Mismatch or missing: no SKU on our line, quantity differs from the document, price differs or we have no cost, or the AI could not pair the line (all four red) | +| Grey | Not applicable: the document shows no code or quantity, or no price (delivery notes never have a price tick) | + +Hover a tick for the values compared (ours vs the document). A robot icon +marks a SKU the AI filled. Click the document chip on a card to see the +extracted lines next to the photo. + +## Errors + +- **"Collection checks are not set up yet: run `npm run pb:migrate:collection-ai` + against PocketBase first"** is the guard. The service refuses to run + until `ai_check` exists on the live PB, because PB silently drops + unknown fields and results would vanish. Run step 1 above. +- **"AI is disabled in settings"** from **Check with AI**: switch on the + global AI switch in Settings, AI. + +## Changing the prompt + +Settings, AI, Collection checks prompt. "Reset to default" loads the +built-in prompt; empty means the built-in default. Keep it to the +`submit_document_read` tool's fields. + +## Stopping it + +Switch off "Collection checks enabled" in Settings, AI and Save, or +deactivate the n8n workflow. Existing ticks stay until the card is +re-checked or the line is cleared. diff --git a/src/app/(app)/settings/ai/ai-settings-form.tsx b/src/app/(app)/settings/ai/ai-settings-form.tsx index c28ff7e..5619087 100644 --- a/src/app/(app)/settings/ai/ai-settings-form.tsx +++ b/src/app/(app)/settings/ai/ai-settings-form.tsx @@ -27,7 +27,9 @@ import { import { DEFAULT_QUOTES_AI_SYSTEM_PROMPT } from "@/lib/server/ai-system-prompt"; // Plain string export, no server-only imports, so it is safe in a client component. import { DEFAULT_QUOTE_ASSISTANT_PROMPT } from "@/lib/server/quote-assistant-prompt"; -import type { QuoteAssistantRunSummary } from "@/lib/types"; +// Plain string export, no server-only imports, so it is safe in a client component. +import { DEFAULT_COLLECTION_AI_PROMPT } from "@/lib/server/collection-ai-prompt"; +import type { CollectionAiRunSummary, QuoteAssistantRunSummary } from "@/lib/types"; export interface AiSettingsFormInitial { provider: string; @@ -50,6 +52,11 @@ export interface AiSettingsFormInitial { /** Empty string = use the built-in default prompt. */ quote_assistant_prompt: string; quote_assistant_last_run_summary: QuoteAssistantRunSummary | null; + /** Collection Confirm AI checks. */ + collection_ai_enabled: boolean; + /** Empty string = use the built-in default prompt. */ + collection_ai_prompt: string; + collection_ai_last_run_summary: CollectionAiRunSummary | null; } interface Props { @@ -97,6 +104,9 @@ export function AiSettingsForm({ initial }: Props) { const [qaConfidence, setQaConfidence] = useState(initial.quote_assistant_min_confidence); const [qaPrompt, setQaPrompt] = useState(initial.quote_assistant_prompt); const [showQaDefault, setShowQaDefault] = useState(false); + const [caEnabled, setCaEnabled] = useState(initial.collection_ai_enabled); + const [caPrompt, setCaPrompt] = useState(initial.collection_ai_prompt); + const [showCaDefault, setShowCaDefault] = useState(false); const [saveState, saveAction] = useActionState( saveAiSettingsAction, null, @@ -455,6 +465,92 @@ export function AiSettingsForm({ initial }: Props) { + + +
+ + Collection checks +
+ + Reads the driver's photos of supplier invoices and delivery notes + on Collection Confirm and ticks each line against the order. + +
+ +
+
+ +

+ Needs the AI switch above to be on too. Reads driver photos on + Collection Confirm and ticks each line; it never confirms anything. +

+
+ +
+ +
+
+ +
+ + +
+
+