This commit is contained in:
2026-08-26 17:42:00 -04:00
commit ad95d530ad
134 changed files with 31750 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Configurazione ambiente per s2s-reasoning-engine
#
# Copia questo file in .env nella root del progetto o nella cartella s2s-reasoning-engine/
# e modifica i valori secondo il tuo deployment.
# ─────────────────────────────────────────────────────────────
# 1) LLM (OpenAI-compatible)
# ─────────────────────────────────────────────────────────────
OPENAI_API_KEY=sk-....................................
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_REASONING_MODEL=gpt-4.1-mini
OPENAI_ORGANIZATION=
OPENAI_PROJECT=
OPENAI_REASONING_TEMPERATURE=0.2
OPENAI_REASONING_MAX_TOKENS=512
# ─────────────────────────────────────────────────────────────
# 2) Aquachile (profilo agente, tool MCP, autenticazione)
# ─────────────────────────────────────────────────────────────
S2S_AGENT_PROFILE=https://aquachile.azurianlab.com/api/v1/agents/profile
S2S_USER_PHONE_AUTH_URL=https://aquachile.azurianlab.com/api/v1/auth/user-by-phone
S2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp
S2S_TOOL_EXECUTION_MCP_API_KEY=....................................
S2S_TOOL_EXECUTION_MCP_API_SECRET=....................................
S2S_SEARCH_MCP=true
# ─────────────────────────────────────────────────────────────
# 3) Node-RED runtime
# ─────────────────────────────────────────────────────────────
NODE_RED_PORT=1881
NODE_RED_LOG_LEVEL=info
NODE_RED_CREDENTIAL_SECRET=change-me-in-production
# ─────────────────────────────────────────────────────────────
# 4) Integrazione con s2s UI (server.py)
# ─────────────────────────────────────────────────────────────
# URL raggiungibile dal container Python della UI
# (se usi rete docker condivisa è http://s2s-reasoning:1880)
S2S_REASONING_NODE_RED_URL=http://localhost:1881
+30
View File
@@ -0,0 +1,30 @@
# s2s-reasoning-engine: Node-RED 5.0 custom image
# Usa Node-RED 5.x ufficiale (immagine Alpine per dimensioni ridotte)
FROM nodered/node-red:5.0.3
# Porta di default di Node-RED (esposta in reti containerizzate)
EXPOSE 1880
# 1) Installa nodi aggiuntivi (a livello utente node-red nella cartella dati)
# - node-red-contrib-openai: integrazione nativa Chat Completions / Embeddings
# - node-red-contrib-http-logger: log HTTP opzionale
# - node-red-contrib-cache: cache in-memory per dati Aquachile (opzionale)
# - node-red-node-rbe: report-by-exception per deduplicare eventi
RUN cd /data && npm install --no-audit --no-fund \
node-red-contrib-openai@0.11.0 \
node-red-contrib-cache@1.1.1 \
node-red-node-rbe@0.5.0
# 2) Copia file di configurazione e flows iniziali
COPY settings.js /data/settings.js
COPY flows.json /data/flows.json
# 3) Imposta Node-RED per caricare flows statici + disabilita progetto e auth
# (per ambiente di sviluppo; in produzione abilitare adminAuth in settings.js)
ENV NODE_RED_FLOWS_FILE=/data/flows.json
ENV NODE_RED_CREDENTIAL_SECRET=s2s-reasoning-dev-secret
ENV FLOWS=/data/flows.json
# Avvio Node-RED
CMD ["npm", "start", "--", "--userDir", "/data", "--settings", "/data/settings.js"]
+254
View File
@@ -0,0 +1,254 @@
[
{
"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": []
}
]
+24
View File
@@ -0,0 +1,24 @@
{
"name": "s2s-reasoning-engine",
"version": "1.0.0",
"description": "Node-RED 5.0 reasoning engine per S2S/Aquachile",
"main": "settings.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"private": "true",
"engines": {
"node": ">=18"
},
"dependencies": {
"dotenv": "^16.4.7",
"node-red": "^5.0.3",
"node-red-contrib-cache": "^2.0.3",
"node-red-contrib-openai": "^1.0.3",
"node-red-node-rbe": "^0.5.0"
}
}
+121
View File
@@ -0,0 +1,121 @@
/**
* Node-RED 5.0 settings for s2s-reasoning-engine — LOCAL INSTALL (non Docker).
* File: <project_root>/s2s-reasoning-engine/.node-red/settings.js
* oppure <project_root>/s2s-reasoning-engine/settings.js (master)
*
* Copiato automaticamente in .node-red/ da start.sh se mancante o più vecchio.
*/
const path = require("path");
const fs = require("fs");
// userDir è sempre <s2s-reasoning-engine>/.node-red (lo script start.sh)
const ENGINE_ROOT = path.resolve(__dirname, "..");
const LOCAL_NODE_MODULES = path.join(ENGINE_ROOT, "node_modules");
const USERDIR_NODE_MODULES = path.join(__dirname, "node_modules");
// ── Fix per editor "tslib.es6.js 404 / net::ERR_ABORTED" ─────────────────
// Installazioni npm locali flat (default npm 3+) spostano tslib in
// node_modules/tslib/ (root) invece che dentro
// @node-red/editor-client/node_modules/tslib/. L'editor di Node-RED 5
// carica `/tslib.es6.js` dalla root HTTP: se la cartella nested non esiste,
// torna 404. Qui aggiorniamo editor-client/public/ con i file tslib mancanti
// a ogni avvio (sopravvive a npm reinstall se tslib è in root).
(function _installTslibVendor() {
try {
const tslibRoot = path.join(LOCAL_NODE_MODULES, "tslib");
if (!fs.existsSync(path.join(tslibRoot, "tslib.es6.js"))) return;
const pub = path.join(LOCAL_NODE_MODULES, "@node-red", "editor-client", "public");
if (!fs.existsSync(pub)) return;
for (const f of ["tslib.es6.js", "tslib.js", "tslib.mjs"]) {
const src = path.join(tslibRoot, f);
const dst = path.join(pub, f);
if (!fs.existsSync(src)) continue;
try {
const s = fs.statSync(src);
const d = fs.existsSync(dst) ? fs.statSync(dst) : null;
if (!d || d.size !== s.size || d.mtimeMs < s.mtimeMs) {
fs.copyFileSync(src, dst);
}
} catch (e) { /* ignore */ }
}
} catch (e) { /* no-op */ }
})();
module.exports = {
uiPort: process.env.PORT || process.env.NODE_RED_PORT || 1881,
uiHost: "0.0.0.0",
httpRequestTimeout: 120000,
httpRequestLegacyMsgOverrides: true,
// ── Native HTTP helper per function node (SENZA externalModules) ─────
// NOTA: non usare functionExternalModules: true — in Node-RED v5 quel
// flag può bloccare il caricamento palette (es. "37/41 nodes loading"
// durante la verifica di tutti i pkg npm).
// Al suo posto usiamo functionGlobalContext: i moduli core Node.js sono
// require'd QUI nel settings.js (prima dell'avvio NR) e poi i function
// node li recuperano via global.get("_core_https") ecc.
functionModuleRequireAllowList: ["https","http","url","path","util","fs","os","buffer","zlib","stream","crypto"],
functionGlobalContext: {
_core_https: require("https"),
_core_http: require("http"),
_core_url: require("url"),
},
// adminAuth: {...} ← scommentare e valorizzare in produzione
// HTTPS (opzionale in produzione)
// https: {
// key: require("fs").readFileSync(path.join(__dirname, "privkey.pem")),
// cert: require("fs").readFileSync(path.join(__dirname, "fullchain.pem"))
// },
flowFile: process.env.FLOWS || path.join(__dirname, "flows.json"),
flowFilePretty: true,
credentialSecret: process.env.NODE_RED_CREDENTIAL_SECRET || "s2s-reasoning-local-dev",
httpNodeCors: {
origin: "*",
methods: "GET,PUT,POST,DELETE,OPTIONS",
credentials: true,
},
csrf: false,
// ── IMPORTANTE: caricamento nodi extra ─────────────────────────────
// Node-RED 5 carica i plugin NODI da:
// 1) {userDir}/node_modules (nostro .node-red/node_modules)
// 2) {nodesDir} (installazione root del progetto)
// 3) npm global
// Senza nodesDir i pacchetti installati nella ROOT del progetto non
// vengono trovati.
nodesDir: LOCAL_NODE_MODULES,
externalModules: {
autoInstall: true, // auto-installa nodi mancanti al boot
autoInstallRetry: 3,
palette: {
allowInstall: true, // permette Install dall'editor
allowUpload: true,
modules: ["node-red-contrib-openai", "node-red-contrib-cache", "node-red-node-rbe"],
allowList: ["node-red-contrib-*", "node-red-node-*"],
denyList: []
}
},
editorTheme: { projects: { enabled: false } },
logging: {
console: {
level: process.env.NODE_RED_LOG_LEVEL || process.env.LOG_LEVEL || "info",
metrics: false,
audit: false,
timestamp: true,
}
},
contextStorage: {
default: { module: "memory" },
file: { module: "localfilesystem" }
},
};
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# =============================================================
# start.sh — Avvia Node-RED 5.0 in locale per s2s-reasoning-engine
#
# Uso:
# ./start.sh # porta default 1881
# PORT=1900 ./start.sh # porta custom
# LOG_LEVEL=debug ./start.sh # log più dettagliato
# DEBUG=nodes:* ./start.sh # debug Node-RED interno
# Stop: Ctrl+C
# =============================================================
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PARENT_DIR="$(dirname "$ROOT_DIR")"
cd "$ROOT_DIR"
# ----------------------------------------------------------
# 1) Carica env: prima locale .env (se esiste) poi padre ../.env
# Il file locale sovrascrive il padre se c'è lo stesso var.
# ----------------------------------------------------------
set -a
if [ -f "$ROOT_DIR/.env" ]; then
echo "[env] loaded $ROOT_DIR/.env"
# shellcheck disable=SC1091
source "$ROOT_DIR/.env"
fi
if [ -f "$PARENT_DIR/.env" ]; then
echo "[env] loaded $PARENT_DIR/.env"
# shellcheck disable=SC1091
source "$PARENT_DIR/.env"
fi
set +a
# ----------------------------------------------------------
# 2) Parametri runtime Node-RED
# ----------------------------------------------------------
PORT="${PORT:-${NODE_RED_PORT:-1881}}"
export PORT
export NODE_RED_PORT="$PORT"
export NODE_RED_LOG_LEVEL="${LOG_LEVEL:-${NODE_RED_LOG_LEVEL:-info}}"
export NODE_RED_CREDENTIAL_SECRET="${NODE_RED_CREDENTIAL_SECRET:-s2s-reasoning-local-dev}"
export FLOWS="${FLOWS:-$ROOT_DIR/.node-red/flows.json}"
# UserDir locale (tutto dentro la cartella corrente, NO ~/.node-red)
USERDIR="${USERDIR:-$ROOT_DIR/.node-red}"
SETTINGS="${SETTINGS:-$USERDIR/settings.js}"
# Assicura che la userDir esista e i file base siano presenti.
mkdir -p "$USERDIR"
if [ ! -f "$SETTINGS" ] || [ "$ROOT_DIR/settings.js" -nt "$SETTINGS" ]; then
cp -f "$ROOT_DIR/settings.js" "$SETTINGS"
echo "[init] copied settings.js -> $SETTINGS"
fi
if [ ! -f "$FLOWS" ] || [ "$ROOT_DIR/flows.json" -nt "$FLOWS" ]; then
cp -f "$ROOT_DIR/flows.json" "$FLOWS"
echo "[init] copied flows.json -> $FLOWS"
fi
# Verifica node_modules installati
if [ ! -d "$ROOT_DIR/node_modules/node-red" ]; then
echo "[init] node_modules non trovati. Avvio npm install..."
npm install --no-audit --no-fund
fi
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ s2s-reasoning-engine — Node-RED v5 locale ║"
echo "╠════════════════════════════════════════════════════════════╣"
echo "║ Editor: http://localhost:${PORT}/ ║"
echo "║ API: http://localhost:${PORT}/api/reasoning ║"
echo "║ UserDir: ${USERDIR}"
echo "║ LogLevel: ${NODE_RED_LOG_LEVEL}"
echo "╚════════════════════════════════════════════════════════════╝"
echo ""
# ----------------------------------------------------------
# 3) Avvio Node-RED
# ----------------------------------------------------------
exec "$ROOT_DIR/node_modules/.bin/node-red" \
--userDir "$USERDIR" \
--settings "$SETTINGS" \
--port "$PORT"