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:
drjones
2026-09-07 18:49:32 -07:00
commit 9f9d812320
37 changed files with 4934 additions and 0 deletions

96
comms.py Normal file
View 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()