Astraea v1.0 — multi-agent WA family-law assistant
Eight specialist agents over a 16-book verified WA law corpus (RAG with citations), per-user document vault, WA court-form PDF auto-fill, comms missions with DV safety guard, no-KYC auth, TTS. Self-hosted: Flask + SQLite + Ollama, stdlib-only RAG. Includes README, LICENSE (MIT + not-legal-advice notice), DEPLOY runbook, .gitignore.
This commit is contained in:
96
comms.py
Normal file
96
comms.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Astraea comms — Twilio (SMS + voice calls) + SMTP email, with a no-contact-order
|
||||
safety guard and formal message templates. stdlib-only (urllib + smtplib + email).
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import smtplib
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
TWILIO_API = "https://api.twilio.com/2010-04-01/Accounts"
|
||||
|
||||
|
||||
def _twilio_request(settings, path, data):
|
||||
sid = settings.get("twilio_sid", "")
|
||||
token = settings.get("twilio_auth_token", "")
|
||||
if not sid or not token:
|
||||
raise RuntimeError("Twilio credentials not configured")
|
||||
url = f"{TWILIO_API}/{sid}/{path}.json"
|
||||
cred = base64.b64encode(f"{sid}:{token}".encode()).decode()
|
||||
body = urllib.parse.urlencode(data).encode()
|
||||
req = urllib.request.Request(url, data=body,
|
||||
headers={"Authorization": f"Basic {cred}",
|
||||
"Content-Type": "application/x-www-form-urlencoded"})
|
||||
op = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
with op.open(req, timeout=30) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def send_sms(settings, to, body):
|
||||
return _twilio_request(settings, "Messages",
|
||||
{"To": to, "From": settings.get("twilio_from", ""), "Body": body})
|
||||
|
||||
|
||||
def make_call(settings, to, twiml_url):
|
||||
return _twilio_request(settings, "Calls",
|
||||
{"To": to, "From": settings.get("twilio_from", ""), "Url": twiml_url})
|
||||
|
||||
|
||||
def send_email(settings, to, subject, body):
|
||||
host = settings.get("smtp_host", "")
|
||||
user = settings.get("smtp_user", "")
|
||||
pwd = settings.get("smtp_pass", "")
|
||||
frm = settings.get("email_from", "") or user
|
||||
if not host or not user or not frm:
|
||||
raise RuntimeError("Email (SMTP) not configured")
|
||||
port = int(settings.get("smtp_port", 587) or 587)
|
||||
msg = MIMEMultipart()
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = frm
|
||||
msg["To"] = to
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
s = smtplib.SMTP(host, port, timeout=30)
|
||||
s.starttls()
|
||||
s.login(user, pwd)
|
||||
s.sendmail(frm, [to], msg.as_string())
|
||||
s.quit()
|
||||
return {"ok": True, "to": to, "subject": subject}
|
||||
|
||||
|
||||
# ── Formal templates (the agent composes content; these wrap it professionally) ──
|
||||
def email_template(display_name, recipient, subject, body):
|
||||
return (
|
||||
f"{recipient},\n\n"
|
||||
f"{body}\n\n"
|
||||
f"Respectfully,\n"
|
||||
f"{display_name}\n\n"
|
||||
f"---\nThis message was drafted on Astraea, an AI legal assistant, on my behalf. "
|
||||
f"Please direct any reply to me directly."
|
||||
)
|
||||
|
||||
|
||||
def sms_template(body):
|
||||
return f"{body}\n\n— sent via Astraea legal assistant"
|
||||
|
||||
|
||||
def call_script(greeting, purpose, body, closing):
|
||||
return (
|
||||
f"Good {greeting}. My name is [your name]. {purpose}.\n\n"
|
||||
f"{body}\n\n"
|
||||
f"{closing}"
|
||||
)
|
||||
|
||||
|
||||
def no_contact_blocked(settings, recipient):
|
||||
"""Return True if a no-contact order OR protection order is in effect AND the recipient
|
||||
looks like the protected/restrained party. Domestic-violence protection orders (RCW 7.105)
|
||||
carry the same no-contact force as a standalone no-contact order."""
|
||||
if not (settings.get("no_contact_order") or settings.get("protection_order")):
|
||||
return False
|
||||
partner = (settings.get("partner_name") or "").strip().lower()
|
||||
if not partner:
|
||||
return True # order in effect but name unset — block to be safe
|
||||
return partner in (recipient or "").lower()
|
||||
Reference in New Issue
Block a user