- decideWithAi throws on stop_reason max_tokens instead of returning a partial answer; fillQuote marks a requested line the AI did not answer as error (retried after 1 h), never a permanent no_match. - QUOTE_MAX_LINES 30 -> 15; the tick also caps lines per quote so the 60-line budget reaches other quotes. - runAssistantTick stops starting quotes after a 90 s time budget and returns reason running while a newer start than the last summary is under 10 minutes old (lease). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.3 KiB
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.
- 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 page has a "Let AI fill this quote" button for one quote now.
MCP tool:
quotes.assistant_fill.
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.
Going live (manual steps, in this order)
- Migrate PocketBase. Run against the live PB:
npm run pb:migrate:quote-assistant(addsquote_items.ai_filland thequote_assistant_*fields onai_settings). Deploying before this makes saves of the AI settings fail. - Deploy the app. Pushing to main does not auto-deploy; it is a manual deploy that takes 5 to 10 minutes.
- Activate the n8n clock (below).
- 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:
- n8n, Workflows, Create workflow. Name it
WFDASH Quote assistant tick. - Add node, Schedule Trigger. Trigger Interval: Minutes, Minutes Between
Triggers:
5. - 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-pendingworkflow (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.
- Method:
- Save. Leave the workflow inactive until steps 1 and 2 above are done.
- 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":{...}}. - 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 AI call failed; 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 rep changed the 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".
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: if most reviewed fills are unchanged and the unchanged rate stays steady for the high-confidence group, consider lowering the minimum confidence to Medium or widening scope. If reps keep changing prices, raise the minimum confidence to High or edit the prompt (Settings, AI, Quote assistant prompt; "Reset to default" restores the built-in one).
Stopping it
Switch off "Quote assistant enabled" in Settings, AI and Save, or deactivate the n8n workflow. Existing purple lines stay until reviewed.