Add backstage Ollama helper for diagnostics

This commit is contained in:
juvdiaz 2026-06-27 17:56:15 -06:00
parent acb6817c98
commit 8488c9523e
4 changed files with 163 additions and 0 deletions

View File

@ -853,6 +853,21 @@ other nodes can reach `OLLAMA_HOST=http://192.168.100.73:11434`:
./jeannie website-ollama-listen
```
`jeannie` also has a backstage local AI helper for doctor commands.
It is not a separate CLI action; when `ai_gateway.enabled` is true in
`homelab.yml`, doctor commands try the configured Ollama model and silently
skip the helper if Ollama is down. The default lightweight model is:
```bash
ollama pull qwen2.5:0.5b
```
Disable it for low-CPU sessions with:
```bash
LAB_BACKSTAGE_BRAIN_ENABLED=false ./jeannie doctor-edge
```
The CV page has two client-side presentation modes:
- `Elegant`: dark, minimal, terminal-inspired styling with a square profile

View File

@ -853,6 +853,21 @@ other nodes can reach `OLLAMA_HOST=http://192.168.100.73:11434`:
./{{ main_script }} website-ollama-listen
```
`{{ main_script }}` also has a backstage local AI helper for doctor commands.
It is not a separate CLI action; when `ai_gateway.enabled` is true in
`homelab.yml`, doctor commands try the configured Ollama model and silently
skip the helper if Ollama is down. The default lightweight model is:
```bash
ollama pull qwen2.5:0.5b
```
Disable it for low-CPU sessions with:
```bash
LAB_BACKSTAGE_BRAIN_ENABLED=false ./{{ main_script }} doctor-edge
```
The CV page has two client-side presentation modes:
- `Elegant`: dark, minimal, terminal-inspired styling with a square profile

View File

@ -117,3 +117,10 @@ pimox:
worker_base_vmid: 9010
default_worker_count: 1
worker_name_prefix: pimox-worker
ai_gateway:
provider: ollama
enabled: true
url: http://192.168.100.73:11434
model: qwen2.5:0.5b
timeout_seconds: 20

126
jeannie
View File

@ -60,6 +60,11 @@ mapping = {
"services.local_registry.endpoint": "LAB_REGISTRY_ENDPOINT",
"services.rpi_dns.pihole_web_port": "PIHOLE_WEB_PORT",
"services.rpi_dns.uptime_kuma_port": "UPTIME_KUMA_PORT",
"ai_gateway.provider": "LAB_AI_GATEWAY_PROVIDER",
"ai_gateway.enabled": "LAB_BACKSTAGE_BRAIN_ENABLED",
"ai_gateway.url": "LAB_AI_GATEWAY_URL",
"ai_gateway.model": "LAB_AI_GATEWAY_MODEL",
"ai_gateway.timeout_seconds": "LAB_AI_GATEWAY_TIMEOUT_SECONDS",
"pimox.worker_base_vmid": "LAB_PIMOX_WORKER_BASE_VMID",
"pimox.default_worker_count": "LAB_PIMOX_WORKER_COUNT",
}
@ -3822,6 +3827,89 @@ status_http() {
fi
}
backstage_brain_enabled() {
local provider="${LAB_AI_GATEWAY_PROVIDER:-ollama}"
local enabled="${LAB_BACKSTAGE_BRAIN_ENABLED:-false}"
[[ "${provider,,}" == "ollama" ]] && truthy "${enabled}"
}
backstage_brain_note() {
local topic="$1"
local endpoint="${LAB_AI_GATEWAY_URL:-${LAB_OLLAMA_URL:-http://127.0.0.1:11434}}"
local model="${LAB_AI_GATEWAY_MODEL:-qwen2.5:0.5b}"
local timeout="${LAB_AI_GATEWAY_TIMEOUT_SECONDS:-20}"
local context
if ! backstage_brain_enabled; then
return 0
fi
if ! command -v python3 >/dev/null 2>&1; then
return 0
fi
context="$(cat)"
BACKSTAGE_CONTEXT="${context}" python3 - "${endpoint}" "${model}" "${timeout}" "${topic}" <<'PY' || true
import json
import os
import sys
import urllib.error
import urllib.request
endpoint, model, timeout, topic = sys.argv[1:5]
context = os.environ.get("BACKSTAGE_CONTEXT", "").strip()
try:
timeout_seconds = int(timeout)
except ValueError:
timeout_seconds = 20
prompt = f"""You are the backstage helper for a personal homelab script named jeannie.
Be concise and operational. Do not invent facts. Use only the supplied context.
Return at most 5 bullets. Focus on likely cause, next command, and risk.
Topic: {topic}
Context:
{context}
"""
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.1,
"num_predict": 220,
},
}
try:
tags_request = urllib.request.Request(f"{endpoint.rstrip('/')}/api/tags")
with urllib.request.urlopen(tags_request, timeout=3) as response:
tags = json.loads(response.read().decode("utf-8"))
available = {item.get("name", "") for item in tags.get("models", [])}
if model not in available and f"{model}:latest" not in available:
print(f"\n== Backstage Notes ==\nOllama is reachable, but model '{model}' is not pulled. Run: ollama pull {model}")
raise SystemExit(0)
request = urllib.request.Request(
f"{endpoint.rstrip('/')}/api/generate",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
result = json.loads(response.read().decode("utf-8"))
except (OSError, TimeoutError, urllib.error.URLError, json.JSONDecodeError):
raise SystemExit(0)
note = (result.get("response") or "").strip()
if note:
print("\n== Backstage Notes ==")
print(note)
PY
}
status_systemd_units() {
local unit
@ -4104,6 +4192,16 @@ Next steps:
- If only Gitea is unreachable, run ./jeannie doctor-gitea.
- If both are unreachable from OCI, check Tailscale routes and bootstrap/edge.
- Runbook: docs/runbooks/edge-failures.md
EOF
backstage_brain_note "doctor-edge" <<EOF
Doctor command: doctor-edge
Public domain: ${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/
Public Gitea URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}
OCI edge SSH target: ${edge_user}@${edge_host}
Traefik backend from edge: http://${traefik_ip}:80/
Gitea backend from edge: http://${gitea_ts_ip}:${gitea_http_port}/
Known runbook: docs/runbooks/edge-failures.md
EOF
}
@ -4140,6 +4238,16 @@ Next steps:
- If local works but public /git/ fails, check Tailscale and bootstrap/edge.
- If Git SSH fails, check the gitea remote and registered SSH key.
- Runbook: docs/runbooks/gitea-failures.md
EOF
backstage_brain_note "doctor-gitea" <<EOF
Doctor command: doctor-gitea
Gitea local URL: http://127.0.0.1:${gitea_http_port}/
Gitea LAN host: ${gitea_host}
Gitea Tailscale backend: http://${gitea_ts_ip}:${gitea_http_port}/
Gitea SSH port: ${gitea_ssh_port}
Gitea public URL: ${LAB_GITEA_ROOT_URL:-${LAB_PUBLIC_URL:-https://lab2025.duckdns.org}/git/}
Known runbook: docs/runbooks/gitea-failures.md
EOF
}
@ -4155,6 +4263,15 @@ Next steps:
- If Pi-hole works but Unbound fails, DNS should still resolve through configured public fallbacks.
- If Pi-hole DNS fails, clients using the RPi as DNS will be affected.
- Reapply the stack with ./jeannie rpi-services.
EOF
backstage_brain_note "doctor-rpi" <<EOF
Doctor command: doctor-rpi
RPi LAN host: ${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}
RPi Tailscale IP: ${LAB_RPI_TAILSCALE_IP:-unknown}
Docker NVMe root: ${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}
Docker fallback root: ${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}
Services: Pi-hole, Unbound, Uptime Kuma
EOF
}
@ -4182,6 +4299,15 @@ Next steps:
- If Pimox workers are stopped, start them with ./jeannie start-cluster.
- If kubelet versions drift, run ./jeannie doctor-versions.
- Runbook: docs/runbooks/cluster-stop-start-failures.md
EOF
backstage_brain_note "doctor-cluster" <<EOF
Doctor command: doctor-cluster
Kubeconfig path: ${KUBECONFIG_PATH}
Traefik LoadBalancer IP: ${LAB_TRAEFIK_LB_IP:-192.168.100.240}
Pimox host: ${LAB_PIMOX_USER:-${TF_VAR_pimox_user:-jv}}@${LAB_PIMOX_HOST:-${TF_VAR_pimox_host:-192.168.100.80}}
Worker storage: ${LAB_PIMOX_WORKER_STORAGE:-${TF_VAR_pimox_worker_storage:-opi5_ssd}}
Known runbook: docs/runbooks/cluster-stop-start-failures.md
EOF
}