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:
parent
6db0f6afc3
commit
479a557f86
1 changed files with 203 additions and 0 deletions
203
src/wetgit/ai/alerts.py
Normal file
203
src/wetgit/ai/alerts.py
Normal 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'}")
|
||||||
Loading…
Add table
Reference in a new issue