Seed Assistent personas as overlay folders and tighten Ollama/Assistent glue.
gpu-rent now writes personas/<id>/ on the VM (not legacy personas.json), adds seed-personas/doctor checks, and shortens mid/high keep-alive now that Assistent parks the LLM before Generate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,11 @@ def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
||||
links.extend(
|
||||
[
|
||||
AccessLink("SwarmUI UI", base, "браузер"),
|
||||
AccessLink(
|
||||
"Assistent",
|
||||
base,
|
||||
"вкладка Assistent в SwarmUI",
|
||||
),
|
||||
AccessLink("SwarmUI API", f"{base}/API/", "HTTP JSON"),
|
||||
AccessLink("SwarmUI MCP", f"{base}/mcp", "Cursor mcp.json"),
|
||||
]
|
||||
|
||||
@@ -324,6 +324,16 @@ def status() -> None:
|
||||
table.add_row("idle-killer", f"{killer_line} · arm ok (сессия)")
|
||||
else:
|
||||
table.add_row("idle-killer", killer_line)
|
||||
try:
|
||||
from gpu_rent.provision import count_wanted_models_on_vm
|
||||
|
||||
wanted_n = count_wanted_models_on_vm(cfg, state.floating_ip)
|
||||
table.add_row(
|
||||
"Assistent wanted",
|
||||
f"{wanted_n} в очереди" if wanted_n else "пусто",
|
||||
)
|
||||
except Exception:
|
||||
table.add_row("Assistent wanted", "—")
|
||||
except GpuRentError as exc:
|
||||
table.add_row("диск used/free", f"SSH: {exc}")
|
||||
table.add_row("idle-killer", "нет SSH")
|
||||
@@ -932,6 +942,18 @@ def seed_extensions_cmd() -> None:
|
||||
_die(exc)
|
||||
|
||||
|
||||
@app.command("seed-personas")
|
||||
def seed_personas_cmd() -> None:
|
||||
"""Push assistent-personas.yaml → VM Assistent overlay (без полного up)."""
|
||||
try:
|
||||
from gpu_rent.provision import seed_assistent_personas
|
||||
|
||||
cfg, host = _live()
|
||||
seed_assistent_personas(cfg, host, log)
|
||||
except GpuRentError as exc:
|
||||
_die(exc)
|
||||
|
||||
|
||||
capture_app = typer.Typer(
|
||||
help=(
|
||||
"Снять с VM инвентарь → локальные манифесты (только ссылки, без весов). "
|
||||
|
||||
+63
-1
@@ -15,7 +15,7 @@ from gpu_rent.inventory import (
|
||||
pick_volume_type,
|
||||
rank_flavors,
|
||||
)
|
||||
from gpu_rent.manifests import parse_extensions, parse_models
|
||||
from gpu_rent.manifests import parse_extensions, parse_models, repo_dirname, repo_matches_runtime
|
||||
from gpu_rent.os_client import (
|
||||
compute_quotas,
|
||||
connect,
|
||||
@@ -243,10 +243,72 @@ def run_doctor() -> list[Check]:
|
||||
_civitai(cfg, checks)
|
||||
_huggingface(cfg, checks)
|
||||
_local_manifests(cfg, checks)
|
||||
_llm_assistent(cfg, checks)
|
||||
_local_folders(cfg, checks)
|
||||
return checks
|
||||
|
||||
|
||||
def _llm_assistent(cfg: Config, checks: list[Check]) -> None:
|
||||
"""When Ollama is on, warn if Assistent stack pieces are missing locally."""
|
||||
from gpu_rent.llm_runtime import normalize_runtime, parse_ollama_models
|
||||
from gpu_rent.paths import ollama_models_manifest_path
|
||||
|
||||
runtime = normalize_runtime(getattr(cfg, "llm_runtime", "none"))
|
||||
if runtime != "ollama":
|
||||
checks.append(Check("LLM / Assistent", True, False, f"LLM_RUNTIME={runtime}"))
|
||||
return
|
||||
|
||||
ollama_path = Path(
|
||||
getattr(cfg, "ollama_models_manifest", None) or ollama_models_manifest_path()
|
||||
)
|
||||
if not ollama_path.is_file():
|
||||
checks.append(
|
||||
Check(
|
||||
"ollama-models.yaml",
|
||||
False,
|
||||
True,
|
||||
f"нет {ollama_path} — Assistent будет пустой. gpu-rent setup / скопируй example",
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
entries = parse_ollama_models(ollama_path)
|
||||
chat = [e for e in entries if getattr(e, "use", "chat") != "memory"]
|
||||
checks.append(
|
||||
Check(
|
||||
"ollama-models.yaml",
|
||||
True,
|
||||
True,
|
||||
f"{len(entries)} тег(ов), chat={len(chat)}",
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
checks.append(Check("ollama-models.yaml", False, True, str(exc)))
|
||||
|
||||
try:
|
||||
repos = parse_extensions(cfg.extensions_manifest)
|
||||
except ConfigError as exc:
|
||||
checks.append(Check("Assistent ext", False, True, str(exc)))
|
||||
return
|
||||
has_assistent = any(
|
||||
"assistent" in repo_dirname(r).lower()
|
||||
or "assistent" in (r.url or "").lower()
|
||||
for r in repos
|
||||
if repo_matches_runtime(r, runtime)
|
||||
)
|
||||
if has_assistent:
|
||||
checks.append(Check("Assistent ext", True, False, "swarm-assistent в extensions.yaml"))
|
||||
else:
|
||||
checks.append(
|
||||
Check(
|
||||
"Assistent ext",
|
||||
True,
|
||||
False,
|
||||
"нет swarm-assistent (requires:ollama) в extensions.yaml — чат не поставится",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _civitai(cfg: Config, checks: list[Check]) -> None:
|
||||
if not cfg.civitai_api_token:
|
||||
checks.append(
|
||||
|
||||
@@ -21,10 +21,13 @@ VALID_RUNTIMES = frozenset({"none", "ollama"})
|
||||
MEMORY_EMBED_MODEL = "nomic-embed-text"
|
||||
|
||||
OLLAMA_PRESETS: dict[str, list[str]] = {
|
||||
# Requirement: uncensored (abliterated) + solid Russian. Qwen2.5 family.
|
||||
# Vision tags preferred for SwarmUI prompt help with images.
|
||||
# Memory embed (nomic) is appended separately with use: memory.
|
||||
"recommended": ["huihui_ai/qwen2.5-vl-abliterated:7b"], # ~6 GB
|
||||
# Requirement: uncensored (abliterated) + solid Russian. Vision for prompt help.
|
||||
# Pin :8b-instruct — :latest on this library is Thinking, not chat.
|
||||
# Qwen3-VL needs Ollama ≥ 0.12.7. Memory embed (nomic) appended with use: memory.
|
||||
"recommended": [
|
||||
"huihui_ai/qwen3-vl-abliterated:8b-instruct", # ~6.1 GB, default
|
||||
"huihui_ai/qwen2.5-vl-abliterated:7b", # ~6 GB, second chat tag
|
||||
],
|
||||
"light": ["huihui_ai/qwen2.5-vl-abliterated:3b"], # ~3 GB
|
||||
"text": ["huihui_ai/qwen2.5-abliterate:7b"], # ~5 GB, no vision
|
||||
"big": ["huihui_ai/qwen2.5-vl-abliterated:32b"], # ~21 GB
|
||||
@@ -32,7 +35,7 @@ OLLAMA_PRESETS: dict[str, list[str]] = {
|
||||
}
|
||||
|
||||
OLLAMA_PRESET_LABELS: dict[str, str] = {
|
||||
"recommended": "VL 7B abliterate — RU + картинки (~6GB)",
|
||||
"recommended": "VL 8B Instruct abliterate — RU + картинки (~6GB)",
|
||||
"light": "VL 3B abliterate — RU + картинки, мало VRAM (~3GB)",
|
||||
"text": "7B abliterate text — RU, без vision (~5GB)",
|
||||
"big": "VL 32B abliterate — RU + картинки (~21GB)",
|
||||
@@ -40,10 +43,8 @@ OLLAMA_PRESET_LABELS: dict[str, str] = {
|
||||
"keep": "не менять ollama-models.yaml",
|
||||
}
|
||||
|
||||
# Deprecated text blob — prefer menu helpers below.
|
||||
PRESET_HELP = "\n".join(
|
||||
f"{k} — {v}" for k, v in OLLAMA_PRESET_LABELS.items() if k != "keep"
|
||||
)
|
||||
# Deprecated: use ollama_preset_menu / OLLAMA_PRESET_LABELS
|
||||
PRESET_HELP = "" # kept empty; menus use OLLAMA_PRESET_LABELS
|
||||
|
||||
LLM_RUNTIME_LABELS: dict[str, str] = {
|
||||
"none": "только SwarmUI",
|
||||
|
||||
@@ -77,16 +77,16 @@ def ollama_tune_for(info: GpuInfo) -> OllamaTune:
|
||||
note = "ultra: flash+q8 KV, 20GiB reserved for Swarm, ctx 32k, keep 30m"
|
||||
elif info.tier == TIER_HIGH:
|
||||
overhead = 14 * 1024**3
|
||||
keep = "15m"
|
||||
keep = "5m" # Assistent parks LLM before Generate; short keep is enough
|
||||
kv = "q8_0"
|
||||
ctx = 16384
|
||||
note = "high: flash+q8 KV, 14GiB reserved for Swarm, ctx 16k, keep 15m"
|
||||
note = "high: flash+q8 KV, 14GiB reserved for Swarm, ctx 16k, keep 5m"
|
||||
elif info.tier == TIER_MID:
|
||||
overhead = 10 * 1024**3
|
||||
keep = "15m"
|
||||
keep = "5m"
|
||||
kv = "q8_0"
|
||||
ctx = 16384
|
||||
note = "mid: flash+q8 KV, 10GiB reserved for Swarm, ctx 16k, keep 15m"
|
||||
note = "mid: flash+q8 KV, 10GiB reserved for Swarm, ctx 16k, keep 5m"
|
||||
else:
|
||||
overhead = 6 * 1024**3
|
||||
keep = "2m"
|
||||
|
||||
+136
-15
@@ -710,8 +710,55 @@ def seed_swarmui_api_keys(cfg: Config, host: str, log: Log) -> None:
|
||||
run_ssh(cfg, host, "rm -f /tmp/gpu-rent-swarm-api-keys.json", check=False)
|
||||
|
||||
|
||||
def _safe_persona_id(raw: object) -> str | None:
|
||||
s = str(raw or "").strip().replace("\\", "/")
|
||||
if not s or ".." in s or "/" in s:
|
||||
return None
|
||||
import re
|
||||
|
||||
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_\-]{0,63}", s):
|
||||
return None
|
||||
return s
|
||||
|
||||
|
||||
def _overlay_num_ctx(cfg: Config, host: str) -> int | None:
|
||||
"""Prefer GPU-tier context from .gpu-rent-gpu.json when present."""
|
||||
raw = run_ssh(
|
||||
cfg,
|
||||
host,
|
||||
f"test -f {DATA}/.gpu-rent-gpu.json && cat {DATA}/.gpu-rent-gpu.json || true",
|
||||
check=False,
|
||||
timeout=15,
|
||||
).strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
probe = json.loads(raw)
|
||||
vram = int(probe.get("vram_mib") or probe.get("memory_total_mib") or 0)
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
return None
|
||||
if vram <= 0:
|
||||
return None
|
||||
from gpu_rent.perf_tiers import GpuInfo, ollama_tune_for, tier_for_vram_mib
|
||||
|
||||
info = GpuInfo(
|
||||
name=str(probe.get("name") or "gpu"),
|
||||
vram_mib=vram,
|
||||
compute_cap=str(probe.get("compute_cap") or "0.0"),
|
||||
uuid=str(probe.get("uuid") or ""),
|
||||
tier=tier_for_vram_mib(vram),
|
||||
)
|
||||
return ollama_tune_for(info).context_length
|
||||
|
||||
|
||||
def seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
|
||||
"""Push local assistent-personas.yaml → VM Assistent/personas.yaml + personas.json."""
|
||||
"""Push local assistent-personas.yaml → VM Assistent/personas/<id>/ overlay.
|
||||
|
||||
Laptop yaml stays the editor; on VM we write persona.json + extra.md and
|
||||
overlay assistant.json (default_persona, optional num_ctx). Does not clobber
|
||||
voice/likes/dislikes/rules already on disk. No longer writes legacy
|
||||
personas.json / personas.yaml dumps.
|
||||
"""
|
||||
from gpu_rent.paths import assistent_personas_example_path, assistent_personas_manifest_path
|
||||
|
||||
local = Path(
|
||||
@@ -728,24 +775,96 @@ def seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
|
||||
if not text.strip():
|
||||
log("assistent-personas: пустой файл — skip")
|
||||
return
|
||||
remote_dir = f"{DATA}/Assistent"
|
||||
remote_yaml = f"{remote_dir}/personas.yaml"
|
||||
remote_json = f"{remote_dir}/personas.json"
|
||||
run_ssh(cfg, host, f"mkdir -p {shlex.quote(remote_dir)}", check=False)
|
||||
put_text(cfg, host, remote_yaml, text if text.endswith("\n") else text + "\n")
|
||||
try:
|
||||
import yaml
|
||||
|
||||
data = yaml.safe_load(text) or {}
|
||||
put_text(
|
||||
cfg,
|
||||
host,
|
||||
remote_json,
|
||||
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"assistent-personas: json convert — {exc}")
|
||||
log(f"assistent-personas → {remote_yaml}")
|
||||
log(f"assistent-personas: yaml parse — {exc}")
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
log("assistent-personas: корень должен быть mapping — skip")
|
||||
return
|
||||
|
||||
remote_dir = f"{DATA}/Assistent"
|
||||
run_ssh(cfg, host, f"mkdir -p {shlex.quote(remote_dir)}/personas {shlex.quote(remote_dir)}/_base", check=False)
|
||||
|
||||
default_id = _safe_persona_id(data.get("default")) or "neutral"
|
||||
personas = data.get("personas") or []
|
||||
written = 0
|
||||
if isinstance(personas, list):
|
||||
for row in personas:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
pid = _safe_persona_id(row.get("id"))
|
||||
if not pid:
|
||||
continue
|
||||
title = str(row.get("title") or pid).strip() or pid
|
||||
prompt = str(row.get("prompt") or "").strip()
|
||||
pdir = f"{remote_dir}/personas/{pid}"
|
||||
run_ssh(cfg, host, f"mkdir -p {shlex.quote(pdir)}", check=False)
|
||||
meta = {"title": title, "tagline": title, "accent": "#8b949e"}
|
||||
put_text(
|
||||
cfg,
|
||||
host,
|
||||
f"{pdir}/persona.json",
|
||||
json.dumps(meta, ensure_ascii=False, indent=2) + "\n",
|
||||
)
|
||||
if prompt:
|
||||
put_text(
|
||||
cfg,
|
||||
host,
|
||||
f"{pdir}/extra.md",
|
||||
prompt if prompt.endswith("\n") else prompt + "\n",
|
||||
)
|
||||
written += 1
|
||||
|
||||
assistant_overlay: dict = {"default_persona": default_id}
|
||||
num_ctx = _overlay_num_ctx(cfg, host)
|
||||
if num_ctx:
|
||||
assistant_overlay["num_ctx"] = num_ctx
|
||||
put_text(
|
||||
cfg,
|
||||
host,
|
||||
f"{remote_dir}/_base/assistant.json",
|
||||
json.dumps(assistant_overlay, ensure_ascii=False, indent=2) + "\n",
|
||||
)
|
||||
# Drop legacy dumps so Assistent does not double-inject prompts.
|
||||
run_ssh(
|
||||
cfg,
|
||||
host,
|
||||
f"rm -f {shlex.quote(remote_dir + '/personas.json')} "
|
||||
f"{shlex.quote(remote_dir + '/personas.yaml')}",
|
||||
check=False,
|
||||
)
|
||||
ctx_note = f", num_ctx={num_ctx}" if num_ctx else ""
|
||||
log(f"assistent-personas → overlay personas/{written} (default={default_id}{ctx_note})")
|
||||
|
||||
|
||||
def count_wanted_models_on_vm(cfg: Config, host: str) -> int:
|
||||
"""Count entries in Assistent wanted queue on the VM (best-effort)."""
|
||||
remote = f"{DATA}/.gpu-rent-wanted-models.yaml"
|
||||
raw = run_ssh(
|
||||
cfg,
|
||||
host,
|
||||
f"test -f {shlex.quote(remote)} && cat {shlex.quote(remote)} || true",
|
||||
check=False,
|
||||
timeout=20,
|
||||
).strip()
|
||||
if not raw:
|
||||
return 0
|
||||
try:
|
||||
import yaml
|
||||
|
||||
data = yaml.safe_load(raw) or {}
|
||||
except Exception:
|
||||
return sum(1 for line in raw.splitlines() if line.strip().startswith("- url:"))
|
||||
n = 0
|
||||
if isinstance(data, dict):
|
||||
for rows in data.values():
|
||||
if isinstance(rows, list):
|
||||
n += sum(1 for r in rows if isinstance(r, dict) and r.get("url"))
|
||||
return n
|
||||
|
||||
|
||||
def merge_wanted_models_from_vm(cfg: Config, host: str, log: Log) -> int:
|
||||
@@ -1072,6 +1191,8 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
|
||||
)
|
||||
|
||||
# Drop LLM units that should not hold VRAM for this runtime.
|
||||
# Tombstone: still stop gpu-rent-llamacpp if an old disk left that unit behind
|
||||
# (llamacpp runtime was removed; do not reinstall it).
|
||||
if runtime == "none":
|
||||
log("LLM: none — останавливаю gpu-rent-ollama / gpu-rent-llamacpp если были")
|
||||
_stop_units("gpu-rent-ollama", "gpu-rent-llamacpp")
|
||||
@@ -1083,7 +1204,7 @@ def provision_llm(cfg: Config, host: str, log: Log) -> None:
|
||||
return
|
||||
still: list[str] = []
|
||||
if runtime == "ollama":
|
||||
_stop_units("gpu-rent-llamacpp")
|
||||
_stop_units("gpu-rent-llamacpp") # tombstone: disable leftover llamacpp unit
|
||||
log("LLM: ставим/запускаем Ollama")
|
||||
run_script_sudo(
|
||||
cfg,
|
||||
|
||||
@@ -129,7 +129,11 @@ def swarm_busy(swarm_url: str, timeout: float = 8.0) -> tuple[bool, str]:
|
||||
|
||||
|
||||
def llm_busy(timeout: float = 3.0) -> tuple[bool, str]:
|
||||
"""Ollama pull / loaded models count as busy."""
|
||||
"""Ollama pull / loaded models count as busy.
|
||||
|
||||
Empty /api/ps after AssistentParkLlm is NOT busy by itself — Swarm queue
|
||||
is checked separately in main() and keeps the GPU alive during Generate.
|
||||
"""
|
||||
pull_marker = DATA / ".gpu-rent-ollama-pulling"
|
||||
if pull_marker.is_file():
|
||||
try:
|
||||
|
||||
@@ -47,9 +47,9 @@ except ValueError:
|
||||
if gib >= 48:
|
||||
tier, overhead, keep, kv, flash, ctx = "ultra", 20 * 1024**3, "30m", "q8_0", True, 32768
|
||||
elif gib >= 24:
|
||||
tier, overhead, keep, kv, flash, ctx = "high", 14 * 1024**3, "15m", "q8_0", True, 16384
|
||||
tier, overhead, keep, kv, flash, ctx = "high", 14 * 1024**3, "5m", "q8_0", True, 16384
|
||||
elif gib >= 16:
|
||||
tier, overhead, keep, kv, flash, ctx = "mid", 10 * 1024**3, "15m", "q8_0", True, 16384
|
||||
tier, overhead, keep, kv, flash, ctx = "mid", 10 * 1024**3, "5m", "q8_0", True, 16384
|
||||
else:
|
||||
tier, overhead, keep, kv, flash, ctx = "low", 6 * 1024**3, "2m", "q4_0", False, 8192
|
||||
flash = bool(flash and (ampere or "A100" in name.upper() or "H100" in name.upper() or gib >= 16))
|
||||
|
||||
Reference in New Issue
Block a user