diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Fallacy_Rules_Symboliques.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Fallacy_Rules_Symboliques.ipynb new file mode 100644 index 0000000000..673ff4731e --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Fallacy_Rules_Symboliques.ipynb @@ -0,0 +1,813 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "refond-md-0", + "metadata": { + "papermill": { + "duration": 0.005489, + "end_time": "2026-09-25T08:50:06.268836+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:06.263347+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "# Détection symbolique de sophismes — l'étage symbolique du cœur EPITA\n", + "\n", + "Ce notebook porte l'**étage symbolique** de la détection de sophismes tel qu'il vit dans le\n", + "**cœur du dépôt EPITA** (`argumentation_analysis/adapters/french_fallacy_adapter.py`,\n", + "`_SYMBOLIC_FALLACY_RULES`) : des motifs déclaratifs de tokens, exécutés par le vrai moteur (spaCy\n", + "et son modèle français `fr_core_news_sm`), et une chaîne de justifications par famille.\n", + "\n", + "La généalogie est explicite. Ces règles ont d'abord existé dans le sous-projet étudiant\n", + "`2.3.2-detection-sophismes` (`jsboigeEpita/2025-Epita-Intelligence-Symbolique`) ; le cœur les a\n", + "ensuite **consolidées** — il en a retenu une partie, écarté une autre, et **corrigé deux motifs**\n", + "que leur forme d'origine rendait inopérants (réparations G4, #1186). Ce notebook porte la version\n", + "du cœur ; le détecteur neuronal CamemBERT de 1,8 Go reste hors périmètre (archéologique, absent\n", + "du dépôt).\n", + "\n", + "L'organe `fallacy_rules.py` vit à côté de ce notebook, dans le même dossier. Il est **pur** :\n", + "sans spaCy installé, il s'importe et ses données sont vérifiables (tests de structure dans\n", + "`tests/test_fallacy_rules.py`).\n", + "\n", + "**Plan** : (1) comptes et structure des règles ; (2) détection sur un corpus français annoté ;\n", + "(3) minage claim/prémisse ; (4) **démonstration des deux réparations du cœur** ; (5) limites\n", + "mesurées ; (6) exercices.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "da9b4146", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:06.281882Z", + "iopub.status.busy": "2026-09-25T08:50:06.281514Z", + "iopub.status.idle": "2026-09-25T08:50:06.294951Z", + "shell.execute_reply": "2026-09-25T08:50:06.292495Z" + }, + "papermill": { + "duration": 0.025677, + "end_time": "2026-09-25T08:50:06.297630+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:06.271953+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'families': 5,\n", + " 'rule_keys': 5,\n", + " 'fallacy_motifs': 13,\n", + " 'claim_motifs': 2,\n", + " 'premise_motifs': 2,\n", + " 'justification_templates': 4}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "\n", + "sys.path.insert(0, str(Path().resolve()))\n", + "\n", + "import fallacy_rules as fr\n", + "\n", + "fr.rule_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-1", + "metadata": { + "papermill": { + "duration": 0.003769, + "end_time": "2026-09-25T08:50:06.307076+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:06.303307+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture des comptes\n", + "\n", + "L'organe porte **5 clés de règles couvrant 5 familles** de sophismes, **13 motifs**, **2 motifs\n", + "de claim**, **2 motifs de prémisse** et **4 gabarits de justification**. Ces comptes sont\n", + "**recomptés sur le fichier de l'organe**, et ce sont exactement ceux de la consolidation du cœur\n", + ": la re-fondation a retiré les deux motifs d'autorité qui reposaient sur une simple paire\n", + "sujet-verbe (`[NOUN] (le) dire`, `[PROPN] dire` — sans marqueur d'autorité), le motif de prémisse\n", + "`les/des NOUN montrer/indiquer que`, et fusionné les deux clés d'autorité en une seule,\n", + "`ARGUMENT_AUTORITE`.\n", + "\n", + "Les identifiants sont désormais **non accentués** (`GENERALISATION_HATIVE`,\n", + "`APPEL_A_LA_TRADITION`, `ARGUMENT_AUTORITE`), comme dans le cœur : un identifiant accentué est un\n", + "risque de régression (cure #2876), et les clés du cœur font autorité de nommage depuis la\n", + "re-fondation.\n", + "\n", + "Chaque motif est un dictionnaire spaCy `Matcher` : une liste de specs de tokens (attributs\n", + "`LOWER`, `LEMMA`, `POS`, `TEXT`…) avec des opérateurs `OP` (`*`, `?`, `+`). C'est cette forme\n", + "déclarative qui rend l'étage distillable : les motifs sont des **données**, examinables,\n", + "critiquables et extensibles sans toucher au moteur.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30ae5fb0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:06.321374Z", + "iopub.status.busy": "2026-09-25T08:50:06.320931Z", + "iopub.status.idle": "2026-09-25T08:50:13.481641Z", + "shell.execute_reply": "2026-09-25T08:50:13.480418Z" + }, + "papermill": { + "duration": 7.170489, + "end_time": "2026-09-25T08:50:13.483050+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:06.312561+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Modèle: core_news_sm | spaCy chargé.\n", + "OK attendu=AD_HOMINEM détecté=— justif=non —\n", + "OK attendu=AD_HOMINEM détecté=AD_HOMINEM_DIRECT justif=oui #1 « On ne peut pas faire confiance à un homme »\n", + "OK attendu=PENTE_GLISSANTE détecté=— justif=non —\n", + "OK attendu=PENTE_GLISSANTE détecté=PENTE_GLISSANTE justif=non #2 « Le premier pas vers le »\n", + "OK attendu=GENERALISATION_HATIVE détecté=— justif=non —\n", + "OK attendu=APPEL_A_LA_TRADITION détecté=APPEL_A_LA_TRADITION justif=non #0 « On a toujours fait comme ça »\n", + "OK attendu=ARGUMENT_D_AUTORITE détecté=— justif=non —\n", + "OK attendu=ARGUMENT_D_AUTORITE détecté=ARGUMENT_AUTORITE justif=oui #1 « Selon un journaliste, »\n", + "OK attendu=SAINE détecté=— justif=non —\n" + ] + } + ], + "source": [ + "import spacy\n", + "\n", + "nlp = spacy.load(\"fr_core_news_sm\")\n", + "print(\"Modèle:\", nlp.meta[\"name\"], \"| spaCy chargé.\")\n", + "\n", + "# Corpus français : une à deux phrases par famille + une phrase saine.\n", + "corpus = [\n", + " (\"Vous êtes incompétent, donc votre argument est invalide.\", \"AD_HOMINEM\"),\n", + " (\"On ne peut pas faire confiance à un homme politique.\", \"AD_HOMINEM\"),\n", + " (\"Si on autorise le mariage pour tous, alors on autorisera la polygamie.\", \"PENTE_GLISSANTE\"),\n", + " (\"Le premier pas vers le déclin de la société.\", \"PENTE_GLISSANTE\"),\n", + " (\"Tous les touristes sont désagréables.\", \"GENERALISATION_HATIVE\"),\n", + " (\"On a toujours fait comme ça, pourquoi changer ?\", \"APPEL_A_LA_TRADITION\"),\n", + " (\"Un expert a dit que le chocolat est bon pour la santé, donc c'est vrai.\", \"ARGUMENT_D_AUTORITE\"),\n", + " (\"Selon un journaliste, la mesure est efficace.\", \"ARGUMENT_D_AUTORITE\"),\n", + " (\"Le prix du pain a augmenté ce mois-ci.\", \"SAINE\"),\n", + "]\n", + "\n", + "resultats = []\n", + "for phrase, attendu in corpus:\n", + " detections = fr.detect_fallacies(phrase, nlp=nlp)\n", + " resultats.append({\n", + " \"phrase\": phrase,\n", + " \"attendu\": attendu,\n", + " \"détecté\": detections[0][\"key\"] if detections else \"—\",\n", + " \"motif\": f'#{detections[0][\"pattern_index\"]} « {detections[0][\"excerpt\"]} »' if detections else \"—\",\n", + " \"justif\": \"oui\" if detections and detections[0][\"justification\"] else \"non\",\n", + " })\n", + "\n", + "for r in resultats:\n", + " marque = \"OK \" if (r[\"attendu\"].startswith(tuple(fr.FALLACY_LABELS)) or r[\"attendu\"] == \"SAINE\") else \" \"\n", + " print(f'{marque} attendu={r[\"attendu\"]:<22} détecté={r[\"détecté\"]:<24} justif={r[\"justif\"]:<4} {r[\"motif\"]}')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6861e9d8", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.493835Z", + "iopub.status.busy": "2026-09-25T08:50:13.493375Z", + "iopub.status.idle": "2026-09-25T08:50:13.507689Z", + "shell.execute_reply": "2026-09-25T08:50:13.506449Z" + }, + "papermill": { + "duration": 0.020802, + "end_time": "2026-09-25T08:50:13.508796+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.487994+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "'Vous' LEMMA=vous POS=PRON LOWER=vous\n", + "'êtes' LEMMA=être POS=AUX LOWER=êtes\n", + "'incompétent' LEMMA=incompéter POS=VERB LOWER=incompétent\n", + "',' LEMMA=, POS=PUNCT LOWER=,\n", + "'donc' LEMMA=donc POS=ADV LOWER=donc\n", + "'votre' LEMMA=votre POS=DET LOWER=votre\n", + "'argument' LEMMA=argument POS=NOUN LOWER=argument\n", + "'est' LEMMA=être POS=AUX LOWER=est\n", + "'invalide' LEMMA=invalide POS=ADJ LOWER=invalide\n", + "'.' LEMMA=. POS=PUNCT LOWER=.\n" + ] + } + ], + "source": [ + "# Diagnostic : pourquoi la phrase ad hominem « propre » échoue.\n", + "# On affiche les attributs que les specs du motif 0 attendent, token par token.\n", + "cas_net = \"Vous êtes incompétent, donc votre argument est invalide.\"\n", + "for tok in nlp(cas_net):\n", + " print(f\"{tok.text!r:<18} LEMMA={tok.lemma_:<12} POS={tok.pos_:<6} LOWER={tok.lower_}\")" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-2", + "metadata": { + "papermill": { + "duration": 0.004125, + "end_time": "2026-09-25T08:50:13.516494+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.512369+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture du corpus : quatre déclarations, cinq silences, un diagnostic\n", + "\n", + "Sur le modèle `sm`, **quatre phrases sur neuf déclenchent** : « On ne peut pas faire confiance à »\n", + "(motif 1, ad hominem par discrédit généralisé), « Le premier pas vers » (motif 2 de la pente\n", + "glissante), « On a toujours fait comme ça » (motif 0 de l'appel à la tradition) et\n", + "« Selon un journaliste, » (motif 1 de l'argument d'autorité) — et la phrase saine sur le prix du\n", + "pain reste silencieuse, témoin négatif attendu. Deux de ces quatre détections portent une\n", + "justification ; les deux autres en sont dépourvues, et ce n'est pas un oubli : les gabarits de\n", + "justification couvrent trois familles sur cinq (voir les limites mesurées).\n", + "\n", + "Ce qui distingue les quatre gagnantes : ce sont des **motifs lexicaux**, des séquences de\n", + "mots-outils exacts, insensibles au taggeur. Les cinq silences sont tous **syntaxiques** — ils\n", + "dépendent des specs `POS`/`LEMMA` — et le diagnostic ci-dessus montre les causes pour la plus\n", + "simple d'entre elles : dans « Vous êtes incompétent**,** donc », la **virgule** s'intercale entre\n", + "l'adjectif et le connecteur (le motif 0 prévoit `ADJ` puis `donc` sans ponctuation entre les\n", + "deux), et surtout le taggeur `sm` étiquette « incompétent » **VERB** (lemme « incompéter »), pas\n", + "`ADJ` — la spec `POS: ADJ` ne peut donc jamais s'allumer sur ce mot sous ce modèle. Même famille\n", + "de cause pour les autres : « Tous **les** touristes » insère un déterminant que le gabarit\n", + "QUANTIFICATEUR → NOM+ n'a pas prévu, et « Si on autorise **le** mariage » fait de même côté du\n", + "verbe.\n", + "\n", + "La leçon de mesure : un moteur à motifs figés a une **précision élevée** (ce qu'il signale, il le\n", + "signale pour la bonne raison) et un **rappel très faible sur du français réel** (4/9 ici) —\n", + "chaque mot fonctionnel oublié du gabarit (virgule, déterminant, adverbe) suffit à casser la\n", + "reconnaissance. La section qui suit montre que le cœur a **réparé deux de ces gabarits**, et que\n", + "la réparation se mesure.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "890c69b8", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.526369Z", + "iopub.status.busy": "2026-09-25T08:50:13.525937Z", + "iopub.status.idle": "2026-09-25T08:50:13.565360Z", + "shell.execute_reply": "2026-09-25T08:50:13.564187Z" + }, + "papermill": { + "duration": 0.045429, + "end_time": "2026-09-25T08:50:13.566452+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.521023+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Avec adverbe : aucune détection\n", + "Sans adverbe : aucune détection\n", + "Claims : ['Je pense que la réforme est nécessaire, parce que les études montrent que les coûts ont triplé. En conclusion, il faut agir maintenant.', 'En conclusion, il faut agir maintenant.']\n", + "Premisses: ['parce que les études montrent que les coûts ont triplé. En conclusion, il faut agir maintenant.']\n" + ] + } + ], + "source": [ + "# La paire avec/sans adverbe : les DEUX échouent, pour des raisons cumulées.\n", + "cas_adverbe = \"Vous êtes vraiment incompétent, donc votre argument est invalide.\"\n", + "print(\"Avec adverbe :\", fr.detect_fallacies(cas_adverbe, nlp=nlp) or \"aucune détection\")\n", + "print(\"Sans adverbe :\", fr.detect_fallacies(cas_net, nlp=nlp) or \"aucune détection\")\n", + "\n", + "# Minage claim/prémisse sur un énoncé argumentatif complet.\n", + "argument = (\"Je pense que la réforme est nécessaire, parce que les études montrent que \"\n", + " \"les coûts ont triplé. En conclusion, il faut agir maintenant.\")\n", + "minage = fr.mine_claims_premises(argument, nlp=nlp)\n", + "print(\"Claims :\", [c[\"excerpt\"] for c in minage[\"claims\"]])\n", + "print(\"Premisses:\", [p[\"excerpt\"] for p in minage[\"premises\"]])" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-3", + "metadata": { + "papermill": { + "duration": 0.003765, + "end_time": "2026-09-25T08:50:13.574177+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.570412+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : géométrie du silence, puis minage propre\n", + "\n", + "La paire confirme le diagnostic : **les deux variantes échouent**, et pour des raisons qui se\n", + "cumulent — la virgule pour les deux, l'adverbe `vraiment` (spec `ADV` absente du gabarit) en plus\n", + "pour la seconde. C'est la géométrie exacte de l'angle mort : chaque élément non prévu entre deux\n", + "specs attendues ajoute une *distance* que le motif ne sait pas franchir, et les distances\n", + "s'additionnent.\n", + "\n", + "Le minage, lui, rend des extraits **propres** : une seule occurrence par marqueur. Ce n'est pas le\n", + "comportement brut du Matcher — le joker `OP: \"+\"` du source, posé sur une spec `TEXT` quelconque,\n", + "fait rendre au Matcher *toutes* les longueurs possibles depuis le même point de départ (des\n", + "extraits imbriqués « Je pense que la », « Je pense que la réforme », « Je pense que la réforme\n", + "est »…). L'organe déduplique en conservant **le plus long match par position de départ** :\n", + "sémantique du marqueur conservée, bruit d'affichage supprimé.\n", + "\n", + "Cette géométrie n'est pas une fatalité. Si la virgule casse un motif parce qu'un seul slot manque,\n", + "alors **ajouter ce slot** répare le motif — c'est exactement le geste que la consolidation du cœur\n", + "a fait sur deux d'entre eux, et la section suivante le mesure.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "refond-code-0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.584019Z", + "iopub.status.busy": "2026-09-25T08:50:13.583642Z", + "iopub.status.idle": "2026-09-25T08:50:13.650992Z", + "shell.execute_reply": "2026-09-25T08:50:13.649759Z" + }, + "papermill": { + "duration": 0.073524, + "end_time": "2026-09-25T08:50:13.652034+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.578510+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Diagnostic du cas ad hominem, token par token :\n", + " 'Pierre' LEMMA=Pierre POS=PROPN \n", + " 'est' LEMMA=être POS=AUX \n", + " 'malhonnête' LEMMA=malhonnête POS=ADJ \n", + " ',' LEMMA=, POS=PUNCT \n", + " 'donc' LEMMA=donc POS=ADV \n", + " 'son' LEMMA=son POS=DET \n", + " 'argument' LEMMA=argument POS=NOUN \n", + " 'est' LEMMA=être POS=AUX \n", + " 'faux' LEMMA=faux POS=ADJ \n", + " '.' LEMMA=. POS=PUNCT \n", + "\n", + "AD_HOMINEM_DIRECT #2 — origine : 0 match | cœur : 1 match\n", + "GENERALISATION_HATIVE #1 — origine : 0 match | cœur : 1 match\n", + "\n", + "AD_HOMINEM_DIRECT #2 : « Pierre est malhonnête, donc son argument est faux »\n", + " justification : L'argument attaque la personne ou le caractère de l'adversaire plutôt que de réfuter son argument.\n", + "GENERALISATION_HATIVE #1 : « Sur la base de 5 exemples »\n", + " justification : Une conclusion générale est tirée à partir d'un échantillon trop limité ou non représentatif.\n", + "\n", + "Couverture des gabarits de justification (5 familles) :\n", + " AD_HOMINEM gabarit\n", + " PENTE_GLISSANTE None (fail-loud)\n", + " GENERALISATION_HATIVE gabarit\n", + " APPEL_A_LA_TRADITION None (fail-loud)\n", + " ARGUMENT_D_AUTORITE gabarit\n" + ] + } + ], + "source": [ + "# Les deux motifs que la consolidation du cœur a restaurés en les corrigeant (#1186).\n", + "cas_ad = \"Pierre est malhonnête, donc son argument est faux.\"\n", + "cas_gen = \"Sur la base de 5 exemples, il conclut que tout le monde ment.\"\n", + "\n", + "print(\"Diagnostic du cas ad hominem, token par token :\")\n", + "for tok in nlp(cas_ad):\n", + " print(f\" {tok.text!r:<14} LEMMA={tok.lemma_:<12} POS={tok.pos_:<6}\")\n", + "\n", + "from spacy.matcher import Matcher\n", + "\n", + "\n", + "def nb_matches(pattern, texte):\n", + " \"\"\"Nombre de matches d'un motif sur un texte (matcher reconstruit à chaque appel).\"\"\"\n", + " matcher = Matcher(nlp.vocab)\n", + " matcher.add(\"motif\", [pattern])\n", + " return len(matcher(nlp(texte)))\n", + "\n", + "\n", + "# Forme d'origine (projet étudiant) vs forme réparée par le cœur, motif par motif.\n", + "ad_origine = [\n", + " {\"POS\": \"PROPN\"}, {\"LEMMA\": \"être\"}, {\"POS\": \"ADJ\"},\n", + " {\"LOWER\": {\"IN\": [\"donc\", \"alors\"]}},\n", + " {\"POS\": \"DET\"}, {\"POS\": \"NOUN\"}, {\"LEMMA\": \"être\"}, {\"LOWER\": \"faux\"},\n", + "]\n", + "ad_coeur = ad_origine[:3] + [{\"IS_PUNCT\": True, \"OP\": \"?\"}] + ad_origine[3:]\n", + "\n", + "gen_origine = [\n", + " {\"LOWER\": \"sur\"}, {\"LOWER\": \"la\"}, {\"LOWER\": \"base\"}, {\"LOWER\": \"de\"},\n", + " {\"POS\": \"NUM\"}, {\"POS\": \"NOUN\"}, {\"LOWER\": \"exemples\"},\n", + "]\n", + "gen_coeur = [\n", + " {\"LOWER\": \"sur\"}, {\"LOWER\": \"la\"}, {\"LOWER\": \"base\"}, {\"LOWER\": \"de\"},\n", + " {\"POS\": \"NUM\"}, {\"LOWER\": \"exemples\"},\n", + "]\n", + "\n", + "print()\n", + "for nom, origine, coeur, cas in [\n", + " (\"AD_HOMINEM_DIRECT #2\", ad_origine, ad_coeur, cas_ad),\n", + " (\"GENERALISATION_HATIVE #1\", gen_origine, gen_coeur, cas_gen),\n", + "]:\n", + " print(f\"{nom:26s} — origine : {nb_matches(origine, cas)} match | cœur : {nb_matches(coeur, cas)} match\")\n", + "\n", + "print()\n", + "for cas in (cas_ad, cas_gen):\n", + " for d in fr.detect_fallacies(cas, nlp=nlp):\n", + " just = d[\"justification\"] or \"aucun gabarit pour cette famille (fail-loud)\"\n", + " print(f'{d[\"key\"]} #{d[\"pattern_index\"]} : « {d[\"excerpt\"]} »')\n", + " print(f\" justification : {just}\")\n", + "\n", + "print()\n", + "print(\"Couverture des gabarits de justification (5 familles) :\")\n", + "for famille, label in fr.FALLACY_LABELS.items():\n", + " gabarit = fr.justify_fallacy(label)\n", + " print(f' {famille:<24} {\"gabarit\" if gabarit else \"None (fail-loud)\"}')\n" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-4", + "metadata": { + "papermill": { + "duration": 0.003853, + "end_time": "2026-09-25T08:50:13.660024+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.656171+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : deux motifs que la consolidation a rendus vivants\n", + "\n", + "Le tableau est net : les deux motifs **ne déclenchaient pas** sous leur forme d'origine, et\n", + "**déclenchent** sous la forme réparée par le cœur. Le diagnostic token par token du cas ad\n", + "hominem montre que l'obstruction était **unique** — la virgule entre l'adjectif et le connecteur ;\n", + "tout le reste du gabarit s'aligne, y compris `est`, qui porte bien le lemme `être` quoique taggé\n", + "`AUX` plutôt que `VERB` : la spec s'appuie sur le lemme, pas sur le POS. Ajouter le slot de\n", + "ponctuation optionnel `{\"IS_PUNCT\": True, \"OP\": \"?\"}` suffit à faire passer le motif de 0 à 1\n", + "match.\n", + "\n", + "La seconde réparation est de nature différente. Dans « Sur la base de 5 exemples », le slot `NOUN`\n", + "surnuméraire du gabarit d'origine attendait un nom **avant** `exemples` — qui est lui-même le nom.\n", + "Deux obstructions là où il n'y avait pas la place d'en franchir une. Le cœur a retiré le slot ; le\n", + "motif passe de 0 à 1 match.\n", + "\n", + "Ce que ces deux réparations enseignent vaut au-delà de ce notebook : **un motif qui ne déclenche\n", + "jamais ne se voit pas**. Il ne produit ni erreur, ni faux positif, ni trace — seulement un silence\n", + "indiscernable d'une absence légitime de sophisme. Seule la mesure de rappel sur un corpus annoté\n", + "(celle de la section précédente) le révèle : 4 détections sur 9 phrases disent à la fois ce que le\n", + "moteur attrape **et** qu'il en manque.\n", + "\n", + "La réparation n'est pas cosmétique : elle a été motivée par un incident (#1186) et le cœur l'a\n", + "annotée à l'endroit exact du gabarit, pour que le prochain lecteur sache pourquoi ce slot existe.\n" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-5", + "metadata": { + "papermill": { + "duration": 0.003232, + "end_time": "2026-09-25T08:50:13.666866+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.663634+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Limites mesurées et portée\n", + "\n", + "1. **Modèle-dépendance** : les specs `LEMMA`/`POS` sont évaluées par le modèle chargé. Cette\n", + " exécution utilise `fr_core_news_sm` (léger) ; le cœur utilise `fr_core_news_lg` (~500 Mo). Le\n", + " diagnostic du corpus en donne la preuve directe : « incompétent » est VERB sous `sm` — sous\n", + " `lg` ou avec un autre mot, la même phrase peut détecter. La sortie committée ici est celle de\n", + " `sm`, reproductible à l'identique.\n", + "2. **Rappel borné par construction, et mesuré** : 4 détections sur 9 phrases du corpus (modèle\n", + " `sm`) — les motifs lexicaux passent, les syntaxiques tombent sur les éléments non prévus\n", + " (virgule, déterminant, adverbe). La démonstration G4 montre que ces obstructions sont\n", + " réparables une à une, mais qu'elles ne se voient **qu'au banc** : le corpus labellisé du cœur\n", + " permettrait de mesurer précision et rappel sur du texte réel — c'est le complément naturel de\n", + " ce grain.\n", + "3. **Justification partielle, par construction** : les 4 gabarits de justification couvrent **3\n", + " des 5 familles**. La pente glissante et l'appel à la tradition n'en ont pas : leur détection\n", + " rend `None`, et l'organe ne fabrique pas de texte à leur place (*fail-loud* #1019). Un\n", + " détecteur honnête dit « je ne sais pas l'expliquer » plutôt que d'improviser.\n", + "4. **Divergences conservées, mesurées** : les specs `{\"OP\": \"+\"}` nues du source sont normalisées\n", + " en jokers explicites — choix de lisibilité, pas contrainte : spaCy 3.8.16 accepte aussi la\n", + " spec nue, et les deux formes rendent les mêmes matches (mesure du 2026-09-23) ; le minage\n", + " déduplique par le plus long match. Tout est consigné dans la docstring de `fallacy_rules.py`.\n", + "\n", + "## Exercices\n", + "\n", + "Les cellules suivantes sont à compléter. Elles s'exécutent sans erreur sur des stubs —\n", + "le notebook doit toujours tourner de bout en bout.\n" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-6", + "metadata": { + "papermill": { + "duration": 0.004366, + "end_time": "2026-09-25T08:50:13.675300+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.670934+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 1 — Réparer le motif ad hominem, une cause à la fois\n", + "\n", + "La phrase « Vous êtes incompétent, donc votre argument est invalide. » échoue pour **trois**\n", + "raisons cumulées, toutes visibles dans la cellule de diagnostic : la virgule (`PUNCT`) entre\n", + "l'adjectif et le connecteur, l'étiquette `POS` de « incompétent » qui est **VERB** et non `ADJ`\n", + "sous ce modèle, puis — dans la variante avec adverbe — l'adverbe entre être et l'adjectif.\n", + "\n", + "La consolidation du cœur a réparé cette **classe** d'obstruction sur deux autres motifs (section\n", + "« deux motifs que la consolidation a rendus vivants ») ; ici, c'est le motif 0 d'`AD_HOMINEM_DIRECT`\n", + "qui est en cause. Ajoutez-lui un **nouveau motif** (sans modifier les existants) qui traite les\n", + "trois causes. Vérifiez sur les DEUX phrases : elles doivent détecter toutes les deux. Étape de\n", + "vérification : les trois phrases saines de l'exercice 3 ne doivent pas se mettre à détecter.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e3e99f21", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.685329Z", + "iopub.status.busy": "2026-09-25T08:50:13.684833Z", + "iopub.status.idle": "2026-09-25T08:50:13.692021Z", + "shell.execute_reply": "2026-09-25T08:50:13.690665Z" + }, + "papermill": { + "duration": 0.013612, + "end_time": "2026-09-25T08:50:13.693039+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.679427+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 1 a completer\n" + ] + } + ], + "source": [ + "# TODO etudiant : construire nouveau_motif (liste de specs de tokens) puis :\n", + "# fr.FALLACY_RULES[\"AD_HOMINEM_DIRECT\"].append({\"PATTERN\": nouveau_motif, \"FALLACY_TYPE\": \"Attaque personnelle (Ad Hominem)\"})\n", + "# puis re-executer fr.detect_fallacies sur les deux phrases et imprimer les resultats.\n", + "# Indice : partez du motif 0 (voir fallacy_rules.py) et (1) remplacez la spec ADJ par\n", + "# {\"POS\": {\"IN\": [\"ADJ\", \"VERB\"]}}, (2) inserez {\"POS\": \"ADV\", \"OP\": \"?\"} avant elle,\n", + "# (3) inserez {\"POS\": \"PUNCT\", \"OP\": \"?\"} avant le connecteur donc.\n", + "nouveau_motif = None # TODO etudiant\n", + "resultat_exercice_1 = None # TODO etudiant\n", + "print(\"Exercice 1 a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "efb2c2c3", + "metadata": { + "papermill": { + "duration": 0.004197, + "end_time": "2026-09-25T08:50:13.701487+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.697290+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 2 — Une phrase, deux familles\n", + "\n", + "Construisez une phrase française qui déclenche **au moins deux familles différentes** en une\n", + "seule passe de `detect_fallacies` (par exemple pente glissante + appel à la tradition).\n", + "Imprimez la liste des détections avec leurs familles. Contrainte d'honnêteté : la phrase doit\n", + "être grammaticale et les deux détections doivent venir de motifs **différents** de l'organe,\n", + "pas d'un motif que vous auriez ajouté." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d3bec768", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.710974Z", + "iopub.status.busy": "2026-09-25T08:50:13.710661Z", + "iopub.status.idle": "2026-09-25T08:50:13.716415Z", + "shell.execute_reply": "2026-09-25T08:50:13.715193Z" + }, + "papermill": { + "duration": 0.011771, + "end_time": "2026-09-25T08:50:13.717515+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.705744+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 2 a completer\n" + ] + } + ], + "source": [ + "# TODO etudiant : ecrire phrase_double puis detections = fr.detect_fallacies(phrase_double, nlp=nlp)\n", + "phrase_double = None # TODO etudiant\n", + "print(\"Exercice 2 a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "a5a8104a", + "metadata": { + "papermill": { + "duration": 0.004041, + "end_time": "2026-09-25T08:50:13.726065+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.722024+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 3 — Témoin négatif\n", + "\n", + "Proposez **trois phrases saines** (aucun sophisme) dont deux contiennent quand même des\n", + "marqueurs superficiellement proches (par ex. « si … alors » sans chaîne catastrophique, ou un\n", + "« selon » citant une source qualifiée). Vérifiez que `detect_fallacies` rend une liste vide sur\n", + "au moins deux d'entre elles ; si une phrase saine déclenche un faux positif, nommez le motif\n", + "coupable et expliquez pourquoi le gabarit est trop large." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "52c87bcf", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-25T08:50:13.736388Z", + "iopub.status.busy": "2026-09-25T08:50:13.735948Z", + "iopub.status.idle": "2026-09-25T08:50:13.742154Z", + "shell.execute_reply": "2026-09-25T08:50:13.741077Z" + }, + "papermill": { + "duration": 0.012813, + "end_time": "2026-09-25T08:50:13.743102+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.730289+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 3 a completer" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# TODO etudiant : completer saines = [...] puis boucle de verification + commentaire.\n", + "saines = [] # TODO etudiant\n", + "print(\"Exercice 3 a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "refond-md-7", + "metadata": { + "papermill": { + "duration": 0.004376, + "end_time": "2026-09-25T08:50:13.752056+00:00", + "exception": false, + "start_time": "2026-09-25T08:50:13.747680+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Conclusion\n", + "\n", + "Ce notebook a porté l'étage symbolique du détecteur de sophismes tel qu'il vit dans le cœur\n", + "EPITA : un organe déclaratif de **13 motifs** et **4 motifs de minage**, exécutable par le vrai\n", + "moteur (spaCy), dont le comportement est **montré tel quel** — succès lexicaux, témoin négatif\n", + "silencieux, angle mort mesuré de l'adverbe, et **deux réparations mesurées** que la consolidation\n", + "a apportées aux motifs hérités du projet étudiant.\n", + "\n", + "La leçon la plus transférable est celle des deux sections centrales : un motif qui ne déclenche\n", + "jamais est **invisible** — il ne lève rien, ne signale rien, et une règle morte ressemble\n", + "exactement à une règle qui n'a rien à dire. Seul un banc annoté les distingue, et c'est pourquoi le\n", + "cœur a attaché à ces deux réparations le numéro de l'incident (#1186) qui les a motivées.\n", + "\n", + "La suite naturelle — banc précision/rappel sur un corpus labellisé, justification complète des cinq\n", + "familles — reste ouverte, rangée en sous-grains dans le recensement de l'EPIC #4960.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.14" + }, + "papermill": { + "default_parameters": {}, + "duration": 9.861259, + "end_time": "2026-09-25T08:50:14.756697+00:00", + "environment_variables": {}, + "exception": null, + "input_path": "Argument_Analysis_Fallacy_Rules_Symboliques.ipynb", + "output_path": "fallacy_exec2.ipynb", + "parameters": {}, + "start_time": "2026-09-25T08:50:04.895438+00:00", + "version": "2.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index 9ba2b30f87..f8b9e776cf 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -97,6 +97,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Ontology_AIF | [Ontology_AIF](Argument_Analysis_Ontology_AIF.ipynb) | Socle ontologique Argumentum (`argumentum_fallacies.owl`, 4,7 MB OWL2/XML) : parseur regex tolérant (37 axiom `ExactCardinality` mal formés bloquent rdflib), inventaire 10 976 NamedIndividual + 1 305 ClassAssertion (skos:Concept dominant) + 4 183 ObjectPropertyAssertion, recherche des schemes Walton dans les labels, sous-graphe autour de l'Equivoque — lien entre la série et l'ontologie upstream Argumentum | Socle ontologique | | CrossLinks | [Ontology_CrossLinks](Argument_Analysis_Ontology_CrossLinks.ipynb) | Complément CSV canonique Argumentum (`Cards/Fallacies/Argumentum Fallacies - Taxonomy.csv`, 1 408 lignes × 102 colonnes) : 8 colonnes `crossLink_*` (PredatesOn, Denounces, Leverages, Allows, Opposes, Inverts, Mirrors, IsRelatedTo — quasi-vides, 22 relations totales, 1,5% des sophismes ont ≥1 crossLink) + 70 mappings AIF (skos:broadMatch/closeMatch/narrowMatch, absents OWL) + 60 schemes Walton uniques (top : OppositeConsequences_Conflict 5 occurrences). Compare le gap OWL (10 976 NI) vs CSV (1 408 sophismes × 8 langues = 11 264 descriptions) — finding méthodologique : l'effort de curation upstream est porté sur la **taxonomie** (8 langues, 8 familles, 9 niveaux), pas sur les **liens transverses** | CSV canonique | | Ontology_Virtues | [Ontology_Virtues](Argument_Analysis_Ontology_Virtues.ipynb) | Pôle **positif** de l'axe argumentatif (`argumentum_virtues.owl`, 863 KB OWL2/XML) : thésaurus SKOS des **vertus** argumentatives, miroir des sophismes (`aif:goodTenorOf` vs `badTenorOf`). Pont regex→rdflib chargeant 2 639 triplets SKOS (rdflib et owlready2 échouent sur l'OWL/XML fonctionnel), 224 `skos:Concept` bilingues (prefLabel fr 223 / en 223), racine `validArgument`, 14 schemes de Walton rattachés — contraste ABox (sophismes = NamedIndividual + ObjectPropertyAssertion) vs thésaurus d'annotations SKOS (vertus) | Pôle vertus / SKOS | +| FallRules | [Fallacy_Rules_Symboliques](Argument_Analysis_Fallacy_Rules_Symboliques.ipynb) | Étage symbolique de la détection de sophismes **tel qu'il vit dans le cœur EPITA** (`_SYMBOLIC_FALLACY_RULES`, sous-grain 1 #4960) : organe pur `fallacy_rules.py` 13 motifs / 5 familles + minage claim/prémisse + 4 gabarits de justification par famille (G5), vrai moteur `fr_core_news_sm`, **rappel mesuré 4/9** avec diagnostic token-table des échecs (virgule PUNCT, déterminant insérés, POS modèle-dépendant — « incompétent » VERB sous sm), **démonstration des deux réparations G4 #1186** (motifs dormants → 0→1 match) et 3 exercices de réparation de motif | Détection d'arguments | | I2 | [I2_Contre_arguments_ASPIC](groupe-I2-contre-arguments-aspic/I2_Contre_arguments_ASPIC.ipynb) | Travail de groupe : contre-arguments ASPIC (sous-répertoire `groupe-I2-contre-arguments-aspic/`, production autonome) | Travail de groupe | ## Ce que chaque notebook apporte @@ -126,6 +127,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Ontology_AIF** | Charger l'ontologie Argumentum (OWL2/XML, 4,7 MB) via un parseur regex tolérant (rdflib échoue sur 37 axiom `ExactCardinality` mal formés), inventorier les 10 976 NamedIndividual + 4 183 ObjectPropertyAssertion, retrouver les schemes Walton dans les labels multilingues (Sign, Rule), et construire le sous-graphe du sophisme Equivoque (`semanticAmbiguity` + variantes) | 35 min | | **Ontology_CrossLinks** | Compléter la vue OWL par le CSV canonique Argumentum (1 408 lignes × 102 colonnes, 8 langues × 8 familles × 9 niveaux) : quantifier les 8 colonnes `crossLink_*` (PredatesOn 9, Denounces 1, Leverages 4, Allows 1, Opposes 2, Inverts 1, Mirrors 2, IsRelatedTo 2 — total 22, soit 1,5% de couverture par sophisme) et les 70 mappings AIF/Walton (`skos:broadMatch` 57, `skos:closeMatch` 10, `skos:narrowMatch` 3) ; démontrer empiriquement le gap OWL↔CSV (×7,8 en NamedIndividual par label multilingue) et la **sparsity structurelle** des relations transverses vs la richesse de l'arbre taxonomique | 30 min | | **Ontology_Virtues** | Charger le pôle **positif** de la taxonomie Argumentum (`argumentum_virtues.owl`, thésaurus SKOS) via un pont regex→rdflib (rdflib et owlready2 échouent sur l'OWL/XML fonctionnel) : construire 2 639 triplets SKOS sur 224 concepts, inventorier les prédicats SKOS (prefLabel / definition / broader / topConceptOf), contraster le paradigme ABox des sophismes (NamedIndividual + ObjectPropertyAssertion) avec le thésaurus d'annotations des vertus, extraire les libellés bilingues FR/EN et relier chaque vertu à ses schemes de Walton via `aif:goodTenorOf` | 35 min | +| **Fallacy_Rules_Symboliques** | Porter l'étage symbolique du cœur (organe 13 motifs spaCy Matcher / 5 familles + 4 gabarits de justification), mesurer son rappel réel sur un corpus annoté (4/9 sur `fr_core_news_sm`), diagnostiquer token par token pourquoi les motifs syntaxiques échouent (virgule, déterminant, tag POS modèle-dépendant) et démontrer les deux réparations G4 qui rendent vivants des motifs dormants | 35 min | | **08c** | Créer une interface interactive (ipywidgets) pour piloter le pipeline en mode exploratoire | 30 min | | **08b** | Exécuter le pipeline complet en mode batch (Papermill/MCP) avec configuration .env | 20 min | @@ -512,6 +514,7 @@ en référence. | Notebook | Rôle dans la série | Prérequis Notebook dans l'arc | |----------|--------------------|-------------------------------| | `Argumentation-03-Dung-AF-Semantics-Python.ipynb` | **fondationnel** — sémantiques grounded/preferred/stable de Dung (1995) | aucun (point d'entrée de l'arc 3) | +| `Argument_Analysis_Fallacy_Rules_Symboliques.ipynb` | Étage symbolique de la détection de sophismes (consolidation du cœur EPITA, #4960) — organe `fallacy_rules.py` + vrai moteur spaCy | aucun (indépendant ; nécessite `pip install spacy && python -m spacy download fr_core_news_sm`) | | `Argumentation-03b-Value-Based-AF-Python.ipynb` | VAF de Bench-Capon (2003) — Dung enrichi par les valeurs | `03` | | `Argumentation-01-Toulmin-Model-Python.ipynb` | Modèle structurel informel de Toulmin (1958) — 6 composants | aucun (indépendant, pont computationnel vers Dung en fin de parcours) | | `Argumentation-01b-Schemes-Walton-Python.ipynb` | Schémas d'argumentation de Walton (10 schémas stéréotypés, questions critiques) et classifieur lexical déterministe | aucun (indépendant ; niveau intermédiaire entre `01` et `03`, cités en contexte) | diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/fallacy_rules.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/fallacy_rules.py new file mode 100644 index 0000000000..27f4bc35c4 --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/fallacy_rules.py @@ -0,0 +1,475 @@ +"""Règles symboliques de détection de sophismes en français. + +Organe pur (aucune dépendance hors stdlib). Il porte l'étage symbolique de la +détection de sophismes **tel qu'il vit dans le cœur du dépôt EPITA** +(``argumentation_analysis/adapters/french_fallacy_adapter.py``, +``_SYMBOLIC_FALLACY_RULES`` : 5 clés, 13 motifs). Le sous-projet étudiant +``2.3.2-detection-sophismes`` (``jsboigeEpita/2025-Epita-Intelligence-Symbolique``, +fichiers ``symbolic_rules.py`` et ``argument_mining_rules.py``) reste en +généalogie : les règles y ont d'abord existé, la consolidation du cœur en a +retenu une partie et corrigé une autre (G4 #1186). + +**Nature de ce qui est repris — déclaration exigée par la liste « bruit »** +(décision coordinateur du 2026-09-26, portée par le corps de l'épic #4960). +Ce que cet organe tient du dépôt tiers est une **extraction de données** — la +table de règles elle-même — et **non un import de code** : il n'importe rien de +``adapters/``, ``mocks/`` ni de l'orchestration, et sa seule dépendance est +spaCy, en import tardif (l'organe reste importable sans elle). La table est +**re-déclarée ici en données** parce que l'étage dont elle vient n'est **pas +consolidé dans le tronc de CoursIA** : ``argumentation_analysis/`` y est absent +— mesuré sur ``main``, où ``_SYMBOLIC_FALLACY_RULES`` n'existe nulle part — et +cet étage ne vit que dans le dépôt tiers. C'est précisément la condition mise à +l'extraction de données : elle est admise quand l'en-tête du module consommateur +la déclare comme consolidation d'un étage que le tronc n'a pas consolidé. + +Contenu porté — 13 motifs / 5 clés, identiques au cœur : + +- ``FALLACY_RULES`` : AD_HOMINEM_DIRECT (3), PENTE_GLISSANTE (3), + GENERALISATION_HATIVE (2), APPEL_A_LA_TRADITION (3), ARGUMENT_AUTORITE (2). +- ``CLAIM_PATTERNS`` : 2 motifs de claim ; ``PREMISE_PATTERNS`` : 2 motifs de + prémisse. +- ``justify_fallacy()`` : justification française par famille (G5 #1186), + *fail-loud* (#1019) — ``None`` plutôt qu'une justification fabriquée. Les quatre + gabarits hérités couvrent **3 des 5 familles** de cet organe : la pente + glissante et l'appel à la tradition n'en ont pas et rendent ``None`` (mesuré, + section des limites du notebook). + +Ce que la re-fondation a changé par rapport à la première distillation : + +1. **Deux motifs retirés.** ``[NOUN] (le) dire`` et ``[PROPN] dire`` reposaient + sur une simple paire sujet-verbe, sans marqueur d'autorité ; le cœur ne les a + pas retenus. Leur clé ``ARGUMENT_D_AUTORITE_GENERAL`` disparaît avec eux — + les deux motifs restants partagent ``ARGUMENT_AUTORITE``. +2. **Un motif de prémisse retiré.** ``les/des NOUN montrer/indiquer que``, + absent du cœur. +3. **Deux réparations G4 (#1186) adoptées.** Elles viennent du cœur, qui les a + restaurées depuis le projet étudiant en corrigeant ce qui les empêchait de + mordre sur du texte réel : + - *ad hominem, motif 2* : le slot de ponctuation optionnel ``IS_PUNCT?`` + manquait — « Pierre est malhonnête, donc son argument est faux. » ne + matchait jamais, la virgule s'intercalant entre l'adjectif et le + connecteur ; + - *généralisation hâtive, motif 1* : un slot ``NOUN`` surnuméraire précédait + ``exemples``, qui est lui-même le nom — le motif était immatchable. + + Le notebook mesure ces deux réparations : dormants avant, vivants après. +4. **Clés non accentuées.** ``GENERALISATION_HATIVE``, ``APPEL_A_LA_TRADITION``, + ``ARGUMENT_AUTORITE`` sont désormais nommées comme dans le cœur. La première + distillation conservait les accents du source étudiant ; un identifiant + accentué est un risque de régression (cure #2876). + +Divergences conservées, mesurées dans le notebook : + +- Les specs de tokens nues ``{"OP": "+"}`` des motifs de minage sont écrites en + jokers explicites ``{"TEXT": {"REGEX": ".*"}}``. Choix de lisibilité : spaCy + 3.8.16 accepte les deux formes, avec ou sans ``Matcher(validate=True)``, et + elles rendent les mêmes matches (mesure du 2026-09-23). +- ``mine_claims_premises`` déduplique par position de début (plus long match + conservé) : le joker ``OP: "+"`` fait rendre au Matcher toutes les longueurs + imbriquées depuis le même point de départ. + +L'API ``detect_fallacies()`` construit le Matcher si spaCy est disponible (le +modèle français doit être installé : ``python -m spacy download fr_core_news_sm``). +""" + +from __future__ import annotations + +# Étiquettes pédagogiques par famille. +FALLACY_LABELS = { + "AD_HOMINEM": "Attaque personnelle (Ad Hominem)", + "PENTE_GLISSANTE": "Pente glissante (Slippery Slope)", + "GENERALISATION_HATIVE": "Généralisation hâtive (Hasty Generalization)", + "APPEL_A_LA_TRADITION": "Appel à la tradition (Appeal to Tradition)", + "ARGUMENT_D_AUTORITE": "Argument d'autorité (Appeal to Authority)", +} + +FAMILY_KEYS = { + "AD_HOMINEM": ["AD_HOMINEM_DIRECT"], + "PENTE_GLISSANTE": ["PENTE_GLISSANTE"], + "GENERALISATION_HATIVE": ["GENERALISATION_HATIVE"], + "APPEL_A_LA_TRADITION": ["APPEL_A_LA_TRADITION"], + "ARGUMENT_D_AUTORITE": ["ARGUMENT_AUTORITE"], +} + +FALLACY_RULES = { + "AD_HOMINEM_DIRECT": [ + # "Vous avez tort parce que vous êtes [ADJECTIF NÉGATIF]" + { + "PATTERN": [ + {"LOWER": {"IN": ["tu", "vous", "il", "elle"]}}, + {"LEMMA": "être"}, + {"POS": "DET", "OP": "*"}, + {"POS": "ADJ"}, + {"LOWER": {"IN": ["donc", "alors", "parce que", "car"]}}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Attaque personnelle (Ad Hominem)", + }, + # Discrédit généralisé : "On ne peut pas faire confiance à [GROUPE]" + { + "PATTERN": [ + {"LOWER": "on"}, + {"LOWER": "ne"}, + {"LEMMA": "pouvoir"}, + {"LOWER": "pas"}, + {"LEMMA": "faire"}, + {"LOWER": "confiance"}, + {"LEMMA": "à"}, + {"POS": "DET", "OP": "?"}, + {"POS": "NOUN"}, + ], + "FALLACY_TYPE": "Attaque personnelle (Ad Hominem)", + }, + # Attaque de caractère : "[Nom] est [adjectif], donc son argument est faux." + # G4 (#1186) : le slot de ponctuation optionnel manquait, la virgule + # s'intercale dans « Pierre est malhonnête, donc … » et le motif ne + # matchait jamais — le cœur l'a restauré avec cette correction. + { + "PATTERN": [ + {"POS": "PROPN"}, + {"LEMMA": "être"}, + {"POS": "ADJ"}, + {"IS_PUNCT": True, "OP": "?"}, + {"LOWER": {"IN": ["donc", "alors"]}}, + {"POS": "DET"}, + {"POS": "NOUN"}, + {"LEMMA": "être"}, + {"LOWER": "faux"}, + ], + "FALLACY_TYPE": "Attaque personnelle (Ad Hominem)", + }, + ], + "PENTE_GLISSANTE": [ + # "Si nous autorisons A, alors B se produira inévitablement." + { + "PATTERN": [ + {"LOWER": "si"}, + {"LOWER": "on"}, + {"LEMMA": "autoriser"}, + {"POS": "NOUN"}, + {"LOWER": "alors"}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Pente glissante (Slippery Slope)", + }, + # Chaîne de conséquences négatives : "Cela mènera inévitablement à..." + { + "PATTERN": [ + {"LOWER": "cela"}, + {"LEMMA": "mener"}, + {"LOWER": "inévitablement"}, + {"LEMMA": "à"}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Pente glissante (Slippery Slope)", + }, + # "Le premier pas vers..." + { + "PATTERN": [ + {"LOWER": "le"}, + {"LOWER": "premier"}, + {"LOWER": "pas"}, + {"LOWER": "vers"}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Pente glissante (Slippery Slope)", + }, + ], + "GENERALISATION_HATIVE": [ + # "[QUANTIFICATEUR] [GROUPE] sont [ADJECTIF]." + { + "PATTERN": [ + {"LOWER": {"IN": ["tous", "toutes", "chaque", "personne"]}}, + {"POS": "NOUN", "OP": "+"}, + {"LEMMA": "être"}, + {"POS": "ADJ"}, + ], + "FALLACY_TYPE": "Généralisation hâtive (Hasty Generalization)", + }, + # Moteur : "Sur la base de [petit nombre] exemples..." + # G4 (#1186) : un slot NOUN surnuméraire précédait "exemples", qui est + # lui-même le nom — le motif ne matchait jamais. Le cœur l'a restauré + # sans ce slot. + { + "PATTERN": [ + {"LOWER": "sur"}, + {"LOWER": "la"}, + {"LOWER": "base"}, + {"LOWER": "de"}, + {"POS": "NUM"}, + {"LOWER": "exemples"}, + ], + "FALLACY_TYPE": "Généralisation hâtive (Hasty Generalization)", + }, + ], + "APPEL_A_LA_TRADITION": [ + # "Nous avons toujours fait comme ça." + { + "PATTERN": [ + {"LOWER": "on"}, + {"LOWER": "a"}, + {"LOWER": "toujours"}, + {"LEMMA": "faire"}, + {"LOWER": "comme"}, + {"LOWER": "ça"}, + ], + "FALLACY_TYPE": "Appel à la tradition (Appeal to Tradition)", + }, + # "Depuis toujours..." + { + "PATTERN": [{"LOWER": "depuis"}, {"LOWER": "toujours"}], + "FALLACY_TYPE": "Appel à la tradition (Appeal to Tradition)", + }, + # G4 (#1186) : "C'est la tradition." — appel nu, restauré du projet + # étudiant par le cœur. + { + "PATTERN": [ + {"LOWER": "c'"}, + {"LEMMA": "être"}, + {"LOWER": "la"}, + {"LOWER": "tradition"}, + ], + "FALLACY_TYPE": "Appel à la tradition (Appeal to Tradition)", + }, + ], + "ARGUMENT_AUTORITE": [ + # "[GROUPE] dit que [PROPOSITION], donc [CONCLUSION]." + { + "PATTERN": [ + {"POS": "NOUN", "OP": "+"}, + {"LEMMA": "dire"}, + {"LOWER": "que"}, + {"TEXT": {"REGEX": ".*"}}, + {"LOWER": {"IN": ["donc", "alors"]}}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Argument d'autorité (Appeal to Authority)", + }, + # "Selon [source non qualifiée]..." + { + "PATTERN": [ + {"LOWER": "selon"}, + {"POS": "DET"}, + {"POS": "NOUN"}, + {"TEXT": {"REGEX": ".*"}}, + ], + "FALLACY_TYPE": "Argument d'autorité (Appeal to Authority)", + }, + ], +} + +# --- Argument mining : claims et prémisses --- + +CLAIM_PATTERNS = [ + # "Je pense que...", "Nous croyons que..." + { + "PATTERN": [ + {"LOWER": {"IN": ["je", "nous"]}}, + {"LEMMA": {"IN": ["penser", "croire", "affirmer"]}}, + {"LOWER": "que"}, + {"TEXT": {"REGEX": ".*"}, "OP": "+"}, + ] + }, + # Marqueurs de conclusion : "Donc...", "En conclusion..." + { + "PATTERN": [ + {"LOWER": {"IN": ["donc", "par", "en"]}}, + {"LOWER": {"IN": ["conséquent", "conclusion"]}}, + {"IS_PUNCT": True, "OP": "?"}, + {"TEXT": {"REGEX": ".*"}, "OP": "+"}, + ] + }, +] + +PREMISE_PATTERNS = [ + # "Parce que...", "Étant donné que..." + { + "PATTERN": [ + {"LOWER": {"IN": ["parce", "car", "étant"]}}, + {"LOWER": {"IN": ["que", "donné"]}}, + {"TEXT": {"REGEX": ".*"}, "OP": "+"}, + ] + }, + # "Selon [source]...", "D'après [source]..." + { + "PATTERN": [ + {"LOWER": {"IN": ["selon", "d'après"]}}, + {"POS": "NOUN"}, + {"TEXT": {"REGEX": ".*"}, "OP": "+"}, + ] + }, +] + +# --- G5 (#1186) : justification française par famille de sophisme --- +# +# Un gabarit spécifique par famille, au lieu d'une ligne générique pour tout. +# Le projet étudiant émettait ces justifications verbatim ; le cœur les a +# restaurées. Chaque gabarit porte les sous-chaînes de familles qu'il couvre : +# la résolution se fait par clé directe OU par sous-chaîne de famille, car +# l'étiquette d'une détection peut venir d'un étage non symbolique (NLI, LLM) +# dont le vocabulaire diffère. +FALLACY_JUSTIFICATIONS_FR = [ + { + "template": ( + "L'argument attaque la personne ou le caractère de l'adversaire " + "plutôt que de réfuter son argument." + ), + "matches": ["Attaque personnelle", "Ad hominem", "Obstruction"], + }, + { + "template": ( + "Une conclusion générale est tirée à partir d'un échantillon trop " + "limité ou non représentatif." + ), + "matches": ["Généralisation", "Erreur mathématique"], + }, + { + "template": ( + "L'argument s'appuie sur l'opinion d'une figure d'autorité ou sur " + "l'émotion sans fournir de preuves suffisantes pour étayer " + "l'affirmation." + ), + "matches": ["autorité", "Influence", "Appel à l'émotion"], + }, + { + "template": ( + "L'argument tente de discréditer une source ou une affirmation sans " + "aborder le fond de la question, ou s'appuie sur des idées reçues " + "sans les remettre en question." + ), + "matches": ["crédibilité", "Préjugé", "Insuffisance"], + }, +] + + +def justify_fallacy(fallacy_type: str) -> str | None: + """Rend la justification française par famille d'un type de sophisme. + + Ordre de résolution : (1) correspondance directe sur l'une des + sous-chaînes ``matches`` (insensible à la casse), (2) aucune + correspondance → ``None``. *Fail-loud* (#1019) : une famille inconnue rend + ``None``, jamais une justification générique fabriquée. + """ + if not fallacy_type: + return None + needle = fallacy_type.lower() + for entry in FALLACY_JUSTIFICATIONS_FR: + for token in entry["matches"]: + if token.lower() in needle: + return entry["template"] + return None + + +def _wildcard(op: str = "+") -> dict: + """Spec de token joker, équivalent de la spec nue ``{"OP": op}``. + + Forme explicite retenue pour la lisibilité ; spaCy accepte aussi la spec + nue (divergence documentée en en-tête de module). + """ + return {"TEXT": {"REGEX": ".*"}, "OP": op} + + +def rule_counts() -> dict: + """Comptes mesurés des motifs (sophismes, claims, prémisses, gabarits).""" + fallacy = sum(len(v) for v in FALLACY_RULES.values()) + return { + "families": len(FALLACY_LABELS), + "rule_keys": len(FALLACY_RULES), + "fallacy_motifs": fallacy, + "claim_motifs": len(CLAIM_PATTERNS), + "premise_motifs": len(PREMISE_PATTERNS), + "justification_templates": len(FALLACY_JUSTIFICATIONS_FR), + } + + +def validate_patterns() -> list[str]: + """Vérifie que chaque motif porte au moins un attribut hors OP. + + Rend la liste des descriptions invalides (vide = tout est valide). + """ + invalid: list[str] = [] + groups = [("fallacy", FALLACY_RULES), ("claim", {"_": CLAIM_PATTERNS}), ("premise", {"_": PREMISE_PATTERNS})] + for group_name, group in groups: + for key, motifs in group.items(): + for i, motif in enumerate(motifs): + for j, spec in enumerate(motif["PATTERN"]): + if set(spec.keys()) == {"OP"}: + invalid.append(f"{group_name}[{key}].{i}.token{j}: OP seul") + return invalid + + +def detect_fallacies(text: str, nlp=None) -> list[dict]: + """Détecte les sophismes de ``text`` par motifs symboliques. + + Rend une liste de détections ``{"key", "label", "pattern_index", "start", + "end", "excerpt", "justification"}`` ordonnée par position. Nécessite spaCy + et un modèle français (``fr_core_news_sm`` ou plus) ; le paramètre ``nlp`` + permet d'injecter un pipeline déjà chargé. + """ + if nlp is None: + import spacy # import tardif : l'organe reste importable sans spaCy + + nlp = spacy.load("fr_core_news_sm") + from spacy.matcher import Matcher + + doc = nlp(text) + matcher = Matcher(nlp.vocab) + for key, motifs in FALLACY_RULES.items(): + for i, motif in enumerate(motifs): + matcher.add(f"{key}#{i}", [motif["PATTERN"]]) + results = [] + for match_id, start, end in matcher(doc): + key, idx = nlp.vocab.strings[match_id].split("#") + family = next((f for f, keys in FAMILY_KEYS.items() if key in keys), key) + results.append( + { + "key": key, + "label": FALLACY_LABELS.get(family, key), + "pattern_index": int(idx), + "start": start, + "end": end, + "excerpt": doc[start:end].text, + # G5 (#1186) : justification par famille, None quand la famille + # n'a pas de gabarit (fail-loud #1019, jamais fabriquée). + "justification": justify_fallacy(FALLACY_LABELS.get(family, key)), + } + ) + return sorted(results, key=lambda r: (r["start"], r["end"])) + + +def mine_claims_premises(text: str, nlp=None) -> dict: + """Extrait claims et prémisses marquées de ``text`` par motifs. + + Rend ``{"claims": [...], "premises": [...]}`` avec pour chaque extraction + ``{"start", "end", "excerpt"}``. + """ + + def run(matcher, doc): + # Le joker OP:"+" du source rend toutes les longueurs imbriquées : + # on conserve le plus long match par position de début. + par_debut: dict[int, tuple[int, int]] = {} + for _, s, e in matcher(doc): + if s not in par_debut or (e - s) > (par_debut[s][1] - par_debut[s][0]): + par_debut[s] = (s, e) + return [ + {"start": s, "end": e, "excerpt": doc[s:e].text} + for s, e in sorted(par_debut.values()) + ] + + if nlp is None: + import spacy + + nlp = spacy.load("fr_core_news_sm") + from spacy.matcher import Matcher + + doc = nlp(text) + claim_matcher = Matcher(nlp.vocab) + for i, motif in enumerate(CLAIM_PATTERNS): + claim_matcher.add(f"claim#{i}", [motif["PATTERN"]]) + premise_matcher = Matcher(nlp.vocab) + for i, motif in enumerate(PREMISE_PATTERNS): + premise_matcher.add(f"premise#{i}", [motif["PATTERN"]]) + return { + "claims": run(claim_matcher, doc), + "premises": run(premise_matcher, doc), + } \ No newline at end of file diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_fallacy_rules.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_fallacy_rules.py new file mode 100644 index 0000000000..da134b33a7 --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_fallacy_rules.py @@ -0,0 +1,128 @@ +"""Tests de structure de l'organe fallacy_rules (purs, sans spaCy). + +Lance : python tests/test_fallacy_rules.py (depuis le dossier de la serie). +La validation EXECUTEE des motifs spaCy (detect_fallacies sur exemples +français) vit dans le notebook de la serie, commite avec ses outputs. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import fallacy_rules as fr + + +class TestComptesMesures(unittest.TestCase): + """Comptes recomptés — alignés sur la consolidation du cœur (13 motifs).""" + + def test_13_motifs_sophismes(self): + self.assertEqual(fr.rule_counts()["fallacy_motifs"], 13) + + def test_5_cles_5_familles(self): + counts = fr.rule_counts() + self.assertEqual(counts["rule_keys"], 5) + self.assertEqual(counts["families"], 5) + + def test_mining_2_claims_2_premisses(self): + counts = fr.rule_counts() + self.assertEqual(counts["claim_motifs"], 2) + self.assertEqual(counts["premise_motifs"], 2) + + def test_4_gabarits_de_justification(self): + self.assertEqual(fr.rule_counts()["justification_templates"], 4) + + +class TestStructure(unittest.TestCase): + def test_aucune_spec_op_seule(self): + # Divergence conservee : toute spec nue {"OP": ...} est normalisee. + self.assertEqual(fr.validate_patterns(), []) + + def test_chaque_motif_porte_une_etiquette(self): + for key, motifs in fr.FALLACY_RULES.items(): + for motif in motifs: + self.assertIn("PATTERN", motif) + self.assertIn("FALLACY_TYPE", motif) + self.assertTrue(motif["PATTERN"]) + + def test_etiquettes_coherentes_avec_familles(self): + for family, keys in fr.FAMILY_KEYS.items(): + label_attendu = fr.FALLACY_LABELS[family] + for key in keys: + for motif in fr.FALLACY_RULES[key]: + self.assertEqual(motif["FALLACY_TYPE"], label_attendu) + + def test_index_familles_couvre_toutes_les_cles(self): + toutes = {k for keys in fr.FAMILY_KEYS.values() for k in keys} + self.assertEqual(toutes, set(fr.FALLACY_RULES.keys())) + + def test_cles_non_accentuees_comme_le_coeur(self): + # Re-fondation : les cles portent le nom du coeur, sans accents. + toutes = set(fr.FALLACY_RULES.keys()) | {k for keys in fr.FAMILY_KEYS.values() for k in keys} + for cle in toutes: + self.assertEqual(cle, cle.encode("ascii", "ignore").decode(), f"cle accentuee: {cle}") + + +class TestReparationsG4(unittest.TestCase): + """G4 (#1186) : les deux motifs que le coeur a restaures en les corrigeant.""" + + def test_ad_hominem_a_le_slot_de_ponctuation(self): + motif = fr.FALLACY_RULES["AD_HOMINEM_DIRECT"][2]["PATTERN"] + ponctuations = [s for s in motif if s.get("IS_PUNCT") is True] + self.assertEqual(len(ponctuations), 1) + self.assertEqual(ponctuations[0].get("OP"), "?") + + def test_generalisation_sans_slot_noun_surnumeraire(self): + motif = fr.FALLACY_RULES["GENERALISATION_HATIVE"][1]["PATTERN"] + # "exemples" est le NOUN : aucun slot NOUN autonome ne doit preceder. + self.assertEqual(motif[-1], {"LOWER": "exemples"}) + self.assertNotIn({"POS": "NOUN"}, motif) + + def test_autorite_est_une_cle_unique(self): + self.assertIn("ARGUMENT_AUTORITE", fr.FALLACY_RULES) + self.assertNotIn("ARGUMENT_D_AUTORITE_GENERAL", fr.FALLACY_RULES) + self.assertEqual(len(fr.FALLACY_RULES["ARGUMENT_AUTORITE"]), 2) + + +class TestJustificationsG5(unittest.TestCase): + """G5 (#1186) : justification par famille, fail-loud (#1019).""" + + # Le coeur porte 4 gabarits, herites du projet etudiant, dont la + # taxonomie nommait 4 familles. Deux familles de cet organe n'y ont pas + # de gabarit : la resolution rend None (fail-loud #1019), jamais une + # justification generique fabriquee. + FAMILLES_SANS_GABARIT = {"PENTE_GLISSANTE", "APPEL_A_LA_TRADITION"} + + def test_resolution_par_famille(self): + couvertes = [] + for famille, label in fr.FALLACY_LABELS.items(): + justification = fr.justify_fallacy(label) + if famille in self.FAMILLES_SANS_GABARIT: + self.assertIsNone(justification, famille) + else: + self.assertIsNotNone(justification, famille) + couvertes.append(famille) + self.assertEqual(len(couvertes), 3) + + def test_fail_loud_sur_famille_inconnue(self): + self.assertIsNone(fr.justify_fallacy("Sophisme imaginaire")) + self.assertIsNone(fr.justify_fallacy("")) + + def test_resolution_par_sous_chaine_non_symbolique(self): + # Un etage non symbolique peut rendre une etiquette d'une autre + # taxonomie : la resolution par sous-chaine la couvre. + self.assertIsNotNone(fr.justify_fallacy("Appel à l'émotion")) + + +class TestJoker(unittest.TestCase): + def test_wildcard_semantique_inchangee(self): + spec = fr._wildcard("+") + self.assertEqual(spec["OP"], "+") + self.assertIn("TEXT", spec) + + +if __name__ == "__main__": + unittest.main(verbosity=2) \ No newline at end of file