From 7eec0f58d0958f98b526badacda43fa67d990e09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 12:57:27 +0000 Subject: [PATCH] add CI workflow, install/run scripts, and quick-start docs - .github/workflows/ci.yml runs pytest across Python 3.10/3.11/3.12, syntax-checks frontend JS via node --check, validates JSON, and smoke-tests install.sh + run.sh with Ollama mocked out. - install.sh is idempotent: detects Python >=3.10, creates backend/.venv, installs deps, probes Ollama, pulls the default model if the daemon is reachable. Never installs system binaries. - run.sh launches uvicorn with TUTOR_SERVE_FRONTEND=1 so the backend serves both the API and the static UI on a single port. - docs/install-runtime-workflow.md compares five install-to-runtime flows and documents the chosen two-script blend. - README gains an idiot-proof Quick start section with troubleshooting and expected behaviour when Ollama is offline. - scripts/smoke_run.sh exercises /api/health and / for CI and local verification. --- .github/workflows/ci.yml | 126 +++++++++++++++++ .gitignore | 21 +++ README.md | 56 +++++++- docs/install-runtime-workflow.md | 230 +++++++++++++++++++++++++++++++ install.sh | 133 ++++++++++++++++++ run.sh | 66 +++++++++ scripts/smoke_run.sh | 35 +++++ 7 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 docs/install-runtime-workflow.md create mode 100755 install.sh create mode 100755 run.sh create mode 100755 scripts/smoke_run.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb2fc52 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend tests (pytest) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + backend/requirements.txt + backend/requirements-dev.txt + + - name: Install backend dependencies + working-directory: backend + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run pytest + working-directory: backend + run: pytest -q + + frontend: + name: Frontend JS syntax check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Syntax-check frontend JS + run: | + set -e + for f in frontend/*.js; do + echo "node --check $f" + node --check "$f" + done + + - name: Validate JSON content + run: | + set -e + python3 -c " + import json, pathlib, sys + errors = 0 + for p in list(pathlib.Path('frontend').rglob('*.json')) + list(pathlib.Path('curriculum').rglob('*.json')): + try: + json.loads(p.read_text()) + print(f'ok {p}') + except Exception as e: + print(f'ERR {p}: {e}', file=sys.stderr) + errors += 1 + sys.exit(1 if errors else 0) + " + + scripts: + name: Shell script lint + smoke + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: shellcheck install/run scripts + run: | + if [ -f install.sh ] || [ -f run.sh ]; then + sudo apt-get update -y + sudo apt-get install -y shellcheck + for f in install.sh run.sh; do + [ -f "$f" ] && shellcheck -x "$f" + done + else + echo "no install.sh / run.sh yet; skipping" + fi + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Smoke-test install.sh (skip model pull, skip Ollama) + env: + TUTOR_SKIP_OLLAMA: "1" + TUTOR_SKIP_MODEL_PULL: "1" + TUTOR_NONINTERACTIVE: "1" + run: | + if [ -f install.sh ]; then + chmod +x install.sh run.sh scripts/smoke_run.sh + ./install.sh + test -d backend/.venv + backend/.venv/bin/python -c "import fastapi, uvicorn, httpx, pydantic" + else + echo "install.sh missing; skipping smoke test" + fi + + - name: Smoke-test run.sh (no Ollama; check /api/health and /) + env: + TUTOR_SKIP_OLLAMA: "1" + TUTOR_PORT: "8801" + run: | + if [ -f scripts/smoke_run.sh ]; then + chmod +x scripts/smoke_run.sh + ./scripts/smoke_run.sh + else + echo "scripts/smoke_run.sh missing; skipping run.sh smoke" + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e607d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Virtualenvs +.venv/ +backend/.venv/ +venv/ + +# OS / editor +.DS_Store +.idea/ +.vscode/ + +# Logs / runtime +/tmp/ +*.log diff --git a/README.md b/README.md index 3e73590..4df7d41 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,61 @@ flowchart TD └── 0001-offline-first-local-llm.md ``` -## Running the Frontend +## Quick start (idiot-proof) + +Two commands. macOS and Linux. Python 3.10+. Ollama is optional for a +first look at the UI; required for chat replies and code evaluation. + +```bash +gh repo clone StewAlexander-com/python-tutor +cd python-tutor +./install.sh # creates venv, installs deps, pulls model if Ollama is up +./run.sh # serves UI + API on http://localhost:8001/ +``` + +Then open in your browser. You'll see the +lesson list, the inline code lab (Run / Evaluate), and the floating +"Ask tutor" chat panel. + +### Expected behaviour when Ollama is not running + +- The web UI loads normally — you can read lessons and run code locally + (`POST /api/run` does not need the LLM). +- `/api/health` reports `status: "degraded"` and `ollama_reachable: false`. +- `Evaluate` and the chat panel return a clear 503 — they don't hang. +- As soon as you start `ollama serve`, everything works without a restart. + +### What if Ollama isn't installed? + +`install.sh` and `run.sh` **never** install system binaries on your +behalf. If Ollama is missing, they print exactly what to run: + +```bash +# macOS +brew install ollama && ollama serve & + +# Linux +curl -fsSL https://ollama.com/install.sh | sh && ollama serve & +``` + +Then `./install.sh` again to pull `gemma3:4b`. + +### Troubleshooting + +| Symptom | Fix | +| -------------------------------------- | ------------------------------------------------------------------------------------------- | +| `Python 3.10+ is required` | Install Python (see [`docs/install-runtime-workflow.md`](docs/install-runtime-workflow.md)) | +| Chat returns `ollama_reachable: false` | Run `ollama serve` in another terminal | +| Port 8001 already in use | `TUTOR_PORT=9001 ./run.sh` | +| Model missing in chat replies | `ollama pull gemma3:4b` (or set `TUTOR_MODEL` to your model) | +| Service worker shows stale UI | Hard-refresh the browser (Cmd/Ctrl-Shift-R) | +| `install.sh` failed mid-`pip install` | Re-run it — it's idempotent and reuses the venv | + +For the design rationale behind the two-script flow (and the five flows +we evaluated), see +[`docs/install-runtime-workflow.md`](docs/install-runtime-workflow.md). + +## Running the Frontend (manual) A static, dependency-free SPA lives in [`frontend/`](frontend/). It was adapted from the [Python Power User](https://github.com/StewAlexander-com/Python-Power-User) project (MIT) and provides the learner-facing UI for this framework. diff --git a/docs/install-runtime-workflow.md b/docs/install-runtime-workflow.md new file mode 100644 index 0000000..c522beb --- /dev/null +++ b/docs/install-runtime-workflow.md @@ -0,0 +1,230 @@ +# Installation-to-Runtime Workflow + +Goal: take a learner from `git clone` to a working Python-tutor web UI in +the browser with the fewest steps, the least human intervention, the +easiest cognitive load, and the lowest risk of mysterious failure. + +The hard constraint: the tutor depends on a **local LLM** (Ollama by +default). We cannot ship Ollama itself — it must come from the host OS. +Anything we *can* automate, we automate; anything we *can't*, we surface +loudly with a single clear remediation line. + +This document presents five candidate flows, evaluates them on the same +axes, and explains the blend that ships as `./install.sh` + `./run.sh`. + +## Evaluation axes + +- **Steps** — number of distinct commands the learner types. +- **Human intervention** — how many decisions or environment edits the + user must make outside their terminal. +- **Ease** — cognitive load (one tool vs. many; one URL vs. many). +- **Risk** — likelihood of a silent-but-broken setup (e.g. Ollama not + installed, wrong Python version, two terminals out of sync). + +--- + +## Candidate A — Documented Manual Setup (status quo) + +The README lists every command. The user runs them by hand. + +```mermaid +flowchart TD + Clone[git clone repo] --> ReadDocs[Read README] + ReadDocs --> InstallOllama[Install Ollama yourself] + InstallOllama --> PullModel[ollama pull gemma3:4b] + PullModel --> Venv[python3 -m venv .venv] + Venv --> PipInstall[pip install -r requirements.txt] + PipInstall --> StartBackend[uvicorn app.main:app ...] + StartBackend --> StartFrontend[python3 -m http.server 8000] + StartFrontend --> Browser[Open http://localhost:8000] +``` + +- Steps: ~7 +- Human intervention: high (multiple commands, two terminals, env vars) +- Ease: low — copy/paste discipline required +- Risk: high — easy to skip `ollama serve`, mix ports, or forget the + `TUTOR_SERVE_FRONTEND=1` flag + +## Candidate B — Docker / docker-compose + +One image bundles backend + frontend; Ollama runs as a separate service. + +```mermaid +flowchart TD + Clone[git clone repo] --> Compose[docker compose up] + Compose --> BackendC[backend container] + Compose --> OllamaC[ollama container] + OllamaC --> PullModel[exec: ollama pull gemma3:4b] + BackendC --> Browser[Open http://localhost:8001] +``` + +- Steps: 2 (if Docker installed) + 1 model pull +- Human intervention: low *after* Docker is installed +- Ease: medium — Docker itself is a heavy prerequisite for many learners +- Risk: medium — GPU passthrough is finicky; Ollama-in-container loses + Apple Silicon Metal acceleration + +## Candidate C — One Big Bootstrap Script + +A single `./install.sh` that installs Ollama (via Homebrew/curl), pulls +the model, creates the venv, installs deps, and launches everything. + +```mermaid +flowchart TD + Clone[git clone repo] --> Bootstrap[./install.sh] + Bootstrap --> Detect[Detect OS/arch] + Detect --> InstallO[Install Ollama if missing] + InstallO --> PullM[Pull gemma3:4b] + PullM --> Venv[Create venv + pip install] + Venv --> Launch[Start uvicorn serving frontend] + Launch --> Browser[Open http://localhost:8001] +``` + +- Steps: 1 +- Human intervention: low — but the script touches the system (installs + Ollama, may require sudo or Homebrew) +- Ease: high +- Risk: medium-high — installing system-level binaries on someone else's + machine is invasive; failures here are confusing and hard to undo + +## Candidate D — Two Scripts: install + run + +Split responsibilities. `install.sh` is idempotent and never starts +servers. `run.sh` only starts the server. Ollama is *checked*, not +installed: if missing, the script prints exact installation instructions +and exits non-zero. + +```mermaid +flowchart TD + Clone[git clone repo] --> Install[./install.sh] + Install --> VenvD[Create/refresh venv] + VenvD --> PipD[pip install -r requirements.txt] + PipD --> CheckO{Ollama installed?} + CheckO -- yes --> CheckS{ollama serve running?} + CheckO -- no --> InstrO[Print install command, exit 0] + CheckS -- yes --> Pull[Pull default model if missing] + CheckS -- no --> InstrS[Print 'run: ollama serve', exit 0] + Pull --> Done[Done] + Done --> Run[./run.sh] + InstrO --> Run + InstrS --> Run + Run --> CheckO2{Ollama reachable?} + CheckO2 -- yes --> Launch[Start uvicorn serving frontend] + CheckO2 -- no --> WarnRun[Warn but still launch; chat will 503] + Launch --> Browser[Open http://localhost:8001] + WarnRun --> Browser +``` + +- Steps: 2 (`./install.sh` then `./run.sh`) +- Human intervention: low — install Ollama once, manually, with the + exact line we print +- Ease: high — both scripts have a single, obvious purpose +- Risk: low — we never silently install system binaries; we never claim + success if dependencies are missing; the UI still loads even without + Ollama, so the user can read the lessons + +## Candidate E — Make + targets + +Same as D but driven by `make install`, `make run`, `make test`. + +```mermaid +flowchart TD + Clone[git clone repo] --> MakeI[make install] + MakeI --> MakeR[make run] + MakeR --> Browser[Open http://localhost:8001] +``` + +- Steps: 2 +- Human intervention: low +- Ease: medium — assumes `make` is installed and the user is comfortable + with Makefiles +- Risk: low + +--- + +## Comparison table + +| Flow | Steps | Intervention | Ease | Risk | Notes | +| ---- | ----- | ------------ | ---- | ---- | -------------------------------------- | +| A | ~7 | high | low | high | Status quo; easy to miss a step | +| B | 2 | low | med | med | Docker prerequisite; loses Metal | +| C | 1 | low | high | med+ | Invasive — installs system packages | +| D | 2 | low | high | low | Scripts check, never silently install | +| E | 2 | low | med | low | Same as D, gated on `make` being there | + +## Decision + +We ship **Candidate D, blended with one ergonomic touch from C**. + +- **From D**: two-script split (`install.sh`, `run.sh`); we *detect* + Ollama rather than installing it; we never start daemons in + `install.sh`; the run-time server still launches if Ollama is down so + the UI is usable and the failure is observable in the chat panel. +- **From C**: in `install.sh`, if Ollama *is* present, we offer to + `ollama pull` the default model on the user's behalf — gated by + `TUTOR_SKIP_MODEL_PULL` and skippable in CI. Pulling a model the user + already chose to have Ollama for is low-risk and saves a step. + +This blend has: + +- 2 commands typed (`./install.sh`, `./run.sh`) — same as B/D/E. +- Zero hidden system-level installs. +- One actionable error message if Ollama is missing (we print the + install command for the user's platform). +- A web UI that loads even when the LLM is unreachable — so the learner + always gets *something* to interact with. + +## How the scripts behave + +### `install.sh` + +1. Detect Python ≥3.10. If missing or too old, print install command, + exit 1. +2. Create `backend/.venv` if it doesn't exist; otherwise reuse it. +3. `pip install -r backend/requirements-dev.txt` (idempotent). +4. Check `ollama` on `PATH`. If missing, print install command and exit + 0 (success — the Python side is set up). User can re-run install + later, or just run. +5. If `ollama` is present, probe `http://localhost:11434/api/tags`. If + the daemon is up, pull the default model (skippable via + `TUTOR_SKIP_MODEL_PULL=1`). If the daemon is down, print + `ollama serve &` and continue. +6. Print next-step banner: `./run.sh`. + +### `run.sh` + +1. Ensure venv exists (re-run `install.sh` if not). +2. Probe Ollama; warn if unreachable but continue. +3. Launch uvicorn with `TUTOR_SERVE_FRONTEND=1` so the backend serves + the static frontend on the same port. +4. Print the URL: `http://localhost:8001/`. + +### Environment overrides + +- `TUTOR_PORT` — backend port (default 8001). +- `TUTOR_HOST` — bind address (default 127.0.0.1). +- `TUTOR_MODEL` — Ollama model tag (default `gemma3:4b`). +- `TUTOR_SKIP_OLLAMA=1` — skip every Ollama probe (CI/offline-dev). +- `TUTOR_SKIP_MODEL_PULL=1` — skip `ollama pull` in install. +- `TUTOR_NONINTERACTIVE=1` — never prompt; assume defaults. + +## What the user does + +```bash +gh repo clone StewAlexander-com/python-tutor +cd python-tutor +./install.sh # ~2 min cold; reuses cache on re-run +./run.sh # opens at http://localhost:8001/ +``` + +If Ollama is missing, `install.sh` will tell them exactly what to type: + +```bash +# macOS +brew install ollama && ollama serve & + +# Linux +curl -fsSL https://ollama.com/install.sh | sh && ollama serve & +``` + +Then `./install.sh && ./run.sh` again. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..190fa7a --- /dev/null +++ b/install.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# install.sh — idempotent setup for the offline Python tutor. +# +# What this script does: +# 1. Verifies Python >= 3.10. +# 2. Creates backend/.venv if missing. +# 3. Installs backend dependencies (dev extras included for tests). +# 4. Checks whether Ollama is installed and running, and offers to pull +# the default model. Never installs Ollama itself. +# +# What this script does NOT do: +# - Start a server. +# - Install Ollama, brew, curl, or any system package. +# - Modify files outside the repository. +# +# Environment overrides: +# TUTOR_MODEL default "gemma3:4b" +# TUTOR_SKIP_OLLAMA=1 skip every Ollama probe +# TUTOR_SKIP_MODEL_PULL=1 skip the `ollama pull` step +# TUTOR_NONINTERACTIVE=1 never prompt; assume defaults +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")" && pwd)" +cd "$repo_root" + +# ----- pretty output --------------------------------------------------------- +if [ -t 1 ]; then + c_red='\033[31m'; c_grn='\033[32m'; c_yel='\033[33m'; c_blu='\033[34m'; c_off='\033[0m' +else + c_red=''; c_grn=''; c_yel=''; c_blu=''; c_off='' +fi +say() { printf "%b%s%b\n" "$c_blu" "[install] $*" "$c_off"; } +ok() { printf "%b%s%b\n" "$c_grn" "[install] $*" "$c_off"; } +warn() { printf "%b%s%b\n" "$c_yel" "[install] $*" "$c_off"; } +err() { printf "%b%s%b\n" "$c_red" "[install] $*" "$c_off" >&2; } + +TUTOR_MODEL="${TUTOR_MODEL:-gemma3:4b}" +TUTOR_SKIP_OLLAMA="${TUTOR_SKIP_OLLAMA:-0}" +TUTOR_SKIP_MODEL_PULL="${TUTOR_SKIP_MODEL_PULL:-0}" +TUTOR_NONINTERACTIVE="${TUTOR_NONINTERACTIVE:-0}" + +# ----- 1. Python ------------------------------------------------------------- +PY="" +for candidate in python3.12 python3.11 python3.10 python3; do + if command -v "$candidate" >/dev/null 2>&1; then + ver="$("$candidate" -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null || echo "0.0")" + major="${ver%%.*}" + minor="${ver##*.}" + if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then + PY="$candidate" + break + fi + fi +done + +if [ -z "$PY" ]; then + err "Python 3.10+ is required and was not found on PATH." + err "macOS: brew install python@3.12" + err "Debian: sudo apt-get install python3.12 python3.12-venv" + err "Fedora: sudo dnf install python3.12" + exit 1 +fi +ok "using $PY ($("$PY" --version 2>&1))" + +# ----- 2. venv --------------------------------------------------------------- +venv_dir="backend/.venv" +if [ ! -d "$venv_dir" ]; then + say "creating virtualenv at $venv_dir" + "$PY" -m venv "$venv_dir" +else + ok "venv already present at $venv_dir" +fi + +# Validate the venv actually works (handles partial/corrupt venvs). +if ! "$venv_dir/bin/python" -c "import sys" >/dev/null 2>&1; then + warn "venv at $venv_dir looks broken; recreating" + rm -rf "$venv_dir" + "$PY" -m venv "$venv_dir" +fi + +# ----- 3. dependencies ------------------------------------------------------- +say "upgrading pip and installing backend deps" +"$venv_dir/bin/python" -m pip install --upgrade --quiet pip +"$venv_dir/bin/pip" install --quiet -r backend/requirements-dev.txt +ok "backend dependencies installed" + +# ----- 4. Ollama ------------------------------------------------------------- +if [ "$TUTOR_SKIP_OLLAMA" = "1" ]; then + warn "TUTOR_SKIP_OLLAMA=1 — skipping Ollama checks" +else + if ! command -v ollama >/dev/null 2>&1; then + warn "ollama is not installed." + warn " macOS: brew install ollama && ollama serve &" + warn " Linux: curl -fsSL https://ollama.com/install.sh | sh && ollama serve &" + warn "Re-run ./install.sh after installing Ollama to pull the default model." + warn "The web UI will still work — chat replies will fail until Ollama is up." + else + ok "ollama is installed ($(command -v ollama))" + # Probe the daemon. + if curl -fsS --max-time 2 http://localhost:11434/api/tags >/dev/null 2>&1; then + ok "ollama daemon is reachable on http://localhost:11434" + if [ "$TUTOR_SKIP_MODEL_PULL" = "1" ]; then + warn "TUTOR_SKIP_MODEL_PULL=1 — skipping model pull" + else + # Does the model already exist locally? + if curl -fsS --max-time 2 http://localhost:11434/api/tags 2>/dev/null \ + | grep -F -q "\"$TUTOR_MODEL\""; then + ok "model '$TUTOR_MODEL' already present" + else + say "pulling model '$TUTOR_MODEL' (this can take several minutes)…" + if ollama pull "$TUTOR_MODEL"; then + ok "model '$TUTOR_MODEL' ready" + else + warn "ollama pull failed. You can retry later with: ollama pull $TUTOR_MODEL" + fi + fi + fi + else + warn "ollama is installed but the daemon is not running." + warn "Start it in another terminal: ollama serve" + warn "Then re-run ./install.sh to pull the default model." + fi + fi +fi + +# ----- next step ------------------------------------------------------------- +echo +ok "install complete." +echo +echo "Next step:" +echo " ./run.sh # starts the tutor at http://localhost:8001/" +echo +echo "Then open http://localhost:8001/ in your browser." diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..ced607f --- /dev/null +++ b/run.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# run.sh — launch the Python tutor backend, which also serves the frontend. +# +# Reads: +# TUTOR_HOST default 127.0.0.1 +# TUTOR_PORT default 8001 +# TUTOR_MODEL default gemma3:4b (forwarded to backend) +# TUTOR_SKIP_OLLAMA=1 skip the Ollama probe (still launches the server) +# +# If Ollama is unreachable we WARN but still start the server, so the user +# can browse lessons and exercises. Chat replies will fail with a clear +# 503 from the backend until Ollama is up. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")" && pwd)" +cd "$repo_root" + +if [ -t 1 ]; then + c_red='\033[31m'; c_grn='\033[32m'; c_yel='\033[33m'; c_blu='\033[34m'; c_off='\033[0m' +else + c_red=''; c_grn=''; c_yel=''; c_blu=''; c_off='' +fi +say() { printf "%b%s%b\n" "$c_blu" "[run] $*" "$c_off"; } +ok() { printf "%b%s%b\n" "$c_grn" "[run] $*" "$c_off"; } +warn() { printf "%b%s%b\n" "$c_yel" "[run] $*" "$c_off"; } +err() { printf "%b%s%b\n" "$c_red" "[run] $*" "$c_off" >&2; } + +TUTOR_HOST="${TUTOR_HOST:-127.0.0.1}" +TUTOR_PORT="${TUTOR_PORT:-8001}" +TUTOR_MODEL="${TUTOR_MODEL:-gemma3:4b}" +TUTOR_SKIP_OLLAMA="${TUTOR_SKIP_OLLAMA:-0}" + +venv_dir="backend/.venv" +if [ ! -x "$venv_dir/bin/uvicorn" ]; then + warn "venv not found or uvicorn missing — running ./install.sh first" + ./install.sh +fi + +if [ "$TUTOR_SKIP_OLLAMA" = "1" ]; then + warn "TUTOR_SKIP_OLLAMA=1 — skipping Ollama reachability check" +elif ! command -v ollama >/dev/null 2>&1; then + warn "ollama is not installed; chat replies will fail (UI still works)." + warn " macOS: brew install ollama && ollama serve &" + warn " Linux: curl -fsSL https://ollama.com/install.sh | sh && ollama serve &" +elif ! curl -fsS --max-time 2 http://localhost:11434/api/tags >/dev/null 2>&1; then + warn "ollama is installed but the daemon is not reachable on :11434." + warn "Start it in another terminal: ollama serve" + warn "Chat replies will return 503 until Ollama is up." +else + ok "ollama daemon reachable on :11434" +fi + +# Forward the chosen model + frontend-serving flag to the backend. +export TUTOR_MODEL +export TUTOR_SERVE_FRONTEND=1 + +# Friendly banner before we hand off to uvicorn. +echo +ok "starting backend on http://${TUTOR_HOST}:${TUTOR_PORT}/" +ok "open that URL in your browser. Press Ctrl-C to stop." +echo + +cd backend +exec ./.venv/bin/uvicorn app.main:app \ + --host "$TUTOR_HOST" \ + --port "$TUTOR_PORT" diff --git a/scripts/smoke_run.sh b/scripts/smoke_run.sh new file mode 100755 index 0000000..a2dbf3f --- /dev/null +++ b/scripts/smoke_run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# scripts/smoke_run.sh — launch the server, hit /api/health and /, then stop. +# Intended for CI and for verifying a local install end-to-end without Ollama. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +cd "$repo_root" + +PORT="${TUTOR_PORT:-8801}" +export TUTOR_PORT="$PORT" +export TUTOR_SKIP_OLLAMA=1 + +# Launch in the background. +./run.sh > /tmp/tutor-smoke.log 2>&1 & +pid=$! +trap 'kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true' EXIT + +# Wait up to 15s for the server to come up. +for _ in $(seq 1 30); do + if curl -fsS --max-time 1 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then + break + fi + sleep 0.5 +done + +echo "--- /api/health ---" +curl -fsS "http://127.0.0.1:$PORT/api/health" +echo +echo "--- / (head) ---" +curl -fsS "http://127.0.0.1:$PORT/" | head -3 +echo +echo "--- /api/config ---" +curl -fsS "http://127.0.0.1:$PORT/api/config" | head -c 200 +echo +echo "smoke ok"