147 lines
4.2 KiB
Python
147 lines
4.2 KiB
Python
"""Фоновый запуск llama-server при старте WESP (если WESP_LLM_AUTOSTART и LLM включён)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
from urllib.parse import urlparse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_proc: Optional[subprocess.Popen] = None
|
|
|
|
|
|
def _should_run_in_this_process(app: Any) -> bool:
|
|
"""Не дублировать в родителе Flask reloader (как sync_client)."""
|
|
if bool(app.config.get("DEBUG", False)):
|
|
return os.environ.get("WERKZEUG_RUN_MAIN") == "true"
|
|
return True
|
|
|
|
|
|
def _parse_base_url(url: str) -> tuple[str, int]:
|
|
u = urlparse((url or "").strip() or "http://127.0.0.1:8080")
|
|
host = u.hostname or "127.0.0.1"
|
|
port = int(u.port or 8080)
|
|
return host, port
|
|
|
|
|
|
def _resolve_llama_binary(base_dir: Path) -> Optional[Path]:
|
|
env = os.getenv("LLAMA_SERVER", "").strip()
|
|
if env:
|
|
p = Path(env)
|
|
if p.is_file():
|
|
return p
|
|
if sys.platform == "win32":
|
|
for rel in ("tools/llama-server.exe", "tools/llama-server/llama-server.exe"):
|
|
cand = base_dir / rel.replace("/", os.sep)
|
|
if cand.is_file():
|
|
return cand
|
|
else:
|
|
cand = base_dir / "tools" / "llama-server"
|
|
if cand.is_file():
|
|
return cand
|
|
which = shutil.which("llama-server")
|
|
if which:
|
|
return Path(which)
|
|
return None
|
|
|
|
|
|
def _gguf_path(app: Any) -> Path:
|
|
cfg = app.config
|
|
root = Path(str(cfg.get("WESP_ASSISTANT_DIR") or ""))
|
|
name = os.getenv("WESP_LLM_GGUF_FILE", "").strip() or str(
|
|
cfg.get("WESP_LLM_MODEL") or ""
|
|
).strip()
|
|
if not name:
|
|
name = "qwen2.5-1.5b-instruct-q5_k_m.gguf"
|
|
return root / "models" / name
|
|
|
|
|
|
def schedule_llm_server_autostart(app: Any) -> None:
|
|
"""Запускает llama-server дочерним процессом; idempotent."""
|
|
global _proc
|
|
if _proc is not None:
|
|
return
|
|
if app.config.get("TESTING"):
|
|
return
|
|
if not app.config.get("WESP_LLM_AUTOSTART"):
|
|
return
|
|
if not app.config.get("WESP_ADMIN_LLM_ENABLED"):
|
|
logger.warning(
|
|
"WESP_LLM_AUTOSTART пропущен: LLM в админке выключен (галка или admin_llm_enabled в wesp_security_settings.json, либо WESP_ADMIN_LLM_ENABLED=0)."
|
|
)
|
|
return
|
|
if not _should_run_in_this_process(app):
|
|
return
|
|
|
|
base_dir = Path(str(app.config.get("BASE_DIR") or ".")).resolve()
|
|
gguf = _gguf_path(app)
|
|
if not gguf.is_file():
|
|
logger.warning(
|
|
"WESP_LLM_AUTOSTART пропущен: нет файла модели %s",
|
|
gguf,
|
|
)
|
|
return
|
|
|
|
bin_path = _resolve_llama_binary(base_dir)
|
|
if not bin_path:
|
|
logger.warning(
|
|
"WESP_LLM_AUTOSTART пропущен: не найден llama-server "
|
|
"(переменная LLAMA_SERVER или %s/tools/).",
|
|
base_dir,
|
|
)
|
|
return
|
|
|
|
host, port = _parse_base_url(str(app.config.get("WESP_LLM_BASE_URL") or ""))
|
|
args = [
|
|
str(bin_path),
|
|
"-m",
|
|
str(gguf),
|
|
"--host",
|
|
host,
|
|
"--port",
|
|
str(port),
|
|
"-c",
|
|
"8192",
|
|
]
|
|
cwd = str(bin_path.parent)
|
|
popen_kw: dict = {"cwd": cwd}
|
|
if sys.platform == "win32":
|
|
popen_kw["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
|
|
try:
|
|
_proc = subprocess.Popen(args, **popen_kw)
|
|
except OSError as exc:
|
|
logger.warning("WESP_LLM_AUTOSTART: не удалось запустить llama-server: %s", exc)
|
|
return
|
|
|
|
logger.info(
|
|
"WESP_LLM_AUTOSTART: llama-server PID=%s, %s:%s, модель %s",
|
|
_proc.pid,
|
|
host,
|
|
port,
|
|
gguf.name,
|
|
)
|
|
|
|
|
|
def stop_llm_server_process() -> None:
|
|
"""Завершить дочерний llama-server при выходе из процесса WESP."""
|
|
global _proc
|
|
if _proc is None:
|
|
return
|
|
p = _proc
|
|
_proc = None
|
|
try:
|
|
p.terminate()
|
|
p.wait(timeout=8)
|
|
except Exception:
|
|
try:
|
|
p.kill()
|
|
except Exception:
|
|
pass
|