CoolFace
Apppublic

Ayush-Kumar0207/panopticon-protocol-v3

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes
_server.py511 linesDownload Raw Back to root
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