Architecture
Live AI Vocal Hadith Agent — spécification d'architecture Realtime Voice
Sur cette page
Statut : spécification documentaire uniquement (TASK 6A). Aucun code runtime, aucune migration, aucune modification Gateway / Supabase / frontend.
Date : 2026-08-30
Branche : feature/live-agent-realtime-arch
Documents de référence :
docs/architecture/live-agent-tool-contract.md— contrat public des huit tools read-only.planning/audits/live-agent-db-capabilities-audit.md— audit des capacités de lecture DB.
Objet : définir précisément comment brancher ultérieurement un fournisseur Realtime Voice au Gateway ISNAD, sans perdre l'indépendance fournisseur et sans que le contexte conversationnel ne devienne jamais une preuve scientifique.
Portée de ce document : conception. Il doit être assez précis pour que TASK 6B implémente un premier Provider Adapter sans redessiner l'architecture, mais il ne contient ni implémentation WebRTC, ni SDK fournisseur, ni conception UI détaillée.
0. Principe fondamental#
L'IA est une couche d'interrogation et d'orchestration. Elle n'est jamais une source scientifique.
Trois invariants découlent de ce principe et gouvernent toute la spécification :
- Le Gateway est la seule autorité scientifique. Toute affirmation dicible provient d'un
tool_resultdu Gateway, avec sonevidence_statuset saprovenance. Le modèle ne peut pas fabriquer une source, un identifiant, un statut ou une citation absents dutool_result. - Indépendance fournisseur. Le Gateway ne connaît ni WebRTC, ni OpenAI Realtime, ni aucun protocole propriétaire de function calling. Une seule couche — le Provider Adapter — traduit le protocole du fournisseur vers le contrat neutre des huit tools.
- Le contexte conversationnel est une commodité d'ergonomie, pas une preuve. Il permet de résoudre « donne-moi sa chaîne » en un
hadith_id, mais il n'ajoute jamais de valeur probante à une réponse.
1. Flux cible#
Browser (micro / haut-parleur)
│ WebRTC (audio bidirectionnel + canal de données fournisseur)
▼
Realtime Provider (modèle vocal + transcription + function calling)
│ tool request (format propriétaire)
▼
Provider Adapter ← SEULE couche qui connaît le protocole fournisseur
│ appel normalisé : { tool_name ∈ allow-list, arguments, turn_id, session_id }
▼
ISNAD Tool Gateway ← fournisseur-agnostique ; valide, borne, qualifie la preuve
│ handler read-only par tool (8 handlers nommés)
▼
Read Adapter ← requêtes/RPC/vues allow-listées, aucun SQL libre
│ rôle client read-only (anon ou rôle DB dédié), RLS active
▼
Supabase (READ ONLY)
1.1 Répartition des responsabilités par couche#
| Couche | Connaît | Ne connaît pas | Responsabilités |
|---|---|---|---|
| Browser | micro, audio de sortie, affichage transcript + sources | clés DB, credentials permanents | capture audio, rendu de la réponse vocale et de la carte « sources » ; barge-in utilisateur |
| Realtime Provider | audio, protocole propriétaire, schémas des 8 tools (injectés par l'Adapter) | schéma DB, SQL, IDs canoniques hors tool_result | transcription, sélection d'intention/tool, génération de la réponse vocale |
| Provider Adapter | protocole fournisseur (events, format tool call/result, annulation, session éphémère) | schéma DB, logique métier ISNAD | traduire tool request ↔ appel Gateway normalisé ; gérer cycle de vie session ; propager annulations ; filtrer sur l'allow-list |
| Tool Gateway | contrat des 8 tools, evidence_status, provenance, caps, codes d'erreur | fournisseur, WebRTC, protocole vocal | valider les arguments (schémas fermés), imposer les caps, exécuter le handler read-only, composer citations + evidence_status, retourner un DTO stable |
| Read Adapter | RPC/vues/tables publiques allow-listées | intention, contexte conversationnel | exécuter la lecture bornée, aucune écriture, aucune primitive .from(table) exposée |
| Supabase | données, RLS, grants | tout le reste | source de données read-only |
1.2 Règle d'or de la frontière#
Le Gateway reçoit : un nom de tool appartenant à l'allow-list, un objet d'arguments JSON, et des identifiants de corrélation (session_id, turn_id).
Le Gateway ne reçoit jamais : du SQL, un nom de table, un nom de RPC libre, une interface repository comportant une méthode d'écriture, une clé service_role, un prompt, un extrait de transcript audio.
2. Session et contexte conversationnel#
2.1 Identifiants conceptuels#
| Champ | Portée | Rôle | Autorité |
|---|---|---|---|
conversation_id | durable (multi-session possible) | regroupe l'échange complet pour audit scientifique | backend ISNAD |
session_id | une connexion Realtime | corrèle une connexion WebRTC / une session provider | backend ISNAD (pas le fournisseur) |
provider_session_ref | une connexion Realtime | référence opaque côté fournisseur (token éphémère, id de session provider) | fournisseur, détenu uniquement par le Provider Adapter |
turn_id | un tour de dialogue | unité d'observabilité et de fraîcheur (voir §6) | backend ISNAD |
language | session, révisable par tour | langue courante de l'utilisateur (fr | ar | auto) | détecté à partir de la transcription, révisable |
current_hadith_id | contexte conversationnel | dernier hadith retourné par un tool_result et confirmé comme focus | dérivé de tool_result, jamais du modèle |
current_narrator_id | contexte conversationnel | dernier narrateur retourné par un tool_result et confirmé comme focus | dérivé de tool_result, jamais du modèle |
2.2 Cycle référentiel attendu#
Tour 1 — utilisateur : « Trouve le hadith sur l'intention. »
search_hadith({ query: "إنما الأعمال بالنيات", query_language: "auto", limit: 5 })
→ results[0].hadith_id = 12345 (evidence_status: "inference", provenance: [...])
Le Gateway retourne la liste. Le backend met à jour current_hadith_id = 12345 seulement si la résolution est non ambiguë (un seul candidat fortement scoré, ou choix explicite de l'utilisateur au tour suivant). En cas d'ambiguïté, current_hadith_id reste null et le modèle doit demander une désambiguïsation.
Tour 2 — utilisateur : « Donne-moi sa chaîne. »
get_hadith_chains({ hadith_id: current_hadith_id }) // 12345 injecté depuis le contexte
2.3 Règles de contexte#
- Le contexte ne fournit que des identifiants, jamais du contenu scientifique réutilisable comme preuve.
- Un
hadith_id/narrator_idde contexte n'entre dans un appel tool que s'il provient d'untool_resultantérieur de cette conversation. - Le contexte est révisable et effaçable : un nouveau
search_*remplace le focus ; une question hors sujet le laisse inchangé ; « oublie ça » le vide. - Le contexte ne survit jamais à
conversation_id. Aucune persistance de focus entre deux conversations distinctes. - Le contexte conversationnel ne devient jamais une preuve scientifique. Si l'utilisateur demande « et c'est authentique ? », la réponse vient d'un nouvel appel tool, pas d'une mémoire du tour précédent.
3. Cycle de vie d'un tool call#
1. speech (utilisateur)
2. provider transcription → texte + language détectée
3. intent / tool selection (modèle, borné à l'allow-list des 8 tools)
4. tool arguments (modèle produit un objet JSON)
5. Provider Adapter → normalise : { tool_name, arguments, session_id, turn_id }
rejette tout nom hors allow-list AVANT le Gateway
6. Gateway validation → schéma fermé, types, domaines, caps ; sinon { code, message }
7. Gateway exécution → handler read-only → Read Adapter → Supabase
8. Gateway result → DTO stable : données + evidence_status + provenance + truncated
9. Provider Adapter → soumet le result au fournisseur dans son format
10. provider response generation (modèle reformule en langage naturel)
11. speech response (assistant) + mise à jour carte « sources » côté UI (voir §4)
3.1 Contraintes impératives#
- Allow-list stricte. Le fournisseur ne peut appeler que :
search_hadith,get_hadith,get_hadith_chains,search_narrator,get_narrator_profile,get_narrator_hadiths,get_transmission_relation,get_hadith_commentary. Tout autre nom → l'Adapter renvoie une erreurUNAVAILABLEsans toucher le Gateway. - Aucun SQL. Aucun paramètre de tool n'accepte de fragment SQL, nom de table, nom de colonne libre ou nom de RPC. Les schémas d'entrée sont fermés (
additionalProperties: falseconceptuellement). - Validation avant exécution. Requête vide,
limit> hard max, type incorrect, id non entier → erreur normalisée (§ C du contrat) avant toute lecture DB. - Caps rappelés côté Gateway même si le RPC borne déjà :
default limit 5,hard max 20(exceptions publiques :get_narrator_profile/get_narrator_hadithsdefault 10,get_narrator_hadithshard max 50). - Un tool call = une lecture bornée. Pas de boucle d'appels en cascade non contrôlée dans un seul tour ; le modèle peut enchaîner des tools, mais chaque appel est validé et compté indépendamment.
- Idempotence de lecture. Tous les tools sont des lectures ; un ré-appel avec les mêmes arguments est sûr (utile pour retry après
UNAVAILABLE).
3.2 Ce que le modèle ne peut pas faire#
- Inventer un
hadith_idounarrator_idnon issu d'untool_resultou du contexte. - Transformer un
evidence_status: "inference"en"fact". - Présenter un score vectoriel/lexical comme une preuve.
- Répondre une donnée « de mémoire générale » quand le tool retourne
insufficient_evidenceou une liste vide (voir §9).
4. Preuve et réponse : séparation stricte#
Chaque tour produit des artefacts conservés séparément. L'audio peut reformuler ; l'interface visuelle et le journal d'audit conservent ce que le Gateway a réellement retourné.
| Artefact | Produit par | Nature | Peut être reformulé ? |
|---|---|---|---|
spoken_answer | modèle (provider) | texte synthétisé en voix, en langue utilisateur | Oui — reformulation libre |
transcript | provider (STT) + modèle (réponse) | transcription mot à mot des deux locuteurs | Non |
tool_calls | Provider Adapter | [{ turn_id, tool_name, arguments, ts }] | Non |
tool_results | Gateway | DTO stables retournés, avec evidence_status, truncated, capability_note | Non |
sources | Gateway | agrégat des provenance[] réellement présents dans les tool_results | Non |
evidence_status | Gateway | statut agrégé du tour : fact | inference | insufficient_evidence | Non |
4.1 Règles#
- La carte « sources » de l'UI est alimentée uniquement par
tool_results[].provenance. Le texte duspoken_answern'y contribue jamais. - Le modèle ne peut pas ajouter une source. Si
spoken_answercite un ouvrage absent desprovenance, c'est un défaut à détecter (voir §8, contrôle de cohérence) — la source n'est pas affichée et le tour est marqué suspect. evidence_statusdu tour = le plus faible desevidence_statusdes affirmations dicibles. Un tour qui mélange unfact(texte du hadith) et uneinference(résultat de recherche) est présenté au niveauinferencepour la partie concernée.- Reformulation encadrée. Le modèle peut raccourcir, traduire, expliquer — mais toute proposition scientifique de
spoken_answerdoit être traçable à untool_result. Les gloses générales (« ce hadith est très connu ») sont autorisées seulement si elles ne portent pas de statut scientifique. - Absence de preuve ≠ preuve du contraire. Voir §9.
4.2 Forme d'enregistrement par tour (conceptuelle)#
{
"conversation_id": "…",
"turn_id": "…",
"language": "fr",
"user_transcript": "Donne-moi sa chaîne.",
"resolved_context": { "current_hadith_id": 12345 },
"tool_calls": [{ "tool_name": "get_hadith_chains", "arguments": { "hadith_id": 12345 } }],
"tool_results": [{ "…": "DTO Gateway complet, non modifié" }],
"sources": [{ "object_type": "chain", "source_title": "…", "locator": "…" }],
"evidence_status": "fact",
"spoken_answer": "La chaîne principale remonte par …",
"coherence_check": "PASS"
}
5. Français / Arabe#
5.1 Stratégie d'entrée#
| Cas | Comportement |
|---|---|
| Entrée française | transcription FR ; query_language: "fr" ou "auto". search_hadith FR est PARTIAL (matn FR ≈ 0 %) → si non couvert, le tool retourne insufficient_evidence + capability_note, jamais un résultat inventé. Le modèle peut proposer de reformuler la requête en arabe. |
| Entrée arabe | transcription AR ; query_language: "ar". Chemin lexical arabe disponible (exact / FTS / fuzzy). |
| Changement de langue en cours de conversation | language est réévaluée à chaque tour à partir de la transcription. Le contexte (current_hadith_id, …) est conservé ; seule la langue de sortie change. |
| Requête mixte FR + citation AR | la portion arabe est passée telle quelle comme query ; query_language: "auto". |
5.2 Texte arabe#
- Le texte arabe retourné par un
tool_result(text_ar,mention_ar,transmission_term_ar,name_ar) est conservé tel quel, sans translittération forcée. - Une translittération n'est affichée que si elle provient d'ISNAD (
public_transliteration_lexicon,*_translit) et elle est marquée comme telle. - Le modèle peut lire à voix haute le texte arabe même dans une réponse française.
5.3 Traduction#
- Aucun moteur de traduction scientifique n'est conçu ici.
- Une traduction présente dans ISNAD (
text_fr,commentary_translationsFRPUBLISHED) porte sonevidence_statuset saprovenance. - Une traduction générée par le modèle est explicitement signalée comme telle (« traduction non officielle, générée automatiquement »), ne porte jamais
evidence_status: "fact", et n'apparaît pas dans la carte « sources ». - La distinction traduction-ISNAD / traduction-modèle est obligatoire à l'oral comme à l'écran.
5.4 Langue de sortie#
- Réponse vocale dans la langue de l'utilisateur lorsque c'est possible.
- Les citations restent dans leur langue d'origine (un matn arabe est cité en arabe même dans une réponse française).
6. Interruption / barge-in#
6.1 Événements à prévoir conceptuellement#
user interruption / barge-in → l'utilisateur parle pendant que l'assistant parle
assistant speech cancellation → arrêt immédiat de la synthèse vocale en cours
new turn → nouveau turn_id ouvert
tool call already running → un tool call du tour précédent n'est pas terminé
stale result → son résultat arrive après l'ouverture du nouveau tour
6.2 Règle simple de fraîcheur#
Un
tool_resultn'est utilisable que s'il porte leturn_iddu tour actif au moment où il revient.
- Chaque tour a un
turn_idmonotone. À l'ouverture d'un nouveau tour,active_turn_idest incrémenté. - Le Provider Adapter tague chaque tool call avec le
turn_idcourant. - Quand un
tool_resultrevient : siresult.turn_id != active_turn_id→ le résultat est journalisé pour audit puis ignoré. Il n'est jamais soumis au modèle, jamais affiché, jamais utilisé pour mettre à jour le contexte. - Un tool call en cours lors d'un barge-in est annulé côté fournisseur (via
cancel responsede l'Adapter, §7). Le Gateway étant read-only, un appel déjà parti peut se terminer côté DB sans effet de bord ; son résultat est simplement écarté par la règle ci-dessus. - Le contexte (
current_hadith_id, …) n'est mis à jour que par untool_resultdu tour actif.
6.3 Non-goal#
Aucune implémentation de la file d'annulation, du buffering audio ou de la reprise de synthèse. Seul le modèle de fraîcheur est spécifié ici.
7. Provider abstraction#
7.1 Interface conceptuelle RealtimeProviderAdapter#
À ne pas coder dans TASK 6A. Décrite ici pour que TASK 6B implémente un premier adapter sans redessiner l'architecture.
| Responsabilité | Entrée conceptuelle | Sortie conceptuelle | Notes |
|---|---|---|---|
connect session | credentials éphémères (créés par le backend, §8) | session_id, provider_session_ref | ouvre la session Realtime ; ne reçoit jamais de clé permanente |
configure tools | les 8 schémas de tools (format neutre) | acquittement | l'Adapter traduit les 8 schémas neutres vers le format function-calling du fournisseur |
receive tool request | event fournisseur (tool call) | { tool_name, arguments, turn_id } normalisé | rejette tout tool_name hors allow-list |
submit tool result | DTO Gateway + turn_id | event fournisseur (tool result) | n'envoie que si turn_id == active_turn_id |
cancel response | turn_id à annuler | acquittement | déclenché par barge-in ou nouveau tour |
close session | session_id | acquittement | libère la session ; invalide provider_session_ref |
7.2 Contraintes d'abstraction#
- Une seule implémentation par fournisseur (
OpenAIRealtimeAdapter,FutureVendorAdapter, …). Le reste d'ISNAD dépend de l'interface, jamais d'un fournisseur concret. - L'Adapter est la seule couche autorisée à importer un SDK fournisseur ou à connaître les noms d'events propriétaires.
- Le mapping des codes d'erreur fournisseur → codes du contrat (§ C :
INVALID_ARGUMENT,NOT_FOUND,AMBIGUOUS,INSUFFICIENT_EVIDENCE,LIMIT_EXCEEDED,UNAVAILABLE,INTERNAL_ERROR) est de la responsabilité de l'Adapter. - Le Gateway et les handlers ne changent pas d'une ligne quand on ajoute un fournisseur.
- Le format de function calling (noms, schémas JSON, enveloppe de résultat) est neutre côté Gateway ; l'Adapter fait la traduction bidirectionnelle.
8. Observabilité#
8.1 Journal technique minimal (par tool call)#
conversation_id
turn_id
tool_name
tool_arguments (redigés : pas de secret ; arguments courts conservés tels quels)
execution_time_ms
result_count
truncated
error ({ code, message } normalisé, ou null)
model / provider (identifiant du modèle vocal + nom du fournisseur)
timestamp
8.2 Journal d'audit scientifique (par tour)#
question / user_transcript
tools utilisés (liste de tool_name)
sources retournées (agrégat des provenance[] des tool_results)
evidence_status (statut agrégé du tour)
coherence_check (PASS / SUSPECT — cohérence spoken_answer ↔ tool_results, §4.1)
8.3 Règles#
- Ne pas enregistrer l'audio brut par défaut. L'audio n'est conservé que sur activation explicite et temporaire (débogage), avec durée de rétention bornée, hors périmètre de ce document.
- Ne pas journaliser les vecteurs, ni les textes complets de matn/commentaire au-delà du nécessaire.
- Les messages d'erreur journalisés respectent le contrat : aucune stack trace, aucun nom de table, aucun SQL.
conversation_id+turn_idpermettent de reconstituer a posteriori : ce qui a été demandé, quels tools ont répondu, quelles sources ont été retournées, et si la réponse vocale est restée cohérente avec ces sources.- La politique de cache et de rétention des logs vocaux est un pré-requis production distinct (cf. gaps de l'audit DB) et n'est pas spécifiée ici.
9. Failure modes#
Documentation des cas essentiels uniquement.
| Cas | Détection | Comportement attendu |
|---|---|---|
microphone denied | permission navigateur refusée | message clair à l'utilisateur ; aucune session Realtime ouverte |
provider unavailable | échec connect session ou event d'erreur fournisseur | erreur UNAVAILABLE ; proposer un nouvel essai ; ne pas basculer silencieusement sur une réponse « de connaissance générale » |
transcription failure | STT vide / faible confiance | demander à l'utilisateur de répéter ; ne pas deviner l'intention |
tool timeout | Gateway/Read Adapter dépasse le budget (responsabilité future du Gateway) | erreur UNAVAILABLE remontée au modèle ; réponse vocale honnête (« je n'ai pas pu interroger la base ») |
tool unavailable | tool PARTIAL sur un chemin non couvert (ex. search_hadith FR/sémantique) | insufficient_evidence + capability_note ; le modèle explique la limite, propose une reformulation |
ambiguous result | plusieurs candidats canoniques, résolution impossible | code AMBIGUOUS ; le modèle demande une désambiguïsation ; current_*_id reste null |
insufficient evidence | tool retourne insufficient_evidence ou liste vide | le modèle dit qu'ISNAD ne permet pas de répondre ; il ne comble pas avec sa connaissance générale |
connection lost | WebRTC / canal fournisseur coupé | fin de session propre ; le contexte de conversation_id peut être repris sur reconnexion, les tool calls en vol sont écartés (§6.2) |
9.1 Règle scientifique impérative sur les erreurs#
En cas d'erreur, d'absence de données ou de capacité manquante, le modèle ne remplace jamais l'absence de résultat par une réponse issue de sa connaissance générale. Il énonce la limite et s'arrête, ou propose une autre requête.
10. Sécurité#
10.1 Ce que le navigateur ne reçoit jamais#
- Supabase
service_role. - Mot de passe de base de données.
- Toute credential DB privée.
- Toute clé API permanente d'un fournisseur Realtime.
10.2 Credential éphémère fournisseur#
Si le fournisseur Realtime exige un token client pour établir la session WebRTC :
- Le backend crée uniquement un credential / session éphémère (durée de vie courte, portée minimale).
- Ce token est transmis au navigateur juste pour ouvrir la session, puis n'est plus réutilisable.
- La clé API permanente du fournisseur reste exclusivement côté backend, jamais exposée au client ni au modèle.
10.3 Frontière Gateway (rappel de l'audit DB)#
- Le modèle n'obtient jamais une primitive
.from(table), ni SQL, ni RPC arbitraire : uniquement les 8 fonctions nommées à schémas d'entrée fermés. - Aucune clé
service_rolepour contourner RLS. Rôleanon(surface publique existante) ou, à terme, rôle DB dédié read-only. - Le Gateway borne IDs, tableaux, longueurs de texte, page/offset et limites, même si un RPC borne déjà certains paramètres.
- Les vues propriétaires (
commentary_hadith_passages,public_narrator_assertions,narrator_canonical_display, …) bypassent la RLS de leurs tables de base par conception : leur projection/filtre ne doit pas être élargi sans revue de frontière de sécurité. - Le Provider Adapter n'a pas d'accès DB ; il ne parle qu'au Gateway.
11. Non-goals (TASK 6A)#
Explicitement hors périmètre de cette spécification et de cette tâche :
- Implémentation WebRTC.
- Intégration d'un SDK fournisseur (OpenAI Realtime ou autre).
- Code du microphone / capture audio frontend.
- Enregistrement audio.
- Sélection de voix / TTS.
- Moteur de traduction (scientifique ou général).
- Migration DB, nouveau RPC, nouvel objet DB.
- Implémentation du Gateway et des handlers.
- Conception UI détaillée (au-delà du principe « carte sources alimentée par les
tool_results»). - Recherche vectorielle / sémantique (conditionnelle, cf. audit DB §8 et §14).
12. Critères d'acceptation pour TASK 6B#
TASK 6B (premier Provider Adapter) peut démarrer sans redessiner l'architecture si :
- Le flux cible §1 est respecté : Browser → Provider → Provider Adapter → Gateway → Read Adapter → Supabase.
- L'Adapter implémente les six responsabilités de
RealtimeProviderAdapter(§7.1) et reste la seule couche dépendante du fournisseur. - Les 8 tools sont injectés depuis un schéma neutre ; le Gateway n'est pas modifié.
- Le modèle de contexte (§2) fournit des IDs issus de
tool_results, jamais du modèle, et ne franchit pasconversation_id. - Le cycle de vie tool call (§3) applique l'allow-list avant le Gateway et la validation avant l'exécution.
- La séparation
spoken_answer/tool_results/sources/evidence_status(§4) est matérialisée dans l'enregistrement de tour. - La stratégie FR/AR (§5) distingue traduction ISNAD et traduction modèle.
- La règle de fraîcheur
turn_id(§6.2) écarte toutstale result. - Aucun secret permanent ne franchit la frontière navigateur (§10).
- Les deux journaux d'observabilité (§8) sont émis.
13. Rapport final#
BRANCH: feature/live-agent-realtime-arch
COMMIT: (voir git log — commit unique : docs/architecture/live-agent-realtime-architecture.md)
PUSH: à pousser sur origin/feature/live-agent-realtime-arch
REALTIME FLOW: PASS (§1)
PROVIDER ABSTRACTION: PASS (§7)
CONVERSATION CONTEXT: PASS (§2)
TOOL CALL LIFECYCLE: PASS (§3)
FR/AR: PASS (§5)
SOURCE SEPARATION: PASS (§4)
SECURITY: PASS (§10)
OBSERVABILITY: PASS (§8)
INTERRUPTION MODEL: PASS (§6)
DB CHANGES: NONE
RUNTIME CODE: NONE
FILES CHANGED: docs/architecture/live-agent-realtime-architecture.md
NEXT: TASK 6B (premier Provider Adapter) après readiness du Gateway.
VERDICT: PASS