OpenEnv documentation

Customizing the Web UI

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.8.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Customizing the Web UI

The web UI is off by default. When ENABLE_WEB_INTERFACE=true (which openenv push sets for Spaces), the server serves a default Gradio app at /web that follows the loop an agent runs: reset, take an action (for MCP environments, pick a tool and fill its arguments), read the result, the episode so far, and the same call in Python. Environment authors can draw the environment’s state and offer one-click actions with two optional methods, or add a whole custom tab with a Gradio builder.

Draw the state and offer one-click actions

Override these Environment methods to make the default playground visual. Both receive the serialized observation, as reset() and step() return it, and both are optional:

  • render_web(observation) returns HTML that draws it (a board, a page, a plot), shown next to the action controls. The default returns None and the playground lists the observation’s fields.
  • web_actions(observation) returns (label, action) pairs shown as buttons, where each action is the dict step() receives. Clicking one runs that step. The default returns [].

For example, openspiel_env draws the Catch board and offers the legal moves:

class OpenSpielEnvironment(Environment):
    def web_actions(self, observation):
        names = {0: "left", 1: "stay", 2: "right"}
        return [(f"{a} · {names[a]}", {"action_id": a}) for a in observation["legal_actions"]]

    def render_web(self, observation):
        ...  # a 10 x 5 grid built from observation["info_state"]

Extension point: gradio_builder

create_app() accepts an optional gradio_builder callable. When set, the UI at /web is built with Gradio’s TabbedInterface: by default the first tab (“Playground”) is the default OpenEnv UI, and the second tab (“Custom”) is the gr.Blocks returned by your builder (see Naming and ordering the tabs to change this). Users can switch between the default Playground and your custom interface without losing either. The same /web/reset, /web/step, /web/state, and /web/metadata API routes remain available; your custom tab can use the provided web_manager in-process or call those endpoints.

Builder signature

def my_gradio_builder(
    web_manager,      # WebInterfaceManager: .reset_environment(), .step_environment(), .get_state()
    action_fields,    # list[dict]: from action schema for form generation
    metadata,        # EnvironmentMetadata | None: name, readme_content, etc.
    is_chat_env,     # bool: True if single message input
    title,           # str: app title (e.g. metadata.name)
    quick_start_md,  # str: Quick Start markdown (class names already replaced)
) -> gr.Blocks:
    ...

Return a gr.Blocks instance. By default it is shown in the “Custom” tab, next to the “Playground” tab with the default OpenEnv UI. Core applies the same theme/css when mounting.

Naming and ordering the tabs

create_app() takes a few options for the custom UI:

OptionDefaultEffect
custom_tab_name"Custom"Label of your tab
custom_tab_primaryFalseShow your tab first, before Playground
show_default_tabTrueWhen False, mount only your builder’s UI, with no Playground and no tabs
title_overrideNoneApp and browser-tab title, instead of "OpenEnv Agentic Environment: {name}"

Option 1: Add a custom tab

Provide a builder that returns your own gr.Blocks; it appears as the second tab (“Custom”) next to the default “Playground” tab:

# server/app.py
from openenv.core.env_server.http_server import create_app
from .my_environment import MyEnvironment
from ..models import MyAction, MyObservation
from .gradio_ui import build_my_gradio_app  # your module

app = create_app(
    MyEnvironment,
    MyAction,
    MyObservation,
    env_name="my_env",
    gradio_builder=build_my_gradio_app,
)

In server/gradio_ui.py implement build_my_gradio_app(web_manager, action_fields, metadata, is_chat_env, title, quick_start_md) returning a gr.Blocks (e.g. env-specific visualizations, extra controls). Use web_manager.reset_environment(), web_manager.step_environment(action_data), and web_manager.get_state() in your Gradio event handlers. The default Playground tab remains available in the first tab.


Option 2: Custom tab that wraps or reuses the default

Your builder can call the core build_gradio_app to get a Blocks instance and embed it inside your custom tab (e.g. in a gr.Tabs or as one section). That way your “Custom” tab can show both the default layout and additional content in one place.


Option 3: Custom Quick Start or README only

You don’t need a custom builder only to change text. The default UI uses:

  • Quick Start: generated from get_quick_start_markdown(metadata, action_cls, observation_cls) (init-style class names).
  • README: metadata.readme_content (loaded from the env’s README).

So you can influence the default UI by ensuring metadata and README are correct. To change the Quick Start template itself (e.g. different wording or placeholders), you would use a custom gradio_builder that calls build_gradio_app with a custom quick_start_md string you build yourself (or by copying and adapting the default template from the core).


Migration from custom HTML override (e.g. wildfire)

Environments that currently override /web with custom HTML (e.g. by removing the default route and adding a GET /web that returns HTML) should migrate to a gradio_builder that returns a gr.Blocks app. The custom UI then appears in the “Custom” tab alongside the default “Playground” tab. Benefits:

  • Single, supported extension point using TabbedInterface.
  • No need to remove or override routes; the default UI stays in the first tab.
  • Same /web path; both tabs can use web_manager or /web/reset, /web/step, /web/state.

If you need a non-Gradio custom UI (e.g. static HTML/JS), you can still register your own route after create_app (e.g. at /web/custom or another path), but the main /web slot is the Gradio tabbed app when ENABLE_WEB_INTERFACE=true.


Sign in with Hugging Face on a Space

A custom tab can sign visitors in with their Hugging Face account, for example so each visitor’s runs use their own Inference Providers credits instead of a token stored on the Space. tau2_env does this. A Docker Space needs four things:

  1. The Space README turns OAuth on, with the scopes you need:

    hf_oauth: true
    hf_oauth_scopes:
      - inference-api
  2. The image installs Gradio’s OAuth dependencies, authlib and itsdangerous (the gradio[oauth] extra).

  3. SYSTEM=spaces is set before the app is created. Gradio only uses the Space’s real OAuth when it is set, and Docker Spaces don’t set it, so it mocks the login instead:

    if os.environ.get("SPACE_ID"):
        os.environ.setdefault("SYSTEM", "spaces")
  4. The OAuth routes are forwarded to /web. The UI, and so Gradio’s /login/huggingface, /login/callback and /logout, are mounted under /web, while the sign-in button and Hugging Face’s callback use them at the root:

    @app.get("/login/huggingface", include_in_schema=False)
    @app.get("/login/callback", include_in_schema=False)
    @app.get("/logout", include_in_schema=False)
    def oauth_under_web(request: Request) -> RedirectResponse:
        query = f"?{request.url.query}" if request.url.query else ""
        return RedirectResponse(f"/web{request.url.path}{query}")

In the builder, add a gr.LoginButton() and take a gr.OAuthToken | None argument in the event handlers that need the visitor’s token. Off a Space the login is mocked with your local Hugging Face login, so show the button only when SPACE_ID is set.


Summary

GoalApproach
Use default UI onlyDo not pass gradio_builder.
Draw the state in the playgroundOverride render_web(observation) to return HTML.
One-click actionsOverride web_actions(observation) to return (label, action) pairs.
Add a custom tabPass gradio_builder=my_builder; return your own gr.Blocks (shown in “Custom” tab).
Custom tab + default insideIn your builder, call build_gradio_app(...) and embed or wrap it in your Blocks.
Change Quick Start / READMERely on metadata/README, or custom builder that builds custom markdown.
Sign visitors in on a Spacehf_oauth in the README, SYSTEM=spaces, OAuth routes forwarded to /web (details).

The default Playground tab is built with openenv.core.env_server.gradio_ui.build_gradio_app; you can import and call it with the same arguments if your custom tab needs to embed or extend it.

Update on GitHub