cloudarena / server.py
saravanatanjiro's picture
Added Boss Fight scenario support to reset endpoint
d13e7d7
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
import json
# Import your existing environment
from cloud_arena_final import CloudArenaEnv
app = FastAPI()
# Initialize the global environment instance
env = CloudArenaEnv([0], [0])
# --- Translation Dictionaries ---
# Map text actions from the LLM back to your integer Action Space
ACTION_MAP = {
"NOOP": 0, "ANALYZE": 1, "VERIFY_DEPS": 2, "RESIZE_DOWN": 3,
"RESIZE_UP": 4, "STOP": 5, "RESTART": 6, "DELETE": 7,
"PATCH": 8, "ENCRYPT": 9, "RESTRICT": 10, "ROTATE_CREDS": 11,
"ENABLE_LOG": 12, "ARCHIVE": 13, "OPT_NET": 14
}
class ActionRequest(BaseModel):
action_text: str # e.g., "RESIZE_DOWN res_2"
def format_semantic_obs(env_instance):
"""
Translates the internal Python objects into a structured text prompt
that an LLM can read and reason over.
"""
obs_dict = {
"global_state": {
"step_count": env_instance.step_count,
"chaos_active": env_instance.chaos_active,
"total_system_cost_score": round(sum(r.get_cost() for r in env_instance.resources), 2),
"total_security_risk": round(env_instance._risk_aggregate(), 2)
},
"resources": {}
}
for r in env_instance.resources:
if r.is_deleted:
continue
res_id = f"res_{r.idx}"
# Base attributes that are ALWAYS visible
res_data = {
"health": "Operational" if r.health else "BROKEN",
"criticality": "HIGH" if r.criticality == 1.0 else "MED" if r.criticality == 0.6 else "LOW",
"status": "Stopped" if r.is_stopped else ("Running" if r.activity_status > 0.5 else "Idle"),
"data_staleness": round(r.staleness, 2)
}
# Handle Fog of War masking
if r.fog_active:
res_data["fog_of_war"] = "ACTIVE - Execute ANALYZE to reveal metrics"
res_data["cost_rate"] = "HIDDEN"
res_data["security_risk"] = "HIDDEN"
res_data["exposure"] = "HIDDEN"
else:
res_data["fog_of_war"] = "LIFTED"
res_data["cost_rate"] = round(r.cost_rate, 2)
res_data["security_risk"] = round(r.risk_score, 2)
res_data["exposure"] = round(r.exposure, 2)
# If the LLM has analyzed it, we can also give it helpful semantic hints
if r.usage < r.allocated - 0.10 and not r.is_stopped:
res_data["efficiency_hint"] = "Overprovisioned (Can be Resized Down)"
obs_dict["resources"][res_id] = res_data
# We return it as a JSON-formatted string so the LLM reads it as text
return json.dumps(obs_dict, indent=2)
# Track the previous veto count to know if the CURRENT action was vetoed
server_state = {"last_veto_count": 0}
class ResetRequest(BaseModel):
scenario_id: int = 0 # Default to 0 (normal random episode)
@app.post("/reset")
def reset(req: ResetRequest = None):
# If no request body is sent, default to scenario 0
scenario = req.scenario_id if req else 0
# Pass the scenario option to your original environment
obs, info = env.reset(options={"scenario": scenario})
server_state["last_veto_count"] = env.veto_count
return {"observation": format_semantic_obs(env)}
@app.post("/step")
def step(req: ActionRequest):
try:
# 1. Parse the text action (e.g., "PATCH res_3")
parts = req.action_text.strip().split()
if len(parts) == 1 and parts[0] == "NOOP":
act_type_str, res_id_str = "NOOP", "0"
else:
act_type_str, res_id_str = parts[0], parts[1].replace("res_", "")
# 2. Convert to your internal discrete action ID
atype = ACTION_MAP[act_type_str]
ridx = int(res_id_str)
discrete_action = (atype * 10) + ridx # MAX_RESOURCES is 10
# 3. Step the environment
obs, reward, terminated, truncated, info = env.step(discrete_action)
# 4. Handle Vetoes (Check if the count went up THIS step!)
current_vetoes = env.veto_count
if current_vetoes > server_state["last_veto_count"]:
server_state["last_veto_count"] = current_vetoes
error_msg = f"Action {req.action_text} failed: Constraints not met (e.g., trying to modify an active critical resource, or resource doesn't need this action)."
return {"observation": error_msg, "reward": -0.1, "done": False}
server_state["last_veto_count"] = current_vetoes
# 5. Return standard OpenEnv semantic response
# FIXED: Pass the 'env' object directly
return {
"observation": format_semantic_obs(env),
"reward": float(reward),
"done": bool(terminated or truncated),
"info": info
}
except KeyError:
return {"observation": f"Syntax Error: Invalid action type. Use one of {list(ACTION_MAP.keys())}", "reward": -0.5, "done": False}
except Exception as e:
return {"observation": f"Execution Error: {str(e)}", "reward": -0.5, "done": False}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)