feat installer and download model on first launch
This commit is contained in:
1 parent
1648b44189
commit
5d4e19f24b
6 files changed
+358
-3
No files matched your search
+51
-1
@@ -1,15 +1,22 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import time
|
||||
import urllib.request
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import certifi
|
||||
|
||||
# CUDA 13.x moved its runtime DLLs to bin/x64, but llama_cpp's loader only
|
||||
# checks CUDA_PATH/bin, so point it at the right folder before importing.
|
||||
# Only relevant when running from source against a system CUDA toolkit - a
|
||||
# packaged build ships the runtime DLLs next to llama_cpp's own, so this is a
|
||||
# no-op there (CUDA_PATH won't be set on a machine with just an NVIDIA driver).
|
||||
_cuda_path = os.environ.get("CUDA_PATH")
|
||||
if _cuda_path:
|
||||
_cuda_bin_x64 = Path(_cuda_path) / "bin" / "x64"
|
||||
@@ -19,7 +26,16 @@ if _cuda_path:
|
||||
from ddgs import DDGS # noqa: E402
|
||||
from llama_cpp import Llama # noqa: E402
|
||||
|
||||
MODEL_PATH = Path(__file__).parent / "Qwen3.5-27B-IQ4_XS.gguf"
|
||||
# Model lives outside the install/source directory - a packaged build's own
|
||||
# folder may be read-only (Program Files) or discarded on update, and the
|
||||
# model is far too large to embed in an installer, so it's fetched into the
|
||||
# user's profile on first run instead.
|
||||
MODEL_FILENAME = "Qwen3.5-27B-IQ4_XS.gguf"
|
||||
MODELS_DIR = Path.home() / ".dev_assist_chatbot" / "models"
|
||||
MODEL_PATH = MODELS_DIR / MODEL_FILENAME
|
||||
MODEL_DOWNLOAD_URL = (
|
||||
"https://huggingface.co/unsloth/Qwen3.5-27B-GGUF/resolve/main/Qwen3.5-27B-IQ4_XS.gguf"
|
||||
)
|
||||
|
||||
SYSTEM_PROMPT = (
|
||||
"You are a knowledgeable developer assistant. Answer questions about "
|
||||
@@ -192,6 +208,40 @@ def build_messages(history: list[tuple[str, str]], question: str) -> list[dict[s
|
||||
return messages
|
||||
|
||||
|
||||
def download_model(on_progress: Callable[[int, int], None] | None = None) -> None:
|
||||
"""Fetch MODEL_PATH from MODEL_DOWNLOAD_URL if it isn't already present.
|
||||
Downloads to a .part file alongside the target and renames on success, so
|
||||
a crash or interrupted download can't leave a truncated file that
|
||||
load_model() would then try (and fail) to use. If given, on_progress is
|
||||
invoked with (bytes_downloaded, total_bytes) as each chunk arrives -
|
||||
total_bytes is 0 if the server doesn't report a Content-Length."""
|
||||
if MODEL_PATH.exists():
|
||||
return
|
||||
MODELS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
part_path = MODEL_PATH.with_suffix(MODEL_PATH.suffix + ".part")
|
||||
|
||||
request = urllib.request.Request(
|
||||
MODEL_DOWNLOAD_URL, headers={"User-Agent": "dev-assist-chatbot"}
|
||||
)
|
||||
# A fresh/sandboxed Windows install may not have fetched the intermediate
|
||||
# CA cert for huggingface.co's chain yet (normally done transparently via
|
||||
# Windows Update on first need, which a locked-down or offline sandbox
|
||||
# can't reach) - use certifi's bundled CA list instead of the OS trust
|
||||
# store so this doesn't depend on that happening.
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
with urllib.request.urlopen(request, context=ssl_context) as response:
|
||||
total = int(response.headers.get("Content-Length", 0))
|
||||
downloaded = 0
|
||||
with open(part_path, "wb") as f:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
if on_progress is not None:
|
||||
on_progress(downloaded, total)
|
||||
|
||||
part_path.rename(MODEL_PATH)
|
||||
|
||||
|
||||
def load_model() -> Llama:
|
||||
return Llama(
|
||||
model_path=str(MODEL_PATH),
|
||||
|
||||
Reference in new issue
Block a user