Prise de commande téléphonique automatisée par IA pour la restauration
OrderLingo est un système de prise de commande vocale par téléphone conçu pour la restauration. Il permet à un client d'appeler un numéro de téléphone et d'interagir avec un assistant vocal intelligent qui comprend le langage naturel, reconnaît les produits du menu, gère les options et variantes, vérifie les disponibilités en temps réel, et confirme la commande avant de l'enregistrer. Le système s'appuie sur Twilio pour la téléphonie, FastAPI pour le backend, et des modèles LLM (Groq, Gemini, Ollama) pour l'interprétation du langage. Une machine à états structure la conversation et un mécanisme de fast-path traite les interactions simples sans appel LLM, réduisant la latence. Un tableau de bord Next.js permet au restaurateur de gérer son menu, ses disponibilités et ses commandes. OrderLingo répond à un besoin réel de disponibilité, de réduction des erreurs et de gain de productivité pour les établissements de restauration.
- Introduction
- Contexte et problématique
- Objectifs du projet
- Présentation détaillée
- Fonctionnalités principales
- Interface — Tableau de bord
- Technologies utilisées
- Architecture du système
- Fonctionnement détaillé
- Modélisation des données
- Choix techniques
- Difficultés rencontrées
- Solutions mises en place
- Sécurité et robustesse
- Résultats et apport
- Limites
- Perspectives d'amélioration
- Installation et démarrage
- Conclusion
- Annexes
L'industrie de la restauration est l'un des secteurs économiques les plus dynamiques et les plus soumis à la pression opérationnelle. Entre les pics d'affluence, la gestion des stocks, la coordination en salle et en cuisine, et la relation client, les restaurateurs font face à une charge de travail considérable. La prise de commande, qu'elle soit sur place, en ligne ou par téléphone, constitue le point de contact le plus direct avec le client, et représente, à ce titre, un maillon critique de toute la chaîne de service.
Au cours des dernières années, l'essor des interfaces conversationnelles et des modèles de traitement du langage naturel (NLP) a ouvert de nouvelles perspectives pour automatiser, enrichir et fluidifier cette interaction. Dans ce contexte, la prise de commande téléphonique constitue un cas d'usage particulièrement pertinent : une part significative des commandes dans la restauration rapide et les pizzerias est encore passée par téléphone.
OrderLingo est un système de prise de commande vocale intelligent, développé pour la restauration. Un client appelle le numéro de téléphone du restaurant, interagit avec un assistant vocal capable de comprendre sa demande en langage naturel, de reconnaître les produits du menu, de gérer les options et variantes, de confirmer la commande, et de la transmettre au système de gestion du restaurant de façon structurée et exploitable.
La restauration rapide et la pizzeria artisanale partagent un défi commun : gérer un volume d'interactions client élevé avec un effectif souvent réduit. Pendant les heures de pointe, le personnel doit simultanément accueillir les clients en salle, gérer les commandes en caisse, superviser la production, et répondre aux appels entrants.
Les erreurs liées à la prise de commande téléphonique sont fréquentes : une option oubliée, une quantité mal comprise, ou une mauvaise interprétation d'une demande multilingue peuvent conduire à une commande incorrecte, un client mécontent, et une perte de rentabilité.
L'automatisation classique — menus téléphoniques à touches, SVI rigides — a montré ses limites depuis longtemps. Ces systèmes contraignent l'utilisateur à s'adapter à la machine. L'IA conversationnelle moderne offre une alternative radicalement différente : c'est la machine qui s'adapte à l'utilisateur, non l'inverse.
| # | Problématique |
|---|---|
| 1 | Comment automatiser une prise de commande téléphonique tout en maintenant une interaction naturelle et agréable ? |
| 2 | Comment comprendre un langage oral imprécis, familier ou mélangé (français/arabe/dialecte) ? |
| 3 | Comment transformer une expression vocale non structurée en commande exploitable par le système ? |
| 4 | Comment gérer les ambiguïtés, les oublis d'options et les produits indisponibles sans bloquer l'interaction ? |
| 5 | Comment assurer la cohérence entre ce que dit le client et ce qui existe réellement dans le menu, en temps réel ? |
Fournir aux restaurateurs un assistant vocal intelligent, déployable et opérationnel, capable de remplacer ou de compléter la prise de commande téléphonique humaine.
Fonctionnels :
- Permettre à un client de passer une commande complète par appel téléphonique, sans intervention humaine
- Comprendre les expressions naturelles du client, y compris les formulations familières ou imprécises
- Reconnaître les articles du menu, leurs variantes, tailles et options
- Gérer les options obligatoires/facultatives et demander les précisions nécessaires
- Confirmer la commande avant validation pour éviter toute erreur
- Permettre au client de suivre sa commande par téléphone via un numéro de suivi
- Fournir des informations sur le restaurant (horaires, adresse, menu)
- Gérer les ruptures de stock en temps réel
Techniques :
- Produire une commande structurée en JSON, exploitable directement par le système de gestion
- Maintenir l'état de la conversation entre les échanges (sessions persistantes)
- Assurer une faible latence entre la parole du client et la réponse vocale
- Garantir la robustesse face aux erreurs de transcription ou aux demandes hors périmètre
OrderLingo est un système logiciel complet de prise de commande vocale par téléphone. Il associe :
- La téléphonie cloud (Twilio) pour recevoir et gérer les appels
- Un backend API moderne (FastAPI / Python) pour la logique métier
- Des modèles IA conversationnels (LLM) pour comprendre le langage naturel
- Une base de données structurée (PostgreSQL) pour gérer menus, options et commandes
- Un tableau de bord web (Next.js) pour que le manager pilote son établissement
- Aux restaurateurs (pizzerias, restauration rapide) souhaitant automatiser la prise de commande téléphonique
- Aux petits et moyens établissements qui ne peuvent maintenir une ligne occupée en permanence
- Aux établissements à fort volume d'appels pendant les heures de pointe
- Aux clients finaux qui bénéficient d'une prise de commande rapide et disponible
Pour le restaurateur : augmentation de la capacité de traitement, réduction des coûts liés à la réception téléphonique, amélioration de la qualité de service.
Pour le client : prise en charge immédiate, précise et agréable, quel que soit le moment de l'appel, avec un numéro de suivi identique entre le canal vocal et le tableau de bord.
Twilio détecte l'appel, décroche et engage la conversation. Le système est disponible 24h/24, 7j/7, sans temps d'attente ni signal « occupé ». Il peut traiter plusieurs appels simultanément.
La conversation est structurée par une machine à états qui guide l'assistant à travers les étapes successives, garantissant qu'aucune étape n'est sautée.
États du flux pizzeria :
idle
├─► waiting_pizza_type
│ ├─► waiting_pizza_size ──► waiting_pizza_garniture ──► waiting_boisson
│ └─► waiting_formule_size ──► waiting_pizza_garniture ──► waiting_formule_boisson
│ │
│ waiting_more
│ │
│ waiting_confirm
│ │
│ waiting_new_order
│
├─► waiting_tracking_code (suivi commande)
└─► idle (informations restaurant)
Pour les interactions simples et prévisibles (oui/non, sélection dans une liste connue), le système prend la décision sans appeler le LLM, réduisant la latence à quelques millisecondes. Le LLM n'est invoqué que pour les expressions complexes ou ambiguës.
Signaux fast-path :
__PIZZA_ORDER__ · __FORMULE_ORDER__ · __BOISSON__ · __FORMULE_BOISSON__ · __RECAP__ · __CONFIRM__ · __END_CALL__ · __INFO__ · __TRACKING__
Le LLM reçoit un prompt système contenant le menu, les règles métier et le contexte de la conversation. Il génère une sortie JSON structurée indiquant les actions à effectuer (créer commande, ajouter article, confirmer, etc.).
La fonction _find_menu_item effectue une recherche en plusieurs passes :
- Correspondance exacte par UUID
- Correspondance par sous-chaîne (label contains ref)
- Correspondance par mots-clés triés par longueur décroissante
Cela permet de reconnaître « marguerite » pour « Margherita », « 4 froms » pour « 4 fromages », etc.
Validation des options obligatoires (_validate_required_options) avant tout ajout à la commande. Des messages d'erreur précis sont générés en cas d'option manquante : « Il faut choisir une garniture. Laquelle souhaitez-vous ? »
Le champ out_of_stock sur MenuItem est vérifié à chaque proposition. Lorsque le manager marque un produit en rupture depuis le tableau de bord, le cache est invalidé immédiatement — pas de délai de 60 secondes.
Manager marque "Fanta" en rupture
↓
invalidate_pizza_menu_cache(restaurant_id) appelé
↓
Prochain appel : "Nous n'avons plus de Fanta.
Souhaitez-vous une autre boisson ?"
_prix_vocal(9.50) → "neuf euros et cinquante centimes"
_prix_vocal(13.00) → "treize euros"
_prix_vocal(0.50) → "cinquante centimes"
_prix_vocal(1.00) → "un euro"Avant validation, la commande est reformulée intégralement :
« Voici votre commande : 1 Formule Medium (Garniture : 4 fromages) à treize euros. Boisson incluse : Sprite. Total : treize euros. C'est correct ? »
Après confirmation, un code de 6 caractères hexadécimaux est généré (secrets.token_hex(3).upper()). Ce même code est :
- Annoncé vocalement au client (« Votre numéro de suivi est A1B2C3 »)
- Affiché dans le tableau de bord manager (« Commande #A1B2C3 »)
Le client peut rappeler et dire « je veux suivre ma commande ». L'assistant demande le numéro de suivi, interroge la base de données, et répond :
« Votre commande numéro A1B2C3 est en cours de préparation. Votre pizzaïolo s'en occupe ! Puis-je faire autre chose pour vous ? »
Sur demande, l'assistant fournit le nom, l'adresse et les horaires d'ouverture du restaurant, directement depuis la base de données.
Le tableau de bord est une application Next.js 14 (App Router, TypeScript, Tailwind CSS, composants Radix UI). Il offre au manager une interface moderne et réactive pour piloter son établissement en temps réel.
La connexion s'effectue via Keycloak (OAuth2 / Direct Access Grant). Trois niveaux d'accès sont définis :
| Rôle | Accès |
|---|---|
platform_admin |
Gestion complète de tous les restaurants, managers et staff |
restaurant_manager |
Gestion complète de son restaurant (menu, stock, commandes, staff) |
staff |
Lecture seule (commandes uniquement) |
La page d'accueil du dashboard liste tous les restaurants accessibles à l'utilisateur connecté. Le platform admin peut créer de nouveaux établissements depuis un dialogue dédié. Chaque restaurant est cliquable et ouvre son tableau de bord détaillé.
Permet de configurer les informations de base du restaurant :
- Nom et slug de l'établissement
- Description
- Numéro Twilio (format E.164, ex :
+33974994861) — associe le restaurant au numéro vocal - Statut actif/inactif
- Managers assignés (platform admin uniquement) — sélecteur de comptes Keycloak
- Staff assigné (manager ou platform admin) — sélecteur de comptes Keycloak
Le numéro Twilio est la clé de routage : quand un client appelle ce numéro, OrderLingo identifie automatiquement le restaurant et charge son menu.
Interface complète de gestion du catalogue produit, organisée en trois sous-onglets :
Liste tous les articles du menu avec filtre par catégorie. Pour chaque produit :
- Nom, description, prix en rouge terracotta
- Badges de catégorie et groupes d'options associés
- Indicateur "Inactif" si le produit est désactivé
- Actions : modifier (dialogue) · supprimer (confirmation)
Formulaire de création/modification d'un produit :
- Nom, description, prix
- Catégorie (sélecteur)
- Groupes de suppléments associés (cases à cocher multiples)
- Ingrédients liés au stock (pour la disponibilité automatique)
- Statut actif/inactif
Organise les produits par familles (Pizzas, Formules, Boissons, Desserts…). Affiche le nombre de produits par catégorie. CRUD complet.
Gère les options associables aux produits (Choix de garniture, Tailles, Sauces…). Chaque groupe est extensible :
- Nom et contraintes de sélection (
min_select,max_select) - Options individuelles avec supplément de prix (ou "Gratuit")
- Ajout d'options depuis le groupe (bouton
+)
Exemple : groupe « Choix de garniture » (min: 1, max: 1) → options : Margherita (gratuit), Reine (gratuit), Orientale (gratuit), 4 fromages (gratuit).
Gestion des ingrédients en stock :
- Liste des ingrédients avec leur unité (kg, litre, unité…)
- Ajout/modification d'ingrédients
- Mise à jour des niveaux de stock (quantité + statut en_stock/rupture)
Les ingrédients peuvent être liés aux produits du menu. Si un ingrédient passe en rupture, tous les produits qui en dépendent deviennent automatiquement indisponibles.
Vue synthétique de la disponibilité de chaque produit du menu :
┌─────────────────────────────────────────────────────────┐
│ Margherita ✅ Disponible │
│ 4 fromages ✅ Disponible │
│ Fanta Rupture manuelle ❌ Indisponible [Rétablir]│
│ Sprite ✅ Disponible │
└─────────────────────────────────────────────────────────┘
Pour chaque produit, le manager peut :
- Marquer en rupture : le produit est immédiatement exclu des propositions de l'assistant vocal (cache invalidé en temps réel)
- Rétablir : le produit redevient disponible instantanément
- Voir la raison de l'indisponibilité (rupture manuelle ou ingrédient manquant)
Impact direct sur la voix : dès que le manager clique sur "Marquer rupture", l'assistant vocal l'annoncera lors du prochain appel — sans redémarrage.
Suivi en temps réel de toutes les commandes passées vocalement (ou manuellement) :
Liste des commandes :
- Référence #TRACKING_CODE — même code que celui annoncé au client par téléphone
- Nombre d'articles, date et heure
- Badge de statut coloré : Brouillon · Confirmée · En préparation · Prête · Livrée · Annulée
- Rafraîchissement automatique toutes les 15 secondes
- Filtre par statut (tous / brouillon / confirmée / en préparation / prête / livrée / annulée)
- Bouton "Nouvelle commande" pour créer une commande manuellement
Dialogue de détail / changement de statut :
En cliquant sur une commande, un dialogue s'ouvre avec :
- Stepper visuel des étapes (Brouillon → Confirmée → En préparation → Prête → Livrée) ou bandeau rouge si annulée
- Statut actuel en badge
- Détail des articles : quantité × libellé, options choisies entre parenthèses, prix unitaire
- Sélecteur pour passer au statut suivant (transitions valides uniquement)
- Mise à jour optimiste immédiate (l'affichage change avant la réponse API)
Commande #A1B2C3
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Étapes : [1]━━━[2]━━━[3]━━━(4)━━━[5]
Draft Conf Prep Prête Livr.
Statut actuel : En préparation
Articles :
1× Formule Medium (Garniture: 4 fromages) — 13.00 €
Passer à : [Prête ▼] [Fermer] [Mettre à jour]
| Technologie | Rôle | Pourquoi |
|---|---|---|
| Python 3.11 | Langage principal | Écosystème IA/ML riche, lisibilité, rapidité de développement |
| FastAPI | Framework API async | Performances élevées, validation Pydantic native, documentation OpenAPI auto |
| SQLAlchemy 2.0 | ORM async | Abstraction DB compatible async, requêtes lisibles et maintenables |
| Alembic | Migrations DB | Versionnage du schéma reproductible et traçable |
| PostgreSQL | Base de données | Robustesse, support JSON natif, transactions ACID |
| Pydantic | Validation des données | Schémas stricts, sérialisation automatique, erreurs claires |
| Technologie | Rôle | Avantage |
|---|---|---|
| Groq | Inférence LLM ultra-rapide | Latence < 500ms, critique pour la voix temps réel |
| Gemini (Google) | LLM multimodal | Compréhension avancée, multi-langues, expressions familières |
| Ollama | LLM local | Zéro dépendance cloud, confidentialité des données, coût nul |
| Technologie | Rôle | Pourquoi |
|---|---|---|
| Twilio Voice | Réception appels + STT + TTS | API simple, fiable, documentée ; abstrait toute la complexité téléphonique |
| TwiML | Contrôle du flux d'appel | Markup déclaratif pour piloter la conversation depuis le backend |
| Technologie | Rôle |
|---|---|
| Next.js 14 (App Router) | Framework React, routing, SSR |
| TypeScript | Typage statique, sécurité à la compilation |
| Tailwind CSS | Styling utilitaire, thème terracotta/sage/cream |
| Radix UI / shadcn | Composants accessibles et composables |
| Keycloak | Authentification OAuth2/OIDC |
| Technologie | Rôle |
|---|---|
| Docker + Docker Compose | Orchestration multi-services (Postgres, Keycloak, Redis, API) |
| Redis | Cache optionnel, sessions |
| Cache TTL in-process | Cache du menu (60s) avec invalidation immédiate sur rupture |
┌────────────────────────────────────────────────────────────────┐
│ CLIENT (téléphone) │
└────────────────────────────┬───────────────────────────────────┘
│ Appel téléphonique
▼
┌────────────────────────────────────────────────────────────────┐
│ COUCHE TÉLÉPHONIE — Twilio │
│ • Réception de l'appel entrant │
│ • Speech-to-Text (transcription vocale) │
│ • Text-to-Speech (synthèse vocale) │
│ • TwiML pour le contrôle du flux d'appel │
│ • Webhooks HTTP POST vers le backend │
└────────────────────────────┬───────────────────────────────────┘
│ POST /twilio/voice/step
▼
┌────────────────────────────────────────────────────────────────┐
│ COUCHE API — FastAPI │
│ • /twilio/voice/incoming (premier webhook) │
│ • /twilio/voice/step (tours de conversation) │
│ • /restaurants, /menu, /orders (dashboard) │
│ • Gestion des sessions CallSession │
└───────────────┬────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ COUCHE LOGIQUE MÉTIER — Services │
│ │
│ pizza_assistant_service.py │
│ ├── Machine à états (idle → confirm → new_order) │
│ ├── Fast-path par règles (sans LLM) │
│ └── Orchestration LLM + actions │
│ │
│ order_actions.py │
│ ├── execute_create_order (création + tracking_code) │
│ ├── execute_add_item (ajout article + validation) │
│ ├── execute_confirm_order (draft → confirmed) │
│ └── execute_track_order (lookup par tracking_code) │
│ │
│ assistant_service.py │
│ ├── _build_recap_message (résumé vocal) │
│ └── _prix_vocal() (format naturel des prix) │
└───────────┬──────────────────────────┬─────────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────────────────────┐
│ COUCHE IA/NLP │ │ COUCHE DONNÉES │
│ │ │ │
│ LLM (Groq / │ │ PostgreSQL + SQLAlchemy async │
│ Gemini / Ollama) │ │ • Restaurant (name, address, hours) │
│ │ │ • MenuItem (label, price, out_of_stock│
│ Prompt system : │ │ • OptionGroup, OptionItem │
│ • menu structuré │ │ • Order (tracking_code, status) │
│ • règles métier │ │ • OrderItem (qty, unit_price, opts) │
│ • contexte convo │ │ • CallSession (flow_state, pending) │
│ │ │ │
│ Sortie : JSON │ │ Cache TTL : menu options (60s) │
│ validé Pydantic │ │ Invalidation immédiate sur rupture │
└────────────────────┘ └──────────────────────────────────────┘
1. Client appelle → Twilio reçoit
2. Twilio POST /incoming → Backend crée CallSession(state=idle)
3. Backend répond TwiML : message d'accueil + <Gather>
4. Client parle → Twilio transcrit (STT)
5. Twilio POST /step → Backend lit CallSession
6. Fast-path : signal détecté sans LLM ? → action directe
Sinon : LLM appelé avec prompt + menu + contexte → JSON
7. JSON validé Pydantic → action métier exécutée (order_actions)
8. CallSession mise à jour (nouvel état, pending_item, order_id)
9. Backend répond TwiML : réponse vocale + <Gather> suivant
10. Boucle jusqu'à __CONFIRM__ ou __END_CALL__
Scénario : client commande une formule avec boisson
Client : "Je voudrais commander."
→ Fast-path détecte "commander" → signal __PIZZA_ORDER__
→ État : waiting_pizza_type
Assistant : "Très bien ! Souhaitez-vous une pizza seule ou une formule ?"
Client : "Une formule."
→ Fast-path détecte "formule" → signal __FORMULE_ORDER__
→ État : waiting_formule_size
Assistant : "Quelle taille ? Nous avons Medium à treize euros et Large à quinze euros."
Client : "Medium."
→ Fast-path détecte "medium" → stocké dans pending_item
→ État : waiting_pizza_garniture
Assistant : "Quelle garniture ? 4 fromages, Margherita, Orientale, Reine."
Client : "4 fromages."
→ Fast-path détecte la garniture → MenuItem trouvé en DB
→ create_order + add_item exécutés
→ État : waiting_formule_boisson
Assistant : "Votre formule inclut une boisson. Laquelle ?
Nous avons Coca-Cola, Sprite, Eau minérale."
Client : "Sprite."
→ _detect_boisson() trouve "Sprite" dans la liste
→ signal __FORMULE_BOISSON__ → stocké dans pending_item SANS add_item DB
→ État : waiting_more
Assistant : "Sprite, parfait. Autre chose ?"
Client : "Non."
→ signal __RECAP__
→ _build_recap_message() construit le résumé
→ formule_boisson ajouté depuis pending_item
Assistant : "Voici votre commande : 1 Formule Medium (Garniture : 4 fromages)
à treize euros. Boisson incluse : Sprite.
Total : treize euros. C'est correct ?"
Client : "Oui."
→ signal __CONFIRM__ → execute_confirm_order()
→ Order status : draft → confirmed
→ tracking_code = "A1B2C3" généré
Assistant : "Commande enregistrée. Le montant total est de treize euros.
Votre numéro de suivi est A1B2C3.
Elle sera prête dans environ 20 minutes.
Souhaitez-vous passer une autre commande ?"
Client : "Non."
→ signal __END_CALL__
Assistant : "Merci et à bientôt !" → Twilio raccroche
Dans le tableau de bord : La commande apparaît sous #A1B2C3 — même référence que celle entendue au téléphone.
Restaurant
├── name, slug, description
├── address, opening_hours
├── phone_number (numéro Twilio associé)
├── is_active
│
├──< Category (Pizzas / Formules / Boissons…)
│ └──< MenuItem
│ ├── label, description, price
│ ├── out_of_stock (bool, géré par le manager)
│ ├── option_group_ids []
│ └── ingredients [] (lien stock)
│
├──< OptionGroup (Choix de garniture / Taille…)
│ ├── min_select, max_select
│ └──< OptionItem (Margherita, Reine… / price_extra)
│
├──< Order
│ ├── tracking_code (6 chars hex, ex: "A1B2C3")
│ ├── status (draft→confirmed→preparing→ready→delivered|cancelled)
│ ├── customer_phone
│ └──< OrderItem
│ ├── menu_item_id, quantity, unit_price
│ └── options (JSON: {group_id: [option_id]})
│
└──< CallSession
├── call_sid (Twilio)
├── flow_state (machine à états)
├── current_order_id
├── pending_item_json (item en cours, formule_boisson…)
└── repeat_count (anti-boucle infinie)
draft ──► confirmed ──► preparing ──► ready ──► delivered
│ │ │ │
└───────────┴──────────────┴───────────┴──────► cancelled
FastAPI gère nativement l'asynchronisme (asyncio), indispensable quand chaque tour de conversation implique des appels concurrents à la DB et à un LLM externe. Flask (synchrone) aurait introduit des blocages. Django est trop lourd pour un projet API-first.
Un appel LLM introduit 500ms à 2s de latence — perceptible au téléphone. Le fast-path traite la majorité des interactions simples (oui/non, sélections connues) sans LLM, réduisant la latence à quelques millisecondes.
La machine à états offre une prévisibilité et une robustesse supérieures à un LLM qui piloterait librement la conversation. Chaque état définit précisément ce qui est attendu et valide. Cela élimine les dérives conversationnelles.
Le LLM est contraint à produire exclusivement du JSON valide. Pydantic valide chaque réponse avant exécution : aucune sortie malformée ne peut atteindre la couche données.
Chaque webhook Twilio est une requête HTTP stateless indépendante. La session ne peut pas être stockée en mémoire. La persistance DB garantit la cohérence même en cas de redémarrage ou de déploiement multi-instances.
Le tracking_code (ex : A1B2C3) est le même identifiant entre le canal vocal et le tableau de bord. Cette unicité élimine toute confusion entre l'UUID interne et la référence communiquée au client.
| # | Problème | Impact |
|---|---|---|
| 1 | Langage oral imprécis (« marguerite » → Margherita) | Commandes mal détectées |
| 2 | Expressions familières ou mélange de langues | LLM mal orienté |
| 3 | Options obligatoires manquantes (garniture non précisée) | Commande incomplète |
| 4 | Hallucinations du LLM (produits inventés) | Données incorrectes en DB |
| 5 | Latence LLM perceptible au téléphone (1-3s) | Mauvaise expérience client |
| 6 | Rupture de stock non reflétée en temps réel (cache) | Produit indispo proposé quand même |
| 7 | Boisson formule ajoutée comme article payant séparé | Facturation incorrecte |
| 8 | Faux positifs dans la détection de boisson | Mauvaise boisson assignée |
| 9 | Dashboard affichait UUID ≠ tracking_code annoncé au tel | Référence introuvable |
| 10 | « je veux suivre » routé vers commande | Mauvais flux déclenché |
| 11 | Prix annoncé « neuf virgule cinq euros » | Incompréhensible à l'oral |
| # | Solution |
|---|---|
| 1 | _find_menu_item : matching multi-niveaux + normalisation sans accents |
| 2 | LLM avec prompt enrichi d'exemples familiers ; fast-path pour les cas fréquents |
| 3 | _validate_required_options côté backend, indépendante du LLM |
| 4 | Sortie JSON stricte + validation Pydantic + re-validation métier avant exécution |
| 5 | Fast-path prioritaire pour toutes les interactions prévisibles |
| 6 | invalidate_pizza_menu_cache(restaurant_id) appelé immédiatement sur update manager |
| 7 | Signal __FORMULE_BOISSON__ : boisson dans pending_item, non créée en DB |
| 8 | _detect_boisson retourne uniquement si item réel de la liste correspond (pas de fallback générique) |
| 9 | tracking_code ajouté à OrderRead + utilisé dans le dashboard à la place de id.slice(0,8) |
| 10 | Vérifications suivre / info exécutées avant la liste intent_words dans l'état idle |
| 11 | Fonction _prix_vocal() : conversion décimale → expression naturelle française |
Requête entrante
│
▼ Pydantic (schéma API)
│
▼ Validation sortie LLM (Pydantic)
│
▼ Validation métier (options, disponibilité, transitions)
│
▼ Exécution DB
Chaque opération critique (appel LLM, requête SQL, parsing JSON) est encadrée par un bloc de gestion d'erreur. En cas d'échec, un message de fallback générique est retourné, maintenant la conversation active.
Un compteur repeat_count par session détecte les interactions sans progression. Après N répétitions, le système propose une sortie alternative ou invite à contacter directement le restaurant.
La commande n'est jamais enregistrée sans confirmation explicite du client — filet de sécurité humain dans la chaîne automatisée.
Tous les événements critiques sont journalisés (réception webhook, état session, signal fast-path, appel LLM, action exécutée, erreurs). Ces logs permettent de diagnostiquer les dysfonctionnements rapidement.
| Apport | Détail |
|---|---|
| Disponibilité 24/7 | Aucun appel sans réponse, aucun signal occupé, appels simultanés |
| Réduction des erreurs | Reformulation + confirmation systématique |
| Libération du personnel | Le personnel se concentre sur la production et le service en salle |
| Expérience client améliorée | Réponse immédiate, fluide, avec numéro de suivi |
| Traçabilité complète | Chaque commande vocale est enregistrée, horodatée, référencée |
| Base évolutive | Architecture extensible vers paiement, fidélité, statistiques |
- Qualité audio : la transcription dépend du bruit ambiant et de la qualité réseau
- Accents marqués : certains accents régionaux ou prononciations atypiques peuvent dégrader la STT
- Menu bien structuré requis : des libellés imprécis ou des contraintes mal définies génèrent des erreurs non compensables
- Coût des services cloud : Twilio (à la minute), LLM cloud (au token) — volume élevé = coût significatif
- Latence résiduelle : 1 à 3s en contexte LLM, perceptible pour un utilisateur impatient
- Couverture fonctionnelle partielle : pas de paiement, pas de modification après confirmation, pas de fidélité
| Priorité | Amélioration |
|---|---|
| Haute | Paiement intégré : lien SMS Stripe ou IVR payment Twilio |
| Haute | Intégration caisse (POS) : Lightspeed, Zelty, Addition |
| Moyenne | Statistiques dashboard : produits les plus commandés, panier moyen, heures de pointe |
| Moyenne | Multilingue avancé : détection auto de la langue, flux adapté |
| Moyenne | Clients réguliers : reconnaissance par numéro, commandes habituelles, fidélité |
| Basse | Modèle STT spécialisé : entraîné sur le vocabulaire restauration |
| Basse | Personnalisation par restaurant : ton de l'assistant, règles spécifiques |
| Basse | Amélioration continue : analyse des transcriptions pour enrichir le matching |
- Docker + Docker Compose
- Node.js 18+
- Python 3.11+ (développement local)
# 1. Lancer l'infrastructure (Postgres, Keycloak, Redis, API)
docker compose up -d
# 2. Vérifier que l'API répond
curl -s http://localhost:8000/health
# => {"status":"ok"}
# 3. Documentation OpenAPI
open http://localhost:8000/docsDATABASE_URL=postgresql+asyncpg://orderlingo:orderlingo_secret@postgres:5432/orderlingo
KEYCLOAK_ISSUER=http://localhost:8081/realms/food
KEYCLOAK_AUDIENCE=food-api
# Twilio
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_PHONE_NUMBER=+33xxxxxxxxx
# LLM (choisir un fournisseur)
GROQ_API_KEY=gsk_xxxxxxxxxxxx
GEMINI_API_KEY=AIzaxxxxxxxxxx
OLLAMA_BASE_URL=http://localhost:11434 # LLM localcd frontend
cp .env.example .env.local
# Éditer .env.local : KEYCLOAK_CLIENT_SECRET = secret du client food-api
npm install
npm run dev
# → http://localhost:3000| Username | Mot de passe | Rôle |
|---|---|---|
admin-food |
admin123 |
platform_admin |
manager1 |
manager123 |
restaurant_manager |
staff1 |
staff123 |
staff |
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL="postgresql+asyncpg://orderlingo:orderlingo_secret@localhost:5432/orderlingo"
alembic upgrade head # appliquer toutes les migrations
alembic revision --autogenerate -m "description" # créer une révision| Méthode | Endpoint | Rôle | Accès |
|---|---|---|---|
| POST | /restaurants |
Créer un restaurant | platform_admin |
| GET | /restaurants |
Lister les restaurants | platform_admin |
| PATCH | /restaurants/{id} |
Modifier un restaurant | platform_admin / manager |
| POST | /restaurants/{id}/menu/items |
Ajouter un produit | manager |
| PATCH | /restaurants/{id}/menu/items/{item_id} |
Modifier un produit | manager |
| GET | /restaurants/{id}/availability |
Disponibilité des produits | staff / manager |
| POST | /restaurants/{id}/orders |
Créer une commande | staff / manager |
| GET | /restaurants/{id}/orders |
Lister les commandes | staff / manager |
| PATCH | /restaurants/{id}/orders/{id}/status |
Changer le statut | staff / manager |
| POST | /twilio/voice/incoming |
Webhook appel entrant | Twilio |
| POST | /twilio/voice/step |
Webhook tour de conversation | Twilio |
OrderLingo représente une réponse concrète, techniquement fondée et commercialement pertinente au défi de l'automatisation de la prise de commande téléphonique dans la restauration. En combinant la puissance de la téléphonie cloud (Twilio), la flexibilité d'un backend moderne asynchrone (FastAPI / Python), les capacités d'interprétation des LLM contemporains, et un tableau de bord intuitif (Next.js), il propose une solution capable de comprendre un client, de l'accompagner dans sa commande, de valider les détails, et de produire un résultat exploitable par le système d'information du restaurant.
Le projet illustre comment l'intelligence artificielle conversationnelle peut être intégrée dans un processus métier réel, avec toutes les contraintes que cela implique : fiabilité des données, latence acceptable, robustesse face aux erreurs, et cohérence de l'expérience utilisateur. Il démontre aussi qu'une architecture en couches bien pensée — séparation claire entre dialogue, logique métier, IA et données — est indispensable pour construire un système de ce type de façon maintenable et évolutive.
Les défis rencontrés ont conduit à des solutions techniques précises : fast-path par règles, invalidation de cache immédiate, format vocal naturel, numéro de suivi unifié entre le canal vocal et le tableau de bord. Ces solutions illustrent la réalité du développement logiciel en contexte IA : les modèles sont des outils puissants, mais ils doivent être encadrés par une logique métier solide pour être fiables en production.
intelligence artificielle vocale · prise de commande téléphonique · NLP · LLM · Twilio · FastAPI · Python · machine à états · webhook · Speech-to-Text · Text-to-Speech · TwiML · restauration automatisée · assistant conversationnel · PostgreSQL · SQLAlchemy · Pydantic · cache TTL · tracking_code · fast-path · prompt engineering · JSON structuré · multilingue · rupture de stock · confirmation vocale · API REST · Groq · Gemini · Ollama · Next.js · Keycloak · tableau de bord · multi-tenant
| Problématique | Solution mise en place |
|---|---|
| Langage oral imprécis (« marguerite ») | Matching multi-niveaux + normalisation sans accents (_normalize_for_match) |
| Expressions familières ou multilingues | LLM avec exemples dans le prompt ; fast-path pour les cas fréquents |
| Confusion entre produits similaires | Tri par longueur décroissante ; demande de précision si ambiguïté |
| Options obligatoires manquantes | _validate_required_options côté backend, indépendante du LLM |
| Hallucinations du LLM | Sortie JSON stricte + validation Pydantic + validation métier avant exécution |
| Latence LLM perceptible | Fast-path prioritaire pour toutes les interactions simples |
| Rupture stock non reflétée en temps réel | invalidate_pizza_menu_cache() appelé immédiatement sur action manager |
| Boisson formule ajoutée comme article payant | Signal __FORMULE_BOISSON__ : boisson dans pending_item uniquement, sans add_item |
| Faux positifs détection boisson | _detect_boisson retourne uniquement si item réel de la liste correspond |
| Dashboard UUID ≠ tracking_code vocal | tracking_code ajouté à OrderRead ; dashboard utilise tracking_code ?? id.slice(0,8) |
| « je veux suivre » routé vers commande | Vérifications suivre/info exécutées avant intent_words dans l'état idle |
| Prix annoncé « neuf virgule cinq » | _prix_vocal() : « neuf euros et cinquante centimes » |
| Boucle infinie sur incompréhension | Compteur repeat_count par session ; fallback après N répétitions |
| Session perdue entre webhooks HTTP | CallSession persistée en DB avec état, pending_item, order_id courant |
Projet OrderLingo — Mars 2026