SHACL TDD pour les agents GraphRAG : une règle de limite exécutive qui empêche les actions néfastes
Encodez une politique d’approbation par les dirigeants avec un plafond de responsabilité de 30 % en SHACL, prouvez-la avec pytest, et observez comment un pare-feu ontologique bloque l’agent lors d’une démonstration de contrat d’un montant de 2,3 millions de dollars.
Lire des notes d’architecture n’est pas la même chose que de mettre en œuvre une règle de gouvernance. Cet article poursuit une comparaison à trois niveaux — RAG classique, GraphRAG et GraphRAG intégré avec OWL/SHACL/policy — sur un contrat de référence d’une valeur de 2,3 millions de dollars, et se concentre sur une compétence transférable : encoder une politique commerciale sous forme de schéma, la valider par un contrôle automatisé, et observer l’agent refuser de continuer.
Le répertoire Ontology RAG Firewall contient le vocabulaire cont:, les fichiers de schéma ainsi que la démonstration hors ligne utilisée ici. Clonez-le, vérifiez que l’ensemble fonctionne correctement sur main, puis, si vous le souhaitez, relancez un commit plus ancien pour expérimenter personnellement le cycle vert-rouge.
Vérifier la version de référence sur main
git clone https://github.com/cloudbadal007/ontology-rag-firewall
cd ontology-rag-firewall
pip install -e ".[dev]"
pytest -q # 18 passed (full suite)
python examples/demo_offline.py
Pincher sur 6318929 est optionnel si la dernière astuce vérifiée de l’article est importante ; l’astuce correspondante sur main pourrait déjà être plus récente.
18 validés. La démo hors ligne devrait déjà présenter une mise en garde destinée aux dirigeants dans la section relative à l’indemnisation, rédigée à peu près comme ceci :
Safe to act: 🚫 NO
- Flagged: 5
...
⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: 25.00%. Agent action requires executive sign-off.
...
AGENT ACTION: HALTED. Routed to human review queue.
Total value protected: $2,300,000
Cette mise en garde correspond exactement à ce que la nouvelle version a introduit. Le reste décrit comment elle a été mise en œuvre grâce à une approche de développement basée sur les tests en premier.
Onze modèles de nœuds existent déjà dans contract_domain_shacl.ttl, couvrant les conditions de paiement, les délais de préavis, les SLA de disponibilité, l’extraction à faible fiabilité, les lacunes dans les solutions proposées, la renouvellement automatique, l’absence de clauses d’indemnisation, l’examen des dommages directs, un ratio plafond/valorisation de 10 %, un cas à forte valeur mais faible plafond absolu, ainsi que le ratio destiné aux dirigeants mis en évidence dans cette présentation.
Dans le cas de démonstration, un plafond de 575 000 $ représente 25 % de 2,3 millions $ — ce qui est supérieur au plafond minimal de 10 % — donc la forme de ratio plus ancienne reste inactive. La forme exécutive comble ce point aveugle pour les accords coûteux.
En termes simples, l’exigence supplémentaire du service des achats est la suivante :
Chaque fois que la valeur de l’accord est d’au moins 500 000 $ et que le plafond des indemnités est inférieur à 30 % de cette valeur, un agent doit obtenir l’approbation exécutive avant d’agir.
Cette phrase se traduit par ExecutiveCapRatioShape ainsi que par une paire de cas pytest.
Étape 1 — Échouer en premier
Rédigez toujours l’assertion avant le TTL.
Dans le main actuel, ces vérifications passent déjà. Pour ressentir l’échec, passez à bbeb15e (forme préliminaire), insérez les tests, observez l’affichage en rouge, ajoutez la forme de l’Étape 2, puis revenez à main.
Ajoutez ou comparez ce cas dans tests/test_shacl_constraints.py:
def test_liability_cap_below_30_percent_on_high_value_contract() -> None:
"""
25% cap on a $2.3M contract must trigger ExecutiveCapRatioShape.
Existing shapes (10% ratio, $100K absolute) do not catch 575K / 2.3M.
"""
clause = ExtractedClause(
"test-cap-ratio",
"LiabilityClause",
"text",
{"liabilityCap": 575_000, "liabilityScope": "DirectDamagesOnly"},
0.9,
1,
)
graph = ClauseRDFBuilder().build(clause, 2_300_000)
conforms, violations, _ = SHACLContractValidator().validate(graph)
assert not conforms
assert any(
"30%" in v or "executive" in v.lower() for v in violations
), violations
def test_liability_cap_at_32_percent_no_executive_flag() -> None:
"""32.6% cap on $2.3M should not trigger the 30% executive rule."""
clause = ExtractedClause(
"test-cap-ratio-ok",
"LiabilityClause",
"text",
{"liabilityCap": 750_000, "liabilityScope": "FullDamages"},
0.9,
1,
)
graph = ClauseRDFBuilder().build(clause, 2_300_000)
_, violations, _ = SHACLContractValidator().validate(graph)
cap_ratio_hits = [
v for v in violations if "30%" in v or "executive" in v.lower()
]
assert len(cap_ratio_hits) == 0, cap_ratio_hits
Exécutez :
pytest tests/test_shacl_constraints.py::test_liability_cap_below_30_percent_on_high_value_contract -v
À quoi ressemble le rouge
Au moment où la forme n’existe pas (par exemple sur bbeb15e) :
FAILED tests/test_shacl_constraints.py::test_liability_cap_below_30_percent_on_high_value_contract
AssertionError: ... executive ...
Dans le main actuel, la même invocation affiche du vert. Ensuite vient le TTL lui-même — déjà intégré dans la version principale, reproduit afin que le modèle puisse être réutilisé.
Étape 2 — Créer l’auteur de la forme
Appendez du contenu à ontologies/contract_domain_shacl.ttl. Assurez-vous que cont: fait référence au espace de noms OWL via l’IRI brut de GitHub (évitez d’inventer un chemin /contract# qui ne se résout pas) :
https://raw.githubusercontent.com/cloudbadal007/ontology-rag-firewall/main/ontologies/contract_domain_owl.ttl#
Les générateurs d’instances créent des URIs sous la même base (…#instance/).
Réexécuter avec bbeb15e ? Utilisez à nouveau l’URI de préfixe déjà présent dans le fichier SHACL de cette révision. Sur la branche main, préférez l’IRI brut afin que l’ontologie, les formes et les tests soient cohérents.
cont:ExecutiveCapRatioShape a sh:NodeShape ;
sh:targetClass cont:LiabilityClause ;
sh:severity sh:Warning ;
sh:message "⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: {?capRatio}%. Agent action requires executive sign-off." ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:select """
PREFIX cont: <https://raw.githubusercontent.com/cloudbadal007/ontology-rag-firewall/main/ontologies/contract_domain_owl.ttl#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT $this ?capRatio WHERE {
?contract cont:hasLiabilityClause $this ;
cont:contractValue ?v .
$this cont:liabilityCap ?cap .
BIND((xsd:decimal(?cap) / xsd:decimal(?v) * 100) AS ?capRatio)
FILTER (xsd:decimal(?v) >= 500000)
FILTER (?capRatio < 30)
}
""" ;
] .
Trois choix de conception sont intentionnels :
- Le
FILTERexigeant une valeur>= 500000cible les transactions à forte valeur ; le même pourcentage a un sens différent pour une commande de 50 000 $. - L’inclusion de
{?capRatio}dans le message destiné aux utilisateurs permet aux examinateurs de voir le pourcentage mesuré plutôt qu’une mise en garde vague. - Le seuil de 30 % correspond à une politique interne de l’organisation — modifiez cette valeur si le service juridique en demande 40 %. Le fichier est l’artefact de cette politique.
Étape 3 — Associer les violations à des clauses lisible
Les formes génèrent des violations de machine ; le pare-feu transforme les occurrences de mots-clés en lignes de rapport au niveau des clauses. Dans main, EXECUTIVE est déjà répertorié parmi les tokens de responsabilité dans firewall.py:
"LiabilityClause": ["LIABILITY", "LOW CONFIDENCE", "LEGAL REVIEW", "HIGH-VALUE", "EXECUTIVE"],
Lors de la réexécution de bbeb15e, ajoutez ce token à côté de la forme — sinon la démonstration pourrait calculer la violation sans l’associer à la clause d’indemnisation.
Étape 4 — Réexécuter le jeu de tests et la démonstration
pytest -q # 18 passed (entire repo)
pytest tests/test_shacl_constraints.py -v # 8 passed (this file)
python examples/demo_offline.py
Attendez que l’ensemble complet soit généré en quelques secondes. Le rapport de la démonstration inclura une ligne dédiée à la clause d’indemnisation (le nombre de drapeaux peut rester inchangé lorsque plusieurs violations concernent la même clause) :
⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: 25.00%. Agent action requires executive sign-off.
AGENT ACTION: HALTED. Routed to human review queue.
Total value protected: $2,300,000
Une politique codée, prouvée et visible. Cet état cyclique est ce qui permet d’appliquer la règle au domaine suivant.
Pourquoi pas simplement le solliciter ?
Insérer les mêmes directives à 30 % dans un prompt système échoue lorsque l’avocat reformule la clause, lorsque l’instruction se perd dans un contexte long, lorsque quelqu’un modifie les prompts sans tenir compte du contexte de conformité, ou encore lorsque les auditeurs demandent quelle version a appliqué quelle règle quel jour.
Une forme est déterministe dans RDF typé, est gérée par un système de contrôle de version, inclut un test de régression, produit des preuves structurées comprenant le ratio mesuré, et ne peut disparaître simplement parce que quelqu’un a privilégié la fluidité ailleurs.
La gouvernance des systèmes agents nécessite des contraintes formelles en plus de la récupération et de la génération d’informations. La politique réside dans la forme ; la preuve se trouve dans le test ; le texte de violation constitue l’artefact d’audit.
Méthode reproductible pour l’extension
docs/extending.md explique cela en détail ; la version abrégée est :
- Formuler la règle dans un langage compris par le responsable de la conformité.
État cible : chaque forme dispose d’un test ; chaque test correspond à une conséquence métier. Les modifications légales ont une durée de vie limitée ; les tests CI valident les fonctionnalités ; la situation reste soumise à révision.
Feuille de route au-delà d’un seul accord
Aujourd’hui, le pare-feu utilise des chemins basés sur un seul accord. Deux lacunes restent à combler : les résumés par lots pour l’ensemble du portefeuille (examples/demo_batch_processing.py en est un exemple de base), ainsi que le contexte multi-nœuds des fournisseurs (stockage, incidents) une fois qu’un graphe de propriétés lié existe — et non une URL provisoire.
Jusqu’à ce moment-là, suivez cette boucle : écrivez la forme, validez-la, observez les résultats, puis appliquez le même schéma au domaine suivant.
Gérez un registre par forme : propriétaire, date d’entrée en vigueur, identifiant de la note source et identifiant du nœud pytest — afin que chaque ligne de git blame permette d’obtenir un historique complet des modifications. Lorsque les seuils changent, versionnez la chaîne de caractères correspondante et ajoutez des tests de limite pour éviter que les anciens critères ne reprennent automatiquement effet. Considérez les mappages mot-clé→clause comme une interface API : capturez l’output des démonstrations dans le CI afin que les modifications ne puissent pas supprimer EXECUTIVE de la liste des tokens d’indemnisation sans entraîner l’échec du processus. Préférez les formes additives aux modifications de blocs SPARQL partagés ; les formes indépendantes permettent un retour en arrière propre en cas d’échec d’une expérience de politique. Enfin, publiez le ratio mesuré dans chaque avertissement destiné aux utilisateurs — les reviewers font plus confiance à des chiffres qu’ils peuvent recalculer à partir du RDF qu’à des messages génériques indiquant simplement « besoin d’approbation ».