[ { "id": "s2s-reasoning-tab", "type": "tab", "label": "S2S Reasoning", "disabled": false, "info": "# S2S reasoning flow — phase 1: log only\n\nThis flow does not generate answers or change the prompt. It:\n 1. Receives POST /api/reasoning from the client server.\n 2. Extracts the transcript, language, and phoneNumber.\n 3. Logs the user request to the console and debug sidebar.\n 4. Returns HTTP 200 with the standard contract and `log_only: true`.\n\nResponse contract (phase 1):\n { answer: \"\", log_only: true, intent: \"log_only\", usage: {prompt_tokens:0,completion_tokens:0,total_tokens:0}, latency_ms: }\n", "wires": [] }, { "id": "http-in-reasoning", "type": "http in", "z": "s2s-reasoning-tab", "name": "POST /api/reasoning", "url": "/api/reasoning", "method": "post", "upload": false, "swaggerDoc": "", "x": 160, "y": 160, "wires": [ [ "parse-request" ] ] }, { "id": "parse-request", "type": "function", "z": "s2s-reasoning-tab", "name": "Parse request (log only)", "func": "// Phase 1: log only; no answer is generated.\n// Validate and normalize the payload received from server.py.\n// msg.req.body = { transcript, instructions, language, user_context, phoneNumber? }\n\nconst body = msg.req?.body || {};\nconst startedAt = Date.now();\n\nconst transcript = String(body.transcript || \"\").trim();\nconst instructions = String(body.instructions || \"\").trim();\nconst language = String(body.language || \"\").trim();\nconst userContext = String(body.user_context || body.user_context || \"\").trim();\nconst phoneNumber = String(body.phoneNumber || body.phone_number || msg.req?.query?.phoneNumber || \"\").trim();\n\nif (!transcript) {\n msg.statusCode = 400;\n msg.payload = { error: \"missing_transcript\", detail: \"transcript field is required\" };\n return [null, msg]; // Output 2 is the error path.\n}\n\nconst detectedLang = language || (transcript.match(/[áéíóúñ¿¡]/i) ? \"es-CL\" : \"en-US\");\n\nmsg.state = {\n startedAt,\n transcript,\n instructionsLen: instructions.length,\n language: detectedLang,\n userContextLen: userContext.length,\n phoneNumber: phoneNumber || null,\n intent: \"log_only\",\n};\n\nmsg.payload = {\n transcript,\n language: detectedLang,\n phone: phoneNumber || \"none\",\n};\n\nreturn [msg, null];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 400, "y": 160, "wires": [ [ "log-transcript-console", "debug-parsed" ], [ "http-response-error" ] ] }, { "id": "log-transcript-console", "type": "function", "z": "s2s-reasoning-tab", "name": "[LOG] user request", "func": "// Phase 1: log the user request to the Node-RED console and debug sidebar.\n// The native S2S backend generates the response; this node does not.\n\nconst state = msg.state || {};\nconst transcript = state.transcript || \"\";\nconst lang = state.language || \"\";\nconst phone = state.phoneNumber || \"none\";\nconst ts = new Date(state.startedAt || Date.now()).toISOString();\n\nconst line = `[S2S NODE_RED LOG_ONLY] ts=${ts} lang=${lang} phone=${phone} transcript=\"${transcript.replace(/\"/g, '\\\\\"')}\"`;\n\nnode.warn(line);\n\nmsg.logLine = line;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 660, "y": 120, "wires": [ [ "build-response" ] ] }, { "id": "debug-parsed", "type": "debug", "z": "s2s-reasoning-tab", "name": "[Debug] parsed state", "active": true, "tosidebar": true, "console": true, "tostatus": false, "complete": "state", "targetType": "msg", "status": "", "x": 650, "y": 200, "wires": [] }, { "id": "build-response", "type": "function", "z": "s2s-reasoning-tab", "name": "Build 200 (log_only)", "func": "// Phase 1: build the HTTP 200 response with an empty answer and log_only=true.\n// The client leaves the native S2S response untouched: it does not call\n// cancelActiveResponse(), appendAssistantText(), or speechSynthesis.\n\nconst state = msg.state || {};\nconst latencyMs = Math.max(0, Date.now() - (state.startedAt || Date.now()));\n\nmsg.statusCode = 200;\nmsg.headers = {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n};\nmsg.payload = {\n answer: \"\",\n log_only: true,\n intent: \"log_only\",\n usage: {\n prompt_tokens: 0,\n completion_tokens: 0,\n total_tokens: 0,\n },\n latency_ms: latencyMs,\n mcp_tools_used: [],\n caller_profile_keys: [],\n debug: {\n phase: \"log_only_phase1\",\n language: state.language || \"\",\n phone: state.phoneNumber || null,\n transcript_len: (state.transcript || \"\").length,\n },\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 920, "y": 160, "wires": [ [ "http-response-ok", "debug-response" ] ] }, { "id": "http-response-ok", "type": "http response", "z": "s2s-reasoning-tab", "name": "HTTP 200 OK", "statusCode": "", "headers": {}, "x": 1160, "y": 120, "wires": [] }, { "id": "debug-response", "type": "debug", "z": "s2s-reasoning-tab", "name": "[Debug] response payload", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "status": "", "x": 1180, "y": 200, "wires": [] }, { "id": "http-response-error", "type": "http response", "z": "s2s-reasoning-tab", "name": "HTTP Error", "statusCode": "", "headers": {}, "x": 400, "y": 240, "wires": [] }, { "id": "http-in-log-response", "type": "http in", "z": "s2s-reasoning-tab", "name": "POST /api/log/system-response", "url": "/api/log/system-response", "method": "post", "upload": false, "swaggerDoc": "", "x": 160, "y": 360, "wires": [ [ "parse-log-response" ] ] }, { "id": "parse-log-response", "type": "function", "z": "s2s-reasoning-tab", "name": "Parse system-response", "func": "// Receive the native S2S backend's TTS response from server.py.\n// msg.req.body = { tts, response_id?, item_id?, client_ip?, role?, source_event?, user_transcript?, phone? }\n// Phase 1: log only; no other side effects.\n\nconst body = msg.req?.body || {};\nconst startedAt = Date.now();\n\nconst tts = String(body.tts || body.text || body.response || \"\").trim();\nconst responseId = String(body.response_id || body.responseId || \"\").trim() || null;\nconst itemId = String(body.item_id || body.itemId || \"\").trim() || null;\nconst clientIp = String(body.client_ip || body.clientIp || body.ip || \"\").trim() || null;\nconst role = String(body.role || \"assistant\").trim() || \"assistant\";\nconst sourceEvent = String(body.source_event || body.sourceEvent || \"\").trim() || null;\nconst userTranscript = String(body.user_transcript || body.userTranscript || \"\").trim() || null;\nconst phone = String(body.phone || body.phoneNumber || \"\").trim() || null;\n\nif (!tts) {\n msg.statusCode = 400;\n msg.headers = { \"Content-Type\": \"application/json\" };\n msg.payload = { error: \"missing_tts\", detail: \"field 'tts' (or 'text'/'response') is required\" };\n return [null, msg];\n}\n\nmsg.state = {\n startedAt,\n kind: \"system_response_log\",\n role,\n tts,\n responseId,\n itemId,\n clientIp,\n sourceEvent,\n userTranscript,\n phone,\n};\n\nmsg.payload = { tts, role, source: sourceEvent, client: clientIp };\nreturn [msg, null];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 420, "y": 360, "wires": [ [ "log-system-response", "debug-system-response" ], [ "http-response-error" ] ] }, { "id": "log-system-response", "type": "function", "z": "s2s-reasoning-tab", "name": "[LOG] system response", "func": "// Log the native backend's TTS response to Node-RED.\n// Use one searchable line per response.\nconst state = msg.state || {};\nconst tts = state.tts || \"\";\nconst role = state.role || \"assistant\";\nconst src = state.sourceEvent || \"\";\nconst user = state.userTranscript || \"\";\nconst phone = state.phone || \"none\";\nconst ts = new Date(state.startedAt || Date.now()).toISOString();\n\nconst userShort = user.length > 120 ? user.slice(0, 117) + \"...\" : user;\nconst ttsShort = tts.length > 240 ? tts.slice(0, 237) + \"...\" : tts;\nconst esc = (s) => String(s || \"\").replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n\nconst line = `[S2S NODE_RED SYSTEM_RESPONSE] ts=${ts} role=${role} phone=${esc(phone)} user=\"${esc(userShort)}\" tts=\"${esc(ttsShort)}\"`;\n\nnode.warn(line);\nmsg.logLine = line;\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 690, "y": 320, "wires": [ [ "build-log-ack" ] ] }, { "id": "debug-system-response", "type": "debug", "z": "s2s-reasoning-tab", "name": "[Debug] system-response state", "active": true, "tosidebar": true, "console": true, "tostatus": false, "complete": "state", "targetType": "msg", "status": "", "x": 710, "y": 400, "wires": [] }, { "id": "build-log-ack", "type": "function", "z": "s2s-reasoning-tab", "name": "Build 200 (ack)", "func": "// Return a minimal HTTP 200 acknowledgment with received=true.\n// No response data is returned.\nconst state = msg.state || {};\nconst latencyMs = Math.max(0, Date.now() - (state.startedAt || Date.now()));\nmsg.statusCode = 200;\nmsg.headers = {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n};\nmsg.payload = {\n received: true,\n log_only: true,\n intent: \"system_response_log\",\n latency_ms: latencyMs,\n};\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 950, "y": 360, "wires": [ [ "http-response-log-ack" ] ] }, { "id": "http-response-log-ack", "type": "http response", "z": "s2s-reasoning-tab", "name": "HTTP 200 ACK", "statusCode": "", "headers": {}, "x": 1180, "y": 360, "wires": [] } ]