VAKYA/Report_generation
0
1"""FastAPI app for the Daily Report OpenEnv server."""2 3import inspect4import os5import smtplib6import threading7from email.message import EmailMessage8from typing import Any, Dict, Optional9 10from fastapi import HTTPException, Response11from fastapi.responses import HTMLResponse12from pathlib import Path13from pydantic import BaseModel, ConfigDict14 15try:16 from openenv.core.env_server.http_server import create_app17 from openenv.core.env_server.serialization import deserialize_action, serialize_observation18 from openenv.core.env_server.types import ResetResponse, StepRequest, StepResponse19 20 from ..models import DailyReportAction, DailyReportObservation21 from .database import ReportTrackingDB22 from .daily_report_environment import DailyReportEnvironment, run_gold_full_episode23 from .scheduler_agents import run_manual_schedule24except ImportError:25 from models import DailyReportAction, DailyReportObservation # type: ignore26 27 from openenv.core.env_server.http_server import create_app # type: ignore28 from openenv.core.env_server.serialization import deserialize_action, serialize_observation # type: ignore29 from openenv.core.env_server.types import ResetResponse, StepRequest, StepResponse # type: ignore30 31 from server.database import ReportTrackingDB # type: ignore32 from server.daily_report_environment import DailyReportEnvironment, run_gold_full_episode # type: ignore33 from server.scheduler_agents import run_manual_schedule # type: ignore34 35 36def _create_app():37 try:38 first_param = next(iter(inspect.signature(create_app).parameters.values()))39 annotation_text = str(first_param.annotation)40 except (StopIteration, TypeError, ValueError):41 annotation_text = "typing.Callable"42 43 expects_instance = "Environment" in annotation_text and "Callable" not in annotation_text44 env_arg = DailyReportEnvironment() if expects_instance else DailyReportEnvironment45 return create_app(46 env_arg,47 DailyReportAction,48 DailyReportObservation,49 env_name="daily_report_env",50 )51 52 53app = _create_app()54 55# Stateful HTTP session (OpenEnv's standard POST /step creates a *new* env each call, so it cannot56# remember prior steps). Use /session/* for browser/curl demos, or WebSocket /ws for agents.57_session_lock = threading.Lock()58_session_env: Optional[DailyReportEnvironment] = None59 60 61class SessionResetBody(BaseModel):62 """Body for POST /session/reset — picks task and optional OpenEnv reset fields."""63 64 model_config = ConfigDict(extra="allow")65 66 task: str = "daily_full"67 seed: Optional[int] = None68 episode_id: Optional[str] = None69 70 71class EmailRequest(BaseModel):72 email: str73 74 75class ManualScheduleRequest(BaseModel):76 slot: str = "both" # 10am | 11am | both77 78 79@app.get("/")80def root() -> HTMLResponse:81 """Serve the frontend HTML."""82 html_path = Path(__file__).parent / "frontend" / "index.html"83 if html_path.exists():84 return HTMLResponse(content=html_path.read_text(encoding="utf-8"))85 return HTMLResponse(content="<h1>Frontend UI not found.</h1>", status_code=404)86 87 88@app.post("/session/reset", tags=["Session (stateful HTTP)"])89def session_reset(body: SessionResetBody = SessionResetBody()) -> ResetResponse:90 """Start a new episode that persists across POST /session/step (same server process)."""91 global _session_env92 with _session_lock:93 _session_env = DailyReportEnvironment()94 kwargs = body.model_dump(exclude_none=True)95 obs = _session_env.reset(**kwargs)96 return ResetResponse(**serialize_observation(obs))97 98 99@app.post("/session/step", tags=["Session (stateful HTTP)"])100def session_step(request: StepRequest) -> StepResponse:101 """Same as OpenEnv /step but uses the session started by POST /session/reset."""102 global _session_env103 with _session_lock:104 if _session_env is None:105 raise HTTPException(106 status_code=400,107 detail="No session. Call POST /session/reset first (or POST /session/run_static_demo).",108 )109 try:110 action = deserialize_action(request.action, DailyReportAction)111 except Exception as exc:112 raise HTTPException(113 status_code=422,114 detail=f"Invalid action: {exc}. Wrap fields in an 'action' object (see /docs).",115 ) from exc116 obs = _session_env.step(action)117 return StepResponse(**serialize_observation(obs))118 119 120@app.get("/session/state", tags=["Session (stateful HTTP)"])121def session_state() -> Dict[str, Any]:122 """Current session state (header fields, metrics, pdf_generated, graded_score, …)."""123 global _session_env124 with _session_lock:125 if _session_env is None:126 raise HTTPException(status_code=400, detail="No session. Call POST /session/reset first.")127 st = _session_env.state128 if hasattr(st, "model_dump"):129 return st.model_dump()130 return dict(st)131 132 133@app.get("/session/report.pdf", tags=["Session (stateful HTTP)"])134def session_report_pdf() -> Response:135 """Download the PDF after `finalize_pdf` (e.g. task daily_full) or after /session/run_static_demo."""136 global _session_env137 with _session_lock:138 if _session_env is None or not _session_env.has_pdf:139 raise HTTPException(140 status_code=404,141 detail=(142 "No PDF in this session. For task daily_full run command finalize_pdf, or call "143 "POST /session/run_static_demo."144 ),145 )146 pdf = _session_env.pdf_bytes147 assert pdf is not None148 return Response(149 content=pdf,150 media_type="application/pdf",151 headers={152 "Content-Disposition": 'attachment; filename="daily_mrg_report.pdf"',153 },154 )155 156 157@app.post("/session/run_static_demo", tags=["Session (stateful HTTP)"])158def session_run_static_demo() -> Dict[str, Any]:159 """Generate the full report from built-in static data + PDF in one request (manual / 7am-style trigger)."""160 global _session_env161 with _session_lock:162 _session_env = DailyReportEnvironment()163 obs = run_gold_full_episode(_session_env)164 return {165 "message": "Static gold report generated. Download: GET /session/report.pdf",166 "result": serialize_observation(obs),167 }168 169 170@app.post("/session/send_email", tags=["Session (stateful HTTP)"])171def session_send_email(body: EmailRequest) -> Dict[str, Any]:172 """Email the generated PDF report via Brevo HTTP API (or mock it)."""173 global _session_env174 with _session_lock:175 if _session_env is None or not _session_env.has_pdf:176 raise HTTPException(177 status_code=400, detail="No PDF has been generated yet. Please generate the report first."178 )179 pdf_bytes = _session_env.pdf_bytes180 assert pdf_bytes is not None181 182 sender_email = os.environ.get("SENDER_EMAIL") or os.environ.get("SMTP_EMAIL")183 api_key = os.environ.get("BREVO_API_KEY") or os.environ.get("SMTP_PASSWORD")184 185 if not sender_email or not api_key:186 print(f"[MOCK EMAIL] Would send report to {body.email} (Configure SENDER_EMAIL and BREVO_API_KEY to send real emails)")187 return {"message": f"Simulated email sent to {body.email} (Credentials missing)"}188 189 try:190 import urllib.request191 import json192 import base64193 194 encoded_pdf = base64.b64encode(pdf_bytes).decode("utf-8")195 196 url = "https://api.brevo.com/v3/smtp/email"197 payload = {198 "sender": {"name": "Reporting Service", "email": sender_email},199 "to": [{"email": body.email}],200 "subject": "Daily Post-Merge Operations Report",201 "htmlContent": "<html><body><p>Hello,<br><br>Please find the attached Daily MRG Report generated automatically by the Reporting Service.<br><br>Thanks.</p></body></html>",202 "attachment": [203 {204 "name": "daily_mrg_report.pdf",205 "content": encoded_pdf206 }207 ]208 }209 210 data = json.dumps(payload).encode("utf-8")211 req = urllib.request.Request(url, data=data, method="POST")212 req.add_header("accept", "application/json")213 req.add_header("api-key", api_key)214 req.add_header("content-type", "application/json")215 216 with urllib.request.urlopen(req) as response:217 result = response.read()218 except Exception as exc:219 print(f"[EMAIL ERROR] HTTP API Failed: {exc}")220 if hasattr(exc, 'read'):221 print(f"[EMAIL ERROR] Response: {exc.read().decode('utf-8', errors='ignore')}")222 raise HTTPException(status_code=500, detail=f"Failed to send email: {exc}")223 224 return {"message": f"Successfully sent email to {body.email}"}225 226 227@app.post("/session/run_manual_schedule", tags=["Scheduling"])228def session_run_manual_schedule(body: ManualScheduleRequest = ManualScheduleRequest()) -> Dict[str, Any]:229 """Manual scheduler trigger for 10am/11am report generation and sending."""230 db = ReportTrackingDB()231 db.seed_static_data()232 try:233 result = run_manual_schedule(db, body.slot)234 except ValueError as exc:235 raise HTTPException(status_code=400, detail=str(exc)) from exc236 return {237 "message": "Manual schedule executed",238 "result": result,239 }240 241 242@app.get("/session/live_tracks", tags=["Scheduling"])243def session_live_tracks() -> Dict[str, Any]:244 """Return live tracking rows for UI table."""245 db = ReportTrackingDB()246 rows = db.list_live_tracks()247 return {"count": len(rows), "rows": rows}248 249 250@app.post("/session/live_tracks/reset", tags=["Scheduling"])251def session_live_tracks_reset() -> Dict[str, Any]:252 """Clear live tracking table for fresh training/evaluation episodes."""253 db = ReportTrackingDB()254 deleted = db.clear_live_tracks()255 return {"message": "Live tracking reset", "deleted_rows": deleted}256 257 258def main() -> None:259 import uvicorn260 261 uvicorn.run(app, host="0.0.0.0", port=8000)262 263 264if __name__ == "__main__":265 main()266 