Ayush-Kumar0207/panopticon-protocol-v3
0
1"""2The Panopticon Protocol v3 — FastAPI Server3=============================================4 5OpenEnv-compliant REST API for the counter-espionage RL environment.6 7Required endpoints:8 POST /reset — Reset to new episode9 POST /step — Execute agent action10 GET /tasks — List tasks with graders11 GET /metadata — Environment metadata12 POST /grade/{task_id} — Grade an episode13"""14 15from __future__ import annotations16from typing import Any17import os18from pathlib import Path19 20from fastapi import FastAPI, HTTPException21from fastapi.middleware.cors import CORSMiddleware22from fastapi.responses import RedirectResponse23from fastapi.staticfiles import StaticFiles24from pydantic import BaseModel25 26from environment import Environment, StepResult27from models import AgentAction, EnvironmentObservation, EnvironmentState, ActionType, SubAction, validate_action28from grader import GRADERS, grade_episode as _grade_episode, list_graders, GraderResult29from tasks import TASK_REGISTRY, get_task, list_tasks, list_tasks_with_graders30 31 32# =============================================================================33# REQUEST / RESPONSE MODELS34# =============================================================================35 36class ResetRequest(BaseModel):37 task_level: str = "easy"38 seed: int | None = None39 40 41class StepRequest(BaseModel):42 action_type: str43 target: str = ""44 sub_action: str = "none"45 reason: str = ""46 47 48class StepResponse(BaseModel):49 observation: dict50 reward: float51 done: bool52 truncated: bool53 info: dict54 55 56class ObservationResponse(BaseModel):57 observation: dict58 59 60class StateResponse(BaseModel):61 state: dict62 63 64class HealthResponse(BaseModel):65 status: str66 environment: str67 version: str68 69 70class AgentStatusResponse(BaseModel):71 model_config = {"protected_namespaces": ()}72 status: str73 policy: str74 model_ref: str75 local_model_present: bool76 loaded: bool77 error: str | None = None78 79 80class AgentStepResponse(BaseModel):81 model_config = {"protected_namespaces": ()}82 observation: dict83 reward: float84 done: bool85 truncated: bool86 info: dict87 agent_action: dict88 agent_policy: str89 agent_raw_text: str = ""90 model_info: dict | None = None91 92 93# =============================================================================94# ENVIRONMENT WRAPPER95# =============================================================================96 97_env: Environment | None = None98_agent_policy: Any | None = None99_agent_load_error: str | None = None100 101 102def environment_flag(name: str, default: bool = False) -> bool:103 value = os.environ.get(name)104 if value is None:105 return default106 return value.strip().lower() in {"1", "true", "yes", "on"}107 108 109PRIVILEGED_DEBUG_ENABLED = environment_flag("PANOPTICON_ENABLE_PRIVILEGED_DEBUG")110CORS_ORIGINS = [111 origin.strip()112 for origin in os.environ.get(113 "PANOPTICON_CORS_ORIGINS", "http://localhost:3000,http://localhost:7860"114 ).split(",")115 if origin.strip()116]117 118 119def get_env() -> Environment:120 global _env121 if _env is None:122 _env = Environment()123 return _env124 125 126def resolve_agent_model_ref() -> str:127 explicit_ref = os.environ.get("ARGUS_MODEL_REF")128 if explicit_ref:129 return explicit_ref130 131 local_model = Path(__file__).parent / "trained_model"132 if local_model.exists():133 return str(local_model)134 135 return "Ayush-Kumar0207/panopticon-argus-qwen-1.5B"136 137 138def get_agent_policy():139 global _agent_policy, _agent_load_error140 if _agent_policy is not None:141 return _agent_policy142 143 try:144 from inference_local import LocalModelPolicy145 146 _agent_policy = LocalModelPolicy(147 resolve_agent_model_ref(),148 deterministic=True,149 temperature=0.0,150 top_p=1.0,151 )152 _agent_load_error = None153 return _agent_policy154 except Exception as exc: # pragma: no cover - depends on local model/runtime availability155 _agent_load_error = str(exc)156 raise157 158 159# =============================================================================160# FASTAPI APP161# =============================================================================162 163app = FastAPI(164 title="The Panopticon Protocol v3",165 description=(166 "Counter-espionage RL environment where ARGUS (AI agent) defends a "167 "corporate network against HYDRA (adaptive adversary) using canary traps, "168 "double agents, and disinformation campaigns. Among Us… for AIs."169 ),170 version="3.0.0",171 docs_url="/docs",172 redoc_url="/redoc",173)174 175app.add_middleware(176 CORSMiddleware,177 allow_origins=CORS_ORIGINS,178 allow_credentials="*" not in CORS_ORIGINS,179 allow_methods=["*"], allow_headers=["*"],180)181 182 183# =============================================================================184# CORE ENDPOINTS (Required by OpenEnv validator)185# =============================================================================186 187@app.get("/")188async def root():189 static_dir = Path(__file__).parent / "static"190 if static_dir.exists() and (static_dir / "index.html").exists():191 return RedirectResponse(url="/dashboard/")192 return {193 "name": "The Panopticon Protocol v3",194 "tagline": "Among Us… for AIs",195 "version": "3.0.0",196 "mechanics": [197 "Canary Traps", "Multi-gen Sleepers", "False Flags",198 "Dead-man's Switches", "Double Agent Turning",199 "Disinformation Campaigns", "HYDRA Adaptive Memory",200 ],201 "endpoints": {202 "health": "GET /health", "reset": "POST /reset",203 "step": "POST /step", "observation": "GET /observation",204 "tasks": "GET /tasks", "metadata": "GET /metadata",205 "grade": "POST /grade/{task_id}",206 },207 }208 209 210@app.get("/health", response_model=HealthResponse)211async def health_check():212 return HealthResponse(213 status="healthy",214 environment="panopticon-protocol-v3",215 version="3.0.0",216 )217 218 219@app.get("/agent/status", response_model=AgentStatusResponse)220async def get_agent_status():221 model_ref = resolve_agent_model_ref()222 local_model_present = Path(model_ref).exists()223 224 if _agent_policy is not None:225 return AgentStatusResponse(226 status="ready",227 policy="trained",228 model_ref=model_ref,229 local_model_present=local_model_present,230 loaded=True,231 error=None,232 )233 234 if _agent_load_error:235 return AgentStatusResponse(236 status="error",237 policy="trained",238 model_ref=model_ref,239 local_model_present=local_model_present,240 loaded=False,241 error=_agent_load_error,242 )243 244 return AgentStatusResponse(245 status="configured",246 policy="trained",247 model_ref=model_ref,248 local_model_present=local_model_present,249 loaded=False,250 error=None,251 )252 253 254@app.post("/reset", response_model=ObservationResponse)255async def reset_environment(request: ResetRequest | None = None):256 try:257 env = get_env()258 if request is None:259 request = ResetRequest()260 obs = env.reset(task_level=request.task_level, seed=request.seed)261 return ObservationResponse(observation=obs.model_dump())262 except ValueError as e:263 raise HTTPException(status_code=400, detail=str(e))264 except Exception as e:265 raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")266 267 268@app.post("/step", response_model=StepResponse)269async def step_environment(request: StepRequest):270 try:271 env = get_env()272 273 # Validate action type274 try:275 action_type = ActionType(request.action_type)276 except ValueError:277 raise HTTPException(278 status_code=400,279 detail=f"Invalid action_type: '{request.action_type}'. "280 f"Valid: {[a.value for a in ActionType]}"281 )282 283 # Validate sub-action284 try:285 sub_action = SubAction(request.sub_action)286 except ValueError:287 sub_action = SubAction.NONE288 289 action = AgentAction(290 action_type=action_type.value,291 target=request.target,292 sub_action=sub_action.value,293 reason=request.reason,294 )295 result = env.step(action)296 return StepResponse(297 observation=result.observation.model_dump(),298 reward=result.reward,299 done=result.done,300 truncated=result.truncated,301 info=result.info,302 )303 except HTTPException:304 raise305 except RuntimeError as e:306 raise HTTPException(status_code=400, detail=str(e))307 except Exception as e:308 raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")309 310 311@app.post("/agent/step", response_model=AgentStepResponse)312async def step_with_trained_agent():313 try:314 env = get_env()315 obs = env.get_observation()316 policy = get_agent_policy()317 decision = policy.act(obs)318 319 is_valid, validation_error = validate_action(decision.action, obs)320 if not is_valid:321 decision.action = AgentAction(322 action_type=ActionType.NOOP.value,323 reason=f"Model produced invalid action: {validation_error}",324 )325 326 result = env.step(decision.action)327 info = dict(result.info)328 info["agent_action"] = decision.action.model_dump()329 if decision.action.reason:330 info["agent_reason"] = decision.action.reason331 332 model_info = policy.model_info() if hasattr(policy, "model_info") else None333 return AgentStepResponse(334 observation=result.observation.model_dump(),335 reward=result.reward,336 done=result.done,337 truncated=result.truncated,338 info=info,339 agent_action=decision.action.model_dump(),340 agent_policy=getattr(policy, "policy_name", "trained"),341 agent_raw_text=decision.raw_text,342 model_info=model_info,343 )344 except HTTPException:345 raise346 except RuntimeError as e:347 raise HTTPException(status_code=400, detail=str(e))348 except Exception as e:349 raise HTTPException(status_code=503, detail=f"Trained agent unavailable: {str(e)}")350 351 352@app.get("/observation", response_model=ObservationResponse)353async def get_observation():354 try:355 env = get_env()356 obs = env.get_observation()357 return ObservationResponse(observation=obs.model_dump())358 except Exception as e:359 raise HTTPException(status_code=500, detail=str(e))360 361 362@app.get("/state", response_model=StateResponse)363async def get_state():364 if not PRIVILEGED_DEBUG_ENABLED:365 raise HTTPException(status_code=404, detail="Privileged debug endpoint is disabled")366 try:367 env = get_env()368 return StateResponse(state=env.state.model_dump())369 except Exception as e:370 raise HTTPException(status_code=500, detail=str(e))371 372 373@app.get("/render")374async def render_environment():375 if not PRIVILEGED_DEBUG_ENABLED:376 raise HTTPException(status_code=404, detail="Privileged debug endpoint is disabled")377 try:378 return {"render": get_env().render()}379 except Exception as e:380 raise HTTPException(status_code=500, detail=str(e))381 382 383@app.get("/schema/action")384async def get_action_schema():385 return AgentAction.model_json_schema()386 387 388@app.get("/schema/observation")389async def get_observation_schema():390 return EnvironmentObservation.model_json_schema()391 392 393@app.get("/schema")394async def get_schemas():395 return {396 "action": AgentAction.model_json_schema(),397 "observation": EnvironmentObservation.model_json_schema(),398 "state": EnvironmentState.model_json_schema(),399 }400 401 402# =============================================================================403# TASKS & GRADERS ENDPOINTS404# =============================================================================405 406def _serialize_task(task: dict) -> dict:407 return {408 "id": task["id"], "name": task["name"],409 "description": task["description"],410 "max_turns": task["max_turns"],411 "difficulty": task["difficulty"],412 "has_grader": task.get("has_grader", False),413 "grader": task.get("grader", {}),414 "success_criteria": task.get("success_criteria", {}),415 }416 417 418@app.get("/tasks")419async def get_tasks_endpoint():420 all_tasks = list_tasks()421 serialized = [_serialize_task(t) for t in all_tasks]422 with_graders = [t for t in serialized if t.get("has_grader")]423 return {424 "tasks": serialized,425 "count": len(serialized),426 "tasks_with_graders": len(with_graders),427 "graders_available": len(with_graders),428 }429 430 431@app.get("/tasks/{task_id}")432async def get_task_endpoint(task_id: str):433 try:434 return _serialize_task(get_task(task_id))435 except ValueError:436 raise HTTPException(status_code=404, detail=f"Task '{task_id}' not found")437 438 439@app.get("/graders")440async def get_graders_endpoint():441 graders = list_graders()442 return {"graders": graders, "count": len(graders)}443 444 445@app.post("/grade/{task_id}")446async def grade_episode_endpoint(task_id: str, episode_data: dict):447 try:448 result = _grade_episode(task_id, episode_data)449 return result.to_dict()450 except ValueError as e:451 raise HTTPException(status_code=404, detail=str(e))452 except Exception as e:453 raise HTTPException(status_code=500, detail=f"Grading error: {str(e)}")454 455 456@app.get("/metadata")457async def get_metadata():458 all_tasks = list_tasks()459 serialized = [_serialize_task(t) for t in all_tasks]460 with_graders = [t for t in serialized if t.get("has_grader")]461 graders = list_graders()462 return {463 "name": "panopticon-protocol-v3",464 "display_name": "The Panopticon Protocol v3",465 "description": (466 "Counter-espionage RL environment where ARGUS defends a corporate "467 "network against HYDRA using canary traps, double agents, and "468 "disinformation campaigns. 7 stacking mechanics, 5 sleeper generations, "469 "6-phase narrative arc with adaptive adversary. Among Us… for AIs."470 ),471 "version": "3.0.0",472 "author": "Team Panopticon",473 "license": "Apache-2.0",474 "tasks": serialized,475 "tasks_count": len(serialized),476 "tasks_with_graders": len(with_graders),477 "graders_count": len(graders),478 "graders": graders,479 "evaluation": {480 "default_task": "medium",481 "scoring_range": [0.0, 1.0],482 "primary_metric": "normalized_score",483 },484 "mechanics": [485 "canary_traps", "multi_gen_sleepers", "false_flags",486 "dead_man_switches", "double_agent_turning",487 "disinformation_campaigns", "hydra_adaptive_memory",488 ],489 "tags": [490 "openenv", "hackathon", "counter-espionage", "adversarial-rl",491 "multi-agent", "deception-games", "turn-based",492 "json-observation", "json-action", "llm-agent",493 "multi-dimensional-grading",494 ],495 }496 497 498# ── Static files (dashboard) ──499_static_dir = Path(__file__).parent / "static"500if _static_dir.exists():501 app.mount("/dashboard", StaticFiles(directory=str(_static_dir), html=True), name="dashboard")502 503 504def main():505 import uvicorn506 uvicorn.run(app, host="0.0.0.0", port=8000)507 508 509if __name__ == "__main__":510 main()511 