WFDASH-WEBAPP/n8n_workfows/WFDASH — Quote Item Rewriter.json

171 lines
15 KiB
JSON
Raw Normal View History

{
"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 generat
},
"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
}