38 KiB
HOWITWORKS — demo S2S
Compagno di ARCHITECTURE.md: spiega il ciclo di vita concreto dei dati in 3 moduli su cui oggi ci siamo concentrati:
- Il campo
instructionsda client aai-server:12345MCP_SEARCH_TRANSLATION_PHRASES+ ranking tool MCP lato client- FASE 2 — Dichiarazione tools nativi a
ai-server:12345via Realtimesession.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 | .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
Valorizza✅ SUPERATO (endpoint pubblico).envMCP API Key/Secret- Restart
s2s_ui - Restart
ai-servercon il codice aggiornato da/home/duxiter/s2s/speech_to_speech - Hard reload browser (Ctrl+Shift+R)
- 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:
- ✏️
.env→ togli#davanti aS2S_TOOL_EXECUTION_MCP_URL=https://aquachile.azurianlab.com/api/v1/mcp - 🔄 Restart
s2s_ui(deploya MCP_EXEC_ENABLED=True + inject formato nested corretto) - 🔄 Restart
ai-servercon codice aggiornato/home/duxiter/s2s/speech_to_speech(fallback parser flat→nested) - 🌐 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.nameetool.descriptionsono 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:
.envhaS2S_TOOL_EXECUTION_MCP_URLconfigurato (URL Aquachile MCP) esearchMcpEnabled=trueritorna da/api/config/agents/profileAquachile torna almeno 1 tool inagentTools[](campojson.agentTools)- La query utente ha token ≥ 3 chars che matchano
tool.nameotool.descriptionotool.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 |