- Runbook: pre-migration settings don't persist (not "saves fail"), eligible statuses, 15 lines per quote, audit attribution, clone/template behaviour, supplier kept, doubts dot, confidence guidance (Medium is the default; raise to High if reps often change AI prices). - quotes.assistant_fill added to docs/api-mcp-layer.md, docs/audit/api-mcp-coverage.md and the Giellie skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
8.4 KiB
Markdown
165 lines
8.4 KiB
Markdown
# AI quote assistant runbook
|
|
|
|
Design: `docs/superpowers/specs/2026-10-16-ai-quote-assistant-design.md`.
|
|
|
|
## What it does
|
|
|
|
On a schedule, the assistant looks at quote lines nobody has priced yet,
|
|
shortlists matching items from the POS catalogue (`stock_items`), asks the
|
|
AI (the provider set in Settings, AI) to pick the right one, and writes a
|
|
**draft** cost, selling price and supplier onto the line. A rep checks the
|
|
draft and saves as usual.
|
|
|
|
- Filled lines show purple with an agent logo on the quote and Process
|
|
pages. Clicking the logo shows the POS item, price source, reason and
|
|
doubts. A small warning-coloured dot on the logo means the AI noted
|
|
doubts to check.
|
|
- Lines it could not match stay unpriced and get a muted "AI: no match"
|
|
badge listing the closest POS items.
|
|
- The Quotes board shows "AI x/y" (unreviewed AI fills / total lines).
|
|
- The quote and Process pages have a "Let AI fill this quote" button for
|
|
one quote now. MCP tool: `quotes.assistant_fill`. On the Process page,
|
|
lines you have typed in but not saved are left as you typed them.
|
|
|
|
Only quotes with status **new**, **follow_up** or **claimed** are filled,
|
|
never templates. Up to 15 lines are sent per quote per run (one AI call);
|
|
the rest follow on the next run.
|
|
|
|
It never sets `is_processed`, never changes quote status, never sends
|
|
email, and never overwrites a line that already has a price, was edited by
|
|
a person, or is processed. Prices come only from the POS item. A supplier
|
|
already on the line is kept; the POS item's supplier is only written when
|
|
the line has none.
|
|
|
|
## Going live (manual steps, in this order)
|
|
|
|
1. **Migrate PocketBase.** Run against the live PB:
|
|
`npm run pb:migrate:quote-assistant`
|
|
(adds `quote_items.ai_fill` and the `quote_assistant_*` fields on
|
|
`ai_settings`). If the app is deployed before this, the Quote assistant
|
|
settings don't persist (the switch, interval, confidence and prompt
|
|
come back as defaults after Save), so the assistant stays off.
|
|
2. **Deploy** the app. Pushing to main does not auto-deploy; it is a
|
|
manual deploy that takes 5 to 10 minutes.
|
|
3. **Activate the n8n clock** (below).
|
|
4. **Turn it on** in Settings, AI: the global "AI enabled" switch must be
|
|
on, then switch on "Quote assistant enabled" in the Quote assistant
|
|
section and click Save. Defaults: every 10 minutes, minimum confidence
|
|
Medium, prompt empty (= built-in default).
|
|
|
|
The tick does nothing until both switches are on, so steps 3 and 4 are
|
|
safe in either order.
|
|
|
|
## n8n workflow: "WFDASH Quote 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 Quote assistant tick` |
|
|
| Trigger | Schedule Trigger, every 5 minutes |
|
|
| Action | HTTP Request |
|
|
| Method / URL | `POST` `https://wfdash.apphub.co.za/api/v1/quotes/assistant/tick` |
|
|
| Authentication | Generic Credential Type, Header Auth, **the same credential** the existing quotes `process-pending` 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 Quote 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/quotes/assistant/tick`.
|
|
- Authentication: Generic Credential Type, Generic Auth Type: Header
|
|
Auth. Pick the credential used by the quotes `process-pending`
|
|
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 steps 1 and 2 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 20 quotes and 60 lines, newest
|
|
first, 15 lines per quote (one AI call per quote). It also stops starting
|
|
new quotes 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.
|
|
|
|
## Reading `ai_fill`
|
|
|
|
`quote_items.ai_fill` (JSON) is the record of what the AI did to a line.
|
|
Read it in the PB admin, or through the MCP `quote_items.get`.
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `status` | `filled`, `no_match`, `no_candidates` (nothing in the POS shortlist, no AI call made) or `error` (the shortlist or AI call failed, the AI answer was cut off, or it skipped the line; retried after 1 hour or by the manual button) |
|
|
| `stock_item_id`, `stock_item_name`, `stock_item_sku` | The POS item picked. The POS SKU lives here only; it is never copied onto the line's own `sku`. |
|
|
| `cost_price`, `selling_price`, `markup`, `supplier_id` | What was written to the line, taken from the POS item |
|
|
| `price_from_default_markup` | Selling price came from the default markup because the POS item had a cost but no selling price |
|
|
| `confidence` | `high`, `medium` or `low` |
|
|
| `reason`, `doubts` | The AI's plain-language reasoning and any mismatches to check (grade, size, pack size, etc.) |
|
|
| `candidates` | Closest POS items looked at (for unmatched lines) |
|
|
| `description_snapshot` | The line description when tried. An unmatched line is only retried if the description changes. |
|
|
| `model`, `attempted_at` | Which model, when |
|
|
| `reviewed_at`, `reviewed_by` | Set when a rep saves the line |
|
|
| `accepted_unchanged` | On review: cost and selling price still equal what the AI wrote |
|
|
| `supplier_changed` | On review: the saved supplier differs from the POS item's supplier (the rep changed it, or the line already had another supplier) |
|
|
|
|
A line is **pending review** (purple) while
|
|
`ai_fill.status = "filled"`, no `reviewed_at`, and
|
|
`pricing_source = "ai_pos"`. The rep's save sets `reviewed_at`; the line
|
|
keeps `pricing_source = "ai_pos"` and a grey logo. Every AI write is in the
|
|
audit log under the actor "AI quote assistant", or "AI quote assistant (run
|
|
by <name>)" for the button (and "(run by MCP)" for the MCP tool). Those
|
|
writes are never credited to the person who clicked, even though the click
|
|
carries their login.
|
|
|
|
Clone and Save as template copy an unreviewed AI line without its cost,
|
|
selling price, markup or supplier, so the copy can't carry an unchecked
|
|
price.
|
|
|
|
The last run is shown in Settings, AI, Quote assistant, and stored in
|
|
`ai_settings.quote_assistant_last_run_summary` (quotes seen, lines filled,
|
|
no match, skipped, errors).
|
|
|
|
## Judging accuracy
|
|
|
|
Among reviewed AI fills (`ai_fill.status = "filled"` and `reviewed_at`
|
|
set), the share with `accepted_unchanged = true` is the headline number:
|
|
how often a rep saved the AI's price as it was. Also watch
|
|
`supplier_changed` (supplier was wrong even when the price was right).
|
|
|
|
To tally: fetch `quote_items` where `pricing_source = "ai_pos"` (for
|
|
example with `quote_items.list` or a short script), keep the ones whose
|
|
`ai_fill.reviewed_at` is set, and compare `accepted_unchanged` true versus
|
|
false.
|
|
|
|
Rough guide: the default minimum confidence is Medium. If reps often
|
|
change AI prices, raise it to High (only the AI's surest matches are
|
|
written) or edit the prompt (Settings, AI, Quote assistant prompt; "Reset
|
|
to default" restores the built-in one). If most reviewed fills are
|
|
unchanged at High, Medium is worth trying again.
|
|
|
|
## Stopping it
|
|
|
|
Switch off "Quote assistant enabled" in Settings, AI and Save, or
|
|
deactivate the n8n workflow. Existing purple lines stay until reviewed.
|