Add opt-in /assistent/chat-eval for AssistentChat via Debug API.
Lets agents POST/GET a real Assistent turn (tunnel or SSH) without folding it into cheap /snapshot; documents VRAM/Sqlite side effects. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
-13
@@ -1,6 +1,8 @@
|
||||
"""Localhost read-only debug HTTP sidecar for gpu-rent.
|
||||
"""Localhost debug HTTP sidecar for gpu-rent.
|
||||
|
||||
Bind only 127.0.0.1. Started from `up` / `tunnel` / `debug`. No mutations.
|
||||
Bind only 127.0.0.1. Started from `up` / `tunnel` / `debug`.
|
||||
Mostly read-only; `/assistent/chat-eval` is an opt-in AssistentChat probe
|
||||
(may load VRAM / persist chat) and is not part of `/snapshot`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,14 +30,49 @@ CACHE_TTL_DIAG = 30.0
|
||||
CACHE_TTL_LOGS = 10.0
|
||||
CACHE_TTL_DEFAULT = 8.0
|
||||
|
||||
_CHAT_EVAL_PARAMS = [
|
||||
{
|
||||
"name": "message",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "User turn (default: short RU checkpoint/steps/cfg ask)",
|
||||
},
|
||||
{
|
||||
"name": "persona",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Persona id (default neutral)",
|
||||
},
|
||||
{
|
||||
"name": "pack",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Pack id (default ordinary)",
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"in": "query",
|
||||
"schema": {"type": "string"},
|
||||
"description": "Ollama chat model override (else preferred / default_chat)",
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"in": "query",
|
||||
"schema": {"type": "number", "default": 120},
|
||||
"description": "Seconds, capped (15–300)",
|
||||
},
|
||||
]
|
||||
|
||||
_OPENAPI: dict[str, Any] = {
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "gpu-rent Debug API",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": (
|
||||
"Read-only localhost diagnostics for installer progress and "
|
||||
"SwarmUI / Comfy / Ollama / Assistent. No mutations."
|
||||
"Localhost diagnostics for installer progress and "
|
||||
"SwarmUI / Comfy / Ollama / Assistent. Mostly read-only; "
|
||||
"POST/GET /assistent/chat-eval is opt-in AssistentChat "
|
||||
"(may load VRAM / write chat) and is skipped from /snapshot."
|
||||
),
|
||||
},
|
||||
"servers": [{"url": "http://127.0.0.1:17821"}],
|
||||
@@ -57,7 +94,9 @@ _OPENAPI: dict[str, Any] = {
|
||||
],
|
||||
}
|
||||
},
|
||||
"/snapshot": {"get": {"summary": "Cheap aggregate (no full diag)"}},
|
||||
"/snapshot": {
|
||||
"get": {"summary": "Cheap aggregate (no full diag, no chat-eval)"}
|
||||
},
|
||||
"/status": {"get": {"summary": "JSON mirror of gpu-rent status"}},
|
||||
"/state": {"get": {"summary": "state.json without secrets"}},
|
||||
"/config": {"get": {"summary": "Config with tokens/passwords redacted"}},
|
||||
@@ -132,6 +171,38 @@ _OPENAPI: dict[str, Any] = {
|
||||
"/assistent/logs": {
|
||||
"get": {"summary": "journalctl swarmui filtered for Assistent/Sqlite/build"}
|
||||
},
|
||||
"/assistent/chat-eval": {
|
||||
"get": {
|
||||
"summary": (
|
||||
"Opt-in AssistentChat eval (GetNewSession→AssistentChat); "
|
||||
"may load VRAM / write chat — not in /snapshot"
|
||||
),
|
||||
"parameters": _CHAT_EVAL_PARAMS,
|
||||
},
|
||||
"post": {
|
||||
"summary": (
|
||||
"Same as GET; prefer JSON body "
|
||||
"{message,persona,pack,model,timeout}"
|
||||
),
|
||||
"requestBody": {
|
||||
"required": False,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"},
|
||||
"persona": {"type": "string"},
|
||||
"pack": {"type": "string"},
|
||||
"model": {"type": "string"},
|
||||
"timeout": {"type": "number"},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -306,27 +377,51 @@ def _make_handler(hub: DebugHub):
|
||||
)
|
||||
self._send(code, body, "application/json; charset=utf-8")
|
||||
|
||||
def _path_summary(self, path: str) -> str:
|
||||
ops = _OPENAPI["paths"].get(path) or {}
|
||||
for method in ("get", "post"):
|
||||
if method in ops and isinstance(ops[method], dict):
|
||||
return str(ops[method].get("summary") or method)
|
||||
return ""
|
||||
|
||||
def _html_index(self) -> None:
|
||||
links = sorted(p for p in _OPENAPI["paths"] if p != "/")
|
||||
rows = "\n".join(
|
||||
f'<li><a href="{p}">{p}</a> — '
|
||||
f'{_OPENAPI["paths"][p]["get"]["summary"]}</li>'
|
||||
f'<li><a href="{p}">{p}</a> — {self._path_summary(p)}</li>'
|
||||
for p in links
|
||||
)
|
||||
html = (
|
||||
"<!DOCTYPE html><html><head><meta charset=utf-8>"
|
||||
"<title>gpu-rent debug</title></head><body>"
|
||||
f"<h1>gpu-rent Debug API</h1>"
|
||||
f"<p>base <code>{hub.base_url}</code> · read-only · 127.0.0.1</p>"
|
||||
f"<p>base <code>{hub.base_url}</code> · mostly read-only · 127.0.0.1</p>"
|
||||
f"<p>Agent: start at <a href='/openapi.json'>/openapi.json</a> "
|
||||
f"then <a href='/snapshot'>/snapshot</a>.</p>"
|
||||
f"then <a href='/snapshot'>/snapshot</a>. "
|
||||
f"Opt-in chat: <code>/assistent/chat-eval</code> (not in snapshot).</p>"
|
||||
f"<ul>{rows}</ul></body></html>"
|
||||
)
|
||||
self._send(200, html.encode("utf-8"), "text/html; charset=utf-8")
|
||||
|
||||
def _read_json_body(self) -> dict[str, Any]:
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
if length <= 0:
|
||||
return {}
|
||||
raw = self.rfile.read(min(length, 1_000_000))
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(raw.decode("utf-8", "replace"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"invalid JSON body: {exc}") from exc
|
||||
if data is None:
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("JSON body must be an object")
|
||||
return data
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
try:
|
||||
self._dispatch()
|
||||
self._dispatch("GET")
|
||||
except Exception as exc:
|
||||
self._json(
|
||||
500,
|
||||
@@ -337,19 +432,47 @@ def _make_handler(hub: DebugHub):
|
||||
},
|
||||
)
|
||||
|
||||
def _dispatch(self) -> None:
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
try:
|
||||
self._dispatch("POST")
|
||||
except Exception as exc:
|
||||
self._json(
|
||||
500,
|
||||
{
|
||||
"ok": False,
|
||||
"error": str(exc)[:300],
|
||||
"trace": traceback.format_exc()[-800:],
|
||||
},
|
||||
)
|
||||
|
||||
def _dispatch(self, method: str = "GET") -> None:
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/") or "/"
|
||||
qs = parse_qs(parsed.query)
|
||||
|
||||
if path == "/":
|
||||
if method != "GET":
|
||||
self._json(405, {"ok": False, "error": "GET only"})
|
||||
return
|
||||
self._html_index()
|
||||
return
|
||||
if path == "/openapi.json":
|
||||
if method != "GET":
|
||||
self._json(405, {"ok": False, "error": "GET only"})
|
||||
return
|
||||
doc = dict(_OPENAPI)
|
||||
doc["servers"] = [{"url": hub.base_url}]
|
||||
self._json(200, doc)
|
||||
return
|
||||
if method == "POST" and path != "/assistent/chat-eval":
|
||||
self._json(
|
||||
405,
|
||||
{
|
||||
"ok": False,
|
||||
"error": "POST only allowed for /assistent/chat-eval",
|
||||
},
|
||||
)
|
||||
return
|
||||
if path == "/progress":
|
||||
self._json(200, hub.progress())
|
||||
return
|
||||
@@ -534,6 +657,30 @@ def _make_handler(hub: DebugHub):
|
||||
CACHE_TTL_LOGS,
|
||||
lambda: debug_assistent.collect_assistent_logs(hub.cfg),
|
||||
)
|
||||
elif sub == "chat-eval":
|
||||
body: dict[str, Any] = {}
|
||||
if method == "POST":
|
||||
try:
|
||||
body = self._read_json_body()
|
||||
except ValueError as exc:
|
||||
self._json(400, {"ok": False, "error": str(exc)})
|
||||
return
|
||||
|
||||
def _q(name: str) -> str | None:
|
||||
if name in body and body[name] is not None:
|
||||
return body[name]
|
||||
vals = qs.get(name)
|
||||
return vals[0] if vals else None
|
||||
|
||||
# Never cache — live AssistentChat / VRAM side effects.
|
||||
payload = debug_assistent.run_assistent_chat_eval(
|
||||
hub.cfg,
|
||||
message=_q("message"),
|
||||
persona=_q("persona"),
|
||||
pack=_q("pack"),
|
||||
model=_q("model"),
|
||||
timeout=_q("timeout"),
|
||||
)
|
||||
else:
|
||||
self._json(
|
||||
404,
|
||||
@@ -549,6 +696,7 @@ def _make_handler(hub: DebugHub):
|
||||
"/assistent/api",
|
||||
"/assistent/wanted",
|
||||
"/assistent/logs",
|
||||
"/assistent/chat-eval",
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -611,6 +759,7 @@ def start_debug_server(
|
||||
f" {base}/openapi.json",
|
||||
f" {base}/snapshot",
|
||||
f" {base}/assistent",
|
||||
f" {base}/assistent/chat-eval (opt-in; not in snapshot)",
|
||||
]
|
||||
if log:
|
||||
for line in lines:
|
||||
@@ -646,7 +795,8 @@ def run_debug_blocking(
|
||||
server.set_step("idle_debug")
|
||||
try:
|
||||
if log:
|
||||
log("Debug API слушает (Ctrl+C — выход). Мутаций нет.")
|
||||
log("Debug API слушает (Ctrl+C — выход). "
|
||||
"Мутаций конфига/GPU нет; /assistent/chat-eval — opt-in AssistentChat.")
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in New Issue
Block a user