Support Assistent persona packs via git and local sync.

Replace assistent-personas overlay seed with assistent-extensions SFTP and an assistent: section in extensions.yaml so personalities install like other extensions without private URLs in the public repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 08:28:31 +03:00
co-authored by Cursor
parent b24c1b5d4b
commit 20ae7bd83a
25 changed files with 258 additions and 206 deletions
+3 -3
View File
@@ -331,11 +331,11 @@ def merge_extensions_yaml(
dry_run: bool,
) -> tuple[list[ExtCaptureItem], list[ExtCaptureItem], list[str]]:
"""Return (added, updated, skipped). Same dir + different URL → update url/ref."""
data: dict[str, list[dict[str, Any]]] = {"swarmui": [], "comfy": []}
data: dict[str, list[dict[str, Any]]] = {"swarmui": [], "comfy": [], "assistent": []}
if path.is_file():
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if isinstance(raw, dict):
for kind in ("swarmui", "comfy"):
for kind in ("swarmui", "comfy", "assistent"):
items = raw.get(kind) or []
if isinstance(items, list):
for it in items:
@@ -701,7 +701,7 @@ def capture_extensions(
report.ext_unknown.append(f"{raw.get('kind')}/{name} ({reason})")
continue
kind = str(raw.get("kind") or "")
if kind not in ("swarmui", "comfy"):
if kind not in ("swarmui", "comfy", "assistent"):
continue
resolved.append(
ExtCaptureItem(
+7 -2
View File
@@ -923,13 +923,18 @@ def seed_models() -> None:
@app.command("push")
def push_all() -> None:
"""SFTP Models + Wildcards + CustomWorkflows."""
"""SFTP Models + Wildcards + CustomWorkflows + assistent-extensions."""
try:
from gpu_rent.paths import assistent_extensions_dir
from gpu_rent.provision import seed_assistent_personas
cfg, host = _live()
push_tree(cfg, host, cfg.local_models_dir, "/mnt/swarm_data/Models", log, models=True)
push_tree(cfg, host, cfg.local_wildcards_dir, "/mnt/swarm_data/Data/Wildcards", log, models=False)
push_tree(cfg, host, cfg.local_workflows_dir, "/mnt/swarm_data/CustomWorkflows", log, models=False)
if assistent_extensions_dir().is_dir():
seed_assistent_personas(cfg, host, log)
except GpuRentError as exc:
_die(exc)
@@ -985,7 +990,7 @@ def seed_extensions_cmd() -> None:
@app.command("seed-personas")
def seed_personas_cmd() -> None:
"""Push assistent-personas/ → VM Assistent overlay (без полного up)."""
"""Push assistent-extensions/ → VM Assistent/extensions (+ _base)."""
try:
from gpu_rent.provision import seed_assistent_personas
+25 -14
View File
@@ -118,6 +118,12 @@ if personas_root.is_dir():
for d in sorted(personas_root.iterdir()):
if d.is_dir():
persona_ids.append(d.name)
pack_root = overlay / "extensions"
pack_dirs = []
if pack_root.is_dir():
for d in sorted(pack_root.iterdir()):
if d.is_dir():
pack_dirs.append(d.name)
base_json = {}
base_path = overlay / "_base" / "assistant.json"
if base_path.is_file():
@@ -186,6 +192,7 @@ print(json.dumps({
"exists": overlay.is_dir(),
"entries": sorted(x.name for x in overlay.iterdir())[:50] if overlay.is_dir() else [],
"persona_ids": persona_ids,
"pack_dirs": pack_dirs,
"default_persona": base_json.get("default_persona") if isinstance(base_json, dict) else None,
"num_ctx": base_json.get("num_ctx") if isinstance(base_json, dict) else None,
"embed_model": base_json.get("embed_model") if isinstance(base_json, dict) else None,
@@ -610,30 +617,31 @@ def collect_assistent_overlay(cfg: Config, *, fs: dict[str, Any] | None = None)
if not fs.get("ok"):
return fs
overlay = fs.get("overlay") or {}
local_ids: list[str] = []
local_packs: list[str] = []
try:
from gpu_rent.paths import assistent_personas_dir
from gpu_rent.paths import assistent_extensions_dir
pdir = assistent_personas_dir()
pdir = assistent_extensions_dir()
if pdir.is_dir():
local_ids = sorted(
x.name for x in pdir.iterdir() if x.is_dir() and not x.name.startswith("_")
local_packs = sorted(
x.name for x in pdir.iterdir() if x.is_dir() and not x.name.startswith(".")
)
except Exception:
pass
remote_ids = list(overlay.get("persona_ids") or [])
missing_on_vm = sorted(set(local_ids) - set(remote_ids))
remote_packs = list(overlay.get("pack_dirs") or [])
missing_on_vm = sorted(set(local_packs) - set(remote_packs))
hints: list[str] = []
if local_ids and missing_on_vm:
if local_packs and missing_on_vm:
hints.append(
f"локальные personas не на VM: {', '.join(missing_on_vm[:8])} — gpu-rent seed-personas"
f"локальные packs не на VM: {', '.join(missing_on_vm[:8])} — gpu-rent seed-personas"
)
if not overlay.get("exists"):
hints.append("нет /mnt/swarm_data/Assistent — seed ещё не писал overlay (bundled personas ок)")
return {
"ok": True,
"overlay": overlay,
"local_persona_ids": local_ids,
"local_pack_dirs": local_packs,
"local_persona_ids": local_packs, # back-compat for older clients
"missing_on_vm": missing_on_vm,
"hints": hints,
}
@@ -1034,16 +1042,17 @@ def _ollama_chat_smoke_ssh(cfg: Config, *, model: str) -> dict[str, Any]:
def _local_assistent_bits(cfg: Config) -> dict[str, Any]:
local: dict[str, Any] = {}
try:
from gpu_rent.paths import assistent_personas_dir
from gpu_rent.paths import assistent_extensions_dir
pdir = assistent_personas_dir()
local["personas_dir"] = {
pdir = assistent_extensions_dir()
local["extensions_dir"] = {
"path": str(pdir),
"exists": pdir.is_dir(),
"entries": sorted(x.name for x in pdir.iterdir())[:40] if pdir.is_dir() else [],
}
local["personas_dir"] = local["extensions_dir"] # back-compat
except Exception as exc:
local["personas_dir"] = {"error": str(exc)[:120]}
local["extensions_dir"] = {"error": str(exc)[:120]}
try:
from gpu_rent.manifests import parse_extensions, repo_dirname
@@ -1051,9 +1060,11 @@ def _local_assistent_bits(cfg: Config) -> dict[str, Any]:
has = any(
"assistent" in repo_dirname(r).lower() or "assistent" in (r.url or "").lower()
for r in repos
if r.kind == "swarmui"
)
local["extensions_yaml"] = {
"has_swarm_assistent": has,
"assistent_packs": sum(1 for r in repos if r.kind == "assistent"),
"manifest": str(cfg.extensions_manifest),
}
except Exception as exc:
+8 -3
View File
@@ -127,9 +127,9 @@ def parse_extensions(path: Path) -> list[GitRepo]:
if not data:
return []
if not isinstance(data, dict):
raise ConfigError(f"{path}: корень swarmui: / comfy:")
raise ConfigError(f"{path}: корень swarmui: / comfy: / assistent:")
repos: list[GitRepo] = []
for kind in ("swarmui", "comfy"):
for kind in ("swarmui", "comfy", "assistent"):
items = data.get(kind) or []
if not items:
continue
@@ -184,5 +184,10 @@ def is_commit_sha(ref: str) -> bool:
def remote_root_for(repo: GitRepo) -> str:
base = "/mnt/swarm_data/Extensions" if repo.kind == "swarmui" else "/mnt/swarm_data/DLNodes"
if repo.kind == "swarmui":
base = "/mnt/swarm_data/Extensions"
elif repo.kind == "assistent":
base = "/mnt/swarm_data/Assistent/extensions"
else:
base = "/mnt/swarm_data/DLNodes"
return f"{base}/{repo_dirname(repo)}"
+12 -2
View File
@@ -48,12 +48,22 @@ def models_manifest_path() -> Path:
return app_root() / "models.yaml"
def assistent_extensions_dir() -> Path:
"""Working persona-pack clones on the laptop (gitignored)."""
return app_root() / "assistent-extensions"
def assistent_extensions_example_dir() -> Path:
return app_root() / "assistent-extensions.example"
def assistent_personas_dir() -> Path:
"""Working overlay source on the laptop (gitignored)."""
"""Deprecated overlay-shelf folder — kept for migration warning only."""
return app_root() / "assistent-personas"
def assistent_personas_example_dir() -> Path:
"""Deprecated example — prefer assistent_extensions_example_dir()."""
return app_root() / "assistent-personas.example"
@@ -63,7 +73,7 @@ def assistent_personas_manifest_path() -> Path:
def assistent_personas_example_path() -> Path:
"""Deprecated yaml example — prefer assistent_personas_example_dir()."""
"""Deprecated yaml example — prefer assistent_extensions_example_dir()."""
return app_root() / "assistent-personas.example.yaml"
+3
View File
@@ -14,6 +14,9 @@ def is_skipped(path: Path) -> bool:
name = path.name
if name in SKIP_NAMES or name.startswith("."):
return True
# Never sync git metadata (persona packs push shelves only).
if any(part == ".git" for part in path.parts):
return True
return False
+48 -78
View File
@@ -895,97 +895,64 @@ def _overlay_num_ctx(cfg: Config, host: str) -> int | None:
def seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
"""Push local assistent-personas/ folder → VM Assistent/ overlay.
"""Push local assistent-extensions/ packs + _base/assistant.json to VM Assistent/.
Copies personas/<id>/*.json (+ optional extra.md) and sparse _base/assistant.json
(default_persona + optional num_ctx). Does not delete overlay personas missing
from the laptop (VM-created clones stay). Does not clobber exact.controls.
Legacy assistent-personas.yaml is no longer read.
Each subdirectory of assistent-extensions/ is SFTP'd to
Assistent/extensions/<dirname>/ (``.git`` skipped). Overlay UI clones under
Assistent/personas/ are never deleted. Legacy assistent-personas/ shelf seed
is no longer read.
"""
from gpu_rent.paths import (
assistent_extensions_dir,
assistent_personas_dir,
assistent_personas_example_dir,
assistent_personas_manifest_path,
)
local = Path(
(getattr(cfg, "assistent_personas_dir", None) or assistent_personas_dir())
)
if not local.is_dir():
example = assistent_personas_example_dir()
if example.is_dir():
local = example
else:
legacy = assistent_personas_manifest_path()
if legacy.is_file():
log(
"assistent-personas: yaml больше не читается — "
"скопируй полки в assistent-personas/ (см. assistent-personas.example/)"
)
else:
log("assistent-personas: нет папки — skip (bundled из расширения)")
return
from gpu_rent.sync_files import push_tree
remote_dir = f"{DATA}/Assistent"
run_ssh(
cfg,
host,
f"mkdir -p {shlex.quote(remote_dir)}/personas {shlex.quote(remote_dir)}/_base",
f"mkdir -p {shlex.quote(remote_dir)}/extensions {shlex.quote(remote_dir)}/_base",
check=False,
)
legacy = Path(
(getattr(cfg, "assistent_personas_dir", None) or assistent_personas_dir())
)
if legacy.is_dir() and any(legacy.iterdir()):
log(
"assistent-personas/: больше не сидится — "
"клонируй паки в assistent-extensions/ (см. assistent-extensions.example/)"
)
legacy_yaml = assistent_personas_manifest_path()
if legacy_yaml.is_file():
log(
"assistent-personas.yaml больше не читается — "
"используй assistent: в extensions.yaml или assistent-extensions/"
)
local_ext = Path(
getattr(cfg, "assistent_extensions_dir", None) or assistent_extensions_dir()
)
pushed = 0
if local_ext.is_dir():
for child in sorted(local_ext.iterdir()):
if not child.is_dir() or child.name.startswith("."):
continue
# Require a pack manifest or persona.json so empty clone dirs are skipped.
if not (
(child / "assistent-pack.yaml").is_file()
or (child / "persona.json").is_file()
or (child / "personas").is_dir()
):
log(f"assistent-extensions/{child.name}: нет assistent-pack.yaml — skip")
continue
remote = f"{remote_dir}/extensions/{child.name}"
push_tree(cfg, host, child, remote, log, models=False)
pushed += 1
default_id = "neutral"
base_asst = local / "_base" / "assistant.json"
if base_asst.is_file():
try:
data = json.loads(base_asst.read_text(encoding="utf-8"))
if isinstance(data, dict):
default_id = _safe_persona_id(data.get("default_persona")) or default_id
except (json.JSONDecodeError, OSError) as exc:
log(f"assistent-personas: _base/assistant.json — {exc}")
personas_root = local / "personas"
written = 0
if personas_root.is_dir():
for pdir in sorted(personas_root.iterdir()):
if not pdir.is_dir():
continue
pid = _safe_persona_id(pdir.name)
if not pid:
continue
remote_p = f"{remote_dir}/personas/{pid}"
run_ssh(cfg, host, f"mkdir -p {shlex.quote(remote_p)}", check=False)
for f in sorted(pdir.iterdir()):
if not f.is_file():
continue
name = f.name
if name.endswith(".json") or name == "extra.md":
if ".." in name or "/" in name or "\\" in name:
continue
# Never overwrite exact.json controls from laptop if remote has
# user-tuned values — merge only when seeding a missing exact,
# or push non-controls shelves always. For simplicity: push all
# shelves except skip exact.json when remote already has it
# (preserves preference_bias). First-time still seeds exact.
remote_f = f"{remote_p}/{name}"
if name == "exact.json":
exists = run_ssh(
cfg,
host,
f"test -f {shlex.quote(remote_f)} && echo yes || true",
check=False,
timeout=15,
).strip()
if exists == "yes":
continue
put_text(
cfg,
host,
remote_f,
f.read_text(encoding="utf-8"),
)
written += 1
assistant_overlay: dict = {"default_persona": default_id}
num_ctx = _overlay_num_ctx(cfg, host)
if num_ctx:
@@ -1005,7 +972,10 @@ def seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
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})")
log(
f"assistent-extensions → packs/{pushed} "
f"(default={default_id}{ctx_note})"
)
seed_civitai_examples(cfg, host, log)
@@ -1623,7 +1593,7 @@ def provision_vm(
try:
seed_assistent_personas(cfg, host, log)
except Exception as exc:
log(f"assistent-personas: {exc}")
log(f"assistent packs: {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
+1
View File
@@ -16,6 +16,7 @@ MARKER = Path("/mnt/swarm_data/.gpu-rent-extensions-seeded")
EXTRA_ROOTS = (
Path("/mnt/swarm_data/Extensions"),
Path("/mnt/swarm_data/DLNodes"),
Path("/mnt/swarm_data/Assistent/extensions"),
)
+1
View File
@@ -30,6 +30,7 @@ FOLDER_TO_KIND = {
EXT_ROOTS = (
("swarmui", DATA / "Extensions"),
("comfy", DATA / "DLNodes"),
("assistent", DATA / "Assistent" / "extensions"),
)
+5 -5
View File
@@ -16,8 +16,8 @@ from gpu_rent.llm_runtime import (
)
from gpu_rent.paths import (
app_root,
assistent_personas_dir,
assistent_personas_example_dir,
assistent_extensions_dir,
assistent_extensions_example_dir,
env_path,
extensions_manifest_path,
models_manifest_path,
@@ -68,9 +68,9 @@ 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_tree_if_missing(
assistent_personas_example_dir(),
assistent_personas_dir(),
"assistent-personas/",
assistent_extensions_example_dir(),
assistent_extensions_dir(),
"assistent-extensions/",
log,
)
_copy_if_missing(