255 lines
11 KiB
JSON
255 lines
11 KiB
JSON
[
|
|
{
|
|
"id": "s2s-reasoning-tab",
|
|
"type": "tab",
|
|
"label": "S2S Reasoning",
|
|
"disabled": false,
|
|
"info": "# Flusso di reasoning S2S — FASE 1: LOG-ONLY\n\nModalità attuale: il flusso NON produce risposte, NON modifica il prompt.\nSi limita a:\n 1. Ricevere POST /api/reasoning dal server demo\n 2. Estrarre transcript, lingua, phoneNumber\n 3. Loggare la richiesta utente su console + sidebar debug\n 4. Risondere HTTP 200 con contratto standard e flag `log_only: true`\n\nContratto risposta (fase 1):\n { answer: \"\", log_only: true, intent: \"log_only\", usage: {prompt_tokens:0,completion_tokens:0,total_tokens:0}, latency_ms: <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 richiesta (log-only)",
|
|
"func": "// FASE 1 — LOG-ONLY: nessuna answer generata.\n// Valida e normalizza il payload in arrivo da 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]; // uscita 2 = errore\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] richiesta utente",
|
|
"func": "// FASE 1 — Stampa la richiesta utente sul log server (Node-RED console)\n// e sul pannello debug sidebar.\n// NON produce alcuna risposta: quella arriva dal backend S2S nativo.\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": "// FASE 1 — Costruisci la risposta HTTP 200.\n// Contratto: answer VUOTA + flag log_only=true.\n// Il client (main.js) vedrà log_only e NON interverrà:\n// - NON chiama cancelActiveResponse()\n// - NON usa appendAssistantText()\n// - NON usa speechSynthesis\n// => la risposta vocale arriva dal backend S2S nativo.\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_fase1\",\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": "// Riceve dal server.py la risposta TTS emessa dal backend S2S nativo.\n// msg.req.body = { tts, response_id?, item_id?, client_ip?, role?, source_event?, user_transcript?, phone? }\n// FASE 1 LOG-ONLY: nessun side-effect oltre al log.\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] risposta sistema",
|
|
"func": "// Stampa la risposta del sistema (TTS dal backend nativo) sul log Node-RED.\n// Formato riga unica ricercabile con grep.\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": "// Risposta minimale: 200 OK + ricevuto=true.\n// Non ritorna dati.\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 ricevuto: 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": []
|
|
}
|
|
]
|