File size: 5,348 Bytes
83136ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
"""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"]