File size: 9,301 Bytes
0f6657d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
Gradio-based web UI for OpenEnv environments.

Replaces the legacy HTML/JavaScript interface when ENABLE_WEB_INTERFACE is set.
Mount at /web via gr.mount_gradio_app() from create_web_interface_app().
"""

from __future__ import annotations

import json
import re
from typing import Any, Dict, List, Optional

import gradio as gr

from .types import EnvironmentMetadata


def _escape_md(text: str) -> str:
    """Escape Markdown special characters in user-controlled content."""
    return re.sub(r"([\\`*_\{\}\[\]()#+\-.!|~>])", r"\\\1", str(text))


def _format_observation(data: Dict[str, Any]) -> str:
    """Format reset/step response for Markdown display."""
    lines: List[str] = []
    obs = data.get("observation", {})
    if isinstance(obs, dict):
        if obs.get("prompt"):
            lines.append(f"**Prompt:**\n\n{_escape_md(obs['prompt'])}\n")
        messages = obs.get("messages", [])
        if messages:
            lines.append("**Messages:**\n")
            for msg in messages:
                sender = _escape_md(str(msg.get("sender_id", "?")))
                content = _escape_md(str(msg.get("content", "")))
                cat = _escape_md(str(msg.get("category", "")))
                lines.append(f"- `[{cat}]` Player {sender}: {content}")
            lines.append("")
    reward = data.get("reward")
    done = data.get("done")
    if reward is not None:
        lines.append(f"**Reward:** `{reward}`")
    if done is not None:
        lines.append(f"**Done:** `{done}`")
    return "\n".join(lines) if lines else "*No observation data*"


def _readme_section(metadata: Optional[EnvironmentMetadata]) -> str:
    """README content for the left panel."""
    if not metadata or not metadata.readme_content:
        return "*No README available.*"
    return metadata.readme_content


def get_gradio_display_title(
    metadata: Optional[EnvironmentMetadata],
    fallback: str = "OpenEnv Environment",
) -> str:
    """Return the title used for the Gradio app (browser tab and Blocks)."""
    name = metadata.name if metadata else fallback
    return f"OpenEnv Agentic Environment: {name}"


def build_gradio_app(
    web_manager: Any,
    action_fields: List[Dict[str, Any]],
    metadata: Optional[EnvironmentMetadata],
    is_chat_env: bool,
    title: str = "OpenEnv Environment",
    quick_start_md: Optional[str] = None,
) -> gr.Blocks:
    """
    Build a Gradio Blocks app for the OpenEnv web interface.

    Args:
        web_manager: WebInterfaceManager (reset/step_environment, get_state).
        action_fields: Field dicts from _extract_action_fields(action_cls).
        metadata: Environment metadata for README/name.
        is_chat_env: If True, single message textbox; else form from action_fields.
        title: App title (overridden by metadata.name when present; see get_gradio_display_title).
        quick_start_md: Optional Quick Start markdown (class names already replaced).

    Returns:
        gr.Blocks to mount with gr.mount_gradio_app(app, blocks, path="/web").
    """
    readme_content = _readme_section(metadata)
    display_title = get_gradio_display_title(metadata, fallback=title)

    async def reset_env():
        try:
            data = await web_manager.reset_environment()
            obs_md = _format_observation(data)
            return (
                obs_md,
                json.dumps(data, indent=2),
                "Environment reset successfully.",
            )
        except Exception as e:
            return ("", "", f"Error: {e}")

    def _step_with_action(action_data: Dict[str, Any]):
        async def _run():
            try:
                data = await web_manager.step_environment(action_data)
                obs_md = _format_observation(data)
                return (
                    obs_md,
                    json.dumps(data, indent=2),
                    "Step complete.",
                )
            except Exception as e:
                return ("", "", f"Error: {e}")

        return _run

    async def step_chat(message: str):
        if not (message or str(message).strip()):
            return ("", "", "Please enter an action message.")
        action = {"message": str(message).strip()}
        return await _step_with_action(action)()

    def get_state_sync():
        try:
            data = web_manager.get_state()
            return json.dumps(data, indent=2)
        except Exception as e:
            return f"Error: {e}"

    with gr.Blocks(title=display_title) as demo:
        with gr.Row():
            with gr.Column(scale=1, elem_classes="col-left"):
                if quick_start_md:
                    with gr.Accordion("Quick Start", open=True):
                        gr.Markdown(quick_start_md)
                with gr.Accordion("README", open=False):
                    gr.Markdown(readme_content)

            with gr.Column(scale=2, elem_classes="col-right"):
                obs_display = gr.Markdown(
                    value=("# Playground\n\nClick **Reset** to start a new episode."),
                )
                with gr.Group():
                    if is_chat_env:
                        action_input = gr.Textbox(
                            label="Action message",
                            placeholder="e.g. Enter your message...",
                        )
                        step_inputs = [action_input]
                        step_fn = step_chat
                    else:
                        step_inputs = []
                        for field in action_fields:
                            name = field["name"]
                            field_type = field.get("type", "text")
                            label = name.replace("_", " ").title()
                            placeholder = field.get("placeholder", "")
                            if field_type == "checkbox":
                                inp = gr.Checkbox(label=label)
                            elif field_type == "number":
                                inp = gr.Number(label=label)
                            elif field_type == "select":
                                choices = field.get("choices") or []
                                inp = gr.Dropdown(
                                    choices=choices,
                                    label=label,
                                    allow_custom_value=False,
                                )
                            elif field_type in ("textarea", "tensor"):
                                inp = gr.Textbox(
                                    label=label,
                                    placeholder=placeholder,
                                    lines=3,
                                )
                            else:
                                inp = gr.Textbox(
                                    label=label,
                                    placeholder=placeholder,
                                )
                            step_inputs.append(inp)

                        async def step_form(*values):
                            if not action_fields:
                                return await _step_with_action({})()
                            action_data = {}
                            for i, field in enumerate(action_fields):
                                if i >= len(values):
                                    break
                                name = field["name"]
                                val = values[i]
                                if field.get("type") == "checkbox":
                                    action_data[name] = bool(val)
                                elif val is not None and val != "":
                                    action_data[name] = val
                            return await _step_with_action(action_data)()

                        step_fn = step_form

                    with gr.Row():
                        step_btn = gr.Button("Step", variant="primary")
                        reset_btn = gr.Button("Reset", variant="secondary")
                        state_btn = gr.Button("Get state", variant="secondary")
                    with gr.Row():
                        status = gr.Textbox(
                            label="Status",
                            interactive=False,
                        )
                    raw_json = gr.Code(
                        label="Raw JSON response",
                        language="json",
                        interactive=False,
                    )

        reset_btn.click(
            fn=reset_env,
            outputs=[obs_display, raw_json, status],
        )
        step_btn.click(
            fn=step_fn,
            inputs=step_inputs,
            outputs=[obs_display, raw_json, status],
        )
        if is_chat_env:
            action_input.submit(
                fn=step_fn,
                inputs=step_inputs,
                outputs=[obs_display, raw_json, status],
            )
        state_btn.click(
            fn=get_state_sync,
            outputs=[raw_json],
        )

    return demo