Seed Assistent personas from a shelf folder instead of yaml prompts.

Push assistent-personas/ JSON overlays without wiping VM clones or exact.controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 01:48:01 +03:00
co-authored by Cursor
parent 03ba4cb6ed
commit 7be2c27a67
13 changed files with 187 additions and 129 deletions
+1 -1
View File
@@ -944,7 +944,7 @@ def seed_extensions_cmd() -> None:
@app.command("seed-personas")
def seed_personas_cmd() -> None:
"""Push assistent-personas.yaml → VM Assistent overlay (без полного up)."""
"""Push assistent-personas/ → VM Assistent overlay (без полного up)."""
try:
from gpu_rent.provision import seed_assistent_personas
+11
View File
@@ -48,11 +48,22 @@ def models_manifest_path() -> Path:
return app_root() / "models.yaml"
def assistent_personas_dir() -> Path:
"""Working overlay source on the laptop (gitignored)."""
return app_root() / "assistent-personas"
def assistent_personas_example_dir() -> Path:
return app_root() / "assistent-personas.example"
def assistent_personas_manifest_path() -> Path:
"""Deprecated yaml path — kept for migration warning only."""
return app_root() / "assistent-personas.yaml"
def assistent_personas_example_path() -> Path:
"""Deprecated yaml example — prefer assistent_personas_example_dir()."""
return app_root() / "assistent-personas.example.yaml"
+74 -50
View File
@@ -752,71 +752,95 @@ 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.yaml → VM Assistent/personas/<id>/ overlay.
"""Push local assistent-personas/ folder → VM Assistent/ 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.
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.
"""
from gpu_rent.paths import assistent_personas_example_path, assistent_personas_manifest_path
from gpu_rent.paths import (
assistent_personas_dir,
assistent_personas_example_dir,
assistent_personas_manifest_path,
)
local = Path(
(getattr(cfg, "assistent_personas_manifest", None) or assistent_personas_manifest_path())
(getattr(cfg, "assistent_personas_dir", None) or assistent_personas_dir())
)
if not local.is_file():
example = assistent_personas_example_path()
if example.is_file():
if not local.is_dir():
example = assistent_personas_example_dir()
if example.is_dir():
local = example
else:
log("assistent-personas: нет yaml — skip")
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
text = local.read_text(encoding="utf-8")
if not text.strip():
log("assistent-personas: пустой файл — skip")
return
try:
import yaml
data = yaml.safe_load(text) or {}
except Exception as exc:
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)
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 []
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 isinstance(personas, list):
for row in personas:
if not isinstance(row, dict):
if personas_root.is_dir():
for pdir in sorted(personas_root.iterdir()):
if not pdir.is_dir():
continue
pid = _safe_persona_id(row.get("id"))
pid = _safe_persona_id(pdir.name)
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",
)
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}
+17 -6
View File
@@ -16,8 +16,8 @@ from gpu_rent.llm_runtime import (
)
from gpu_rent.paths import (
app_root,
assistent_personas_example_path,
assistent_personas_manifest_path,
assistent_personas_dir,
assistent_personas_example_dir,
env_path,
extensions_manifest_path,
models_manifest_path,
@@ -42,6 +42,17 @@ def _copy_if_missing(src: Path, dst: Path, label: str, log: Log) -> None:
log(f"нет example для {label}: {src}")
def _copy_tree_if_missing(src: Path, dst: Path, label: str, log: Log) -> None:
if dst.exists():
log(f"есть {label}")
return
if src.is_dir():
shutil.copytree(src, dst)
log(f"создал {label} из example")
else:
log(f"нет example для {label}: {src}")
def run_setup(
*,
llm: str | None = None,
@@ -56,10 +67,10 @@ 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",
_copy_tree_if_missing(
assistent_personas_example_dir(),
assistent_personas_dir(),
"assistent-personas/",
log,
)
_copy_if_missing(