diff --git a/docs/cli.md b/docs/cli.md
index a174b36..bd5b613 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -163,6 +163,9 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
| `/assistent/session/{id}/chat` | **POST** — ход + объект `trace` |
| `/assistent/session/{id}` | **GET** / **DELETE** — состояние / сброс |
| `/assistent/chat-eval` | One-shot: session + один chat + delete |
+| `/assistent/analyze-reply` | **POST** `{message, reply}` — офлайн extract + `client.would_generate` (без VRAM) |
+| `/assistent/client-event` | **POST** — beacon из вкладки Assistent (последний ход UI) |
+| `/assistent/client-events` | **GET** `?since=` — кольцо beacon'ов |
Симптомы → куда смотреть: поле `playbook` в `/assistent`.
@@ -199,7 +202,13 @@ curl -sS -X DELETE "$BASE/assistent/session/$SID"
curl -sS -X POST "$BASE/assistent/chat-eval" \
-H 'Content-Type: application/json' \
-d '{"message":"какие steps/cfg для turbo?","persona":"neutral","timeout":120}' \
- | jq '{ok,reply,patch,trace}'
+ | jq '{ok,reply,patch,client,trace}'
+
+# Offline: paste a live UI reply (no GPU)
+curl -sS -X POST "$BASE/assistent/analyze-reply" \
+ -H 'Content-Type: application/json' \
+ -d '{"persona":"neutral","message":"а ты знаешь какие то промпты с civitai","reply":"```json\n{\"prompt\":\"x\",\"generate\":true}\n```"}' \
+ | jq '{patch,client}'
```
Ответ chat: `ok`, `reply`, `trace` (`timings_ms`, `patch`, `exact_merge`, `compact_context`, `system_chars` / `system_layers`, `skills`, `hops`, `gaps`, `errors`/`warnings`/`hints`). Sqlite SaveChat fail — soft error, если текст ответа есть.
diff --git a/src/gpu_rent/cli.py b/src/gpu_rent/cli.py
index 80dd818..7900f0b 100644
--- a/src/gpu_rent/cli.py
+++ b/src/gpu_rent/cli.py
@@ -976,13 +976,17 @@ def pull_output_cmd() -> None:
def seed_extensions_cmd() -> None:
"""Clone/fetch extensions.yaml, затем restart swarmui."""
try:
- from gpu_rent.provision import verify_assistent_sqlite_bins
+ from gpu_rent.provision import (
+ repair_assistent_sqlite_bins,
+ verify_assistent_sqlite_bins,
+ )
cfg, host = _live()
seed_extensions(cfg, host, log)
ensure_swarmui_running(cfg, host, log, restart=True)
# Build lands Sqlite private deps beside the extension DLL (≥0.13.1).
+ repair_assistent_sqlite_bins(cfg, host, log)
verify_assistent_sqlite_bins(cfg, host, log)
except GpuRentError as exc:
_die(exc)
diff --git a/src/gpu_rent/debug_api.py b/src/gpu_rent/debug_api.py
index c980fc4..92fd6f9 100644
--- a/src/gpu_rent/debug_api.py
+++ b/src/gpu_rent/debug_api.py
@@ -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 /snapshot. "
f"Opt-in Assistent: /assistent/session (multi-turn) · "
f"/assistent/chat-eval (one-shot) — not in snapshot; "
- f"may load VRAM.
/assistent/analyze-reply."
f"