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