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
+2
View File
@@ -8,6 +8,8 @@ models.yaml
assistent-personas.yaml assistent-personas.yaml
assistent-personas/ assistent-personas/
!assistent-personas.example/ !assistent-personas.example/
assistent-extensions/
!assistent-extensions.example/
extensions.yaml extensions.yaml
# SwarmUI checkpoints / LoRA dropped into the app tree # SwarmUI checkpoints / LoRA dropped into the app tree
+23
View File
@@ -0,0 +1,23 @@
# Assistent persona packs (laptop → VM)
Clone git persona packs into this folder (one subdirectory per pack).
On `gpu-rent up` / `seed-personas` / `push` each pack is SFTP'd to
`/mnt/swarm_data/Assistent/extensions/<dirname>/` (``.git`` is not uploaded).
Pack layout (repo root):
```
assistent-pack.yaml # kind: persona / id: my_persona
persona.json
voice.json
```
Or multi-pack: `personas/<id>/persona.json` under the clone.
Bundled personas ship with swarm-assistent (`neutral`, `aggressive`, `dreamer`).
Private packs stay only here — do not put their URLs in `extensions.yaml` if you
do not want them captured/shared.
Overlay clones created in the Assistent UI are **not** deleted by seed.
-16
View File
@@ -1,16 +0,0 @@
# Assistent personas (laptop → VM overlay)
Copy this folder to `assistent-personas/` (gitignored) and edit shelves.
On `gpu-rent up` / `seed-personas` files under `personas/<id>/` are pushed to
`/mnt/swarm_data/Assistent/personas/<id>/`. Bundled personas (including **leonid**)
ship with the swarm-assistent extension — you do not need to duplicate them here.
- `_base/assistant.json` — only `default_persona` (and gpu-rent adds `num_ctx`)
- `personas/<id>/*.json` — sparse identity shelves (bio, voice, humor, …)
- Overlay personas created in the Assistent UI are **not** deleted by seed
This is a **seed overlay**, not `gpu-rent push` (see [docs/local-folders.md](../docs/local-folders.md)).
Debug overlay vs laptop: `GET http://127.0.0.1:17821/assistent/overlay`.
See swarm-assistent `Config/personas/README.md` for the shelf schema.
@@ -1,3 +0,0 @@
{
"default_persona": "neutral"
}
+3 -3
View File
@@ -79,7 +79,7 @@ gpu-rent up --yes --ollama
| `gpu-rent ssh` | Оболочка на VM | | `gpu-rent ssh` | Оболочка на VM |
| `gpu-rent seed-models` | Докачать новые строки Civitai-манифеста на живой диск | | `gpu-rent seed-models` | Докачать новые строки Civitai-манифеста на живой диск |
| `gpu-rent seed-extensions` | Доклонировать/обновить git-репы; restart swarmui | | `gpu-rent seed-extensions` | Доклонировать/обновить git-репы; restart swarmui |
| `gpu-rent seed-personas` | Пуш `assistent-personas/`overlay на VM (без полного up); также `search.jsonl` → FTS, если есть | | `gpu-rent seed-personas` | Пуш `assistent-extensions/``Assistent/extensions/` (+ `_base`); также `search.jsonl` → FTS, если есть |
| `gpu-rent push` / `push-models` | Локальные деревья → VM | | `gpu-rent push` / `push-models` | Локальные деревья → VM |
| `gpu-rent pull-output` | VM `Output/``./Output` | | `gpu-rent pull-output` | VM `Output/``./Output` |
| `gpu-rent capture` / `capture all` | Инвентарь VM → merge **ссылок** в `models.yaml` + `extensions.yaml` (веса не качать) | | `gpu-rent capture` / `capture all` | Инвентарь VM → merge **ссылок** в `models.yaml` + `extensions.yaml` (веса не качать) |
@@ -151,7 +151,7 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
| `/assistent` | Сводка: extension + overlay + roles + memory + live API + hints + playbook | | `/assistent` | Сводка: extension + overlay + roles + memory + live API + hints + playbook |
| `/assistent/diagnose` | То же + journal по умолчанию + session store meta | | `/assistent/diagnose` | То же + journal по умолчанию + session store meta |
| `/assistent/extension` | DLL, Sqlite deps, Tab/Assets, git HEAD | | `/assistent/extension` | DLL, Sqlite deps, Tab/Assets, git HEAD |
| `/assistent/overlay` | personas на VM vs локальный `assistent-personas/` | | `/assistent/overlay` | personas/packs на VM vs локальный `assistent-extensions/` |
| `/assistent/roles` | `ollama-roles.json``/api/tags` (`default_chat`) | | `/assistent/roles` | `ollama-roles.json``/api/tags` (`default_chat`) |
| `/assistent/memory` | `assistent.sqlite` + counts | | `/assistent/memory` | `assistent.sqlite` + counts |
| `/assistent/api` | `AssistentListPersonas/Models/Memory/Chats` через туннель (или SSH) | | `/assistent/api` | `AssistentListPersonas/Models/Memory/Chats` через туннель (или SSH) |
@@ -198,7 +198,7 @@ curl -sS -X DELETE "$BASE/assistent/session/$SID"
# One-shot convenience (session + one chat + delete) # One-shot convenience (session + one chat + delete)
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":"leonid","timeout":120}' \ -d '{"message":"какие steps/cfg для turbo?","persona":"neutral","timeout":120}' \
| jq '{ok,reply,patch,trace}' | jq '{ok,reply,patch,trace}'
``` ```
+1 -1
View File
@@ -10,7 +10,7 @@
| Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена | | Civitai хост | Дефолт API `civitai.red` (полный каталог). `.com` — SFW-витрина, NSFW с неё часто 404. Ссылки `.com`/`.red`/`.green` в манифесте принимаем. 404 → один retry на второй хост. Один токен на оба домена |
| Пул GPU | Перед `up`/`flavors` сканируем `SCAN_POOLS` (дефолт `ru-6,ru-7`) по compute URL. Сегменты `ru-6a/b/c` берём из Nova `availability_zones`, не отдельными пулами. Пресет размера `FLAVOR_SIZE_PRESET` (дефолт `cheap` = 1×GPU, min vCPU/RAM). Диск уже есть — AZ диска, не дешёвый чужой сегмент. Первый запуск — дешёвый SKU в сегменте, где есть карта из `FLAVOR_PREFERENCE`. `.env` автоматом не пишем | | Пул GPU | Перед `up`/`flavors` сканируем `SCAN_POOLS` (дефолт `ru-6,ru-7`) по compute URL. Сегменты `ru-6a/b/c` берём из Nova `availability_zones`, не отдельными пулами. Пресет размера `FLAVOR_SIZE_PRESET` (дефолт `cheap` = 1×GPU, min vCPU/RAM). Диск уже есть — AZ диска, не дешёвый чужой сегмент. Первый запуск — дешёвый SKU в сегменте, где есть карта из `FLAVOR_PREFERENCE`. `.env` автоматом не пишем |
| Манифест моделей | `<repo>/models.yaml`, типы: checkpoint / lora / vae / embedding / controlnet / upscaler. В git только `models.example.yaml` | | Манифест моделей | `<repo>/models.yaml`, типы: checkpoint / lora / vae / embedding / controlnet / upscaler. В git только `models.example.yaml` |
| Расширения | `<repo>/extensions.yaml`: git-репы `swarmui``src/Extensions`, `comfy` → DLNodes. Поле `requires: none\|ollama\|any-llm` фильтрует по `LLM_RUNTIME`. Пример: swarm-assistent с `requires: ollama`. Personas: `assistent-personas/`overlay `Assistent/personas/<id>/` на `up` / `seed-personas` (не сносит UI-клоны) | | Расширения | `<repo>/extensions.yaml`: git-репы `swarmui``src/Extensions`, `comfy` → DLNodes, `assistent``Assistent/extensions`. Поле `requires: none\|ollama\|any-llm` фильтрует по `LLM_RUNTIME`. Пример: swarm-assistent с `requires: ollama`. Persona packs: также `assistent-extensions/`SFTP на `up` / `seed-personas` (не сносит UI-клоны) |
| Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) | | Autocomplete | До первого старта: скачать word-list в `Data/Autocompletions`, прописать `DefaultUser.AutoComplete.Source`. На каждом `up` сверить GitHub blob sha и обновить файл, если изменился. Дефолт: `tags/danbooru.csv` из a1111-sd-webui-tagcomplete (как в доке SwarmUI) |
| Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель | | Доступ | Браузер на туннеле; MCP переключается на облако, пока оно живо; HTTP API SwarmUI через тот же туннель |
| Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` | | Preemptible | По умолчанию всегда. Обычный сервер — только `--no-spot` |
+16 -1
View File
@@ -26,6 +26,7 @@ copy extensions.example.yaml extensions.yaml
| --- | --- | --- | | --- | --- | --- |
| `swarmui` | `/mnt/swarm_data/Extensions/<dir>``/opt/swarmui/src/Extensions/<dir>` | C# extension SwarmUI (класс `: Extension`). После рестарта подхватывается сам | | `swarmui` | `/mnt/swarm_data/Extensions/<dir>``/opt/swarmui/src/Extensions/<dir>` | C# extension SwarmUI (класс `: Extension`). После рестарта подхватывается сам |
| `comfy` | `/mnt/swarm_data/DLNodes/<dir>``…/ComfyUIBackend/DLNodes/<dir>` | Custom nodes ComfyUI (Python). `requirements.txt` ставит SwarmUI/Comfy при первом старте backend | | `comfy` | `/mnt/swarm_data/DLNodes/<dir>``…/ComfyUIBackend/DLNodes/<dir>` | Custom nodes ComfyUI (Python). `requirements.txt` ставит SwarmUI/Comfy при первом старте backend |
| `assistent` | `/mnt/swarm_data/Assistent/extensions/<dir>/` | Persona pack (полки JSON + `assistent-pack.yaml`). Без рестарта C# — Assistent читает с диска |
Пустой манифест или его нет — ничего не клонируем, SwarmUI как из коробки. Это не связано с Civitai-токеном. Пустой манифест или его нет — ничего не клонируем, SwarmUI как из коробки. Это не связано с Civitai-токеном.
@@ -42,6 +43,11 @@ swarmui:
- url: https://github.com/example/SwarmUI-SomeExt.git - url: https://github.com/example/SwarmUI-SomeExt.git
ref: main ref: main
dir: SomeExt dir: SomeExt
assistent: []
# - url: https://example.com/org/some-persona-pack.git
# ref: main
# dir: some-persona-pack
# requires: ollama
comfy: comfy:
- url: https://github.com/example/ComfyUI-Something.git - url: https://github.com/example/ComfyUI-Something.git
ref: v1.2.0 ref: v1.2.0
@@ -53,7 +59,16 @@ comfy:
| `ollama` | только при `LLM_RUNTIME=ollama` | | `ollama` | только при `LLM_RUNTIME=ollama` |
| `any-llm` | при `LLM_RUNTIME=ollama` (сейчас единственный LLM-рантайм; llama.cpp снят) | | `any-llm` | при `LLM_RUNTIME=ollama` (сейчас единственный LLM-рантайм; llama.cpp снят) |
Строки с несовпавшим `requires` пропускаются (лог), остальные ставятся как обычно. В `extensions.example.yaml` по умолчанию — **swarm-assistent** с `requires: ollama` (чат + доска Generate/Ref + persona/Cards, ветка `main`). После `seed-extensions` + рестарта SwarmUI подтягивается текущий `main` (с **0.13.1+**: `CopyLocalLockFileAssemblies` копирует `Microsoft.Data.Sqlite` + `SQLitePCLRaw*` рядом с extension DLL — иначе ListMemory/ListChats/SaveChat падают; с **0.10.11**: default chat = `default_chat` / старший тег, warm той же моделью). На `up``gpu-rent seed-personas`) сидится папка `assistent-personas/` → overlay `/mnt/swarm_data/Assistent/personas/<id>/*.json`, плюс `_base/assistant.json` (`default_persona`, опционально `num_ctx` с GPU tier). Seed **не** удаляет overlay-личности, созданные в UI. Legacy `personas.json` / yaml-prompt больше не пишутся. Строки с несовпавшим `requires` пропускаются (лог), остальные ставятся как обычно. В `extensions.example.yaml` по умолчанию — **swarm-assistent** с `requires: ollama` (чат + доска Generate/Ref + persona/Cards, ветка `main`). После `seed-extensions` + рестарта SwarmUI подтягивается текущий `main` (с **0.13.1+**: `CopyLocalLockFileAssemblies` копирует `Microsoft.Data.Sqlite` + `SQLitePCLRaw*` рядом с extension DLL — иначе ListMemory/ListChats/SaveChat падают; с **0.10.11**: default chat = `default_chat` / старший тег, warm той же моделью).
### Persona packs
Bundled в swarm-assistent: `neutral` (Нормальный), `aggressive`, `dreamer`. Дополнительные личности — git-паки:
1. **Конфиг** — секция `assistent:` в `extensions.yaml` (clone на VM в `Assistent/extensions/<dir>/`).
2. **Локально** — клоны в `assistent-extensions/<dir>/` (gitignored). На `up` / `seed-personas` / `push` уезжают SFTP **без** `.git` в тот же dest. Приватные паки можно держать только так — без URL в yaml (capture их не увидит).
Формат пака: `assistent-pack.yaml` (`kind: persona`, `id: …`) + полки JSON в корне (или `personas/<id>/` для мульти-пака). На `up` также пишется `_base/assistant.json` (`default_persona: neutral`, опционально `num_ctx` с GPU tier). Seed **не** удаляет overlay-личности, созданные в UI. Legacy `assistent-personas/` / `personas.json` больше не сидятся.
Если есть `datasets/civitai/search.jsonl`, на том же шаге пушится FTS-индекс → `/mnt/swarm_data/Assistent/civitai-examples.jsonl` ([datasets/README](../datasets/README.md)). Диагностика Assistent: Debug API `/assistent/*` ([cli.md](cli.md)). Если есть `datasets/civitai/search.jsonl`, на том же шаге пушится FTS-индекс → `/mnt/swarm_data/Assistent/civitai-examples.jsonl` ([datasets/README](../datasets/README.md)). Диагностика Assistent: Debug API `/assistent/*` ([cli.md](cli.md)).
+1 -1
View File
@@ -113,7 +113,7 @@ Unit `gpu-rent-ollama` читает `/mnt/swarm_data/.gpu-rent-gpu.json`:
\*Flash на Ampere+ (compute ≥ 8.0). `NUM_PARALLEL=2`, `MAX_LOADED_MODELS=2` (chat VL + memory embed). Memory models use `use: memory` and a CPU Modelfile (`num_gpu 0`) so embed does not steal VRAM from the chat model. Ollama default `num_ctx` is 4096; we set `OLLAMA_CONTEXT_LENGTH` so Assistent + vision fits. Mid/high keep-alive is **5m** because Assistent parks the chat model before Generate (`AssistentParkLlm`) and warms after. Env: `/mnt/swarm_data/.gpu-rent-ollama.env`. \*Flash на Ampere+ (compute ≥ 8.0). `NUM_PARALLEL=2`, `MAX_LOADED_MODELS=2` (chat VL + memory embed). Memory models use `use: memory` and a CPU Modelfile (`num_gpu 0`) so embed does not steal VRAM from the chat model. Ollama default `num_ctx` is 4096; we set `OLLAMA_CONTEXT_LENGTH` so Assistent + vision fits. Mid/high keep-alive is **5m** because Assistent parks the chat model before Generate (`AssistentParkLlm`) and warms after. Env: `/mnt/swarm_data/.gpu-rent-ollama.env`.
Personas: laptop `assistent-personas/` → on `up` / `seed-personas` overlay folders under `/mnt/swarm_data/Assistent/personas/<id>/` (+ `_base/assistant.json` with `default_persona` / `num_ctx`). Shelves are short JSON files (not a long prompt). See [extensions.md](extensions.md). Optional FTS examples: [datasets/README](../datasets/README.md) (`search.jsonl``Assistent/civitai-examples.jsonl`). Journal: `gpu-rent logs -u ollama`. Personas: bundled `neutral` / `aggressive` / `dreamer` in swarm-assistent; extra packs via `extensions.yaml` `assistent:` or laptop `assistent-extensions/` `Assistent/extensions/` on `up` / `seed-personas` / `push` (+ `_base/assistant.json` with `default_persona` / `num_ctx`). See [extensions.md](extensions.md). Optional FTS examples: [datasets/README](../datasets/README.md) (`search.jsonl``Assistent/civitai-examples.jsonl`). Journal: `gpu-rent logs -u ollama`.
`ollama-models.yaml` entries: `ollama-models.yaml` entries:
+5 -5
View File
@@ -19,18 +19,18 @@
Веса, картинки, воркфлоу в git не коммитить (`.gitignore` + `.gitkeep`). Веса, картинки, воркфлоу в git не коммитить (`.gitignore` + `.gitkeep`).
## Seed overlay (не `gpu-rent push`) ## Seed overlay (не только `gpu-rent push`)
Другой механизм — не SHA256-синк четырёх деревьев выше: Другой механизм — не SHA256-синк Models/Wildcards/workflows:
| Локально | На VM | Когда | | Локально | На VM | Когда |
| --- | --- | --- | | --- | --- | --- |
| `assistent-personas/` | `/mnt/swarm_data/Assistent/personas/<id>/` | `up` / `seed-personas`; см. [extensions.md](extensions.md) | | `assistent-extensions/<pack>/` | `/mnt/swarm_data/Assistent/extensions/<pack>/` | `up` / `seed-personas` / `push` (без `.git`); см. [extensions.md](extensions.md) |
| `datasets/civitai/search.jsonl` | `Assistent/civitai-examples.jsonl` | `up` / `seed-personas`, если файл есть; см. [datasets/README](../datasets/README.md) | | `datasets/civitai/search.jsonl` | `Assistent/civitai-examples.jsonl` | `up` / `seed-personas`, если файл есть; см. [datasets/README](../datasets/README.md) |
## Push (Models / Wildcards / CustomWorkflows) ## Push (Models / Wildcards / CustomWorkflows / packs)
Как у моделей: нет содержимого кроме README/`.gitkeep` — skip. Новый или изменённый файл — SFTP. Метадата рядом с весами едет парой. Команда на все три дерева: `gpu-rent push`. Как у моделей: нет содержимого кроме README/`.gitkeep` — skip. Новый или изменённый файл — SFTP. Метадата рядом с весами едет парой. Команда: `gpu-rent push` (также сидит persona-паки).
Wildcards: `.txt` (одна строка — один вариант), как в доке SwarmUI `Data/Wildcards`. Wildcards: `.txt` (одна строка — один вариант), как в доке SwarmUI `Data/Wildcards`.
+1 -1
View File
@@ -181,7 +181,7 @@ copy models.example.yaml models.yaml
Опционально позже (не нужно для первой картинки): Опционально позже (не нужно для первой картинки):
- `SELECTEL_API_TOKEN` (X-Token панели) + `gpu-rent watchdog install` — toast по балансу ₽; см. [cli.md](cli.md) / [decisions.md](decisions.md). - `SELECTEL_API_TOKEN` (X-Token панели) + `gpu-rent watchdog install` — toast по балансу ₽; см. [cli.md](cli.md) / [decisions.md](decisions.md).
- `copy assistent-personas.example assistent-personas` overlay personas; см. [extensions.md](extensions.md). - `copy assistent-extensions.example assistent-extensions` — persona packs; см. [extensions.md](extensions.md).
- `.\civitai-dataset.bat` — scrape метаданных Krea2 → Assistent FTS; см. [datasets/README](../datasets/README.md). - `.\civitai-dataset.bat` — scrape метаданных Krea2 → Assistent FTS; см. [datasets/README](../datasets/README.md).
--- ---
+10 -1
View File
@@ -2,6 +2,7 @@
# Empty/missing file → no extra extensions, stock SwarmUI. # Empty/missing file → no extra extensions, stock SwarmUI.
# swarmui = C# repos cloned to src/Extensions # swarmui = C# repos cloned to src/Extensions
# comfy = Python custom nodes cloned to ComfyUI DLNodes # comfy = Python custom nodes cloned to ComfyUI DLNodes
# assistent = persona packs cloned to Assistent/extensions (requires ollama)
# requires: none (default) | ollama | any-llm # requires: none (default) | ollama | any-llm
# — clone only when LLM_RUNTIME matches (ollama / either LLM) # — clone only when LLM_RUNTIME matches (ollama / either LLM)
@@ -14,6 +15,14 @@ swarmui:
# ref: main # ref: main
# dir: MyExt # dir: MyExt
# Persona packs (git): optional. Or clone packs into assistent-extensions/ locally
# (SFTP on up / seed-personas / push — no URL needed in this file).
assistent: []
# - url: https://example.com/org/some-persona-pack.git
# ref: main
# dir: some-persona-pack
# requires: ollama
comfy: [] comfy: []
# - url: https://github.com/org/ComfyUI-CustomNodes.git # - url: https://github.com/example/ComfyUI-CustomNodes.git
# ref: v1.0.0 # ref: v1.0.0
+3 -3
View File
@@ -331,11 +331,11 @@ def merge_extensions_yaml(
dry_run: bool, dry_run: bool,
) -> tuple[list[ExtCaptureItem], list[ExtCaptureItem], list[str]]: ) -> tuple[list[ExtCaptureItem], list[ExtCaptureItem], list[str]]:
"""Return (added, updated, skipped). Same dir + different URL → update url/ref.""" """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(): if path.is_file():
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if isinstance(raw, dict): if isinstance(raw, dict):
for kind in ("swarmui", "comfy"): for kind in ("swarmui", "comfy", "assistent"):
items = raw.get(kind) or [] items = raw.get(kind) or []
if isinstance(items, list): if isinstance(items, list):
for it in items: for it in items:
@@ -701,7 +701,7 @@ def capture_extensions(
report.ext_unknown.append(f"{raw.get('kind')}/{name} ({reason})") report.ext_unknown.append(f"{raw.get('kind')}/{name} ({reason})")
continue continue
kind = str(raw.get("kind") or "") kind = str(raw.get("kind") or "")
if kind not in ("swarmui", "comfy"): if kind not in ("swarmui", "comfy", "assistent"):
continue continue
resolved.append( resolved.append(
ExtCaptureItem( ExtCaptureItem(
+7 -2
View File
@@ -923,13 +923,18 @@ def seed_models() -> None:
@app.command("push") @app.command("push")
def push_all() -> None: def push_all() -> None:
"""SFTP Models + Wildcards + CustomWorkflows.""" """SFTP Models + Wildcards + CustomWorkflows + assistent-extensions."""
try: try:
from gpu_rent.paths import assistent_extensions_dir
from gpu_rent.provision import seed_assistent_personas
cfg, host = _live() cfg, host = _live()
push_tree(cfg, host, cfg.local_models_dir, "/mnt/swarm_data/Models", log, models=True) 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_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) 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: except GpuRentError as exc:
_die(exc) _die(exc)
@@ -985,7 +990,7 @@ def seed_extensions_cmd() -> None:
@app.command("seed-personas") @app.command("seed-personas")
def seed_personas_cmd() -> None: def seed_personas_cmd() -> None:
"""Push assistent-personas/ → VM Assistent overlay (без полного up).""" """Push assistent-extensions/ → VM Assistent/extensions (+ _base)."""
try: try:
from gpu_rent.provision import seed_assistent_personas 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()): for d in sorted(personas_root.iterdir()):
if d.is_dir(): if d.is_dir():
persona_ids.append(d.name) 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_json = {}
base_path = overlay / "_base" / "assistant.json" base_path = overlay / "_base" / "assistant.json"
if base_path.is_file(): if base_path.is_file():
@@ -186,6 +192,7 @@ print(json.dumps({
"exists": overlay.is_dir(), "exists": overlay.is_dir(),
"entries": sorted(x.name for x in overlay.iterdir())[:50] if overlay.is_dir() else [], "entries": sorted(x.name for x in overlay.iterdir())[:50] if overlay.is_dir() else [],
"persona_ids": persona_ids, "persona_ids": persona_ids,
"pack_dirs": pack_dirs,
"default_persona": base_json.get("default_persona") if isinstance(base_json, dict) else None, "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, "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, "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"): if not fs.get("ok"):
return fs return fs
overlay = fs.get("overlay") or {} overlay = fs.get("overlay") or {}
local_ids: list[str] = [] local_packs: list[str] = []
try: 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(): if pdir.is_dir():
local_ids = sorted( local_packs = sorted(
x.name for x in pdir.iterdir() if x.is_dir() and not x.name.startswith("_") x.name for x in pdir.iterdir() if x.is_dir() and not x.name.startswith(".")
) )
except Exception: except Exception:
pass pass
remote_ids = list(overlay.get("persona_ids") or []) remote_packs = list(overlay.get("pack_dirs") or [])
missing_on_vm = sorted(set(local_ids) - set(remote_ids)) missing_on_vm = sorted(set(local_packs) - set(remote_packs))
hints: list[str] = [] hints: list[str] = []
if local_ids and missing_on_vm: if local_packs and missing_on_vm:
hints.append( 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"): if not overlay.get("exists"):
hints.append("нет /mnt/swarm_data/Assistent — seed ещё не писал overlay (bundled personas ок)") hints.append("нет /mnt/swarm_data/Assistent — seed ещё не писал overlay (bundled personas ок)")
return { return {
"ok": True, "ok": True,
"overlay": overlay, "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, "missing_on_vm": missing_on_vm,
"hints": hints, "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]: def _local_assistent_bits(cfg: Config) -> dict[str, Any]:
local: dict[str, Any] = {} local: dict[str, Any] = {}
try: 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()
local["personas_dir"] = { local["extensions_dir"] = {
"path": str(pdir), "path": str(pdir),
"exists": pdir.is_dir(), "exists": pdir.is_dir(),
"entries": sorted(x.name for x in pdir.iterdir())[:40] if pdir.is_dir() else [], "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: except Exception as exc:
local["personas_dir"] = {"error": str(exc)[:120]} local["extensions_dir"] = {"error": str(exc)[:120]}
try: try:
from gpu_rent.manifests import parse_extensions, repo_dirname from gpu_rent.manifests import parse_extensions, repo_dirname
@@ -1051,9 +1060,11 @@ def _local_assistent_bits(cfg: Config) -> dict[str, Any]:
has = any( has = any(
"assistent" in repo_dirname(r).lower() or "assistent" in (r.url or "").lower() "assistent" in repo_dirname(r).lower() or "assistent" in (r.url or "").lower()
for r in repos for r in repos
if r.kind == "swarmui"
) )
local["extensions_yaml"] = { local["extensions_yaml"] = {
"has_swarm_assistent": has, "has_swarm_assistent": has,
"assistent_packs": sum(1 for r in repos if r.kind == "assistent"),
"manifest": str(cfg.extensions_manifest), "manifest": str(cfg.extensions_manifest),
} }
except Exception as exc: except Exception as exc:
+8 -3
View File
@@ -127,9 +127,9 @@ def parse_extensions(path: Path) -> list[GitRepo]:
if not data: if not data:
return [] return []
if not isinstance(data, dict): if not isinstance(data, dict):
raise ConfigError(f"{path}: корень swarmui: / comfy:") raise ConfigError(f"{path}: корень swarmui: / comfy: / assistent:")
repos: list[GitRepo] = [] repos: list[GitRepo] = []
for kind in ("swarmui", "comfy"): for kind in ("swarmui", "comfy", "assistent"):
items = data.get(kind) or [] items = data.get(kind) or []
if not items: if not items:
continue continue
@@ -184,5 +184,10 @@ def is_commit_sha(ref: str) -> bool:
def remote_root_for(repo: GitRepo) -> str: 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)}" return f"{base}/{repo_dirname(repo)}"
+12 -2
View File
@@ -48,12 +48,22 @@ def models_manifest_path() -> Path:
return app_root() / "models.yaml" 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: 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" return app_root() / "assistent-personas"
def assistent_personas_example_dir() -> Path: def assistent_personas_example_dir() -> Path:
"""Deprecated example — prefer assistent_extensions_example_dir()."""
return app_root() / "assistent-personas.example" return app_root() / "assistent-personas.example"
@@ -63,7 +73,7 @@ def assistent_personas_manifest_path() -> Path:
def assistent_personas_example_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" return app_root() / "assistent-personas.example.yaml"
+3
View File
@@ -14,6 +14,9 @@ def is_skipped(path: Path) -> bool:
name = path.name name = path.name
if name in SKIP_NAMES or name.startswith("."): if name in SKIP_NAMES or name.startswith("."):
return True return True
# Never sync git metadata (persona packs push shelves only).
if any(part == ".git" for part in path.parts):
return True
return False 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: 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 Each subdirectory of assistent-extensions/ is SFTP'd to
(default_persona + optional num_ctx). Does not delete overlay personas missing Assistent/extensions/<dirname>/ (``.git`` skipped). Overlay UI clones under
from the laptop (VM-created clones stay). Does not clobber exact.controls. Assistent/personas/ are never deleted. Legacy assistent-personas/ shelf seed
Legacy assistent-personas.yaml is no longer read. is no longer read.
""" """
from gpu_rent.paths import ( from gpu_rent.paths import (
assistent_extensions_dir,
assistent_personas_dir, assistent_personas_dir,
assistent_personas_example_dir,
assistent_personas_manifest_path, assistent_personas_manifest_path,
) )
from gpu_rent.sync_files import push_tree
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
remote_dir = f"{DATA}/Assistent" remote_dir = f"{DATA}/Assistent"
run_ssh( run_ssh(
cfg, cfg,
host, 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, 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" 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} assistant_overlay: dict = {"default_persona": default_id}
num_ctx = _overlay_num_ctx(cfg, host) num_ctx = _overlay_num_ctx(cfg, host)
if num_ctx: if num_ctx:
@@ -1005,7 +972,10 @@ def seed_assistent_personas(cfg: Config, host: str, log: Log) -> None:
check=False, check=False,
) )
ctx_note = f", num_ctx={num_ctx}" if num_ctx else "" 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) seed_civitai_examples(cfg, host, log)
@@ -1623,7 +1593,7 @@ def provision_vm(
try: try:
seed_assistent_personas(cfg, host, log) seed_assistent_personas(cfg, host, log)
except Exception as exc: 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_models_dir, f"{DATA}/Models", log, models=True)
push_tree( push_tree(
cfg, host, cfg.local_wildcards_dir, f"{DATA}/Data/Wildcards", log, models=False 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 = ( EXTRA_ROOTS = (
Path("/mnt/swarm_data/Extensions"), Path("/mnt/swarm_data/Extensions"),
Path("/mnt/swarm_data/DLNodes"), Path("/mnt/swarm_data/DLNodes"),
Path("/mnt/swarm_data/Assistent/extensions"),
) )
+1
View File
@@ -30,6 +30,7 @@ FOLDER_TO_KIND = {
EXT_ROOTS = ( EXT_ROOTS = (
("swarmui", DATA / "Extensions"), ("swarmui", DATA / "Extensions"),
("comfy", DATA / "DLNodes"), ("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 ( from gpu_rent.paths import (
app_root, app_root,
assistent_personas_dir, assistent_extensions_dir,
assistent_personas_example_dir, assistent_extensions_example_dir,
env_path, env_path,
extensions_manifest_path, extensions_manifest_path,
models_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 / "env.example", env_path(), ".env", log)
_copy_if_missing(root / "models.example.yaml", models_manifest_path(), "models.yaml", log) _copy_if_missing(root / "models.example.yaml", models_manifest_path(), "models.yaml", log)
_copy_tree_if_missing( _copy_tree_if_missing(
assistent_personas_example_dir(), assistent_extensions_example_dir(),
assistent_personas_dir(), assistent_extensions_dir(),
"assistent-personas/", "assistent-extensions/",
log, log,
) )
_copy_if_missing( _copy_if_missing(
+52 -62
View File
@@ -1,4 +1,4 @@
"""Unit tests for assistent personas overlay seed helpers.""" """Unit tests for assistent persona-pack seed helpers."""
from __future__ import annotations from __future__ import annotations
@@ -6,35 +6,29 @@ import json
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
from gpu_rent.provision import _safe_persona_id, count_wanted_models_on_vm, seed_assistent_personas from gpu_rent.provision import _safe_persona_id, seed_assistent_personas
def test_safe_persona_id(): def test_safe_persona_id():
assert _safe_persona_id("neutral") == "neutral" assert _safe_persona_id("neutral") == "neutral"
assert _safe_persona_id("lewd_v2") == "lewd_v2" assert _safe_persona_id("dreamer_v2") == "dreamer_v2"
assert _safe_persona_id("../x") is None assert _safe_persona_id("../x") is None
assert _safe_persona_id("a/b") is None assert _safe_persona_id("a/b") is None
assert _safe_persona_id("") is None assert _safe_persona_id("") is None
def test_seed_assistent_personas_writes_overlay(tmp_path: Path, monkeypatch): def test_seed_assistent_personas_pushes_packs(tmp_path: Path, monkeypatch):
root = tmp_path / "assistent-personas" root = tmp_path / "assistent-extensions"
(root / "_base").mkdir(parents=True) pack = root / "sample-pack"
(root / "personas" / "cinema").mkdir(parents=True) pack.mkdir(parents=True)
(root / "_base" / "assistant.json").write_text( (pack / "assistent-pack.yaml").write_text("kind: persona\nid: sample\n", encoding="utf-8")
json.dumps({"default_persona": "cinema"}), encoding="utf-8" (pack / "persona.json").write_text(
) json.dumps({"title": "Sample", "accent": "#7aa2f7"}), encoding="utf-8"
(root / "personas" / "cinema" / "persona.json").write_text(
json.dumps({"title": "Кино", "accent": "#7aa2f7"}), encoding="utf-8"
)
(root / "personas" / "cinema" / "bio.json").write_text(
json.dumps({"role": "DP"}), encoding="utf-8"
)
(root / "personas" / "cinema" / "humor.json").write_text(
json.dumps({"frequency": "rare"}), encoding="utf-8"
) )
(pack / "voice.json").write_text(json.dumps({"tone": ["calm"]}), encoding="utf-8")
puts: dict[str, str] = {} puts: dict[str, str] = {}
pushed: list[tuple[str, str]] = []
ssh_cmds: list[str] = [] ssh_cmds: list[str] = []
def fake_put(cfg, host, remote, text, mode=0o644): def fake_put(cfg, host, remote, text, mode=0o644):
@@ -46,76 +40,72 @@ def test_seed_assistent_personas_writes_overlay(tmp_path: Path, monkeypatch):
return json.dumps({"vram_mib": 24576, "name": "4090", "compute_cap": "8.9"}) return json.dumps({"vram_mib": 24576, "name": "4090", "compute_cap": "8.9"})
return "" return ""
def fake_push(cfg, host, local_root, remote_root, log, *, models):
pushed.append((str(local_root), remote_root))
log(f"push {Path(local_root).name}")
return 2
monkeypatch.setattr("gpu_rent.provision.put_text", fake_put) monkeypatch.setattr("gpu_rent.provision.put_text", fake_put)
monkeypatch.setattr("gpu_rent.provision.run_ssh", fake_ssh) monkeypatch.setattr("gpu_rent.provision.run_ssh", fake_ssh)
monkeypatch.setattr("gpu_rent.paths.assistent_personas_dir", lambda: root) monkeypatch.setattr("gpu_rent.sync_files.push_tree", fake_push)
monkeypatch.setattr("gpu_rent.paths.assistent_extensions_dir", lambda: root)
monkeypatch.setattr( monkeypatch.setattr(
"gpu_rent.paths.assistent_personas_example_dir", lambda: tmp_path / "missing" "gpu_rent.paths.assistent_personas_dir", lambda: tmp_path / "no-personas"
) )
monkeypatch.setattr( monkeypatch.setattr(
"gpu_rent.paths.assistent_personas_manifest_path", lambda: tmp_path / "no.yaml" "gpu_rent.paths.assistent_personas_manifest_path", lambda: tmp_path / "no.yaml"
) )
monkeypatch.setattr(
"gpu_rent.provision.seed_civitai_examples", lambda *a, **k: None
)
cfg = MagicMock() cfg = MagicMock()
cfg.assistent_personas_dir = str(root) cfg.assistent_extensions_dir = str(root)
logs: list[str] = [] logs: list[str] = []
seed_assistent_personas(cfg, "1.2.3.4", logs.append) seed_assistent_personas(cfg, "1.2.3.4", logs.append)
assert any(p.endswith("personas/cinema/persona.json") for p in puts) assert len(pushed) == 1
assert any(p.endswith("personas/cinema/bio.json") for p in puts) assert pushed[0][1].endswith("/Assistent/extensions/sample-pack")
assert any(p.endswith("personas/cinema/humor.json") for p in puts)
assert not any(p.endswith("extra.md") for p in puts)
assert "/mnt/swarm_data/Assistent/_base/assistant.json" in puts assert "/mnt/swarm_data/Assistent/_base/assistant.json" in puts
asst = json.loads(puts["/mnt/swarm_data/Assistent/_base/assistant.json"]) asst = json.loads(puts["/mnt/swarm_data/Assistent/_base/assistant.json"])
assert asst["default_persona"] == "cinema" assert asst["default_persona"] == "neutral"
assert asst["num_ctx"] == 16384 assert asst["num_ctx"] == 16384
assert any("rm -f" in c and "personas.json" in c for c in ssh_cmds) assert any("rm -f" in c and "personas.json" in c for c in ssh_cmds)
assert any("overlay personas/1" in m for m in logs) assert any("packs/1" in m for m in logs)
def test_seed_skips_exact_if_remote_exists(tmp_path: Path, monkeypatch): def test_seed_skips_empty_pack_dir(tmp_path: Path, monkeypatch):
root = tmp_path / "assistent-personas" root = tmp_path / "assistent-extensions"
(root / "personas" / "leonid").mkdir(parents=True) (root / "empty").mkdir(parents=True)
(root / "personas" / "leonid" / "persona.json").write_text(
'{"title":"L"}', encoding="utf-8"
)
(root / "personas" / "leonid" / "exact.json").write_text(
'{"controls":{"preference_bias":0.9}}', encoding="utf-8"
)
puts: dict[str, str] = {}
def fake_put(cfg, host, remote, text, mode=0o644): pushed: list[str] = []
puts[remote] = text
def fake_ssh(cfg, host, cmd, check=False, timeout=60): def fake_push(cfg, host, local_root, remote_root, log, *, models):
if "exact.json" in cmd and "test -f" in cmd: pushed.append(remote_root)
return "yes" return 0
if ".gpu-rent-gpu.json" in cmd:
return "{}"
return ""
monkeypatch.setattr("gpu_rent.provision.put_text", fake_put) monkeypatch.setattr("gpu_rent.provision.put_text", lambda *a, **k: None)
monkeypatch.setattr("gpu_rent.provision.run_ssh", fake_ssh)
monkeypatch.setattr("gpu_rent.paths.assistent_personas_dir", lambda: root)
monkeypatch.setattr( monkeypatch.setattr(
"gpu_rent.paths.assistent_personas_example_dir", lambda: tmp_path / "missing" "gpu_rent.provision.run_ssh",
lambda *a, **k: json.dumps({"vram_mib": 24576}) if ".gpu-rent-gpu" in str(a) else "",
)
monkeypatch.setattr("gpu_rent.sync_files.push_tree", fake_push)
monkeypatch.setattr("gpu_rent.paths.assistent_extensions_dir", lambda: root)
monkeypatch.setattr(
"gpu_rent.paths.assistent_personas_dir", lambda: tmp_path / "no-personas"
) )
monkeypatch.setattr( monkeypatch.setattr(
"gpu_rent.paths.assistent_personas_manifest_path", lambda: tmp_path / "no.yaml" "gpu_rent.paths.assistent_personas_manifest_path", lambda: tmp_path / "no.yaml"
) )
monkeypatch.setattr(
"gpu_rent.provision.seed_civitai_examples", lambda *a, **k: None
)
cfg = MagicMock() cfg = MagicMock()
cfg.assistent_personas_dir = str(root) cfg.assistent_extensions_dir = str(root)
seed_assistent_personas(cfg, "h", lambda *_: None) logs: list[str] = []
assert not any(p.endswith("exact.json") for p in puts) seed_assistent_personas(cfg, "1.2.3.4", logs.append)
assert any(p.endswith("persona.json") for p in puts)
assert pushed == []
def test_count_wanted_models(monkeypatch): assert any("нет assistent-pack.yaml" in m for m in logs)
raw = "lora:\n - url: https://civitai.red/x?modelVersionId=1\n title: A\n" assert any("packs/0" in m for m in logs)
monkeypatch.setattr(
"gpu_rent.provision.run_ssh",
lambda *a, **k: raw,
)
assert count_wanted_models_on_vm(MagicMock(), "h") == 1
+4 -4
View File
@@ -221,7 +221,7 @@ def test_chat_eval_mocked_http(monkeypatch, tmp_path):
if url.endswith("/API/AssistentGetConfig"): if url.endswith("/API/AssistentGetConfig"):
return True, { return True, {
"success": True, "success": True,
"persona": "leonid", "persona": "neutral",
"exact": { "exact": {
"profiles": {"turbo": {"steps": 8, "cfg": 1, "sigma_shift": 1.15}} "profiles": {"turbo": {"steps": 8, "cfg": 1, "sigma_shift": 1.15}}
}, },
@@ -245,14 +245,14 @@ def test_chat_eval_mocked_http(monkeypatch, tmp_path):
out = debug_assistent.run_assistent_chat_eval( out = debug_assistent.run_assistent_chat_eval(
cfg, cfg,
message="какой checkpoint?", message="какой checkpoint?",
persona="leonid", persona="neutral",
timeout=60, timeout=60,
) )
assert out["ok"] is True assert out["ok"] is True
assert out["via"] == "local" assert out["via"] == "local"
assert out["model"] == "qwen3-vl:8b" assert out["model"] == "qwen3-vl:8b"
assert out["preferred"] == "qwen3-vl:8b" assert out["preferred"] == "qwen3-vl:8b"
assert out["persona"] == "leonid" assert out["persona"] == "neutral"
assert out["patch"]["steps"] == 4 assert out["patch"]["steps"] == 4
assert out["patch"]["cfg"] == 1 assert out["patch"]["cfg"] == 1
assert out["patch"]["aspect"] == "3:4" assert out["patch"]["aspect"] == "3:4"
@@ -317,7 +317,7 @@ def test_assistent_compact_and_roles_local(monkeypatch, tmp_path):
], ],
"overlay": { "overlay": {
"exists": True, "exists": True,
"persona_ids": ["leonid"], "persona_ids": ["neutral"],
"roles_present": True, "roles_present": True,
"roles": { "roles": {
"chat": ["qwen3-vl:8b"], "chat": ["qwen3-vl:8b"],
+27 -1
View File
@@ -3,7 +3,12 @@ from pathlib import Path
import pytest import pytest
from gpu_rent.errors import ConfigError from gpu_rent.errors import ConfigError
from gpu_rent.manifests import parse_extensions, parse_models, repo_matches_runtime from gpu_rent.manifests import (
parse_extensions,
parse_models,
remote_root_for,
repo_matches_runtime,
)
def test_models_skips_version_zero(tmp_path: Path): def test_models_skips_version_zero(tmp_path: Path):
@@ -77,3 +82,24 @@ def test_extensions_requires_invalid(tmp_path: Path):
) )
with pytest.raises(ConfigError, match="requires"): with pytest.raises(ConfigError, match="requires"):
parse_extensions(path) parse_extensions(path)
def test_extensions_assistent_kind(tmp_path: Path):
path = tmp_path / "extensions.yaml"
path.write_text(
"assistent:\n"
" - url: https://example.com/org/sample-pack.git\n"
" ref: main\n"
" dir: sample-pack\n"
" requires: ollama\n",
encoding="utf-8",
)
repos = parse_extensions(path)
assert len(repos) == 1
assert repos[0].kind == "assistent"
assert repos[0].directory == "sample-pack"
assert remote_root_for(repos[0]) == (
"/mnt/swarm_data/Assistent/extensions/sample-pack"
)
assert repo_matches_runtime(repos[0], "ollama")
assert not repo_matches_runtime(repos[0], "none")