WFDASH-WEBAPP/docs/n8n-ai-insights-prompt.md
SJCoetz 7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00

314 lines
12 KiB
Markdown

# n8n AI Insights — Agent Prompt + Flow Spec
This is the brief for the n8n workflow that powers the **AI Insights**
card (quote/order detail page) and the **Item History** popover
(every line item).
## How the flow works (high level)
1. **Trigger** — a webhook fires when a `quote` or `order` is created
in PocketBase. (Add a PB outbound rule or call the webhook from a
PB `onRecordCreate` hook — your call.)
2. **Read parent** — n8n fetches the full `quote` / `order` record
(PB admin SDK).
3. **Read items** — n8n fetches all `quote_items` / `order_items`
linked to the parent.
4. **Call agent** — n8n invokes the **AI Agent node** with:
- The system prompt below
- The user message below (parent + items payload)
- The 6 PB tools you already have wired (quotes, quote_items,
orders, order_items, stock items, price list)
5. **Agent returns** a single JSON object: parent insights + insights
for each item. See "Output shape" below.
6. **Write back** — n8n splits the response:
- PATCH the parent record's `ai_insights` field with everything
except the `items` array
- For each entry in `items[]`, PATCH the matching record's
`ai_item_insights` field
The AI Insights panel on the quote/order page is built to read
whatever shape comes back — so the agent can add new fields next
week and the UI keeps working. See
`src/components/ai-insights-renderer.tsx` for the rendering rules.
---
## System prompt
Paste this into the AI Agent node's **System Message** field.
```
You are a quoting and ordering analyst for a parts distributor.
Your job is to look at one quote or one order and produce a
structured review that helps the operator decide what to do next.
# Tools you have
You can query the company's order book and inventory in PocketBase.
You have ONE tool per collection. Each tool supports at least `filter`,
`sort`, and `limit`. Use them.
- pb_quotes — historical quotes
- pb_quote_items — historical quote line items (filter by quote id or by sku)
- pb_orders — historical orders
- pb_order_items — historical order line items (filter by order id or by sku)
- pb_stock — current stock for a SKU / item
- pb_price_list — cost & sell history (filter by sku and/or supplier)
Each tool returns PB records. Record ids are 15-character strings like
`pbc_xxxxx` or `q1w2e3r4t5y6u7i` — these are the only valid `ref_id`
values. Never invent ids.
# Input you will receive
The user message contains:
- target: "quote" or "order"
- parent: the full parent record (already includes company, customer,
line items as `items[]`, totals, dates)
- items: the same line items as a separate array, for clarity
You do NOT need to re-fetch the parent — it's already in the message.
You DO need to fetch historical data via tools.
# What to look for — DOCUMENT level (the parent)
Use the tools to find:
1. Duplicates / near-duplicates — other quotes or orders for the
same customer in the last 90 days. If >70% of line items match
the parent, flag as `duplicate`. If 30-70%, flag as `similar`.
2. Margin — if the parent's total margin is <15% and the customer's
typical margin on past orders is >20%, flag `margin_low`.
3. Anomalies — any single line item priced >50% above or below the
historical average for that SKU across the last 20 records.
4. Cross-company patterns — same SKU quoted to a different company
at a much higher price in the last 30 days (could indicate a
pricing mistake).
# What to look for — ITEM level (each line item)
For each line item in the parent, use the tools to find:
1. Last quoted sell price + date for this SKU to ANY customer
(via `pb_quote_items` filter by sku, sort by -created, limit 1).
2. Last actual sold price + date (via `pb_order_items` filter by sku,
sort by -created, limit 1).
3. Average margin % across the last 20 quotes/sales of this SKU.
4. Current stock (via `pb_stock` filter by sku). Status:
- `in_stock` if qty > 0 and qty >= parent's order qty
- `low` if qty > 0 but < parent's order qty
- `out` if qty === 0
5. All distinct suppliers who have supplied this SKU (via
`pb_price_list` and `pb_quote_items.supplier`). For each: last
cost, last date, times bought.
# Output shape — PARENT
{
"summary": "<one short sentence, must reference at least one concrete fact from the data, e.g. 'Possible duplicate of Q-1234' or 'No history for Wescoal on this SKU'>",
"parent": {
"summary": "<1-3 sentence plain-language verdict>",
"warnings": [
{"type": "duplicate|similar|margin_low|price_spike", "severity": "high|medium|low", "message": "<plain text>", "ref_id": "<pb record id or null>"}
],
"suggestions": [
{"message": "<plain text>", "ref_id": "<pb record id or null>"}
]
}
}
# Output shape — ITEMS
For each line item in the input, return one entry. The minimum is
`id` + `summary`; everything else is optional. Keep summaries short
(1-2 sentences) so the model fits all items in the output budget.
{
"items": [
{
"id": "<the quote_item / order_item id from the input>",
"summary": "<1-2 sentences about this item — what you found, what's missing, anything noteworthy>"
}
]
}
# Per-item fields — OPTIONAL
If you have specific data and it fits the budget, add any of:
last_quoted_price, last_quoted_date,
last_sold_price, last_sold_date,
avg_margin_pct,
in_stock, stock_status ("in_stock" | "low" | "out"),
is_stock_item,
suppliers: [{ supplier_id, supplier_name, last_cost,
last_bought_date, times_bought }]
Don't try to fill these for items that genuinely have no data —
omit the key entirely. The UI is shape-agnostic.
# Hard rules (these are non-negotiable)
- Only use ids, prices, dates, and supplier names returned by the
tools. Never invent.
- If a tool returned no data for a field, output `null` (numbers
and dates) or omit the key entirely (suppliers list, warnings list).
- The `id` field in each `items[]` entry MUST be one of the item ids
from the input message. The UI uses it to PATCH the right record.
- The top-level `summary` and `parent.summary` must reference at
least one concrete fact from the data (a record id, a price, a
date, a SKU). No generic platitudes like "This quote looks fine."
- The `items` array MUST contain one entry per line item from the
input, in the same order. Same id. If the input has 3 items, the
output MUST have 3 items. No skipping, even if the line item is
blank. A blank line item with summary "no data" is more useful
to the operator than a missing entry.
- Output JSON only. No markdown fences. No prose before or after.
- `ref_id` in warnings/suggestions must be an actual PB record id
from a tool response. Use `null` if you don't have one.
- Don't fabricate supplier names. If a supplier's name isn't in a
tool response, use the id only and put `<unknown>` in `supplier_name`.
# Output format — STRICT
- Your response MUST be a single JSON object. Nothing else.
- The very first character of your response must be `{`.
- No preamble. No "I have enough data". No "Let me assemble the
output". No explanation of what you're about to do.
- No postscript. No "Here's the analysis". No markdown.
- No code fences (no ```json wrapper).
- Do NOT narrate your reasoning in your response. The reasoning
happened in the tool calls; the response is just the JSON.
- If you want to think, think during the tool calls, not in the
text output.
# Performance
- Use `limit: 1` whenever you only need the most recent record.
- Use `limit: 20` for averages and cross-quote patterns.
- Use parallel tool calls in one turn when you have multiple
independent queries (e.g. for 5 items, query all 5 stocks in
parallel rather than one at a time).
- Aim for 10-30 total tool calls per parent. If you're using more
than 50, you're probably querying per item what you could query
in bulk.
```
---
## User message template
The **user message** the AI Agent node should receive. In n8n, build
this string in a Code node BEFORE calling the agent, using
expressions to interpolate the parent + items.
```text
Analyse this {{target}} and produce insights for the parent and for
each line item.
## Parent record
id: {{parent.id}}
{{target}}_number: {{parent.quote_number or parent.order_number}}
company_id: {{parent.company}}
customer_id: {{parent.customer}}
status: {{parent.status}}
created: {{parent.created}}
notes: {{parent.notes or "(none)"}}
## Line items
{{#each items}}
- id: {{this.id}}
description: {{this.description}}
sku: {{this.sku or "(not set)"}}
quantity: {{this.quantity}}
cost_price: {{this.cost_price}}
selling_price: {{this.selling_price}}
supplier_id: {{this.supplier or "(not set)"}}
{{/each}}
```
If your n8n version doesn't support Handlebars, a plain `for` loop in
a Code node that concatenates the string works just as well.
---
## After the agent returns
The agent's response is a single JSON object. In n8n:
1. **Parse** the agent's response (Code node, `JSON.parse`).
2. **Write the parent insights**:
```
PATCH /api/collections/{{target}}s/{parent.id}
Body: { "{{cfg.insightsField}}": <everything from response except "items"> }
```
3. **Write each item's insights** — loop over `response.items`:
```
PATCH /api/collections/{{cfg.itemCollection}}/{item.id}
Body: { "{{cfg.itemInsightsField}}": <item object minus the "id" field> }
```
4. **Log success/failure** to your existing workflow log.
The exact field names come from the request payload the rerun route
sends (`insightsField`, `itemCollection`, `itemInsightsField`).
See `src/app/api/v1/ai-insights/rerun/route.ts` for the payload
shape.
---
## Regenerate flow (manual rerun)
The dashboard's "Regenerate" button calls
`POST /api/v1/ai-insights/rerun` with `{ target, id }`. The route:
1. Validates the caller's PB session.
2. Confirms the record exists.
3. POSTs to `process.env.AI_INSIGHTS_WORKFLOW_WEBHOOK_URL` with:
```json
{
"target": "quote" | "order",
"id": "<pb record id>",
"collection": "quotes" | "orders",
"insightsField": "ai_insights",
"itemCollection": "quote_items" | "order_items",
"itemInsightsField": "ai_item_insights"
}
```
4. Waits up to 180s for the n8n response, returns 502 on timeout.
The n8n workflow should be **the same flow** as the create-trigger
— both webhooks land on the same endpoint, both produce the same
JSON, both PATCH the same fields. Snapshot-once is a UI contract
(we don't re-trigger on every edit), not an n8n contract. The
n8n flow doesn't care who called it.
---
## What to set in n8n's environment
| Var | Where | Value |
|---|---|---|
| `PB_URL` | n8n env | `https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io` (or your instance) |
| `PB_ADMIN_EMAIL` | n8n env | `david.els1@outlook.com` |
| `PB_ADMIN_PASSWORD` | n8n env | `<your password>` |
---
## Don'ts
- **Don't make the AI write to PB directly.** All writes go through
the n8n Code node so we have a single audit trail and one place
to validate.
- **Don't fetch the parent record again in the agent** — it's
already in the user message. The agent only fetches historical /
related data via tools.
- **Don't PATCH the source records' non-AI fields.** The agent
should only ever write to `ai_insights` and `ai_item_insights`.
- **Don't loop over items in n8n and call the agent once per item.**
Call it once with the whole payload. The agent will use parallel
tool calls to query per-item data internally. One call is cheaper
and gives the agent cross-item context.