docs: the size-aware POS matcher and the exact/closest rule
The runbook and AGENTS.md described the old word-only shortlist and did not mention the shared matcher. Both now say how the size is used, that a price is written only for an exact match, and what ai_fill.closest holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2213bd36dd
commit
33813b4264
2 changed files with 22 additions and 1 deletions
|
|
@ -189,7 +189,14 @@ Runbook: `docs/quote-assistant.md`. Files: `src/lib/server/quote-assistant-{core
|
|||
(client-safe review/save helpers). Routes: `POST /api/v1/quotes/assistant/tick` (n8n, `assertAgentAuth`),
|
||||
`POST /api/v1/quotes/{id}/assistant/run` (browser), MCP `quotes.assistant_fill`. Settings live in Settings → AI.
|
||||
|
||||
The POS matcher is **shared** and **size-aware**: `src/lib/server/pos-match.ts`
|
||||
(the candidate pool, the size-aware ranking and `pickPosItems`) and
|
||||
`src/lib/item-size.ts` (the size parser) are used by the quote assistant,
|
||||
the order fill and the blue price check. `quote-assistant-shortlist.ts`
|
||||
delegates to it.
|
||||
|
||||
- A line is **pending** (purple) only when `ai_fill.status === "filled" && !ai_fill.reviewed_at && pricing_source === "ai_pos"` (`isAiFillPending`).
|
||||
- A price is written **only for an exact match**. A closest match writes no price and records `ai_fill.closest` (the nearest item + what differs); the badge then reads "Closest: …". The AI answers **exact**, **closest** or **none** per line.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ 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.
|
||||
|
||||
The shortlist is **size-aware**. The size is parsed out of the line
|
||||
(`src/lib/item-size.ts`) and the candidates are ranked by it, so
|
||||
"BOLT HEX M16X60 GR8.8 ZP" is offered the M16x60 bolt and not the M22x65
|
||||
that merely shares the words "BOLT 8.8 ZP". The AI then answers **exact**,
|
||||
**closest** or **none** per line:
|
||||
|
||||
- **exact** — the same product and every size attribute the line states
|
||||
matches. The price is written.
|
||||
- **closest** — the nearest item, but something differs. **No price is
|
||||
written**; the item and what differs are recorded in `ai_fill.closest`
|
||||
and the badge reads "Closest: …".
|
||||
- **none** — nothing in the catalogue is close enough. No price.
|
||||
|
||||
- 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
|
||||
|
|
@ -111,13 +124,14 @@ 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) |
|
||||
| `status` | `filled` (an **exact** match — the only status that writes a price), `no_match` (no exact match; `closest` may say what the nearest item was), `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) |
|
||||
| `closest` | The nearest POS item when the AI could only find a close one — `stock_item_id`, `name`, `sku`, `cost_price`, `selling_price` and `differs`. The line keeps **no** price. |
|
||||
| `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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue