Consolidated local ollama model logic

This commit is contained in:
2026-04-07 15:42:00 +02:00
parent 9d8aa88bd0
commit 7309541c24
9 changed files with 1171 additions and 527 deletions
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_OVERRIDE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export ROOT_OVERRIDE
# shellcheck disable=SC1091
source "${ROOT_OVERRIDE}/scripts/common.sh"
init_log bootstrap-native
acquire_lock jr-sql-ai-native-model.lock
print_header "bootstrap-native: START"
need_cmd curl
need_cmd python
load_env
need_cmd "$OLLAMA_BIN"
check_for_legacy_docker_ollama
[[ -n "$BASE_MODEL" ]] || fail "BASE_MODEL is empty" 50
log "bootstrap-native: ensuring local Ollama runtime is ready at ${OLLAMA_URL}"
ensure_ollama_ready "$OLLAMA_API_MAX_WAIT" "$OLLAMA_API_START_WAIT" || fail "Ollama API not reachable at ${OLLAMA_URL}" 51
ok "bootstrap-native: Ollama API reachable"
if fetch_version >/dev/null 2>&1; then
ok "bootstrap-native: Ollama version endpoint reachable"
fi
pull_configured_models
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
render_modelfile "$tmp"
log "bootstrap-native: building expert model ${EXPERT_MODEL}"
ollama_cmd create "${EXPERT_MODEL}" -f "$tmp"
verify_models_available
ok "bootstrap-native: base and expert models are available"
warmup_sqlai
print_footer "bootstrap-native: END status=OK log=${LOG_FILE}"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="${ROOT}/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/cleanup-docker-ollama-$(date -Iseconds).log"
exec > >(tee -a "$LOG_FILE") 2>&1
ts(){ date -Is; }
log(){ echo "[$(ts)] $*"; }
log "cleanup-docker-ollama: START ROOT=${ROOT}"
if ! command -v docker >/dev/null 2>&1; then
log "cleanup-docker-ollama: docker not installed; nothing to do"
exit 0
fi
if [[ -f "${ROOT}/docker-compose.yml" ]] && docker compose version >/dev/null 2>&1; then
log "cleanup-docker-ollama: docker compose down --remove-orphans"
docker compose -f "${ROOT}/docker-compose.yml" down --remove-orphans || true
fi
if docker ps -a --format '{{.Names}}' | grep -qx 'ollama'; then
log "cleanup-docker-ollama: removing container ollama"
docker rm -f ollama || true
else
log "cleanup-docker-ollama: container ollama not present"
fi
if docker images --format '{{.Repository}}:{{.Tag}}' | grep -qx 'ollama/ollama:latest'; then
log "cleanup-docker-ollama: removing image ollama/ollama:latest"
docker image rm -f ollama/ollama:latest || true
else
log "cleanup-docker-ollama: image ollama/ollama:latest not present"
fi
if [[ "${PURGE_OLLAMA_DOCKER_VOLUMES:-0}" == "1" ]]; then
mapfile -t volumes < <(docker volume ls --format '{{.Name}}' | grep -E 'ollama' || true)
if (( ${#volumes[@]} > 0 )); then
log "cleanup-docker-ollama: removing volumes: ${volumes[*]}"
docker volume rm "${volumes[@]}" || true
else
log "cleanup-docker-ollama: no ollama volumes found"
fi
else
log "cleanup-docker-ollama: keeping volumes (set PURGE_OLLAMA_DOCKER_VOLUMES=1 to remove them)"
fi
log "cleanup-docker-ollama: END log=${LOG_FILE}"
+287
View File
@@ -0,0 +1,287 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="${ROOT_OVERRIDE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-${ROOT}/.env}"
MODFILE="${MODFILE:-${ROOT}/Modelfile}"
SQLAI_BIN="${SQLAI_BIN:-${ROOT}/bin/sqlai}"
OLLAMA_BIN="${OLLAMA_BIN:-ollama}"
LOG_FILE=""
LOCK_FD=""
_ts_prefix(){ date -Is; }
ts(){ _ts_prefix; }
log(){ echo "[$(ts)] $*"; }
ok(){ log "OK $*"; }
warn(){ log "WARN $*"; }
fail(){
local msg="$1"
local code="${2:-1}"
log "FAIL $msg"
exit "$code"
}
need_cmd(){
command -v "$1" >/dev/null 2>&1 || fail "missing command: $1" 10
}
init_log(){
local name="$1"
local log_dir="${ROOT}/logs"
mkdir -p "$log_dir"
LOG_FILE="${log_dir}/${name}-$(date -Iseconds).log"
export LOG_FILE
exec > >(tee -a "$LOG_FILE") 2>&1
}
acquire_lock(){
local name="${1:-jr-sql-ai-native.lock}"
local wait_seconds="${LOCK_WAIT_SECONDS:-600}"
local lock_dir="${ROOT}/.locks"
mkdir -p "$lock_dir"
local lock_path="${lock_dir}/${name}"
if command -v flock >/dev/null 2>&1; then
exec 8>"$lock_path"
flock -w "$wait_seconds" 8 || fail "could not acquire lock ${lock_path}" 11
LOCK_FD="8"
return 0
fi
local i
for ((i=0; i<wait_seconds; i++)); do
if mkdir "$lock_path.lock" 2>/dev/null; then
trap 'rmdir "'$lock_path'.lock" >/dev/null 2>&1 || true' EXIT
return 0
fi
sleep 1
done
fail "could not acquire lock ${lock_path}.lock" 11
}
load_env(){
if [[ ! -f "$ENV_FILE" && -f "${ROOT}/.env.example" ]]; then
cp -n "${ROOT}/.env.example" "$ENV_FILE" || true
fi
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
: "${OLLAMA_URL:=http://127.0.0.1:11434}"
: "${EXPERT_MODEL:=jr-sql-expert}"
: "${BASE_MODEL:=}"
: "${EXTRA_MODELS:=}"
: "${OLLAMA_BIN:=ollama}"
: "${OLLAMA_AUTOSTART:=1}"
: "${OLLAMA_SERVE_FALLBACK:=1}"
: "${OLLAMA_SYSTEMD_SERVICE:=ollama.service}"
: "${OLLAMA_SYSTEMD_SCOPE:=auto}"
: "${OLLAMA_API_MAX_WAIT:=120}"
: "${OLLAMA_API_START_WAIT:=45}"
export OLLAMA_URL EXPERT_MODEL BASE_MODEL EXTRA_MODELS OLLAMA_BIN OLLAMA_AUTOSTART OLLAMA_SERVE_FALLBACK OLLAMA_SYSTEMD_SERVICE OLLAMA_SYSTEMD_SCOPE OLLAMA_API_MAX_WAIT OLLAMA_API_START_WAIT
export OLLAMA_HOST="$OLLAMA_URL"
}
is_json(){
python -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1
}
http_get(){
local path="$1"
curl -fsS --connect-timeout 3 --max-time 20 "${OLLAMA_URL}${path}"
}
wait_for_ollama_api(){
local max_wait="${1:-120}"
local i
for ((i=1; i<=max_wait; i++)); do
if http_get "/api/tags" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
fetch_tags(){
http_get "/api/tags"
}
fetch_version(){
http_get "/api/version"
}
model_in_tags(){
local model="$1"
local tags="$2"
TAGS_JSON="$tags" python - "$model" <<'PY'
import json
import os
import sys
m = sys.argv[1]
obj = json.loads(os.environ['TAGS_JSON'])
names = {it.get('name') for it in obj.get('models', []) if it.get('name')}
ok = (
m in names or
f"{m}:latest" in names or
(m.endswith(':latest') and m[:-7] in names)
)
sys.exit(0 if ok else 1)
PY
}
render_modelfile(){
local out_file="$1"
[[ -f "$MODFILE" ]] || fail "Modelfile not found at $MODFILE" 20
[[ -n "$BASE_MODEL" ]] || fail "BASE_MODEL is empty" 21
python - "$MODFILE" "$out_file" "$BASE_MODEL" <<'PY'
from pathlib import Path
import sys
src = Path(sys.argv[1]).read_text(encoding='utf-8')
out = src.replace('${BASE_MODEL}', sys.argv[3])
Path(sys.argv[2]).write_text(out, encoding='utf-8')
PY
}
ollama_cmd(){
OLLAMA_HOST="$OLLAMA_URL" "$OLLAMA_BIN" "$@"
}
ollama_service_exists(){
local scope="$1"
if [[ "$scope" == "user" ]]; then
systemctl --user cat "$OLLAMA_SYSTEMD_SERVICE" >/dev/null 2>&1
else
systemctl cat "$OLLAMA_SYSTEMD_SERVICE" >/dev/null 2>&1
fi
}
start_ollama_via_systemd(){
command -v systemctl >/dev/null 2>&1 || return 1
if [[ "$OLLAMA_SYSTEMD_SCOPE" == "auto" || "$OLLAMA_SYSTEMD_SCOPE" == "user" ]]; then
if ollama_service_exists user; then
log "starting Ollama via systemd user service ${OLLAMA_SYSTEMD_SERVICE}"
systemctl --user start "$OLLAMA_SYSTEMD_SERVICE"
return 0
fi
fi
if [[ "$OLLAMA_SYSTEMD_SCOPE" == "auto" || "$OLLAMA_SYSTEMD_SCOPE" == "system" ]]; then
if ollama_service_exists system; then
log "starting Ollama via systemd system service ${OLLAMA_SYSTEMD_SERVICE}"
systemctl start "$OLLAMA_SYSTEMD_SERVICE"
return 0
fi
fi
return 1
}
start_ollama_fallback(){
[[ "$OLLAMA_SERVE_FALLBACK" == "1" ]] || return 1
if pgrep -fa '(^|/)ollama( |$).*serve' >/dev/null 2>&1; then
warn "ollama serve process already exists but API is not reachable yet"
return 0
fi
local serve_log="${ROOT}/logs/ollama-serve-fallback.log"
mkdir -p "${ROOT}/logs"
log "starting Ollama via detached fallback process (${OLLAMA_BIN} serve)"
nohup env OLLAMA_HOST="$OLLAMA_URL" "$OLLAMA_BIN" serve >>"$serve_log" 2>&1 &
return 0
}
ensure_ollama_ready(){
local max_wait="${1:-$OLLAMA_API_MAX_WAIT}"
local start_wait="${2:-$OLLAMA_API_START_WAIT}"
if wait_for_ollama_api 3; then
return 0
fi
[[ "$OLLAMA_AUTOSTART" == "1" ]] || return 1
if start_ollama_via_systemd; then
wait_for_ollama_api "$start_wait" && return 0
warn "Ollama service start issued, but API is still not reachable"
fi
if start_ollama_fallback; then
wait_for_ollama_api "$max_wait" && return 0
fi
return 1
}
iter_models(){
if [[ -n "$BASE_MODEL" ]]; then
printf '%s\n' "$BASE_MODEL"
fi
if [[ -n "$EXTRA_MODELS" ]]; then
printf '%s\n' "$EXTRA_MODELS" | tr ' ' '\n' | sed '/^$/d'
fi
}
pull_configured_models(){
local model
while IFS= read -r model; do
[[ -n "$model" ]] || continue
log "pulling model ${model}"
ollama_cmd pull "$model"
done < <(iter_models)
}
check_for_legacy_docker_ollama(){
if ! command -v docker >/dev/null 2>&1; then
return 0
fi
if docker ps --format '{{.Names}}' | grep -qx 'ollama'; then
fail "legacy docker container 'ollama' is still running; run cleanup-docker-ollama.sh first" 30
fi
if docker ps -a --format '{{.Names}}' | grep -qx 'ollama'; then
warn "legacy docker container 'ollama' still exists in stopped state; cleanup recommended"
fi
if docker images --format '{{.Repository}}:{{.Tag}}' | grep -qx 'ollama/ollama:latest'; then
warn "legacy docker image ollama/ollama:latest still exists; cleanup recommended"
fi
}
warmup_sqlai(){
if [[ -x "$SQLAI_BIN" ]]; then
log "warmup via ${SQLAI_BIN}"
"$SQLAI_BIN" ask --text "Warmup: reply with exactly 'OK'." --no-metrics || warn "warmup failed"
else
warn "${SQLAI_BIN} not executable; skipping warmup"
fi
}
verify_models_available(){
local tags
tags="$(fetch_tags)"
printf '%s' "$tags" | is_json || fail "/api/tags did not return valid JSON" 40
[[ -z "$BASE_MODEL" ]] || model_in_tags "$BASE_MODEL" "$tags" || fail "base model missing: ${BASE_MODEL}" 41
model_in_tags "$EXPERT_MODEL" "$tags" || fail "expert model missing: ${EXPERT_MODEL}" 42
}
print_header(){
echo "================================================================================"
log "$1 ROOT=$ROOT"
}
print_footer(){
log "$1"
echo "================================================================================"
}
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_OVERRIDE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export ROOT_OVERRIDE
# shellcheck disable=SC1091
source "${ROOT_OVERRIDE}/scripts/common.sh"
init_log selftest-native
acquire_lock jr-sql-ai-native-model.lock
print_header "selftest-native: START"
load_env
need_cmd curl
need_cmd python
need_cmd "$OLLAMA_BIN"
need_cmd grep
check_for_legacy_docker_ollama
[[ -x "$SQLAI_BIN" ]] || fail "bin/sqlai missing or not executable at ${SQLAI_BIN}" 70
log "selftest-native: ensuring local Ollama runtime is ready at ${OLLAMA_URL}"
ensure_ollama_ready "$OLLAMA_API_MAX_WAIT" "$OLLAMA_API_START_WAIT" || fail "Ollama API not reachable at ${OLLAMA_URL}" 71
ok "selftest-native: Ollama API reachable"
tags="$(fetch_tags)"
[[ -n "$tags" ]] || fail "/api/tags returned empty body" 72
printf '%s' "$tags" | is_json || fail "/api/tags did not return valid JSON" 73
ok "selftest-native: /api/tags returned valid JSON"
model_in_tags "$EXPERT_MODEL" "$tags" || fail "expert model missing: ${EXPERT_MODEL}" 74
ok "selftest-native: expert model present: ${EXPERT_MODEL}"
if [[ -n "$BASE_MODEL" ]]; then
model_in_tags "$BASE_MODEL" "$tags" || fail "base model missing: ${BASE_MODEL}" 75
ok "selftest-native: base model present: ${BASE_MODEL}"
fi
log "selftest-native: warmup request via sqlai"
set +e
"$SQLAI_BIN" ask --text "Warmup: reply with exactly 'OK'." --no-metrics >/dev/null 2>&1
rc=$?
set -e
[[ "$rc" -eq 0 ]] || warn "warmup failed rc=${rc}"
log "selftest-native: real query via sqlai"
set +e
out="$("$SQLAI_BIN" ask --text "Give a concise checklist to troubleshoot parameter sniffing in SQL Server 2022. Keep it technical." --no-metrics 2>&1)"
rc=$?
set -e
[[ "$rc" -eq 0 ]] || fail "real query failed rc=${rc}" 76
[[ -n "${out//[[:space:]]/}" ]] || fail "real query returned empty output" 77
ok "selftest-native: real query returned non-empty output"
print_footer "selftest-native: END status=OK log=${LOG_FILE}"
+1 -1
View File
@@ -8,7 +8,7 @@ export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland}"
# Optional: Wenn du einen venv nutzt
if [[ -x "$APP_DIR/.venv/bin/python" ]]; then
exec "$APP_DIR/.venv/bin/python" "$APP_DIR/sql_ai_gui.py"
exec "$APP_DIR/.venv/bin/python" "$APP_DIR/sql_ai_gui_local_ollama.py"
else
exec python "$APP_DIR/sql_ai_gui.py"
fi
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_OVERRIDE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export ROOT_OVERRIDE
# shellcheck disable=SC1091
source "${ROOT_OVERRIDE}/scripts/common.sh"
init_log update-native
acquire_lock jr-sql-ai-native-model.lock
print_header "update-native: START"
load_env
need_cmd curl
need_cmd python
need_cmd "$OLLAMA_BIN"
check_for_legacy_docker_ollama
[[ -n "$BASE_MODEL" ]] || fail "BASE_MODEL is empty" 60
log "update-native: ensuring local Ollama runtime is ready at ${OLLAMA_URL}"
ensure_ollama_ready "$OLLAMA_API_MAX_WAIT" "$OLLAMA_API_START_WAIT" || fail "Ollama API not reachable at ${OLLAMA_URL}" 61
ok "update-native: Ollama API reachable"
pull_configured_models
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
render_modelfile "$tmp"
log "update-native: rebuilding expert model ${EXPERT_MODEL}"
ollama_cmd create "${EXPERT_MODEL}" -f "$tmp"
verify_models_available
ok "update-native: base and expert models are available"
warmup_sqlai
print_footer "update-native: END status=OK log=${LOG_FILE}"