Add Légifrance API integration for live legal text injection

Implements a two-layer approach to ensure legal texts are accurate and current:

Option A — runtime injection (legifrance_client.py):
- LegifranceClient: OAuth2 via PISTE (oauth.piste.gouv.fr), fetches consolidated
  article text via api.piste.gouv.fr/dila/legifrance/lf-engine-app
- Redis cache with 24h TTL (in-memory fallback if no Redis)
- build_filter_prompt() injects live texts into the prompt with explicit priority
  over static descriptions — model uses official Légifrance text, not training memory
- Graceful fallback to static prompt if API is unavailable — filtering never blocks

Option B — weekly sync script (scripts/check_legal_refs.py):
- Reads legal_refs.yaml (11 tracked articles, priority critical/high/medium)
- Fetches current text from Légifrance, diffs against stored baseline
- --init: resolves LEGIARTI IDs on first run
- --check: report only; --update: saves new texts to YAML
- Exit code 2 when changes detected (CI/n8n-compatible)

Supporting changes:
- legal_refs.yaml: tracked articles with code LEGITEXT IDs, priorities, rationale
- requirements.txt: add pyyaml>=6.0.2 and requests>=2.32.0
- .env.example: document PISTE_CLIENT_ID, PISTE_CLIENT_SECRET, LEGIFRANCE_CACHE_TTL
- transparence.tsx: document the Légifrance injection for public transparency
- Fixed portal URL: piste.gouv.fr (not piste.api.gouv.fr — verified June 2026)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 09:19:23 +02:00
parent e8b4bde2f4
commit 67bb0095fa
8 changed files with 764 additions and 2 deletions
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
La Voix du Peuple — Vérification des références légales via Légifrance (PISTE)
Copyright (C) 2026 billisdead — Licence EUPL-1.2
Usage :
python scripts/check_legal_refs.py --init Peuple les legiarti_id (première fois)
python scripts/check_legal_refs.py --check Rapport des changements (sans modifier)
python scripts/check_legal_refs.py --update Met à jour last_text dans le YAML
Codes de sortie :
0 Tout est à jour
1 Erreur (API, fichier, credentials)
2 Changements détectés (utile pour intégration CI ou n8n webhook)
Nécessite PISTE_CLIENT_ID et PISTE_CLIENT_SECRET dans .env
Inscription gratuite : https://piste.gouv.fr
"""
import argparse
import difflib
import os
import sys
from datetime import UTC, datetime
# Ajoute flask-api au path pour importer legifrance_client
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO_ROOT, "artifacts", "flask-api"))
from dotenv import load_dotenv
load_dotenv(os.path.join(REPO_ROOT, ".env"))
try:
import yaml
except ImportError:
print("❌ pyyaml manquant — installez : pip install pyyaml", file=sys.stderr)
sys.exit(1)
from legifrance_client import LegifranceClient
REFS_PATH = os.path.join(REPO_ROOT, "artifacts", "flask-api", "legal_refs.yaml")
PRIORITY_LABELS = {
"critical": "🔴 CRITIQUE",
"high": "🟠 HAUTE",
"medium": "🟡 MOYENNE",
}
def load_refs() -> dict:
with open(REFS_PATH, encoding="utf-8") as f:
return yaml.safe_load(f)
def save_refs(refs: dict):
with open(REFS_PATH, "w", encoding="utf-8") as f:
yaml.dump(refs, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
def cmd_init(client: LegifranceClient, refs: dict):
"""Peuple les legiarti_id manquants dans le YAML."""
print("Résolution des LEGIARTI IDs manquants via Légifrance…\n")
updated = 0
for article in refs.get("articles", []):
if article.get("legiarti_id"):
print(f"{article['ref']} — ID déjà connu")
continue
new_id, text = client.search_article(article["code"], article["article_num"])
if new_id:
article["legiarti_id"] = new_id
updated += 1
snippet = (text or "")[:120].replace("\n", " ")
print(f"{article['ref']}")
print(f" ID : {new_id}")
print(f" Ext : {snippet}")
else:
print(f" ⚠️ {article['ref']} — introuvable (vérifier code/num)")
if updated:
refs["meta"]["last_sync"] = datetime.now(UTC).isoformat()
save_refs(refs)
print(f"\n{updated} ID(s) sauvegardé(s) dans legal_refs.yaml")
else:
print("\nAucun nouvel ID trouvé.")
def cmd_check(client: LegifranceClient, refs: dict, update: bool) -> int:
"""Compare les textes actuels avec ceux stockés. Retourne le nombre de changements."""
today = datetime.now(UTC).date().isoformat()
changes = 0
for article in refs.get("articles", []):
key = article["key"]
ref = article["ref"]
priority = PRIORITY_LABELS.get(article.get("priority", ""), "")
legiarti_id = article.get("legiarti_id")
last_text = (article.get("last_text") or "").strip()
print(f"\n{'' * 62}")
print(f"{priority} {ref} [{key}]")
if legiarti_id:
live_text = client.fetch_article_by_id(legiarti_id) or ""
else:
new_id, live_text = client.search_article(article["code"], article["article_num"])
live_text = live_text or ""
if new_id and update and not legiarti_id:
article["legiarti_id"] = new_id
print(f" ↳ LEGIARTI ID résolu : {new_id}")
live_text = live_text.strip()
if not live_text:
print(" ⚠️ Texte non récupéré — API indisponible ou article introuvable")
continue
if not last_text:
print(" ️ Premier enregistrement (pas de baseline)")
print(f" Extrait : {live_text[:200].replace(chr(10), ' ')}")
changes += 1
if update:
article["last_text"] = live_text
article["verified_at"] = today
elif live_text == last_text:
print(" ✅ Inchangé")
else:
changes += 1
print(" ⚠️ CHANGEMENT DÉTECTÉ")
diff = list(
difflib.unified_diff(
last_text.splitlines(),
live_text.splitlines(),
fromfile="ancienne version",
tofile="version Légifrance actuelle",
lineterm="",
n=2,
)
)
for line in diff[:40]:
prefix = " "
if line.startswith("+"):
prefix = " +"
elif line.startswith("-"):
prefix = " -"
print(f"{prefix}{line}")
if len(diff) > 40:
print(f" … ({len(diff) - 40} lignes supplémentaires)")
if update:
article["last_text"] = live_text
article["verified_at"] = today
if update and changes:
refs["meta"]["last_sync"] = datetime.now(UTC).isoformat()
save_refs(refs)
print(f"\n✅ legal_refs.yaml mis à jour ({changes} article(s) modifié(s))")
return changes
def main():
parser = argparse.ArgumentParser(
description="Vérification des références légales via API Légifrance (PISTE)"
)
parser.add_argument(
"--init",
action="store_true",
help="Peuple les legiarti_id manquants (première exécution)",
)
parser.add_argument(
"--check",
action="store_true",
help="Rapport des changements — ne modifie pas le YAML",
)
parser.add_argument(
"--update",
action="store_true",
help="Met à jour last_text dans legal_refs.yaml",
)
args = parser.parse_args()
if not (args.init or args.check or args.update):
parser.print_help()
sys.exit(0)
client = LegifranceClient()
if not client.available:
print(
"❌ PISTE_CLIENT_ID et/ou PISTE_CLIENT_SECRET non définis dans .env\n"
" Inscription gratuite : https://piste.gouv.fr\n"
" Ajoutez ensuite dans .env :\n"
" PISTE_CLIENT_ID=...\n"
" PISTE_CLIENT_SECRET=...",
file=sys.stderr,
)
sys.exit(1)
refs = load_refs()
if args.init:
cmd_init(client, refs)
return
changes = cmd_check(client, refs, update=args.update)
print(f"\n{'' * 62}")
if changes:
print(f"⚠️ {changes} article(s) à vérifier / mis à jour.")
if not args.update:
print(" Relancez avec --update pour sauvegarder les textes actuels.")
sys.exit(2)
else:
print("✅ Toutes les références sont à jour.")
sys.exit(0)
if __name__ == "__main__":
main()