Spaces:
Running
Running
File size: 6,249 Bytes
d57737f df568bc d57737f df568bc d57737f df568bc d57737f 18d1b65 df568bc 18d1b65 ca3b01f df568bc d57737f 18d1b65 d57737f 18d1b65 d57737f 18d1b65 d57737f 18d1b65 74174cc df568bc 74174cc df568bc 18d1b65 df568bc 18d1b65 df568bc 0a9af47 cba593c 18d1b65 cba593c df568bc cba593c 18d1b65 df568bc d57737f 0a9af47 92b9ac9 ca3b01f 8fb4488 ca3b01f 8fb4488 ca3b01f 8fb4488 ca3b01f df568bc 0a9af47 92b9ac9 ca3b01f 92b9ac9 0a9af47 ca3b01f 0a9af47 065d894 7738e45 065d894 d57737f ca3b01f d57737f | 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | """
Each environment in Agent World Model is a self-contained FastAPI application
with SQLAlchemy/SQLite backend and MCP tool interface.
Usage:
PYTHONPATH=src:envs uvicorn envs.agent_world_model_env.server.app:app \\
--host 0.0.0.0 --port 8000
HTTP /reset and /step are disabled because AWM requires stateful WebSocket
connections — each HTTP request would create a fresh environment, dropping
the subprocess and tool cache.
"""
import os
import uvicorn
import gradio as gr
from fastapi import Request
from fastapi.responses import JSONResponse, RedirectResponse
from openenv.core.env_server.http_server import create_app
from ..models import AWMAction, AWMObservation
from .awm_environment import AWMEnvironment
from .config import MAX_CONCURRENT_ENVS
from .data_loader import AWMDataLoader
from .session_registry import registry as _registry
from .web_ui import build_awm_gradio_app
_shared_data_loader = AWMDataLoader()
def _env_factory():
return AWMEnvironment(data_loader=_shared_data_loader)
app = create_app(
_env_factory,
AWMAction,
AWMObservation,
env_name="agent_world_model_env",
max_concurrent_envs=MAX_CONCURRENT_ENVS,
)
def _swap_in_custom_gradio_ui() -> None:
"""Replace openenv-core's default web UI with the AWM Web Console.
The framework's ``gradio_builder`` parameter wraps our blocks inside a
``Playground | Custom`` TabbedInterface, which we don't want. Instead we
let the framework set up its default UI, then drop the default Mount +
legacy ``/web/*`` HTTP endpoints and mount our own blocks at ``/web``.
Pulls ``WebInterfaceManager`` out of the existing route closures.
"""
if os.environ.get("ENABLE_WEB_INTERFACE", "false").lower() not in (
"true",
"1",
"yes",
):
return
web_manager = None
metadata = None
for r in app.routes:
for cell in getattr(getattr(r, "endpoint", None), "__closure__", None) or ():
try:
v = cell.cell_contents
except ValueError:
continue
if web_manager is None and v.__class__.__name__ == "WebInterfaceManager":
web_manager = v
if metadata is None and v.__class__.__name__ == "EnvironmentMetadata":
metadata = v
if web_manager is not None and metadata is not None:
break
if web_manager is None:
return
# /web in 0.2.1 is the legacy "HumanAgent Interface" HTMLResponse, not a
# redirect — drop it together with the rest of the default UI's HTTP API.
legacy_paths = {
"/web",
"/web/reset",
"/web/step",
"/web/state",
"/web/metadata",
"/ws/ui",
}
app.routes[:] = [
r
for r in app.routes
if not (
(getattr(r, "path", None) == "/web" and r.__class__.__name__ == "Mount")
or getattr(r, "path", None) in legacy_paths
)
]
blocks = build_awm_gradio_app(
web_manager,
action_fields=None,
metadata=metadata,
is_chat_env=False,
title="agent_world_model_env",
quick_start_md=None,
)
gr.mount_gradio_app(app, blocks, path="/web")
_swap_in_custom_gradio_ui()
_HTTP_NOT_SUPPORTED_RESPONSE = {
"error": "HTTP mode not supported for AWM environment",
"reason": "AWM launches subprocesses on reset() that must persist across step() calls. "
"HTTP is stateless - each request creates a new environment instance, "
"losing the subprocess and all loaded tools.",
"solution": "Use WebSocket endpoint instead",
"examples": [
"Python: AWMEnv(base_url='http://host:port') # uses /ws internally",
"Direct: connect to ws://host:port/ws",
],
}
app.routes[:] = [
r for r in app.routes if getattr(r, "path", None) not in ("/reset", "/step")
]
@app.post("/reset", tags=["disabled"])
async def reset_not_supported():
return JSONResponse(status_code=400, content=_HTTP_NOT_SUPPORTED_RESPONSE)
@app.post("/step", tags=["disabled"])
async def step_not_supported():
return JSONResponse(status_code=400, content=_HTTP_NOT_SUPPORTED_RESPONSE)
@app.get("/stats", tags=["monitoring"])
async def stats():
return JSONResponse(content=_registry.get_stats())
def _has_route(path: str) -> bool:
return any(getattr(r, "path", None) == path for r in app.routes)
def _https_aware_redirect(request: Request, path: str) -> RedirectResponse:
# HF's reverse proxy rewrites relative redirects into absolute URLs and
# picks the scheme from the upstream request — which is HTTP. Build an
# explicit absolute URL with the original scheme so the iframe doesn't
# get blocked as mixed content.
host = request.headers.get("x-forwarded-host") or request.headers.get(
"host", request.url.netloc
)
proto = request.headers.get("x-forwarded-proto") or (
"https" if host.endswith(".hf.space") else request.url.scheme
)
return RedirectResponse(url=f"{proto}://{host}{path}")
# 0.2.1 doesn't auto-redirect / and /web to /web/. HF Spaces hits both.
if not _has_route("/"):
@app.get("/", include_in_schema=False)
async def _root_redirect(request: Request):
return _https_aware_redirect(request, "/web/")
if not _has_route("/web"):
@app.get("/web", include_in_schema=False)
async def _web_redirect(request: Request):
return _https_aware_redirect(request, "/web/")
@app.middleware("http")
async def _force_https_redirects(request: Request, call_next):
# HF Spaces' reverse proxy strips the original https scheme; any
# absolute Location header we emit goes out as http:// which gets
# blocked as mixed content inside the HF iframe. Force https for
# *.hf.space hosts.
response = await call_next(request)
loc = response.headers.get("location")
if loc and loc.startswith("http://") and ".hf.space" in loc:
response.headers["location"] = "https://" + loc[len("http://") :]
return response
def main():
uvicorn.run(
app,
host="0.0.0.0",
port=8000,
proxy_headers=True,
forwarded_allow_ips="*",
)
if __name__ == "__main__":
main()
|