Add assistent-personas support and enhance model merging functionality

- Introduced `assistent-personas.yaml` to the project and updated the `.gitignore` accordingly.
- Implemented functions to seed and merge assistent personas from local files to the VM.
- Enhanced the model capture process to include merging of wanted models from the VM into `models.yaml`.
- Updated documentation to reflect changes in the assistent personas and model management processes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 21:21:53 +03:00
co-authored by Cursor
parent 36e29668b0
commit ec8bd28ce0
8 changed files with 257 additions and 3 deletions
+70
View File
@@ -405,6 +405,64 @@ def merge_extensions_yaml(
return added, updated, skipped
def merge_wanted_models_from_vm(cfg: Config, host: str, log: Log) -> int:
"""Pull VM Assistent wanted queue into local models.yaml. Returns new url count."""
from gpu_rent.manifests import MODEL_DIRS
from gpu_rent.provision import DATA
remote = f"{DATA}/.gpu-rent-wanted-models.yaml"
raw = run_ssh(
cfg,
host,
f"test -f {remote!r} && cat {remote!r} || true".replace("'", '"')
if False
else f'test -f {remote} && cat {remote} || true',
check=False,
timeout=20,
).strip()
if not raw:
return 0
try:
data = yaml.safe_load(raw) or {}
except Exception as exc:
log(f"wanted-models: bad yaml — {exc}")
return 0
if not isinstance(data, dict):
return 0
items: list[ModelCaptureItem] = []
for kind, rows in data.items():
if kind not in MODEL_DIRS or not isinstance(rows, list):
continue
for row in rows:
if not isinstance(row, dict):
continue
url = str(row.get("url") or "").strip()
if not url:
continue
vid = row.get("version_id")
try:
vid_i = int(vid) if vid not in (None, "", 0, "0") else extract_version_id(url)
except (TypeError, ValueError):
vid_i = extract_version_id(url)
items.append(
ModelCaptureItem(
kind=kind,
url=url,
title=str(row.get("title") or Path(url).name),
version_id=vid_i,
source="wanted",
)
)
if not items:
return 0
added, skipped = merge_models_yaml(Path(cfg.models_manifest), items, dry_run=False)
if added:
log(f"wanted→models.yaml: +{len(added)} (skip {len(skipped)})")
elif skipped:
log(f"wanted→models.yaml: уже есть ({len(skipped)})")
return len(added)
def capture_models(
cfg: Config,
host: str,
@@ -551,6 +609,18 @@ def capture_models(
if hf_fallback:
log(f"capture: Hugging Face fallback — {hf_fallback} файл(ов)")
# Also merge Assistent wanted-queue (models not yet on disk / seed list).
try:
from gpu_rent.provision import merge_wanted_models_from_vm
# merge writes directly; still collect for report when dry_run=False
if not dry_run:
n = merge_wanted_models_from_vm(cfg, host, log)
if n:
log(f"capture: wanted queue → models.yaml (+{n})")
except Exception as exc:
log(f"capture wanted: {exc}")
added, skipped = merge_models_yaml(
cfg.models_manifest, resolved, dry_run=dry_run
)
+8
View File
@@ -48,6 +48,14 @@ def models_manifest_path() -> Path:
return app_root() / "models.yaml"
def assistent_personas_manifest_path() -> Path:
return app_root() / "assistent-personas.yaml"
def assistent_personas_example_path() -> Path:
return app_root() / "assistent-personas.example.yaml"
def ollama_models_manifest_path() -> Path:
return app_root() / "ollama-models.yaml"
+140
View File
@@ -710,9 +710,139 @@ 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 seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
"""Push local assistent-personas.yaml → VM Assistent/personas.yaml + personas.json."""
from gpu_rent.paths import assistent_personas_example_path, assistent_personas_manifest_path
local = Path(
(getattr(cfg, "assistent_personas_manifest", None) or assistent_personas_manifest_path())
)
if not local.is_file():
example = assistent_personas_example_path()
if example.is_file():
local = example
else:
log("assistent-personas: нет yaml — skip")
return
text = local.read_text(encoding="utf-8")
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}")
def merge_wanted_models_from_vm(cfg: Config, host: str, log: Log) -> int:
"""Pull VM wanted queue into local models.yaml. Returns count of new urls merged."""
from gpu_rent.capture import ModelCaptureItem, merge_models_yaml
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 as exc:
log(f"wanted-models: bad yaml — {exc}")
return 0
if not isinstance(data, dict):
return 0
items: list[ModelCaptureItem] = []
for kind, rows in data.items():
if kind not in MODEL_DIRS or not isinstance(rows, list):
continue
for row in rows:
if not isinstance(row, dict):
continue
url = str(row.get("url") or "").strip()
if not url:
continue
vid = row.get("version_id")
try:
vid_i = int(vid) if vid not in (None, "", 0, "0") else extract_version_id(url)
except (TypeError, ValueError):
vid_i = extract_version_id(url)
items.append(
ModelCaptureItem(
kind=kind,
url=url,
title=str(row.get("title") or Path(url).name),
version_id=vid_i,
source="wanted",
)
)
if not items:
return 0
added, skipped = merge_models_yaml(Path(cfg.models_manifest), items, dry_run=False)
if added:
log(f"wanted→models.yaml: +{len(added)} (skip {len(skipped)})")
elif skipped:
log(f"wanted→models.yaml: уже есть ({len(skipped)})")
return len(added)
def _place_wanted_cards(cfg: Config, host: str, jobs: list[dict], log: Log) -> None:
"""Copy drafted .assistent.json from wanted-cards next to freshly seeded weights."""
cards_dir = f"{DATA}/.gpu-rent-wanted-cards"
exists = run_ssh(
cfg, host, f"test -d {shlex.quote(cards_dir)} && echo YES || true", check=False
).strip()
if "YES" not in exists:
return
placed = 0
for job in jobs:
dest = str(job.get("dest") or "")
vid = job.get("version_id")
if not dest or vid in (None, "", 0, "0"):
continue
stem = Path(dest).stem
card_src = f"{cards_dir}/{vid}.assistent.json"
card_dst = str(Path(dest).with_name(f"{stem}.assistent.json"))
out = run_ssh(
cfg,
host,
f"if test -f {shlex.quote(card_src)} && test -f {shlex.quote(dest)}; then "
f"cp -n {shlex.quote(card_src)} {shlex.quote(card_dst)} && echo PLACED; fi",
check=False,
).strip()
if "PLACED" in out:
placed += 1
if placed:
log(f"wanted-cards: положил {placed} .assistent.json рядом с весами")
def seed_civitai(cfg: Config, host: str, log: Log) -> None:
from gpu_rent.huggingface import is_huggingface_url
try:
merge_wanted_models_from_vm(cfg, host, log)
except Exception as exc:
log(f"wanted-models merge: {exc}")
entries = parse_models(cfg.models_manifest)
if not entries:
log("model-seed пропущен: манифест пуст — дефолт SwarmUI")
@@ -775,6 +905,7 @@ def seed_civitai(cfg: Config, host: str, log: Log) -> None:
"url": url,
"sha256": "",
"auth": "hf",
"version_id": None,
"sidecars": {
f"{stem}.swarm.json": json.dumps(swarm, ensure_ascii=False, indent=2),
},
@@ -825,6 +956,7 @@ def seed_civitai(cfg: Config, host: str, log: Log) -> None:
"url": _download_url(api_host, vid, info),
"sha256": sha,
"auth": "civitai",
"version_id": int(vid),
"sidecars": {
f"{stem}.civitai.json": civitai_json,
f"{stem}.swarm.json": json.dumps(swarm, ensure_ascii=False, indent=2),
@@ -903,6 +1035,10 @@ def seed_civitai(cfg: Config, host: str, log: Log) -> None:
timeout=7200,
log=log,
)
try:
_place_wanted_cards(cfg, host, jobs, log)
except Exception as exc:
log(f"wanted-cards: {exc}")
put_text(
cfg,
host,
@@ -1159,6 +1295,10 @@ def provision_vm(
except GpuRentError as exc:
log(f"autocomplete: {exc}")
seed_civitai(cfg, host, log)
try:
seed_assistent_personas(cfg, host, log)
except Exception as exc:
log(f"assistent-personas: {exc}")
push_tree(cfg, host, cfg.local_models_dir, f"{DATA}/Models", log, models=True)
push_tree(
cfg, host, cfg.local_wildcards_dir, f"{DATA}/Data/Wildcards", log, models=False
+8
View File
@@ -16,6 +16,8 @@ from gpu_rent.llm_runtime import (
)
from gpu_rent.paths import (
app_root,
assistent_personas_example_path,
assistent_personas_manifest_path,
env_path,
extensions_manifest_path,
models_manifest_path,
@@ -54,6 +56,12 @@ def run_setup(
_copy_if_missing(root / "env.example", env_path(), ".env", log)
_copy_if_missing(root / "models.example.yaml", models_manifest_path(), "models.yaml", log)
_copy_if_missing(
assistent_personas_example_path(),
assistent_personas_manifest_path(),
"assistent-personas.yaml",
log,
)
_copy_if_missing(
root / "extensions.example.yaml", extensions_manifest_path(), "extensions.yaml", log
)