CoolFace
Apppublic

VAKYA/Report_generation

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py266 linesDownload Raw Back to server
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