terminus_env / server /gradio_ui.py
burtenshaw's picture
burtenshaw HF Staff
Upload folder using huggingface_hub
3661dd4 verified
Raw
History Blame Contribute Delete
11.5 kB
# 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.
"""Custom Gradio terminal UI for the Terminus environment."""
from __future__ import annotations
from typing import Any, Optional
import gradio as gr
COMMAND_EXAMPLES = {
"Inspect workspace": "pwd && ls -la",
"Create answer file": "mkdir -p /home/user/work && echo done > /home/user/work/answer.txt",
"Run Python": "python - <<'PY'\nprint('hello from the sandbox')\nPY",
"Install package": "pip install -q pytest",
"Run tests": "pytest -q",
}
DEFAULT_SETUP = "mkdir -p /home/user/work"
DEFAULT_VERIFY = "test -f /home/user/work/answer.txt"
_STYLE = """
<style>
.term-shell {
background: #020403;
border: 1px solid #16a34a;
box-shadow: inset 0 0 0 1px #052e16, 0 0 24px rgba(22, 163, 74, 0.18);
color: #d1fae5;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
min-height: 520px;
max-height: 720px;
overflow-y: auto;
padding: 14px;
}
.term-empty {
color: #6ee7b7;
padding: 30px 10px;
text-align: center;
}
.term-entry {
border-bottom: 1px solid #064e3b;
padding: 10px 0 12px;
}
.term-entry:last-child { border-bottom: 0; }
.term-meta {
color: #34d399;
font-size: 12px;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0;
}
.term-prompt {
color: #22c55e;
white-space: pre-wrap;
word-break: break-word;
}
.term-prompt::before {
color: #86efac;
}
.term-output {
color: #d1fae5;
margin-top: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.term-error {
color: #fca5a5;
margin-top: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.term-success { color: #86efac; }
.term-fail { color: #f87171; }
.term-final {
border: 1px solid #16a34a;
background: #04130a;
margin-top: 12px;
padding: 12px;
}
</style>
"""
_EMPTY_TERMINAL = (
"<div class='term-shell'><div class='term-empty'>"
"Reset the environment to create an HF sandbox, then run commands."
"</div></div>"
)
def _escape(value: Any) -> str:
return str(value).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def _lines(value: str) -> list[str]:
return [line.strip() for line in value.splitlines() if line.strip()]
def _extract_tool_text(result: dict[str, Any]) -> str:
obs = result.get("observation", {})
if not isinstance(obs, dict):
return ""
tool_result = obs.get("result", {})
if not isinstance(tool_result, dict):
return ""
content = tool_result.get("content", [])
if isinstance(content, list) and content:
first = content[0]
if isinstance(first, dict):
return str(first.get("text", ""))
return str(tool_result.get("data", ""))
def _state_summary(state: dict[str, Any]) -> dict[str, Any]:
return {
"episode_id": state.get("episode_id"),
"sandbox_id": state.get("sandbox_id"),
"step_count": state.get("step_count", 0),
"commands": len(state.get("commands", [])),
"setup_commands": len(state.get("setup_results", [])),
"verify_commands": len(state.get("verify_commands", [])),
"last_reward": state.get("last_reward"),
"last_error": state.get("last_error"),
}
def render_terminal_html(state: dict[str, Any]) -> str:
entries: list[str] = []
for index, item in enumerate(state.get("setup_results", []), start=1):
status = "ok" if item.get("success", False) else "failed"
status_cls = "term-success" if item.get("success", False) else "term-fail"
entries.append(
"<div class='term-entry'>"
f"<div class='term-meta'>setup {index} - <span class='{status_cls}'>{status}</span></div>"
f"<div class='term-prompt'>$ {_escape(item.get('command', ''))}</div>"
f"<div class='term-output'>{_escape(item.get('output', ''))}</div>"
f"{_error_html(item)}"
"</div>"
)
for index, item in enumerate(state.get("commands", []), start=1):
status = "ok" if item.get("success", False) else "failed"
status_cls = "term-success" if item.get("success", False) else "term-fail"
entries.append(
"<div class='term-entry'>"
f"<div class='term-meta'>command {index} - <span class='{status_cls}'>{status}</span></div>"
f"<div class='term-prompt'>$ {_escape(item.get('command', ''))}</div>"
f"<div class='term-output'>{_escape(item.get('output', ''))}</div>"
f"{_error_html(item)}"
"</div>"
)
if state.get("submitted_answer") is not None:
answer = _escape(state.get("submitted_answer", ""))
reward = _escape(state.get("last_reward", ""))
entries.append(
"<div class='term-final'>"
"<div class='term-meta'>final answer</div>"
f"<div>{answer}</div>"
f"<div class='term-meta'>reward: {reward}</div>"
"</div>"
)
if not entries:
return _STYLE + _EMPTY_TERMINAL
return _STYLE + "<div class='term-shell'>" + "\n".join(entries) + "</div>"
def _closed_terminal_html() -> str:
return (
_STYLE + "<div class='term-shell'><div class='term-empty'>"
"Session closed. Reset the sandbox to start a new terminal."
"</div></div>"
)
def _error_html(item: dict[str, Any]) -> str:
if not item.get("error"):
return ""
return f"<div class='term-error'>{_escape(item['error'])}</div>"
def terminus_ui_builder(
web_manager,
action_fields: list[dict[str, Any]],
metadata: Optional[Any],
is_chat_env: bool,
title: str,
quick_start_md: Optional[str],
) -> gr.Blocks:
def current_state() -> dict[str, Any]:
try:
return web_manager.get_state()
except RuntimeError:
return {}
with gr.Blocks(title=f"{title} - Terminal") as demo:
gr.Markdown(f"# {title}")
gr.Markdown(
"Single-tool terminal environment backed by an HF sandbox. "
"Reset creates a fresh session; commands run through `terminal(command=...)`."
)
with gr.Row():
with gr.Column(scale=1, min_width=320):
gr.Markdown("### Episode")
setup_input = gr.Textbox(
label="Setup commands",
value=DEFAULT_SETUP,
lines=5,
placeholder="One shell command per line",
)
verify_input = gr.Textbox(
label="Verify commands",
value=DEFAULT_VERIFY,
lines=4,
placeholder="One shell command per line",
)
reset_btn = gr.Button("Reset sandbox", variant="primary", size="lg")
close_btn = gr.Button("Stop / Close session", variant="secondary")
state_box = gr.JSON(label="Session state", value={})
gr.Markdown("### Command")
example_dd = gr.Dropdown(
choices=list(COMMAND_EXAMPLES.keys()),
label="Load example",
value=None,
interactive=True,
)
command_input = gr.Textbox(
label="Terminal command",
value="pwd && ls -la",
lines=6,
placeholder="Run shell commands in the HF sandbox",
)
run_btn = gr.Button("Run command", variant="primary")
gr.Markdown("### Submit")
answer_input = gr.Textbox(
label="Final answer",
value="done",
lines=2,
)
submit_btn = gr.Button("Submit final answer", variant="secondary")
with gr.Column(scale=2):
gr.Markdown("### Terminal")
terminal_display = gr.HTML(value=_STYLE + _EMPTY_TERMINAL)
status_bar = gr.Textbox(
label="Status",
value="Reset the sandbox before running commands.",
interactive=False,
lines=2,
)
async def on_reset(setup_text: str, verify_text: str):
payload = {
"setup": _lines(setup_text),
"verify": _lines(verify_text),
}
result = await web_manager.reset_environment(payload)
state = current_state()
done = result.get("done", False)
metadata = result.get("metadata", {})
if done and isinstance(metadata, dict):
status = (
metadata.get("error") or metadata.get("message") or "Reset failed."
)
else:
status = "Sandbox reset. Setup and verify configuration applied."
return render_terminal_html(state), _state_summary(state), status
async def on_close():
await web_manager._run_sync_in_thread_pool(web_manager.env.close)
return (
_closed_terminal_html(),
{},
"Session closed. The HF sandbox was released.",
)
async def on_run(command: str):
if not command.strip():
return gr.update(), gr.update(), "No command provided."
result = await web_manager.step_environment(
{
"tool_name": "terminal",
"arguments": {"command": command},
}
)
state = current_state()
text = _extract_tool_text(result)
status = text if text.startswith("Error:") else "Command completed."
return render_terminal_html(state), _state_summary(state), status
async def on_submit(answer: str):
if not answer.strip():
return gr.update(), gr.update(), "No final answer provided."
result = await web_manager.step_environment(
{
"tool_name": "terminal",
"arguments": {"final_answer": answer},
}
)
state = current_state()
status = _extract_tool_text(result) or "Final answer submitted."
return render_terminal_html(state), _state_summary(state), status
def on_example(choice: str):
if choice and choice in COMMAND_EXAMPLES:
return COMMAND_EXAMPLES[choice]
return gr.update()
reset_btn.click(
fn=on_reset,
inputs=[setup_input, verify_input],
outputs=[terminal_display, state_box, status_bar],
)
close_btn.click(
fn=on_close,
outputs=[terminal_display, state_box, status_bar],
)
run_btn.click(
fn=on_run,
inputs=[command_input],
outputs=[terminal_display, state_box, status_bar],
)
submit_btn.click(
fn=on_submit,
inputs=[answer_input],
outputs=[terminal_display, state_box, status_bar],
)
example_dd.change(
fn=on_example,
inputs=[example_dd],
outputs=[command_input],
)
return demo