Some checks failed
Contributor Attribution Check / check-attribution (pull_request) Failing after 36s
Docker Build and Publish / build-and-push (pull_request) Has been skipped
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 31s
Tests / e2e (pull_request) Successful in 3m37s
Tests / test (pull_request) Failing after 38m26s
195 lines
7.7 KiB
Python
195 lines
7.7 KiB
Python
"""Thin operator web console for the API server.
|
|
|
|
This keeps the UI intentionally small: an aiohttp-mounted cockpit that
|
|
surfaces Hermes health, browser runtime state, and ecosystem discovery
|
|
without introducing a second heavyweight frontend architecture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from html import escape
|
|
from typing import Any, Dict
|
|
|
|
from aiohttp import web
|
|
|
|
from tools.browser_tool import browser_runtime_heal, browser_runtime_status
|
|
|
|
_DISCOVERY_FRONTENDS = [
|
|
"Open WebUI",
|
|
"LobeChat",
|
|
"LibreChat",
|
|
"AnythingLLM",
|
|
"NextChat",
|
|
"ChatBox",
|
|
]
|
|
|
|
|
|
def _adapter(request: web.Request):
|
|
return request.app["api_server_adapter"]
|
|
|
|
|
|
def _auth_or_none(request: web.Request):
|
|
adapter = _adapter(request)
|
|
return adapter._check_auth(request)
|
|
|
|
|
|
def _render_console_html(adapter) -> str:
|
|
health = {
|
|
"platform": "api_server",
|
|
"host": adapter._host,
|
|
"port": adapter._port,
|
|
"model": adapter._model_name,
|
|
"auth_required": bool(adapter._api_key),
|
|
}
|
|
health_json = escape(json.dumps(health, indent=2, ensure_ascii=False))
|
|
return f'''<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Hermes Web Console</title>
|
|
<style>
|
|
:root {{ color-scheme: dark; --bg: #0b1020; --panel: #121933; --fg: #e5ecff; --muted: #9aa8d1; --accent: #72b8ff; --good: #6dde8a; }}
|
|
body {{ margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--bg); color: var(--fg); }}
|
|
header {{ padding: 20px 24px; border-bottom: 1px solid #243056; }}
|
|
main {{ padding: 24px; display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }}
|
|
.panel {{ background: var(--panel); border: 1px solid #243056; border-radius: 12px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.2); }}
|
|
h1, h2 {{ margin: 0 0 12px; }}
|
|
h1 {{ font-size: 24px; color: var(--accent); }}
|
|
h2 {{ font-size: 16px; color: var(--accent); }}
|
|
p, li, label {{ color: var(--muted); line-height: 1.5; }}
|
|
pre {{ margin: 0; white-space: pre-wrap; word-break: break-word; color: var(--fg); }}
|
|
button, input {{ font: inherit; }}
|
|
button {{ background: #1e2a52; color: var(--fg); border: 1px solid #39508f; border-radius: 8px; padding: 10px 14px; cursor: pointer; }}
|
|
button:hover {{ border-color: var(--accent); }}
|
|
input {{ width: 100%; box-sizing: border-box; background: #0d142a; color: var(--fg); border: 1px solid #243056; border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; }}
|
|
.row {{ display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }}
|
|
.badge {{ display: inline-block; color: var(--good); border: 1px solid #2f6940; border-radius: 999px; padding: 2px 10px; margin-left: 10px; font-size: 12px; }}
|
|
ul {{ margin: 0; padding-left: 18px; }}
|
|
code {{ color: var(--good); }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Hermes Web Console <span class="badge">operator cockpit</span></h1>
|
|
<p>Thin web UI over the existing API server, browser runtime, and streaming endpoints.</p>
|
|
</header>
|
|
<main>
|
|
<section class="panel">
|
|
<h2>Gateway Health</h2>
|
|
<pre id="health">{health_json}</pre>
|
|
</section>
|
|
<section class="panel">
|
|
<h2>Browser Cockpit</h2>
|
|
<label for="apiKey">Optional API key (only needed when API_SERVER_KEY is configured)</label>
|
|
<input id="apiKey" type="password" placeholder="sk-... or bearer token">
|
|
<div class="row">
|
|
<button id="refreshBtn">Refresh Browser Status</button>
|
|
<button id="healBtn">Heal Browser Layer</button>
|
|
</div>
|
|
<pre id="browserStatus">Loading...</pre>
|
|
</section>
|
|
<section class="panel">
|
|
<h2>Ecosystem Discovery</h2>
|
|
<ul>
|
|
<li><code>GET /v1/models</code> — OpenAI-compatible model discovery</li>
|
|
<li><code>POST /v1/chat/completions</code> — chat frontend compatibility</li>
|
|
<li><code>POST /v1/responses</code> — stateful responses API</li>
|
|
<li><code>POST /v1/runs</code> + <code>GET /v1/runs/{{run_id}}/events</code> — SSE lifecycle stream</li>
|
|
<li><code>GET /api/gui/browser/status</code> — browser runtime status</li>
|
|
<li><code>POST /api/gui/browser/heal</code> — cleanup + orphan reaper</li>
|
|
</ul>
|
|
<pre id="discovery">Loading...</pre>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
function authHeaders() {{
|
|
const key = document.getElementById('apiKey').value.trim();
|
|
return key ? {{ 'Authorization': 'Bearer ' + key }} : {{}};
|
|
}}
|
|
async function loadJson(path, options) {{
|
|
const response = await fetch(path, options);
|
|
const text = await response.text();
|
|
try {{ return {{ status: response.status, body: JSON.parse(text) }}; }}
|
|
catch (_) {{ return {{ status: response.status, body: {{ raw: text }} }}; }}
|
|
}}
|
|
async function refreshBrowser() {{
|
|
const result = await loadJson('/api/gui/browser/status', {{ headers: authHeaders() }});
|
|
document.getElementById('browserStatus').textContent = JSON.stringify(result, null, 2);
|
|
}}
|
|
async function healBrowser() {{
|
|
const result = await loadJson('/api/gui/browser/heal', {{ method: 'POST', headers: authHeaders() }});
|
|
document.getElementById('browserStatus').textContent = JSON.stringify(result, null, 2);
|
|
}}
|
|
async function loadDiscovery() {{
|
|
const result = await loadJson('/api/gui/discovery');
|
|
document.getElementById('discovery').textContent = JSON.stringify(result, null, 2);
|
|
}}
|
|
document.getElementById('refreshBtn').addEventListener('click', refreshBrowser);
|
|
document.getElementById('healBtn').addEventListener('click', healBrowser);
|
|
refreshBrowser();
|
|
loadDiscovery();
|
|
</script>
|
|
</body>
|
|
</html>'''
|
|
|
|
|
|
async def handle_web_console_index(request: web.Request) -> web.Response:
|
|
return web.Response(text=_render_console_html(_adapter(request)), content_type="text/html")
|
|
|
|
|
|
async def handle_gui_health(request: web.Request) -> web.Response:
|
|
adapter = _adapter(request)
|
|
return web.json_response({
|
|
"status": "ok",
|
|
"platform": "api_server",
|
|
"host": adapter._host,
|
|
"port": adapter._port,
|
|
"model": adapter._model_name,
|
|
"auth_required": bool(adapter._api_key),
|
|
})
|
|
|
|
|
|
async def handle_browser_status(request: web.Request) -> web.Response:
|
|
auth_err = _auth_or_none(request)
|
|
if auth_err is not None:
|
|
return auth_err
|
|
return web.json_response(browser_runtime_status())
|
|
|
|
|
|
async def handle_browser_heal(request: web.Request) -> web.Response:
|
|
auth_err = _auth_or_none(request)
|
|
if auth_err is not None:
|
|
return auth_err
|
|
return web.json_response(browser_runtime_heal())
|
|
|
|
|
|
async def handle_discovery(request: web.Request) -> web.Response:
|
|
adapter = _adapter(request)
|
|
return web.json_response({
|
|
"frontends": _DISCOVERY_FRONTENDS,
|
|
"operator_cockpit": {
|
|
"root": "/",
|
|
"health": "/api/gui/health",
|
|
"browser_status": "/api/gui/browser/status",
|
|
"browser_heal": "/api/gui/browser/heal",
|
|
},
|
|
"openai_compatible": {
|
|
"models": "/v1/models",
|
|
"chat_completions": "/v1/chat/completions",
|
|
"responses": "/v1/responses",
|
|
"runs": "/v1/runs",
|
|
"run_events": "/v1/runs/{run_id}/events",
|
|
"model_name": adapter._model_name,
|
|
},
|
|
})
|
|
|
|
|
|
def maybe_register_web_console(app: web.Application) -> None:
|
|
app.router.add_get("/", handle_web_console_index)
|
|
app.router.add_get("/api/gui/health", handle_gui_health)
|
|
app.router.add_get("/api/gui/browser/status", handle_browser_status)
|
|
app.router.add_post("/api/gui/browser/heal", handle_browser_heal)
|
|
app.router.add_get("/api/gui/discovery", handle_discovery)
|