Dialogues protocolisés : inquiry et persuasion (Walton–Krabbe)
Carnet fondé sur le carnet de référence du sas EPITA dialogue_protocols (docs/coursia_contrib), qui fait autorité pour son contenu, et sur sa fixture partagée — neuf transitions et sept historiques de verdicts attendus, rangés dans data/dialogue_protocols_examples.json (exemples synthétiques domaine public). L’organe dialogue_protocols.py implémente le modèle ; ce carnet le lit, le met à l’épreuve de la fixture, et mesure ses propriétés propres.
Idée directrice. Un protocole de dialogue est une machine à états sur des actes de langage : chaque coup est un acte parmi neuf (affirmer, questionner, contester, argumenter, concéder, retirer, soutenir, réfuter, comprendre) ; chaque enchaînement est permis ou interdit par une table de transitions ; et la fin du dialogue n’est pas une impression mais une condition testable. Deux protocoles suffisent à couvrir deux natures opposées de l’échange : l’inquiry, coopératif, qui cherche un accord sur les faits, et la persuasion, adversarial, où l’un des deux doit céder.
Statut et provenance. Les trois classes DialogueProtocol / InquiryProtocol / PersuasionProtocol que ce carnet enseigne ont ete retirees du tronc amont par #2137 (2026-09-17). Le tronc EPITA declare mot pour mot : “dead twins — the living workflow path for formal dialogue is the JVM logic/dialogue_handler.py” ; leurs seuls utilisateurs restants y sont “the CoursIA teaching notebooks”. Le module dialogue_protocols.py documente ce statut (lignes 19-32) et assume la distillation comme contenu pedagogique executable sans JVM. Le pendant de production est argumentation_analysis/agents/core/logic/dialogue_handler.py du tronc EPITA (DialogueHandler.execute_dialogue), un pont jpype vers Tweety qui execute un dialogue proponent/opponent reel : fusion des vues du cadre de Dung, extension fondée via SimpleGroundedReasoner, verdict accepted/rejected sur le sujet debattu. Ce carnet n’est donc pas le moteur de la maison : c’est la seule implementation vivante de cette abstraction, sous forme de pure Python stdlib, et il le dit.
Pourquoi protocoliser un dialogue ?
Walton et Krabbe (Commitment in Dialogue, 1995) typent les dialogues par leur but : chercher de l’information, enquêter, persuader, négocier, délibérer, ou simplement s’affronter. Un dialogue laissé libre ne sait dire ni qu’il a mal tourné, ni qu’il est fini : une conversation qui tourne en rond et une conversation qui conclut se ressemblent, vues de l’extérieur, tant qu’aucune règle ne les distingue.
Protocoliser, c’est rendre ces deux jugements mécaniques :
Un coup illégal a une position. La table de transitions dit, pour chaque acte, quels actes peuvent légitimement suivre. Une contestation — « tu n’avais pas le droit de concéder ici » — se tranche en consultant la table, pas en argumentant sur les intentions.
La fin est une condition, pas un sentiment. Compréhension mutuelle, capitulation, impasse, longueur maximale : le protocole énumère ses façons de mourir, et coupe lui-même les dialogues qui tournent en rond.
Le dialogue devient reproductible. Deux traces identiques reçoivent le même verdict : le dialogue devient l’objet d’une expérience, pas seulement d’une conversation.
Le registre Walton–Krabbe déclare six types de dialogue ; deux seulement ont une machine. Ce n’est pas un défaut à corriger mais un état de la recherche : une typologie nomme plus qu’elle ne construit, et l’écart entre les deux est en soi instructif — c’est la première chose que mesure ce carnet.
Note de portee. Le pendant de production d’un dialogue protocolise est DialogueHandler (Tweety/JVM) dans le tronc amont — voir l’encart de la cellule d’introduction pour le statut de cette distillation et le chemin vivant.
import syssys.path.insert(0, ".") # organe dialogue_protocols.py : dossier du carnetfrom dialogue_protocols import ( DialogueType, SpeechAct, DialogueMove, InquiryProtocol, PersuasionProtocol, PROTOCOLS, protocol_counts,)counts = protocol_counts()print("Types Walton-Krabbe au registre :", counts["walton_krabbe_types"])for t in DialogueType:print(" -", t.name)print("Actes de parole formalises :", counts["speech_acts"])print("Protocoles implantes (machine) :", counts["protocols"])print("Transitions inquiry :", counts["inquiry_transitions"])print("Transitions persuasion :", counts["persuasion_transitions"])
Le registre déclare six types — INFORMATION_SEEKING, INQUIRY, PERSUASION, NEGOTIATION, DELIBERATION, ERISTIC — mais seuls inquiry et persuasion ont une machine : quatre types sont des noms sans protocole. Les deux machines sont d’une taille quasi identique (25 transitions contre 26) alors que leurs natures s’opposent : la différence entre coopératif et adversarial n’est pas une question de volume mais de composition — c’est ce que lira le tableau croisé de fin de carnet, ligne par ligne.
Les neuf actes de parole
La pragmatique linguistique classe les actes de langage par ce qu’ils font plutôt que par ce qu’ils disent. Le registre des actes en formalise neuf, que l’on peut grouper en quatre familles :
Famille
Actes
Effet sur l’engagement
Assertifs
CLAIM, SUPPORT
le locuteur s’engage sur un contenu
Directifs
QUESTION, CHALLENGE
le locuteur exige quelque chose de l’interlocuteur — une réponse, une justification
Argumentatifs
ARGUE, REFUTE
le locuteur attaque ou défend un contenu par des raisons
Conclusifs
CONCEDE, RETRACT, UNDERSTAND
le locuteur clôt un point : il cède, retire sa parole, ou déclare avoir compris
La donnée que manipule la machine n’est ni la phrase ni sa syntaxe : c’est l’acte. Deux phrases différentes qui contestent — « pourquoi cela ? » et « sur quoi repose votre chiffre ? » — sont le même coup de CHALLENGE. C’est ce nivellement qui rend l’état d’un dialogue dénombrable : la machine ne compte pas des mots, elle compte des coups.
inq = InquiryProtocol()per = PersuasionProtocol()i =", ".join(a.name for a in inq.get_allowed_responses(SpeechAct.QUESTION))p =", ".join(a.name for a in per.get_allowed_responses(SpeechAct.QUESTION))print("Apres QUESTION, l'inquiry autorise :", i)print("Apres QUESTION, la persuasion autorise :", p)print()print("Actes menant a CONCEDE en inquiry :", [a.name for a in SpeechActif SpeechAct.CONCEDE in inq.allowed_transitions.get(a, [])] or"aucun")print("Actes menant a CONCEDE en persuasion :", [a.name for a in SpeechActif SpeechAct.CONCEDE in per.allowed_transitions.get(a, [])])
Apres QUESTION, l'inquiry autorise : CLAIM, ARGUE, QUESTION, SUPPORT
Apres QUESTION, la persuasion autorise : CLAIM, ARGUE, SUPPORT
Actes menant a CONCEDE en inquiry : aucun
Actes menant a CONCEDE en persuasion : ['CLAIM', 'ARGUE', 'REFUTE']
Lecture : deux tables, deux mondes
Après QUESTION, l’inquiry autorise… QUESTION à nouveau : relancer la clarification est légitime dans une enquête. La persuasion l’exclut — après une question, on répond (CLAIM, ARGUE, SUPPORT), on ne questionne pas.
Plus frappant : CONCEDE n’est atteignable par aucun acte en inquiry — l’acte de capitulation existe dans l’énumération des neuf, mais aucune transition n’y mène : c’est un acte mort dans ce protocole. En persuasion, trois actes y mènent (CLAIM, ARGUE, REFUTE). La capitulation est une porte qui n’existe que dans le monde adversarial.
Le verdict attendu : la fixture partagée
Les tables ci-dessus disent ce que le moteur fait ; la fixture partagée dit ce qu’il doit faire. Neuf transitions et sept historiques y sont consignés avec leur verdict attendu et sa raison — c’est la source de vérité commune du carnet de référence et de la garde aller-retour tests/test_dialogue_protocols.py. Passer les tables au crible de la fixture, c’est séparer la spécification de l’implémentation : si un jour le moteur diverge, c’est ici que le désaccord aura un lieu.
# Les 9 transitions de la fixture : is_valid_move doit rendre le verdict attendu.import json as _jsonfrom pathlib import PathEXAMPLES = _json.loads( Path("data/dialogue_protocols_examples.json").read_text(encoding="utf-8"))protos = {"inquiry": InquiryProtocol(), "persuasion": PersuasionProtocol()}for ex in EXAMPLES["transitions"]: proto = protos[ex["dialogue"]] got = proto.is_valid_move(SpeechAct[ex["from"]], SpeechAct[ex["to"]]) verdict ="autorisé"if got else"interdit" marque ="OK "if got == ex["allowed"] else"ECART"print(f"{marque}{ex['dialogue']:<11}{ex['from']} -> {ex['to']:<11}{verdict}")print(f" {ex['why']}")ok =all( protos[ex["dialogue"]].is_valid_move(SpeechAct[ex["from"]], SpeechAct[ex["to"]]) == ex["allowed"]for ex in EXAMPLES["transitions"])print(f"\n9/9 transitions rendent le verdict attendu : {ok}")
OK inquiry QUESTION -> CLAIM autorisé
En inquiry, une question peut recevoir une affirmation directe.
OK inquiry CLAIM -> CONCEDE interdit
Asymétrie clé : en inquiry on ne concède pas une affirmation — on la soutient, la conteste ou la questionne.
OK inquiry CHALLENGE -> ARGUE autorisé
Une contestation appelle un argument.
OK inquiry UNDERSTAND -> UNDERSTAND autorisé
La compréhension mutuelle peut s'enchaîner.
OK persuasion CLAIM -> CONCEDE autorisé
En persuasion au contraire, concéder une affirmation est la sortie normale — l'objectif du dialogue.
OK persuasion CHALLENGE -> RETRACT autorisé
Contesté, le locuteur peut retirer sa position.
OK persuasion ARGUE -> REFUTE autorisé
Un argument peut être réfuté de front.
OK persuasion QUESTION -> CHALLENGE interdit
Une question n'appelle pas une contestation directe — d'abord une réponse (claim/argue/support).
OK persuasion CONCEDE -> ARGUE interdit
Après une concession, on reformule une position ou on questionne, on ne relance pas un argument.
9/9 transitions rendent le verdict attendu : True
Lecture : la spécification a un fichier
Chaque ligne porte son verdict et sa raison — l’asymétrie la plus instructive est CLAIM -> CONCEDE : interdit en inquiry (« on ne concède pas une affirmation qu’on cherche à établir ensemble »), autorisé en persuasion (« concéder est la fin normale du débat »). Le même couple d’actes, deux mondes : ce n’est pas le moteur qui le décide, c’est la fixture qui l’exige.
# Les 7 historiques de la fixture : is_terminal_state doit rendre le verdict attendu.def historique(acts):return [DialogueMove(speaker=f"loc{i %2}", act=SpeechAct[a], content="-")for i, a inenumerate(acts)]for ex in EXAMPLES["terminations"]: proto = protos[ex["dialogue"]] got = proto.is_terminal_state(historique(ex["acts"])) verdict ="TERMINAL"if got else"en cours" marque ="OK "if got == ex["terminal"] else"ECART"print(f"{marque}{ex['dialogue']:<11}{verdict:<9}{' -> '.join(ex['acts'])}")print(f" {ex['why']}")ok_t =all( protos[ex["dialogue"]].is_terminal_state(historique(ex["acts"])) == ex["terminal"]for ex in EXAMPLES["terminations"])print(f"\n7/7 historiques rendent le verdict attendu : {ok_t}")
OK inquiry TERMINAL UNDERSTAND -> UNDERSTAND -> UNDERSTAND
Trois compréhensions/consécutions consécutives en fin d'historique : dialogue clos.
OK inquiry TERMINAL CLAIM -> QUESTION -> UNDERSTAND -> UNDERSTAND
Deux compréhensions consécutives en position 4 : condition de convergence atteinte.
OK inquiry TERMINAL CLAIM -> SUPPORT -> CLAIM -> SUPPORT -> CLAIM -> SUPPORT
Boucle de motif (claim, support) répétée trois fois : le dialogue tourne en rond, on coupe.
OK inquiry en cours QUESTION -> CLAIM -> SUPPORT -> CHALLENGE -> ARGUE
Historique sain et mixte : aucune condition de terminaison ne s'applique.
OK persuasion TERMINAL CLAIM -> CHALLENGE -> ARGUE -> CONCEDE
Une concession en dernière position termine la persuasion — l'objectif est atteint.
OK persuasion TERMINAL CLAIM -> CHALLENGE -> RETRACT -> RETRACT
Deux retraits consécutifs : plus rien à persuader.
OK persuasion en cours CLAIM -> CHALLENGE -> ARGUE
Persuasion en cours, pas de concession ni retrait final.
7/7 historiques rendent le verdict attendu : True
Lecture : une entrée à double fond
L’historique CHALLENGE -> RETRACT -> RETRACT est consigné TERMINAL par la fixture — et pourtant la table de transitions interdit d’y arriver : seul CHALLENGE mène à RETRACT, et RETRACT ne mène pas à RETRACT. La condition de terminaison _term_double_retract vit donc sur un état que aucun dialogue légal ne peut produire. Ce n’est pas une erreur de la fixture : elle teste l’état, pas la légalité du chemin — et le croisement des deux tables met au jour une propriété du moteur que la section finale mesure en détail.
La trace, unité d’observation
L’objet que manipule le validateur n’est pas le dialogue en train de se dérouler mais sa trace : la liste des coups joués, dans l’ordre. Chaque coup est un DialogueMove — un locuteur, un acte, un contenu libre et une cible optionnelle. La machine à états ne lit que l’acte : le contenu sert à l’humain qui relit la trace, pas au protocole.
validate_trace(moves) rend quatre choses : la validité transition par transition (valid), l’index du premier coup illégal s’il y en a un (first_invalid), la terminaison (terminated) et la condition de terminaison satisfaite (reason). C’est l’instrument unique de ce carnet : chaque expérience qui suit se lit dans ces quatre champs.
def mv(speaker, act, content=""):return DialogueMove(speaker, act, content)trace_inq = [ mv("A", SpeechAct.QUESTION, "Le seuil de 80 % est-il atteint ?"), mv("B", SpeechAct.CLAIM, "Oui, la mesure du jour donne 82 %"), mv("A", SpeechAct.SUPPORT, "J'ajoute : hier on etait deja a 81 %"), mv("B", SpeechAct.UNDERSTAND, "Nos deux mesures se corroborent"), mv("A", SpeechAct.UNDERSTAND, "Compris, nous sommes d'accord sur le fait"),]res = inq.validate_trace(trace_inq)print("Trace :", " -> ".join(m.act.name for m in trace_inq))print("valid :", res["valid"], "| first_invalid :", res["first_invalid"])print("terminated :", res["terminated"], "| reason :", res["reason"])
Les cinq coups sont légitimes (valid: True, first_invalid: None) et le dialogue est terminé par _term_converged : deux UNDERSTAND consécutifs. Personne n’a cédé, personne n’a gagné : les deux parties déclarent avoir compris la même chose. C’est la signature de l’inquiry — l’accord sur les faits, pas la victoire d’une thèse. Noter que la condition ne regarde que la fin de la trace : les deux derniers coups suffisent, le reste de l’historique est indifférent à cette terminaison-là.
s = SpeechActpaires = [("A", s.QUESTION), ("B", s.CLAIM)]trace_loop = [DialogueMove(sp, a) for sp, a in paires *3]res_loop = inq.validate_trace(trace_loop)print("Trace :", " -> ".join(m.act.name for m in trace_loop))print("valid :", res_loop["valid"], "| terminated :", res_loop["terminated"])print("reason :", res_loop["reason"])
valid: Trueetterminated: True : chaque transition est permise, et pourtant le protocole déclare le dialogue terminé (_term_loop). La même paire d’actes (QUESTION, CLAIM) répétée trois fois est irréprochable coup par coup et vide dans son ensemble. C’est la limite de la validité transitionnelle — elle ne voit que les enchaînements locaux — et la raison pour laquelle la terminaison est une couche séparée de la table : détecter qu’un dialogue ne progresse plus demande de regarder la forme globale de la trace, pas seulement ses arêtes.
motif3 = [("A", s.QUESTION), ("B", s.CLAIM), ("A", s.SUPPORT)]trace_p3 = [DialogueMove(sp, a) for sp, a in motif3 *3]acts_p3 = [m.act.name for m in trace_p3]print("Motif de periode 3 :", acts_p3[:3], "repete trois fois")res_p3 = inq.validate_trace(trace_p3)print("valid :", res_p3["valid"], "| terminated :", res_p3["terminated"],"| reason :", res_p3["reason"])
Motif de periode 3 : ['QUESTION', 'CLAIM', 'SUPPORT'] repete trois fois
valid : True | terminated : False | reason : None
Lecture : la limite mesurée du détecteur
Le motif (QUESTION, CLAIM, SUPPORT) répété trois fois passe : terminated: False, aucune raison de terminaison. Neuf coups qui tournent en rond traversent le filtre, alors que six coups en période 2 étaient coupés juste avant. La cause est dans la définition du détecteur : il compare les paires adjacentes des six derniers coups — recent[0:2] == recent[2:4] == recent[4:6] — et une période 3 ne fait jamais coïncider ces paires. C’est la divergence 3 du port, mesurée ici sur une trace exécutée ; l’exercice 1 fait écrire le détecteur de période 3 qui manque.
Persuasion : le même formalisme, une autre nature
Le passage de l’inquiry à la persuasion ne change ni le formalisme ni l’instrument : mêmes neuf actes, même validate_trace. Ce qui change est la table — et à travers elle, la nature de l’échange. L’inquiry est coopératif : deux parties construisent un accord sur les faits, et personne ne « gagne ». La persuasion est adversariale : deux thèses s’affrontent, et le dialogue se conclut par une capitulation — CONCEDE — ou par épuisement.
Où se lit l’adversarialité ? Dans les transitions elles-mêmes. REFUTE est vivant en persuasion (on peut attaquer une thèse, et l’attaque appelle une riposte) ; il est quasi muet en inquiry (on demande, on clarifie, on ne détruit pas). RETRACT n’existe qu’en persuasion : dans une enquête on ne retire pas sa parole, on la précise. La table de transitions est donc une théorie du dialogue encodée en machine — les expériences qui suivent la lisent coup par coup.
trace_per = [ mv("A", SpeechAct.CLAIM, "La dessalinisation est la meilleure option pour la ville"), mv("B", SpeechAct.CHALLENGE, "Pourquoi celle-la plutot que le recyclage ?"), mv("A", SpeechAct.ARGUE, "Cout par m3 le plus bas a l'echelle de nos besoins"), mv("B", SpeechAct.CONCEDE, "Soit : a cette echelle, j'accepte l'argument"),]res_per = per.validate_trace(trace_per)print("Trace :", " -> ".join(m.act.name for m in trace_per))print("valid :", res_per["valid"], "| terminated :", res_per["terminated"])print("reason :", res_per["reason"])
reason: _term_concede — le dernier coup est une capitulation, et cela suffit. Comparée à la trace d’inquiry (conclure par UNDERSTAND), la différence tient en un acte : le même formalisme, un CONCEDE à la place d’un UNDERSTAND, et le sens de la fin change — victoire d’une thèse contre accord sur un fait. Aucune comptabilité de score n’est nécessaire : la condition de terminaison est le verdict.
menent_a_retract = [a.name for a in SpeechActif SpeechAct.RETRACT in per.allowed_transitions.get(a, [])]successeurs_retract = [a.name for a in per.allowed_transitions[SpeechAct.RETRACT]]print("Actes menant a RETRACT (persuasion) :", menent_a_retract)print("Successeurs de RETRACT :", successeurs_retract)impasse = [DialogueMove("A", SpeechAct.CHALLENGE), DialogueMove("B", SpeechAct.RETRACT, "Je retire ma these"), DialogueMove("A", SpeechAct.RETRACT, "Moi aussi")]res_imp = per.validate_trace(impasse)print()print("Trace CHALLENGE -> RETRACT -> RETRACT :")print("valid :", res_imp["valid"], "| first_invalid :", res_imp["first_invalid"])print("is_terminal_state :", per.is_terminal_state(impasse))
La trace CHALLENGE -> RETRACT -> RETRACT est invalide à l’index 2 (RETRACT n’admet que CLAIM et QUESTION), et pourtant is_terminal_state rend True : la condition _term_double_retract est satisfaite par une trace qu’aucun dialogue légal ne peut produire. En effet, seul CHALLENGE mène à RETRACT, et RETRACT ne mène pas à RETRACT : deux retraits consécutifs sont un état interdit par la table. La condition vit dans le code et reste morte dans le dialogue — c’est la divergence 5 du port, découverte en écrivant cette démonstration. Leçon de spécification : une machine à états se vérifie aussi du côté de ses conditions de terminaison — chacune doit correspondre à un état atteignable par les transitions permises.
trace_bad = [mv("A", SpeechAct.REFUTE, "Votre mesure est biaisee par la saison"), mv("B", SpeechAct.CONCEDE, "Vous avez raison")]res_bad = inq.validate_trace(trace_bad)print("Trace REFUTE -> CONCEDE en inquiry :")print("valid :", res_bad["valid"], "| first_invalid :", res_bad["first_invalid"])print("REFUTE n'admet que :", [a.name for a in inq.get_allowed_responses(SpeechAct.REFUTE)])print("Meme trace en persuasion : valid =", per.validate_trace(trace_bad)["valid"], "(REFUTE -> CONCEDE y est permise)")
Trace REFUTE -> CONCEDE en inquiry :
valid : False | first_invalid : 1
REFUTE n'admet que : ['ARGUE', 'QUESTION']
Meme trace en persuasion : valid = True (REFUTE -> CONCEDE y est permise)
Lecture : illégal relativement à quoi ?
REFUTE -> CONCEDE est rejeté à l’index 1 en inquiry : une réfutation appelle des raisons (ARGUE) ou une précision (QUESTION), jamais une capitulation. La même trace, exécutée sur la table de persuasion, est légale : là, REFUTE admet explicitement CONCEDE — détruire un argument peut faire céder l’adversaire. Un coup n’est donc pas illégal dans l’absolu : il est illégal relativement à un protocole. C’est tout ce que « protocoliser » achète : la contestation « tu n’avais pas le droit de conclure ici » reçoit une réponse mécanique, tranchée par la table et localisée à l’index près.
print(f"{'acte':<12}| {'inquiry':<40}| {'persuasion'}")print("-"*100)for act in SpeechAct: i =",".join(a.name for a in inq.get_allowed_responses(act)) or"(muet)" p =",".join(a.name for a in per.get_allowed_responses(act)) or"(muet)"print(f"{act.name:<12}| {i:<40}| {p}")
Quatre différences structurelles sautent du tableau :
RETRACT est muet en inquiry, vivant en persuasion — dans une enquête on précise sa parole, on ne la retire pas ; dans un affrontement, se rétracter est un coup tactique.
UNDERSTAND boucle sur lui-même en inquiry (UNDERSTAND est dans ses propres successeurs — c’est le mécanisme exact de _term_converged), pas en persuasion où il ne mène qu’à CLAIM ou QUESTION : comprendre est une fin en enquête, un simple tour de passe en dispute.
SUPPORT ouvre sur UNDERSTAND en persuasion mais sur QUESTION en inquiry : soutenir y appelle une réaction immédiate, ici une clarification.
CONCEDE est la cible de trois actes en persuasion, d’aucun en inquiry — la porte de sortie existe à chaque étage du monde adversarial, et n’existe nulle part dans le monde coopératif.
La nature d’un dialogue n’est donc pas un attribut déclaratif : c’est la composition de sa table — et les colonnes ci-dessus en sont la radiographie.
Jonction avec les assets voisins : le schéma d’un argument du dialogue
Le protocole échange des actes ; l’argument lui-même a une structure. Deux assets voisins de la série la portent : le classifieur de schémas de Walton (argumentation_schemes.py, carnet 01b) reconnaît, sur son texte, le schéma d’un argument — appel à l’expert, exemple, cause… — et la base de connaissances (knowledge_base.py, carnet 04b) structure prémisses, conclusion et schéma dans un FormalArgument. Le carnet de référence joint les trois niveaux : un argument du dialogue dont le champ scheme est peuplé par le classifieur.
Le pendant formel de l’echange lui-meme — pas seulement de l’argument unitaire — est DialogueHandler (Tweety/JVM) cote production : la trace d’un dialogue execute se materialise par un verdict accepted/ rejected sur le sujet debattu, et c’est ce qui rend le scheme ci-dessus non seulement descriptible mais decidable.
# Un FormalArgument dont le schéma est peuplé par le classifieur Walton (asset 01b).from argumentation_schemes import classify_schemefrom knowledge_base import FormalArgument, PropositionTEXTE ="Selon un chercheur spécialiste du domaine, la méthode est fiable."scheme = classify_scheme(TEXTE)assert scheme isnotNoneand scheme.key =="expert_opinion", schemeargument = FormalArgument( premises=[Proposition(content="Le chercheur est spécialiste du domaine", confidence=0.9)], conclusion=Proposition(content="La méthode est fiable", confidence=0.8), scheme=scheme.key,)print(f"Argument : {argument}")print(f"Schéma : {scheme.key} (force a priori {scheme.strength:.2f})")print(f"Identifiant: {argument.id[:8]}...")
Argument : [Le chercheur est spécialiste du domaine] -> La méthode est fiable
Schéma : expert_opinion (force a priori 0.80)
Identifiant: b62f390e...
Lecture : trois niveaux, une seule chaîne
Le classifieur rend un ArgumentationScheme (expert_opinion, force a priori), la base range l’argument ([premisses] -> conclusion) et le protocole, lui, dira quand cet argument peut être posé (CLAIM), soutenu (SUPPORT) ou réfuté (REFUTE). Le champ scheme est la clé de jonction : c’est elle qu’une démonstration automatique lirait pour relier le tour de parole à la théorie de l’argumentation qu’il instancie.
Propriétés mesurées du moteur
Le moteur est volontairement documenté plutôt que corrigé : ses angles morts sont mesurables, et chacun enseigne ce qu’un protocole bien spécifié doit vérifier.
Six types déclarés, deux machines. Le registre Walton–Krabbe énumère six types de dialogue ; seuls INQUIRY et PERSUASION ont un protocole implémenté. Une typologie nomme plus qu’elle ne construit — l’écart est en soi instructif.
La boucle de période 3 n’est pas vue. Le détecteur de boucle compare les paires d’actes adjacentes sur les six derniers coups : un motif de période 2 répété est coupé, un motif de période 3 traverse le filtre (démonstration plus haut, exercice 1).
Limites de longueur asymétriques. 25 coups pour l’inquiry, 30 pour la persuasion, sans justification documentée — portées telles quelles.
La condition _term_double_retract est inatteignable. Elle teste deux RETRACT consécutifs — un état que la table de transitions interdit : seul CHALLENGE mène à RETRACT, et RETRACT ne mène pas à RETRACT. La fixture partagée le met en évidence sans le nommer : elle consigne l’historique CHALLENGE -> RETRACT -> RETRACT comme TERMINAL (la condition tire sur l’état), alors qu’aucun dialogue légal ne peut l’atteindre (la table interdit le chemin). Condition vive sur l’état, morte par la table : du code que seule la croisée des deux spécifications fait voir.
Documenter plutôt que corriger est ici une décision d’ingénierie : l’essence d’un moteur de protocole, ce sont aussi ses angles morts. Ils enseignent ce qu’un protocole bien spécifié doit vérifier — ici, que chaque condition de terminaison corresponde à un état réellement atteignable par les transitions permises.
Exercice 1 — le détecteur de période 3
Le détecteur de boucle du protocole ne voit que les répétitions de paires d’actes (période 2). Écrire detect_period3_loop(history) qui rend True si et seulement si les neuf derniers coups forment trois répétitions d’un même triplet d’actes, et False sinon.
Témoins attendus :
la trace trace_p3 de la démonstration ci-dessus doit rendre True ;
la trace trace_loop (période 2 : (QUESTION, CLAIM) répété) doit rendre False ;
une trace de moins de neuf coups doit rendre False.
Indications : extraire les neuf derniers actes dans une liste motif, puis comparer le triplet de tête aux deux suivants — motif[0:3] == motif[3:6] == motif[6:9].
def detect_period3_loop(history):# TODO etudiant : rendre True ssi les 9 derniers coups forment un motif de# periode 3 repete trois fois, False sinon.# Etape 1 : si moins de 9 coups, rendre False.# Etape 2 : extraire les 9 derniers actes dans une liste motif.# Etape 3 : rendre motif[0:3] == motif[3:6] == motif[6:9].returnNone# TODO etudiantprint("Exercice 1 a completer")
Exercice 1 a completer
Exercice 2 — une persuasion qui ne meurt jamais (presque)
Construire une trace légale de persuasion de 31 coups qui atteint la condition _term_length (plus de 30 coups) sans jamais satisfaire ni _term_concede ni _term_double_retract : autrement dit, un dialogue où personne ne cède et personne ne retire deux fois de suite, jusqu’à épuisement du temps imparti.
La fonction trace_persuasion_longue() doit renvoyer la liste des DialogueMove. Le témoin est validate_trace : valid doit valoir True et reason doit valoir _term_length.
Indication : il existe un cycle de quatre actes légal en persuasion, par exemple CHALLENGE -> SUPPORT -> QUESTION -> CLAIM -> CHALLENGE -> ... — vérifier chaque arête avec is_valid_move avant d’écrire la boucle.
def trace_persuasion_longue():# TODO etudiant : construire 31 DialogueMove formant une trace legale de# persuasion, terminee uniquement par la limite de longueur.# Etape 1 : choisir un cycle d'actes dont chaque transition est valide.# Etape 2 : generer 31 coups en alternant les locuteurs A et B.# Etape 3 : verifier avec PersuasionProtocol().validate_trace.returnNone# TODO etudiantprint("Exercice 2 a completer")
Exercice 2 a completer
Exercice 3 — qui meurt, et comment ?
Simuler 200 dialogues par protocole et compter les causes de mort. Deux locuteurs A et B alternent ; chaque coup est tiré au hasard (générateur seedé) parmi les réponses permises au dernier acte ; le dialogue démarre sur QUESTION (inquiry) ou CLAIM (persuasion) et s’arrête dès que is_terminal_state est vrai. La fonction simuler_dialogues(protocole, n, seed) doit renvoyer un dictionnaire {raison_de_terminaison: compte}.
Questions auxquelles la distribution répondra : l’inquiry meurt-elle plutôt par compréhension mutuelle ou par longueur ? La persuasion cède-t-elle (_term_concede) plus souvent qu’elle ne s’épuise (_term_length) ? La réponse n’est pas intuitive — c’est le propre d’une expérience : on la lance pour savoir, pas pour confirmer.
import randomdef simuler_dialogues(protocole, n=200, seed=0):# TODO etudiant : simuler n dialogues et compter les raisons de terminaison.# Etape 1 : pour chaque dialogue i, tirer les coups avec random.Random(seed + i).# Etape 2 : demarrer sur QUESTION (inquiry) ou CLAIM (persuasion).# Etape 3 : chaque coup suivant est rng.choice(get_allowed_responses(acte)).# Etape 4 : arreter des que is_terminal_state(trace) est vrai ; consigner# la raison dans un dictionnaire {raison: compte}.returnNone# TODO etudiantprint("Exercice 3 a completer")
Exercice 3 a completer
Conclusion — la procédure est un objet d’étude
Ce carnet a lu deux protocoles de dialogue comme ce qu’ils sont : des machines à états sur des actes de langage. Trois choses à retenir :
La table de transitions EST la théorie. Coopératif ou adversarial ne se décrivent pas en adjectifs : REFUTE vivant ou muet, RETRACT présent ou absent, CONCEDE cible de trois actes ou d’aucun — la nature du dialogue se lit dans la table, coup par coup.
La terminaison se spécifie, sinon elle se décrète. Le protocole coupe les dialogues qui tournent en rond et énumère ses morts ; la divergence 5 enseigne la réciproque — une condition de terminaison écrite pour un état que la table interdit est du code mort, qu’on ne découvre qu’en cherchant à le déclencher.
Le formalisme est léger, les conséquences ne le sont pas. Neuf actes et une vingtaine de transitions suffisent à rendre une contestation tranchable, une terminaison garantie et une expérience reproductible.
Dans la série, ce carnet complète ses voisins : les notebooks de Toulmin structurent l’argument unitaire, les Agentic orchestrent le travail collectif d’agents, la gouvernance multi-agents décide collectivement — ici, c’est l’échange lui-même qui est procéduralisé, acte par acte. La question « comment parle-t-on ? » précède la question « que conclut-on ? » : le protocole est ce qui rend la seconde posée honnêtement.
Statut et perenite. Les trois machines que ce carnet enseigne sont un snapshot du retrait tronc #2137 (2026-09-17) ; le chemin de production est logic/dialogue_handler.py (Tweety/JVM). La distillation est assumee comme contenu pedagogique executable sans JVM — sa portee est l’enseignement de la machine a etats, pas l’execution de dialogue reel. Le present carnet est l’un des derniers lieux ou ce code mort-vivant est maintenu ; tant qu’il existe comme organe d’enseignement, les divergences documentees (periode 3, _term_double_retract inatteignable, asymetrie 25/30) restent visibles aux eleves qui etudient la theorie des protocoles.