VAKYA/Report_generation
0
1"""Execution agent loop for retries, routing, and email tracking."""2 3from __future__ import annotations4 5import base646import os7import smtplib8from email.message import EmailMessage9from typing import Dict10 11import requests12 13from .database import ReportTrackingDB14from .report_builder import build_10am_account_statement, build_11am_finance_summary15 16 17def _send_agent_email(18 recipient_email: str,19 subject: str,20 body: str,21 attachment_name: str | None = None,22 attachment_bytes: bytes | None = None,23) -> tuple[bool, str]:24 """Send scheduler email using SMTP.25 26 Behavior:27 - If AGENT_EMAIL_MOCK=1, do mock send (useful for local testing).28 - Otherwise requires SMTP_EMAIL and SMTP_PASSWORD to send real emails.29 """30 if os.environ.get("AGENT_EMAIL_MOCK", "0") == "1":31 print(f"[EMAIL][MOCK] recipient={recipient_email} subject={subject}")32 return True, "mock_sent"33 34 sender_email = os.environ.get("SENDER_EMAIL") or os.environ.get("SMTP_EMAIL")35 api_key = os.environ.get("BREVO_API_KEY") or os.environ.get("SMTP_PASSWORD")36 37 if not sender_email or not api_key:38 print(f"[EMAIL][SKIP] smtp_not_configured recipient={recipient_email} (Configure SENDER_EMAIL and BREVO_API_KEY)")39 return False, "api_keys_not_configured"40 41 try:42 url = "https://api.brevo.com/v3/smtp/email"43 payload: Dict[str, object] = {44 "sender": {"name": "Reporting Service Agent", "email": sender_email},45 "to": [{"email": recipient_email}],46 "subject": subject,47 "textContent": body,48 }49 50 if attachment_name and attachment_bytes:51 payload["attachment"] = [52 {53 "name": attachment_name,54 "content": base64.b64encode(attachment_bytes).decode("ascii")55 }56 ]57 58 print(f"[EMAIL][BREVO] attempting recipient={recipient_email}")59 60 headers = {61 "accept": "application/json",62 "api-key": api_key,63 "content-type": "application/json"64 }65 66 response = requests.post(url, json=payload, headers=headers, timeout=15)67 response.raise_for_status()68 print(f"[EMAIL][BREVO] sent recipient={recipient_email}")69 return True, "sent_via_brevo"70 except Exception as exc:71 err_msg = str(exc)72 if hasattr(exc, 'response') and exc.response is not None:73 err_msg += f" body={exc.response.text}"74 print(f"[EMAIL][BREVO][ERROR] recipient={recipient_email} err={err_msg}")75 return False, f"brevo_error:{exc}"76 77 78def execute_report_job(db: ReportTrackingDB, report_id: str) -> Dict[str, object]:79 """Run one report job with max retries and DB live updates.80 81 Rules:82 - Permanent LAN failure -> retry until max_retries, then fail.83 - Success -> send report to resolved customer email.84 - Failure -> send failure email only to fixed failure email.85 """86 track = db.get_live_track(report_id)87 if track is None:88 raise ValueError(f"Unknown report_id: {report_id}")89 90 max_retries = int(track["max_retries"])91 lan_id = int(track["lan_id"])92 lan_code = None93 should_fail_permanently = 094 for pair in db.get_customer_lan_pairs():95 if int(pair["lan_id"]) == lan_id:96 lan_code = str(pair["lan_code"])97 should_fail_permanently = int(pair["should_fail_permanently"])98 break99 100 if lan_code is None:101 raise ValueError(f"LAN mapping not found for report_id: {report_id}")102 103 for attempt in range(1, max_retries + 1):104 db.update_live_track_status(105 report_id=report_id,106 status="in_progress" if attempt == 1 else "retrying",107 retries_used=attempt,108 error_code=None,109 error_message=None,110 report_generated=False,111 finished=False,112 )113 114 if should_fail_permanently == 1:115 db.update_live_track_status(116 report_id=report_id,117 status="retrying" if attempt < max_retries else "failed",118 retries_used=attempt,119 error_code="lan_permanent_failure",120 error_message=f"{lan_code} report generation failed",121 report_generated=False,122 finished=attempt == max_retries,123 )124 continue125 126 # Success path127 db.update_live_track_status(128 report_id=report_id,129 status="success",130 retries_used=attempt,131 error_code=None,132 error_message=None,133 report_generated=True,134 finished=True,135 )136 recipient = db.resolve_recipient_email(report_id, is_failure=False)137 context = db.get_report_context(report_id)138 if context is None:139 raise ValueError(f"Report context not found: {report_id}")140 if str(track["scheduler_slot"]) == "10am":141 pdf_bytes = build_10am_account_statement(context)142 else:143 pdf_bytes = build_11am_finance_summary(context)144 sent, reason = _send_agent_email(145 recipient_email=recipient,146 subject=f"[SUCCESS] Report generated for {lan_code}",147 body=(148 f"Hello,\n\nYour report has been generated and sent by agent.\n"149 f"Report ID: {report_id}\nLAN: {lan_code}\nType: {track['report_type']}\n"150 ),151 attachment_name=f"{report_id}.pdf",152 attachment_bytes=pdf_bytes,153 )154 # SMTP can accept mail even if remote mailbox later bounces.155 email_status = "success_accepted_by_smtp" if sent else f"success_failed:{reason}"156 db.update_email_status(report_id, sent=sent, status=email_status)157 return {158 "report_id": report_id,159 "status": "success",160 "attempts": attempt,161 "email_recipient": recipient,162 "email_sent": sent,163 "email_status": email_status,164 }165 166 # Final failure path: email goes only to fixed failure notification address.167 recipient = db.resolve_recipient_email(report_id, is_failure=True)168 sent, reason = _send_agent_email(169 recipient_email=recipient,170 subject=f"[FAILED] Report generation failed for {lan_code}",171 body=(172 "Report generation failed after max retries.\n"173 f"Report ID: {report_id}\n"174 f"LAN: {lan_code}\n"175 f"Retries: {max_retries}\n"176 "Please check and resolve the issue.\n"177 ),178 )179 email_status = "failure_accepted_by_smtp" if sent else f"failure_failed:{reason}"180 db.update_email_status(report_id, sent=sent, status=email_status)181 final_state = db.get_live_track(report_id)182 return {183 "report_id": report_id,184 "status": "failed",185 "attempts": max_retries,186 "email_recipient": recipient,187 "email_sent": sent,188 "email_status": email_status,189 "last_error_code": final_state["last_error_code"] if final_state else None,190 }191 