refactor: standardize developer text and identifiers in English

This commit is contained in:
2026-10-05 10:13:25 -03:00
parent 6b8c27fd76
commit dfc1f663c7
20 changed files with 933 additions and 1142 deletions
+7 -7
View File
@@ -1,7 +1,7 @@
# Configurazione ambiente per s2s-reasoning-engine
# Environment configuration for 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.
# Copy this file to .env in the project root or s2s-reasoning-engine directory
# and adjust the values for your deployment.
# ─────────────────────────────────────────────────────────────
# 1) LLM (OpenAI-compatible)
@@ -15,7 +15,7 @@ OPENAI_REASONING_TEMPERATURE=0.2
OPENAI_REASONING_MAX_TOKENS=512
# ─────────────────────────────────────────────────────────────
# 2) Aquachile (profilo agente, tool MCP, autenticazione)
# 2) Aquachile (agent profile, MCP tools, authentication)
# ─────────────────────────────────────────────────────────────
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
@@ -32,8 +32,8 @@ NODE_RED_LOG_LEVEL=info
NODE_RED_CREDENTIAL_SECRET=change-me-in-production
# ─────────────────────────────────────────────────────────────
# 4) Integrazione con s2s UI (server.py)
# 4) S2S UI integration (server.py)
# ─────────────────────────────────────────────────────────────
# URL raggiungibile dal container Python della UI
# (se usi rete docker condivisa è http://s2s-reasoning:1880)
# URL reachable from the Python UI container
# (for a shared Docker network, use http://s2s-reasoning:1880).
S2S_REASONING_NODE_RED_URL=http://localhost:1881
+10 -11
View File
@@ -1,30 +1,29 @@
# s2s-reasoning-engine: Node-RED 5.0 custom image
# Usa Node-RED 5.x ufficiale (immagine Alpine per dimensioni ridotte)
# Use the official Node-RED 5.x Alpine image to keep the image small.
FROM nodered/node-red:5.0.3
# Porta di default di Node-RED (esposta in reti containerizzate)
# Default Node-RED port, exposed to the container network.
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
# 1) Install additional nodes in the Node-RED user data directory.
# - node-red-contrib-openai: native Chat Completions / Embeddings integration
# - node-red-contrib-cache: optional in-memory cache for Aquachile data
# - node-red-node-rbe: report-by-exception event deduplication
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
# 2) Copy the configuration and initial flows.
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)
# 3) Load static flows with projects and authentication disabled for development.
# Enable adminAuth in settings.js for production.
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
# Start Node-RED.
CMD ["npm", "start", "--", "--userDir", "/data", "--settings", "/data/settings.js"]
+10 -10
View File
@@ -4,7 +4,7 @@
"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",
"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: <ms> }\n",
"wires": []
},
{
@@ -28,8 +28,8 @@
"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];",
"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,
@@ -52,8 +52,8 @@
"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;",
"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,
@@ -89,7 +89,7 @@
"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;",
"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,
@@ -165,7 +165,7 @@
"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];",
"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,
@@ -188,8 +188,8 @@
"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;",
"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,
@@ -225,7 +225,7 @@
"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;",
"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,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "s2s-reasoning-engine",
"version": "1.0.0",
"description": "Node-RED 5.0 reasoning engine per S2S/Aquachile",
"description": "Node-RED 5.0 reasoning engine for S2S/Aquachile",
"main": "settings.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
+21 -28
View File
@@ -1,26 +1,24 @@
/**
* Node-RED 5.0 settings for s2s-reasoning-engine — LOCAL INSTALL (non Docker).
* Node-RED 5.0 settings for s2s-reasoning-engine — local installation.
* File: <project_root>/s2s-reasoning-engine/.node-red/settings.js
* oppure <project_root>/s2s-reasoning-engine/settings.js (master)
* or <project_root>/s2s-reasoning-engine/settings.js (source)
*
* Copiato automaticamente in .node-red/ da start.sh se mancante o più vecchio.
* start.sh copies this into .node-red/ when missing or older than the source.
*/
const path = require("path");
const fs = require("fs");
// userDir è sempre <s2s-reasoning-engine>/.node-red (lo script start.sh)
// start.sh uses <s2s-reasoning-engine>/.node-red as its local user directory.
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).
// Fix the editor's "tslib.es6.js 404 / net::ERR_ABORTED" error.
// Flat npm installations place tslib in the root node_modules/tslib directory
// rather than @node-red/editor-client/node_modules/tslib. The Node-RED 5 editor
// loads /tslib.es6.js from the HTTP root. Refresh the editor's public files at
// startup so the workaround survives dependency reinstallation.
(function _installTslibVendor() {
try {
const tslibRoot = path.join(LOCAL_NODE_MODULES, "tslib");
@@ -49,13 +47,10 @@ module.exports = {
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.
// Native HTTP helpers for function nodes, without externalModules.
// functionExternalModules can stall palette loading in Node-RED 5.
// Load core Node.js modules here before startup and expose them through
// functionGlobalContext, accessed with global.get("_core_https") etc.
functionModuleRequireAllowList: ["https","http","url","path","util","fs","os","buffer","zlib","stream","crypto"],
functionGlobalContext: {
_core_https: require("https"),
@@ -63,9 +58,9 @@ module.exports = {
_core_url: require("url"),
},
// adminAuth: {...} ← scommentare e valorizzare in produzione
// adminAuth: {...} // Uncomment and configure for production.
// HTTPS (opzionale in produzione)
// HTTPS (optional for production).
// https: {
// key: require("fs").readFileSync(path.join(__dirname, "privkey.pem")),
// cert: require("fs").readFileSync(path.join(__dirname, "fullchain.pem"))
@@ -82,20 +77,18 @@ module.exports = {
},
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)
// Additional node loading: Node-RED 5 looks for plugins in:
// 1) {userDir}/node_modules (our .node-red/node_modules)
// 2) {nodesDir} (the project-root installation)
// 3) npm global
// Senza nodesDir i pacchetti installati nella ROOT del progetto non
// vengono trovati.
// Set nodesDir so packages installed at the project root are discovered.
nodesDir: LOCAL_NODE_MODULES,
externalModules: {
autoInstall: true, // auto-installa nodi mancanti al boot
autoInstall: true, // Install missing nodes at startup.
autoInstallRetry: 3,
palette: {
allowInstall: true, // permette Install dall'editor
allowInstall: true, // Allow installation from the editor.
allowUpload: true,
modules: ["node-red-contrib-openai", "node-red-contrib-cache", "node-red-node-rbe"],
allowList: ["node-red-contrib-*", "node-red-node-*"],
+15 -15
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env bash
# =============================================================
# start.sh — Avvia Node-RED 5.0 in locale per s2s-reasoning-engine
# start.sh — Run Node-RED 5.0 locally for 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
# Usage:
# ./start.sh # default port 1881
# PORT=1900 ./start.sh # custom port
# LOG_LEVEL=debug ./start.sh # detailed logging
# DEBUG=nodes:* ./start.sh # internal Node-RED diagnostics
# Stop: Ctrl+C
# =============================================================
set -euo pipefail
@@ -17,8 +17,8 @@ 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.
# 1) Load the local .env first, then the parent ../.env when present.
# The parent file takes precedence when both define the same variable.
# ----------------------------------------------------------
set -a
if [ -f "$ROOT_DIR/.env" ]; then
@@ -34,7 +34,7 @@ fi
set +a
# ----------------------------------------------------------
# 2) Parametri runtime Node-RED
# 2) Node-RED runtime parameters
# ----------------------------------------------------------
PORT="${PORT:-${NODE_RED_PORT:-1881}}"
export PORT
@@ -43,11 +43,11 @@ 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)
# Local user directory; keep state here rather than in ~/.node-red.
USERDIR="${USERDIR:-$ROOT_DIR/.node-red}"
SETTINGS="${SETTINGS:-$USERDIR/settings.js}"
# Assicura che la userDir esista e i file base siano presenti.
# Create the user directory and refresh its configuration from the source files.
mkdir -p "$USERDIR"
if [ ! -f "$SETTINGS" ] || [ "$ROOT_DIR/settings.js" -nt "$SETTINGS" ]; then
cp -f "$ROOT_DIR/settings.js" "$SETTINGS"
@@ -58,15 +58,15 @@ if [ ! -f "$FLOWS" ] || [ "$ROOT_DIR/flows.json" -nt "$FLOWS" ]; then
echo "[init] copied flows.json -> $FLOWS"
fi
# Verifica node_modules installati
# Check that the local Node-RED dependencies are installed.
if [ ! -d "$ROOT_DIR/node_modules/node-red" ]; then
echo "[init] node_modules non trovati. Avvio npm install..."
echo "[init] node_modules was not found. Running npm install..."
npm install --no-audit --no-fund
fi
echo ""
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ s2s-reasoning-engine — Node-RED v5 locale ║"
echo "║ s2s-reasoning-engine — Local Node-RED v5 ║"
echo "╠════════════════════════════════════════════════════════════╣"
echo "║ Editor: http://localhost:${PORT}/ ║"
echo "║ API: http://localhost:${PORT}/api/reasoning ║"
@@ -76,7 +76,7 @@ echo "╚═══════════════════════
echo ""
# ----------------------------------------------------------
# 3) Avvio Node-RED
# 3) Start Node-RED
# ----------------------------------------------------------
exec "$ROOT_DIR/node_modules/.bin/node-red" \
--userDir "$USERDIR" \