import os from fastapi import FastAPI, Query, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from env import ExecOpsEnv, Action, grade app = FastAPI(title="AI Executive Operations Manager", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # Global environment instance (single session) env = ExecOpsEnv("easy") @app.api_route("/reset", methods=["GET", "POST"]) def reset(task: str = Query("easy")): """Reset the environment to a fresh state for the given task.""" global env valid_tasks = ["easy", "medium", "hard"] if task not in valid_tasks: raise HTTPException(status_code=400, detail=f"Invalid task. Choose from: {valid_tasks}") env = ExecOpsEnv(task) return env.reset() @app.post("/step") def step(action: Action): """Execute one action step in the environment.""" global env try: result = env.step(action) return result except (ValueError, RuntimeError) as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/state") def get_state(): """Return the full current environment state.""" global env return env.get_state() @app.get("/grade") def get_grade(): """Return the current grade score (0.0-1.0).""" global env return {"score": grade(env._state), "task_id": env.task_id} @app.get("/health") def health(): """OpenEnv runtime health check.""" return {"status": "healthy"} @app.get("/metadata") def metadata(): """OpenEnv runtime metadata.""" return { "name": "ai-executive-ops-manager", "description": ( "Simulates a startup CEO's operational day. An AI agent manages a realistic " "executive inbox under time pressure, making triage decisions - reply, schedule, " "delegate, or ignore - to handle conflicting priorities and cascading crises." ), "version": "1.0.0", "tasks": ["easy", "medium", "hard"], } @app.get("/schema") def schema(): """OpenEnv runtime schema for action, observation, and state.""" return { "action": { "type": "object", "properties": { "type": {"type": "string", "enum": ["reply", "schedule", "delegate", "ignore"]}, "email_id": {"type": "string", "description": "ID of the email to act on (e.g. 'e1')"}, }, "required": ["type", "email_id"], }, "observation": { "type": "object", "properties": { "time": {"type": "string"}, "step": {"type": "integer"}, "max_steps": {"type": "integer"}, "steps_remaining": {"type": "integer"}, "inbox": {"type": "array", "items": {"type": "object"}}, "calendar": {"type": "array", "items": {"type": "object"}}, "goals": {"type": "array", "items": {"type": "object"}}, "pending_goals": {"type": "array", "items": {"type": "object"}}, "total_reward": {"type": "number"}, "done": {"type": "boolean"}, }, }, "state": { "type": "object", "properties": { "current_step": {"type": "integer"}, "max_steps": {"type": "integer"}, "current_time": {"type": "string"}, "inbox": {"type": "array"}, "calendar": {"type": "array"}, "goals": {"type": "array"}, "history": {"type": "array"}, "total_reward": {"type": "number"}, "done": {"type": "boolean"}, "task_id": {"type": "string"}, }, }, } @app.post("/mcp") def mcp(request: dict = None): """Minimal MCP/JSON-RPC endpoint for OpenEnv runtime compliance.""" return {"jsonrpc": "2.0", "result": {"tools": []}, "id": None} @app.get("/tasks") def list_tasks(): """Return available task descriptions.""" return { "tasks": [ { "id": "easy", "name": "Monday Morning Catchup", "difficulty": "Easy", "description": "A manageable Monday inbox. Priorities are clear.", "max_steps": 6, }, { "id": "medium", "name": "Investor Demo Day Prep", "difficulty": "Medium", "description": "Conflicting priorities on demo day. Scheduling required.", "max_steps": 8, }, { "id": "hard", "name": "Series B Crisis Day", "difficulty": "Hard", "description": "Cascading crises. Impossible to handle everything.", "max_steps": 8, }, ] } # Serve React frontend - MUST come after all API routes FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "frontend", "build") if os.path.exists(FRONTEND_DIR): app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend") else: # Fallback: return health check at root if frontend not built yet @app.get("/") def root(): return {"status": "ok", "message": "AI Executive Operations Manager API", "frontend": "not built"}