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:
+10
-1
@@ -163,6 +163,9 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
|
|||||||
| `/assistent/session/{id}/chat` | **POST** — ход + объект `trace` |
|
| `/assistent/session/{id}/chat` | **POST** — ход + объект `trace` |
|
||||||
| `/assistent/session/{id}` | **GET** / **DELETE** — состояние / сброс |
|
| `/assistent/session/{id}` | **GET** / **DELETE** — состояние / сброс |
|
||||||
| `/assistent/chat-eval` | One-shot: session + один chat + 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`.
|
Симптомы → куда смотреть: поле `playbook` в `/assistent`.
|
||||||
|
|
||||||
@@ -199,7 +202,13 @@ curl -sS -X DELETE "$BASE/assistent/session/$SID"
|
|||||||
curl -sS -X POST "$BASE/assistent/chat-eval" \
|
curl -sS -X POST "$BASE/assistent/chat-eval" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"message":"какие steps/cfg для turbo?","persona":"neutral","timeout":120}' \
|
-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, если текст ответа есть.
|
Ответ 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, если текст ответа есть.
|
||||||
|
|||||||
+5
-1
@@ -976,13 +976,17 @@ def pull_output_cmd() -> None:
|
|||||||
def seed_extensions_cmd() -> None:
|
def seed_extensions_cmd() -> None:
|
||||||
"""Clone/fetch extensions.yaml, затем restart swarmui."""
|
"""Clone/fetch extensions.yaml, затем restart swarmui."""
|
||||||
try:
|
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()
|
cfg, host = _live()
|
||||||
|
|
||||||
seed_extensions(cfg, host, log)
|
seed_extensions(cfg, host, log)
|
||||||
ensure_swarmui_running(cfg, host, log, restart=True)
|
ensure_swarmui_running(cfg, host, log, restart=True)
|
||||||
# Build lands Sqlite private deps beside the extension DLL (≥0.13.1).
|
# 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)
|
verify_assistent_sqlite_bins(cfg, host, log)
|
||||||
except GpuRentError as exc:
|
except GpuRentError as exc:
|
||||||
_die(exc)
|
_die(exc)
|
||||||
|
|||||||
+155
-2
@@ -74,13 +74,15 @@ _OPENAPI: dict[str, Any] = {
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "gpu-rent Debug API",
|
"title": "gpu-rent Debug API",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": (
|
"description": (
|
||||||
"Localhost diagnostics for installer progress and "
|
"Localhost diagnostics for installer progress and "
|
||||||
"SwarmUI / Comfy / Ollama / Assistent. Mostly read-only. "
|
"SwarmUI / Comfy / Ollama / Assistent. Mostly read-only. "
|
||||||
"Assistent multi-turn: POST /assistent/session + "
|
"Assistent multi-turn: POST /assistent/session + "
|
||||||
"/assistent/session/{id}/chat (trace object). "
|
"/assistent/session/{id}/chat (trace object). "
|
||||||
"One-shot: POST/GET /assistent/chat-eval. "
|
"One-shot: POST/GET /assistent/chat-eval. "
|
||||||
|
"Offline reply score: POST /assistent/analyze-reply "
|
||||||
|
"(no VRAM). "
|
||||||
+ _WARN_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": {
|
"/assistent/chat-eval": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": (
|
"summary": (
|
||||||
@@ -339,6 +389,10 @@ class DebugHub:
|
|||||||
step: str = "starting"
|
step: str = "starting"
|
||||||
step_started_at: float = field(default_factory=time.time)
|
step_started_at: float = field(default_factory=time.time)
|
||||||
_events: deque[dict[str, Any]] = field(default_factory=lambda: deque(maxlen=EVENT_CAPACITY))
|
_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
|
_seq: int = 0
|
||||||
_lock: threading.Lock = field(default_factory=threading.Lock)
|
_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
_cache: dict[str, tuple[float, Any]] = field(default_factory=dict)
|
_cache: dict[str, tuple[float, Any]] = field(default_factory=dict)
|
||||||
@@ -403,6 +457,21 @@ class DebugHub:
|
|||||||
items = list(self._events)
|
items = list(self._events)
|
||||||
return items[-n:]
|
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]:
|
def progress(self) -> dict[str, Any]:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
last = self._events[-1] if self._events else None
|
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-Type", content_type)
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.send_header("Cache-Control", "no-store")
|
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.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
@@ -521,7 +593,8 @@ def _make_handler(hub: DebugHub):
|
|||||||
f"then <a href='/snapshot'>/snapshot</a>. "
|
f"then <a href='/snapshot'>/snapshot</a>. "
|
||||||
f"Opt-in Assistent: <code>/assistent/session</code> (multi-turn) · "
|
f"Opt-in Assistent: <code>/assistent/session</code> (multi-turn) · "
|
||||||
f"<code>/assistent/chat-eval</code> (one-shot) — not in snapshot; "
|
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>"
|
f"<ul>{rows}</ul></body></html>"
|
||||||
)
|
)
|
||||||
self._send(200, html.encode("utf-8"), "text/html; charset=utf-8")
|
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")
|
raise ValueError("JSON body must be an object")
|
||||||
return data
|
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
|
def do_GET(self) -> None: # noqa: N802
|
||||||
try:
|
try:
|
||||||
self._dispatch("GET")
|
self._dispatch("GET")
|
||||||
@@ -603,6 +684,8 @@ def _make_handler(hub: DebugHub):
|
|||||||
return
|
return
|
||||||
if method == "POST" and path not in {
|
if method == "POST" and path not in {
|
||||||
"/assistent/chat-eval",
|
"/assistent/chat-eval",
|
||||||
|
"/assistent/analyze-reply",
|
||||||
|
"/assistent/client-event",
|
||||||
"/assistent/session",
|
"/assistent/session",
|
||||||
} and not _is_assistent_session_chat(path):
|
} and not _is_assistent_session_chat(path):
|
||||||
self._json(
|
self._json(
|
||||||
@@ -611,6 +694,7 @@ def _make_handler(hub: DebugHub):
|
|||||||
"ok": False,
|
"ok": False,
|
||||||
"error": (
|
"error": (
|
||||||
"POST only for /assistent/chat-eval, "
|
"POST only for /assistent/chat-eval, "
|
||||||
|
"/assistent/analyze-reply, /assistent/client-event, "
|
||||||
"/assistent/session, /assistent/session/{id}/chat"
|
"/assistent/session, /assistent/session/{id}/chat"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -769,6 +853,9 @@ def _make_handler(hub: DebugHub):
|
|||||||
"/assistent/session",
|
"/assistent/session",
|
||||||
"/assistent/session/{id}",
|
"/assistent/session/{id}",
|
||||||
"/assistent/session/{id}/chat",
|
"/assistent/session/{id}/chat",
|
||||||
|
"/assistent/analyze-reply",
|
||||||
|
"/assistent/client-event",
|
||||||
|
"/assistent/client-events",
|
||||||
"/assistent/chat-eval",
|
"/assistent/chat-eval",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -924,6 +1011,70 @@ def _make_handler(hub: DebugHub):
|
|||||||
skills=skills,
|
skills=skills,
|
||||||
persona=body.get("persona"),
|
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":
|
elif sub == "chat-eval":
|
||||||
body = {}
|
body = {}
|
||||||
if method == "POST":
|
if method == "POST":
|
||||||
@@ -1029,6 +1180,8 @@ def start_debug_server(
|
|||||||
f" {base}/assistent/diagnose",
|
f" {base}/assistent/diagnose",
|
||||||
f" {base}/assistent/session (multi-turn; not in snapshot)",
|
f" {base}/assistent/session (multi-turn; not in snapshot)",
|
||||||
f" {base}/assistent/chat-eval (one-shot; 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:
|
if log:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
|||||||
+146
-12
@@ -65,25 +65,28 @@ DATA = Path("/mnt/swarm_data")
|
|||||||
OPT = Path("/opt/swarmui/src/Extensions")
|
OPT = Path("/opt/swarmui/src/Extensions")
|
||||||
roots = [DATA / "Extensions", OPT]
|
roots = [DATA / "Extensions", OPT]
|
||||||
found = []
|
found = []
|
||||||
|
dll_seen = set()
|
||||||
|
for search in (DATA, Path("/opt/swarmui")):
|
||||||
|
if not search.is_dir():
|
||||||
|
continue
|
||||||
|
for dll in search.rglob("SwarmAssistentExtension.dll"):
|
||||||
|
if any(p in {"obj", "node_modules"} for p in dll.parts):
|
||||||
|
continue
|
||||||
|
dll_seen.add(dll)
|
||||||
for root in roots:
|
for root in roots:
|
||||||
if not root.is_dir():
|
if not root.is_dir():
|
||||||
continue
|
continue
|
||||||
for p in sorted(root.iterdir()):
|
for p in sorted(root.iterdir()):
|
||||||
if "assistent" not in p.name.lower():
|
if "assistent" not in p.name.lower():
|
||||||
continue
|
continue
|
||||||
# Prefer TFM output dirs (Debug/Release net*), then any bin/** fallback
|
dlls = [d for d in dll_seen if p in d.parents or d.parent == p]
|
||||||
dlls = []
|
# Prefer TFM output dirs (Debug/Release net*), then any match
|
||||||
|
ranked = []
|
||||||
for cfg_name in ("Debug", "Release"):
|
for cfg_name in ("Debug", "Release"):
|
||||||
dlls.extend(sorted(p.glob(f"bin/{cfg_name}/net*/SwarmAssistentExtension.dll")))
|
ranked.extend(sorted(p.glob(f"bin/{cfg_name}/net*/SwarmAssistentExtension.dll")))
|
||||||
if not dlls:
|
if not ranked:
|
||||||
dlls = sorted(p.glob("bin/**/SwarmAssistentExtension.dll"))
|
ranked = sorted(dlls) or sorted(p.glob("bin/**/SwarmAssistentExtension.dll"))
|
||||||
if not dlls:
|
dll = ranked[0] if ranked else None
|
||||||
# Older layout / alternate assembly name
|
|
||||||
for cfg_name in ("Debug", "Release"):
|
|
||||||
dlls.extend(sorted(p.glob(f"bin/{cfg_name}/net*/*Assistent*.dll")))
|
|
||||||
if not dlls:
|
|
||||||
dlls = sorted(p.glob("bin/**/*Assistent*.dll"))
|
|
||||||
dll = dlls[0] if dlls else None
|
|
||||||
dll_dir = dll.parent if dll else None
|
dll_dir = dll.parent if dll else None
|
||||||
sqlite_dll = (dll_dir / "Microsoft.Data.Sqlite.dll") if dll_dir else None
|
sqlite_dll = (dll_dir / "Microsoft.Data.Sqlite.dll") if dll_dir else None
|
||||||
csproj = next(p.glob("*.csproj"), None)
|
csproj = next(p.glob("*.csproj"), None)
|
||||||
@@ -356,6 +359,137 @@ def _generate_flag_on(obj: dict[str, Any] | None) -> bool:
|
|||||||
return isinstance(acts, list) and "generate" in [str(a) for a in acts]
|
return isinstance(acts, list) and "generate" in [str(a) for a in acts]
|
||||||
|
|
||||||
|
|
||||||
|
# Mirrors swarm-assistent src/intent.js (0.15.2). Keep in lockstep.
|
||||||
|
_CYR_BOUND = r"(^|[^0-9A-Za-z_А-Яа-яЁё])"
|
||||||
|
_CYR_END = r"(?=$|[^0-9A-Za-z_А-Яа-яЁё])"
|
||||||
|
_PERSONA_CHIP = {
|
||||||
|
"neutral": "Нормальный",
|
||||||
|
"aggressive": "Агрессивный",
|
||||||
|
"dreamer": "Мечтатель",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cyr_token_re(alts: str) -> re.Pattern[str]:
|
||||||
|
return re.compile(f"{_CYR_BOUND}(?:{alts}){_CYR_END}", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def user_asks_no_generate(text: str | None) -> bool:
|
||||||
|
t = (text or "").strip()
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
if re.search(
|
||||||
|
r"\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|"
|
||||||
|
r"don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b",
|
||||||
|
t,
|
||||||
|
re.I,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return bool(
|
||||||
|
_cyr_token_re(
|
||||||
|
r"запомн|запомни|запомним|сохрани|сохраним|шаблон|"
|
||||||
|
r"базов(ый|ого|ому|ым|ая|ую|ое)?\s+промпт|"
|
||||||
|
r"не\s+генерир[а-яё]*|без\s+генерац[а-яё]*|не\s+надо\s+генер[а-яё]*|"
|
||||||
|
r"только\s+запомн[а-яё]*|пока\s+запомн[а-яё]*|"
|
||||||
|
r"не\s+рисуй|не\s+запускай\s+генер[а-яё]*|"
|
||||||
|
r"только\s+(ответь|скажи|объясни)"
|
||||||
|
).search(t)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_asks_generate(text: str | None) -> bool:
|
||||||
|
t = (text or "").strip()
|
||||||
|
if not t or user_asks_no_generate(t):
|
||||||
|
return False
|
||||||
|
if re.search(
|
||||||
|
r"\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b",
|
||||||
|
t,
|
||||||
|
re.I,
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return bool(
|
||||||
|
_cyr_token_re(
|
||||||
|
r"сгенер[а-яё]*|нарисуй|нарисуйте|нарисуем|"
|
||||||
|
r"запусти\s+генер[а-яё]*|"
|
||||||
|
r"сдела(й|ем|йте)\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)"
|
||||||
|
).search(t)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def predict_client_turn(
|
||||||
|
message: str | None,
|
||||||
|
patch: dict[str, Any] | None,
|
||||||
|
*,
|
||||||
|
persona: str | None = None,
|
||||||
|
pack: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""What swarm-assistent 0.15.3 JS would do after this HTTP reply.
|
||||||
|
|
||||||
|
Generate is user-owned: model generate:true is advisory. AssistentChat
|
||||||
|
never starts Swarm Generate.
|
||||||
|
"""
|
||||||
|
model_generate = _generate_flag_on(patch)
|
||||||
|
vetoed = user_asks_no_generate(message)
|
||||||
|
user_gen = user_asks_generate(message)
|
||||||
|
has_prompt = bool(str((patch or {}).get("prompt") or "").strip())
|
||||||
|
user_asked = user_gen and (model_generate or has_prompt)
|
||||||
|
would_generate = bool(not vetoed and user_asked)
|
||||||
|
if vetoed:
|
||||||
|
reason = "vetoed"
|
||||||
|
elif user_asked:
|
||||||
|
reason = "user_phrase"
|
||||||
|
elif model_generate:
|
||||||
|
reason = "model_flag_ignored"
|
||||||
|
else:
|
||||||
|
reason = "none"
|
||||||
|
pid = (persona or "").strip() or None
|
||||||
|
toast = None
|
||||||
|
if would_generate:
|
||||||
|
toast = "Промпт обновлён · Generate" if has_prompt else "Запускаю Generate"
|
||||||
|
elif patch and has_prompt:
|
||||||
|
toast = None
|
||||||
|
return {
|
||||||
|
"user_asks_generate": user_gen,
|
||||||
|
"user_asks_no_generate": vetoed,
|
||||||
|
"model_generate": model_generate,
|
||||||
|
"would_apply_patch": bool(would_generate and patch),
|
||||||
|
"would_generate": would_generate,
|
||||||
|
"generate_reason": reason,
|
||||||
|
"http_starts_generate": False,
|
||||||
|
"toast": toast,
|
||||||
|
"persona": pid,
|
||||||
|
"persona_chip": _PERSONA_CHIP.get(pid or "", pid) if pid else None,
|
||||||
|
"pack": (pack or "").strip() or None,
|
||||||
|
"note": (
|
||||||
|
"0.15.3: HTTP AssistentChat never runs Generate. "
|
||||||
|
"would_generate mirrors resolveTurnIntent — user «нарисуй»/"
|
||||||
|
"«сгенерируй»/«сделаем изображение», not model generate:true alone."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_assistent_reply(
|
||||||
|
*,
|
||||||
|
message: str | None = None,
|
||||||
|
reply: str | None = None,
|
||||||
|
persona: str | None = None,
|
||||||
|
pack: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Offline extract + 0.15.2 client prediction. No Swarm / VRAM."""
|
||||||
|
extracted = extract_assistent_patch(reply)
|
||||||
|
patch = extracted.get("patch")
|
||||||
|
client = predict_client_turn(message, patch, persona=persona, pack=pack)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"offline": True,
|
||||||
|
"message": message or "",
|
||||||
|
"reply": reply or "",
|
||||||
|
"reply_prose": extracted.get("prose") or "",
|
||||||
|
"patch": _summarize_patch(patch),
|
||||||
|
"client": client,
|
||||||
|
"note": "No AssistentChat — paste a live UI reply to see what 0.15.2 would apply/Generate.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_extracted_patch(obj: dict[str, Any]) -> dict[str, Any]:
|
def _normalize_extracted_patch(obj: dict[str, Any]) -> dict[str, Any]:
|
||||||
patch = dict(obj)
|
patch = dict(obj)
|
||||||
if _generate_flag_on(patch):
|
if _generate_flag_on(patch):
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from gpu_rent.debug_assistent import (
|
|||||||
collect_assistent_deep,
|
collect_assistent_deep,
|
||||||
extract_assistent_patch,
|
extract_assistent_patch,
|
||||||
_generate_flag_on,
|
_generate_flag_on,
|
||||||
|
predict_client_turn,
|
||||||
)
|
)
|
||||||
|
|
||||||
SESSION_TTL_SEC = 45 * 60
|
SESSION_TTL_SEC = 45 * 60
|
||||||
@@ -56,6 +57,10 @@ _TRACE_GAPS = [
|
|||||||
"Park/Warm not run on chat turns (AssistentParkLlm/WarmLlm are Generate-path)",
|
"Park/Warm not run on chat turns (AssistentParkLlm/WarmLlm are Generate-path)",
|
||||||
"Sqlite SaveChat is client-side; AssistentChat may still return reply when "
|
"Sqlite SaveChat is client-side; AssistentChat may still return reply when "
|
||||||
"memory DLL is missing — soft error in trace.errors",
|
"memory DLL is missing — soft error in trace.errors",
|
||||||
|
"HTTP AssistentChat never starts Swarm Generate; trace.client.would_generate "
|
||||||
|
"is a 0.15.3 JS simulation (user draw phrase, not model generate:true). "
|
||||||
|
"Live UI last turn: GET /assistent/client-events (JS beacon) or "
|
||||||
|
"POST /assistent/analyze-reply. ListChats/GetUiState need Sqlite DLL.",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -417,6 +422,7 @@ def build_chat_trace(
|
|||||||
errors: list[str],
|
errors: list[str],
|
||||||
warnings: list[str],
|
warnings: list[str],
|
||||||
hints: list[str],
|
hints: list[str],
|
||||||
|
message: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
extracted = extract_assistent_patch(reply)
|
extracted = extract_assistent_patch(reply)
|
||||||
patch = extracted.get("patch")
|
patch = extracted.get("patch")
|
||||||
@@ -465,7 +471,13 @@ def build_chat_trace(
|
|||||||
elif raw is not None:
|
elif raw is not None:
|
||||||
raw_preview = str(raw)[:400]
|
raw_preview = str(raw)[:400]
|
||||||
|
|
||||||
|
client = predict_client_turn(message, patch, persona=persona, pack=pack)
|
||||||
out_hints = list(hints) + list(exact_merge.get("hints") or [])
|
out_hints = list(hints) + list(exact_merge.get("hints") or [])
|
||||||
|
if client.get("model_generate") and not client.get("user_asks_generate"):
|
||||||
|
out_hints.append(
|
||||||
|
"model generate:true without user draw phrase — 0.15.3 will NOT "
|
||||||
|
"auto-Generate; UI shows JSON card + Сгенерировать"
|
||||||
|
)
|
||||||
out: dict[str, Any] = {
|
out: dict[str, Any] = {
|
||||||
"ok": ok,
|
"ok": ok,
|
||||||
"timings_ms": timings_ms,
|
"timings_ms": timings_ms,
|
||||||
@@ -498,6 +510,7 @@ def build_chat_trace(
|
|||||||
"reply": reply,
|
"reply": reply,
|
||||||
"reply_prose": extracted.get("prose"),
|
"reply_prose": extracted.get("prose"),
|
||||||
"patch": _summarize_patch(patch),
|
"patch": _summarize_patch(patch),
|
||||||
|
"client": client,
|
||||||
"exact_merge": exact_merge,
|
"exact_merge": exact_merge,
|
||||||
"park_warm": {
|
"park_warm": {
|
||||||
"invoked": False,
|
"invoked": False,
|
||||||
@@ -876,6 +889,7 @@ def chat_debug_session(
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
warnings=warnings,
|
warnings=warnings,
|
||||||
hints=hints,
|
hints=hints,
|
||||||
|
message=text,
|
||||||
)
|
)
|
||||||
session.last_trace = trace
|
session.last_trace = trace
|
||||||
session.touch()
|
session.touch()
|
||||||
@@ -1114,6 +1128,7 @@ def run_assistent_chat_eval_via_session(
|
|||||||
"reply": result.get("reply"),
|
"reply": result.get("reply"),
|
||||||
"reply_prose": trace.get("reply_prose"),
|
"reply_prose": trace.get("reply_prose"),
|
||||||
"patch": trace.get("patch"),
|
"patch": trace.get("patch"),
|
||||||
|
"client": trace.get("client"),
|
||||||
"raw": trace.get("raw"),
|
"raw": trace.get("raw"),
|
||||||
"system_chars": trace.get("system_chars"),
|
"system_chars": trace.get("system_chars"),
|
||||||
"system_layers": trace.get("system_layers"),
|
"system_layers": trace.get("system_layers"),
|
||||||
|
|||||||
+105
-17
@@ -229,34 +229,87 @@ print(__import__("json").dumps(found))
|
|||||||
_ASSISTENT_SQLITE_BIN_PY = r"""
|
_ASSISTENT_SQLITE_BIN_PY = r"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
roots = [Path("/mnt/swarm_data/Extensions"), Path("/opt/swarmui/src/Extensions")]
|
roots = [Path("/mnt/swarm_data"), Path("/opt/swarmui")]
|
||||||
|
seen = set()
|
||||||
found = []
|
found = []
|
||||||
for root in roots:
|
for root in roots:
|
||||||
if not root.is_dir():
|
if not root.is_dir():
|
||||||
continue
|
continue
|
||||||
for p in sorted(root.iterdir()):
|
for dll in root.rglob("SwarmAssistentExtension.dll"):
|
||||||
if "assistent" not in p.name.lower():
|
if any(p in {"obj", "node_modules"} for p in dll.parts):
|
||||||
continue
|
continue
|
||||||
dlls = []
|
key = str(dll)
|
||||||
for cfg_name in ("Debug", "Release"):
|
if key in seen:
|
||||||
dlls.extend(sorted(p.glob(f"bin/{cfg_name}/net*/SwarmAssistentExtension.dll")))
|
continue
|
||||||
if not dlls:
|
seen.add(key)
|
||||||
dlls = sorted(p.glob("bin/**/SwarmAssistentExtension.dll"))
|
dll_dir = dll.parent
|
||||||
dll = dlls[0] if dlls else None
|
sqlite = dll_dir / "Microsoft.Data.Sqlite.dll"
|
||||||
dll_dir = dll.parent if dll else None
|
pcl = list(dll_dir.glob("SQLitePCLRaw*.dll"))
|
||||||
sqlite = (dll_dir / "Microsoft.Data.Sqlite.dll") if dll_dir else None
|
|
||||||
pcl = list(dll_dir.glob("SQLitePCLRaw*.dll")) if dll_dir else []
|
|
||||||
found.append({
|
found.append({
|
||||||
"path": str(p),
|
"path": str(dll.parent.parent) if dll.parent.name.startswith("net") else str(dll_dir),
|
||||||
"name": p.name,
|
"name": "swarm-assistent",
|
||||||
"dll": str(dll) if dll else None,
|
"dll": str(dll),
|
||||||
"dll_dir": str(dll_dir) if dll_dir else None,
|
"dll_dir": str(dll_dir),
|
||||||
"sqlite_dll": bool(sqlite and sqlite.is_file()),
|
"sqlite_dll": sqlite.is_file(),
|
||||||
"sqlitepcl": bool(pcl),
|
"sqlitepcl": bool(pcl),
|
||||||
})
|
})
|
||||||
print(json.dumps(found))
|
print(json.dumps(found))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ASSISTENT_SQLITE_REPAIR_PY = r"""
|
||||||
|
from pathlib import Path
|
||||||
|
import json, shutil
|
||||||
|
skip = {"obj", "node_modules"}
|
||||||
|
dlls = []
|
||||||
|
for root in (Path("/mnt/swarm_data"), Path("/opt/swarmui")):
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for dll in root.rglob("SwarmAssistentExtension.dll"):
|
||||||
|
if any(p in skip for p in dll.parts):
|
||||||
|
continue
|
||||||
|
dlls.append(dll)
|
||||||
|
|
||||||
|
def has_sqlite(d):
|
||||||
|
return (d / "Microsoft.Data.Sqlite.dll").is_file() and list(d.glob("SQLitePCLRaw*.dll"))
|
||||||
|
|
||||||
|
donors = [d.parent for d in dlls if has_sqlite(d.parent)]
|
||||||
|
if not donors:
|
||||||
|
nuget = Path.home() / ".nuget" / "packages"
|
||||||
|
extra = []
|
||||||
|
if nuget.is_dir():
|
||||||
|
extra.extend(nuget.rglob("Microsoft.Data.Sqlite.dll"))
|
||||||
|
extra.extend(nuget.rglob("SQLitePCLRaw.core.dll"))
|
||||||
|
# last resort: any copy on the disk under swarm
|
||||||
|
for root in (Path("/opt/swarmui"), Path("/root/.nuget")):
|
||||||
|
if root.is_dir():
|
||||||
|
extra.extend(root.rglob("Microsoft.Data.Sqlite.dll"))
|
||||||
|
for f in extra:
|
||||||
|
if f.parent not in donors:
|
||||||
|
donors.append(f.parent)
|
||||||
|
|
||||||
|
copied = []
|
||||||
|
missing = []
|
||||||
|
for dll in dlls:
|
||||||
|
dest = dll.parent
|
||||||
|
if has_sqlite(dest):
|
||||||
|
continue
|
||||||
|
src = next((d for d in donors if d != dest and has_sqlite(d)), None)
|
||||||
|
if src is None:
|
||||||
|
src = next((d for d in donors if (d / "Microsoft.Data.Sqlite.dll").is_file()), None)
|
||||||
|
if src is None:
|
||||||
|
missing.append(str(dest))
|
||||||
|
continue
|
||||||
|
names = ["Microsoft.Data.Sqlite.dll"]
|
||||||
|
names += [p.name for p in src.glob("SQLitePCLRaw*.dll")]
|
||||||
|
names += [p.name for p in src.glob("e_sqlite3.*")]
|
||||||
|
for name in names:
|
||||||
|
fp = src / name
|
||||||
|
if fp.is_file():
|
||||||
|
shutil.copy2(fp, dest / name)
|
||||||
|
copied.append({"dll_dir": str(dest), "from": str(src)})
|
||||||
|
print(json.dumps({"copied": copied, "missing": missing, "dlls": [str(d) for d in dlls]}))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _log_assistent_source_sqlite_hint(cfg: Config, host: str, log: Log) -> None:
|
def _log_assistent_source_sqlite_hint(cfg: Config, host: str, log: Log) -> None:
|
||||||
"""After clone: warn if Assistent csproj lacks Sqlite private-dep wiring (≥0.13.1)."""
|
"""After clone: warn if Assistent csproj lacks Sqlite private-dep wiring (≥0.13.1)."""
|
||||||
@@ -298,6 +351,40 @@ def _log_assistent_source_sqlite_hint(cfg: Config, host: str, log: Log) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def repair_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool:
|
||||||
|
"""Copy Microsoft.Data.Sqlite + SQLitePCLRaw next to every loaded Assistent DLL."""
|
||||||
|
try:
|
||||||
|
raw = run_ssh(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
"python3 - <<'PY'\n" + _ASSISTENT_SQLITE_REPAIR_PY + "\nPY",
|
||||||
|
check=False,
|
||||||
|
timeout=40,
|
||||||
|
).strip()
|
||||||
|
if not raw:
|
||||||
|
return False
|
||||||
|
data = json.loads(raw.splitlines()[-1])
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"⚠ Assistent Sqlite repair: {exc}")
|
||||||
|
return False
|
||||||
|
copied = data.get("copied") or []
|
||||||
|
missing = data.get("missing") or []
|
||||||
|
dlls = data.get("dlls") or []
|
||||||
|
if not dlls:
|
||||||
|
log("⚠ Assistent Sqlite repair: SwarmAssistentExtension.dll не найден — compile/restart")
|
||||||
|
return False
|
||||||
|
for row in copied:
|
||||||
|
log(f"Assistent Sqlite: скопировал рядом с {row.get('dll_dir')}")
|
||||||
|
for path in missing:
|
||||||
|
log(f"⚠ Assistent Sqlite: нет donor DLL для {path}")
|
||||||
|
if copied:
|
||||||
|
return True
|
||||||
|
if not missing:
|
||||||
|
log("Assistent Sqlite: private deps уже рядом с DLL")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def verify_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool:
|
def verify_assistent_sqlite_bins(cfg: Config, host: str, log: Log) -> bool:
|
||||||
"""After SwarmUI build/restart: Microsoft.Data.Sqlite (+ SQLitePCLRaw) beside extension DLL."""
|
"""After SwarmUI build/restart: Microsoft.Data.Sqlite (+ SQLitePCLRaw) beside extension DLL."""
|
||||||
try:
|
try:
|
||||||
@@ -1630,6 +1717,7 @@ def provision_vm(
|
|||||||
if swarm:
|
if swarm:
|
||||||
ensure_swarmui_running(cfg, host, log, restart=restart)
|
ensure_swarmui_running(cfg, host, log, restart=restart)
|
||||||
try:
|
try:
|
||||||
|
repair_assistent_sqlite_bins(cfg, host, log)
|
||||||
verify_assistent_sqlite_bins(cfg, host, log)
|
verify_assistent_sqlite_bins(cfg, host, log)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Assistent Sqlite check: {exc}")
|
log(f"Assistent Sqlite check: {exc}")
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from gpu_rent.debug_assistent import extract_assistent_patch
|
from gpu_rent.debug_assistent import (
|
||||||
|
analyze_assistent_reply,
|
||||||
|
extract_assistent_patch,
|
||||||
|
predict_client_turn,
|
||||||
|
user_asks_generate,
|
||||||
|
)
|
||||||
from gpu_rent import debug_assistent_session as das
|
from gpu_rent import debug_assistent_session as das
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +38,51 @@ def test_extract_assistent_patch_ignores_non_patch_json():
|
|||||||
assert out["patch"] is None
|
assert out["patch"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_client_turn_civitai_question_no_patch():
|
||||||
|
pred = predict_client_turn("а ты знаешь какие то промпты с civitai", None)
|
||||||
|
assert pred["user_asks_generate"] is False
|
||||||
|
assert pred["would_generate"] is False
|
||||||
|
assert pred["http_starts_generate"] is False
|
||||||
|
assert pred["generate_reason"] == "none"
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_client_turn_model_flag_without_user_phrase():
|
||||||
|
pred = predict_client_turn(
|
||||||
|
"а ты знаешь какие то промпты с civitai",
|
||||||
|
{"prompt": "a redhead", "generate": True},
|
||||||
|
persona="neutral",
|
||||||
|
)
|
||||||
|
assert pred["user_asks_generate"] is False
|
||||||
|
assert pred["model_generate"] is True
|
||||||
|
assert pred["would_generate"] is False
|
||||||
|
assert pred["generate_reason"] == "model_flag_ignored"
|
||||||
|
assert pred["persona_chip"] == "Нормальный"
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_client_turn_narisuy_with_prompt():
|
||||||
|
pred = predict_client_turn("нарисуй лису", {"prompt": "a fox"})
|
||||||
|
assert pred["user_asks_generate"] is True
|
||||||
|
assert pred["would_generate"] is True
|
||||||
|
assert pred["generate_reason"] == "user_phrase"
|
||||||
|
assert user_asks_generate("нарисуй лису") is True
|
||||||
|
assert user_asks_generate("Давай сделаем изображение рыжей") is True
|
||||||
|
assert user_asks_generate("что ты умеешь") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_assistent_reply_offline():
|
||||||
|
out = analyze_assistent_reply(
|
||||||
|
message="сгенерируй",
|
||||||
|
reply='ok\n```json\n{"prompt":"a cat","generate":true}\n```\n',
|
||||||
|
persona="leonid",
|
||||||
|
pack="ordinary",
|
||||||
|
)
|
||||||
|
assert out["ok"] is True
|
||||||
|
assert out["offline"] is True
|
||||||
|
assert out["patch"]["generate"] is True
|
||||||
|
assert out["client"]["would_generate"] is True
|
||||||
|
assert out["client"]["generate_reason"] == "user_phrase"
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_exact_merge_fills_omitted_params():
|
def test_analyze_exact_merge_fills_omitted_params():
|
||||||
patch = {"actions": ["generate"], "prompt": "x"}
|
patch = {"actions": ["generate"], "prompt": "x"}
|
||||||
exact = {
|
exact = {
|
||||||
|
|||||||
@@ -145,7 +145,49 @@ def test_debug_server_routes(monkeypatch, tmp_path):
|
|||||||
assert "/assistent/extension" in data["paths"]
|
assert "/assistent/extension" in data["paths"]
|
||||||
assert "/assistent/api" in data["paths"]
|
assert "/assistent/api" in data["paths"]
|
||||||
assert "/assistent/chat-eval" in data["paths"]
|
assert "/assistent/chat-eval" in data["paths"]
|
||||||
|
assert "/assistent/analyze-reply" in data["paths"]
|
||||||
assert "post" in data["paths"]["/assistent/chat-eval"]
|
assert "post" in data["paths"]["/assistent/chat-eval"]
|
||||||
|
assert "post" in data["paths"]["/assistent/analyze-reply"]
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{port}/assistent/analyze-reply",
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"message": "а ты знаешь какие то промпты с civitai",
|
||||||
|
"reply": '```json\n{"prompt":"x","generate":true}\n```',
|
||||||
|
"persona": "neutral",
|
||||||
|
}
|
||||||
|
).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
scored = json.loads(resp.read().decode("utf-8"))
|
||||||
|
assert scored["ok"] is True
|
||||||
|
assert scored["client"]["would_generate"] is False
|
||||||
|
assert scored["client"]["user_asks_generate"] is False
|
||||||
|
assert scored["client"]["model_generate"] is True
|
||||||
|
assert scored["client"]["persona_chip"] == "Нормальный"
|
||||||
|
|
||||||
|
beacon = urllib.request.Request(
|
||||||
|
f"http://127.0.0.1:{port}/assistent/client-event",
|
||||||
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
"user": "а ты знаешь какие то промпты с civitai",
|
||||||
|
"generating": False,
|
||||||
|
"intent": {"generate": False, "modelAsked": True},
|
||||||
|
"swarm_prompt": "old prompt",
|
||||||
|
}
|
||||||
|
).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(beacon, timeout=5) as resp:
|
||||||
|
stored = json.loads(resp.read().decode("utf-8"))
|
||||||
|
assert stored["ok"] is True
|
||||||
|
code, data = get("/assistent/client-events")
|
||||||
|
assert data["ok"] is True
|
||||||
|
assert data["events"][-1]["generating"] is False
|
||||||
|
|
||||||
code, data = get("/assistent/chat-eval")
|
code, data = get("/assistent/chat-eval")
|
||||||
assert "ok" in data
|
assert "ok" in data
|
||||||
|
|||||||
Reference in New Issue
Block a user