Spaces:
Sleeping
Sleeping
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the BSD-style license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| """ | |
| FastAPI application for the Email Campaign Simulation environment. | |
| This server exposes OpenEnv-compatible endpoints and serves a lightweight | |
| campaign analytics dashboard at "/" and "/web". | |
| """ | |
| from __future__ import annotations | |
| import csv | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from typing import Any, Dict, List | |
| try: | |
| from openenv.core.env_server.http_server import create_app | |
| except Exception as e: # pragma: no cover | |
| raise ImportError( | |
| "openenv is required for the web interface. Install dependencies with '\n uv sync\n'" | |
| ) from e | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| try: | |
| from ..models import ClothingBrandCtrAction, ClothingBrandCtrObservation | |
| except ImportError: # pragma: no cover - supports direct server.app imports | |
| from models import ClothingBrandCtrAction, ClothingBrandCtrObservation | |
| from .clothing_brand_ctr_env_environment import ClothingBrandCtrEnvironment | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| WEB_INDEX_PATH = PROJECT_ROOT / "web" / "index.html" | |
| EVALS_DIR = PROJECT_ROOT / "outputs" / "evals" | |
| def _safe_float(value: object, default: float = 0.0) -> float: | |
| """Best-effort float parser.""" | |
| try: | |
| return float(value) # type: ignore[arg-type] | |
| except (TypeError, ValueError): | |
| return default | |
| def _safe_int(value: object, default: int = 0) -> int: | |
| """Best-effort integer parser.""" | |
| try: | |
| return int(float(value)) # type: ignore[arg-type] | |
| except (TypeError, ValueError): | |
| return default | |
| def _read_csv_rows(path: Path, limit: int | None = None) -> List[Dict[str, str]]: | |
| """Read CSV rows if file exists.""" | |
| if not path.exists(): | |
| return [] | |
| rows: List[Dict[str, str]] = [] | |
| with path.open("r", newline="", encoding="utf-8") as handle: | |
| reader = csv.DictReader(handle) | |
| for idx, row in enumerate(reader): | |
| rows.append(dict(row)) | |
| if limit is not None and idx + 1 >= limit: | |
| break | |
| return rows | |
| def _format_file_meta(path: Path) -> Dict[str, object]: | |
| """Return file metadata used by dashboard.""" | |
| if not path.exists(): | |
| return {"path": str(path.relative_to(PROJECT_ROOT)), "exists": False} | |
| stat = path.stat() | |
| updated = datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat() | |
| return { | |
| "path": str(path.relative_to(PROJECT_ROOT)), | |
| "exists": True, | |
| "size_bytes": stat.st_size, | |
| "updated_at_utc": updated, | |
| } | |
| def load_campaign_stats() -> Dict[str, object]: | |
| """Aggregate latest simulation output files for dashboard display.""" | |
| schedule_path = EVALS_DIR / "five_email_schedule_results.csv" | |
| step_path = EVALS_DIR / "five_email_best_schedule_steps.csv" | |
| arm_path = EVALS_DIR / "brand_campaign_arm_results.csv" | |
| schedule_rows = _read_csv_rows(schedule_path, limit=25) | |
| step_rows = _read_csv_rows(step_path, limit=25) | |
| arm_rows = _read_csv_rows(arm_path, limit=25) | |
| top_schedules = [ | |
| { | |
| "rank": _safe_int(row.get("rank")), | |
| "schedule": row.get("schedule", ""), | |
| "open_rate": _safe_float(row.get("open_rate")), | |
| "ctr": _safe_float(row.get("ctr")), | |
| "purchase_rate": _safe_float(row.get("purchase_rate")), | |
| "composite_score": _safe_float(row.get("composite_score")), | |
| "generation_source": row.get("generation_source", ""), | |
| "marketer_score": _safe_float(row.get("marketer_score")), | |
| "opens": _safe_int(row.get("opens")), | |
| "clicks": _safe_int(row.get("clicks")), | |
| "purchases": _safe_int(row.get("purchases")), | |
| } | |
| for row in schedule_rows[:10] | |
| ] | |
| best_schedule = top_schedules[0] if top_schedules else {} | |
| step_breakdown = [ | |
| { | |
| "step_idx": _safe_int(row.get("step_idx")), | |
| "step_name": row.get("step_name", ""), | |
| "send_day": row.get("send_day", ""), | |
| "send_hour": _safe_int(row.get("send_hour")), | |
| "open_rate": _safe_float(row.get("open_rate")), | |
| "ctr": _safe_float(row.get("ctr")), | |
| "purchase_rate": _safe_float(row.get("purchase_rate")), | |
| "subject_line": row.get("subject_line", ""), | |
| } | |
| for row in step_rows | |
| ] | |
| top_arms = [ | |
| { | |
| "rank": _safe_int(row.get("rank")), | |
| "arm_id": row.get("arm_id", ""), | |
| "variant_name": row.get("variant_name", ""), | |
| "brand_voice": row.get("brand_voice", ""), | |
| "send_hour": _safe_int(row.get("send_hour")), | |
| "open_rate": _safe_float(row.get("open_rate")), | |
| "ctr": _safe_float(row.get("ctr")), | |
| "purchase_rate": _safe_float(row.get("purchase_rate")), | |
| "composite_score": _safe_float(row.get("composite_score")), | |
| "marketer_score": _safe_float(row.get("marketer_score")), | |
| "subject_line": row.get("subject_line", ""), | |
| } | |
| for row in arm_rows[:10] | |
| ] | |
| best_arm = top_arms[0] if top_arms else {} | |
| return { | |
| "status": "ok", | |
| "generated_at_utc": datetime.now(tz=UTC).isoformat(), | |
| "summary": { | |
| "best_schedule": best_schedule, | |
| "best_arm": best_arm, | |
| "top_schedule_count": len(top_schedules), | |
| "step_count": len(step_breakdown), | |
| "top_arm_count": len(top_arms), | |
| }, | |
| "top_schedules": top_schedules, | |
| "step_breakdown": step_breakdown, | |
| "top_arms": top_arms, | |
| "files": { | |
| "five_email_schedule_results": _format_file_meta(schedule_path), | |
| "five_email_best_schedule_steps": _format_file_meta(step_path), | |
| "brand_campaign_arm_results": _format_file_meta(arm_path), | |
| }, | |
| } | |
| def _load_dashboard_html() -> str: | |
| """Load dashboard HTML from disk with safe fallback.""" | |
| if WEB_INDEX_PATH.exists(): | |
| return WEB_INDEX_PATH.read_text(encoding="utf-8") | |
| return ( | |
| "<!doctype html><html><body><h1>Email Campaign Dashboard Missing</h1>" | |
| "<p>Create web/index.html in the project root.</p></body></html>" | |
| ) | |
| # Create the app with web interface and README integration | |
| app = create_app( | |
| ClothingBrandCtrEnvironment, | |
| ClothingBrandCtrAction, | |
| ClothingBrandCtrObservation, | |
| env_name="clothing_brand_ctr_env", | |
| max_concurrent_envs=1, # increase for more concurrent WebSocket sessions | |
| ) | |
| def landing_page() -> HTMLResponse: | |
| """Dashboard landing page.""" | |
| return HTMLResponse(_load_dashboard_html()) | |
| def landing_page_web() -> HTMLResponse: | |
| """Compatibility route for Space base path.""" | |
| return HTMLResponse(_load_dashboard_html()) | |
| def campaign_stats() -> JSONResponse: | |
| """Return latest aggregated campaign stats from simulation CSV outputs.""" | |
| return JSONResponse(load_campaign_stats()) | |
| def main(host: str = "0.0.0.0", port: int = 8000): | |
| """ | |
| Entry point for direct execution via uv run or python -m. | |
| Args: | |
| host: Host address to bind to. | |
| port: Port number to listen on. | |
| """ | |
| import uvicorn | |
| uvicorn.run(app, host=host, port=port) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--port", type=int, default=8000) | |
| args = parser.parse_args() | |
| main(port=args.port) | |