Files
vocero-client/s2s-client/HOWITWORKS.md
T

38 KiB
Raw Blame History

HOWITWORKS — demo S2S

Compagno di ARCHITECTURE.md: spiega il ciclo di vita concreto dei dati in 3 moduli su cui oggi ci siamo concentrati:

  1. Il campo instructions da client a ai-server:12345
  2. MCP_SEARCH_TRANSLATION_PHRASES + ranking tool MCP lato client
  3. FASE 2 — Dichiarazione tools nativi a ai-server:12345 via Realtime session.update.tools[]

0. FASE 2 — Integrazione tools nativi S2S upstream (ai-server:12345)

Obiettivo

Prima della Fase 2, i 100+ tool MCP Aquachile erano solo testo dentro instructions (9 KB di prompt). L'upstream non sapeva eseguirli e produceva frasi tipo:

"Perfecto, buscaré… [list_recent_reports]"

Con la Fase 2 dichiariamo i tools tramite il protocollo nativo OpenAI Realtime GA:

session.update { tools: [...], tool_choice: "auto" }

→ Se ai-server lo supporta, produce eventi veri response.function_call_arguments.done con nome tool + argomenti JSON. Il listener già esistente nel client (s2s-ws-client.js) si attiva automaticamente e tutto il resto della pipeline — runTool() → POST /api/mcp/tool Aquachile → client.sendToolOutput() — funziona senza altre modifiche.

0.1 Dove è stato implementato lato proxy (s2s_ui container)

Modulo File / Righe Cosa fa
Cache + normalizzazione server.py:451-544 _normalize_tools_for_realtime(raw_tools): converte qualsiasi formato tool Aquachile → formato OpenAI Realtime standard. Non alza eccezioni; tool invalidi saltati con warning.
Inject WS upstream server.py:1034-1075 Subito dopo TUNNEL ESTABLISHED, PRIMA che inizi lo streaming audio: fetch cache _agent_profile_data() → session.update con instructions (profilo agente Aquachile) + tools[] + tool_choice=auto. Errori suppressi: non rompiamo mai il flusso audio.
Log di successo KEY server.py:312-334 Quando arriva response.function_call_arguments.done dall'upstream → 2 log [MCP INJECT] ✓ UPSTREAM ha emesso… (segnale CHE LA STRATEGIA B FUNZIONA)
Proxy MCP già esistente server.py:600-656 POST /api/mcp/tool → Aquachile JSON-RPC 2.0 tools/call. Nessuna modifica necessaria.

0.2 Sequenza temporale WS Realtime (Fase 2)

1. Browser → ws://s2s_server/v1/realtime        (connessione)
2. server.py → websockets.connect(ai-server:12345)
3. [MCP INJECT] server.py → upstream:
      { "type": "session.update",
        "session": {                              // ⚠️ FORMATO CORRETTO — TUTTO dentro session:{}
          "instructions": "…system prompt profilo Aquachile…",
          "tools": [ {type: "function", name:"list_recent_reports", …}, …79 più… ],
          "tool_choice": "auto"
        }
      }
   ← LOG "[MCP INJECT] session.update upstream inviato (tools=79, fmt=nested_session OK)"
4. browser → ai-server: (turno audio utente) "el reporte de ayer por favor"
5. (se upstream ha tools[] in RuntimeConfig.session.tools — ORA SUPPORTATO Nativamente)
   ai-server → server.py → browser:
      { "type": "response.function_call_arguments.done",
        "call_id": "call_abc",
        "name": "list_recent_reports",
        "arguments": "{\"date_from\":\"2026-08-24\",\"limit\":5}" }
   ← LOG "[MCP INJECT] ✓ UPSTREAM ha emesso response.function_call_arguments.done!"
6. s2s-ws-client.js:803 → dispatch "toolcall"
7. main.js:3023 → runTool("list_recent_reports", args, "call_abc")
      branch agentToolDefs → execRemoteMcpTool
8. execRemoteMcpTool → POST /api/mcp/tool → Aquachile JSON-RPC tools/call
      LOG: "[Vocero MCP CALL]" → "[Vocero MCP HTTP]" → "[Vocero MCP OUT]"
9. client.sendToolOutput("call_abc", "Risultati: 5 report, ultimo=Report_2026-08-24.xlsx")
      → WS payload:
        { "type": "conversation.item.create",
          "item": { "type": "function_call_output", "call_id": "call_abc", "output": "..." } }
10. ai-server continua il turno con TTS nativo:
    "Aquí tienes los 5 reportes de ayer, el más reciente es Report_2026-08-24.xlsx..."
    (nessun [tool_name] nel testo del TTS!)

0.3. Formato atteso da ai-server:12345 — guida per modifiche sul backend S2S

Se dopo il deploy non vedi il log [MCP INJECT] ✓ UPSTREAM ha emesso…, vuol dire che ai-server oggi scarta il campo tools[] dal session.update. Ecco esattamente i campi che devono essere supportati (copia-incollabile nel codice di ai-server):

0.3.1 session.update in ingresso (cosa manda oggi server.py — FORMATO NESTED CORRETTO)

⚠️ REGOLA NON NEGOZIABILE: TUTTI i campi di configurazione (instructions, tools, tool_choice, voice, model) devono stare DENTRO l'oggetto session. Non mandarli MAI alla radice del payload. Il validatore Pydantic strict di OpenAI Realtime GA rifiuta qualsiasi campo sconosciuto alla root.

{
  "type": "session.update",

  // ⚠️ TUTTO dentro "session":{} — il validator Pydantic strict è extra=forbid sulla root.
  "session": {

    // 1) Istruzioni di sistema (profilo agente Aquachile)
    "instructions": "María, asistente virtual Aquachile, habla español de Chile…",

    // 2) CATALOGO TOOLS — 79 tool Aquachile normalizzati
    "tools": [
      {
        "type": "function",
        "name": "list_recent_reports",
        "description": "Restituisce l'elenco dei report più recenti, opzionalmente filtrati per data.",
        "parameters": {
          "type": "object",
          "properties": {
            "date_from": {"type": "string", "format": "date", "description": "Data inizio YYYY-MM-DD"},
            "limit":     {"type": "integer", "minimum": 1, "maximum": 50, "default": 10}
          },
          "required": []
        }
      }
      // … altri 78 tool
    ],

    // 3) Strategia selezione tool
    "tool_choice": "auto"
  }
}

Perché prima non funzionava? (ROOT CAUSE) Il proxy stava inviando instructions, tools, tool_choice ALLA RADICE del payload, fuori da session:{}. Il modello Pydantic SessionUpdateEvent (OpenAI Realtime GA) ha extra=forbid e ha rifiutato con Unknown or invalid event: session.update. Non era un rifiuto del campo tools in quanto tale — era un rifiuto di qualsiasi campo non riconosciuto messo nel posto sbagliato. La prova: runtime_config.session.instructions, .session.tools, .session.tool_choice in language_model.py:526-532 leggono TUTTI i campi da dentro .session.*.

Audit sorgente backend speech_to_speech conferma: GIA' 100% WIRED PER NATIVI TOOLS

Componente File / Righe Conferma
LLM pick tools da sessione LLM/language_model.py:526-532 tools = runtime_config.session.tools
Chat buffer gestisce FC/FCO LLM/chat.py:141-223 / 342-366 / 394-427 RealtimeConversationItemFunctionCall + FunctionCallOutput
Response handler emette WS done event api/openai_realtime/handlers/response.py:336-345 ResponseFunctionCallArgumentsDoneEvent con call_id, name, arguments(STRINGA), response_id, item_id, output_index
Conversation handler accetta ritorno output api/openai_realtime/handlers/conversation.py:30-96 handle_conversation_item_create con deferred_items anti-race
Fallback parser retrocompatibile api/openai_realtime/service.py:267-325 Se arrivano campi flat alla radice → promossi a nested session wrapper

Regole convalida obbligatorie lato ai-server per evitare rigetti silenziosi:

Regola Valore
Nome tool lunghezza ≤ 64, match regex ^[a-z0-9_]+$
Numero max tools Consigliato ≤ 80. Oggi il proxy manda 100+ con warning.
parameters.type Sempre "object" (anche se vuoto)
parameters.properties Sempre {} presente, anche se 0 proprietà
tool_choice Valori accettati: "none", "auto", oppure {type:"function",name:"…"}
Chiavi ignote NON fare strict reject. Logga warning e scarta solo la chiave.

0.3.2 Evento di ritorno richiesto da ai-server (quando il modello decide di chiamare un tool)

Quando il modello determina che serve un tool → emetti 3 eventi in ordine ESATTO:

a) { "type": "response.function_call_arguments.started", "call_id": "call_abc123", "name": "list_recent_reports", "response_id":"resp_X", "item_id":"item_Y" }
   (opzionale — se non hai streaming arguments puoi anche saltarlo)

b) (opzionale) N × { "type": "response.function_call_arguments.delta", "call_id": "call_abc123", "delta": "{\"dat" }
   (solo se streammi il JSON argomenti per pezzi)

c) ✅ EVENTO RICHIESTO OBBLIGATORIO (senza questo il client non esegue il tool):
{
  "type": "response.function_call_arguments.done",
  "call_id": "call_abc123",        // STRINGA OBBLIGATORIA — serve per la sendToolOutput
  "name": "list_recent_reports",   // STRINGA OBBLIGATORIA — nome del tool (come sopra)
  "arguments": "{\"date_from\":\"2026-08-24\",\"limit\":5}",
  "response_id": "resp_xyz",
  "item_id": "item_zzz",
  "output_index": 0
}

⚠️ Importante: arguments DEVE essere una STRINGA JSON serializzata (non un oggetto). Il client fa JSON.parse(event.arguments). Se mandi un oggetto JS → parser fallisce e il tool non viene eseguito.

0.3.3 Atteso come input successivo (il ritorno del tool dal client)

Dopo ~500ms-45s, il client manda indietro al S2S upstream:

{
  "type": "conversation.item.create",
  "item": {
    "type": "function_call_output",
    "call_id": "call_abc123",
    "output": "5 report trovati. Ultimo: Report_2026-08-24.xlsx - Aquachile ventas"
  }
}
→ Poi { "type": "response.create" }

→ L'S2S dovrebbe usare output come contesto nel prossimo turno di generazione TTS (quindi parla del risultato, senza tag [tool]).

0.4 Checklist di deploy Fase 2 (2026-08-25 — SORGENTE ai-server TROVATA in locale)

Prerequisito: Sorgente backend ai-server:12345 = /home/duxiter/s2s/speech_to_speech (Python puro). ✅ TEST CURL 2026-08-25 CONFERMATO: https://aquachile.azurianlab.com/api/v1/mcp/ è ATTUALMENTE PUBBLICO — accetta tools/list e tools/call SENZA API KEY. Con credenziali dummy dummy-test-key/secret restituisce HTTP 200 OUTPUT IDENTICO alla chiamata senza auth. Quindi NON serve valorizzare:

S2S_TOOL_EXECUTION_MCP_API_KEY=
S2S_TOOL_EXECUTION_MCP_API_SECRET=

⚠️ Nota: Il server accetta entrambi gli auth headers, ma attualmente non li valida. Restano configurati come best practice per eventuali versioni future dell'endpoint che attiveranno auth.

Ordine deploy 3 step

# Azione Perché
0 Valorizza .env MCP credentials ✅ SUPERATO — endpoint MCP Aquachile pubblico
1 Restart container s2s_ui Deploy fix formato inject fmt=nested_session in server.py:1054-1102
2 Riavvia backend ai-server:12345 con il codice aggiornato di /home/duxiter/s2s/speech_to_speech (in particolare api/openai_realtime/service.py) Deploy fallback parser retrocompatibile flat→nested in service.py:267-325
3 Hard Reload browser (Ctrl+Shift+R o Disable cache + refresh) Pulisce cache vecchio client JS

Checklist di verifica DOPO deploy 3+1 step

# Terminal A
docker logs -f s2s_ui 2>&1 | grep -E 'MCP|Vocero MCP|session.update|function_call_arguments'
Step Trigger Log atteso nel container Esito
1 Apri pagina demo (connessione WS) [MCP INJECT] session.update upstream → tools=79 hasAgentPrompt=True tool_choice=auto ✅ Inject partito
2 Subito dopo step 1 [MCP INJECT] session.update upstream inviato (tools=79, fmt=nested_session OK)
🔴 NON DEVE più comparire [MCP INJECT] suppressed upstream error — se compare ancora, il deploy di speech_to_speech service.py non è riuscito
✅ Nessun rigetto
3 Console browser 0 occorrenze Unknown or invalid event: session.update ✅ Filtro OK
4 Frase trigger tool: "¿dónde está el report de ayer?" KEY LOG 🔑
[MCP INJECT] ✓ UPSTREAM ha emesso response.function_call_arguments.done! tool=list_recent_reports args_keys=[...]
✅ Strategia B FUNZIONA — il modello produce tool calls native
5 0.5-1s dopo Step 4 [Vocero MCP CALL] name=list_recent_reports target=https://aquachile...
[Vocero MCP HTTP] name=list_recent_reports status=200
[Vocero MCP OUT] name=list_recent_reports preview=...resultKeys=...
✅ Tool eseguito realmente via REST Aquachile
6 1-2s dopo Step 5 TTS vocale naturale — es: "Aquí tienes los 5 reportes de ayer, el más reciente es Report_2026-08-24.xlsx..." ✅ Nessun (list_recent_reports) / [tool] nel testo TTS
7 Frase non-tool: "Hola, ¿cómo estás?" Risposta standard SENZA tool calls, POST /api/reasoning 200, Node-RED LOG_ONLY OK ✅ Nessuna regressione Fase 1

0.5 Stato attuale (2026-08-25 11:00) — BLOCCO RISOLTO, pipeline 100% implementata

🎯 Root Causa Reale (99.99% confidence, sorprendente)

Ipotesi iniziale, SUPERATA: "ai-server rifiuta strict il campo tools perché non è implementato"

Realtà, AUDIT SORGENTE CONFERMATO: TUTTO il sistema — backend speech_to_speech + proxy demo/server.py + client main.js / s2s-ws-client.js — è GIA' COMPLETAMENTE IMPLEMENTATO end-to-end per il tool calling nativo OpenAI Realtime GA. L'unico blocco era un errore di formato payload nel proxy:

# ❌ SBAGLIATO (come era prima) — campi alla RADICE
_inject_payload = {
    "type": "session.update",
    "instructions": "...",
    "tools": [...],
    "tool_choice": "auto"
}  # ← Pydantic SessionUpdateEvent extra=forbid → Unknown or invalid event

# ✅ CORRETTO (deployato in server.py:1054-1102) — TUTTO dentro session:{}
_inject_payload = {
    "type": "session.update",
    "session": {
        "instructions": "...",
        "tools": [...],
        "tool_choice": "auto"
    }
}  # ← Perfetto, corrisponde a runtime_config.session.tools in LLM

Prova inconfutabile: language_model.py:526-532 legge istruzioni, tools e tool_choice TUTTI dentro .session.*:

instructions = response.instructions if ... else runtime_config.session.instructions
tools = response.tools if ... else runtime_config.session.tools
tool_choice = response.tool_choice if ... else runtime_config.session.tool_choice

📋 Mappa riassuntiva: 7 componenti TUTTI già funzionanti

Componente File Stato Cosa fa
🔧 Normalizzatore tool demo/server.py:464-544 ✅ 79 tool Aquachile → formato Realtime GA standard, 0 eccezioni
🚀 Inject WS upstream demo/server.py:1054-1102 ✅ NUOVO FIX 2026-08-25 Invia nested session:{instructions,tools,tool_choice} subito dopo tunnel
🛡️ Fallback parser retrocompatibile speech_to_speech/service.py:267-325 ✅ NUOVO FIX 2026-08-25 Se arrivano campi flat root → promossi a nested session
🧠 LLM + Chat buffer speech_to_speech/LLM/* ✅ GIA' ESISTENTE Tools da sessione, FC/FCO nel chat, pending dict anti-race
📡 WS response handler (tool call OUT) speech_to_speech/handlers/response.py:336-345 ✅ GIA' ESISTENTE Emette ResponseFunctionCallArgumentsDoneEvent con arguments=STRINGA JSON, call_id, name, response_id, item_id, output_index=0
💾 WS conversation handler (tool output IN) speech_to_speech/handlers/conversation.py:30-96 ✅ GIA' ESISTENTE conversation.item.create function_call_output con deferred_items anti-race
🌐 Client WS → MCP REST → ritorno WS demo/ws/s2s-ws-client.js:803-1077 + demo/main.js:2180-2288 + 3023-3043 ✅ GIA' ESISTENTE (1) response.function_call_arguments.done → dispatch toolcall event
(2) runTool() branch agentToolDefs → execRemoteMcpTool() → POST /api/mcp/tool
(3) client.sendToolOutput(callId, output) → conversation.item.create function_call_output
(4) client.requestResponse() → prossimo turno TTS
📣 Proxy MCP REST Aquachile demo/server.py:704-760 ✅ GIA' ESISTENTE POST /api/mcp/tool JSON-RPC 2.0 tools/call + log [Vocero MCP CALL/HTTP/OUT]
🛡️ Filtro errori WS V2 demo/server.py:1713-1792 ✅ GIA' ESISTENTE Sopprime unknown_or_invalid_event se mai dovesse ricomparire, passthrough altri errori

🚀 Prossimo passo: ESEGUIRE I 3 STEP DI §0.4

  1. Valorizza .env MCP API Key/Secret ✅ SUPERATO (endpoint pubblico)
  2. Restart s2s_ui
  3. Restart ai-server con il codice aggiornato da /home/duxiter/s2s/speech_to_speech
  4. Hard reload browser (Ctrl+Shift+R)
  5. Testa: "¿dónde está el report de ayer?" → aspetta il key log [MCP INJECT] ✓ UPSTREAM ha emesso response.function_call_arguments.done!

Se Step 5 (function_call_arguments.done) non compare dopo 3 frasi trigger: apri una shell con docker logs -f ai-server per vedere se la sessione sta leggendo .session.tools dal runtime config.


⚙️ MCP MASTER SWITCH (nuovo feature 2026-08-25)

Controllato da una sola riga in .env:

#S2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp   # ⬅️ se commentata/vuota → MCP DISABLED
S2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp   # ⬅️ se decommentata/valorizzata → MCP ENABLED

Implementato come MCP_EXEC_ENABLED = bool(S2S_TOOL_EXECUTION_MCP_URL) in server.py:106-114. Quando DISABLED (stato attuale .env):

Punto di controllo Comportamento con MCP DISABLED Riferimento codice
Startup log [MCP MASTER] ⛔ DISABLED — S2S_TOOL_EXECUTION_MCP_URL is empty/commented in .env… server.py:172-182
WS /v1/realtime inject tools session.update contiene solo instructions (profilo agente); tools[] e tool_choice non vengono mai mandati. Log marker [MCP INJECT] ⛔ SKIPPED tool injection …. ⚠️ Il prompt profilo continua a funzionare. server.py:1081-1123
/api/config bootstrap client Invia al browser agentTools=[] (invece dei 79) + searchMcp=false. Effetti lato client: ranker MCP early exit, prompt non contiene più "Available MCP tools:" e ranking hints, UI pannello MCP mostra "No MCP tools available.", nessun pulsante "Esegui tool". server.py:660-694
POST /api/mcp/tool endpoint Se un client tenta comunque la chiamata (es. con codice vecchio): log [MCP DISABLED] ⛔ /api/mcp/tool called but… + HTTP 503 dettagliato (non crash, non exception trace). server.py:721-733

Per riabilitare la Fase 2 MCP in futuro, 4 step:

  1. ✏️ .env → togli # davanti a S2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp
  2. 🔄 Restart s2s_ui (deploya MCP_EXEC_ENABLED=True + inject formato nested corretto)
  3. 🔄 Restart ai-server con codice aggiornato /home/duxiter/s2s/speech_to_speech (fallback parser flat→nested)
  4. 🌐 Hard reload browser Ctrl+Shift+R

1. Flow di instructions — da impostazioni utente a S2S upstream

Definizione: instructions è il system prompt del protocollo OpenAI Realtime GA. Viene mandato al backend in 2 punti distinti:

Canale Dove Destinatario
✅ TUNNEL WS AUDIO → session.update all'inizio di ogni sessione Browser → tunnel proxy → upstream S2S nativo (ai-server:12345) Usato dal modello S2S stesso per la risposta vocale nativa
✅ REST PARALLELO → body POST /api/reasoning Browser → server.py → Node-RED o OpenAI Usato dal reasoning hook esterno (oggi Fase 1 LOG_ONLY)

Entrambi i canali partono dalla stessa funzione generatrice effectiveReasoningInstructions() nel client.

Step-by-step

┌────────────────────────────────────────────────────────────────────┐
│  1. BASE — DEFAULT_INSTRUCTIONS (main.js:26)                        │
│     "You are a friendly voice assistant. Keep replies short..."     │
│     OR textarea Settings #instructions salvato in localStorage      │
│     chiave "s2s.ws.instructions" (main.js:138, 201, 223)            │
└───────────────────────────────────────┬────────────────────────────┘
                                        │
                                        ▼
┌────────────────────────────────────────────────────────────────────┐
│  2. effectiveReasoningInstructions()  (main.js:516-533)             │
│     Appende in coda, in ordine:                                      │
│                                                                      │
│  + (se ≥1 tool attivo)  TOOL_USE_HINT (main.js:32-35)               │
│     → "do not describe your capabilities... call tool immediately"  │
│                                                                      │
│  + (se searchMcpEnabled + agentToolDefs) MCP_SEARCH_HINT +          │
│     "Available MCP tools: …catalogo completo…"                      │
│                                                                      │
│  + (se searchMcpEnabled + tool suggeriti) riga ranking client-side  │
│     "Current user request: \"${lastUserQuery}\". Ranking hint: 1. X  │
│      2. Y 3. Z — use as secondary signal only"                      │
│                                                                      │
│  + (se utente autenticato phone) voceroUserContextText()             │
│     → es: "User profile: name=Juan Pérez phone=+569… account=…"     │
└───────────────────────────────────────┬────────────────────────────┘
                                        │
                     ┌──────────────────┴──────────────────┐
                     ▼                                     ▼
  ┌─────────────────────────────┐           ┌──────────────────────────────┐
  │ 3a. effectiveInstructions() │           │ 3b. POST /api/reasoning       │
  │     (main.js:537-539)       │           │     body req.instructions     │
  │                             │           │     (main.js:569)              │
  │ • mode=OPENAI → accoda      │           │  = effectiveReasoningInst…()  │
  │   OPENAI_RENDER_HINT        │           │     tale e quale (NO append)  │
  │ • mode=NODE_RED / PROXY →   │           │                               │
  │   ritorna il valore         │           │ → server.py lo gira a         │
  │   senza modifiche           │           │   Node-RED o OpenAI           │
  └──────────────┬──────────────┘           └──────────────┬───────────────┘
                 │                                         │
                 ▼                                         ▼
  ┌─────────────────────────────┐           ┌──────────────────────────────┐
  │ 4a. startInstructions =     │           │ 4b. /api/reasoning →        │
  │     effectiveInstructions() │           │     Node-RED log:             │
  │     (main.js:2829)          │           │     [S2S NODE_RED LOG_ONLY]  │
  └──────────────┬──────────────┘           │     instructions len=…       │
                 │                          └──────────────────────────────┘
                 ▼
  ┌─────────────────────────────────────────────────────┐
  │ 5. new S2sWsRealtimeClient({                        │
  │     instructions: startInstructions                 │
  │     voice, tools, noiseGate, audioContext           │
  │   })  main.js:2839-2847                             │
  └──────────────┬──────────────────────────────────────┘
                 │
                 ▼
  ┌─────────────────────────────────────────────────────┐
  │ 6. this.options.instructions → _sendSessionUpdate() │
  │    s2s-ws-client.js:995 + 1016                      │
  │    { type: "session.update",                        │
  │      session: { type: "realtime",                   │
  │                 instructions: this.options.…,       │
  │                 audio:{output:{voice}},             │
  │                 tools, tool_choice:"auto" } }       │
  └──────────────┬──────────────────────────────────────┘
                 │
                 ▼
  ┌─────────────────────────────────────────────────────┐
  │ 7. server.py _client_to_upstream — TUNNEL 1:1       │
  │    → forward esatto byte-per-byte all'upstream      │
  │    → server.py NON tocca MAI instructions qui       │
  │    → Logga solo summary.instructionLength = len()   │
  │      (server.py:197-203) — NON MODIFICA             │
  └──────────────┬──────────────────────────────────────┘
                 │
                 ▼
  ┌─────────────────────────────────────────────────────┐
  │ 8. ai-server:12345 riceve session.update →          │
  │    istruzioni usate come system prompt da VLM/LMM   │
  │    per TUTTA la durata della sessione finché        │
  │    non arriva un altro session.update (patch)       │
  └─────────────────────────────────────────────────────┘

Patch di instructions durante la sessione (viva)

Se vuoi aggiornare istruzioni a metà conversazione senza chiudere la WS:

  • Client → chiama client.updateSession({ instructions }) (main.js:1021-1035)
  • Oppure, in futuro: proxy server-side inietta patch via upstream.send({type:"session.update", session:{...}}) in qualsiasi momento del tunnel

Tabella riassuntiva — chi oggi modifica instructions?

Livello Modifica? Note
🔵 Browser main.js Sì — generatore principale DEFAULT_INSTRUCTIONS + hints + user context
🟣 Browser s2s-ws-client.js Solo forwarding Incapsula in session.update
🟢 Proxy server.py tunnel WS NO — copia byte-per-byte Logga solo lunghezza, non tocca
🟠 Proxy server.py reasoning REST Sì (Modalità B) Usa req.instructions come system part nel prompt OpenAI (Fase 2)
🔴 Upstream ai-server:12345 Sì — applica come system prompt Entra nel ciclo STT→VLM→TTS

2. Ranking tool MCP lato client + MCP_SEARCH_TRANSLATION_PHRASES

Obiettivo: quando arriva la query utente (es. "Quiero el reporte de hoy" in spagnolo), ordinare velocemente i tool MCP del catalogo Aquachile per rilevanza, così le instructions possono includere un hint di ranking ("prova prima questi 3") e il modello sceglie lo strumento giusto con meno tentativi.

2.1 Perché serve la traduzione

Il catalogo dei tool agentToolDefs arriva dall'endpoint Aquachile MCP:

  • tool.name e tool.description sono scritti in INGLESE per convenzione di OpenAI Functions / MCP protocol.
  • Ma l'utente parla spagnolo del Cile o italiano con l'agente.

Senza traduzione il matching frase ES/IT ↔ tool EN → score 0 (zero corrispondenze). I tool non vengono mai suggeriti.

2.2 I due dizionari di traduzione (main.js)

Costante Tipo Riga Cosa fa
MCP_SEARCH_TRANSLATION_PHRASES [RegExp, string][] frasi complete L51-L70 Sostituisce frasi multi-parola prima del tokenizer. Es. reporte de hoy → today report. Priorità alta: viene applicato PER PRIMO perché le frasi intere non si possono ricostruire da token singoli.
MCP_SEARCH_TRANSLATION_TOKENS Record<it/es, en> token singolo L72 Sostituisce il resto parola per parola dopo la phase-phrase. Es. ayuda → help, cliente → customer.

2.3 Catalogo completo traduzioni oggi attivo

Phrase-level (MCP_SEARCH_TRANSLATION_PHRASES)

Lingua Input Output EN Riga
🇪🇸 informaciones del usuario / informacion del usuario user information L52-L53
🇮🇹 informazioni utente / informazione utente user information L54-L55
🇪🇸 report del dia / reporte del dia day report L56-L57
🇮🇹 report di oggi today report L58
🇪🇸 reporte de hoy today report L59
🇮🇹 report giornaliero daily report L60
🇪🇸 reporte diario daily report L61
🇪🇸 mi numero es my number is L62
🇮🇹 il mio numero e my number is L63
🇮🇹 numero di telefono phone number L64
🇪🇸 numero telefonico phone number L65
🇪🇸 numero de telefono phone number L66
🇪🇸 numero de whatsapp whatsapp number L67
🇪🇸 informacion api api information L68
🇮🇹 informazioni api api information L69

💡 Aggiungere nuove frasi: è sufficiente aggiungere righe qui, senza toccare il codice del ranking. La pipeline le applicherà automaticamente.

2.4 Pipeline di ranking chiamata per ogni turno

Utente dice: "Quiero el reporte diario y informaciones del usuario"
       │
       ▼
rankAgentTools(query)  main.js:1262
       │
       ▼
buildMcpSearchTexts(query)  main.js:1330
       │
       ├─── [0] normalizeMcpSearchText(query)   ← versione ORIGINALE normalizzata
       │        = "quiero el reporte diario y informaciones del usuario"
       │
       └─── [1] translateMcpSearchTextToEnglish(query)  main.js:1314
            │
            ├─ step 1: normalize
            ├─ step 2: loop replace per tutte le entry di MCP_SEARCH_TRANSLATION_PHRASES
            │    → "quiero el daily report y user information"
            ├─ step 3: split token, map per MCP_SEARCH_TRANSLATION_TOKENS singoli
            │    → "want the daily report and user information"
            └─ step 4: normalize finale
       │
       ▼
searchTexts = [ originale normalizzato, versione tradotta in EN ]
               (duplicati eliminati via new Set)
       │
       ▼
tokens = split searchTexts + lunghezza ≥ 3 chars + dedup
       = [ quiero, reporte, diario, informaciones, usuario,
           want, daily, report, and, user, information ]
       │
       ▼
Check rule hard-coded:
  dailyReportIntent = match ("day"|"daily"|"today") + "report" → TRUE
  (se match: boost score dei tool che hanno report in name/description)
       │
       ▼
Per ogni tool in agentToolDefs[]:
  score = somma (frequenza token in tool.name + tool.description)
        + 2× bonus dailyReportIntent se tool matcha "report"
        + 3× bonus se esatta match name vs query originale
Sort(score desc, name asc)
       │
       ▼
Torna al chiamante: [ {tool:"get_daily_report", score:42},
                      {tool:"get_user_info",     score:31},
                      ... ]
       │
       ▼
Usato in 2 punti:
  A) effectiveReasoningInstructions() main.js:525-529  → hint "Current user request…
     ranking hint: 1. get_daily_report… 2. get_user_info…" dentro le instructions
  B) UI pannello (se visibile) — lista tool suggeriti accanto alla trascrizione

2.5 Quando è chiamato rankAgentTools — ogni turno?

Trigger Frequenza Perché
maybeRunOpenAiReasoning(transcript) subito dopo transcript STT 1× per turno utente Include ranking hint dentro instructions che andrà a Node-RED / OpenAI
Live edit textarea domanda (autocomplete) Mentre utente scrive Suggerisce tool nella UI

2.6 Disattivazione automatica

Ranking ritorna [] vuoto e salta tutta la traduzione se:

  • searchMcpEnabled == false (MCP Aquachile non configurato)
  • agentToolDefs.length == 0 (catalogo tool non ricevuto da /agents/profile)
  • Query normalizzata è vuota o troppo corta
  • Nessun token dopo split ≥ 3 caratteri

2bis. DOVE VEDERE I LOG MCP — due postazioni

Il ranking MCP produce log in 2 posti diversi per design:

Postazione Tipo di log Filtro DevTools / grep Quando appare
🖥️ Browser — DevTools Console Client-side ranking dettagliato: tutta la pipeline translate → build → rank per-tool matching MCP RANK (case-sensitive) Sempre per ogni turno — anche in caso di EARLY EXIT (motivo esplicito: searchMcpEnabled=false, agentToolDefs=0, ecc.)
🐳 Server — container s2s_ui (uvicorn Python) Server-side snapshot del payload instructions quando arriva POST /api/reasoning — quanti tool MCP ci sono nel catalogo hint, TOP3 suggeriti dal ranking lato client grep '[MCP RANK]' o filter terminale MCP RANK 1 riga per ogni POST /api/reasoning (lo hai già nel flusso tra [INPUT UTENTE] e [EXT][NODE_RED] reasoning POST)

Nel tuo log di oggi (FASE 1 LOG_ONLY, senza MCP tools abilitati)

Aspettati questo flusso dopo il deploy di adesso:

7|s2s_ui  | INFO   [s2s] [INPUT UTENTE] client=… stt=Necesito el report.
7|s2s_ui  | INFO   [s2s] [MCP RANK] reasoning payload transcript="Necesito el report" instructionsLen=97 hasMcpCatalogHint=False hasRankingHint=False mcpCatalogTools=0 top3Suggested=none
7|s2s_ui  | DEBUG  [s2s] [EXT][NODE_RED] reasoning POST http://localhost:1881/api/reasoning …

E se apri DevTools Console (Verbose/Debug on, Disable cache ✓ Ctrl+Shift+R) vedi:

[MCP RANK] updateMcpToolSuggestions ENTRY partial=false itemId=- query="necesito el report" searchMcpEnabled=false
[MCP RANK] ════════════════════════════════════════
[MCP RANK] rankAgentTools START query="necesito el report" searchMcpEnabled=false agentToolDefsCount=0
[MCP RANK] buildSearchTexts raw="Necesito el report" normalized="necesito el report" translated="necesito el report" count=1 final=["necesito el report"]
[MCP RANK] translate input="necesito el report" phrasesApplied=0 tokensApplied=0 output="necesito el report"
[MCP RANK] → EARLY EXIT reason="searchMcpEnabled=false — da /api/config: MCP Aquachile feature OFF oppure S2S_TOOL_EXECUTION_MCP_URL non configurato; agentToolDefs=0 — catalogo tool MCP vuoto: profilo agente non ha tools in agentProfile via Aquachile /api/agents/profile" searchMcpEnabled=false normalized=true agentToolDefs=0
[MCP RANK] ════════════════════════════════════════ (empty result)
[MCP RANK] updateMcpToolSuggestions RESULTS partial=false matchedTools=0 ranked=none

Quando il ranking MCP inizia a produrre score > 0

Affinché un turno abbia log MCP con tool score>0, devono essere veri TUTTI e 3:

  1. .env ha S2S_TOOL_EXECUTION_MCP_URL configurato (URL Aquachile MCP) e searchMcpEnabled=true ritorna da /api/config
  2. /agents/profile Aquachile torna almeno 1 tool in agentTools[] (campo json.agentTools)
  3. La query utente ha token ≥ 3 chars che matchano tool.name o tool.description o tool.parameters

Se 1 solo dei 3 è falso → early exit spiegato nel log [MCP RANK] → EARLY EXIT reason="…".


3. Come estendere (quick cheatsheet)

Se vuoi… Dove modificare
Aggiungere 1 frase tradotta ES/IT → EN per tool matching MCP_SEARCH_TRANSLATION_PHRASES main.js — nuova riga
Aggiungere 1 parola singola tradotta MCP_SEARCH_TRANSLATION_TOKENS main.js:72
Boost score di un tipo di tool (es. report) rankAgentTools main.js:1268-1270 + mappa subito dopo
Aggiungere nuovo hint a instructions all'avvio sessione effectiveReasoningInstructions() main.js:516-533 — nuova concatenazione
Iniettare instructions server-side (segret@ dati Aquachile) senza toccare client Proxy tunnel realtime_proxy() in server.py → await upstream.send({type:"session.update", session:{instructions}}) prima dei 2 create_task