VIP Le Club IA VIP
REJOINDRE LE CLUB
← RETOUR À L'AGENT

GUIDE · LE CLUB IA VIP

Construire un agent vocal utile

Exemple fil rouge : Margot, hôtesse vocale du Petit Bistro, qui répond au téléphone et réserve dans Google Calendar. Modèle : grok-voice-think-fast-1.0.

1. Comment ça marche, en 30 secondes

2. Les fichiers à connaître

FichierRôleEdit
web/config/system_prompt.txtPersonnalité, règles, conversation flow.Souvent
web/config/tools.jsonOutils que l'agent peut appeler (function, MCP, web_search…).Oui
web/static/voice.jsImplémentations des function tools côté navigateur (map FUNCTIONS).Si tu ajoutes un tool
web/server.pyToken éphémère, /config, relais Twilio, handlers serveur des function tools.Si tu ajoutes un tool (path Twilio)
.envXAI_API_KEY, COMPOSIO_*, TWILIO_*. Gitignored.Une fois
Pas besoin de redémarrer le serveur pour changer system_prompt.txt ou tools.json/config relit le disque à chaque session. En prod, le dossier web/config est bind-mounté dans le container, donc git pull suffit (pas de docker compose up).

3. Écrire un bon system prompt

Suivre la structure 8 sections du Realtime Prompting Guide d'OpenAI (xAI mirroirent l'API) :

  1. Role & Objective — qui tu es, quel est l'appel réussi.
  2. Personality & Tone — ton, débit, longueur cible des phrases, vouvoiement.
  3. Unclear audio — "ne devine pas, demande de répéter". Critique pour le téléphone.
  4. Reference Pronunciations — comment dire les horaires, numéros, noms propres.
  5. Tools — chaque outil + ses paramètres + quand l'appeler.
  6. Rules — les ABSOLUES (ne jamais mentir, tirer sur la gâchette dès que les infos sont là).
  7. Conversation Flow — un état par étape (Greeting / Collect / Check / Book / Goodbye), avec sample phrases et "Exit when".
  8. Safety & Escalation — quoi dire quand c'est hors-cadre.

4. Les outils — le cœur d'un vrai agent

Un agent qui ne peut que parler n'est pas un agent. xAI accepte plusieurs types d'outils dans session.update.tools :

4.1 function — tools maison

Tu déclares un nom + un schéma JSON. Quand le modèle appelle, il émet response.function_call_arguments.done. Ton code exécute et renvoie le résultat.

// tools.json
{
  "type": "function",
  "name": "book_reservation",
  "description": "Réserve une table. À appeler après confirmation orale.",
  "parameters": {
    "type": "object",
    "properties": {
      "name":       { "type": "string" },
      "phone":      { "type": "string" },
      "date":       { "type": "string", "description": "YYYY-MM-DD" },
      "time":       { "type": "string", "description": "HH:MM 24h" },
      "party_size": { "type": "integer" },
      "notes":      { "type": "string" }
    },
    "required": ["name","phone","date","time","party_size"]
  }
}
Important : pour chaque function tool tu DOIS ajouter un handler à deux endroits : Garde-les synchronisés.

4.2 web_search — recherche web (zéro code)

xAI exécute la recherche, pas toi.

{ "type": "web_search" }

4.3 x_search — recherche sur X / Twitter

{ "type": "x_search", "allowed_x_handles": ["votre_resto"] }

4.4 file_search — RAG sur tes documents

Upload ton menu via la Collections API, récupère un collection_id, puis :

{
  "type": "file_search",
  "vector_store_ids": ["ton-collection-id"],
  "max_num_results": 8
}

4.5 mcp — serveurs Model Context Protocol

Le plus puissant. Tu plug n'importe quel serveur MCP et xAI s'occupe de tout (list_tools, appels, retours). Voir section 5 pour la liste active + comment en ajouter un.

5. MCP — la liste active et comment en ajouter

5.1 Outils MCP actuellement câblés dans tools.json

ServeurOutil exposéUsage par Margot
Composio · Google Calendar
${COMPOSIO_MCP_URL}
GOOGLECALENDAR_FIND_FREE_SLOTS Vérifier la dispo d'un créneau avant de proposer une heure. Lecture seule — la création passe par book_reservation (voir 5.3).

Le bloc dans tools.json :

{
  "type": "mcp",
  "server_url": "${COMPOSIO_MCP_URL}",
  "server_label": "google_calendar",
  "server_description": "Read-only Google Calendar access via Composio. Use GOOGLECALENDAR_FIND_FREE_SLOTS to check availability before quoting a time. Do NOT use this for creating events — use the book_reservation function tool instead.",
  "headers": { "x-api-key": "${COMPOSIO_API_KEY}" },
  "allowed_tools": ["GOOGLECALENDAR_FIND_FREE_SLOTS"]
}

5.2 Ajouter un nouveau MCP (recette en 4 étapes)

Exemple : connecter un MCP Notion pour que Margot puisse lire les "Spéciaux du jour" dans une page.

  1. Trouver un serveur MCP — soit officiel (référentiel MCP), soit via une plateforme comme Composio qui héberge des centaines d'intégrations (Notion, Slack, HubSpot, Stripe, Linear…) avec OAuth déjà géré.
  2. Récupérer l'URL + l'auth :
    • Composio → connecte le compte cible → crée un MCP server → tu obtiens une URL https://mcp.composio.dev/... et tu utilises ta COMPOSIO_API_KEY comme header x-api-key.
    • Serveur MCP auto-hébergé → URL + Bearer <token> en authorization.
  3. Ajouter le bloc dans tools.json :
    {
      "type": "mcp",
      "server_url": "${NOTION_MCP_URL}",
      "server_label": "notion",
      "server_description": "Read-only access à la base Notion du restaurant. Utilise NOTION_QUERY_DATABASE pour les Spéciaux du jour.",
      "headers": { "x-api-key": "${COMPOSIO_API_KEY}" },
      "allowed_tools": ["NOTION_QUERY_DATABASE"]
    }
  4. Mettre les vars dans .env (et redémarrer le container une seule fois, ensuite tout est hot-reloaded) :
    NOTION_MCP_URL=https://mcp.composio.dev/composio/server/...

Mentionne le nouvel outil dans system_prompt.txt section Tools avec un exemple d'appel — sans ça, le modèle ne saura pas qu'il existe même s'il est listé techniquement.

Vérifier que xAI a bien accepté ton MCP : regarde les logs du container — au démarrage de chaque appel tu verras [xai] session.updated tools=[..., mcp:notion]. Si notion n'apparaît pas, ton URL ou tes headers sont faux.

5.3 Pourquoi book_reservation est une function, pas un appel direct au MCP ?

Composio expose aussi GOOGLECALENDAR_CREATE_EVENT, mais on ne le branche pas dans tools.json. Raison : le schéma complet a beaucoup de champs (recurrence, working location, attendees…) et Grok a tendance à les remplir avec des valeurs hallucinées. On préfère une function maison qui construit le payload côté serveur (dans /api/calendar/book) à partir des 5 champs métier, et POST elle-même vers l'API REST Composio. Le modèle ne voit que les 5 champs qui comptent.

Garde-fou supplémentaire côté serveur : si le modèle passe quand même phone="anonymous" (cas masked CallerID), book_reservation retourne {"status":"error","message":"INVALID_PHONE: …"} pour forcer Margot à redemander.

6. Connecter un vrai numéro de téléphone (Twilio)

Implémenté dans ce projet. Architecture :

PSTN → Twilio → wss://<ton-domaine>/twilio/stream → wss://api.x.ai/v1/realtime
                          ↓
                  tool calls server-side
                  (book_reservation, end_call, ...)
  1. Configurer le webhook dans la console Twilio sur ton numéro : Voice & Fax → A call comes in → Webhookhttps://<ton-domaine>/twilio/voice (POST). Pas de TwiML Bin à créer, le serveur génère la TwiML dynamiquement.
  2. Ajouter dans .env : TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN — nécessaires pour raccrocher via l'API REST (close WS seul ≠ raccrocher).
  3. Récupération du numéro appelant : Twilio envoie From=+33... dans le body du webhook. /twilio/voice le passe en <Parameter name="from"> dans la TwiML, et /twilio/stream l'injecte dans le system prompt comme contexte. Si l'appelant masque son numéro (Twilio envoie From=anonymous), un bloc différent est injecté : "demande le numéro explicitement".
  4. Format audio — Twilio parle µ-law 8 kHz nativement. xAI accepte aussi via audio.input.format = audio.output.format = "audio/pcmu". Zéro transcoding, on relaye juste les bytes base64.
  5. Filet de sécurité au raccrochage — Grok-voice oublie souvent d'appeler end_call après avoir dit "au revoir". Le serveur scanne le transcript de l'assistant et déclenche le hangup lui-même si une phrase d'au revoir est détectée mais que end_call n'a pas été émis.
Démasquer son numéro pour un appel (utile en debug FR) : composer 3651 avant le numéro — ça force la présentation du CallerID pour cet appel uniquement.

7. Déployer 24/7 sur un VPS (Docker + Caddy)

Le projet livre tout ce qu'il faut : Dockerfile, docker-compose.yml, et un example Nginx (deploy/nginx.conf.example). En prod sur ce démo on tourne sur Caddy (TLS automatique, WebSockets natifs).

  1. Cloner sur ton VPS : git clone https://github.com/Thomas-Berton/template-grok-voice-agent.git /opt/margot-voice
  2. Créer .env avec XAI_API_KEY, COMPOSIO_API_KEY, COMPOSIO_MCP_URL, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN.
  3. Lancer le container :
    cd /opt/margot-voice
    docker compose up -d --build
    docker logs -f margot-voice
    Le container écoute sur 127.0.0.1:8001 uniquement — il ne touche pas les autres apps de la machine.
  4. Configurer le reverse proxy — exemple Caddy minimaliste, à ajouter dans /etc/caddy/Caddyfile :
    voiceagents.tondomaine.com {
        reverse_proxy 127.0.0.1:8001 {
            header_up Host {host}
        }
    }
    Puis systemctl reload caddy. Caddy provisionne automatiquement le cert Let's Encrypt.
  5. Pointer ton DNS (enregistrement A) sur l'IP du VPS, attendre la propagation, et c'est jouable.
Le header_up Host {host} est obligatoire — /twilio/voice lit le header Host pour construire l'URL wss://<public-host>/twilio/stream retournée dans la TwiML. Sans ça, Twilio essaierait de joindre wss://127.0.0.1:8001.

8. Logs et debug

Le serveur logue des lignes taggées pour que docker logs -f margot-voice suffise à diagnostiquer :

9. Astuces voix & latence

10. Ressources

← RETOUR À L'AGENT