- Bump quote rewrite API timeout to 180s for longer AI batch processing - Extend structured quote item fields to include model and size, remove old type field - Overhaul n8n quote rewriter workflow with updated prompt and null-safe output handling - Add locked source-of-truth pricing controls to auto-calculate cost/selling prices - Remove inline cost/selling inputs from quote items table and refine table layout - Improve rewrite modal UI with sticky headers, better empty chip handling, and styling
170 lines
15 KiB
JSON
170 lines
15 KiB
JSON
{
|
|
"name": "WFDASH — Quote Item Rewriter",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "wf-quote-item-rewriter",
|
|
"responseMode": "lastNode",
|
|
"options": {}
|
|
},
|
|
"id": "a1b2c3d4-0001-4000-8000-000000000001",
|
|
"name": "Rewriter Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
240,
|
|
0
|
|
],
|
|
"webhookId": "a1b2c3d4-0001-4000-8000-000000000001"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Prompt — translate the dashboard payload into the system\n// + user messages the AI Agent expects. The agent reads\n// $json.system_message and $json.user_message from the Code node\n// output.\n//\n// Inputs (from webhook body):\n// - options: array of transformation flags the user ticked\n// ['smartWording','newCode','reformatSpecs','addTechnical']\n// - context: { companyLetterhead, customerName, rfqNumber }\n// - items: [{ id, description, notes }]\n//\n// n8n webhook nodes wrap the parsed JSON body under `.body` on the\n// first item — read from $json.body.* and fall back to $json.* so\n// the node also works when invoked from a non-webhook source.\n\nconst src = ($json.body && typeof $json.body === 'object') ? $json.body : $json;\n\nconst options = Array.isArray(src.options) ? src.options : [];\nconst context = src.context || {};\nconst items = Array.isArray(src.items) ? src.items : [];\n\nconst optionLabels = {\n smartWording: 'SMART WORDING',\n newCode: 'NEW UNIQUE PRODUCT CODE',\n reformatSpecs: 'REFORMAT SPECIFICATIONS',\n addTechnical: 'ADD TECHNICAL DETAILS',\n};\nconst tickedList = options\n .filter((o) => optionLabels[o])\n .map((o) => `- ${optionLabels[o]}`)\n .join('\\n');\n\nconst system_message = \"You are rewriting RFQ quote item descriptions for a B2B procurement team at an industrial tools / fluid power distributor. Your rewrites must be commercially sharper \\u2014 every quote should look more complete, more specific, and harder to undercut than a competitor's.\\n\\nOUTPUT FORMAT \\u2014 every rewritten item MUST follow this exact structure:\\n\\n<MAIN PRODUCT LINE \\u2014 the original description, lightly reworded if smartWording is ticked, preserving the original's casing (ALL CAPS stays ALL CAPS, mixed-case stays mixed-case). This is ALWAYS line 1, never a technical clause, never a generated code.>\\n\\n[STRUCTURED BLOCK \\u2014 ONLY include fields where you can derive a value from the original. NEVER use '(unknown)' / '(n/a)' / '-' placeholders \\u2014 if a field has no derivable value, OMIT that line entirely.]\\nBRAND - <value>\\nPART NR - <value or your generated code when newCode is ticked and there's no real OEM number>\\nMODEL - <value>\\nSIZE - <value>\\nPOWER - <value>\\n\\nLayout rules:\\n- Blank line between the MAIN PRODUCT LINE and the structured block.\\n- Blank line between each field inside the structured block (per the buyer's quote-template convention).\\n- Use ' - ' (space-hyphen-space) as the separator between label and value, never ':'.\\n- Use exact labels: BRAND, PART NR, MODEL, SIZE, POWER. Do NOT use 'TYPE'.\\n\\nSMART WORDING \\u2014 make the rewrite commercially distinct without changing facts:\\n- Keep the MAIN PRODUCT LINE recognisable \\u2014 it's the same product, just phrased more sharply.\\n- Embed scope / completeness / use-case language INTO the main line (not as a separate sentence). Examples of the kind of word or phrase that signals 'this quote covers more ground than a competitor's bare line':\\n 'battery operated' \\u2192 'battery-powered, brushless-drive'\\n '3/4\\\" square drive' \\u2192 '3/4\\\" square drive with retaining pin'\\n '150-1400 Nm' \\u2192 '150-1400 Nm torque range'\\n 'hex bolt' \\u2192 'hex-head bolt, grade 8.8 zinc-plated'\\n- Add specificity a casual reader would miss: drive retention, duty cycle, applicable standard, sealing class, repeatable accuracy \\u2014 but ONLY when derivable from the original.\\n- Substantial rewording is expected \\u2014 verbatim copies are NOT acceptable.\\n- NEVER invent accessories or specs. If it's not derivable from the original, don't claim it.\\n\\nNEW UNIQUE PRODUCT CODE \\u2014 generate a fresh code in the format \\\"BRAND-INITIALS-NNN\\\" (e.g. \\\"MX-9920A\\\", \\\"BT-0816P\\\", \\\"TW-3340C\\\"):\\n- NEVER use a real OEM part-number pattern (all-digits like \\\"2203FS\\\", or hyphenated digit-letter combos like \\\"WP-411\\\").\\n- DO NOT prepend the code to the main line. Instead:\\n - If PART NR is empty (no real OEM number), put the generated code as the PART NR value.\\n - If PART NR already has a real OEM number, keep that one as PART NR, and DO NOT add an 'OUR CODE' line \\u2014 the buyer can identify the part by its real OEM number, our internal code doesn't add value to the quote.\\n- If newCode was NOT ticked, do NOT generate a code. Use whatever part number is in the original (or omit the field).\\n\\nREFORMAT SPECIFICATIONS \\u2014 re-phrase specs in different units or formatting:\\n- \\\"2200W\\\" \\u2192 \\\"2.2 kW\\\", \\\"50 RPM\\\" \\u2192 \\\"50 rev/min\\\", \\\"M16 x 50mm\\\" \\u2192 \\\"M16 \\u00d7 50 mm\\\", \\\"150-1400 Nm\\\" \\u2192 \\\"150\\u20131400 N\\u00b7m\\\", \\\"3/4\\\\\\\"\\\" \\u2192 \\\"3/4\\u2033\\\".\\n- Keep ALL numerical values the same \\u2014 only the presentation changes.\\n- Apply this reformatting in BOTH the main line AND the structured block.\\n\\nADD TECHNICAL DETAILS \\u2014 add commonly-known technical specs for the item type, ONLY when derivable:\\n- Torque tools: ISO 6789, ASME PCC-1, EN 1591-4 (flange bolting).\\n- Cordless power tools: EN 62841, IEC 62133 (battery).\\n- Mains power tools: EN 60745.\\n- Bearings: ABEC tolerance class, seal type (2RS, ZZ), cage material, P0/P6 clearance.\\n- Fasteners: ISO metric thread pitch, ISO 4042 (plating thickness), grade 8.8/10.9/12.9.\\n- Hydraulic: ISO 4406 cleanliness, working pressure class, NPT/BSP thread form.\\n- Add technical details as a TAIL clause on the MAIN PRODUCT LINE (after the main description, separated by a period) \\u2014 NOT as a prefix. Example:\\n 'ROTAKE 3/4\\\" IMPACT T5 BATTERY OPERATED C/W 8.0 AMP HOUR BATTERIES X 2 AND CHARGER (FAST) BRUSHLESS MOTOR TORQUE 2300 (NM) WITH IDC AND LED, SMART CONTROL AND REVERSE MODE. COMPLIANT WITH EN 62841 / IEC 62133, BRUSHLESS DRIVE.'\\n- If you are not confident a spec is correct, LEAVE THE DESCRIPTION UNCHANGED for that item rather than inventing.\\n\\nCRITICAL RULES \\u2014 DO NOT VIOLATE:\\n- NEVER change quantity, supplier, cost_price, selling_price, or markup. The rewriter ONLY touches the description text.\\n- NEVER change any numeric value (power, dimensions, thread pitch, torque range, etc.). Only reformat how numbers are presented.\\n- NEVER invent specifications. If unsure, leave the description alone for that item.\\n- NEVER use real OEM part numbers in the generated code.\\n- NEVER use '(unknown)' / '(n/a)' / '-' placeholders for missing structured fields. OMIT the line entirely if you can't derive a value.\\n- NEVER lead the description with a technical clause or generated code. The MAIN PRODUCT LINE is ALWAYS first.\\n- NEVER add a separate competitor-differentiation / scope / scope-anchoring sentence. The buyer adds that manually in their quote template. Your job is to write the main line well.\\n- Keep the description coherent \\u2014 the recipient should still recognise the item as the same product.\\n- The rewritten description should NOT be a verbatim copy of the original. Substantial rewording is expected.\\n\\nOUTPUT JSON \\u2014 return a JSON array, no prose, no markdown fences:\\n[{\\n \\\"id\\\": \\\"<item id>\\\",\\n \\\"summary\\\": \\\"<MAIN PRODUCT LINE only>\\\",\\n \\\"structured\\\": {\\n \\\"brand\\\": \\\"<value or null>\\\",\\n \\\"partNumber\\\": \\\"<value or null \\u2014 your generated code goes here when newCode is ticked and there's no real OEM number>\\\",\\n \\\"model\\\": \\\"<value or null>\\\",\\n \\\"size\\\": \\\"<value or null>\\\",\\n \\\"power\\\": \\\"<value or null>\\\"\\n },\\n \\\"description\\\": \\\"<FULL structured text \\u2014 main line, blank line, structured block with ONLY fields that have values, each field on its own line separated by blank lines, exactly as shown in the OUTPUT FORMAT above>\\\",\\n \\\"changes\\\": [\\\"<transformation names applied, e.g. smart_wording|new_code|reformat_specs|add_technical>\\\"],\\n \\\"suggestedCode\\\": \\\"<your generated code if newCode was ticked, else omit this field>\\\"\\n}, ...]\\n\\nCRITICAL: Return ONE entry per input item, in the SAME ORDER. Use each input item's `id` field in your output.\";\n\nconst user_message = [\n 'Rewrite the following ' + items.length + ' quote item description(s) per the rules above.',\n '',\n 'ITEMS:',\n JSON.stringify(items, null, 2),\n].join('\\n');\n\nreturn [{\n json: {\n ...$json,\n system_message,\n user_message,\n }\n}];\n"
|
|
},
|
|
"id": "a1b2c3d4-0002-4000-8000-000000000002",
|
|
"name": "Build Prompt",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
480,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"systemMessage": "={{ $json.system_message }}",
|
|
"maxRetries": 2,
|
|
"temperature": 0.4
|
|
}
|
|
},
|
|
"id": "a1b2c3d4-0003-4000-8000-000000000003",
|
|
"name": "AI Rewriter Agent",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
720,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"modelId": {
|
|
"__rl": true,
|
|
"value": "claude-sonnet-4-5",
|
|
"mode": "list",
|
|
"cachedResultName": "Claude Sonnet 4.5"
|
|
},
|
|
"options": {
|
|
"temperature": 0.4,
|
|
"maxTokensToSample": 8192
|
|
}
|
|
},
|
|
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
560,
|
|
160
|
|
],
|
|
"id": "a1b2c3d4-0004-4000-8000-000000000004",
|
|
"name": "Claude Sonnet",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "REPLACE_WITH_YOUR_ANTHROPIC_CRED_ID",
|
|
"name": "Anthropic account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsonSchemaExample": "{\n \"results\": [\n {\n \"id\": \"abc123\",\n \"summary\": \"ROTAKE 3/4\\\" IMPACT T5 BATTERY OPERATED C/W 8.0 AMP HOUR BATTERIES X 2 AND CHARGER (FAST) BRUSHLESS MOTOR TORQUE 2300 (NM) WITH IDC AND LED, SMART CONTROL AND REVERSE MODE\",\n \"structured\": {\n \"brand\": \"ROTAKE\",\n \"partNumber\": \"WF-T5-CIW\",\n \"model\": \"T5\",\n \"size\": \"3/4\\\"\",\n \"power\": \"2400NM\"\n },\n \"description\": \"ROTAKE 3/4\\\" IMPACT T5 BATTERY OPERATED C/W 8.0 AMP HOUR BATTERIES X 2 AND CHARGER (FAST) BRUSHLESS MOTOR TORQUE 2300 (NM) WITH IDC AND LED, SMART CONTROL AND REVERSE MODE\\n\\nBRAND - ROTAKE\\n\\nPART NR - WF-T5-CIW\\n\\nMODEL - T5\\n\\nSIZE - 3/4\\\"\\n\\nPOWER - 2400NM\",\n \"changes\": [\n \"smart_wording\",\n \"new_code\",\n \"reformat_specs\",\n \"add_technical\"\n ],\n \"suggestedCode\": \"WF-T5-CIW\"\n },\n {\n \"id\": \"def456\",\n \"summary\": \"TRADE-SPEC TRUCK WASH CLEANING SOLUTION FOR WORKSHOP AND FLEET-DEPOT USE\",\n \"structured\": {\n \"brand\": null,\n \"partNumber\": \"TW-3340C\",\n \"model\": null,\n \"size\": null,\n \"power\": null\n },\n \"description\": \"TRADE-SPEC TRUCK WASH CLEANING SOLUTION FOR WORKSHOP AND FLEET-DEPOT USE\\n\\nPART NR - TW-3340C\",\n \"changes\": [\n \"smart_wording\",\n \"new_code\"\n ],\n \"suggestedCode\": \"TW-3340C\"\n }\n ]\n}",
|
|
"autoFix": true
|
|
},
|
|
"id": "a1b2c3d4-0005-4000-8000-000000000005",
|
|
"name": "Structured Output Parser",
|
|
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
|
|
"typeVersion": 1.3,
|
|
"position": [
|
|
880,
|
|
160
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Normalize Output — flatten whatever shape the AI Agent returns into\n// the consistent envelope the dashboard expects:\n// { results: [{ id, original, rewritten, summary, structured?, changes, suggestedCode? }] }\n//\n// The AI returns `description` per item (the full structured text) plus\n// optional `summary` (main product line) and `structured` (parsed labelled block —\n// fields may be null when omitted). We rename `description` → `rewritten` so the\n// dashboard code stays clean and pass the others through untouched. Null/missing\n// structured fields are preserved so the modal can render only the present fields.\n\nconst out = $json;\nconst raw = out.output || out.results || out;\n\nlet results = [];\nif (Array.isArray(raw)) {\n results = raw;\n} else if (raw && Array.isArray(raw.results)) {\n results = raw.results;\n} else if (raw && typeof raw === 'object') {\n results = [raw];\n}\n\nconst pickStrOrNull = (v) => (v == null ? null : String(v));\n\nconst normalized = results.map((r) => {\n const result = {\n id: String(r.id || ''),\n original: String(r.original || ''),\n rewritten: String(r.description || r.rewritten || r.text || ''),\n summary: String(r.summary || ''),\n changes: Array.isArray(r.changes) ? r.changes.map(String) : [],\n };\n if (r.structured && typeof r.structured === 'object') {\n result.structured = {\n brand: pickStrOrNull(r.structured.brand),\n partNumber: pickStrOrNull(r.structured.partNumber),\n model: pickStrOrNull(r.structured.model),\n size: pickStrOrNull(r.structured.size),\n power: pickStrOrNull(r.structured.power),\n };\n }\n if (r.suggestedCode) {\n result.suggestedCode = String(r.suggestedCode);\n }\n return result;\n});\n\nreturn [{\n json: { results: normalized }\n}];\n"
|
|
},
|
|
"id": "a1b2c3d4-0006-4000-8000-000000000006",
|
|
"name": "Normalize Output",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
960,
|
|
0
|
|
]
|
|
}
|
|
],
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"versionId": "1",
|
|
"connections": {
|
|
"Rewriter Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Prompt",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Prompt": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "AI Rewriter Agent",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"AI Rewriter Agent": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Normalize Output",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Claude Sonnet": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Rewriter Agent",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Structured Output Parser": {
|
|
"ai_outputParser": [
|
|
[
|
|
{
|
|
"node": "AI Rewriter Agent",
|
|
"type": "ai_outputParser",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": false
|
|
}
|