Self-healing agent (supervisor, recovery, login fixes, operator mail) and Portal sage redesign
Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fafa5285eb
commit
d500f750da
26 files changed
+1704
-357
No files matched your search
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
The agent's email to the operator (operator, 2026-10-30): when something
|
||||
technical goes wrong — a login that fails, a screen that sticks, a run that
|
||||
stops — say what happened, what the agent did about it and whether that
|
||||
worked, instead of leaving it for someone to find in the morning.
|
||||
|
||||
Sent from the bot's own mailbox (the 2FA mailbox's account, over SMTP). What
|
||||
it may carry is fixed here, not by the caller:
|
||||
* screenshots of login and error pages only (`_EMAILABLE`), PNG only — a
|
||||
claim screen or a page's HTML carries PHI and never leaves the PC;
|
||||
* no claim numbers: any run of 6+ digits in the subject or body is masked.
|
||||
|
||||
Never fatal: a mail that cannot be sent is a log line, and the run goes on.
|
||||
"""
|
||||
|
||||
import re
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
|
||||
from .. import settings as cfg
|
||||
|
||||
# `<HHMMSS>_<step>.png` as page_utils.shot() names them. Login and error
|
||||
# pages only: not "logged_in" or "landing" (eCW's home can list patients).
|
||||
_EMAILABLE = re.compile(
|
||||
r"^\d{6}_(security_image_page|login_unconfirmed|username_error|"
|
||||
r"username_step_disabled|no_login_form|captcha|restricted|"
|
||||
r"password_expired|password_expiry_no_cancel|2fa_[a-z_]+|"
|
||||
r"approval_(page|done|no_button))\.png$")
|
||||
_DIGITS = re.compile(r"\d{6,}")
|
||||
|
||||
|
||||
def _mask(text: str) -> str:
|
||||
return _DIGITS.sub("<claim>", text or "")
|
||||
|
||||
|
||||
def emailable(path) -> bool:
|
||||
return bool(_EMAILABLE.match(Path(path).name))
|
||||
|
||||
|
||||
def _transport(msg: EmailMessage) -> None:
|
||||
with smtplib.SMTP_SSL(cfg.SMTP_HOST, cfg.SMTP_PORT, timeout=30,
|
||||
context=ssl.create_default_context()) as smtp:
|
||||
smtp.login(cfg.SMTP_USERNAME, cfg.SMTP_PASSWORD)
|
||||
smtp.send_message(msg)
|
||||
|
||||
|
||||
def _send(subject: str, body: str, shots=()) -> bool:
|
||||
if not cfg.NOTIFY_EMAIL_TO:
|
||||
return False
|
||||
msg = EmailMessage()
|
||||
msg["From"] = cfg.SMTP_USERNAME
|
||||
msg["To"] = cfg.NOTIFY_EMAIL_TO
|
||||
msg["Subject"] = f"[eCW Agent] {_mask(subject)}"
|
||||
msg.set_content(_mask(body))
|
||||
for p in shots or ():
|
||||
p = Path(p)
|
||||
if emailable(p) and p.exists():
|
||||
msg.add_attachment(p.read_bytes(), maintype="image",
|
||||
subtype="png", filename=p.name)
|
||||
try:
|
||||
_transport(msg)
|
||||
print(f"[NOTIFY] Mailed: {msg['Subject']}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"[NOTIFY] Could not mail '{msg['Subject']}': "
|
||||
f"{str(exc).splitlines()[0] if str(exc) else type(exc).__name__}")
|
||||
return False
|
||||
|
||||
|
||||
def incident(title: str, what_happened: str, what_agent_did: str,
|
||||
recovered: bool, shots=()) -> bool:
|
||||
"""One technical problem: what, what the agent did, and the outcome."""
|
||||
outcome = "Recovered - the run goes on." if recovered else \
|
||||
"Not recovered - the agent stopped or is waiting to retry."
|
||||
body = (f"What happened:\n {what_happened}\n\n"
|
||||
f"What the agent did:\n {what_agent_did}\n\n"
|
||||
f"Outcome:\n {outcome}\n\n"
|
||||
f"Run log and screenshots are on the bot PC and in the Portal.")
|
||||
return _send(title, body, shots)
|
||||
|
||||
|
||||
def summary(title: str, lines) -> bool:
|
||||
"""The end of a run: counts and anything left for a person."""
|
||||
return _send(title, "\n".join(lines))
|
||||
Reference in new issue
Block a user