WFDASH-WEBAPP/docs/order-assistant.md
Dave 85533245bf feat(orders): order assistant routes, MCP tool and runbook
Task 7 of the AI order assistant plan.

- POST /api/v1/orders/assistant/tick (n8n, assertAgentAuth)
- POST /api/v1/orders/{id}/assistant/run (browser session; 409 for
  "AI is disabled" and "not set up yet")
- MCP orders.assistant_match { orderId } -> matchOrder(force) with a
  TOOL_HINTS entry; deliberately NOT in READ_TOOLS
- docs/order-assistant.md runbook and the AGENTS.md section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:06:51 +02:00

178 lines
8.1 KiB
Markdown

# AI order assistant runbook
Design: `docs/superpowers/specs/2026-10-24-ai-order-assistant-design.md`.
Plan: `docs/superpowers/plans/2026-10-24-ai-order-assistant.md`.
## What it does
An order that came from a quote should point at that quote. Today only an
order created *from* a quote carries the link; every other order — a
walk-in, an email intake, a counter sale — has to be linked by hand.
The order assistant works out which quote an order came from. On a
schedule it looks at orders that have no quote yet, asks the AI which of
the candidate quotes the order came from, and then either:
- **links** the order to the quote itself (when it is sure), or
- **suggests** the quote, and a person accepts it.
The AI never touches prices, never changes a line's status, never creates a
purchase order and never sends anything. It only decides which quote an
order belongs to.
## The auto-link rule, in plain words
The assistant links an order to a quote **only** when all of these are
true:
- The **"Auto-link when sure"** setting is on (it is on by default).
- The AI says it is **sure** about the match.
- **Every** real order line is paired to a quote line, and each of those
pairs is a **sure** match.
- **Every** pair points at a line of the **one** chosen quote (not a
mixture of two quotes).
- The chosen quote belongs to the **same company** as the order.
If any of those fail, the assistant only writes a **suggestion**. The
order page then shows "AI suggests Q…" with the reason, the doubts and the
line-by-line pairing, and the existing **Link** button is there to accept
it.
The assistant also refuses to link when PocketBase says no — for example
when a line on the order has already moved past `pending`, or when
another order already points at that quote. A refusal is **not** an error:
the assistant falls back to a suggestion.
## Going live (manual steps, in this order)
1. **Migrate PocketBase.** Run against the live PB:
`npm run pb:migrate:order-assistant`
(adds `orders.ai_quote_match`, `order_items.ai_fill` and the
`order_assistant_*` fields on `ai_settings`). If the app is deployed
before this, PocketBase drops the unknown fields silently and the
assistant's result lands nowhere.
2. **Publish the PB hook change.** `ai_quote_match` and `ai_fill` were
added to `IGNORED_FIELDS` in `pocketbase/pb_hooks/audit_log_utils.js`,
so the AI's own writes do not flood the audit log. Publish it the same
way the other hook changes are published
(`npm run pb:hooks:publish`).
3. **Deploy** the app. Pushing to main does not auto-deploy; it is a
manual deploy that takes 5 to 10 minutes.
4. **Clean up the stale suggestions.** The old rule-based matcher wrote
soft suggestions that were not backed by a matching line (one quote
was suggested to 16 different orders). Run the clean-up script against
the live PB — **dry run first**:
```bash
npx tsx scripts/2026-10-24-rematch-quote-suggestions.ts
npx tsx scripts/2026-10-24-rematch-quote-suggestions.ts --apply
```
The dry run only reads. Read its report before running it with
`--apply`.
5. **Activate the n8n clock** (below).
6. **Turn it on** in Settings, AI: the global "AI enabled" switch must be
on, then switch on **"Order assistant enabled"** in the Order assistant
section and click Save. Defaults: every 10 minutes, auto-link on.
The tick does nothing until both switches are on, so steps 5 and 6 are
safe in either order.
## n8n workflow: "WFDASH Order assistant tick"
n8n only acts as a clock. The app decides whether a run is due (AI and
the assistant on, and at least "Run every" minutes since the last run), so
the setting in the app controls the real frequency.
Spec:
| Node | Setting |
|---|---|
| Name | `WFDASH Order assistant tick` |
| Trigger | Schedule Trigger, every 5 minutes |
| Action | HTTP Request |
| Method / URL | `POST` `https://wfdash.apphub.co.za/api/v1/orders/assistant/tick` |
| Authentication | Generic Credential Type, Header Auth, **the same credential** the quote assistant tick workflow uses (an `Authorization: Bearer <AGENT_API_KEY>` header). Do not create a new secret. |
| Body | none |
| Settings tab | Retry On Fail: **off**; Timeout: **120000 ms** (120 s) |
| Status | **inactive** until the app is deployed and migrated |
Click-by-click:
1. n8n, Workflows, Create workflow. Name it `WFDASH Order assistant tick`.
2. Add node, Schedule Trigger. Trigger Interval: Minutes, Minutes Between
Triggers: `5`.
3. Add node, HTTP Request, connect it after the trigger.
- Method: `POST`.
- URL: `https://wfdash.apphub.co.za/api/v1/orders/assistant/tick`.
- Authentication: Generic Credential Type, Generic Auth Type: Header
Auth. Pick the credential used by the quote assistant tick workflow
(open that workflow and read its HTTP Request node to see which one).
- Options, Add option, Timeout: `120000`.
- Settings tab on the node: turn **Retry On Fail** off.
4. Save. Leave the workflow **inactive** until the steps above are done.
5. After deploy, click Execute workflow once. Expected response while the
assistant is off: `{"ran":false,"reason":"disabled"}`. When on and not
yet due: `{"ran":false,"reason":"not_due"}`. While an earlier run is
still going: `{"ran":false,"reason":"running"}`. When it ran:
`{"ran":true,"summary":{...}}`.
6. Toggle the workflow Active.
A 401 means the credential is wrong (it must send the app's
`AGENT_API_KEY` as a Bearer token). A tick that takes long is normal when
there is work. Each run is bounded: at most **10 orders**, oldest first,
and it stops starting new orders once 90 seconds have passed, so a run
normally answers inside the 120 s timeout; leftovers carry to the next run.
Runs never overlap. A run stamps its start time before it does any work and
writes its summary when it ends. While a start is newer than the last
summary and less than 10 minutes old, other ticks answer
`{"ran":false,"reason":"running"}`. If a run dies without writing its
summary, the next tick after those 10 minutes takes over.
## Running it by hand
- **Order page** — the **"Match with AI"** button on the quote match card
runs the assistant for that one order and reloads the order.
- **MCP tool** — `orders.assistant_match` with `{"orderId": "..."}`.
Both force a run past the "already looked at these lines" rule.
## Reading `ai_quote_match`
`orders.ai_quote_match` (JSON) is the record of what the assistant decided
about an order. Read it in the PB admin.
| Field | Meaning |
|---|---|
| `status` | `matched` (the order was linked), `suggested`, `no_match` or `error` |
| `quote_id`, `quote_number` | The quote the assistant chose |
| `confidence` | `high`, `medium` or `low` |
| `reason`, `doubts` | The AI's plain-language reasoning and anything to check |
| `lines` | The pairing: order line id → quote line id, with `same_item` (`sure` / `likely` / `unsure`) |
| `auto_linked` | True only when the assistant itself created the link |
| `lines_hash` | Fingerprint of the order lines the run looked at — the same lines are not looked at twice |
| `model` | The model that answered |
| `run_at` | When the run happened |
| `error` | Set when `status` is `error` |
## Settings (Settings → AI → Order assistant)
| Setting | Default | Meaning |
|---|---|---|
| Order assistant enabled | off | The tick does nothing while this is off |
| Run every (minutes) | 10 | How often the tick may run |
| Auto-link when sure | on | May the assistant link the order itself? |
| Fill lines from the quote | on | Reserved for the next phase — the assistant does not fill lines yet |
| Prompt | empty | Overrides the built-in prompt when set |
`orders.ai_quote_match` and `order_items.ai_fill` are in `IGNORED_FIELDS`
of the audit hook, so the assistant's own writes do not flood the audit
log.
## Not in this phase
- The assistant does **not** fill order lines from the quote yet — that is
the next phase. The `order_items.ai_fill` field and the "Fill lines from
the quote" switch are already in place for it.
- The assistant does not touch prices, suppliers or SKUs.
- It never creates a purchase order, never sends an email and never
changes a line's status.