fix(04): add Prepare Claude Request node for robust API body

Split Claude API call into two nodes:
- Prepare Claude Request: Code node that builds the request body
- Claude Intent Parser: HTTP Request node that sends the request

This fixes the 'model: Field Required' error caused by complex
expression evaluation issues in the HTTP Request node's jsonBody.

Also updated Parse Intent to get original message from the new node.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-01-30 22:10:50 -05:00
parent a08b8dba6b
commit f423f4ae4c
+29 -5
View File
@@ -2066,6 +2066,19 @@
600
]
},
{
"parameters": {
"jsCode": "// Build Claude API request body\nconst userMessage = $json.message?.text || '';\n\nconst systemPrompt = `You are a Docker container management assistant. Parse user requests and return ONLY valid JSON.\n\nValid actions:\n- view_logs: User wants to see container logs\n- query_stats: User asks about resource usage (memory, CPU)\n- container_action: User wants to start/stop/restart/update a container\n- container_status: User asks about container status\n- list_containers: User wants to see all containers\n- unknown: Cannot determine intent\n\nRespond with JSON: {\"action\": \"<action>\", \"container\": \"<name or null>\", \"parameters\": {}}\n\nExamples:\n- \"show me plex logs\" -> {\"action\": \"view_logs\", \"container\": \"plex\", \"parameters\": {\"lines\": 50}}\n- \"what's using the most memory?\" -> {\"action\": \"query_stats\", \"container\": null, \"parameters\": {\"metric\": \"memory\", \"sort\": \"desc\"}}\n- \"restart nginx\" -> {\"action\": \"container_action\", \"container\": \"nginx\", \"parameters\": {\"action\": \"restart\"}}\n- \"how's sonarr doing?\" -> {\"action\": \"container_status\", \"container\": \"sonarr\", \"parameters\": {}}\n- \"hello\" -> {\"action\": \"unknown\", \"container\": null, \"parameters\": {\"message\": \"I can help with Docker containers. Try: 'show logs', 'restart plex', or 'what's using memory?'\"}}`;\n\nconst body = {\n model: 'claude-sonnet-4-5-20250929',\n max_tokens: 256,\n system: [{\n type: 'text',\n text: systemPrompt,\n cache_control: { type: 'ephemeral' }\n }],\n messages: [{\n role: 'user',\n content: userMessage\n }]\n};\n\nreturn {\n claudeBody: JSON.stringify(body),\n message: $json.message\n};"
},
"id": "code-prepare-claude-body",
"name": "Prepare Claude Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"method": "POST",
@@ -2082,8 +2095,8 @@
]
},
"sendBody": true,
"contentType": "json",
"jsonBody": "={{ JSON.stringify({\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"max_tokens\": 256,\n \"system\": [{\n \"type\": \"text\",\n \"text\": \"You are a Docker container management assistant. Parse user requests and return ONLY valid JSON.\\n\\nValid actions:\\n- view_logs: User wants to see container logs\\n- query_stats: User asks about resource usage (memory, CPU)\\n- container_action: User wants to start/stop/restart/update a container\\n- container_status: User asks about container status\\n- list_containers: User wants to see all containers\\n- unknown: Cannot determine intent\\n\\nRespond with JSON: {\\\"action\\\": \\\"<action>\\\", \\\"container\\\": \\\"<name or null>\\\", \\\"parameters\\\": {}}\\n\\nExamples:\\n- \\\"show me plex logs\\\" -> {\\\"action\\\": \\\"view_logs\\\", \\\"container\\\": \\\"plex\\\", \\\"parameters\\\": {\\\"lines\\\": 50}}\\n- \\\"what's using the most memory?\\\" -> {\\\"action\\\": \\\"query_stats\\\", \\\"container\\\": null, \\\"parameters\\\": {\\\"metric\\\": \\\"memory\\\", \\\"sort\\\": \\\"desc\\\"}}\\n- \\\"restart nginx\\\" -> {\\\"action\\\": \\\"container_action\\\", \\\"container\\\": \\\"nginx\\\", \\\"parameters\\\": {\\\"action\\\": \\\"restart\\\"}}\\n- \\\"how's sonarr doing?\\\" -> {\\\"action\\\": \\\"container_status\\\", \\\"container\\\": \\\"sonarr\\\", \\\"parameters\\\": {}}\\n- \\\"hello\\\" -> {\\\"action\\\": \\\"unknown\\\", \\\"container\\\": null, \\\"parameters\\\": {\\\"message\\\": \\\"I can help with Docker containers. Try: 'show logs', 'restart plex', or 'what's using memory?'\\\"}}\",\n \"cache_control\": {\"type\": \"ephemeral\"}\n }],\n \"messages\": [{\n \"role\": \"user\",\n \"content\": $json.message.text\n }]\n}) }}",
"specifyBody": "json",
"jsonBody": "={{ $json.claudeBody }}",
"options": {
"timeout": 30000,
"retry": {
@@ -2097,7 +2110,7 @@
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
1010,
300
],
"credentials": {
@@ -2109,7 +2122,7 @@
},
{
"parameters": {
"jsCode": "// Parse and validate Claude's intent response\nconst response = $input.item.json;\n\n// Claude response structure: { content: [{ type: \"text\", text: \"...\" }] }\nlet intentText = '';\ntry {\n intentText = response.content[0].text;\n} catch (e) {\n return {\n action: 'error',\n error: 'Invalid Claude response structure',\n raw: JSON.stringify(response)\n };\n}\n\n// Parse JSON from Claude's response\nlet intent;\ntry {\n // Claude might wrap JSON in markdown code blocks, strip them\n const cleaned = intentText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n intent = JSON.parse(cleaned);\n} catch (e) {\n return {\n action: 'error',\n error: 'Could not parse intent JSON',\n raw: intentText\n };\n}\n\n// Validate required fields\nconst validActions = ['view_logs', 'query_stats', 'container_action', 'container_status', 'list_containers', 'unknown'];\nif (!intent.action || !validActions.includes(intent.action)) {\n return {\n action: 'unknown',\n error: 'Invalid or missing action',\n parameters: { message: 'I didn\\'t understand that. Try: \"show logs plex\" or \"restart nginx\"' }\n };\n}\n\n// Normalize container name if present\nif (intent.container) {\n intent.container = intent.container.toLowerCase().trim();\n}\n\n// Set defaults for parameters\nintent.parameters = intent.parameters || {};\n\n// Preserve original message for fallback\nintent.original_message = $input.item.json.message || {};\n\nreturn intent;"
"jsCode": "// Parse and validate Claude's intent response\nconst response = $input.item.json;\n\n// Claude response structure: { content: [{ type: \"text\", text: \"...\" }] }\nlet intentText = '';\ntry {\n intentText = response.content[0].text;\n} catch (e) {\n return {\n action: 'error',\n error: 'Invalid Claude response structure',\n raw: JSON.stringify(response)\n };\n}\n\n// Parse JSON from Claude's response\nlet intent;\ntry {\n // Claude might wrap JSON in markdown code blocks, strip them\n const cleaned = intentText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n intent = JSON.parse(cleaned);\n} catch (e) {\n return {\n action: 'error',\n error: 'Could not parse intent JSON',\n raw: intentText\n };\n}\n\n// Validate required fields\nconst validActions = ['view_logs', 'query_stats', 'container_action', 'container_status', 'list_containers', 'unknown'];\nif (!intent.action || !validActions.includes(intent.action)) {\n return {\n action: 'unknown',\n error: 'Invalid or missing action',\n parameters: { message: 'I didn\\'t understand that. Try: \"show logs plex\" or \"restart nginx\"' }\n };\n}\n\n// Normalize container name if present\nif (intent.container) {\n intent.container = intent.container.toLowerCase().trim();\n}\n\n// Set defaults for parameters\nintent.parameters = intent.parameters || {};\n\n// Preserve original message for fallback - get from Prepare Claude Request node\nintent.original_message = $('Prepare Claude Request').item.json.message || {};\n\nreturn intent;"
},
"id": "code-parse-intent",
"name": "Parse Intent",
@@ -2411,7 +2424,7 @@
"main": [
[
{
"node": "Claude Intent Parser",
"node": "Prepare Claude Request",
"type": "main",
"index": 0
}
@@ -2419,6 +2432,17 @@
[]
]
},
"Prepare Claude Request": {
"main": [
[
{
"node": "Claude Intent Parser",
"type": "main",
"index": 0
}
]
]
},
"IF Callback Authenticated": {
"main": [
[