chaosops / env /openenv_wrapper.py
helloAK96's picture
Initializing space
83136ac
"""OpenEnv client + FastAPI server entry points.
The :class:`ChaosOpsClient` is the thing TRL-style training code imports:
from chaosops.env.openenv_wrapper import ChaosOpsClient
with ChaosOpsClient(base_url="http://localhost:8000").sync() as env:
obs = env.reset()
obs = env.step(action)
The server side is created with :func:`build_fastapi_app`, which returns a
ready-to-serve FastAPI instance. ``server/app.py`` simply re-exports it so
``uvicorn chaosops.env.openenv_wrapper:app`` works out of the box.
If ``openenv-core`` is not installed the client import will raise at the
call site β€” consistent with the rest of the package: the simulator itself
has no hard dependency on OpenEnv so unit tests stay lightweight.
"""
from __future__ import annotations
from typing import Any
from chaosops.env.environment import ChaosOpsEnvironment
from chaosops.env.models import (
ChaosOpsAction,
ChaosOpsObservation,
ChaosOpsState,
RoleView,
)
try: # pragma: no cover β€” optional dependency
from openenv.core.env_client import EnvClient
from openenv.core.env_server import create_fastapi_app
from openenv.core.client_types import StepResult
_HAS_OPENENV = True
except ImportError: # pragma: no cover
_HAS_OPENENV = False
EnvClient = object # type: ignore[assignment, misc]
StepResult = None # type: ignore[assignment]
create_fastapi_app = None # type: ignore[assignment]
if _HAS_OPENENV:
class ChaosOpsClient(EnvClient[ChaosOpsAction, ChaosOpsObservation, ChaosOpsState]): # type: ignore[misc]
"""Typed OpenEnv client for ChaosOps AI."""
def _step_payload(self, action: ChaosOpsAction) -> dict[str, Any]:
return action.model_dump(mode="json")
def _parse_result(self, payload: dict[str, Any]) -> "StepResult": # type: ignore[name-defined]
obs_data = payload.get("observation", {})
view_data = obs_data.get("view", {})
view = RoleView.model_validate(view_data) if view_data else None
observation = ChaosOpsObservation(
done=payload.get("done", False),
reward=payload.get("reward"),
view=view, # type: ignore[arg-type]
step=obs_data.get("step", 0),
turn_role=obs_data.get("turn_role"),
message=obs_data.get("message", ""),
)
return StepResult( # type: ignore[operator]
observation=observation,
reward=payload.get("reward"),
done=payload.get("done", False),
)
def _parse_state(self, payload: dict[str, Any]) -> ChaosOpsState:
return ChaosOpsState.model_validate(payload)
def build_fastapi_app(): # type: ignore[no-untyped-def]
"""Return a ready-to-serve FastAPI app exposing ChaosOps AI.
``create_fastapi_app`` expects an environment *factory*, plus the
action and observation Pydantic classes so it can validate incoming
payloads before handing them to the simulator.
"""
assert create_fastapi_app is not None
return create_fastapi_app(
env=ChaosOpsEnvironment,
action_cls=ChaosOpsAction,
observation_cls=ChaosOpsObservation,
)
# Build the app at import time so ``uvicorn chaosops.env.openenv_wrapper:app``
# works. Any error here is a config/install problem β€” fail loudly rather
# than serve a half-broken app.
try:
app = build_fastapi_app()
except Exception: # pragma: no cover β€” surfaced via chaosops-serve
app = None
else:
class ChaosOpsClient: # type: ignore[no-redef]
"""Placeholder β€” install ``openenv-core`` to use the HTTP client."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
raise ImportError(
"openenv-core is not installed. Run `pip install openenv-core` "
"to use the ChaosOps HTTP client."
)
def build_fastapi_app(): # type: ignore[no-untyped-def]
raise ImportError("openenv-core is required to build the FastAPI server app.")
app = None
def serve_cli() -> None:
"""``chaosops-serve`` console entry point.
Boots the FastAPI app with uvicorn. Raises a clear error if the
optional ``openenv`` extras are not installed.
"""
try:
import uvicorn # type: ignore[import-not-found]
except ImportError as exc:
raise SystemExit(
"uvicorn is required for chaosops-serve. "
"Install with: pip install 'chaosops[openenv]'"
) from exc
if not _HAS_OPENENV:
raise SystemExit(
"openenv-core is required for chaosops-serve. "
"Install with: pip install 'chaosops[openenv]'"
)
import argparse
parser = argparse.ArgumentParser(description="ChaosOps AI HTTP server")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--reload", action="store_true")
args = parser.parse_args()
uvicorn.run(
"chaosops.env.openenv_wrapper:app",
host=args.host,
port=args.port,
reload=args.reload,
)
__all__ = ["ChaosOpsClient", "build_fastapi_app", "app", "serve_cli"]