Add offline analysis and client event tracking to Assistent API
- Introduced `/assistent/analyze-reply` endpoint for offline extraction of patches and client predictions without VRAM. - Added `/assistent/client-event` and `/assistent/client-events` endpoints for tracking UI interactions and retrieving event history. - Updated Debug API documentation to reflect new endpoints and their functionalities. - Enhanced Assistent session handling with improved client prediction logic and diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+155
-2
@@ -74,13 +74,15 @@ _OPENAPI: dict[str, Any] = {
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "gpu-rent Debug API",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": (
|
||||
"Localhost diagnostics for installer progress and "
|
||||
"SwarmUI / Comfy / Ollama / Assistent. Mostly read-only. "
|
||||
"Assistent multi-turn: POST /assistent/session + "
|
||||
"/assistent/session/{id}/chat (trace object). "
|
||||
"One-shot: POST/GET /assistent/chat-eval. "
|
||||
"Offline reply score: POST /assistent/analyze-reply "
|
||||
"(no VRAM). "
|
||||
+ _WARN_VRAM
|
||||
),
|
||||
},
|
||||
@@ -278,6 +280,54 @@ _OPENAPI: dict[str, Any] = {
|
||||
},
|
||||
}
|
||||
},
|
||||
"/assistent/analyze-reply": {
|
||||
"post": {
|
||||
"summary": (
|
||||
"Offline: extract patch + 0.15.3 client would_generate "
|
||||
"from a pasted {message, reply}. No Swarm / VRAM."
|
||||
),
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["reply"],
|
||||
"properties": {
|
||||
"message": {"type": "string"},
|
||||
"reply": {"type": "string"},
|
||||
"persona": {"type": "string"},
|
||||
"pack": {"type": "string"},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"get": {
|
||||
"summary": "Same as POST; query message/reply (short only)",
|
||||
},
|
||||
},
|
||||
"/assistent/client-event": {
|
||||
"post": {
|
||||
"summary": (
|
||||
"Browser beacon of last UI turn (intent, patch, "
|
||||
"swarm_prompt). CORS localhost. No VRAM."
|
||||
),
|
||||
}
|
||||
},
|
||||
"/assistent/client-events": {
|
||||
"get": {
|
||||
"summary": "In-memory UI beacons (query since=seq)",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "since",
|
||||
"in": "query",
|
||||
"schema": {"type": "integer"},
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
"/assistent/chat-eval": {
|
||||
"get": {
|
||||
"summary": (
|
||||
@@ -339,6 +389,10 @@ class DebugHub:
|
||||
step: str = "starting"
|
||||
step_started_at: float = field(default_factory=time.time)
|
||||
_events: deque[dict[str, Any]] = field(default_factory=lambda: deque(maxlen=EVENT_CAPACITY))
|
||||
_client_events: deque[dict[str, Any]] = field(
|
||||
default_factory=lambda: deque(maxlen=80)
|
||||
)
|
||||
_client_seq: int = 0
|
||||
_seq: int = 0
|
||||
_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||
_cache: dict[str, tuple[float, Any]] = field(default_factory=dict)
|
||||
@@ -403,6 +457,21 @@ class DebugHub:
|
||||
items = list(self._events)
|
||||
return items[-n:]
|
||||
|
||||
def push_client_event(self, event: dict[str, Any]) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
self._client_seq += 1
|
||||
row = {
|
||||
"seq": self._client_seq,
|
||||
"ts": time.time(),
|
||||
**{k: event[k] for k in event if k != "seq"},
|
||||
}
|
||||
self._client_events.append(row)
|
||||
return {"ok": True, "seq": row["seq"], "stored": True}
|
||||
|
||||
def client_events_since(self, since: int = 0) -> list[dict[str, Any]]:
|
||||
with self._lock:
|
||||
return [e for e in self._client_events if int(e.get("seq") or 0) > since]
|
||||
|
||||
def progress(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
last = self._events[-1] if self._events else None
|
||||
@@ -490,6 +559,9 @@ def _make_handler(hub: DebugHub):
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
@@ -521,7 +593,8 @@ def _make_handler(hub: DebugHub):
|
||||
f"then <a href='/snapshot'>/snapshot</a>. "
|
||||
f"Opt-in Assistent: <code>/assistent/session</code> (multi-turn) · "
|
||||
f"<code>/assistent/chat-eval</code> (one-shot) — not in snapshot; "
|
||||
f"may load VRAM.</p>"
|
||||
f"may load VRAM. Offline: "
|
||||
f"<code>/assistent/analyze-reply</code>.</p>"
|
||||
f"<ul>{rows}</ul></body></html>"
|
||||
)
|
||||
self._send(200, html.encode("utf-8"), "text/html; charset=utf-8")
|
||||
@@ -543,6 +616,14 @@ def _make_handler(hub: DebugHub):
|
||||
raise ValueError("JSON body must be an object")
|
||||
return data
|
||||
|
||||
def do_OPTIONS(self) -> None: # noqa: N802
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
try:
|
||||
self._dispatch("GET")
|
||||
@@ -603,6 +684,8 @@ def _make_handler(hub: DebugHub):
|
||||
return
|
||||
if method == "POST" and path not in {
|
||||
"/assistent/chat-eval",
|
||||
"/assistent/analyze-reply",
|
||||
"/assistent/client-event",
|
||||
"/assistent/session",
|
||||
} and not _is_assistent_session_chat(path):
|
||||
self._json(
|
||||
@@ -611,6 +694,7 @@ def _make_handler(hub: DebugHub):
|
||||
"ok": False,
|
||||
"error": (
|
||||
"POST only for /assistent/chat-eval, "
|
||||
"/assistent/analyze-reply, /assistent/client-event, "
|
||||
"/assistent/session, /assistent/session/{id}/chat"
|
||||
),
|
||||
},
|
||||
@@ -769,6 +853,9 @@ def _make_handler(hub: DebugHub):
|
||||
"/assistent/session",
|
||||
"/assistent/session/{id}",
|
||||
"/assistent/session/{id}/chat",
|
||||
"/assistent/analyze-reply",
|
||||
"/assistent/client-event",
|
||||
"/assistent/client-events",
|
||||
"/assistent/chat-eval",
|
||||
]
|
||||
|
||||
@@ -924,6 +1011,70 @@ def _make_handler(hub: DebugHub):
|
||||
skills=skills,
|
||||
persona=body.get("persona"),
|
||||
)
|
||||
elif sub == "client-events":
|
||||
if method != "GET":
|
||||
self._json(405, {"ok": False, "error": "GET only"})
|
||||
return
|
||||
since = 0
|
||||
try:
|
||||
since = int((qs.get("since") or ["0"])[0])
|
||||
except (TypeError, ValueError):
|
||||
since = 0
|
||||
payload = {
|
||||
"ok": True,
|
||||
"events": hub.client_events_since(since),
|
||||
}
|
||||
elif sub == "client-event":
|
||||
if method != "POST":
|
||||
self._json(405, {"ok": False, "error": "POST only"})
|
||||
return
|
||||
try:
|
||||
body = self._read_json_body()
|
||||
except ValueError as exc:
|
||||
self._json(400, {"ok": False, "error": str(exc)})
|
||||
return
|
||||
if not isinstance(body, dict):
|
||||
self._json(400, {"ok": False, "error": "JSON object required"})
|
||||
return
|
||||
payload = hub.push_client_event(body)
|
||||
elif sub == "analyze-reply":
|
||||
body = {}
|
||||
if method == "POST":
|
||||
try:
|
||||
body = self._read_json_body()
|
||||
except ValueError as exc:
|
||||
self._json(400, {"ok": False, "error": str(exc)})
|
||||
return
|
||||
elif method != "GET":
|
||||
self._json(405, {"ok": False, "error": "GET or POST"})
|
||||
return
|
||||
|
||||
def _aq(name: str) -> Any:
|
||||
if name in body and body[name] is not None:
|
||||
return body[name]
|
||||
vals = qs.get(name)
|
||||
return vals[0] if vals else None
|
||||
|
||||
reply_text = _aq("reply")
|
||||
if not (reply_text or "").strip():
|
||||
self._json(
|
||||
400,
|
||||
{
|
||||
"ok": False,
|
||||
"error": "reply required",
|
||||
"usage": (
|
||||
"POST {message, reply, persona?, pack?} "
|
||||
"— offline extract + client.would_generate"
|
||||
),
|
||||
},
|
||||
)
|
||||
return
|
||||
payload = debug_assistent.analyze_assistent_reply(
|
||||
message=_aq("message"),
|
||||
reply=str(reply_text),
|
||||
persona=_aq("persona"),
|
||||
pack=_aq("pack"),
|
||||
)
|
||||
elif sub == "chat-eval":
|
||||
body = {}
|
||||
if method == "POST":
|
||||
@@ -1029,6 +1180,8 @@ def start_debug_server(
|
||||
f" {base}/assistent/diagnose",
|
||||
f" {base}/assistent/session (multi-turn; not in snapshot)",
|
||||
f" {base}/assistent/chat-eval (one-shot; not in snapshot)",
|
||||
f" {base}/assistent/analyze-reply (offline; no VRAM)",
|
||||
f" {base}/assistent/client-events (UI beacons)",
|
||||
]
|
||||
if log:
|
||||
for line in lines:
|
||||
|
||||
Reference in New Issue
Block a user