Dialogues Multi-Agents Argumentatifs (twin C#)

Ce notebook est le jumeau C# (.NET Interactive) du notebook Python Tweety-08-Agent-Dialogues-Python.ipynb. Il réimplémente from-scratch (BCL .NET, 0 NuGet, 0 dépendance externe) les concepts des dialogues argumentatifs multi-agents tels que modélisés par TweetyProject, sans recourir à la machine virtuelle Java ni à jpype.

Objectifs pédagogiques

  1. Modéliser un framework de Dung (arguments, attaques, sémantique grounded) en C# pur.
  2. Construire des agents argumentatifs dotés d’une base de connaissances et d’un commitment store.
  3. Implémenter un protocole de dialogue (tours de parole, locutions : Claim / Argue / Concede / Retract, condition d’arrêt).
  4. Simuler une loterie argumentative (probabilités sur les arguments, fonction d’utilité, décision d’un agent rationnel).

Pourquoi un twin from-scratch ?

Le notebook Python original explore l’API Java TweetyProject via jpype/JVM (org.tweetyproject.arg.dung.*, agents.dialogues.lotteries.*). Cette approche est inaccessible côté .NET (la DLL Tweety .NET via IKVM est défectueuse au niveau cluster). Le twin C# reconstruit donc les concepts — pas l’API — pour les rendre observables, testables et exécutables end-to-end sur toute machine .NET 9+. Le value-add (EPIC #4956 Prong B) est la transparence : chaque mécanisme (propagation d’attaques, calcul de l’extension grounded, terminaison d’un dialogue) est visible dans le code C#, alors que la JVM les masque.

Prérequis

.NET 9.0+ (.NET Interactive kernel csharp). Aucune librairie externe. Familiarité avec le framework de Dung (cf. Tweety-5-Abstract-Argumentation-Csharp.ipynb).

Setup et utilitaires d’affichage

En exécution headless (papermill / nbconvert), Console.WriteLine est parfois avalé. On définit un helper Show qui passe par display (API .NET Interactive, avec repli Console.WriteLine si l’affichage interactif n’est pas disponible), et un formatteur tabulaire léger.

// --- Setup : utilitaires d'affichage headless-safe ---
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

// Helper d'affichage : en headless, Console.WriteLine peut etre avale.
// On utilise display() (extension .NET Interactive) si dispo, sinon Console.
static void Show(object o)
{
    try { display(o); } catch { Console.WriteLine(o); }
}
static void Show(string label, object o) => Show($"{label}: {o}");

// Formateur de tableau compact (specifique a ce notebook).
static string Table(IEnumerable<string> headers, IEnumerable<IEnumerable<string>> rows)
{
    var allRows = rows.ToList();
    var widths = headers.Select((h, i) =>
        Math.Max(h.Length, allRows.Count > 0 ? allRows.Max(r => r.Count() > i ? r.ElementAt(i).Length : 0) : 0)).ToList();
    var sb = new StringBuilder();
    sb.AppendLine(string.Join(" | ", headers.Select((h, i) => h.PadRight(widths[i]))));
    sb.AppendLine(string.Join("-+-", widths.Select(w => new string('-', w))));
    foreach (var row in allRows)
        sb.AppendLine(string.Join(" | ", row.Select((c, i) => c.PadRight(i < widths.Count ? widths[i] : c.Length))));
    return sb.ToString();
}

Show("Setup", "OK — utilitaires charges (BCL .NET, 0 NuGet).");
Setup: OK — utilitaires charges (BCL .NET, 0 NuGet).

Partie 1 — Framework de Dung from-scratch

On rappelle le modèle canonique de Dung (1995) : un abstract argumentation framework est un couple \(\langle \mathcal{A}, \mathcal{R} \rangle\) où \(\mathcal{A}\) est un ensemble d’arguments et \(\mathcal{R} \subseteq \mathcal{A} \times \mathcal{A}\) est une relation d’attaque. Un argument \(a\) défait \(b\) si \((a, b) \in \mathcal{R}\).

La sémantique grounded est l’unique extension \(E\) minimale pour l’inclusion telle que : - \(E\) est sans conflit (aucun argument de \(E\) n’en attaque un autre), - \(E\) défait tout argument qui en attaque un membre (\(a\) attaqué par \(b \Rightarrow \exists c \in E, (c, b) \in \mathcal{R}\)), - \(E\) contient tout argument accepté par défaut (non attaqué, ou dont tous les attaquants sont défaits par \(E\)).

On la calcule par point fixe : \(E_0 = \{a \mid a \text{ non attaqué}\}\), puis \(E_{k+1} = E_k \cup \{a \mid \forall b \text{ attaquant } a, \exists c \in E_k, (c, b) \in \mathcal{R}\}\).

// --- Framework de Dung from-scratch (cf. Tweety-5-Csharp) ---
public sealed class Argument
{
    public string Name { get; }
    public string Claim { get; }
    public Argument(string name, string claim = "") { Name = name; Claim = claim; }
    public override string ToString() => Claim.Length > 0 ? $"{Name}({Claim})" : Name;
    public override int GetHashCode() => Name.GetHashCode();
    public override bool Equals(object o) => o is Argument a && a.Name == Name;
}

public sealed class DungTheory
{
    public HashSet<Argument> Arguments { get; } = new();
    // attackers[X] = { arguments qui attaquent X }
    public Dictionary<Argument, HashSet<Argument>> Attackers { get; } = new();
    // attacked[X] = { arguments attaques par X }
    public Dictionary<Argument, HashSet<Argument>> Attacked { get; } = new();

    public void Add(Argument a)
    {
        Arguments.Add(a);
        if (!Attackers.ContainsKey(a)) Attackers[a] = new();
        if (!Attacked.ContainsKey(a)) Attacked[a] = new();
    }
    public void Add(Argument attacker, Argument target)
    {
        Add(attacker); Add(target);
        Attackers[target].Add(attacker);
        Attacked[attacker].Add(target);
    }
    public IEnumerable<Argument> AttackersOf(Argument a) => Attackers.TryGetValue(a, out var s) ? s : Enumerable.Empty<Argument>();

    // Extension grounded par point fixe (Dung 1995, Theorem 25).
    public HashSet<Argument> GroundedExtension()
    {
        var E = new HashSet<Argument>();
        // E0 = arguments non attaques
        foreach (var a in Arguments)
            if (Attackers[a].Count == 0) E.Add(a);
        // point fixe : ajouter tout argument dont tous les attaquants sont defaits par E
        bool changed = true;
        while (changed)
        {
            changed = false;
            foreach (var a in Arguments)
            {
                if (E.Contains(a)) continue;
                // un attaquant est "defait" ssi un de SES attaquants est dans E
                // (E.Contains(att) serait faux : un attaquant accepte n'est PAS defait)
                bool allAttackersDefeated = Attackers[a].All(att => AttackersOf(att).Any(E.Contains));
                if (allAttackersDefeated && Attackers[a].Count > 0) { E.Add(a); changed = true; }
            }
        }
        return E;
    }

    public string Describe()
    {
        var sb = new StringBuilder();
        sb.AppendLine($"AF: {Arguments.Count} arguments, {Attackers.Values.SelectMany(x => x).Count()} attaques");
        foreach (var a in Arguments)
        {
            var atk = Attackers[a];
            sb.AppendLine(atk.Count == 0 ? $"  {a} : (non attaque)" : $"  {a} <= {string.Join(", ", atk)}");
        }
        return sb.ToString();
    }
}

// --- Demonstration : le classique A->B, A<-C, C<-D ---
var af = new DungTheory();
var A = new Argument("A", "Le projet est rentable");
var B = new Argument("B", "Le projet est trop risque");
var C = new Argument("C", "Les risques sont maitrises");
var D = new Argument("D", "etude de faisabilite positive");
af.Add(A, B);   // A attaque B (la rentabilite defait "trop risque")
af.Add(C, A);   // C attaque A (risques maitrises defait la rentabilite ? contre-exemple)
af.Add(D, C);   // D attaque C (l'etude defait "risques maitrises")
// Recomposons pour un exemple grounded propre : D non-attaque, D->C, C->A, A->B
af = new DungTheory();
af.Add(A, B); af.Add(C, A); af.Add(D, C);
// D non-attaque => grounded commence par D, defait C, qui defait A => A ne peut pas entrer
Show("AF", af.Describe());
Show("Extension grounded", string.Join(", ", af.GroundedExtension()));
// Attendu : {D} (D defait C, donc C exclu ; sans C, A est attaquant ? non, A attaque B, A n'est pas defait car C est defait -> A indefait mais attaquant par C defait => A entre. Re-calculons.)
AF: AF: 4 arguments, 3 attaques
  A(Le projet est rentable) <= C(Les risques sont maitrises)
  B(Le projet est trop risque) <= A(Le projet est rentable)
  C(Les risques sont maitrises) <= D(etude de faisabilite positive)
  D(etude de faisabilite positive) : (non attaque)
Extension grounded: D(etude de faisabilite positive), A(Le projet est rentable)

Lecture de l’extension grounded. La sortie distingue acceptés (D, A) et rejetés implicitement (B, C attaqués hors de l’extension). La sémantique grounded de Dung est la plus prudente : elle part de l’ensemble vide et n’accepte un argument que s’il survit à toutes les attaques non réfutées — construction par point fixe (d’abord les arguments sans attaquant, puis ceux qu’ils défendent, etc.). Deux lectures du verdict affiché :

  • sceptique par construction : tout ce que grounded accepte, toutes les autres sémantiques (préférée, stable) l’acceptent aussi — c’est le plancher commun ;
  • rejet ≠ faux : C n’est pas « faux », il est non défendable dans cette théorie — changer une seule attaque peut le réhabiliter — les Scénarios de la Partie 4 examinent ce qu’un enrichissement d’AF change (et ne change pas).

Partie 2 — Modèle d’agent argumentatif

Un agent dans un dialogue argumentatif possède : - une base de connaissances \(KB_a \subseteq \mathcal{A}\) (les arguments qu’il connaît / soutient), - un commitment store \(CS_a\) : les propositions qu’il a engagées publiquement durant le dialogue (Hamblin 1970). Un agent rationnel cherche la cohérence interne de son \(CS\).

L’agent peut, à son tour de parole, produire des locutions. On adopte un sous-ensemble du formalisme de Parsons & McBurney : Claim(p), Argue(a) (où \(a\) est un argument soutenant une claim), Concede(p) (accepter une claim adverse), Retract(p) (retirer un engagement).

// --- Modele d'agent argumentatif ---
public enum LocutionKind { Claim, Argue, Concede, Retract }

public readonly struct Locution
{
    public LocutionKind Kind { get; }
    public Argument Arg { get; }
    public string Proposition { get; }   // pour Claim/Concede/Retract
    public string Speaker { get; }
    public Locution(LocutionKind k, Argument a, string prop, string speaker)
    { Kind = k; Arg = a; Proposition = prop; Speaker = speaker; }
    public override string ToString()
    {
        return Kind switch
        {
            LocutionKind.Claim   => $"{Speaker}: Claim({Proposition})",
            LocutionKind.Argue   => $"{Speaker}: Argue({Arg})",
            LocutionKind.Concede => $"{Speaker}: Concede({Proposition})",
            LocutionKind.Retract => $"{Speaker}: Retract({Proposition})",
            _ => $"{Speaker}: ?"
        };
    }
}

public sealed class ArgAgent
{
    public string Name { get; }
    public HashSet<Argument> Knowledge { get; }       // KB_a
    public HashSet<string> CommitmentStore { get; } = new();  // CS_a (propositions)
    public ArgAgent(string name, IEnumerable<Argument> kb) { Name = name; Knowledge = new(kb); }

    // Un agent soutient une proposition si un de ses arguments (non defait dans KB) la couvre.
    public bool Supports(string proposition, DungTheory localAf)
    {
        var grounded = localAf.GroundedExtension();
        return Knowledge.Any(a => a.Claim == proposition && grounded.Contains(a));
    }
}

// --- Deux agents avec KB divergentes sur le theme "teletravail" ---
var t = new Argument("T", "le_teletravail_augmente_la_productivite");
var ct = new Argument("CT", "le_teletravail_isole_socialement");
var e = new Argument("E", "une_etude_montre_+13pct_productivite");
var s = new Argument("S", "le_presentiel_favorise_la_cohesion");

// AF global partage
var afT = new DungTheory();
afT.Add(e, ct);   // l'etude defait "isolemment social" ? non : reformulons proprement.
// AF coherent : E soutient T ; CT attaque T ; S soutient CT ; (rien n'attaque E ni S)
afT = new DungTheory();
afT.Add(e, ct);   // E (productivite) attaque CT (isolement) — position favorable au teletravail
afT.Add(ct, t);   // CT attaque T
// (E et S non attaques)
Show("AF teletravail", afT.Describe());

var proAgent = new ArgAgent("Pro-TL", new[] { e, t });
var antiAgent = new ArgAgent("Anti-TL", new[] { ct });
Show(proAgent.Name + " KB", string.Join(", ", proAgent.Knowledge));
Show(antiAgent.Name + " KB", string.Join(", ", antiAgent.Knowledge));
AF teletravail: AF: 3 arguments, 2 attaques
  E(une_etude_montre_+13pct_productivite) : (non attaque)
  CT(le_teletravail_isole_socialement) <= E(une_etude_montre_+13pct_productivite)
  T(le_teletravail_augmente_la_productivite) <= CT(le_teletravail_isole_socialement)
Pro-TL KB: E(une_etude_montre_+13pct_productivite), T(le_teletravail_augmente_la_productivite)
Anti-TL KB: CT(le_teletravail_isole_socialement)

Ce qui fait un agent « argumentatif ». Le modèle ci-dessus tient en trois états : une base de connaissances (ce que l’agent croit), un stock d’engagements (ce qu’il a dit — et dont il devra répondre), et les locutions (Claim, Argue, Concede, Retract) comme seuls gestes de parole. La distinction croire/dit est la clé : un agent peut être forcé de Retract un Claim attaqué sans renier sa KB — il reconnaît avoir perdu ce point-ci, pas la guerre. C’est ce qui distingue un dialogue argumentatif d’un échange d’opinions : chaque locution crée une obligation vérifiable (justifier un Claim si on l’attaque), et le protocole peut rejeter un coup illégal — la rationalité est procédurale, pas supposée.

Partie 3 — Protocole de dialogue (turn-based)

Le protocole régit les tours de parole. On implémente une variante du jeu de persuasion grounded (Prakken, 2005 ; Cayrol et Lagasquie-Schiex) :

  1. Le Proponent ouvre avec Claim(p).
  2. L’Opponent peut Argue(a) où \(a\) attaque la claim, ou Concede(p).
  3. Le Proponent riposte par Argue(b) où \(b\) défait \(a\), ou Retract(p).
  4. Terminaison : l’Opponent Concede, ou bien aucun argument n’est plus disponible (stalemate), ou bien on atteint un plafond de tours.

La validité d’une locution Argue(a) requiert que \(a\) soit dans le \(KB\) du locuteur et que \(a\) ne soit pas déjà défaite dans l’AF courant par un argument déjà avancé par l’adversaire.

// --- Protocole de dialogue grounded (variante persuasion) ---
public sealed class DialogueGame
{
    public DungTheory Af { get; }
    public ArgAgent Proponent { get; }
    public ArgAgent Opponent { get; }
    public List<Locution> Trace { get; } = new();
    public int MaxTurns { get; }
    public string Topic { get; }

    public DialogueGame(DungTheory af, ArgAgent pro, ArgAgent opp, string topic, int maxTurns = 10)
    { Af = af; Proponent = pro; Opponent = opp; Topic = topic; MaxTurns = maxTurns; }

    // Un argument de speaker est-il "jouable" ? (dans KB, pas deja avance par speaker,
    // et non defait par un argument deja avance par l'adversaire).
    private bool IsPlayable(ArgAgent speaker, ArgAgent adversary, Argument a)
    {
        // deja avance ?
        if (Trace.Any(l => l.Kind == LocutionKind.Argue && l.Arg.Equals(a) && l.Speaker == speaker.Name))
            return false;
        // defait par un argument adverse deja avance ?
        if (Af.Attackers.TryGetValue(a, out var atkrs))
            if (atkrs.Any(x => Trace.Any(l => l.Kind == LocutionKind.Argue && l.Arg.Equals(x) && l.Speaker == adversary.Name)))
                return false;
        return true;
    }

    public enum Outcome { ProponentWins, OpponentWins, Stalemate }

    public (Outcome, string) Run()
    {
        // Tour 0 : Proponent ouvre
        Trace.Add(new Locution(LocutionKind.Claim, null, Topic, Proponent.Name));
        Proponent.CommitmentStore.Add(Topic);

        for (int turn = 1; turn <= MaxTurns; turn++)
        {
            // Opponent cherche un argument (jouable) attaquant un argument du Proponent KB
            Argument oppAttack = Opponent.Knowledge.FirstOrDefault(oa =>
                Af.Attacked.TryGetValue(oa, out var tgts)
                && tgts.Any(t => Proponent.Knowledge.Contains(t))
                && IsPlayable(Opponent, Proponent, oa));

            if (oppAttack == null)
            {
                Trace.Add(new Locution(LocutionKind.Concede, null, Topic, Opponent.Name));
                return (Outcome.ProponentWins, $"Opponent concede '{Topic}' au tour {turn} (aucun contre-argument jouable).");
            }
            Trace.Add(new Locution(LocutionKind.Argue, oppAttack, null, Opponent.Name));

            // Proponent riposte : argument (jouable) defaisant oppAttack
            Argument proReply = Proponent.Knowledge.FirstOrDefault(pa =>
                Af.Attacked.TryGetValue(pa, out var tgts2)
                && tgts2.Contains(oppAttack)
                && IsPlayable(Proponent, Opponent, pa));
            if (proReply == null)
            {
                Trace.Add(new Locution(LocutionKind.Retract, null, Topic, Proponent.Name));
                return (Outcome.OpponentWins, $"Proponent retire '{Topic}' au tour {turn} (riposte impossible).");
            }
            Trace.Add(new Locution(LocutionKind.Argue, proReply, null, Proponent.Name));
        }
        return (Outcome.Stalemate, $"Stalemate : plafond de {MaxTurns} tours atteint sans resolution.");
    }
}

// --- Lancement du dialogue teletravail ---
var game = new DialogueGame(afT, proAgent, antiAgent, "le_teletravail_est_souhaitable");
var (outcome, why) = game.Run();
Show("Trace du dialogue", string.Join("\n", game.Trace.Select(l => "  " + l)));
Show("Verdict", $"{outcome} — {why}");
Trace du dialogue:   Pro-TL: Claim(le_teletravail_est_souhaitable)
  Anti-TL: Argue(CT(le_teletravail_isole_socialement))
  Pro-TL: Argue(E(une_etude_montre_+13pct_productivite))
  Anti-TL: Concede(le_teletravail_est_souhaitable)
Verdict: ProponentWins — Opponent concede 'le_teletravail_est_souhaitable' au tour 2 (aucun contre-argument jouable).

Lecture de la trace — pourquoi Pro-TL gagne. Le verdict ProponentWins vient d’un événement précis : Concede(le_teletravail_est_souhaitable) au tour 2. Le protocole ne dit pas comment gagner (il autorise les coups légaux), la stratégie de l’agent (quelle locution jouer) décide. Ici, Anti-TL n’a pas su produire de contre-argument inattaquable — son unique attaque est contrée dans la KB adverse. Deux observations :

  • le résultat du dialogue coïncide avec l’acceptation grounded de la thèse dans l’AF sous-jacent — ce n’est pas un hasard : un dialogue bien formé est une preuve interactive de statut accepté ;
  • un protocole sans règle de terminaison peut boucler (Claim/Retract indéfinis) — la limite de tours du framework est une garde, pas un détail.

Partie 4 — Scénarios

Scénario 1 : Négociation immobilière

Un acheteur soutient qu’une maison est un bon achat (Claim) ; le vendeur doit défendre. On modélise le conflit informel par un AF puis on lance le protocole.

// --- Scenario 1 : negociation immobilire ---
var buy = new Argument("BUY", "bon_achat_prix_juste");
var overpriced = new Argument("OVR", "prix_haute_vs_marche");
var comp = new Argument("COMP", "comparables_bas");      // soutient OVR
var upgrade = new Argument("UPG", "travaux_prevus_deductibles"); // defait OVR
var af1 = new DungTheory();
af1.Add(overpriced, buy);
af1.Add(comp, buy);       // les comparables bas defaient "bon achat"
af1.Add(upgrade, overpriced);
// UPG non-attaque => grounded contient UPG, qui defait OVR. BUY reste attaque par COMP.
Show("AF negociation", af1.Describe());
Show("Grounded", string.Join(", ", af1.GroundedExtension()));

var buyer = new ArgAgent("Acheteur", new[] { buy, upgrade });
var seller = new ArgAgent("Vendeur", new[] { comp });
var g1 = new DialogueGame(af1, buyer, seller, "la_maison_est_un_bon_achat");
var (o1, w1) = g1.Run();
Show("Trace negociation", string.Join("\n", g1.Trace.Select(l => "  " + l)));
Show("Verdict negociation", $"{o1} — {w1}");
AF negociation: AF: 4 arguments, 3 attaques
  OVR(prix_haute_vs_marche) <= UPG(travaux_prevus_deductibles)
  BUY(bon_achat_prix_juste) <= OVR(prix_haute_vs_marche), COMP(comparables_bas)
  COMP(comparables_bas) : (non attaque)
  UPG(travaux_prevus_deductibles) : (non attaque)
Grounded: COMP(comparables_bas), UPG(travaux_prevus_deductibles)
Trace negociation:   Acheteur: Claim(la_maison_est_un_bon_achat)
  Vendeur: Argue(COMP(comparables_bas))
  Acheteur: Retract(la_maison_est_un_bon_achat)
Verdict negociation: OpponentWins — Proponent retire 'la_maison_est_un_bon_achat' au tour 1 (riposte impossible).

Lecture du scénario d’achat. L’extension grounded {COMP, UPG} dit tout : UPG(travaux_prevus_deductibles) défait bien OVR(prix_haute_vs_marche) — l’acheteur a une bonne carte — mais COMP(comparables_bas) n’est attaqué par rien : la carte du vendeur est inattaquable. D’où la trace : Vendeur: Argue(COMP) au tour 1, Retract immédiat de l’acheteur — riposte impossible, verdict OpponentWins. Leçon : défendre une claim exige de répondre à chaque attaque ; un bon argument contre une objection (UPG vs OVR) ne sauve pas la claim tant qu’une autre objection (COMP) reste debout. Les contre-offres et concessions conditionnelles d’une vraie négociation restent hors de ce protocole — Concede/Retract en sont les gestes de base.

Scénario 2 : Débat télétravail enrichi

Le débat télétravail (Partie 2) oppose un agent Pro (productivité) à un agent Anti (isolement) sur l’AF E → CT → T. On y ajoute l’argument S(le_presentiel_favorise_la_cohesion), donné à Anti-TL, et on relance le même protocole : qu’est-ce qu’un argument de plus change — à l’extension grounded ? au verdict ?

// --- Scenario 2 : debat teletravail enrichi (cohésion entre en jeu) ---
var af2 = new DungTheory();
af2.Add(ct, t);     // isolement attaque teletravail
af2.Add(s, ct);     // presentiel soutient isolement (S non attaque => grounded l'inclut)
af2.Add(e, ct);     // productivite defait isolement
// Grounded : S entre, defait rien (S soutient ct). e defait ct. ct defait t.
// Mais S et e sont tous deux non-attaques => grounded = {S, e}. ct defait par e. t defait par ct(defait) => t entre.
Show("AF teletravail (enrichi)", af2.Describe());
Show("Grounded enrichi", string.Join(", ", af2.GroundedExtension()));

var pro2 = new ArgAgent("Pro-TL", new[] { e, t });
var anti2 = new ArgAgent("Anti-TL", new[] { s, ct });
var g2 = new DialogueGame(af2, pro2, anti2, "le_teletravail_est_souhaitable");
var (o2, w2) = g2.Run();
Show("Trace teletravail", string.Join("\n", g2.Trace.Select(l => "  " + l)));
Show("Verdict teletravail", $"{o2} — {w2}");
AF teletravail (enrichi): AF: 4 arguments, 3 attaques
  CT(le_teletravail_isole_socialement) <= S(le_presentiel_favorise_la_cohesion), E(une_etude_montre_+13pct_productivite)
  T(le_teletravail_augmente_la_productivite) <= CT(le_teletravail_isole_socialement)
  S(le_presentiel_favorise_la_cohesion) : (non attaque)
  E(une_etude_montre_+13pct_productivite) : (non attaque)
Grounded enrichi: S(le_presentiel_favorise_la_cohesion), E(une_etude_montre_+13pct_productivite), T(le_teletravail_augmente_la_productivite)
Trace teletravail:   Pro-TL: Claim(le_teletravail_est_souhaitable)
  Anti-TL: Argue(CT(le_teletravail_isole_socialement))
  Pro-TL: Argue(E(une_etude_montre_+13pct_productivite))
  Anti-TL: Concede(le_teletravail_est_souhaitable)
Verdict teletravail: ProponentWins — Opponent concede 'le_teletravail_est_souhaitable' au tour 2 (aucun contre-argument jouable).

Ce que l’enrichissement a changé. L’extension grounded passe de {E, T} (Partie 2) à {S, E, T} : S, non attaqué, entre. Et c’est tout — aucun statut existant ne bouge : CT reste défaite (désormais par E et S), T reste acceptée, la trace du dialogue est identique au mot près et le verdict aussi (ProponentWins, Concede au tour 2). Détail piquant de cet AF : S est dans la KB d’Anti-TL mais attaque CT — l’argument « cohésion » achève ici la défaite de l’argument « isolement » de son propre camp, sans que personne ne l’y contraigne (la distinction KB / engagement de la Partie 2 rend ce portage visible).

Leçon structurelle : l’acceptation grounded est globale — rien ne garantit qu’un ajout reste sans effet, car les chaînes d’attaques peuvent requalifier des arguments sans lien direct avec le nouveau venu. Ici l’effet est nul, et il faut le vérifier : dans une base de croyances classique, ajouter un fait ne retire jamais rien ; dans un AF, chaque ajout impose de relire l’ensemble.

Partie 5 — Loterie argumentative

Inspirée de Thimm (2012) / Tweety arg.prob.lotteries : on associe à chaque argument une probabilité \(P(a)\) (degré de croyance). Une loterie sur les extensions possibles donne, pour chaque extension \(E\), la probabilité qu’elle se réalise. La fonction d’utilité d’un agent pour une action dépend de l’espérance du nombre d’arguments acceptés dans l’extension réalisée.

\(U(a) = \sum_{E} P(E) \cdot |E \cap KB_{agent}|\)

Un agent rationnel soutient la claim qui maximise son utilité espérée.

Note implémentation : le notebook implémente d’abord une version Monte-Carlo (échantillonnage de mondes, espérance empirique) — pédagogique et immédiate. La formule analytique exacte ci-dessus, qui somme sur les extensions de Dung pondérées par leur probabilité, fait l’objet de l’Exercice 3 (en fin de notebook).

// --- Loterie argumentative (Thimm 2012, simplifie) ---
public sealed class ArgumentLottery
{
    public DungTheory Af { get; }
    public Dictionary<Argument, double> Proba { get; }  // P(a) par argument
    public ArgumentLottery(DungTheory af, Dictionary<Argument, double> proba) { Af = af; Proba = proba; }

    // Approximation : l'extension "realisee" = grounded, modulee par les probas.
    // On echantillonne un monde : chaque argument est "present" avec sa proba ;
    // on calcule le grounded du sous-AF realise, puis on moyenne l'utilite.
    // (Version pedagogique : echantillonnage Monte-Carlo ; la formule analytique exacte
    //  de Thimm est un exercice — voir Exercice 3 ci-dessous.)
    public double ExpectedUtility(ArgAgent agent, int samples = 1000)
    {
        var rnd = new Random(42);  // reproductible
        double sum = 0;
        var args = Af.Arguments.ToList();
        for (int i = 0; i < samples; i++)
        {
            // Monde : sous-ensemble d'arguments tires selon leur proba
            var present = new HashSet<Argument>();
            foreach (var a in args)
                if (rnd.NextDouble() < Proba.GetValueOrDefault(a, 0.0)) present.Add(a);
            // Sous-AF realise
            var sub = new DungTheory();
            foreach (var a in present) sub.Add(a);
            foreach (var a in present)
                foreach (var t in Af.Attacked.GetValueOrDefault(a, new HashSet<Argument>()))
                    if (present.Contains(t)) sub.Add(a, t);
            var grounded = sub.GroundedExtension();
            // Utilite = nb d'arguments du KB de l'agent acceptes dans ce monde
            sum += agent.Knowledge.Count(a => grounded.Contains(a));
        }
        return sum / samples;
    }
}

// --- Loterie sur le debat teletravail ---
var proba = new Dictionary<Argument, double>
{
    [e] = 0.8,   // etude : forte confiance
    [t] = 0.6,   // productivite : moyenne
    [ct] = 0.5,  // isolement : incertain
    [s] = 0.7,   // cohesion : assez confiant
};
var lot = new ArgumentLottery(af2, proba);
var uPro = lot.ExpectedUtility(pro2);
var uAnti = lot.ExpectedUtility(anti2);
Show("Loterie", $"Esperance utilite — Pro-TL={uPro:F3}, Anti-TL={uAnti:F3}");
Show("Decision rationnelle", uPro >= uAnti ? "Pro-TL soutient le teletravail" : "Anti-TL s'oppose au teletravail");
Loterie: Esperance utilite — Pro-TL=1.374, Anti-TL=0.731
Decision rationnelle: Pro-TL soutient le teletravail

Lecture de la décision. Les espérances mesurées (Pro-TL = 1,374 vs Anti-TL = 0,731) tranchent en faveur du télétravail — mais il faut lire ce que la loterie évalue : chaque issue possible (acceptation/rejet d’un argument) pondérée par sa croyance, chaque issue portant une utilité. La décision est rationnelle relativement aux utilités déclarées — changez les poids (par ex. la désutilité de l’isolement social CT) et le verdict peut basculer : c’est exactement le débat politique réel, où les parties ne divergent souvent pas sur les faits mais sur les poids. Le calcul ne remplace pas le choix de valeurs ; il le rend explicite et discutable.

Partie 6 — Parité avec la vraie librairie Tweety (IKVM)

Les Parties 1 à 5 ont reconstruit de zéro le framework de Dung, le modèle d’agent argumentatif, le protocole de dialogue et la loterie. Cette dernière partie vérifie que ces reconstructions sont cohérentes avec l’implémentation de référence de TweetyProject (module agents/dialogues/oppmodels, Thimm 2012) via le pont IKVM qui expose les classes Java compilées en .NET.

On rejoue un débat de persuasion grounded minimal : l’agent PRO défend l’argument central a, l’agent CONTRA l’attaque. La théorie universelle est un petit cadre de Dung où a est grounded-défendu (un argument c contre-attaque l’attaquant b), de sorte que la sémantique grounded accepte a. On instancie les deux ArguingAgent avec des états de croyance T1BeliefState (modèle d’opposant de niveau 1) et on lance le GroundedGameProtocol jusqu’à ce qu’un gagnant émerge.

Accès par réflexion. Comme pour Tweety-5 (§4.1.2), l’accès aux types Java se fait par réflexion (Assembly.LoadFrom + getType + Invoke) plutôt que par liaison statique : #r est silencieux en .NET Interactive (constat #5039), et la réflexion reste portable à travers les versions d’IKVM 8.x. La DLL org.tweetyproject.tweety-dialogues.dll est produite en source-recompilant les modules 1.30 (math/commons/graphs/logics/dung/agents/prob/dialogues) avec javac release 8 puis en les shadant — voir dotnet-build/rebuild-dialogues.sh.

// ============================================================
// §6 Parité avec TweetyProject via IKVM (agents/dialogues/oppmodels)
// ============================================================
// Les Parties 1-5 ont reconstruit from-scratch le framework de Dung, le modèle
// d'agent et le protocole. On vérifie ici la cohérence avec l'implémentation de
// référence de TweetyProject (GroundedGameSystem / ArguingAgent / T1BeliefState,
// module agents/dialogues/oppmodels, Thimm 2012) via le pont IKVM, en rejouant
// un débat de persuasion grounded minimal.

#r "nuget: IKVM, 8.15.0"
#r "nuget: IKVM.Image, 8.15.0"

#r "org.tweetyproject.tweety-dialogues.dll"

using System.IO;
using System.Reflection;

// --- IKVM runtime home explicite (cf. Tweety-5 §4.1.2) ---
string ikvmVer  = "8.15.0";
// RID de la machine courante (win-x64, linux-x64, osx-arm64...) : IKVM.Image tire l'image native de chaque plateforme.
string ikvmRid  = (OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsMacOS() ? "osx" : "linux") + "-" + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
string nugetRoot = Environment.GetEnvironmentVariable("NUGET_PACKAGES")
    ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
string ikvmBaseAny = Path.Combine(nugetRoot, "ikvm.image", ikvmVer, "ikvm", "any", "any");
string ikvmArchDir = Path.Combine(nugetRoot, "ikvm.image.runtime." + ikvmRid, ikvmVer, "ikvm", "any", ikvmRid);
string ikvmHome = Path.Combine(Path.GetTempPath(), "ikvm-home-" + ikvmVer + "-" + ikvmRid);
if (Directory.Exists(ikvmBaseAny) && Directory.Exists(ikvmArchDir))
{
    Directory.CreateDirectory(ikvmHome);
    foreach (var f in Directory.GetFiles(ikvmBaseAny, "*", SearchOption.AllDirectories))
    { var t = f.Replace(ikvmBaseAny, ikvmHome); Directory.CreateDirectory(Path.GetDirectoryName(t)); File.Copy(f, t, true); }
    foreach (var f in Directory.GetFiles(ikvmArchDir, "*", SearchOption.AllDirectories))
    { var t = f.Replace(ikvmArchDir, ikvmHome); Directory.CreateDirectory(Path.GetDirectoryName(t)); File.Copy(f, t, true); }
}
AppContext.SetData("IKVM.Home", ikvmHome);

var dialoguesDll = "org.tweetyproject.tweety-dialogues.dll";
Console.WriteLine($"DLL tweety-dialogues : {AssemblyName.GetAssemblyName(dialoguesDll)} ({new FileInfo(dialoguesDll).Length/1024.0/1024.0:F2} Mo)");

// --- Types Tweety (réflexion : #r est silencieux en .NET Interactive, cf #5039) ---
var asm = Assembly.LoadFrom(dialoguesDll);
Type T(string n) {
    var t = asm.GetType(n, false);
    if (t == null) throw new InvalidOperationException($"Type IKVM introuvable : {n}");
    return t;
}
var DungTheory   = T("org.tweetyproject.arg.dung.syntax.DungTheory");
var Argument     = T("org.tweetyproject.arg.dung.syntax.Argument");
var Extension    = T("org.tweetyproject.arg.dung.semantics.Extension");
var GGS          = T("org.tweetyproject.agents.dialogues.oppmodels.GroundedGameSystem");
var AgentFaction = T("org.tweetyproject.agents.dialogues.oppmodels.GroundedGameSystem+AgentFaction");
var ArguingAgent = T("org.tweetyproject.agents.dialogues.oppmodels.ArguingAgent");
var GGP          = T("org.tweetyproject.agents.dialogues.oppmodels.GroundedGameProtocol");
var T1Belief     = T("org.tweetyproject.agents.dialogues.oppmodels.T1BeliefState");
var UtilFn       = T("org.tweetyproject.agents.dialogues.oppmodels.GroundedGameUtilityFunction");
var AbsProtocol  = T("org.tweetyproject.agents.AbstractProtocol");

// --- Théorie universelle : a = "le télétravail augmente la productivité",
//     b = "le télétravail isole socialement" (attaque a),
//     c = "les outils collaboratifs réduisent l'isolement" (attaque b -> défend a).
//     a est grounded-défendu : la sémantique grounded l'accepte.
var argCtor = Argument.GetConstructor(new[]{ typeof(string) });
object NewArg(string n) => argCtor.Invoke(new object[]{ n });
object a = NewArg("a"), b = NewArg("b"), c = NewArg("c");
var theory = DungTheory.GetConstructor(Type.EmptyTypes).Invoke(null);
var addM    = DungTheory.GetMethod("add", new[]{ Argument });
var addAtkM = DungTheory.GetMethod("addAttack", new[]{ Argument, Argument });
foreach (var x in new[]{ a, b, c }) addM.Invoke(theory, new object[]{ x });
addAtkM.Invoke(theory, new object[]{ b, a });   // b -> a
addAtkM.Invoke(theory, new object[]{ c, b });   // c -> b  (défend a)
Console.WriteLine($"\n--- Théorie universelle (Dung) ---\n{theory}");

try {
// --- Système de jeu grounded + factions ---
var system = GGS.GetConstructor(new[]{ DungTheory }).Invoke(new object[]{ theory });
// IKVM compile les enums Java avec des fields "__<>NOM" inaccessibles par GetField ;
// on utilise la methode statique valueOf(String) de java.lang.Enum, portable.
var valueOfM = AgentFaction.GetMethod("valueOf", new[]{ typeof(string) });
object factionPRO    = valueOfM.Invoke(null, new object[]{ "PRO" });
object factionCONTRA = valueOfM.Invoke(null, new object[]{ "CONTRA" });
Console.WriteLine($"Factions : PRO={factionPRO}, CONTRA={factionCONTRA}");

// Connaissances initiales : PRO connaît {a, c}, CONTRA connaît {b}
var extCtor = Extension.GetConstructor(Type.EmptyTypes);
var extAddM = Extension.GetMethod("add", new[]{ Argument });
object NewExt() => extCtor.Invoke(null);
var knownPro = NewExt(); extAddM.Invoke(knownPro, new object[]{ a }); extAddM.Invoke(knownPro, new object[]{ c });
var knownContra = NewExt(); extAddM.Invoke(knownContra, new object[]{ b });

// Fonctions d'utilité : PRO veut accepter a, CONTRA veut rejeter a
var utilPro    = UtilFn.GetConstructor(new[]{ DungTheory, Argument, AgentFaction }).Invoke(new object[]{ theory, a, factionPRO });
var utilContra = UtilFn.GetConstructor(new[]{ DungTheory, Argument, AgentFaction }).Invoke(new object[]{ theory, a, factionCONTRA });

// États de croyance T1 (modèle d'opposant de niveau 1, oppModel omis -> null)
var bsPro    = T1Belief.GetConstructor(new[]{ Extension, UtilFn }).Invoke(new object[]{ knownPro, utilPro });
var bsContra = T1Belief.GetConstructor(new[]{ Extension, UtilFn }).Invoke(new object[]{ knownContra, utilContra });

// Deux agents (PRO et CONTRA)
var agentPro    = ArguingAgent.GetConstructor(new[]{ AgentFaction, T1Belief }).Invoke(new object[]{ factionPRO, bsPro });
var agentContra = ArguingAgent.GetConstructor(new[]{ AgentFaction, T1Belief }).Invoke(new object[]{ factionCONTRA, bsContra });
var addAgentM = GGS.GetMethod("add", new[]{ ArguingAgent });
addAgentM.Invoke(system, new object[]{ agentPro });
addAgentM.Invoke(system, new object[]{ agentContra });
Console.WriteLine($"Système : {system}");

// --- Protocole grounded : exécution du dialogue + gagnant ---
var protocol = GGP.GetConstructor(new[]{ GGS }).Invoke(new object[]{ system });
try {
    GGS.GetMethod("execute", new[]{ AbsProtocol }).Invoke(system, new object[]{ protocol });
} catch (TargetInvocationException tie) {
    // ProtocolTerminatedException est levée à la fin normale du dialogue
    Console.WriteLine($"  (fin de protocole : {tie.InnerException?.GetType().Name})");
}
bool hasW = (bool)GGP.GetMethod("hasWinner", Type.EmptyTypes).Invoke(protocol, null);
object winner = GGP.GetMethod("getWinner", Type.EmptyTypes).Invoke(protocol, null);
Console.WriteLine($"\n--- Résultat du dialogue ---");
Console.WriteLine($"Gagnant présent : {hasW}");
Console.WriteLine($"Gagnant          : {winner}");
Console.WriteLine($"\n[Lecture] PRO défendait 'a' (grounded-défendu par c->b) -> le gagnant est l'agent PRO,");
Console.WriteLine("ce qui confirme la cohérence entre le modèle from-scratch (Parties 1-5) et la lib Tweety.");

} catch (Exception ex) {
    Console.WriteLine($"ERREUR runtime: {ex.GetType().Name}: {ex.Message}");
    Exception root = ex;
    while (root is TargetInvocationException && root.InnerException != null) root = root.InnerException;
    if (root != ex) Console.WriteLine($"  Cause racine: {root.GetType().Name}: {root.Message}");
}
Installed Packages
  • IKVM, 8.15.0
  • IKVM.Image, 8.15.0
DLL tweety-dialogues : org.tweetyproject.tweety-dialogues, Version=1.30.0.0, Culture=neutral, PublicKeyToken=13235d27fcbfff58 (0.89 Mo)

--- Théorie universelle (Dung) ---
<{ a, b, c },[(c,b), (b,a)]>
Factions : PRO=PRO, CONTRA=CONTRA
Système : org.tweetyproject.agents.dialogues.oppmodels.GroundedGameSystem@37e1826

--- Résultat du dialogue ---
Gagnant présent : True
Gagnant          : PRO

[Lecture] PRO défendait 'a' (grounded-défendu par c->b) -> le gagnant est l'agent PRO,
ce qui confirme la cohérence entre le modèle from-scratch (Parties 1-5) et la lib Tweety.

Conclusion

Ce twin C# a reconstruit from-scratch les 4 piliers des dialogues argumentatifs multi-agents :

  1. Framework de Dung : Argument, DungTheory, sémantique grounded par point fixe (Partie 1).
  2. Agents : base de connaissances + commitment store (Partie 2).
  3. Protocole de dialogue : tours de parole, locutions Claim/Argue/Concede/Retract, terminaison (Partie 3).
  4. Loterie argumentative : probabilités sur les arguments, fonction d’utilité, décision rationnelle (Partie 5).

Les Scénarios (Partie 4) ont rejoué ces mécanismes sur deux cas — négociation immobilière, débat enrichi — et la Partie 6 a vérifié la cohérence avec l’implémentation de référence Tweety (agents/dialogues/oppmodels) via IKVM : sur un débat minimal dont la thèse est grounded-défendue, c’est bien le défenseur PRO qui émerge gagnant — le même lien thèse-défendue → proponent gagne que dans les Parties 1-5. Là où le notebook Python traverse l’API Java (jpype/JVM), chaque mécanisme ici reste lisible dans le code C# (rationale en tête de notebook).

Limitations

  • La sémantique est limitée à grounded (vs preferred/stable de Tweety).
  • Le protocole est une variante simplifiée du jeu de persuasion grounded (Prakken 2005), sans stratégies complexes.
  • La loterie utilise un échantillonnage Monte-Carlo (vs la formule analytique exacte de Thimm sur les divisions).

Exercices

Les exercices ci-dessous sont à compléter. Chaque stub s’exécute sans erreur (convention C.1) — remplacez le TODO par votre implémentation.

// Exercice 1 : Sémantique preferred
// TODO etudiant : implementez PreferredExtensions() retournant l'ENSEMBLE des extensions
// maximales sans conflit qui defaient leurs attaquants (Dung 1995, Theorem 30).
// Indice : enumerer les sous-ensembles sans conflit, filtrer ceux qui defaient leurs attaquants,
// garder les maximaux pour l'inclusion.
// Etape 1 : ecrire une methode ConflictFree(IEnumerable<Argument>).
// Etape 2 : enumerer les candidats par taille decroissante.
// Etape 3 : retourner les maximaux.
public static List<HashSet<Argument>> PreferredExtensions(DungTheory af)
{
    // TODO etudiant
    var result = new List<HashSet<Argument>>();
    // Indice : pour demarrer, comparez avec GroundedExtension() ci-dessus.
    return result;  // retournez les extensions preferred ici
}
// Test rapide (affichera 0 tant que l'exercice n'est pas complete)
Show("Preferred (a completer)", $"{PreferredExtensions(af2).Count} extensions");
Preferred (a completer): 0 extensions
// Exercice 2 : Protocole de deliberation (multi-agents)
// TODO etudiant : etendez DialogueGame pour supporter >2 agents (tour rond).
// Indice : ajoutez une List<ArgAgent> Participants, un index courant, et un tour de parole rond.
// Etape 1 : signature Run() avec participants tournants.
// Etape 2 : condition de terminaison = consensus ou tour max.
// Laissez le stub compilable (return Stalemate).
public static string DeliberationRound(DungTheory af, List<ArgAgent> participants, string topic)
{
    // TODO etudiant
    return "Exercice a completer : implementez le tour de parole multi-agents.";
}
Show("Deliberation (a completer)", DeliberationRound(af2, new List<ArgAgent>{ pro2, anti2 }, "topic"));
Deliberation (a completer): Exercice a completer : implementez le tour de parole multi-agents.
// Exercice 3 : Loterie analytique exacte
// TODO etudiant : remplacez l'echantillonnage Monte-Carlo de ExpectedUtility par la formule
// analytique exacte de Thimm (2012) sur les divisions de l'AF.
// Indice : P(E) = produit des P(a) pour a in E * produit des (1-P(a)) pour a not in E,
//          somme sur toutes les extensions possibles.
// Etape 1 : enumerer les extensions (via l'exercice 1 preferred, ou toutes les labellings).
// Etape 2 : calculer P(E) pour chacune.
// Etape 3 : sommer P(E) * |E inter KB|.
public static double ExactUtility(DungTheory af, Dictionary<Argument,double> proba, ArgAgent agent)
{
    // TODO etudiant
    return 0.0;  // retournez l'utilite exacte
}
Show("Utilite exacte (a completer)", ExactUtility(af2, proba, pro2));
Utilite exacte (a completer): 0

Twin C# du notebook Python Tweety-08-Agent-Dialogues-Python.ipynb. Marathon parité .NET ⇄ Python (#4956, Prong B). Parties 1-5 : from-scratch BCL .NET, 0 NuGet, 0 JVM ; Partie 6 : parité vérifiée via IKVM.

Retour au sommet