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

585 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HOWITWORKS — demo S2S
> Compagno di [ARCHITECTURE.md](file:///home/duxiter/s2s/demo/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](file:///home/duxiter/s2s/demo/server.py#L451-L544) | `_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](file:///home/duxiter/s2s/demo/server.py#L1034-L1075) | 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](file:///home/duxiter/s2s/demo/server.py#L312-L334) | 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](file:///home/duxiter/s2s/demo/server.py#L600-L656) | `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.
```jsonc
{
"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:
```jsonc
{
"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](file:///home/duxiter/s2s/demo/server.py#L1054-L1102) |
| 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](file:///home/duxiter/s2s/speech_to_speech/api/openai_realtime/service.py#L267-L325) |
| 3 | **Hard Reload browser** (Ctrl+Shift+R o Disable cache + refresh) | Pulisce cache vecchio client JS |
#### Checklist di verifica DOPO deploy 3+1 step
```bash
# 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)`<br>🔴 **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 🔑**<br>`[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...`<br>`[Vocero MCP HTTP] name=list_recent_reports status=200`<br>`[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**:
```python
# ❌ 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](file:///home/duxiter/s2s/speech_to_speech/LLM/language_model.py#L526-L532) legge istruzioni, tools e tool_choice **TUTTI dentro `.session.*`**:
```python
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<br>(2) `runTool()` branch `agentToolDefs` → `execRemoteMcpTool()` → `POST /api/mcp/tool`<br>(3) `client.sendToolOutput(callId, output)` → `conversation.item.create function_call_output`<br>(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](file:///home/duxiter/s2s/demo/server.py#L106-L114). 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](file:///home/duxiter/s2s/demo/server.py#L172-L182) |
| **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](file:///home/duxiter/s2s/demo/server.py#L1081-L1123) |
| **`/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](file:///home/duxiter/s2s/demo/server.py#L660-L694) |
| **`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](file:///home/duxiter/s2s/demo/server.py#L721-L733) |
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](file:///home/duxiter/s2s/demo/main.js#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](file:///home/duxiter/s2s/demo/main.js#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](file:///home/duxiter/s2s/demo/main.js#L51-L70) — nuova riga |
| Aggiungere 1 parola singola tradotta | `MCP_SEARCH_TRANSLATION_TOKENS` [main.js:72](file:///home/duxiter/s2s/demo/main.js#L72) |
| Boost score di un tipo di tool (es. report) | `rankAgentTools` [main.js:1268-1270](file:///home/duxiter/s2s/demo/main.js#L1268-L1270) + mappa subito dopo |
| Aggiungere nuovo hint a `instructions` all'avvio sessione | `effectiveReasoningInstructions()` [main.js:516-533](file:///home/duxiter/s2s/demo/main.js#L516-L533) — nuova concatenazione |
| Iniettare `instructions` server-side (segret@ dati Aquachile) senza toccare client | **Proxy tunnel** `realtime_proxy()` in [server.py](file:///home/duxiter/s2s/demo/server.py#L891-L908) → `await upstream.send({type:"session.update", session:{instructions}})` **prima** dei 2 `create_task` |