call-qa-processing / backend /app /email_notifications.py
aniketqxp's picture
fix: align email records and acoustic progress
d35d7c0 verified
Raw
History Blame Contribute Delete
6.33 kB
"""Idempotent SMTP delivery for evaluator-recommended email actions."""
from __future__ import annotations
import hashlib
import json
import os
import smtplib
import ssl
from datetime import datetime
from email.message import EmailMessage
from app.models import EmailNotification
def _enabled():
return os.environ.get("EMAIL_AUTOMATION_ENABLED", "true").lower() not in {
"0",
"false",
"no",
"off",
}
def _recipient(audience):
variable = (
"DEMO_CUSTOMER_EMAIL"
if audience == "customer"
else "DEMO_MANAGER_EMAIL"
)
return (
os.environ.get(variable)
or os.environ.get("SMTP_USERNAME")
or os.environ.get("EMAIL_FROM")
or "clarayoussef01@gmail.com"
)
def _notification_key(public_call_id, decision_sha256, action):
payload = {
"call_id": public_call_id,
"decision_sha256": decision_sha256,
"action_type": action.get("action_type"),
"execution": action.get("execution"),
}
return hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()
).hexdigest()
def _content(public_call_id, evaluation, action, audience):
presentation = evaluation.get("presentation") or {}
label = action.get("label") or str(
action.get("action_type") or "call review"
).replace("_", " ").title()
reason = action.get("reason") or presentation.get("summary") or (
"The evaluator identified a call that requires attention."
)
findings = presentation.get("primary_reasons") or []
finding_lines = "\n".join(
f"- {item.get('title')}: {item.get('summary')}"
for item in findings[:3]
)
if audience == "customer":
return {
"subject": "Follow-up regarding your recent support call",
"body": (
"Hello,\n\nWe are following up regarding your recent support "
"call. A support representative will review the interaction "
"and follow up as appropriate. Please do not reply with "
"passwords, PINs, or full account numbers.\n\nRegards,\n"
"Customer Support"
),
}
details = f"\n\nReview findings:\n{finding_lines}" if finding_lines else ""
return {
"subject": f"{label}: {public_call_id}",
"body": (
"Hello Manager,\n\nThe call evaluator recommends your attention."
f"\n\nCall: {public_call_id}\nAction: {label}\nReason: {reason}"
f"{details}\n\nOpen the call evaluator for the transcript and "
"timestamped evidence.\n\nRegards,\nCall Quality Analysis"
),
}
def _send(recipient, subject, body):
sender = os.environ.get("EMAIL_FROM") or os.environ.get("SMTP_USERNAME")
password = os.environ.get("SMTP_PASSWORD", "")
if not sender or not password:
raise RuntimeError("SMTP credentials are not configured")
message = EmailMessage()
message["From"] = sender
message["To"] = recipient
message["Subject"] = subject
message.set_content(body)
host = os.environ.get("SMTP_HOST", "smtp.gmail.com")
port = int(os.environ.get("SMTP_PORT", "465"))
with smtplib.SMTP_SSL(
host,
port,
context=ssl.create_default_context(),
timeout=30,
) as smtp:
smtp.login(os.environ.get("SMTP_USERNAME", sender), password)
smtp.send_message(message)
def serialize_notification(record):
return {
"notification_id": str(record.notification_id),
"status": record.status,
"action_type": record.action_type,
"audience": record.audience,
"recipient": record.recipient,
"subject": record.subject,
"sent_at": record.sent_at.isoformat() if record.sent_at else None,
"error": record.error,
}
def process_recommended_email(
db,
call,
public_call_id,
evaluation,
*,
approved=False,
):
"""Create the action record and send when automatic or approved."""
decision_sha256 = str(evaluation.get("decision_sha256") or "")
decision = evaluation.get("decision") or {}
action = decision.get("recommended_action") or {}
action_type = str(action.get("action_type") or "none")
if action_type == "none":
return None
audience = "customer" if action_type == "customer_follow_up" else "manager"
key = _notification_key(public_call_id, decision_sha256, action)
record = (
db.query(EmailNotification)
.filter(EmailNotification.notification_key == key)
.first()
)
if record and record.status == "sent":
return serialize_notification(record)
content = _content(public_call_id, evaluation, action, audience)
recipient = _recipient(audience)
sender = os.environ.get("EMAIL_FROM") or os.environ.get("SMTP_USERNAME") or ""
if not record:
record = EmailNotification(
notification_key=key,
call_id=call.call_id,
public_call_id=public_call_id,
action_id=str(action.get("action_id") or action_type)[:50],
action_type=action_type,
audience=audience,
recipient=recipient,
sender=sender,
status="pending",
llm_generated=False,
)
db.add(record)
record.recipient = recipient
record.sender = sender
record.subject = content["subject"]
record.body = content["body"]
requires_approval = action.get("execution") == "requires_approval"
if requires_approval and not approved:
record.status = "awaiting_approval"
record.error = None
db.flush()
return serialize_notification(record)
try:
if not _enabled():
record.status = "disabled"
record.error = "EMAIL_AUTOMATION_ENABLED is false"
else:
_send(recipient, record.subject, record.body)
record.status = "sent"
record.error = None
record.sent_at = datetime.utcnow()
except Exception as exc:
record.status = (
"configuration_required"
if "configured" in str(exc).lower()
else "failed"
)
record.error = str(exc)[:1000]
db.flush()
return serialize_notification(record)