State Machine IVR complète (JSON / YAML > Synthèse en 3 bullet points ?
Voici la synthèse en 3 points de la State Machine IVR complète (JSON/YAML), adaptée à votre MVP RDV (C0 → C7) et à une architecture FreeSWITCH + orchestrateur externe :
1) La state machine est externe à FreeSWITCH (source of truth en JSON/YAML)
- FreeSWITCH n’interprète pas de JSON/YAML : il exécute uniquement des commandes téléphonie (play, record, collect, bridge).
- La logique IVR (états, transitions, erreurs, reprises) est décrite dans un fichier JSON/YAML versionné (Git).
- Un orchestrateur (Node/Python) lit cette state machine et pilote FreeSWITCH via ESL (events ↔ commands).
2) Chaque état = Prompt vocal + Intents attendus + Actions métier (GET/PUT)
-
Un état IVR correspond exactement à vos écrans C0→C7 :
- Prompt TTS (statique ou généré par backend),
- Intents STT attendus (confirmer, déplacer, annuler, choisir un créneau…),
- Actions métier (GET/PUT Odoo, envoi SMS/ICS, callback Ringover/FS),
- Transitions selon succès / échec / silence / incompréhension.
- Les mêmes endpoints que le web sont utilisés → parité fonctionnelle totale.
3) Résilience native : retries, no-input, no-match, reprise
- La state machine prévoit retries, timeouts, fallback DTMF, et handoff humain.
- Chaque transition est idempotente (event_id / session_id).
- L’appel peut reprendre un état précédent ou basculer vers un agent sans perdre le contexte.
> fournir le YAML complet prêt à exécuter (C0→C7 + erreurs + fallback humain) ou le schéma JSON minimal exploitable directement par un orchestrateur Node.js. ?
Ci-dessous, je vous fournis une State Machine IVR complète en YAML, directement exploitable par un orchestrateur (Node/Python) pilotant FreeSWITCH via ESL. Elle couvre C0 → C7, avec : retries, timeouts, no-match/no-input, fallback DTMF, handoff agent, et hooks GET/PUT vers vos services (qui eux appellent Odoo JSON-RPC + Brevo).
Vous pourrez la versionner telle quelle dans Git.
machine: synergia_rdv_mvp_ivr version: "1.0.0" locale: fr-FR timezone: Europe/Paris # ---------------------------- # Runtime contracts (orchestrateur) # ---------------------------- # - ctx.call_id: unique call identifier (FS Unique-ID) # - ctx.session_id: stable IVR session id (uuid) # - ctx.phone_e164: phone normalized # - ctx.portal_session_token: session token (from OTP) # - ctx.partner_id: Odoo partner id # - ctx.appt_list: appointments list cache (C2) # - ctx.appt_selected_id: current appointment id # - ctx.slots: proposed slots list (C4) # - ctx.slot_selected_id: selected slot id # - ctx.preferred_channel: whatsapp|sms|email # - ctx.consent_ok: boolean globals: # Generic limits max_noinput: 2 max_nomatch: 2 max_retries: 2 # Voice behaviors allow_dtmf: true dtmf_terminator: "#" # Security otp: length: 6 ttl_sec: 600 max_attempts: 5 # Human handoff (queue / agent) handoff: enabled: true queue_id: "care_queue_1" prompt: "Je vous mets en relation avec un conseiller." # Endpoints called by orchestrator (your backend, not Odoo directly) endpoints: send_magic_link: "PUT /portal/send-otp-link" request_otp: "PUT /auth/request-otp" verify_otp: "PUT /auth/verify-otp" get_me: "GET /portal/me" list_appointments: "GET /appointments" get_appointment: "GET /appointments/{id}" confirm_appointment: "PUT /appointments/{id}/confirm" cancel_appointment: "PUT /appointments/{id}/cancel" request_callback: "PUT /appointments/{id}/callback" send_ics: "GET /appointments/{id}/ics" get_proposals: "GET /appointments/{id}/proposals" select_slot: "PUT /appointments/{id}/select-slot" submit_preferences: "PUT /appointments/{id}/preferences" propose_new_slots: "PUT /appointments/{id}/propose-slots" share_link: "PUT /appointments/{id}/share-link" notify_agent: "PUT /agent/notify" # optional audit # ---------------------------- # Intents and DTMF mappings # ---------------------------- intents: # Entry take_appointment: utterances: ["rendez-vous", "rdv", "mes rendez vous", "mon rendez-vous", "modifier rendez-vous", "prendre rendez-vous"] dtmf: ["1"] manage_team: utterances: ["équipe", "agent", "secrétariat", "administration", "gestion"] dtmf: ["2"] send_link: utterances: ["recevoir un lien", "lien", "sms", "whatsapp", "envoyer un lien"] dtmf: ["3"] # Common navigation repeat: utterances: ["répète", "répéter", "encore", "pardon"] dtmf: ["9"] back: utterances: ["retour", "précédent"] dtmf: ["8"] operator: utterances: ["conseiller", "agent", "humain", "accueil"] dtmf: ["0"] # Appointments list / detail list_upcoming: utterances: ["à venir", "prochains", "prochain", "avenir"] dtmf: ["1"] list_past: utterances: ["passés", "ancien", "historique"] dtmf: ["2"] choose_first: utterances: ["premier", "un", "1"] dtmf: ["1"] choose_second: utterances: ["deuxième", "deux", "2"] dtmf: ["2"] choose_third: utterances: ["troisième", "trois", "3"] dtmf: ["3"] details: utterances: ["détails", "détail", "voir", "fiche"] dtmf: ["4"] confirm: utterances: ["confirmer", "je confirme", "ok", "oui je confirme"] dtmf: ["5"] reschedule: utterances: ["déplacer", "replanifier", "changer", "modifier"] dtmf: ["6"] cancel: utterances: ["annuler", "supprimer", "je veux annuler"] dtmf: ["7"] add_to_calendar: utterances: ["agenda", "calendrier", "ajouter au calendrier", "ics"] dtmf: ["#"] # optional quick callback: utterances: ["me rappeler", "rappel", "appelez moi", "rappeler"] dtmf: ["*"] # optional quick # Slots / preferences none_of_these: utterances: ["aucun", "aucun de ces créneaux", "pas possible", "non", "je ne suis pas disponible"] dtmf: ["0"] morning: utterances: ["matin", "le matin"] dtmf: ["1"] afternoon: utterances: ["après-midi", "apres midi", "l'après-midi"] dtmf: ["2"] mode_home: utterances: ["domicile", "à domicile", "chez moi"] dtmf: ["1"] mode_video: utterances: ["visio", "vidéo", "video", "en ligne"] dtmf: ["2"] mode_clinic: utterances: ["cabinet", "clinique", "sur place"] dtmf: ["3"] # Cancel reasons reason_personal: utterances: ["personnel", "raison personnelle", "imprévu"] dtmf: ["1"] reason_health: utterances: ["santé", "malade", "problème de santé"] dtmf: ["2"] reason_unavailable: utterances: ["indisponible", "travail", "pas disponible"] dtmf: ["3"] reason_other: utterances: ["autre"] dtmf: ["4"] # Consent & channel yes: utterances: ["oui", "j'accepte", "d'accord", "ok"] dtmf: ["1"] no: utterances: ["non", "je refuse"] dtmf: ["2"] chan_whatsapp: utterances: ["whatsapp"] dtmf: ["1"] chan_sms: utterances: ["sms"] dtmf: ["2"] chan_email: utterances: ["email", "e-mail", "mail"] dtmf: ["3"] # ---------------------------- # State machine # ---------------------------- states: # ====== C0 ====== ivr.entry: type: prompt_and_listen prompt: > Bonjour et bienvenue. Pour prendre ou modifier un rendez-vous, dites "rendez-vous" ou tapez 1. Pour la zone équipe, dites "équipe" ou tapez 2. Pour recevoir un lien sur votre téléphone, dites "recevoir un lien" ou tapez 3. noinput: max: 2 next: ivr.entry.retry nomatch: max: 2 next: ivr.entry.retry routes: - when_intent: take_appointment next: ivr.auth.phone_capture - when_intent: manage_team next: ivr.handoff.agent_gate - when_intent: send_link next: ivr.portal.send_link - when_intent: operator next: ivr.handoff.to_agent ivr.entry.retry: type: prompt_and_listen prompt: > Je n'ai pas compris. Dites "rendez-vous", "équipe", ou "recevoir un lien". Vous pouvez aussi taper 1, 2 ou 3. routes: - when_intent: take_appointment next: ivr.auth.phone_capture - when_intent: manage_team next: ivr.handoff.agent_gate - when_intent: send_link next: ivr.portal.send_link - when_intent: operator next: ivr.handoff.to_agent fallback: next: ivr.goodbye # ====== C0 (send link) ====== ivr.portal.send_link: type: prompt_and_listen prompt: > Très bien. Dites votre numéro de téléphone, ou tapez-le au clavier, puis dièse. collect: mode: phone allow_dtmf: true action: call: ${globals.endpoints.send_magic_link} body: phone_e164: "{{ctx.phone_e164}}" channel_preference: "sms_or_whatsapp" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: > C'est envoyé. Vous pouvez raccrocher et continuer via le lien reçu. next: ivr.goodbye on_fail: prompt: > Je n'ai pas pu envoyer le lien. Souhaitez-vous parler à un conseiller ? next: ivr.handoff.to_agent # ====== C1 ====== ivr.auth.phone_capture: type: prompt_and_listen prompt: > Pour sécuriser votre accès, dites votre numéro de téléphone, ou tapez-le au clavier puis dièse. collect: mode: phone allow_dtmf: true validate: phone_e164: required routes: - when_intent: operator next: ivr.handoff.to_agent - when_intent: repeat next: ivr.auth.phone_capture next: ivr.auth.request_otp ivr.auth.request_otp: type: action_then_prompt action: call: ${globals.endpoints.request_otp} body: phone_e164: "{{ctx.phone_e164}}" channel: "sms" purpose: "portal_login" role: "client" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: set: otp_challenge_id: "{{resp.challenge_id}}" prompt: > Un code à six chiffres vient de vous être envoyé par SMS. Dites-le, ou saisissez-le au clavier puis dièse. next: ivr.auth.verify_otp on_fail: prompt: > Je n'ai pas pu envoyer le code. Souhaitez-vous recevoir un lien ou parler à un conseiller ? next: ivr.auth.request_otp.fail_menu ivr.auth.request_otp.fail_menu: type: prompt_and_listen prompt: > Dites "recevoir un lien" ou tapez 3. Pour un conseiller, dites "conseiller" ou tapez 0. routes: - when_intent: send_link next: ivr.portal.send_link - when_intent: operator next: ivr.handoff.to_agent fallback: next: ivr.goodbye ivr.auth.verify_otp: type: prompt_and_listen prompt: "Veuillez donner le code." collect: mode: otp allow_dtmf: true length: 6 retry: max: ${globals.max_retries} on_fail_next: ivr.auth.verify_otp.retry action: call: ${globals.endpoints.verify_otp} body: phone_e164: "{{ctx.phone_e164}}" otp: "{{ctx.otp}}" challenge_id: "{{ctx.otp_challenge_id}}" role: "client" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: set: portal_session_token: "{{resp.session_token}}" partner_id: "{{resp.partner_id}}" next: ivr.auth.consent on_fail: next: ivr.auth.verify_otp.retry ivr.auth.verify_otp.retry: type: prompt_and_listen prompt: > Code incorrect ou expiré. Dites le code à nouveau. Pour renvoyer un code, dites "renvoyer". Pour un conseiller, dites "conseiller". routes: - when_text: "renvoyer" next: ivr.auth.request_otp - when_intent: operator next: ivr.handoff.to_agent next: ivr.auth.verify_otp ivr.auth.consent: type: prompt_and_listen prompt: > Pour gérer vos rendez-vous, acceptez-vous l'utilisation minimale de vos données ? Dites "oui" ou "non". Tapez 1 pour oui, 2 pour non. routes: - when_intent: yes set: consent_ok: true next: ivr.auth.preferred_channel - when_intent: no set: consent_ok: false next: ivr.auth.consent.refused noinput: max: 1 next: ivr.auth.consent nomatch: max: 1 next: ivr.auth.consent ivr.auth.consent.refused: type: prompt_and_listen prompt: > Sans consentement, je ne peux pas vous donner accès au portail. Souhaitez-vous parler à un conseiller ? routes: - when_intent: yes next: ivr.handoff.to_agent - when_intent: no next: ivr.goodbye fallback: next: ivr.goodbye ivr.auth.preferred_channel: type: prompt_and_listen prompt: > Quel canal préférez-vous pour les confirmations et rappels ? Dites "WhatsApp", "SMS" ou "email". Tapez 1 WhatsApp, 2 SMS, 3 email. routes: - when_intent: chan_whatsapp set: { preferred_channel: "whatsapp" } next: ivr.auth.persist_prefs - when_intent: chan_sms set: { preferred_channel: "sms" } next: ivr.auth.persist_prefs - when_intent: chan_email set: { preferred_channel: "email" } next: ivr.auth.persist_prefs fallback: set: { preferred_channel: "whatsapp" } next: ivr.auth.persist_prefs ivr.auth.persist_prefs: type: action_then_prompt action: call: ${globals.endpoints.get_me} headers: Authorization: "Bearer {{ctx.portal_session_token}}" on_success: prompt: "Merci. Vous êtes maintenant connecté." next: ivr.appointments.list.upcoming # ====== C2 ====== ivr.appointments.list.upcoming: type: action_then_prompt action: call: ${globals.endpoints.list_appointments} headers: Authorization: "Bearer {{ctx.portal_session_token}}" query: scope: "upcoming" on_success: set: appt_list: "{{resp.appointments}}" prompt_from_action: true prompt_template: > {{#if resp.count == 0}} Vous n'avez aucun rendez-vous à venir. Dites "nouveau rendez-vous" ou "historique". {{else}} Vous avez {{resp.count}} rendez-vous à venir. {{resp.summary_voice}} Dites "détails", "confirmer", "déplacer", "annuler", ou "agenda". Pour choisir un rendez-vous, dites "premier", "deuxième", "troisième". {{/if}} next: ivr.appointments.list.router on_fail: prompt: "Je n'arrive pas à accéder à vos rendez-vous. Souhaitez-vous un conseiller ?" next: ivr.handoff.to_agent ivr.appointments.list.past: type: action_then_prompt action: call: ${globals.endpoints.list_appointments} headers: Authorization: "Bearer {{ctx.portal_session_token}}" query: scope: "past" on_success: set: appt_list: "{{resp.appointments}}" prompt_from_action: true prompt_template: > Vous avez {{resp.count}} rendez-vous passés. {{resp.summary_voice}} Dites "à venir" pour revenir, ou "détails" pour entendre une fiche. next: ivr.appointments.list.router ivr.appointments.list.router: type: listen routes: - when_intent: list_upcoming next: ivr.appointments.list.upcoming - when_intent: list_past next: ivr.appointments.list.past - when_intent: choose_first set: { appt_selected_id: "{{ctx.appt_list[0].id}}" } next: ivr.appointment.detail - when_intent: choose_second set: { appt_selected_id: "{{ctx.appt_list[1].id}}" } next: ivr.appointment.detail - when_intent: choose_third set: { appt_selected_id: "{{ctx.appt_list[2].id}}" } next: ivr.appointment.detail - when_intent: details next: ivr.appointment.detail - when_intent: confirm next: ivr.appointment.confirm - when_intent: reschedule next: ivr.reschedule.preferences - when_intent: cancel next: ivr.cancel.reason - when_intent: add_to_calendar next: ivr.appointment.send_ics - when_intent: operator next: ivr.handoff.to_agent - when_intent: repeat next: ivr.appointments.list.upcoming noinput: max: 2 next: ivr.appointments.list.upcoming nomatch: max: 2 next: ivr.appointments.list.upcoming # ====== C3 ====== ivr.appointment.detail: type: action_then_prompt action: call: ${globals.endpoints.get_appointment} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" on_success: prompt_from_action: true prompt_template: > {{resp.voice_detail}} Vous pouvez dire "agenda" pour recevoir l'invitation, "déplacer", "annuler", ou "me rappeler". next: ivr.appointment.detail.router on_fail: prompt: "Je n'ai pas retrouvé ce rendez-vous. Retour à la liste." next: ivr.appointments.list.upcoming ivr.appointment.detail.router: type: listen routes: - when_intent: add_to_calendar next: ivr.appointment.send_ics - when_intent: reschedule next: ivr.reschedule.preferences - when_intent: cancel next: ivr.cancel.reason - when_intent: callback next: ivr.appointment.callback - when_intent: back next: ivr.appointments.list.upcoming - when_intent: operator next: ivr.handoff.to_agent - when_intent: repeat next: ivr.appointment.detail noinput: max: 2 next: ivr.appointment.detail nomatch: max: 2 next: ivr.appointment.detail ivr.appointment.send_ics: type: action_then_prompt action: call: ${globals.endpoints.send_ics} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" on_success: prompt: "C'est fait. Je vous envoie l'invitation calendrier." next: ivr.appointment.detail on_fail: prompt: "Je n'ai pas pu envoyer l'invitation. Souhaitez-vous un conseiller ?" next: ivr.handoff.to_agent ivr.appointment.callback: type: action_then_prompt action: call: ${globals.endpoints.request_callback} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: reason: "portal_callback" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: "Très bien. Un conseiller vous rappellera dès que possible." next: ivr.goodbye on_fail: prompt: "Je n'ai pas pu enregistrer la demande de rappel." next: ivr.handoff.to_agent # ====== Confirm action ====== ivr.appointment.confirm: type: action_then_prompt action: call: ${globals.endpoints.confirm_appointment} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" on_success: prompt: "Votre rendez-vous est confirmé." next: ivr.confirmation on_fail: prompt: "Je n'ai pas pu confirmer. Retour à la liste." next: ivr.appointments.list.upcoming # ====== C5 -> C4 (reschedule) ====== ivr.reschedule.preferences: type: prompt_and_listen prompt: > Pour déplacer votre rendez-vous, nous allons définir vos préférences. Souhaitez-vous le matin ou l'après-midi ? Tapez 1 matin, 2 après-midi. routes: - when_intent: morning set: { pref_time_window: "morning" } next: ivr.reschedule.mode - when_intent: afternoon set: { pref_time_window: "afternoon" } next: ivr.reschedule.mode - when_intent: operator next: ivr.handoff.to_agent fallback: set: { pref_time_window: "morning" } next: ivr.reschedule.mode ivr.reschedule.mode: type: prompt_and_listen prompt: > Préférez-vous une consultation à domicile, en visio, ou au cabinet ? Tapez 1 domicile, 2 visio, 3 cabinet. routes: - when_intent: mode_home set: { pref_mode: "home" } next: ivr.reschedule.date_window - when_intent: mode_video set: { pref_mode: "video" } next: ivr.reschedule.date_window - when_intent: mode_clinic set: { pref_mode: "clinic" } next: ivr.reschedule.date_window fallback: set: { pref_mode: "home" } next: ivr.reschedule.date_window ivr.reschedule.date_window: type: prompt_and_listen prompt: > À partir de quelle date êtes-vous disponible ? Dites par exemple "lundi prochain". Si vous ne savez pas, dites "dès que possible". collect: mode: date_nl # natural language date; orchestrator resolves to ISO dates next: ivr.reschedule.submit_preferences ivr.reschedule.submit_preferences: type: action_then_prompt action: call: ${globals.endpoints.submit_preferences} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: date_from: "{{ctx.pref_date_from}}" date_to: "{{ctx.pref_date_to}}" time_window: "{{ctx.pref_time_window}}" mode: "{{ctx.pref_mode}}" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: "Merci. Je vous propose maintenant des créneaux." next: ivr.slots.fetch on_fail: prompt: "Je n'ai pas pu enregistrer vos préférences. Un conseiller peut vous aider." next: ivr.handoff.to_agent ivr.slots.fetch: type: action_then_prompt action: call: ${globals.endpoints.get_proposals} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" on_success: set: { slots: "{{resp.slots}}" } next: ivr.slots.choose on_fail: prompt: "Je n'ai pas pu récupérer de créneaux. Souhaitez-vous un conseiller ?" next: ivr.handoff.to_agent # ====== C4 ====== ivr.slots.choose: type: prompt_and_listen prompt_from_action: true prompt_template: > Voici trois créneaux. Premier : {{ctx.slots[0].voice_label}}. Deuxième : {{ctx.slots[1].voice_label}}. Troisième : {{ctx.slots[2].voice_label}}. Dites "un", "deux", "trois" ou "aucun". routes: - when_intent: choose_first set: { slot_selected_id: "{{ctx.slots[0].id}}" } next: ivr.slots.select - when_intent: choose_second set: { slot_selected_id: "{{ctx.slots[1].id}}" } next: ivr.slots.select - when_intent: choose_third set: { slot_selected_id: "{{ctx.slots[2].id}}" } next: ivr.slots.select - when_intent: none_of_these next: ivr.slots.none - when_intent: operator next: ivr.handoff.to_agent - when_intent: repeat next: ivr.slots.choose noinput: max: 2 next: ivr.slots.choose nomatch: max: 2 next: ivr.slots.choose ivr.slots.select: type: action_then_prompt action: call: ${globals.endpoints.select_slot} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: slot_id: "{{ctx.slot_selected_id}}" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: "Parfait. Votre rendez-vous est replanifié." next: ivr.confirmation on_fail: prompt: "Je n'ai pas pu valider ce créneau. Essayons à nouveau." next: ivr.slots.fetch ivr.slots.none: type: action_then_prompt action: call: ${globals.endpoints.propose_new_slots} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: reason: "none_of_these" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: "Très bien. Je recherche d'autres créneaux." next: ivr.slots.fetch on_fail: prompt: "Je n'ai pas trouvé d'autres créneaux. Un conseiller peut vous aider." next: ivr.handoff.to_agent # ====== C6 ====== ivr.cancel.reason: type: prompt_and_listen prompt: > Pour quel motif souhaitez-vous annuler ? 1 raison personnelle, 2 santé, 3 indisponibilité, 4 autre. routes: - when_intent: reason_personal set: { cancel_reason: "personal" } next: ivr.cancel.comment - when_intent: reason_health set: { cancel_reason: "health" } next: ivr.cancel.comment - when_intent: reason_unavailable set: { cancel_reason: "unavailable" } next: ivr.cancel.comment - when_intent: reason_other set: { cancel_reason: "other" } next: ivr.cancel.comment - when_intent: back next: ivr.appointment.detail fallback: set: { cancel_reason: "other" } next: ivr.cancel.comment ivr.cancel.comment: type: prompt_and_listen prompt: > Souhaitez-vous ajouter un commentaire ? Dites-le après le bip, ou dites "non". collect: mode: free_speech_or_no next: ivr.cancel.execute ivr.cancel.execute: type: action_then_prompt action: call: ${globals.endpoints.cancel_appointment} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: reason: "{{ctx.cancel_reason}}" comment: "{{ctx.cancel_comment}}" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: > Votre rendez-vous est annulé. Souhaitez-vous demander un nouveau rendez-vous ? Dites oui ou non. next: ivr.cancel.after on_fail: prompt: "Je n'ai pas pu annuler. Un conseiller peut vous aider." next: ivr.handoff.to_agent ivr.cancel.after: type: listen routes: - when_intent: yes next: ivr.reschedule.preferences - when_intent: no next: ivr.goodbye fallback: next: ivr.goodbye # ====== C7 ====== ivr.confirmation: type: prompt_and_listen prompt: > C'est confirmé. Souhaitez-vous l'ajouter à votre agenda, le partager à un proche, ou contacter l'accueil ? Dites "agenda", "partager", ou "accueil". routes: - when_intent: add_to_calendar next: ivr.appointment.send_ics - when_text: "partager" next: ivr.confirmation.share - when_text: "accueil" next: ivr.handoff.to_agent - when_intent: operator next: ivr.handoff.to_agent - when_intent: no next: ivr.goodbye noinput: max: 1 next: ivr.goodbye nomatch: max: 1 next: ivr.goodbye ivr.confirmation.share: type: action_then_prompt action: call: ${globals.endpoints.share_link} headers: Authorization: "Bearer {{ctx.portal_session_token}}" path: id: "{{ctx.appt_selected_id}}" body: channel: "{{ctx.preferred_channel}}" call_id: "{{ctx.call_id}}" session_id: "{{ctx.session_id}}" on_success: prompt: "C'est partagé. Merci et à bientôt." next: ivr.goodbye on_fail: prompt: "Je n'ai pas pu partager. Souhaitez-vous un conseiller ?" next: ivr.handoff.to_agent # ====== Agent zone gate (minimal) ====== ivr.handoff.agent_gate: type: prompt_and_listen prompt: > Accès équipe. Pour continuer, dites "conseiller" pour être mis en relation. Sinon, dites "retour". routes: - when_intent: operator next: ivr.handoff.to_agent - when_intent: back next: ivr.entry fallback: next: ivr.entry ivr.handoff.to_agent: type: handoff enabled: ${globals.handoff.enabled} queue_id: ${globals.handoff.queue_id} prompt: ${globals.handoff.prompt} on_fail: prompt: "Tous nos conseillers sont indisponibles. Vous pouvez recevoir un lien par SMS." next: ivr.portal.send_link # ====== End ====== ivr.goodbye: type: prompt_end prompt: "Merci. Au revoir."
Comment l’exécuter (très concrètement)
Votre orchestrateur doit fournir seulement 5 “capabilités” :
- TTS(prompt) → jouer audio dans l’appel
- STT(listen) + NLU simple → produire intent + slots (ou DTMF)
- collect(mode) → phone/otp/date/free_speech (DTMF autorisé)
- action(call endpoint) → effectuer GET/PUT vers vos endpoints backend (qui eux font Odoo JSON-RPC + Brevo)
- transition(next) → avancer/retry/fallback, en gardant ctx (Redis/Postgres)
Notes de conception (importantes pour que ça marche “comme OBS”)
- Les champs resp.summary_voice et resp.voice_detail sont volontairement côté backend : c’est le backend qui transforme la liste RDV en texte TTS-friendly (dates, heures, lieu) et gère les cas 0/1/N RDV.