Le problème et l'architecture
Mardi, 8 h 40, Cabinet des Tanneurs (cabinet fictif de kinésithérapie et d'ostéopathie, deux praticiens, une secrétaire à mi-temps). Le téléphone a sonné onze fois depuis 8 h. La secrétaire, occupée à l'accueil physique, en a pris quatre. Sur les sept autres, cinq appelants voulaient un rendez-vous, un voulait parler d'une facture, un demandait s'il devait mettre de la glace sur sa cheville. Les cinq premiers iront ailleurs si personne ne rappelle avant midi.
Le copilote de ce projet répond à ces appels : il qualifie la demande, consulte l'agenda, propose des créneaux, réserve après un accord explicite, envoie un récapitulatif, et passe la main à la secrétaire dès que l'appel sort de son périmètre.
Le mécanisme : trois boucles qui ne tournent pas à la même vitesse
Un assistant vocal n'est pas un chatbot avec un synthétiseur. Trois boucles s'imbriquent, chacune avec son budget de temps :
| Boucle | Qui | Budget | Conséquence si dépassé |
|---|---|---|---|
| Audio | navigateur ↔ serveur ↔ Live API, trames de 100 ms | quelques dizaines de ms par trame | voix hachée, coupures |
| Tour de parole | fin de phrase de l'appelant → premier son de l'assistant | de l'ordre de 1 à 2 s | l'appelant répète, ou raccroche |
| Métier | outil chercher_creneaux, reserver_creneau… | quelques ms (SQLite local) | silence au milieu d'une phrase |
La Gemini Live API traite la deuxième boucle de bout en bout : elle reçoit l'audio brut, détecte la fin de parole (VAD, détection d'activité vocale), transcrit, décide, appelle un outil si besoin, et renvoie de l'audio. Il n'y a pas de chaîne « reconnaissance vocale → LLM → synthèse » à assembler, donc pas trois latences à additionner. Mesuré le 2026-10-15 sur 14 tours de parole : médiane 1,38 s entre la fin de phrase et le premier son, p90 1,89 s.
flowchart LR
N["Navigateur : micro (AudioWorklet 16 kHz)"] -->|"WebSocket : PCM 100 ms"| S["Serveur FastAPI : Appel"]
S -->|"LiveRequestQueue"| A["Agent ADK run_live"]
A <-->|"audio + transcriptions"| L["Gemini Live API"]
A --> O["Outils + garde-fous"]
O --> DB[("SQLite : créneaux, SMS, transferts")]
S -->|"PCM 24 kHz + événements JSON"| N
S --> J["journal.jsonl + transcription.txt"]
Les quatre décisions qui structurent le projet
- Les garde-fous qui comptent sont dans le code des outils. La consigne dit « ne réserve qu'après un oui explicite ». L'outil
reserver_creneaule vérifie aussi : il relit la dernière phrase transcrite de l'appelant et refuse sans accord. Il refuse également un créneau que l'outil de recherche n'a pas proposé dans cet appel, ce qui empêche le modèle d'inventer un horaire. - La base tranche les conflits. Deux appels simultanés peuvent viser le même créneau de 9 h 30. La réservation est un
UPDATE … WHERE statut = 'libre': un seul gagne, l'autre reçoit une erreur que le modèle sait traiter (« proposez-en un autre »). - Ce qui est testé est ce qui tourne. La même classe
Appelsert au navigateur et aux appels de test. Les tests de bout en bout jouent des fichiers audio au rythme réel, comme un micro, et vérifient le résultat dans l'agenda, pas dans le texte de la réponse. - Un humain reste joignable. Toute demande d'humain, de facture, de résultat ou de réclamation déclenche
transferer_humain, qui écrit un résumé d'une phrase pour la secrétaire et met fin au tour de l'assistant.
Le script de qualification
Le cabinet a fourni à la secrétaire un ordre de questions ; l'assistant suit le même, en sautant ce que l'appelant a déjà dit :
stateDiagram-v2 [*] --> Accueil : décroché Accueil --> Motif Motif --> Transfert : humain, facture, réclamation Motif --> Refus : conseil médical Refus --> Motif : propose un rendez-vous Motif --> Historique : kiné ou ostéo Historique --> Ordonnance : kinésithérapie Historique --> Disponibilites : ostéopathie Ordonnance --> Disponibilites Disponibilites --> Proposition : chercher_creneaux Proposition --> Confirmation : nom + portable relus Confirmation --> Reserve : oui explicite Confirmation --> Proposition : non, autre créneau Reserve --> [*] : SMS récapitulatif Transfert --> [*]
Une urgence vitale (douleur thoracique, malaise) court-circuite tout : l'assistant dit d'appeler le 15 et ne propose rien d'autre.
Mise en œuvre : le dépôt
copilote/ config.py agenda.py outils.py agent.py audio.py appel.py serveur.py scenarios.py e2e.py
web/ index.html app.js micro-worklet.js
appels/ 14 répliques WAV synthétisées (4 scénarios d'appel)
tests/ conftest.py test_agenda.py test_outils.py test_audio.py test_appel.py test_serveur.py
Makefile Dockerfile docker-compose.yml requirements.txt .env.example ci/github-actions.yml linkedin/La date de l'agenda de démonstration est fixée par DATE_REFERENCE=2026-10-15 dans .env : l'ostéopathe est complet le lendemain, c'est le scénario « créneau indisponible ». Sans cette date fixe, le test changerait de sens selon le jour où vous le lancez.
cd files
make setup # .venv, dépendances, .env copié depuis .env.example
make test # sans clé ni réseauCe qui casse en production
| Symptôme | Cause | Parade dans ce projet |
|---|---|---|
| L'assistant annonce un rendez-vous qui n'existe pas dans l'agenda | horaire inventé par le modèle | reserver_creneau n'accepte qu'un creneau_id renvoyé par chercher_creneaux dans cet appel |
| Deux patients au même créneau | lecture puis écriture non atomiques | UPDATE conditionnel, contrainte UNIQUE (praticien, debut) |
| Réservation faite sur un « euh, attendez » | confirmation laissée au jugement du modèle | accord_explicite() sur la dernière phrase transcrite |
| L'appelant croit parler à un humain | pas d'annonce | la consigne impose « assistant vocal automatique » dans l'accueil (obligation de transparence de l'AI Act, article 50) |
La règle de décision
Utilisez la Live API de bout en bout quand la conversation est courte, guidée, et que la latence perçue compte plus que le contrôle fin de chaque étape. Gardez une chaîne séparée (transcription, LLM texte, synthèse) si vous devez auditer chaque transcription avant de répondre, ou utiliser une voix de synthèse de marque que la Live API ne propose pas. Dans les deux cas, gardez les décisions irréversibles (réserver, annuler, payer) dans des outils qui vérifient leurs préconditions.
Résultat obtenu
Exécution du 2026-10-16, sans clé ni réseau :
....................................... [100%]
39 passed, 1 warning in 4.82sPratique
Un appelant dit « le premier créneau me va, mon numéro est le 06 12 34 56 78 ». Le modèle appelle aussitôt reserver_creneau. Que se passe-t-il dans ce projet ?
Choisir un créneau n'est pas confirmer une réservation relue : l'appelant n'a pas encore entendu le récapitulatif. La phrase ne contient aucun marqueur d'accord (« oui », « je confirme »), accord_explicite() renvoie faux et l'erreur dit au modèle quoi faire. Le numéro est valide : normaliser_telephone accepte le format national.
La direction veut ajouter un troisième praticien et propose de ne rien changer au code : « le modèle comprendra ». Où se situe le vrai travail ?
chercher_creneaux refuse un type_seance inconnu et la table refuse un type absent du CHECK. C'est voulu : le modèle ne peut pas inventer une spécialité. Ajouter un praticien, c'est d'abord des données et une liste de valeurs, puis une ligne de consigne. Changer de modèle ne règle rien.
Exercice. Sans lancer le code, listez les outils que l'assistant devrait appeler, dans l'ordre, pour l'appel suivant : « Bonjour, je voudrais voir l'ostéopathe demain. » (le cabinet est complet demain), puis « D'accord pour lundi 14 h », puis le numéro, puis « oui, je confirme », puis « non, c'est tout ».
Voir la solution
chercher_creneaux(type_seance="osteopathie", a_partir_du=<demain>): il renvoiejour_demande_disponible: falseet, dans le même appel, les premiers créneaux libres des jours suivants (la recherche s'élargit seule sur 7 jours, le modèle n'a pas à boucler).- Aucun outil pendant la collecte du numéro et la relecture.
reserver_creneau(creneau_id=<celui de lundi 14 h>, …)après le « oui, je confirme ».envoyer_recapitulatif().
C'est exactement la séquence mesurée dans le scénario « indisponible » de la leçon 5 : chercher_creneaux > reserver_creneau > envoyer_recapitulatif.
- Je sais expliquer pourquoi la Live API évite d'additionner trois latences
- Je sais dire quelles règles vivent dans les outils et lesquelles dans la consigne
- J'ai lancé
make setuppuismake test(42 tests verts)