feat: MVP change-alert systeem via AgentMail + Mistral

- alerts.py: detecteert wetswijzigingen, genereert AI change-summary,
  stuurt e-mail via AgentMail API
- Mistral Large voor begrijpelijke samenvatting van de diff
- AgentMail endpoint: /inboxes/{addr}/messages/send
- Test: gesimuleerde Grondwet art. 13 wijziging succesvol verstuurd

Sluit #38
This commit is contained in:
Coornhert 2026-03-30 10:52:22 +02:00
parent 6db0f6afc3
commit 479a557f86

203
src/wetgit/ai/alerts.py Normal file
View file

@ -0,0 +1,203 @@
"""Change-alerts — stuur notificaties bij wetswijzigingen.
Vergelijkt de huidige staat met de vorige en stuurt een e-mail
met een AI-gegenereerde change-summary via AgentMail.
Usage:
python -m wetgit.ai.alerts --bwb-id BWBR0001840 --diff "..."
python -m wetgit.ai.alerts --test # Stuur test-alert
"""
from __future__ import annotations
import logging
import os
from datetime import date
import httpx
logger = logging.getLogger(__name__)
MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions"
AGENTMAIL_API_URL = "https://api.agentmail.to/v0"
SUMMARY_PROMPT = """Je bent een juridisch communicatie-expert. Vat de volgende wetswijziging samen in begrijpelijk Nederlands (B1-niveau).
Structuur:
1. **Wat is er veranderd?** (1-2 zinnen)
2. **Welke artikelen zijn geraakt?** (lijst)
3. **Waarom is dit relevant?** (1-2 zinnen, voor een niet-jurist)
Wees kort en concreet. Max 150 woorden."""
def send_change_alert(
bwb_id: str,
titel: str,
diff_text: str,
recipients: list[str] | None = None,
mistral_api_key: str | None = None,
agentmail_api_key: str | None = None,
) -> bool:
"""Genereer een change-summary en stuur een e-mail alert.
Args:
bwb_id: BWB identificatienummer van de gewijzigde regeling.
titel: Titel van de regeling.
diff_text: Git diff van de wijziging.
recipients: E-mailadressen (default: coornhert@wetgit.nl).
mistral_api_key: Mistral API key.
agentmail_api_key: AgentMail API key.
Returns:
True als de alert succesvol verstuurd is.
"""
mistral_key = mistral_api_key or os.environ.get("MISTRAL_API_KEY", "")
agentmail_key = agentmail_api_key or os.environ.get("AGENTMAIL_API_KEY", "")
recipients = recipients or ["coornhert@wetgit.nl"]
if not mistral_key or not agentmail_key:
logger.error("MISTRAL_API_KEY of AGENTMAIL_API_KEY ontbreekt")
return False
# Stap 1: Analyseer de diff
added = sum(1 for l in diff_text.split("\n") if l.startswith("+") and not l.startswith("+++"))
removed = sum(1 for l in diff_text.split("\n") if l.startswith("-") and not l.startswith("---"))
# Stap 2: Genereer AI-samenvatting van de wijziging
summary = _generate_change_summary(titel, diff_text, mistral_key)
if not summary:
summary = f"De {titel} is gewijzigd (+{added}/-{removed} regels). Bekijk de diff voor details."
# Stap 3: Bouw de e-mail
subject = f"WetGit Alert: {titel} gewijzigd ({date.today().isoformat()})"
body = f"""WetGit Change Alert
{'=' * 40}
Regeling: {titel}
BWB-ID: {bwb_id}
Datum: {date.today().isoformat()}
Wijziging: +{added} / -{removed} regels
Samenvatting
{'-' * 40}
{summary}
Details
{'-' * 40}
Bekijk de volledige wijziging:
https://git.wetgit.nl/wetgit/rijk/commits/branch/main
Officiele tekst:
https://wetten.overheid.nl/{bwb_id}
---
Dit is een automatisch bericht van WetGit (wetgit.nl).
Dit is geen juridisch advies.
"""
# Stap 4: Verstuur via AgentMail
return _send_email(
from_address="coornhert@wetgit.nl",
to_addresses=recipients,
subject=subject,
body=body,
agentmail_key=agentmail_key,
)
def _generate_change_summary(titel: str, diff_text: str, api_key: str) -> str | None:
"""Genereer een AI-samenvatting van de wetswijziging."""
# Beperk diff tot ~4000 chars
if len(diff_text) > 4000:
diff_text = diff_text[:4000] + "\n\n[...diff ingekort...]"
try:
resp = httpx.post(
MISTRAL_API_URL,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": "mistral-large-latest",
"messages": [
{"role": "system", "content": SUMMARY_PROMPT},
{"role": "user", "content": f"Wijziging in de {titel}:\n\n```diff\n{diff_text}\n```"},
],
"temperature": 0.3,
"max_tokens": 300,
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
logger.error("Mistral API fout bij change summary: %s", e)
return None
def _send_email(
from_address: str,
to_addresses: list[str],
subject: str,
body: str,
agentmail_key: str,
) -> bool:
"""Verstuur e-mail via AgentMail API."""
try:
resp = httpx.post(
f"{AGENTMAIL_API_URL}/inboxes/{from_address}/messages/send",
headers={
"Authorization": f"Bearer {agentmail_key}",
"Content-Type": "application/json",
},
json={
"to": to_addresses,
"subject": subject,
"text": body,
},
timeout=15,
)
resp.raise_for_status()
logger.info("Alert verstuurd naar %s: %s", to_addresses, subject)
return True
except httpx.HTTPError as e:
logger.error("AgentMail fout: %s%s", e, getattr(e, 'response', {}).text if hasattr(e, 'response') else '')
return False
if __name__ == "__main__":
import argparse
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
parser = argparse.ArgumentParser(description="WetGit change alerts")
parser.add_argument("--test", action="store_true", help="Stuur een test-alert")
parser.add_argument("--bwb-id", default="BWBR0001840")
parser.add_argument("--to", default="coornhert@wetgit.nl")
args = parser.parse_args()
if args.test:
# Simuleer een wijziging in de Grondwet
test_diff = """--- a/wet/grondwet/BWBR0001840/README.md
+++ b/wet/grondwet/BWBR0001840/README.md
@@ -25,7 +25,7 @@
### Artikel 13
-**1.** Het briefgeheim is onschendbaar, behalve, in de gevallen bij de wet bepaald, op last van de rechter.
+**1.** Ieder heeft recht op eerbiediging van het brief- en telecommunicatiegeheim, behalve in de gevallen bij de wet bepaald, op last van de rechter.
-**2.** Het telefoon- en telegraafgeheim is onschendbaar, behalve, in de gevallen bij de wet bepaald, door of met machtiging van hen die daartoe bij de wet zijn aangewezen.
+**2.** Het recht op eerbiediging van het brief- en telecommunicatiegeheim kan worden beperkt bij of krachtens de wet, met inachtneming van de voorwaarden die bij de wet zijn gesteld.
"""
success = send_change_alert(
bwb_id=args.bwb_id,
titel="Grondwet",
diff_text=test_diff,
recipients=[args.to],
)
print(f"Test alert {'verstuurd' if success else 'MISLUKT'}")