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>
178 lines
8.1 KiB
Markdown
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.
|