Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ CoursIA copy
| `_probabilistic_handler.py` (152 lines) | `argumentation_analysis/agents/core/logic/probabilistic_handler.py` | a8025f60 (2026-07-02) |
| `_dialogue_handler.py` (153 lines) | `argumentation_analysis/agents/core/logic/dialogue_handler.py` | a8025f60 (2026-07-02) |
| `_tweety_initializer.py` (365 lines) | `argumentation_analysis/agents/core/logic/tweety_initializer.py` | a8025f60 (2026-07-02) |
| `_fallacy_workflow_plugin.py` (1763 lines) | `argumentation_analysis/plugins/fallacy_workflow_plugin.py` | ecfd9b9c (2026-09-29, tronc EPITA figé sur issue #18391 portee 2) |
| `_exploration_plugin.py` (191 lines) | `argumentation_analysis/plugins/exploration_plugin.py` | ecfd9b9c (2026-09-29, tronc EPITA figé sur issue #18391 portee 2) |
| `_taxonomy_navigator.py` (192 lines) | `argumentation_analysis/agents/utils/taxonomy_navigator.py` | ecfd9b9c (2026-09-29, tronc EPITA figé sur issue #18391 portee 2) |
| `_taxonomy_local_overrides.py` (123 lines) | `argumentation_analysis/utils/taxonomy_local_overrides.py` | ecfd9b9c (2026-09-29, dep transitive de fallacy_workflow_plugin) |
| `_taxonomy_tree.py` (55 lines) | `argumentation_analysis/utils/taxonomy_tree.py` | ecfd9b9c (2026-09-29, dep transitive de taxonomy_navigator) |
| `_plaintext_destination.py` (75 lines) | `argumentation_analysis/core/plaintext_destination.py` | ecfd9b9c (2026-09-29, dep transitive de fallacy_workflow_plugin) |
| `_identification_models.py` (59 lines) | `argumentation_analysis/plugins/identification_models.py` | ecfd9b9c (2026-09-29, dep transitive de fallacy_workflow_plugin) |

Rationale for verbatim import
-----------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# _epita_vendor_shim.py -- EPITA-IS verbatim import bridge for issue #18391
#
# PURPOSE
# -------
# Verbatim upstream files vendored in this directory:
#
# - `_fallacy_workflow_plugin.py` (commit ecfd9b9c)
# - `_exploration_plugin.py` (commit ecfd9b9c)
# - `_taxonomy_navigator.py` (commit ecfd9b9c)
# - `_taxonomy_local_overrides.py` (commit ecfd9b9c)
# - `_taxonomy_tree.py` (commit ecfd9b9c)
# - `_plaintext_destination.py` (commit ecfd9b9c)
# - `_identification_models.py` (commit ecfd9b9c)
#
# All seven contain literal imports of the form
# `from argumentation_analysis.<sub>.<name> import ...`. Their bodies
# are byte-identical to the upstream source at commit ecfd9b9c -- the
# "Verbatim integrity: byte-for-byte identical" header on each file
# is the contract documented in NOTICE-EPITA.
#
# This module re-exposes each vendored CoursIA file as a virtual
# `argumentation_analysis.<sub>.<name>` sys.modules entry, so the
# verbatim imports inside the vendored files resolve transparently
# (just like the C186g `_jvm_setup_compat` shim does for
# `argumentation_analysis.core.jvm_setup`).
#
# ANTI-REGRESSION (D)
# -------------------
# 0 `pass`, 0 `return None`, 0 `sorry`, 0 `raise NotImplementedError`
# added. This is a thin glue layer -- it only registers already-
# vendored modules under their upstream Python dotted names. No
# derivation logic.
#
# SCOPE
# -----
# This shim is the portee 2 of issue #18391 acceptance 2/5 of
# EPIC #4960 (entonnoir taxonomique agentique). It does NOT cover
# the LLM-backed execution (run_guided_analysis requires an LLM
# endpoint and a key) -- it only unblocks `import` so that consumers
# can read class definitions, type hints, and constant tables.

from __future__ import annotations

import importlib
import sys
import types


_PROXY_INSTALLED: bool = False


# Mapping : upstream_dotted_name -> local_vendored_module_name
# Each local module is the byte-for-byte verbatim file we vendored
# at commit ecfd9b9c (tronc EPITA fige). The mapping is deterministic
# (one-line per file) and the SHA of each is recorded in NOTICE-EPITA.
#
# LOAD ORDER matters: leaf modules first (no internal deps), then
# mid-level, then leaves-of-leaves-on-leaves that import them.
# - `taxonomy_local_overrides`, `taxonomy_tree`, `plaintext_destination`,
# `identification_models` are pure utility modules (no internal
# `argumentation_analysis` imports in their bodies beyond the
# `from semantic_kernel...` lines they may share).
# - `taxonomy_navigator` imports from `taxonomy_local_overrides` and
# `taxonomy_tree` (verified at vendor time, body imports at L5-9).
# - `exploration_plugin` imports from `taxonomy_navigator`.
# - `fallacy_workflow_plugin` imports from all of the above plus
# `plaintext_destination` and `identification_models`.
# Hence: leaves first, then mid, then top.
VENDOR_MAP: list[tuple[str, str]] = [
# Leaves (no internal argumentation_analysis deps beyond stdlib/SK)
("argumentation_analysis.utils.taxonomy_local_overrides",
"argumentation_lib._taxonomy_local_overrides"),
("argumentation_analysis.utils.taxonomy_tree",
"argumentation_lib._taxonomy_tree"),
("argumentation_analysis.core.plaintext_destination",
"argumentation_lib._plaintext_destination"),
("argumentation_analysis.plugins.identification_models",
"argumentation_lib._identification_models"),
# Mid (depends on leaves)
("argumentation_analysis.agents.utils.taxonomy_navigator",
"argumentation_lib._taxonomy_navigator"),
("argumentation_analysis.plugins.exploration_plugin",
"argumentation_lib._exploration_plugin"),
# Top (depends on everything)
("argumentation_analysis.plugins.fallacy_workflow_plugin",
"argumentation_lib._fallacy_workflow_plugin"),
]


def install_epita_vendor_shim() -> bool:
"""Register vendored modules under their upstream dotted names.

Idempotent: returns False (no-op) if already installed in this
process. Best-effort: if a single mapping fails to import, the
other six still install; the failure is logged but not raised.

Returns:
True if installation took place, False if already installed.
"""
global _PROXY_INSTALLED
if _PROXY_INSTALLED:
return False

def _ensure_package(name: str) -> types.ModuleType:
mod = sys.modules.get(name)
if mod is None:
mod = types.ModuleType(name)
mod.__path__ = [] # namespace package
sys.modules[name] = mod
return mod

# Build the parent package chain lazily as we register submodules.
# We need to ensure all of these exist:
# argumentation_analysis
# argumentation_analysis.plugins
# argumentation_analysis.agents
# argumentation_analysis.agents.utils
# argumentation_analysis.utils
# argumentation_analysis.core
for parent in {
"argumentation_analysis",
"argumentation_analysis.plugins",
"argumentation_analysis.agents",
"argumentation_analysis.agents.utils",
"argumentation_analysis.utils",
"argumentation_analysis.core",
}:
_ensure_package(parent)

for upstream_name, local_name in VENDOR_MAP:
try:
local_mod = importlib.import_module(local_name)
except Exception as exc: # noqa: BLE001
# Best-effort: a missing optional dep (semantic_kernel, etc.)
# may keep one of the seven from importing. Skip silently;
# the verbatim import will then re-raise the G.9 fingerprint.
sys.stderr.write(
f"[epita_vendor_shim] skip {upstream_name}: {exc!r}\n"
)
continue
sys.modules[upstream_name] = local_mod

_PROXY_INSTALLED = True
return True


# Module-load-time installation (idempotent, safe to re-import).
try:
install_epita_vendor_shim()
except Exception: # noqa: BLE001
# Best-effort: if anything blows up at module-load (cycle, missing
# import), the verbatim modules stay unbridged and downstream
# imports re-raise the original G.9 ModuleNotFoundError.
pass


__all__ = ["install_epita_vendor_shim", "VENDOR_MAP"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-


# Verbatim copy of `argumentation_analysis/plugins/exploration_plugin.py` from
# the 2025-Epita-Intelligence-Symbolique project
# (https://github.com/jsboigeEpita/2025-Epita-Intelligence-Symbolique),
# Copyright (c) 2025 jsboigeEpita, MIT License.
# Source commit: ecfd9b9c (2026-09-29).
# Verbatim import rationale: see NOTICE-EPITA at the root of this directory.
#
# Verbatim integrity: file content below this header is byte-for-byte identical
# to the upstream source at the cited commit. No CoursIA modification.
#
# This module is a verbatim vendoring of the EPITA-IS tronc plugin set
# (FallacyWorkflowPlugin + ExplorationPlugin + TaxonomyNavigator) used by
# Argumentation-02-Fallacies-Detection. The Python package is renamed
# `argumentation_lib` here so that the upstream `from argumentation_analysis.X`
# imports are rewritten (via a conftest-time sys.path shim, see
# `_paths.py`) -- see NOTICE-EPITA.
#
# The vendoring covers portee 2 of issue #18391 (entonnoir taxonomique
# agentique). The Lexique (DETECTEUR_SOPHISMES, 38 entrees) shipped in
# #18506 is kept as the deterministic baseline; this file enables the
# agentic comparison path (run_guided_analysis + exploration_plugin).
"""ExplorationPlugin — constrained kernel functions for taxonomy navigation.

This plugin provides the ONLY tools available to the 'slave' LLM during
hierarchical fallacy detection. The LLM must navigate the taxonomy tree
by calling these functions — it cannot respond freely.

Recovered from commit d2fdd930 and enhanced with structured output.
"""

import json
import logging
import math
from typing import Annotated, Optional, Tuple

from semantic_kernel.functions.kernel_function_decorator import kernel_function

from argumentation_analysis.agents.utils.taxonomy_navigator import TaxonomyNavigator

logger = logging.getLogger(__name__)

#: Scores des niveaux lexicaux historiques — acceptés, mais le contrat que le
#: prompt feuille énonce est un nombre (#2746).
_CONFIDENCE_LEVELS = {"high": 0.9, "medium": 0.7, "low": 0.4}

#: Ce qu'une confiance illisible dégrade : le défaut "medium" du paramètre,
#: même valeur que le lecteur aval (fallacy_workflow_plugin lit
#: ``lr.get("confidence", 0.7)``). L'entrée rejetée est nommée dans la note,
#: jamais silencieuse.
_UNREADABLE_CONFIDENCE_SCORE = 0.7


def _confidence_score(raw: object) -> Tuple[float, str]:
"""Le contrat unique de ``confirm_fallacy`` : un nombre dans ``[0, 1]``
(ce que le prompt feuille demande) ou un nom de niveau.

Rend ``(score, note)``. La note est vide quand l'entrée a été lue
fidèlement ; sinon elle nomme l'entrée rejetée. Jamais d'exception — un
appel de confirmation valide ne doit pas se perdre sur la forme de sa
confiance (#2746).
"""
rejected = (
f"unreadable confidence {raw!r}, defaulted to "
f"{_UNREADABLE_CONFIDENCE_SCORE}"
)
if isinstance(raw, bool) or raw is None:
return _UNREADABLE_CONFIDENCE_SCORE, rejected
if isinstance(raw, (int, float)):
value = float(raw)
elif isinstance(raw, str):
text = raw.strip()
level = _CONFIDENCE_LEVELS.get(text.lower())
if level is not None:
return level, ""
try:
value = float(text.replace(",", "."))
except ValueError:
return _UNREADABLE_CONFIDENCE_SCORE, rejected
else:
return _UNREADABLE_CONFIDENCE_SCORE, rejected
if math.isnan(value) or math.isinf(value):
return _UNREADABLE_CONFIDENCE_SCORE, rejected
return min(max(value, 0.0), 1.0), ""


class ExplorationPlugin:
"""Plugin for constrained taxonomy navigation during fallacy detection.

Provides 3 kernel functions:
- explore_branch: Navigate to a taxonomy node and see its children
- confirm_fallacy: Confirm a specific node as the identified fallacy
- conclude_no_fallacy: Abandon exploration — no fallacy found in this branch
"""

def __init__(self, taxonomy_navigator: TaxonomyNavigator, language: str = "fr"):
self.taxonomy_navigator = taxonomy_navigator
self.language = language
self._alt_lang = "en" if language == "fr" else "fr"

@kernel_function(
name="explore_branch",
description=(
"Navigate to a taxonomy node to see its details and children. "
"Use this to explore deeper into a branch that might contain "
"the relevant fallacy. Returns the node info and its children."
),
)
def explore_branch(
self,
node_pk: Annotated[str, "The PK (ID) of the taxonomy node to explore"],
) -> Annotated[str, "JSON with node details and children"]:
"""Navigate to a taxonomy node and return its details + children."""
node = self.taxonomy_navigator.get_node(node_pk)
if not node:
return json.dumps({"error": f"Node {node_pk} not found"})

children = self.taxonomy_navigator.get_children(node_pk)
lang = self.language
branch_info = {
"node": {
"pk": node.get("PK", ""),
"path": node.get("path", ""),
"name": (
node.get(f"text_{lang}", "")
or node.get("nom_vulgarisé", "")
or node.get(f"text_{self._alt_lang}", "")
),
"description": node.get(f"desc_{lang}", "")
or node.get(f"desc_{self._alt_lang}", ""),
"example": node.get(f"example_{lang}", "")
or node.get(f"example_{self._alt_lang}", ""),
"depth": node.get("depth", ""),
"is_leaf": len(children) == 0,
},
"children": [
{
"pk": c.get("PK", ""),
"name": (
c.get(f"text_{lang}", "")
or c.get("nom_vulgarisé", "")
or c.get(f"text_{self._alt_lang}", "")
),
"description": c.get(f"desc_{lang}", "")
or c.get(f"desc_{self._alt_lang}", ""),
"example": (
c.get(f"example_{lang}", "")
or c.get(f"example_{self._alt_lang}", "")
)[:200],
}
for c in children
],
"children_count": len(children),
}
return json.dumps(branch_info, ensure_ascii=False)

@kernel_function(
name="confirm_fallacy",
description=(
"Confirm that a specific taxonomy node is the identified fallacy. "
"Call this when you are confident that the current node matches "
"the fallacy in the analyzed text. Provide justification."
),
)
def confirm_fallacy(
self,
node_pk: Annotated[str, "The PK of the confirmed fallacy node"],
confidence: Annotated[
str,
"Confidence: a number between 0.0 and 1.0 (as the leaf prompt "
"specifies), or a level 'high'/'medium'/'low'",
] = "medium",
justification: Annotated[str, "Why this fallacy matches the text"] = "",
) -> Annotated[str, "Confirmation result"]:
"""Confirm a fallacy identification with justification."""
node = self.taxonomy_navigator.get_node(node_pk)
if not node:
return json.dumps({"error": f"Node {node_pk} not found"})

confidence_score, confidence_note = _confidence_score(confidence)

lang = self.language
result = {
"confirmed": True,
"pk": node.get("PK", ""),
"path": node.get("path", ""),
"name": (
node.get(f"text_{lang}", "")
or node.get("nom_vulgarisé", "")
or node.get(f"text_{self._alt_lang}", "")
),
"name_fr": node.get("text_fr", ""),
"confidence": confidence_score,
"justification": justification,
}
if confidence_note:
result["confidence_note"] = confidence_note
return json.dumps(result, ensure_ascii=False)

@kernel_function(
name="conclude_no_fallacy",
description=(
"Conclude that no relevant fallacy was found in the current branch. "
"Call this when you have explored the branch sufficiently and determined "
"that none of the nodes match the analyzed text."
),
)
def conclude_no_fallacy(
self,
reason: Annotated[str, "Why no fallacy was found in this branch"],
) -> Annotated[str, "Conclusion recorded"]:
"""Conclude that no fallacy was found in this branch."""
return json.dumps({"confirmed": False, "reason": reason}, ensure_ascii=False)
Loading
Loading