docs(quote-assistant): runbook accuracy, MCP tool listed in API docs
- 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>
This commit is contained in:
parent
6604e8b837
commit
3091ac94de
4 changed files with 37 additions and 18 deletions
|
|
@ -219,6 +219,7 @@ The `letterhead` body field accepts only `"CNL"` or `"ISHERWOOD"`. Anything else
|
|||
| `quotes.clone` | Clone a quote + items. |
|
||||
| `quotes.save_as_template`, `quotes.create_from_template` | Template management. |
|
||||
| `quotes.split`, `quotes.partial_approve`, `quotes.re_price`, `quotes.next_number`, `quotes.lookup` | Pricing workflow. **Note:** `quotes.lookup` is currently broken — see Known bugs §4. |
|
||||
| `quotes.assistant_fill` | Run the AI quote assistant on one quote (`quote_id`): fills unpriced lines from POS items as purple drafts a rep must review; unmatched lines get the closest POS items noted. Only status new / follow_up / claimed, never templates. Problems come back in `error`. Never marks lines done or sends anything. |
|
||||
| `quotes.items.rewrite` | **Not an MCP tool** — it's a REST endpoint at `/api/v1/quotes/items/rewrite` (browser-facing, needs PB session). |
|
||||
| `quote_items.populate_insights` | **Deterministic** compute-and-write of `quote_items.ai_item_insights`. Replaces the n8n AI rerun for the per-item popover. SKU-exact → SKU-normalised → description-fuzzy (Jaccard ≥ 0.6). See "Quote-item insights backfill" below. |
|
||||
|
||||
|
|
@ -355,7 +356,7 @@ REST mirror at `/api/v1/quote_items` (GET list + POST create) and `/api/v1/quote
|
|||
|
||||
**Why this exists:** the only pre-existing path for adding to the catalogue was operator uploads via `/pricelist` → n8n ingest → `raw_price_list` → AI extraction → `price_list`. That flow is async, human-in-the-loop, and gates on n8n being healthy. This tool gives you a synchronous, agent-driven shortcut for ad-hoc updates — paste a PDF, get the catalogue updated, no async hops.
|
||||
|
||||
### Full tool index (75)
|
||||
### Full tool index (77)
|
||||
|
||||
```
|
||||
orders.list, orders.get, orders.create_from_quote
|
||||
|
|
@ -366,7 +367,7 @@ quotes.create_from_email, quotes.append_reply, quotes.find_by_message_id,
|
|||
quotes.list, quotes.get, quotes.send_email, quotes.retry_email,
|
||||
quotes.clone, quotes.save_as_template, quotes.create_from_template,
|
||||
quotes.split, quotes.partial_approve, quotes.re_price, quotes.next_number,
|
||||
quotes.lookup
|
||||
quotes.lookup, quotes.assistant_fill
|
||||
quote_items.list, quote_items.get, quote_items.create, quote_items.update,
|
||||
quote_items.populate_insights
|
||||
attachments.upload_for_quote
|
||||
|
|
|
|||
|
|
@ -558,6 +558,7 @@ Grouped by resource family. The full description is in the table column; the des
|
|||
| `quotes.partial_approve` | Mark a subset of quote items as `approved` (or `rejected`) on a quote. Body: `{ quote_id, decisions: [{ item_id, decision }] }`. Useful when the customer accepts some lines but not others. |
|
||||
| `quotes.submit` | Flip a `new` quote to `approval_pending` once every line is priced. Idempotent — re-submitting an already-`approval_pending` or `partial_approval_pending` quote is a no-op (returns `statusChanged: false`). Gated: every line on the quote ... |
|
||||
| `quotes.re_price` | Re-run pricing on every quote_item under a quote. Whitelist of patchable fields is the same as `bulk_update_quote_items`. Returns the per-row result list. |
|
||||
| `quotes.assistant_fill` | Run the AI quote assistant on one quote now (`{ quote_id }`): fills unpriced lines from matching POS items as purple, unreviewed drafts and notes the closest items on unmatched lines. Only quotes with status new / follow_up / claimed, never templates; at most 15 lines per run. Problems come back in `error` (not thrown). Never marks lines done, changes the quote status or sends anything. Wraps `fillQuote` (same as `POST /api/v1/quotes/[id]/assistant/run`). |
|
||||
| `quotes.next_number` | Reserve the next quote number (`Q{yy}{mm}{NNN}`). Atomic counter; gaps are possible if the caller never persists the resulting draft. |
|
||||
| `quotes.lookup` | Free-text lookup over quotes for the order page's quote-picker dropdown. Substring match across quote_number, quote_reference, rfq_number, customer name, company name. Excludes templates by default. |
|
||||
| `quotes.list_attachments` | List all attachments on a quote (the generated PDF, original RFQ email attachments, operator-uploaded proofs, outbound attachments). Newest first. Returns up to 50 per page. |
|
||||
|
|
@ -746,6 +747,6 @@ Grouped by resource family. The full description is in the table column; the des
|
|||
|
||||
### Alphabetical index
|
||||
|
||||
`ai_insights.rerun`, `ai_settings.get`, `ai_settings.update`, `attachments.upload_for_quote`, `audit_log.get`, `bulk_update_quote_items`, `buyer_tasks.generate_po`, `buyer_tasks.list`, `buyer_tasks.mark_ready`, `collection_confirm.confirm_order_items`, `collection_confirm.list`, `collection_confirm.upload_invoice`, `collection_pool.list`, `companies.create`, `companies.delete`, `companies.get`, `companies.list`, `companies.update`, `customers.create`, `customers.delete`, `customers.get`, `customers.list`, `customers.update`, `dashboard.daily_summary`, `delivery_notes.cancel`, `delivery_notes.combine`, `delivery_notes.create`, `delivery_notes.create_quote`, `delivery_notes.list`, `delivery_notes.mark_complete`, `delivery_notes.mark_invoiced`, `delivery_notes.regenerate_pdf`, `delivery_notes.set_customer_po`, `delivery_notes.set_item_quantity`, `delivery_notes.set_order`, `drivers.list_stops`, `drivers.update_stop_status`, `files.fetch`, `order_items.confirm_collected`, `order_items.create`, `order_items.get`, `order_items.list`, `order_items.update`, `order_items.update_status`, `orders.add_attachment`, `orders.add_note`, `orders.archive`, `orders.cancel`, `orders.create`, `orders.create_from_quote`, `orders.daily_summary`, `orders.find_by_email_message_id`, `orders.find_by_order_number`, `orders.find_duplicates`, `orders.get`, `orders.list`, `orders.list_attachments`, `orders.mark_delivered`, `orders.mark_invoiced`, `orders.matched_attachments`, `orders.quote_match_sweep`, `orders.review`, `orders.review_save`, `orders.set_quote`, `orders.split`, `orders.sprint_counts`, `orders.stats`, `orders.update`, `price_list.add_items`, `purchase_orders.add_attachment`, `purchase_orders.diff`, `purchase_orders.get`, `purchase_orders.list`, `purchase_orders.record_diff_report`, `purchase_orders.record_send_outcome`, `purchase_orders.retry_email`, `purchase_orders.scan`, `purchase_orders.send_email`, `purchase_orders.stale`, `quote_items.create`, `quote_items.get`, `quote_items.list`, `quote_items.populate_insights`, `quote_items.update`, `quotes.append_reply`, `quotes.clone`, `quotes.create`, `quotes.create_from_email`, `quotes.create_from_template`, `quotes.find_by_message_id`, `quotes.get`, `quotes.get_pdf`, `quotes.ingest_from_email`, `quotes.list`, `quotes.list_attachments`, `quotes.lookup`, `quotes.next_number`, `quotes.partial_approve`, `quotes.process_pending`, `quotes.re_price`, `quotes.re_run_ai`, `quotes.retry_email`, `quotes.save_as_template`, `quotes.send_email`, `quotes.split`, `quotes.submit`, `quotes.update`, `sprint.attachment_counts`, `sprint.claim`, `sprint.item_attachments`, `sprint.list`, `stock.fulfill`, `stock.surplus_book`, `stock_items.adjust`, `stock_items.create`, `stock_items.get`, `stock_items.list`, `stock_items.set_pricing`, `stock_items.update`, `stock_purchase_items.assign_driver`, `stock_purchase_items.bulk_update`, `stock_purchase_items.list`, `stock_purchase_items.mark_collected`, `stock_purchase_items.update`, `stock_purchases.add_attachment`, `stock_purchases.create`, `stock_purchases.get`, `stock_purchases.list`, `stock_purchases.match_and_receive_from_invoice`, `stock_purchases.next_number`, `stock_purchases.receive`, `stock_purchases.retry_email`, `stock_purchases.send`, `stock_purchases.send_email`, `stock_quick_draft.add_line`, `stock_quick_draft.search`, `stock_quick_draft.suppliers`, `suppliers.create`, `suppliers.delete`, `suppliers.get`, `suppliers.list`, `suppliers.update`, `tools.discovery`, `users.lookup`
|
||||
`ai_insights.rerun`, `ai_settings.get`, `ai_settings.update`, `attachments.upload_for_quote`, `audit_log.get`, `bulk_update_quote_items`, `buyer_tasks.generate_po`, `buyer_tasks.list`, `buyer_tasks.mark_ready`, `collection_confirm.confirm_order_items`, `collection_confirm.list`, `collection_confirm.upload_invoice`, `collection_pool.list`, `companies.create`, `companies.delete`, `companies.get`, `companies.list`, `companies.update`, `customers.create`, `customers.delete`, `customers.get`, `customers.list`, `customers.update`, `dashboard.daily_summary`, `delivery_notes.cancel`, `delivery_notes.combine`, `delivery_notes.create`, `delivery_notes.create_quote`, `delivery_notes.list`, `delivery_notes.mark_complete`, `delivery_notes.mark_invoiced`, `delivery_notes.regenerate_pdf`, `delivery_notes.set_customer_po`, `delivery_notes.set_item_quantity`, `delivery_notes.set_order`, `drivers.list_stops`, `drivers.update_stop_status`, `files.fetch`, `order_items.confirm_collected`, `order_items.create`, `order_items.get`, `order_items.list`, `order_items.update`, `order_items.update_status`, `orders.add_attachment`, `orders.add_note`, `orders.archive`, `orders.cancel`, `orders.create`, `orders.create_from_quote`, `orders.daily_summary`, `orders.find_by_email_message_id`, `orders.find_by_order_number`, `orders.find_duplicates`, `orders.get`, `orders.list`, `orders.list_attachments`, `orders.mark_delivered`, `orders.mark_invoiced`, `orders.matched_attachments`, `orders.quote_match_sweep`, `orders.review`, `orders.review_save`, `orders.set_quote`, `orders.split`, `orders.sprint_counts`, `orders.stats`, `orders.update`, `price_list.add_items`, `purchase_orders.add_attachment`, `purchase_orders.diff`, `purchase_orders.get`, `purchase_orders.list`, `purchase_orders.record_diff_report`, `purchase_orders.record_send_outcome`, `purchase_orders.retry_email`, `purchase_orders.scan`, `purchase_orders.send_email`, `purchase_orders.stale`, `quote_items.create`, `quote_items.get`, `quote_items.list`, `quote_items.populate_insights`, `quote_items.update`, `quotes.append_reply`, `quotes.assistant_fill`, `quotes.clone`, `quotes.create`, `quotes.create_from_email`, `quotes.create_from_template`, `quotes.find_by_message_id`, `quotes.get`, `quotes.get_pdf`, `quotes.ingest_from_email`, `quotes.list`, `quotes.list_attachments`, `quotes.lookup`, `quotes.next_number`, `quotes.partial_approve`, `quotes.process_pending`, `quotes.re_price`, `quotes.re_run_ai`, `quotes.retry_email`, `quotes.save_as_template`, `quotes.send_email`, `quotes.split`, `quotes.submit`, `quotes.update`, `sprint.attachment_counts`, `sprint.claim`, `sprint.item_attachments`, `sprint.list`, `stock.fulfill`, `stock.surplus_book`, `stock_items.adjust`, `stock_items.create`, `stock_items.get`, `stock_items.list`, `stock_items.set_pricing`, `stock_items.update`, `stock_purchase_items.assign_driver`, `stock_purchase_items.bulk_update`, `stock_purchase_items.list`, `stock_purchase_items.mark_collected`, `stock_purchase_items.update`, `stock_purchases.add_attachment`, `stock_purchases.create`, `stock_purchases.get`, `stock_purchases.list`, `stock_purchases.match_and_receive_from_invoice`, `stock_purchases.next_number`, `stock_purchases.receive`, `stock_purchases.retry_email`, `stock_purchases.send`, `stock_purchases.send_email`, `stock_quick_draft.add_line`, `stock_quick_draft.search`, `stock_quick_draft.suppliers`, `suppliers.create`, `suppliers.delete`, `suppliers.get`, `suppliers.list`, `suppliers.update`, `tools.discovery`, `users.lookup`
|
||||
|
||||
<!-- END mcp-doc-gen -->
|
||||
|
|
@ -59,6 +59,7 @@ Legend: ✅ shipped · 🟡 partial · ❌ missing
|
|||
| Partial approve | detail page | `POST /quotes/[id]/partial-approve` | `quotes.partial_approve` | ✅ |
|
||||
| Review-approve | `/quotes/approvals` | `POST /quotes/[id]/review-approve` | ❌ | MCP gap (see §1.1) |
|
||||
| Re-price items | inline | `POST /quotes/[id]/re-price` | `quotes.re_price` | ✅ |
|
||||
| AI fill from POS | quote / Process page button | `POST /quotes/[id]/assistant/run` | `quotes.assistant_fill` | ✅ |
|
||||
| Split quote | inline | `POST /quotes/[id]/split` | `quotes.split` | ✅ |
|
||||
| Send email | email modal | `POST /quotes/[id]/send-email` | `quotes.send_email` | ✅ |
|
||||
| Retry email | email modal | `POST /quotes/[id]/retry-email` | `quotes.retry_email` | ✅ |
|
||||
|
|
@ -71,7 +72,7 @@ Legend: ✅ shipped · 🟡 partial · ❌ missing
|
|||
| Generate next quote # | form | `GET /quotes/next-number` | `quotes.next_number` | ✅ |
|
||||
| Archive quotes | `/quotes/archive` | `POST /quotes/archive` | ❌ | MCP gap (see §1.1) |
|
||||
|
||||
Result: 27 capabilities, 25 fully covered. **2 MCP gaps** (review-approve, archive).
|
||||
Result: 28 capabilities, 26 fully covered. **2 MCP gaps** (review-approve, archive).
|
||||
|
||||
### Quote items
|
||||
|
||||
|
|
|
|||
|
|
@ -12,24 +12,33 @@ 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.
|
||||
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 page has a "Let AI fill this quote" button for one quote now.
|
||||
MCP tool: `quotes.assistant_fill`.
|
||||
- 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 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`). Deploying before this makes saves of the AI settings
|
||||
fail.
|
||||
`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).
|
||||
|
|
@ -102,7 +111,7 @@ 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) |
|
||||
| `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 |
|
||||
|
|
@ -113,13 +122,20 @@ Read it in the PB admin, or through the MCP `quote_items.get`.
|
|||
| `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 |
|
||||
| `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".
|
||||
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,
|
||||
|
|
@ -137,11 +153,11 @@ 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).
|
||||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue