ISNAD Scientific Platform

Documentation Center
V1 · Référence officielle

Architecture

Live AI Vocal Hadith Agent — spécification d'architecture Realtime Voice

14 min de lectureMis à jour le 29 octobre 2026
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 :

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 :

  1. Le Gateway est la seule autorité scientifique. Toute affirmation dicible provient d'un tool_result du Gateway, avec son evidence_status et sa provenance. Le modèle ne peut pas fabriquer une source, un identifiant, un statut ou une citation absents du tool_result.
  2. 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.
  3. 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#

CoucheConnaîtNe connaît pasResponsabilités
Browsermicro, audio de sortie, affichage transcript + sourcesclés DB, credentials permanentscapture audio, rendu de la réponse vocale et de la carte « sources » ; barge-in utilisateur
Realtime Provideraudio, protocole propriétaire, schémas des 8 tools (injectés par l'Adapter)schéma DB, SQL, IDs canoniques hors tool_resulttranscription, sélection d'intention/tool, génération de la réponse vocale
Provider Adapterprotocole fournisseur (events, format tool call/result, annulation, session éphémère)schéma DB, logique métier ISNADtraduire tool request ↔ appel Gateway normalisé ; gérer cycle de vie session ; propager annulations ; filtrer sur l'allow-list
Tool Gatewaycontrat des 8 tools, evidence_status, provenance, caps, codes d'erreurfournisseur, WebRTC, protocole vocalvalider les arguments (schémas fermés), imposer les caps, exécuter le handler read-only, composer citations + evidence_status, retourner un DTO stable
Read AdapterRPC/vues/tables publiques allow-listéesintention, contexte conversationnelexécuter la lecture bornée, aucune écriture, aucune primitive .from(table) exposée
Supabasedonnées, RLS, grantstout le restesource 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#

ChampPortéeRôleAutorité
conversation_iddurable (multi-session possible)regroupe l'échange complet pour audit scientifiquebackend ISNAD
session_idune connexion Realtimecorrèle une connexion WebRTC / une session providerbackend ISNAD (pas le fournisseur)
provider_session_refune connexion Realtimeréférence opaque côté fournisseur (token éphémère, id de session provider)fournisseur, détenu uniquement par le Provider Adapter
turn_idun tour de dialogueunité d'observabilité et de fraîcheur (voir §6)backend ISNAD
languagesession, révisable par tourlangue courante de l'utilisateur (fr | ar | auto)détecté à partir de la transcription, révisable
current_hadith_idcontexte conversationneldernier hadith retourné par un tool_result et confirmé comme focusdérivé de tool_result, jamais du modèle
current_narrator_idcontexte conversationneldernier narrateur retourné par un tool_result et confirmé comme focusdé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_id de contexte n'entre dans un appel tool que s'il provient d'un tool_result anté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 erreur UNAVAILABLE sans 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: false conceptuellement).
  • 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_hadiths default 10, get_narrator_hadiths hard 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_id ou narrator_id non issu d'un tool_result ou 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_evidence ou 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é.

ArtefactProduit parNaturePeut être reformulé ?
spoken_answermodèle (provider)texte synthétisé en voix, en langue utilisateurOui — reformulation libre
transcriptprovider (STT) + modèle (réponse)transcription mot à mot des deux locuteursNon
tool_callsProvider Adapter[{ turn_id, tool_name, arguments, ts }]Non
tool_resultsGatewayDTO stables retournés, avec evidence_status, truncated, capability_noteNon
sourcesGatewayagrégat des provenance[] réellement présents dans les tool_resultsNon
evidence_statusGatewaystatut agrégé du tour : fact | inference | insufficient_evidenceNon

4.1 Règles#

  • La carte « sources » de l'UI est alimentée uniquement par tool_results[].provenance. Le texte du spoken_answer n'y contribue jamais.
  • Le modèle ne peut pas ajouter une source. Si spoken_answer cite un ouvrage absent des provenance, 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_status du tour = le plus faible des evidence_status des affirmations dicibles. Un tour qui mélange un fact (texte du hadith) et une inference (résultat de recherche) est présenté au niveau inference pour la partie concernée.
  • Reformulation encadrée. Le modèle peut raccourcir, traduire, expliquer — mais toute proposition scientifique de spoken_answer doit être traçable à un tool_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#

CasComportement
Entrée françaisetranscription 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 arabetranscription AR ; query_language: "ar". Chemin lexical arabe disponible (exact / FTS / fuzzy).
Changement de langue en cours de conversationlanguage 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 ARla 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_translations FR PUBLISHED) porte son evidence_status et sa provenance.
  • 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_result n'est utilisable que s'il porte le turn_id du tour actif au moment où il revient.

  • Chaque tour a un turn_id monotone. À l'ouverture d'un nouveau tour, active_turn_id est incrémenté.
  • Le Provider Adapter tague chaque tool call avec le turn_id courant.
  • Quand un tool_result revient : si result.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 response de 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 un tool_result du 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 conceptuelleSortie conceptuelleNotes
connect sessioncredentials éphémères (créés par le backend, §8)session_id, provider_session_refouvre la session Realtime ; ne reçoit jamais de clé permanente
configure toolsles 8 schémas de tools (format neutre)acquittementl'Adapter traduit les 8 schémas neutres vers le format function-calling du fournisseur
receive tool requestevent fournisseur (tool call){ tool_name, arguments, turn_id } normalisérejette tout tool_name hors allow-list
submit tool resultDTO Gateway + turn_idevent fournisseur (tool result)n'envoie que si turn_id == active_turn_id
cancel responseturn_id à annuleracquittementdéclenché par barge-in ou nouveau tour
close sessionsession_idacquittementlibè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_id permettent 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.

CasDétectionComportement attendu
microphone deniedpermission navigateur refuséemessage clair à l'utilisateur ; aucune session Realtime ouverte
provider unavailableéchec connect session ou event d'erreur fournisseurerreur UNAVAILABLE ; proposer un nouvel essai ; ne pas basculer silencieusement sur une réponse « de connaissance générale »
transcription failureSTT vide / faible confiancedemander à l'utilisateur de répéter ; ne pas deviner l'intention
tool timeoutGateway/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 unavailabletool 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 resultplusieurs candidats canoniques, résolution impossiblecode AMBIGUOUS ; le modèle demande une désambiguïsation ; current_*_id reste null
insufficient evidencetool retourne insufficient_evidence ou liste videle modèle dit qu'ISNAD ne permet pas de répondre ; il ne comble pas avec sa connaissance générale
connection lostWebRTC / 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_role pour contourner RLS. Rôle anon (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 :

  1. Le flux cible §1 est respecté : Browser → Provider → Provider Adapter → Gateway → Read Adapter → Supabase.
  2. L'Adapter implémente les six responsabilités de RealtimeProviderAdapter (§7.1) et reste la seule couche dépendante du fournisseur.
  3. Les 8 tools sont injectés depuis un schéma neutre ; le Gateway n'est pas modifié.
  4. Le modèle de contexte (§2) fournit des IDs issus de tool_results, jamais du modèle, et ne franchit pas conversation_id.
  5. Le cycle de vie tool call (§3) applique l'allow-list avant le Gateway et la validation avant l'exécution.
  6. La séparation spoken_answer / tool_results / sources / evidence_status (§4) est matérialisée dans l'enregistrement de tour.
  7. La stratégie FR/AR (§5) distingue traduction ISNAD et traduction modèle.
  8. La règle de fraîcheur turn_id (§6.2) écarte tout stale result.
  9. Aucun secret permanent ne franchit la frontière navigateur (§10).
  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