Weitere Tools eingebaut. Damit ist die SQL AI einsatzbereit.
This commit is contained in:
56
README.md
56
README.md
@@ -1,21 +1,51 @@
|
|||||||
# jr-sql-ai (Terminal-first SQL Server Expert KI, lokal)
|
# jr-sql-ai (Terminal-first SQL Server 2022 Expert KI, lokal)
|
||||||
|
|
||||||
Ziel: Lokale Expert-KI für **SQL Server 2022** (T-SQL, Views, Stored Procedures, Execution Plans, UTF-8 Migration),
|
Lokale Expert-KI für **SQL Server 2022** (T-SQL, Views, Stored Procedures, Execution Plans, UTF-8 Migration),
|
||||||
aufrufbar **vom Terminal**, ohne direkte DB-Verbindung (nur Copy & Paste / Dateien).
|
aufrufbar **vom Terminal**, ohne direkte DB-Verbindung (nur Copy & Paste / Dateien).
|
||||||
|
|
||||||
|
Die KI läuft lokal via **Ollama** in Docker und wird über ein kleines CLI (`sqlai`) genutzt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **Einfacher Tech-Stack:** Docker + Ollama + Bash + curl + python
|
|
||||||
- **Host Networking:** nutzt den Host-Netzwerkstack (Routing/DNS wie Host; ideal wenn nur `br0` zuverlässig ist)
|
- **Schmaler Tech-Stack:** Docker + Ollama + Bash + curl + python
|
||||||
- **Auto-Updates:** Runtime + Models via `systemd --user` Timer
|
- **Terminal-first:** `sqlai ask ...` / `sqlai analyze-tsql ...` etc.
|
||||||
- **Viele Logs:** jede Ausführung schreibt detaillierte Logs unter `./logs/`
|
- **Ohne DB-Verbindung:** nur Analyse/Planung/Empfehlung anhand von Input
|
||||||
- **Freie SQL Server 2022 Q&A:** Modus `ask` für allgemeine Fragen, ohne Pipe via `--text`
|
- **Auto-Updates:** Runtime + Models via `scripts/update.sh` und optional `systemd --user` Timer
|
||||||
|
- **Warmup:** nach Updates (und optional nach Bootstrap) wird ein kurzer Request gesendet (schneller “First Real Query”)
|
||||||
|
- **Selftest:** `scripts/selftest.sh` prüft End-to-End (Docker, API, Models, echte Anfrage)
|
||||||
|
- **Resilient:** Model-Fallback (wenn Expert-Model fehlt → Base-Model)
|
||||||
|
- **Viele Logs:** jedes Script schreibt nachvollziehbare Logs nach `./logs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Voraussetzungen (Arch Linux)
|
## Voraussetzungen (Arch Linux)
|
||||||
- docker + docker compose
|
|
||||||
- curl
|
|
||||||
- python
|
|
||||||
|
|
||||||
## Quickstart
|
- `docker` + `docker compose`
|
||||||
|
- `curl`
|
||||||
|
- `python`
|
||||||
|
- optional (für GPU): NVIDIA Container Toolkit + Compose GPU-Konfiguration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Struktur (Kurzüberblick)
|
||||||
|
|
||||||
|
- `bin/sqlai` – CLI (ask + Analyse-Modi) inkl. Logging & Model-Fallback
|
||||||
|
- `scripts/bootstrap.sh` – startet Container, pullt Modelle, baut Expert-Model, Warmup
|
||||||
|
- `scripts/update.sh` – pullt Runtime+Modelle, rebuild Expert-Model, Warmup
|
||||||
|
- `scripts/selftest.sh` – End-to-End Check inkl. echter Anfrage
|
||||||
|
- `docker-compose.yml` – Ollama Service (Host networking, optional GPU)
|
||||||
|
- `Modelfile` – Expert-Systemprompt (SQL Server 2022 Fokus)
|
||||||
|
- `prompts/` – Prompt-Library (Copy & Paste Templates)
|
||||||
|
- `systemd/user/` – optionaler Daily Update Timer (User Service)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1) Installation / Build (Bootstrap)
|
||||||
|
|
||||||
|
## 1.1 Repo auspacken / klonen
|
||||||
|
Wenn du ein tar.gz erhalten hast:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
tar -xzf jr-sql-ai.tar.gz
|
||||||
./scripts/bootstrap.sh
|
cd jr-sql-ai
|
||||||
|
|||||||
200
bin/sqlai
200
bin/sqlai
@@ -7,6 +7,7 @@ ENV_FILE="${ROOT}/.env"
|
|||||||
|
|
||||||
: "${OLLAMA_URL:=http://127.0.0.1:11434}"
|
: "${OLLAMA_URL:=http://127.0.0.1:11434}"
|
||||||
: "${EXPERT_MODEL:=jr-sql-expert}"
|
: "${EXPERT_MODEL:=jr-sql-expert}"
|
||||||
|
: "${BASE_MODEL:=}" # fallback
|
||||||
|
|
||||||
ts() { date -Is; }
|
ts() { date -Is; }
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ log_dir="${ROOT}/logs"
|
|||||||
mkdir -p "$log_dir"
|
mkdir -p "$log_dir"
|
||||||
log_file="${log_dir}/sqlai-$(date -I).log"
|
log_file="${log_dir}/sqlai-$(date -I).log"
|
||||||
|
|
||||||
# Log everything (stdout+stderr)
|
|
||||||
exec > >(tee -a "$log_file") 2>&1
|
exec > >(tee -a "$log_file") 2>&1
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -83,7 +83,6 @@ if [[ -z "${input//[[:space:]]/}" ]]; then
|
|||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Short instruction per mode (core policy is in Modelfile SYSTEM prompt)
|
|
||||||
case "$mode" in
|
case "$mode" in
|
||||||
ask)
|
ask)
|
||||||
instruction=$'Beantworte die Frage als SQL Server 2022 Experte.\n\nWichtig:\n- Wenn Kontext fehlt: keine Rückfragen stellen; stattdessen Annahmen offenlegen und Optionen (A/B/C) mit Vor-/Nachteilen geben.\n- Ergebnis immer strukturiert mit: Kurzfazit, Optionen, Risiken/Checks, Nächste Schritte.\n- Wenn sinnvoll: konkrete T-SQL/DDL Snippets und Checklisten liefern.'
|
instruction=$'Beantworte die Frage als SQL Server 2022 Experte.\n\nWichtig:\n- Wenn Kontext fehlt: keine Rückfragen stellen; stattdessen Annahmen offenlegen und Optionen (A/B/C) mit Vor-/Nachteilen geben.\n- Ergebnis immer strukturiert mit: Kurzfazit, Optionen, Risiken/Checks, Nächste Schritte.\n- Wenn sinnvoll: konkrete T-SQL/DDL Snippets und Checklisten liefern.'
|
||||||
@@ -112,12 +111,50 @@ esac
|
|||||||
|
|
||||||
req_id="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
|
req_id="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
|
||||||
|
|
||||||
|
# --- helper: model existence via /api/tags ---
|
||||||
|
model_exists() {
|
||||||
|
# args: modelname
|
||||||
|
local m="$1"
|
||||||
|
local tags
|
||||||
|
tags="$(curl -sS "${OLLAMA_URL}/api/tags" 2>/dev/null || true)"
|
||||||
|
[[ -z "$tags" ]] && return 1
|
||||||
|
printf '%s' "$tags" | python -c '
|
||||||
|
import json,sys
|
||||||
|
m=sys.argv[1]
|
||||||
|
try:
|
||||||
|
obj=json.load(sys.stdin)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(2)
|
||||||
|
names=set()
|
||||||
|
for it in obj.get("models", []):
|
||||||
|
n=it.get("name")
|
||||||
|
if n: names.add(n)
|
||||||
|
# Accept exact match, or ":latest" variants
|
||||||
|
ok = (m in names) or (m + ":latest" in names) or (m.endswith(":latest") and m[:-7] in names)
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
' "$m" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_model() {
|
||||||
|
# Prefer expert, fallback to base
|
||||||
|
if model_exists "$EXPERT_MODEL"; then
|
||||||
|
printf '%s' "$EXPERT_MODEL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -n "${BASE_MODEL:-}" ]] && model_exists "$BASE_MODEL"; then
|
||||||
|
echo "[$(ts)] sqlai: WARN: expert model not found in /api/tags; falling back to BASE_MODEL=${BASE_MODEL}"
|
||||||
|
printf '%s' "$BASE_MODEL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "[$(ts)] sqlai: ERROR: neither EXPERT_MODEL=${EXPERT_MODEL} nor BASE_MODEL=${BASE_MODEL:-<empty>} found (api/tags)."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
echo "================================================================================"
|
echo "================================================================================"
|
||||||
echo "[$(ts)] sqlai: REQUEST_START id=$req_id"
|
echo "[$(ts)] sqlai: REQUEST_START id=$req_id"
|
||||||
echo "[$(ts)] sqlai: MODE=$mode MODEL=$EXPERT_MODEL OLLAMA_URL=$OLLAMA_URL"
|
echo "[$(ts)] sqlai: MODE=$mode EXPERT_MODEL=$EXPERT_MODEL BASE_MODEL=${BASE_MODEL:-<empty>} OLLAMA_URL=$OLLAMA_URL"
|
||||||
echo "[$(ts)] sqlai: INPUT_SOURCE=$input_src INPUT_BYTES=$(printf "%s" "$input" | wc -c)"
|
echo "[$(ts)] sqlai: INPUT_SOURCE=$input_src INPUT_BYTES=$(printf "%s" "$input" | wc -c)"
|
||||||
|
|
||||||
# Delimited markup
|
|
||||||
prompt=$(cat <<EOF
|
prompt=$(cat <<EOF
|
||||||
${instruction}
|
${instruction}
|
||||||
|
|
||||||
@@ -127,43 +164,51 @@ ${input}
|
|||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build JSON safely (no heredoc+herestring combos)
|
payload_for_model() {
|
||||||
payload="$(
|
local model="$1"
|
||||||
printf '%s' "$prompt" | python -c '
|
printf '%s' "$prompt" | python -c '
|
||||||
import json, os, sys
|
import json, os, sys
|
||||||
model=os.environ.get("EXPERT_MODEL","jr-sql-expert")
|
model=sys.argv[1]
|
||||||
prompt=sys.stdin.read()
|
prompt=sys.stdin.read()
|
||||||
print(json.dumps({"model": model, "prompt": prompt, "stream": False}, ensure_ascii=False))
|
print(json.dumps({"model": model, "prompt": prompt, "stream": False}, ensure_ascii=False))
|
||||||
'
|
' "$model"
|
||||||
)"
|
}
|
||||||
|
|
||||||
resp_file="$(mktemp)"
|
do_request() {
|
||||||
http_code="$(
|
# args: model, attempt
|
||||||
curl -sS -o "$resp_file" -w "%{http_code}" \
|
local model="$1"
|
||||||
|
local attempt="$2"
|
||||||
|
|
||||||
|
local payload resp_file http_code resp resp_bytes extracted error_msg response_txt
|
||||||
|
|
||||||
|
payload="$(payload_for_model "$model")"
|
||||||
|
|
||||||
|
resp_file="$(mktemp)"
|
||||||
|
http_code="$(
|
||||||
|
curl -sS -o "$resp_file" -w "%{http_code}" \
|
||||||
-X POST "${OLLAMA_URL}/api/generate" \
|
-X POST "${OLLAMA_URL}/api/generate" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
--data-binary "$payload" \
|
--data-binary "$payload" \
|
||||||
|| true
|
|| true
|
||||||
)"
|
)"
|
||||||
|
|
||||||
resp="$(cat "$resp_file")"
|
resp="$(cat "$resp_file")"
|
||||||
rm -f "$resp_file"
|
rm -f "$resp_file"
|
||||||
|
|
||||||
echo "[$(ts)] sqlai: HTTP_CODE=$http_code RESP_BYTES=$(printf "%s" "$resp" | wc -c) id=$req_id"
|
resp_bytes="$(printf "%s" "$resp" | wc -c)"
|
||||||
|
echo "[$(ts)] sqlai: attempt=$attempt model=$model HTTP_CODE=$http_code RESP_BYTES=$resp_bytes id=$req_id"
|
||||||
|
|
||||||
# Validate JSON
|
# Validate JSON
|
||||||
if ! printf '%s' "$resp" | python -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then
|
if ! printf '%s' "$resp" | python -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1; then
|
||||||
echo "[$(ts)] sqlai: ERROR: response is not valid JSON id=$req_id"
|
echo "[$(ts)] sqlai: ERROR: invalid JSON response attempt=$attempt id=$req_id"
|
||||||
echo "[$(ts)] sqlai: RAW_RESPONSE_BEGIN id=$req_id"
|
echo "[$(ts)] sqlai: RAW_RESPONSE_BEGIN attempt=$attempt id=$req_id"
|
||||||
printf '%s\n' "$resp"
|
printf '%s\n' "$resp" | head -n 200
|
||||||
echo "[$(ts)] sqlai: RAW_RESPONSE_END id=$req_id"
|
echo "[$(ts)] sqlai: RAW_RESPONSE_END attempt=$attempt id=$req_id"
|
||||||
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=error"
|
return 90
|
||||||
exit 11
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract error/response/metrics in one pass
|
extracted="$(
|
||||||
extracted="$(
|
printf '%s' "$resp" | python -c '
|
||||||
printf '%s' "$resp" | python -c '
|
|
||||||
import json,sys
|
import json,sys
|
||||||
obj=json.load(sys.stdin)
|
obj=json.load(sys.stdin)
|
||||||
out={
|
out={
|
||||||
@@ -180,47 +225,72 @@ out={
|
|||||||
}
|
}
|
||||||
print(json.dumps(out, ensure_ascii=False))
|
print(json.dumps(out, ensure_ascii=False))
|
||||||
'
|
'
|
||||||
)"
|
)"
|
||||||
|
|
||||||
error_msg="$(printf '%s' "$extracted" | python -c 'import json,sys; print((json.load(sys.stdin).get("error") or "").strip())')"
|
error_msg="$(printf '%s' "$extracted" | python -c 'import json,sys; print((json.load(sys.stdin).get("error") or "").strip())')"
|
||||||
response_txt="$(printf '%s' "$extracted" | python -c 'import json,sys; print(json.load(sys.stdin).get("response") or "")')"
|
response_txt="$(printf '%s' "$extracted" | python -c 'import json,sys; print(json.load(sys.stdin).get("response") or "")')"
|
||||||
|
|
||||||
# HTTP != 200 is error
|
# HTTP != 200 -> error
|
||||||
if [[ "$http_code" != "200" ]]; then
|
if [[ "$http_code" != "200" ]]; then
|
||||||
echo "[$(ts)] sqlai: ERROR: non-200 HTTP_CODE=$http_code id=$req_id"
|
echo "[$(ts)] sqlai: ERROR: non-200 HTTP_CODE=$http_code attempt=$attempt id=$req_id"
|
||||||
if [[ -n "$error_msg" ]]; then
|
[[ -n "$error_msg" ]] && echo "[$(ts)] sqlai: OLLAMA_ERROR=$error_msg attempt=$attempt id=$req_id"
|
||||||
echo "[$(ts)] sqlai: OLLAMA_ERROR=$error_msg id=$req_id"
|
return 91
|
||||||
else
|
fi
|
||||||
echo "[$(ts)] sqlai: BODY_SNIPPET_BEGIN id=$req_id"
|
|
||||||
printf '%s\n' "$resp" | head -n 120
|
# API-level error
|
||||||
echo "[$(ts)] sqlai: BODY_SNIPPET_END id=$req_id"
|
if [[ -n "$error_msg" ]]; then
|
||||||
|
echo "[$(ts)] sqlai: ERROR: OLLAMA_ERROR=$error_msg attempt=$attempt id=$req_id"
|
||||||
|
# special code for retry decisions
|
||||||
|
if printf '%s' "$error_msg" | grep -qiE 'model .*not found|not found'; then
|
||||||
|
return 42
|
||||||
|
fi
|
||||||
|
return 92
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print answer
|
||||||
|
printf "\n%s\n\n" "$(printf "%s" "$response_txt" | sed 's/[[:space:]]*$//')"
|
||||||
|
|
||||||
|
# Print metrics optionally
|
||||||
|
if [[ "$no_metrics" != "1" ]]; then
|
||||||
|
echo "METRICS=$(printf '%s' "$extracted" | python -c 'import json,sys; import json as j; print(j.dumps(json.load(sys.stdin)["metrics"], ensure_ascii=False))')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Empty answer warning
|
||||||
|
if [[ -z "${response_txt//[[:space:]]/}" ]]; then
|
||||||
|
echo "[$(ts)] sqlai: WARN: empty response attempt=$attempt model=$model id=$req_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
model_primary="$(pick_model)"
|
||||||
|
echo "[$(ts)] sqlai: selected_model_primary=$model_primary id=$req_id"
|
||||||
|
|
||||||
|
# Attempt 1
|
||||||
|
set +e
|
||||||
|
do_request "$model_primary" "1"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Retry with BASE_MODEL if "model not found" and we weren't already using it
|
||||||
|
if [[ "$rc" -eq 42 ]]; then
|
||||||
|
if [[ -n "${BASE_MODEL:-}" ]] && [[ "$model_primary" != "$BASE_MODEL" ]] && model_exists "$BASE_MODEL"; then
|
||||||
|
echo "[$(ts)] sqlai: WARN: retrying with BASE_MODEL=$BASE_MODEL (expert model not found) id=$req_id"
|
||||||
|
set +e
|
||||||
|
do_request "$BASE_MODEL" "2"
|
||||||
|
rc2=$?
|
||||||
|
set -e
|
||||||
|
rc="$rc2"
|
||||||
|
else
|
||||||
|
echo "[$(ts)] sqlai: ERROR: retry requested but BASE_MODEL unavailable id=$req_id"
|
||||||
|
rc=93
|
||||||
fi
|
fi
|
||||||
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=error"
|
|
||||||
exit 12
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# API-level error
|
if [[ "$rc" -ne 0 ]]; then
|
||||||
if [[ -n "$error_msg" ]]; then
|
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=error rc=$rc"
|
||||||
echo "[$(ts)] sqlai: ERROR: OLLAMA_ERROR=$error_msg id=$req_id"
|
echo "================================================================================"
|
||||||
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=error"
|
exit "$rc"
|
||||||
exit 13
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Print answer
|
|
||||||
printf "\n%s\n\n" "$(printf "%s" "$response_txt" | sed 's/[[:space:]]*$//')"
|
|
||||||
|
|
||||||
# If response empty, dump JSON snippet to log
|
|
||||||
if [[ -z "${response_txt//[[:space:]]/}" ]]; then
|
|
||||||
echo "[$(ts)] sqlai: WARN: empty response id=$req_id"
|
|
||||||
echo "[$(ts)] sqlai: RAW_JSON_SNIPPET_BEGIN id=$req_id"
|
|
||||||
printf '%s\n' "$resp" | head -n 200
|
|
||||||
echo "[$(ts)] sqlai: RAW_JSON_SNIPPET_END id=$req_id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Print metrics optionally
|
|
||||||
if [[ "$no_metrics" != "1" ]]; then
|
|
||||||
metrics_line="$(printf '%s' "$extracted" | python -c 'import json,sys; print("METRICS="+json.dumps(json.load(sys.stdin)["metrics"], ensure_ascii=False))')"
|
|
||||||
echo "$metrics_line"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=ok"
|
echo "[$(ts)] sqlai: REQUEST_END id=$req_id status=ok"
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
# Create .env if missing (do not overwrite)
|
|
||||||
cp -n "${ROOT}/.env.example" "${ROOT}/.env" || true
|
cp -n "${ROOT}/.env.example" "${ROOT}/.env" || true
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "${ROOT}/.env"
|
source "${ROOT}/.env"
|
||||||
|
|
||||||
ts(){ date -Is; }
|
ts(){ date -Is; }
|
||||||
@@ -14,8 +10,6 @@ ts(){ date -Is; }
|
|||||||
log_dir="${ROOT}/logs"
|
log_dir="${ROOT}/logs"
|
||||||
mkdir -p "$log_dir"
|
mkdir -p "$log_dir"
|
||||||
log_file="${log_dir}/bootstrap-$(date -Iseconds).log"
|
log_file="${log_dir}/bootstrap-$(date -Iseconds).log"
|
||||||
|
|
||||||
# log everything (stdout+stderr)
|
|
||||||
exec > >(tee -a "$log_file") 2>&1
|
exec > >(tee -a "$log_file") 2>&1
|
||||||
|
|
||||||
echo "[$(ts)] bootstrap: starting (ROOT=$ROOT)"
|
echo "[$(ts)] bootstrap: starting (ROOT=$ROOT)"
|
||||||
@@ -51,7 +45,6 @@ echo "[$(ts)] bootstrap: building expert model: ${EXPERT_MODEL}"
|
|||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
sed "s/\${BASE_MODEL}/${BASE_MODEL}/g" "${ROOT}/Modelfile" > "$tmp"
|
sed "s/\${BASE_MODEL}/${BASE_MODEL}/g" "${ROOT}/Modelfile" > "$tmp"
|
||||||
|
|
||||||
# Copy Modelfile into container and build from explicit path (robust)
|
|
||||||
docker cp "$tmp" ollama:/tmp/Modelfile.jr-sql-expert
|
docker cp "$tmp" ollama:/tmp/Modelfile.jr-sql-expert
|
||||||
docker exec -it ollama ollama create "${EXPERT_MODEL}" -f /tmp/Modelfile.jr-sql-expert
|
docker exec -it ollama ollama create "${EXPERT_MODEL}" -f /tmp/Modelfile.jr-sql-expert
|
||||||
|
|
||||||
@@ -61,15 +54,15 @@ echo "[$(ts)] bootstrap: verifying model exists..."
|
|||||||
docker exec -it ollama ollama list | grep -F "${EXPERT_MODEL}" >/dev/null && \
|
docker exec -it ollama ollama list | grep -F "${EXPERT_MODEL}" >/dev/null && \
|
||||||
echo "[$(ts)] bootstrap: OK: ${EXPERT_MODEL} is available."
|
echo "[$(ts)] bootstrap: OK: ${EXPERT_MODEL} is available."
|
||||||
|
|
||||||
# End-to-end test
|
# Warmup
|
||||||
if [[ ! -x "${ROOT}/bin/sqlai" ]]; then
|
if [[ -x "${ROOT}/bin/sqlai" ]]; then
|
||||||
echo "[$(ts)] bootstrap: ERROR: ${ROOT}/bin/sqlai not found or not executable"
|
echo "[$(ts)] bootstrap: warmup: sending a short request (no-metrics)..."
|
||||||
ls -la "${ROOT}/bin" || true
|
"${ROOT}/bin/sqlai" ask --text "Warmup: reply with exactly 'OK'." --no-metrics || {
|
||||||
exit 2
|
echo "[$(ts)] bootstrap: WARN: warmup failed (continuing)."
|
||||||
|
}
|
||||||
|
echo "[$(ts)] bootstrap: warmup: done"
|
||||||
|
else
|
||||||
|
echo "[$(ts)] bootstrap: WARN: ${ROOT}/bin/sqlai not executable; skipping warmup."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[$(ts)] bootstrap: test (running one request)..."
|
|
||||||
echo "SELECT 1;" | "${ROOT}/bin/sqlai" analyze-tsql
|
|
||||||
echo "[$(ts)] bootstrap: test done"
|
|
||||||
|
|
||||||
echo "[$(ts)] bootstrap: done"
|
echo "[$(ts)] bootstrap: done"
|
||||||
|
|||||||
212
scripts/selftest.sh
Executable file
212
scripts/selftest.sh
Executable file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ENV_FILE="${ROOT}/.env"
|
||||||
|
|
||||||
|
ts(){ date -Is; }
|
||||||
|
|
||||||
|
log_dir="${ROOT}/logs"
|
||||||
|
mkdir -p "$log_dir"
|
||||||
|
log_file="${log_dir}/selftest-$(date -Iseconds).log"
|
||||||
|
exec > >(tee -a "$log_file") 2>&1
|
||||||
|
|
||||||
|
echo "================================================================================"
|
||||||
|
echo "[$(ts)] selftest: START ROOT=$ROOT"
|
||||||
|
|
||||||
|
# -------- helpers --------
|
||||||
|
fail() {
|
||||||
|
local msg="$1"
|
||||||
|
local code="${2:-1}"
|
||||||
|
echo "[$(ts)] selftest: FAIL code=$code msg=$msg"
|
||||||
|
echo "[$(ts)] selftest: END status=FAIL"
|
||||||
|
echo "================================================================================"
|
||||||
|
exit "$code"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "[$(ts)] selftest: WARN $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() {
|
||||||
|
echo "[$(ts)] selftest: OK $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || fail "missing command: $1" 10
|
||||||
|
}
|
||||||
|
|
||||||
|
# robust JSON check
|
||||||
|
is_json() {
|
||||||
|
# reads stdin
|
||||||
|
python -c 'import json,sys; json.load(sys.stdin)' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract model names from /api/tags JSON and check membership
|
||||||
|
model_in_tags() {
|
||||||
|
# args: modelname, tags_json_string
|
||||||
|
local model="$1"
|
||||||
|
local tags="$2"
|
||||||
|
printf '%s' "$tags" | python -c '
|
||||||
|
import json,sys
|
||||||
|
m=sys.argv[1]
|
||||||
|
obj=json.load(sys.stdin)
|
||||||
|
names=set()
|
||||||
|
for it in obj.get("models", []):
|
||||||
|
n=it.get("name")
|
||||||
|
if n: names.add(n)
|
||||||
|
ok = (m in names) or (m + ":latest" in names) or (m.endswith(":latest") and m[:-7] in names)
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
' "$model" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------- preflight --------
|
||||||
|
need_cmd docker
|
||||||
|
need_cmd curl
|
||||||
|
need_cmd python
|
||||||
|
need_cmd grep
|
||||||
|
need_cmd sed
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
warn ".env not found, creating from .env.example"
|
||||||
|
cp -n "${ROOT}/.env.example" "${ROOT}/.env" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "${ROOT}/.env"
|
||||||
|
|
||||||
|
: "${OLLAMA_URL:=http://127.0.0.1:11434}"
|
||||||
|
: "${EXPERT_MODEL:=jr-sql-expert}"
|
||||||
|
: "${BASE_MODEL:=}"
|
||||||
|
|
||||||
|
ok "loaded env: OLLAMA_URL=$OLLAMA_URL EXPERT_MODEL=$EXPERT_MODEL BASE_MODEL=${BASE_MODEL:-<empty>}"
|
||||||
|
|
||||||
|
if [[ ! -x "${ROOT}/bin/sqlai" ]]; then
|
||||||
|
fail "bin/sqlai missing or not executable at ${ROOT}/bin/sqlai" 11
|
||||||
|
fi
|
||||||
|
|
||||||
|
# docker compose availability (plugin or legacy)
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
ok "docker compose available"
|
||||||
|
else
|
||||||
|
fail "docker compose not available (install docker compose plugin)" 12
|
||||||
|
fi
|
||||||
|
|
||||||
|
# container state
|
||||||
|
if docker ps --format '{{.Names}}' | grep -qx 'ollama'; then
|
||||||
|
ok "container 'ollama' is running"
|
||||||
|
else
|
||||||
|
warn "container 'ollama' not running; attempting 'docker compose up -d'"
|
||||||
|
docker compose -f "${ROOT}/docker-compose.yml" up -d || fail "docker compose up failed" 13
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -qx 'ollama'; then
|
||||||
|
fail "container 'ollama' still not running after compose up" 14
|
||||||
|
fi
|
||||||
|
ok "container 'ollama' is running after compose up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# API wait loop
|
||||||
|
ok "waiting for Ollama API at $OLLAMA_URL ..."
|
||||||
|
api_ok="0"
|
||||||
|
for i in {1..120}; do
|
||||||
|
if curl -sS "${OLLAMA_URL}/api/tags" >/dev/null 2>&1; then
|
||||||
|
api_ok="1"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[[ "$api_ok" == "1" ]] || fail "Ollama API not reachable at $OLLAMA_URL after waiting" 20
|
||||||
|
ok "Ollama API reachable"
|
||||||
|
|
||||||
|
# Fetch /api/tags and validate JSON
|
||||||
|
tags="$(curl -sS "${OLLAMA_URL}/api/tags" || true)"
|
||||||
|
[[ -n "$tags" ]] || fail "/api/tags returned empty body" 21
|
||||||
|
|
||||||
|
if ! printf '%s' "$tags" | is_json; then
|
||||||
|
echo "[$(ts)] selftest: /api/tags RAW_BEGIN"
|
||||||
|
printf '%s\n' "$tags" | head -n 200
|
||||||
|
echo "[$(ts)] selftest: /api/tags RAW_END"
|
||||||
|
fail "/api/tags did not return JSON" 22
|
||||||
|
fi
|
||||||
|
ok "/api/tags returned valid JSON"
|
||||||
|
|
||||||
|
# Model availability checks
|
||||||
|
expert_present="0"
|
||||||
|
base_present="0"
|
||||||
|
|
||||||
|
if model_in_tags "$EXPERT_MODEL" "$tags"; then
|
||||||
|
expert_present="1"
|
||||||
|
ok "EXPERT_MODEL present: $EXPERT_MODEL"
|
||||||
|
else
|
||||||
|
warn "EXPERT_MODEL not present in tags: $EXPERT_MODEL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${BASE_MODEL:-}" ]] && model_in_tags "$BASE_MODEL" "$tags"; then
|
||||||
|
base_present="1"
|
||||||
|
ok "BASE_MODEL present: $BASE_MODEL"
|
||||||
|
else
|
||||||
|
if [[ -n "${BASE_MODEL:-}" ]]; then
|
||||||
|
warn "BASE_MODEL not present in tags: $BASE_MODEL"
|
||||||
|
else
|
||||||
|
warn "BASE_MODEL empty in .env"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$expert_present" != "1" && "$base_present" != "1" ]]; then
|
||||||
|
warn "No expert/base model found. Attempting to pull BASE_MODEL if set..."
|
||||||
|
if [[ -n "${BASE_MODEL:-}" ]]; then
|
||||||
|
docker exec -it ollama ollama pull "${BASE_MODEL}" || warn "ollama pull BASE_MODEL failed"
|
||||||
|
tags2="$(curl -sS "${OLLAMA_URL}/api/tags" || true)"
|
||||||
|
if printf '%s' "$tags2" | is_json && model_in_tags "$BASE_MODEL" "$tags2"; then
|
||||||
|
ok "BASE_MODEL now present after pull: $BASE_MODEL"
|
||||||
|
base_present="1"
|
||||||
|
else
|
||||||
|
fail "Neither EXPERT_MODEL nor BASE_MODEL available after attempted pull" 23
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "Neither EXPERT_MODEL nor BASE_MODEL available and BASE_MODEL is empty" 24
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Warmup request
|
||||||
|
ok "warmup request via sqlai (no-metrics)"
|
||||||
|
set +e
|
||||||
|
"${ROOT}/bin/sqlai" ask --text "Warmup: reply with exactly 'OK'." --no-metrics
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$rc" -ne 0 ]]; then
|
||||||
|
warn "warmup failed rc=$rc (continuing to real query)"
|
||||||
|
else
|
||||||
|
ok "warmup succeeded"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Real query (slightly longer)
|
||||||
|
ok "real query via sqlai (ask)"
|
||||||
|
set +e
|
||||||
|
out="$("${ROOT}/bin/sqlai" ask --text "Give a concise checklist to troubleshoot parameter sniffing in SQL Server 2022. Keep it technical." --no-metrics 2>&1)"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$rc" -ne 0 ]]; then
|
||||||
|
echo "[$(ts)] selftest: sqlai output BEGIN"
|
||||||
|
printf '%s\n' "$out" | tail -n 200
|
||||||
|
echo "[$(ts)] selftest: sqlai output END"
|
||||||
|
fail "real query failed rc=$rc" 30
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Basic sanity: must contain at least some non-empty text
|
||||||
|
if [[ -z "${out//[[:space:]]/}" ]]; then
|
||||||
|
fail "real query returned empty output" 31
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "real query returned non-empty output"
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "--------------------------------------------------------------------------------"
|
||||||
|
echo "[$(ts)] selftest: SUMMARY"
|
||||||
|
echo "[$(ts)] selftest: API=OK"
|
||||||
|
echo "[$(ts)] selftest: EXPERT_MODEL_PRESENT=$expert_present"
|
||||||
|
echo "[$(ts)] selftest: BASE_MODEL_PRESENT=$base_present"
|
||||||
|
echo "[$(ts)] selftest: LOG_FILE=$log_file"
|
||||||
|
echo "--------------------------------------------------------------------------------"
|
||||||
|
|
||||||
|
echo "[$(ts)] selftest: END status=OK"
|
||||||
|
echo "================================================================================"
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "${ROOT}/.env"
|
source "${ROOT}/.env"
|
||||||
|
|
||||||
ts(){ date -Is; }
|
ts(){ date -Is; }
|
||||||
@@ -11,8 +9,6 @@ ts(){ date -Is; }
|
|||||||
log_dir="${ROOT}/logs"
|
log_dir="${ROOT}/logs"
|
||||||
mkdir -p "$log_dir"
|
mkdir -p "$log_dir"
|
||||||
log_file="${log_dir}/update-$(date -Iseconds).log"
|
log_file="${log_dir}/update-$(date -Iseconds).log"
|
||||||
|
|
||||||
# log everything (stdout+stderr)
|
|
||||||
exec > >(tee -a "$log_file") 2>&1
|
exec > >(tee -a "$log_file") 2>&1
|
||||||
|
|
||||||
echo "[$(ts)] update: starting (ROOT=$ROOT)"
|
echo "[$(ts)] update: starting (ROOT=$ROOT)"
|
||||||
@@ -60,4 +56,15 @@ echo "[$(ts)] update: verifying model exists..."
|
|||||||
docker exec -it ollama ollama list | grep -F "${EXPERT_MODEL}" >/dev/null && \
|
docker exec -it ollama ollama list | grep -F "${EXPERT_MODEL}" >/dev/null && \
|
||||||
echo "[$(ts)] update: OK: ${EXPERT_MODEL} is available."
|
echo "[$(ts)] update: OK: ${EXPERT_MODEL} is available."
|
||||||
|
|
||||||
|
# Warmup (loads model + GPU kernels, reduces first real query latency)
|
||||||
|
if [[ -x "${ROOT}/bin/sqlai" ]]; then
|
||||||
|
echo "[$(ts)] update: warmup: sending a short request (no-metrics)..."
|
||||||
|
"${ROOT}/bin/sqlai" ask --text "Warmup: reply with exactly 'OK'." --no-metrics || {
|
||||||
|
echo "[$(ts)] update: WARN: warmup failed (continuing)."
|
||||||
|
}
|
||||||
|
echo "[$(ts)] update: warmup: done"
|
||||||
|
else
|
||||||
|
echo "[$(ts)] update: WARN: ${ROOT}/bin/sqlai not executable; skipping warmup."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[$(ts)] update: complete"
|
echo "[$(ts)] update: complete"
|
||||||
|
|||||||
Reference in New Issue
Block a user