Version 1 checkpoint
This commit is contained in:
@@ -61,7 +61,7 @@ For non-Ubuntu WSL distros, install the CUDA toolkit manually before running the
|
||||
- The host-side install path assumes modern local tooling, but TransformerLab itself is provisioned from a pinned classic single-user layout.
|
||||
- TransformerLab is intentionally pinned to the older single-user `v0.28.2` release because newer upstream releases changed the project structure and behavior in ways that break this courseware.
|
||||
- This project does not rely on TransformerLab's upstream `install.sh`; the Ansible role provisions the pinned release directly so web assets, env layout, and runtime behavior stay reproducible.
|
||||
- The scripts do not patch TransformerLab plugins or preserve the VM's special-case fixes.
|
||||
- The courseware repairs installed TransformerLab Fastchat plugin manifests so Fastchat-gated features such as Model Architecture and Visualize Logprobs stay available on pinned installs.
|
||||
- No Ollama models are pulled during `./labctl up`; students pull models manually as part of the courseware.
|
||||
- WhiteRabbitNeo GGUFs are no longer pulled during `./labctl up`. After base setup, run `state/lab2/download_whiterabbitneo-gguf.sh` to fetch only the `BF16`, `Q8_0`, `Q4_K_M`, and `Q2_K` files from `bartowski/WhiteRabbitNeo_WhiteRabbitNeo-V3-7B-GGUF` and register local Ollama models `WhiteRabbitNeo`, `WhiteRabbitNeo-BF16`, `WhiteRabbitNeo-Q8`, `WhiteRabbitNeo-Q4`, and `WhiteRabbitNeo-Q2`.
|
||||
- TransformerLab and Unsloth homes are redirected into this project's `state/` tree via symlinks.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[defaults]
|
||||
inventory = inventory/localhost.yml
|
||||
inventory = inventory
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
interpreter_python = auto_silent
|
||||
|
||||
@@ -11,7 +11,9 @@ courseware_apps_dir: "{{ courseware_state_dir }}/apps"
|
||||
courseware_downloads_dir: "{{ courseware_state_dir }}/downloads"
|
||||
courseware_lab2_dir: "{{ courseware_state_dir }}/lab2"
|
||||
courseware_lab6_dir: "{{ courseware_state_dir }}/lab6"
|
||||
courseware_transformerlab_home: "{{ courseware_state_dir }}/transformerlab-home"
|
||||
courseware_transformerlab_legacy_home: "{{ courseware_state_dir }}/transformerlab-home"
|
||||
courseware_safe_homes_dir: "{{ lookup('env', 'HOME') }}/.local/share/local-lab-deployment"
|
||||
courseware_transformerlab_home: "{{ (courseware_safe_homes_dir ~ '/transformerlab-home') if ' ' in courseware_root else courseware_transformerlab_legacy_home }}"
|
||||
courseware_unsloth_home: "{{ courseware_state_dir }}/unsloth-home"
|
||||
courseware_ollama_models_dir: "{{ courseware_models_dir }}/ollama"
|
||||
courseware_node_runtime_dir: "{{ courseware_tools_dir }}/node-runtime"
|
||||
@@ -32,8 +34,30 @@ courseware_ports:
|
||||
promptfoo: 15500
|
||||
wiki: 80
|
||||
|
||||
courseware_transformerlab_install_mode: "single-user-pinned"
|
||||
courseware_transformerlab_version: "v0.28.2"
|
||||
courseware_transformerlab_version_dir: "{{ courseware_transformerlab_version | regex_replace('^v', '') }}"
|
||||
courseware_transformerlab_source_archive: "{{ courseware_downloads_dir }}/transformerlab-app-{{ courseware_transformerlab_version_dir }}.tar.gz"
|
||||
courseware_transformerlab_web_archive: "{{ courseware_downloads_dir }}/transformerlab-web-{{ courseware_transformerlab_version_dir }}.tar.gz"
|
||||
courseware_transformerlab_miniforge_installer: "{{ courseware_downloads_dir }}/transformerlab-miniforge-installer.sh"
|
||||
courseware_transformerlab_default_user_email: "student@zuccaro.me"
|
||||
courseware_transformerlab_default_user_password: "student"
|
||||
courseware_transformerlab_default_user_first_name: "Student"
|
||||
courseware_transformerlab_default_user_last_name: ""
|
||||
courseware_transformerlab_required_loader_plugins:
|
||||
- "fastchat_server"
|
||||
courseware_transformerlab_required_supports_fastchat:
|
||||
- "chat"
|
||||
- "completion"
|
||||
- "visualize_model"
|
||||
- "model_layers"
|
||||
- "rag"
|
||||
- "tools"
|
||||
- "template"
|
||||
- "embeddings"
|
||||
- "tokenize"
|
||||
- "logprobs"
|
||||
- "batched"
|
||||
courseware_llama_cpp_commit: "51fa458a92d6a3f305f8fd76fc8f702e3e87ddb5"
|
||||
courseware_chunkviz_commit: "a891eacafda1f28a12373ad3b00102e68f07c57f"
|
||||
courseware_promptfoo_version: "0.119.0"
|
||||
|
||||
@@ -192,6 +192,19 @@
|
||||
or courseware_down_transformerlab_marker.stat.exists
|
||||
failed_when: false
|
||||
|
||||
- name: Remove managed TransformerLab home
|
||||
file:
|
||||
path: "{{ courseware_transformerlab_home }}"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove legacy managed TransformerLab home
|
||||
file:
|
||||
path: "{{ courseware_transformerlab_legacy_home }}"
|
||||
state: absent
|
||||
when: courseware_transformerlab_legacy_home != courseware_transformerlab_home
|
||||
failed_when: false
|
||||
|
||||
- name: Remove managed Unsloth path
|
||||
file:
|
||||
path: "{{ ansible_env.HOME }}/.unsloth"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- "{{ courseware_apps_dir }}"
|
||||
- "{{ courseware_downloads_dir }}"
|
||||
- "{{ courseware_lab2_dir }}"
|
||||
- "{{ courseware_safe_homes_dir }}"
|
||||
- "{{ courseware_transformerlab_home }}"
|
||||
- "{{ courseware_unsloth_home }}"
|
||||
- "{{ courseware_ollama_models_dir }}"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- name: Create Open WebUI virtual environment
|
||||
command:
|
||||
argv:
|
||||
- "{{ courseware_python_bin }}"
|
||||
- "{{ courseware_transformerlab_home }}/envs/transformerlab/bin/python"
|
||||
- -m
|
||||
- venv
|
||||
- "{{ courseware_venvs_dir }}/open-webui"
|
||||
@@ -39,7 +39,7 @@
|
||||
- name: Create Embedding Atlas virtual environment
|
||||
command:
|
||||
argv:
|
||||
- "{{ courseware_python_bin }}"
|
||||
- "{{ courseware_transformerlab_home }}/envs/transformerlab/bin/python"
|
||||
- -m
|
||||
- venv
|
||||
- "{{ courseware_venvs_dir }}/embedding-atlas"
|
||||
|
||||
@@ -1,139 +1,290 @@
|
||||
- name: Bootstrap TransformerLab release files
|
||||
- name: Require pinned single-user TransformerLab install mode
|
||||
assert:
|
||||
that:
|
||||
- courseware_transformerlab_install_mode == "single-user-pinned"
|
||||
fail_msg: "This courseware only supports the pinned single-user TransformerLab install mode."
|
||||
|
||||
- name: Check pinned TransformerLab source tree
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
register: courseware_transformerlab_src_install
|
||||
|
||||
- name: Check pinned TransformerLab version file
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/src/LATEST_VERSION"
|
||||
register: courseware_transformerlab_src_version
|
||||
|
||||
- name: Check pinned TransformerLab web app
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/webapp/index.html"
|
||||
register: courseware_transformerlab_web_index
|
||||
|
||||
- name: Check pinned TransformerLab conda runtime
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/miniforge3/bin/conda"
|
||||
register: courseware_transformerlab_conda
|
||||
|
||||
- name: Check pinned TransformerLab Python runtime
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/envs/transformerlab/bin/python"
|
||||
register: courseware_transformerlab_env_python
|
||||
|
||||
- name: Check pinned TransformerLab install marker
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/.install_complete"
|
||||
register: courseware_transformerlab_install_marker
|
||||
|
||||
- name: Read pinned TransformerLab version file
|
||||
slurp:
|
||||
src: "{{ courseware_transformerlab_home }}/src/LATEST_VERSION"
|
||||
register: courseware_transformerlab_version_contents
|
||||
when: courseware_transformerlab_src_version.stat.exists
|
||||
|
||||
- name: Determine pinned TransformerLab drift state
|
||||
set_fact:
|
||||
courseware_transformerlab_installed_version: "{{ (courseware_transformerlab_version_contents.content | b64decode | trim) if courseware_transformerlab_src_version.stat.exists else '' }}"
|
||||
courseware_transformerlab_refresh_required: >-
|
||||
{{
|
||||
(not courseware_transformerlab_src_install.stat.exists) or
|
||||
(not courseware_transformerlab_src_version.stat.exists) or
|
||||
(not courseware_transformerlab_web_index.stat.exists) or
|
||||
((courseware_transformerlab_version_contents.content | b64decode | trim) != courseware_transformerlab_version)
|
||||
}}
|
||||
courseware_transformerlab_env_refresh_required: >-
|
||||
{{
|
||||
(not courseware_transformerlab_conda.stat.exists) or
|
||||
(not courseware_transformerlab_env_python.stat.exists) or
|
||||
(not courseware_transformerlab_install_marker.stat.exists)
|
||||
}}
|
||||
|
||||
- name: Download pinned TransformerLab source archive
|
||||
get_url:
|
||||
url: "https://github.com/transformerlab/transformerlab-app/archive/refs/tags/{{ courseware_transformerlab_version }}.tar.gz"
|
||||
dest: "{{ courseware_transformerlab_source_archive }}"
|
||||
mode: "0644"
|
||||
when: courseware_transformerlab_refresh_required
|
||||
|
||||
- name: Download pinned TransformerLab web archive
|
||||
get_url:
|
||||
url: "https://github.com/transformerlab/transformerlab-app/releases/download/{{ courseware_transformerlab_version }}/transformerlab_web.tar.gz"
|
||||
dest: "{{ courseware_transformerlab_web_archive }}"
|
||||
mode: "0644"
|
||||
when: courseware_transformerlab_refresh_required
|
||||
|
||||
- name: Refresh pinned TransformerLab release layout
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
cd "{{ courseware_transformerlab_home }}"
|
||||
curl -L "https://github.com/transformerlab/transformerlab-app/archive/refs/tags/{{ courseware_transformerlab_version }}.tar.gz" -o transformerlab.tar.gz
|
||||
tar -xzf transformerlab.tar.gz
|
||||
rm -f transformerlab.tar.gz
|
||||
rm -rf src
|
||||
mv "transformerlab-app-{{ courseware_transformerlab_version_dir }}/api" src
|
||||
echo "{{ courseware_transformerlab_version }}" > src/LATEST_VERSION
|
||||
curl -L "https://github.com/transformerlab/transformerlab-app/releases/download/{{ courseware_transformerlab_version }}/transformerlab_web.tar.gz" -o transformerlab_web.tar.gz
|
||||
rm -rf webapp
|
||||
mkdir -p webapp
|
||||
tar -xzf transformerlab_web.tar.gz -C webapp
|
||||
rm -f transformerlab_web.tar.gz
|
||||
args:
|
||||
executable: /bin/bash
|
||||
creates: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
home_dir="{{ courseware_transformerlab_home }}"
|
||||
source_archive="{{ courseware_transformerlab_source_archive }}"
|
||||
web_archive="{{ courseware_transformerlab_web_archive }}"
|
||||
version="{{ courseware_transformerlab_version }}"
|
||||
version_dir="{{ courseware_transformerlab_version_dir }}"
|
||||
extract_dir="${home_dir}/transformerlab-app-${version_dir}"
|
||||
|
||||
- name: Add TransformerLab Miniforge Python path for space-safe bootstrap
|
||||
replace:
|
||||
path: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
regexp: 'CONDA_BIN=\$\{MINIFORGE_ROOT\}/bin/conda\n'
|
||||
replace: |
|
||||
CONDA_BIN=${MINIFORGE_ROOT}/bin/conda
|
||||
CONDA_PYTHON_BIN=${MINIFORGE_ROOT}/bin/python
|
||||
when: "' ' in courseware_transformerlab_home"
|
||||
rm -rf "${extract_dir}"
|
||||
tar -xzf "${source_archive}" -C "${home_dir}"
|
||||
|
||||
- name: Inject space-safe TransformerLab conda runner
|
||||
blockinfile:
|
||||
path: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
insertbefore: '^check_conda\(\) \{$'
|
||||
marker: '# {mark} courseware conda runner'
|
||||
block: |
|
||||
conda_direct_exec_works() {
|
||||
"${CONDA_BIN}" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_conda() {
|
||||
if conda_direct_exec_works; then
|
||||
"${CONDA_BIN}" "$@"
|
||||
else
|
||||
"${CONDA_PYTHON_BIN}" "${CONDA_BIN}" "$@"
|
||||
if [ ! -d "${extract_dir}/api" ]; then
|
||||
echo "Pinned TransformerLab source archive did not contain api/." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
when: "' ' in courseware_transformerlab_home"
|
||||
|
||||
- name: Rewrite TransformerLab installer to use the space-safe conda runner
|
||||
replace:
|
||||
path: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
regexp: 'eval "\$\(\$\{CONDA_BIN\} shell\.bash hook\)"'
|
||||
replace: 'eval "$(run_conda shell.bash hook)"'
|
||||
when: "' ' in courseware_transformerlab_home"
|
||||
rm -rf "${home_dir}/src" "${home_dir}/lab-sdk" "${home_dir}/webapp"
|
||||
mv "${extract_dir}/api" "${home_dir}/src"
|
||||
|
||||
- name: Rewrite TransformerLab doctor output to use the space-safe conda runner
|
||||
replace:
|
||||
path: "{{ courseware_transformerlab_home }}/src/install.sh"
|
||||
regexp: '\$\(\$\{CONDA_BIN\} --version\)'
|
||||
replace: '$(run_conda --version)'
|
||||
when: "' ' in courseware_transformerlab_home"
|
||||
if [ -d "${extract_dir}/lab-sdk" ]; then
|
||||
mv "${extract_dir}/lab-sdk" "${home_dir}/lab-sdk"
|
||||
fi
|
||||
|
||||
- name: Install TransformerLab
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
./src/install.sh 2>&1 | tee "{{ courseware_logs_dir }}/transformerlab_install.log"
|
||||
touch "{{ courseware_transformerlab_home }}/.courseware-managed"
|
||||
printf '%s\n' "${version}" > "${home_dir}/src/LATEST_VERSION"
|
||||
|
||||
mkdir -p "${home_dir}/webapp"
|
||||
tar -xzf "${web_archive}" -C "${home_dir}/webapp"
|
||||
if [ -d "${home_dir}/webapp/transformerlab_web" ]; then
|
||||
shopt -s dotglob nullglob
|
||||
mv "${home_dir}/webapp/transformerlab_web/"* "${home_dir}/webapp/"
|
||||
rmdir "${home_dir}/webapp/transformerlab_web"
|
||||
fi
|
||||
|
||||
rm -rf "${extract_dir}"
|
||||
rm -f "${home_dir}/.install_complete"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
chdir: "{{ courseware_transformerlab_home }}"
|
||||
creates: "{{ courseware_transformerlab_home }}/miniforge3/bin/conda"
|
||||
when: courseware_transformerlab_refresh_required
|
||||
|
||||
- name: Rewrite TransformerLab Miniforge entrypoints to a space-safe shebang path
|
||||
- name: Ensure pinned TransformerLab environment file exists
|
||||
file:
|
||||
path: "{{ courseware_transformerlab_home }}/.env"
|
||||
state: touch
|
||||
mode: "0600"
|
||||
|
||||
- name: Ensure pinned TransformerLab JWT secrets exist
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
actual_prefix="{{ courseware_transformerlab_home }}/miniforge3/bin/"
|
||||
safe_prefix="{{ ansible_env.HOME }}/.transformerlab/miniforge3/bin/"
|
||||
env_file="{{ courseware_transformerlab_home }}/.env"
|
||||
|
||||
find "{{ courseware_transformerlab_home }}/miniforge3/bin" -maxdepth 1 -type f -print0 |
|
||||
while IFS= read -r -d '' file; do
|
||||
first_line=$(head -n 1 "$file" || true)
|
||||
case "$first_line" in
|
||||
"#!${actual_prefix}"*)
|
||||
suffix=${first_line#\#!}
|
||||
suffix=${suffix#"${actual_prefix}"}
|
||||
replacement="#!${safe_prefix}${suffix}"
|
||||
tmp_file=$(mktemp)
|
||||
{
|
||||
printf '%s\n' "$replacement"
|
||||
tail -n +2 "$file"
|
||||
} >"$tmp_file"
|
||||
chmod --reference="$file" "$tmp_file"
|
||||
mv "$tmp_file" "$file"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if ! grep -q '^TRANSFORMERLAB_JWT_SECRET=' "$env_file"; then
|
||||
printf 'TRANSFORMERLAB_JWT_SECRET=%s\n' "$(od -An -N 64 -tx1 /dev/urandom | tr -d ' \n')" >> "$env_file"
|
||||
fi
|
||||
|
||||
if ! grep -q '^TRANSFORMERLAB_REFRESH_SECRET=' "$env_file"; then
|
||||
printf 'TRANSFORMERLAB_REFRESH_SECRET=%s\n' "$(od -An -N 64 -tx1 /dev/urandom | tr -d ' \n')" >> "$env_file"
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: "' ' in courseware_transformerlab_home"
|
||||
|
||||
- name: Install TransformerLab multiuser dependencies
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
./src/install.sh multiuser_setup 2>&1 | tee "{{ courseware_logs_dir }}/transformerlab_multiuser_setup.log"
|
||||
touch "{{ courseware_transformerlab_home }}/.courseware-managed"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
chdir: "{{ courseware_transformerlab_home }}"
|
||||
creates: "{{ courseware_transformerlab_home }}/envs/general-uv/bin/python"
|
||||
- name: Remove stale TransformerLab general-uv environment
|
||||
file:
|
||||
path: "{{ courseware_transformerlab_home }}/envs/general-uv"
|
||||
state: absent
|
||||
|
||||
- name: Check TransformerLab general uv environment
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/envs/general-uv/bin/python"
|
||||
register: courseware_transformerlab_general_uv
|
||||
|
||||
- name: Retry TransformerLab multiuser setup after source refresh
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
./src/install.sh multiuser_setup 2>&1 | tee "{{ courseware_logs_dir }}/transformerlab_multiuser_setup_retry.log"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
chdir: "{{ courseware_transformerlab_home }}"
|
||||
when: not courseware_transformerlab_general_uv.stat.exists
|
||||
|
||||
- name: Recheck TransformerLab general uv environment
|
||||
stat:
|
||||
path: "{{ courseware_transformerlab_home }}/envs/general-uv/bin/python"
|
||||
register: courseware_transformerlab_general_uv
|
||||
|
||||
- name: Mark TransformerLab multiuser setup complete
|
||||
- name: Remove stale TransformerLab multiuser marker
|
||||
file:
|
||||
path: "{{ courseware_transformerlab_home }}/.multiuser_setup_complete"
|
||||
state: touch
|
||||
mode: "0644"
|
||||
when: courseware_transformerlab_general_uv.stat.exists
|
||||
state: absent
|
||||
|
||||
- name: Fail if TransformerLab general uv environment is missing
|
||||
- name: Determine Miniforge platform suffix
|
||||
set_fact:
|
||||
courseware_transformerlab_miniforge_platform: "{{ 'Linux-x86_64' if ansible_system == 'Linux' and ansible_architecture == 'x86_64' else 'Linux-aarch64' if ansible_system == 'Linux' and ansible_architecture in ['aarch64', 'arm64'] else 'MacOSX-arm64' if ansible_system == 'Darwin' and ansible_architecture == 'arm64' else 'MacOSX-x86_64' if ansible_system == 'Darwin' and ansible_architecture == 'x86_64' else 'unsupported' }}"
|
||||
|
||||
- name: Fail for unsupported Miniforge platform
|
||||
fail:
|
||||
msg: "TransformerLab multiuser setup completed without creating {{ courseware_transformerlab_home }}/envs/general-uv/bin/python."
|
||||
when: not courseware_transformerlab_general_uv.stat.exists
|
||||
msg: "Unsupported TransformerLab Miniforge platform: {{ ansible_system }} {{ ansible_architecture }}"
|
||||
when: courseware_transformerlab_miniforge_platform == "unsupported"
|
||||
|
||||
- name: Download Miniforge installer for pinned TransformerLab
|
||||
get_url:
|
||||
url: "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-{{ courseware_transformerlab_miniforge_platform }}.sh"
|
||||
dest: "{{ courseware_transformerlab_miniforge_installer }}"
|
||||
mode: "0755"
|
||||
when: not courseware_transformerlab_conda.stat.exists
|
||||
|
||||
- name: Install Miniforge for pinned TransformerLab
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
rm -rf "{{ courseware_transformerlab_home }}/miniforge3"
|
||||
bash "{{ courseware_transformerlab_miniforge_installer }}" -b -p "{{ courseware_transformerlab_home }}/miniforge3"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: not courseware_transformerlab_conda.stat.exists
|
||||
|
||||
- name: Create pinned TransformerLab environment
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
"{{ courseware_transformerlab_home }}/miniforge3/bin/conda" create -y -k --prefix "{{ courseware_transformerlab_home }}/envs/transformerlab" python=3.11
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: not courseware_transformerlab_env_python.stat.exists
|
||||
|
||||
- name: Detect pinned TransformerLab accelerator profile
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi --query-gpu=name --format=csv,noheader,nounits >/dev/null 2>&1; then
|
||||
printf '%s\n' nvidia
|
||||
elif command -v rocminfo >/dev/null 2>&1; then
|
||||
printf '%s\n' rocm
|
||||
else
|
||||
printf '%s\n' cpu
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: courseware_transformerlab_accelerator
|
||||
changed_when: false
|
||||
|
||||
- name: Install pinned TransformerLab Python dependencies
|
||||
shell: |
|
||||
set -euo pipefail
|
||||
env_python="{{ courseware_transformerlab_home }}/envs/transformerlab/bin/python"
|
||||
conda_bin="{{ courseware_transformerlab_home }}/miniforge3/bin/conda"
|
||||
accelerator="{{ courseware_transformerlab_accelerator.stdout | trim }}"
|
||||
project_dir="{{ courseware_transformerlab_home }}/src"
|
||||
wheel_args=()
|
||||
extra="cpu"
|
||||
|
||||
"$env_python" -m pip install --upgrade pip
|
||||
"$env_python" -m pip install --upgrade uv
|
||||
|
||||
if [ "$accelerator" = "nvidia" ]; then
|
||||
"$conda_bin" install -y --prefix "{{ courseware_transformerlab_home }}/envs/transformerlab" cuda==12.8.1 --force-reinstall -c nvidia/label/cuda-12.8.1
|
||||
extra="nvidia"
|
||||
elif [ "$accelerator" = "rocm" ]; then
|
||||
wheel_args+=(--index https://download.pytorch.org/whl/rocm6.4 --index-strategy unsafe-best-match)
|
||||
extra="rocm"
|
||||
elif [ "{{ ansible_system }}" != "Darwin" ]; then
|
||||
wheel_args+=(--index https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match)
|
||||
fi
|
||||
|
||||
cd "$project_dir"
|
||||
"$env_python" -m uv pip install --python "$env_python" "${wheel_args[@]}" ".[${extra}]"
|
||||
|
||||
if [ -d "{{ courseware_transformerlab_home }}/lab-sdk" ]; then
|
||||
"$env_python" -m uv pip install --python "$env_python" "{{ courseware_transformerlab_home }}/lab-sdk"
|
||||
fi
|
||||
|
||||
"$env_python" -c "import uvicorn"
|
||||
touch "{{ courseware_transformerlab_home }}/.install_complete"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
when: courseware_transformerlab_refresh_required or courseware_transformerlab_env_refresh_required
|
||||
|
||||
- name: Install TransformerLab plugin repair helper
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../../scripts/repair_transformerlab_plugin_supports.py"
|
||||
dest: "{{ courseware_state_dir }}/repair_transformerlab_plugin_supports.py"
|
||||
mode: "0755"
|
||||
|
||||
- name: Install TransformerLab default-user helper
|
||||
copy:
|
||||
src: "{{ playbook_dir }}/../../scripts/ensure_transformerlab_user.py"
|
||||
dest: "{{ courseware_state_dir }}/ensure_transformerlab_user.py"
|
||||
mode: "0755"
|
||||
|
||||
- name: Repair installed Fastchat plugin supports
|
||||
command:
|
||||
argv:
|
||||
- python3
|
||||
- "{{ courseware_state_dir }}/repair_transformerlab_plugin_supports.py"
|
||||
- --transformerlab-dir
|
||||
- "{{ courseware_transformerlab_home }}"
|
||||
- --plugin
|
||||
- fastchat_server
|
||||
- --required-support
|
||||
- chat
|
||||
- --required-support
|
||||
- completion
|
||||
- --required-support
|
||||
- visualize_model
|
||||
- --required-support
|
||||
- model_layers
|
||||
- --required-support
|
||||
- rag
|
||||
- --required-support
|
||||
- tools
|
||||
- --required-support
|
||||
- template
|
||||
- --required-support
|
||||
- embeddings
|
||||
- --required-support
|
||||
- tokenize
|
||||
- --required-support
|
||||
- logprobs
|
||||
- --required-support
|
||||
- batched
|
||||
changed_when: false
|
||||
|
||||
- name: Ensure pinned TransformerLab default user when database already exists
|
||||
command:
|
||||
argv:
|
||||
- "{{ courseware_transformerlab_home }}/envs/transformerlab/bin/python"
|
||||
- "{{ courseware_state_dir }}/ensure_transformerlab_user.py"
|
||||
- --transformerlab-dir
|
||||
- "{{ courseware_transformerlab_home }}"
|
||||
- --email
|
||||
- "{{ courseware_transformerlab_default_user_email }}"
|
||||
- --password
|
||||
- "{{ courseware_transformerlab_default_user_password }}"
|
||||
- --first-name
|
||||
- "{{ courseware_transformerlab_default_user_first_name }}"
|
||||
- --last-name
|
||||
- "{{ courseware_transformerlab_default_user_last_name }}"
|
||||
changed_when: false
|
||||
|
||||
@@ -20,6 +20,10 @@ EMBEDDING_ATLAS_VENV="{{ courseware_venvs_dir }}/embedding-atlas"
|
||||
TTPS_DATASET_PATH="{{ courseware_datasets_dir }}/ttps_dataset.parquet"
|
||||
WIKI_TEST_RAW_PATH="{{ courseware_datasets_dir }}/wiki.test.raw"
|
||||
TRANSFORMERLAB_DIR="{{ courseware_transformerlab_home }}"
|
||||
TRANSFORMERLAB_DEFAULT_USER_EMAIL="{{ courseware_transformerlab_default_user_email }}"
|
||||
TRANSFORMERLAB_DEFAULT_USER_PASSWORD="{{ courseware_transformerlab_default_user_password }}"
|
||||
TRANSFORMERLAB_DEFAULT_USER_FIRST_NAME="{{ courseware_transformerlab_default_user_first_name }}"
|
||||
TRANSFORMERLAB_DEFAULT_USER_LAST_NAME="{{ courseware_transformerlab_default_user_last_name }}"
|
||||
UNSLOTH_BIN="{{ ansible_env.HOME }}/.local/bin/unsloth"
|
||||
PROMPTFOO_DIR="{{ courseware_promptfoo_dir }}"
|
||||
PROMPTFOO_BIN="{{ courseware_tools_dir }}/promptfoo/node_modules/.bin/promptfoo"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+8
-1
@@ -21,6 +21,10 @@ load_runtime_env() {
|
||||
: "${PROMPTFOO_DIR:=$COURSEWARE_STATE_DIR/lab6}"
|
||||
: "${WIKI_DIR:=$COURSEWARE_STATE_DIR/repos/LLM-Labs}"
|
||||
: "${LLAMA_CPP_BIN_DIR:=$COURSEWARE_STATE_DIR/repos/llama.cpp/build/bin}"
|
||||
|
||||
if [ -n "${OLLAMA_BIN:-}" ] && [[ "$OLLAMA_BIN" != */* ]] && command -v "$OLLAMA_BIN" >/dev/null 2>&1; then
|
||||
OLLAMA_BIN=$(command -v "$OLLAMA_BIN")
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_runtime_env() {
|
||||
@@ -97,7 +101,10 @@ service_command() {
|
||||
"$COURSEWARE_OPEN_WEBUI_PORT"
|
||||
;;
|
||||
transformerlab)
|
||||
printf 'cd "%s/src" && exec ./run.sh -h %s -p %s' \
|
||||
printf 'export PATH="%s/envs/transformerlab/bin:$PATH"; export VIRTUAL_ENV="%s/envs/transformerlab"; export CONDA_PREFIX="%s/envs/transformerlab"; cd "%s/src" && exec ./run.sh -c -h %s -p %s' \
|
||||
"$TRANSFORMERLAB_DIR" \
|
||||
"$TRANSFORMERLAB_DIR" \
|
||||
"$TRANSFORMERLAB_DIR" \
|
||||
"$TRANSFORMERLAB_DIR" \
|
||||
"$COURSEWARE_BIND_HOST" \
|
||||
"$COURSEWARE_TRANSFORMERLAB_PORT"
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Ensure a known verified TransformerLab user exists for single-user courseware installs."
|
||||
)
|
||||
parser.add_argument("--transformerlab-dir", required=True, help="Path to the managed TransformerLab home directory.")
|
||||
parser.add_argument("--email", required=True, help="Email address for the default user.")
|
||||
parser.add_argument("--password", required=True, help="Password for the default user.")
|
||||
parser.add_argument("--first-name", default="Student", help="Optional first name for the default user.")
|
||||
parser.add_argument("--last-name", default="", help="Optional last name for the default user.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def bootstrap_source(transformerlab_dir: Path) -> None:
|
||||
source_dir = transformerlab_dir / "src"
|
||||
if not source_dir.is_dir():
|
||||
raise SystemExit(f"TransformerLab source directory not found: {source_dir}")
|
||||
|
||||
sys.path.insert(0, str(source_dir))
|
||||
|
||||
env_file = transformerlab_dir / ".env"
|
||||
if env_file.exists():
|
||||
for line in env_file.read_text().splitlines():
|
||||
if not line or line.lstrip().startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
||||
|
||||
|
||||
async def ensure_user(args: argparse.Namespace) -> int:
|
||||
from sqlalchemy import select
|
||||
from transformerlab.db.constants import DATABASE_FILE_NAME
|
||||
from transformerlab.shared.models.models import OAuthAccount, TeamRole, User, UserTeam
|
||||
from transformerlab.shared.models.user_model import (
|
||||
AsyncSessionLocal,
|
||||
SQLAlchemyUserDatabaseWithOAuth,
|
||||
create_personal_team,
|
||||
)
|
||||
from transformerlab.models.users import UserCreate, UserManager
|
||||
|
||||
database_path = Path(DATABASE_FILE_NAME)
|
||||
if not database_path.exists():
|
||||
print(f"TransformerLab database is not ready yet at {database_path}; skipping default-user sync.")
|
||||
return 0
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
user_db = SQLAlchemyUserDatabaseWithOAuth(session, User, OAuthAccount)
|
||||
user_manager = UserManager(user_db)
|
||||
|
||||
stmt = select(User).where(User.email == args.email)
|
||||
result = await session.execute(stmt)
|
||||
user = result.unique().scalar_one_or_none()
|
||||
created = False
|
||||
changed = False
|
||||
|
||||
if user is None:
|
||||
user = await user_manager.create(
|
||||
UserCreate(
|
||||
email=args.email,
|
||||
password=args.password,
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
is_verified=True,
|
||||
first_name=args.first_name or None,
|
||||
last_name=args.last_name or None,
|
||||
),
|
||||
safe=False,
|
||||
request=None,
|
||||
)
|
||||
created = True
|
||||
changed = True
|
||||
else:
|
||||
verified, new_hash = user_manager.password_helper.verify_and_update(args.password, user.hashed_password)
|
||||
if not verified:
|
||||
user.hashed_password = user_manager.password_helper.hash(args.password)
|
||||
changed = True
|
||||
elif new_hash:
|
||||
user.hashed_password = new_hash
|
||||
changed = True
|
||||
|
||||
if not user.is_active:
|
||||
user.is_active = True
|
||||
changed = True
|
||||
|
||||
if not user.is_verified:
|
||||
user.is_verified = True
|
||||
changed = True
|
||||
|
||||
if not user.is_superuser:
|
||||
user.is_superuser = True
|
||||
changed = True
|
||||
|
||||
if args.first_name and user.first_name != args.first_name:
|
||||
user.first_name = args.first_name
|
||||
changed = True
|
||||
|
||||
desired_last_name = args.last_name or None
|
||||
if user.last_name != desired_last_name:
|
||||
user.last_name = desired_last_name
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
team_stmt = select(UserTeam).where(UserTeam.user_id == str(user.id))
|
||||
team_result = await session.execute(team_stmt)
|
||||
user_team = team_result.scalar_one_or_none()
|
||||
|
||||
if user_team is None:
|
||||
personal_team = await create_personal_team(session, user)
|
||||
user_team = UserTeam(user_id=str(user.id), team_id=personal_team.id, role=TeamRole.OWNER.value)
|
||||
session.add(user_team)
|
||||
await session.commit()
|
||||
print(f"Created personal team '{personal_team.name}' for {args.email}.")
|
||||
elif user_team.role != TeamRole.OWNER.value:
|
||||
user_team.role = TeamRole.OWNER.value
|
||||
session.add(user_team)
|
||||
await session.commit()
|
||||
print(f"Updated team role to owner for {args.email}.")
|
||||
|
||||
action = "Created" if created else "Verified"
|
||||
print(f"{action} default TransformerLab user {args.email}.")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
args.email = args.email.strip()
|
||||
args.password = args.password.strip()
|
||||
args.first_name = args.first_name.strip()
|
||||
args.last_name = args.last_name.strip()
|
||||
bootstrap_source(Path(args.transformerlab_dir))
|
||||
return asyncio.run(ensure_user(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Repair installed TransformerLab plugin manifests from the pinned source manifest."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict) -> None:
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=False)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def candidate_manifest_paths(root: Path, plugin: str) -> list[Path]:
|
||||
candidates = []
|
||||
patterns = [
|
||||
f"workspace/plugins/{plugin}/index.json",
|
||||
f"orgs/*/workspace/plugins/{plugin}/index.json",
|
||||
f"orgs/*/plugins/{plugin}/index.json",
|
||||
]
|
||||
for pattern in patterns:
|
||||
candidates.extend(root.glob(pattern))
|
||||
unique = []
|
||||
seen = set()
|
||||
for path in candidates:
|
||||
resolved = path.resolve()
|
||||
if resolved in seen:
|
||||
continue
|
||||
seen.add(resolved)
|
||||
unique.append(path)
|
||||
return unique
|
||||
|
||||
|
||||
def repair_manifest(path: Path, required_supports: list[str], source_supports: list[str]) -> bool:
|
||||
payload = load_json(path)
|
||||
existing = payload.get("supports", [])
|
||||
if not isinstance(existing, list):
|
||||
existing = []
|
||||
|
||||
desired = []
|
||||
seen = set()
|
||||
for item in source_supports:
|
||||
if isinstance(item, str) and item not in seen:
|
||||
desired.append(item)
|
||||
seen.add(item)
|
||||
for item in required_supports:
|
||||
if item not in seen:
|
||||
desired.append(item)
|
||||
seen.add(item)
|
||||
|
||||
if existing == desired:
|
||||
return False
|
||||
|
||||
payload["supports"] = desired
|
||||
write_json(path, payload)
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--transformerlab-dir", required=True)
|
||||
parser.add_argument("--plugin", required=True)
|
||||
parser.add_argument("--required-support", action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.transformerlab_dir).expanduser().resolve()
|
||||
source_manifest = root / "src" / "transformerlab" / "plugins" / args.plugin / "index.json"
|
||||
if not source_manifest.exists():
|
||||
print(f"missing source plugin manifest: {source_manifest}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
source_payload = load_json(source_manifest)
|
||||
source_supports = source_payload.get("supports", [])
|
||||
if not isinstance(source_supports, list):
|
||||
print(f"invalid supports array in {source_manifest}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
missing_required = [item for item in args.required_support if item not in source_supports]
|
||||
if missing_required:
|
||||
print(
|
||||
f"source plugin manifest {source_manifest} is missing required supports: {', '.join(missing_required)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
updated = 0
|
||||
for manifest in candidate_manifest_paths(root, args.plugin):
|
||||
if repair_manifest(manifest, args.required_support, source_supports):
|
||||
updated += 1
|
||||
print(f"repaired {manifest}")
|
||||
|
||||
if updated == 0:
|
||||
print(f"no installed {args.plugin} manifests needed repair")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -9,6 +9,25 @@ load_runtime_env
|
||||
|
||||
mkdir -p "$STATE_DIR/run" "$STATE_DIR/logs"
|
||||
|
||||
ensure_transformerlab_default_user() {
|
||||
local helper_python="${TRANSFORMERLAB_DIR}/envs/transformerlab/bin/python"
|
||||
|
||||
if [ ! -x "$helper_python" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "${TRANSFORMERLAB_DEFAULT_USER_EMAIL:-}" ] || [ -z "${TRANSFORMERLAB_DEFAULT_USER_PASSWORD:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
"$helper_python" "$SCRIPT_DIR/ensure_transformerlab_user.py" \
|
||||
--transformerlab-dir "$TRANSFORMERLAB_DIR" \
|
||||
--email "$TRANSFORMERLAB_DEFAULT_USER_EMAIL" \
|
||||
--password "$TRANSFORMERLAB_DEFAULT_USER_PASSWORD" \
|
||||
--first-name "${TRANSFORMERLAB_DEFAULT_USER_FIRST_NAME:-Student}" \
|
||||
--last-name "${TRANSFORMERLAB_DEFAULT_USER_LAST_NAME:-}" >>"$STATE_DIR/logs/transformerlab_default_user.log" 2>&1 || true
|
||||
}
|
||||
|
||||
resolve_targets() {
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No target specified." >&2
|
||||
@@ -57,10 +76,13 @@ service_ready() {
|
||||
ollama)
|
||||
curl -fsS "$(service_url "$service")/api/tags" >/dev/null 2>&1
|
||||
;;
|
||||
transformerlab)
|
||||
curl -fsS "$(service_url "$service")/healthz" >/dev/null 2>&1
|
||||
;;
|
||||
promptfoo)
|
||||
curl -fsS "$(service_url "$service")/health" >/dev/null 2>&1
|
||||
;;
|
||||
open-webui|transformerlab|chunkviz|embedding-atlas|unsloth|wiki)
|
||||
open-webui|chunkviz|embedding-atlas|unsloth|wiki)
|
||||
curl -fsS "$(service_url "$service")" >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
@@ -75,13 +97,20 @@ start_one() {
|
||||
local log_file
|
||||
local pid_file
|
||||
local attempt
|
||||
local pid_grace_attempts=5
|
||||
|
||||
if has_live_pid "$service"; then
|
||||
if [ "$service" = "transformerlab" ]; then
|
||||
ensure_transformerlab_default_user
|
||||
fi
|
||||
echo "$service already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if service_ready "$service"; then
|
||||
if [ "$service" = "transformerlab" ]; then
|
||||
ensure_transformerlab_default_user
|
||||
fi
|
||||
echo "$service already available"
|
||||
return 0
|
||||
fi
|
||||
@@ -90,6 +119,24 @@ start_one() {
|
||||
open-webui)
|
||||
start_one ollama
|
||||
;;
|
||||
transformerlab)
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 "$SCRIPT_DIR/repair_transformerlab_plugin_supports.py" \
|
||||
--transformerlab-dir "$TRANSFORMERLAB_DIR" \
|
||||
--plugin "fastchat_server" \
|
||||
--required-support "chat" \
|
||||
--required-support "completion" \
|
||||
--required-support "visualize_model" \
|
||||
--required-support "model_layers" \
|
||||
--required-support "rag" \
|
||||
--required-support "tools" \
|
||||
--required-support "template" \
|
||||
--required-support "embeddings" \
|
||||
--required-support "tokenize" \
|
||||
--required-support "logprobs" \
|
||||
--required-support "batched" >>"$STATE_DIR/logs/transformerlab_plugin_supports.log" 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
@@ -98,7 +145,12 @@ start_one() {
|
||||
log_file=$(service_log_file "$service")
|
||||
pid_file=$(service_pid_file "$service")
|
||||
|
||||
if command -v setsid >/dev/null 2>&1; then
|
||||
if [ "$service" = "ollama" ]; then
|
||||
env \
|
||||
OLLAMA_HOST="${COURSEWARE_BIND_HOST}:${COURSEWARE_OLLAMA_PORT}" \
|
||||
OLLAMA_MODELS="$OLLAMA_MODELS_DIR" \
|
||||
"$OLLAMA_BIN" serve </dev/null >>"$log_file" 2>&1 &
|
||||
elif command -v setsid >/dev/null 2>&1; then
|
||||
nohup setsid bash -lc "$cmd" </dev/null >>"$log_file" 2>&1 &
|
||||
else
|
||||
nohup bash -lc "$cmd" </dev/null >>"$log_file" 2>&1 &
|
||||
@@ -107,15 +159,20 @@ start_one() {
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
if service_ready "$service"; then
|
||||
if [ "$service" = "transformerlab" ]; then
|
||||
ensure_transformerlab_default_user
|
||||
fi
|
||||
echo "started $service"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! has_live_pid "$service"; then
|
||||
if [ "$attempt" -ge "$pid_grace_attempts" ]; then
|
||||
rm -f "$pid_file"
|
||||
echo "failed to start $service; check $log_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user