""" server/app.py – FastAPI server for Smart Farm Resource Manager. Exposes: POST /reset -> Observation POST /step -> StepResult GET /state -> dict GET /health -> dict GET /tasks -> dict """ from __future__ import annotations import json import os from typing import Any, Dict import uvicorn from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from src.envs.smart_farm_env.models import Action, Observation, StepResult, TaskConfig from src.envs.smart_farm_env.server.environment import SmartFarmEnv # --------------------------------------------------------------------------- # App # --------------------------------------------------------------------------- app = FastAPI( title="Smart Farm Resource Manager", description="OpenEnv-compatible precision agriculture simulation API", version="1.0.0", docs_url="/docs", # default, but explicit redoc_url="/redoc", ) @app.get("/") async def root(): return { "message": "🚜 Smart Farm Resource Manager API is running!", "version": "1.0.0", "docs": "/docs", "redoc": "/redoc", "available_endpoints": [ "/health", "/reset", "/step", "/state", "/tasks" ] } app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # Singleton env (one active episode per server instance) _env: SmartFarmEnv | None = None # Load task configs once at startup _TASKS_PATH = os.path.join(os.path.dirname(__file__), "..", "tasks", "tasks.json") with open(_TASKS_PATH) as _f: _tasks_raw = json.load(_f)["tasks"] TASK_MAP: Dict[str, TaskConfig] = {t["name"]: TaskConfig(**t) for t in _tasks_raw} def _require_env() -> SmartFarmEnv: if _env is None: raise HTTPException(status_code=400, detail="Not initialised — call POST /reset first.") return _env # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- @app.get("/health") def health() -> Dict[str, str]: return {"status": "ok", "env": "smart-farm-env"} @app.post("/reset", response_model=Observation) def reset(task_name: str = "easy") -> Observation: global _env if task_name not in TASK_MAP: raise HTTPException( status_code=404, detail=f"Unknown task '{task_name}'. Available: {list(TASK_MAP)}", ) _env = SmartFarmEnv(TASK_MAP[task_name]) return _env.reset() @app.post("/step", response_model=StepResult) def step(action: Action) -> StepResult: env = _require_env() try: return env.step(action) except RuntimeError as exc: raise HTTPException(status_code=400, detail=str(exc)) @app.get("/state") def state() -> Dict[str, Any]: return _require_env().state() @app.get("/tasks") def list_tasks() -> Dict[str, Any]: return { "tasks": [ { "name": t.name, "description": t.description, "num_plots": t.num_plots, "allowed_actions": t.allowed_actions, } for t in TASK_MAP.values() ] } # --------------------------------------------------------------------------- # Entry point (used by server Dockerfile CMD) # --------------------------------------------------------------------------- if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) uvicorn.run("src.envs.smart_farm_env.server.app:app", host="0.0.0.0", port=port, reload=False) def main(): import uvicorn uvicorn.run(app, host='0.0.0.0', port=7860) if __name__ == '__main__': main()