Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,12 @@ jobs:
else
echo "scripts/smoke_run.sh missing; skipping run.sh smoke"
fi

- name: Smoke-test install.sh y/N prompts (noninteractive)
run: |
if [ -f scripts/smoke_prompts.sh ]; then
chmod +x scripts/smoke_prompts.sh
./scripts/smoke_prompts.sh
else
echo "scripts/smoke_prompts.sh missing; skipping prompt smoke"
fi
68 changes: 55 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,36 +81,76 @@ 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
./install.sh # creates venv, installs deps, then prompts y/N for
# any host-level setup (install Ollama, start daemon,
# pull model, launch app)
./run.sh # serves UI + API on http://localhost:8001/
```

Then open <http://localhost:8001/> in your browser. You'll see the
lesson list, the inline code lab (Run / Evaluate), and the floating
"Ask tutor" chat panel.

### Opt-in install prompts

`install.sh` does the Python-side setup (venv + pip) without asking —
those changes live inside the repository. Anything that touches the
host system or your home directory is **opt-in** via a y/N prompt:

| Detected condition | Prompt | Default |
| --------------------------------------------- | -------------------------------------------------------- | ------- |
| Ollama binary missing (macOS or Linux) | "Install Ollama now? (will run the official installer)" | **No** |
| Ollama installed but daemon not on :11434 | "Start 'ollama serve' in the background now?" | **No** |
| Default model (`gemma3:4b`) missing locally | "Pull '<model>' now? (this can take several minutes)" | **No** |
| Install finished | "Launch the tutor now (./run.sh)?" | **No** |

Accepted "yes" answers are `y`, `Y`, `yes`, `Yes`, `YES`. Anything else
— including pressing Enter on an empty line — is treated as "no". The
upstream Ollama installer on Linux (`curl https://ollama.com/install.sh | sh`)
may itself invoke `sudo`; that is the documented upstream path.

### Non-interactive / CI usage

The same flow runs unattended:

```bash
# CI: do not touch Ollama, do nothing system-level.
TUTOR_SKIP_OLLAMA=1 TUTOR_NONINTERACTIVE=1 ./install.sh

# Unattended local install where the operator has pre-approved everything.
PYTHON_TUTOR_ASSUME_YES=1 ./install.sh
```

Relevant env vars:

| Variable | Effect |
| ------------------------------ | ------------------------------------------------------------------- |
| `TUTOR_NONINTERACTIVE=1` | Never prompt; answer **no** to every install/start/pull/launch ask. |
| `PYTHON_TUTOR_NONINTERACTIVE=1`| Alias for the above. |
| `PYTHON_TUTOR_ASSUME_YES=1` | Never prompt; answer **yes** (use only if you trust the host). |
| `PYTHON_TUTOR_AUTOLAUNCH=1` | After install, `exec ./run.sh` without asking. |
| `TUTOR_SKIP_OLLAMA=1` | Skip every Ollama probe entirely. |
| `TUTOR_SKIP_MODEL_PULL=1` | Skip the `ollama pull` step (binary/daemon checks still run). |
| `TUTOR_MODEL=<tag>` | Override the default `gemma3:4b`. |

### 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.
- `run.sh` will offer to start `ollama serve` for you if it sees the
binary but not the daemon. Decline and it continues with the
degraded-mode warning.

### 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`.
`install.sh` will offer to install it for you on macOS (`brew install
ollama`) or Linux (`curl -fsSL https://ollama.com/install.sh | sh`).
Answer `y` to proceed, anything else to skip. If you skip, the same
commands are printed for you to run by hand, then re-run `./install.sh`
to pull the model.

### Troubleshooting

Expand All @@ -122,6 +162,8 @@ Then `./install.sh` again to pull `gemma3:4b`.
| 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 |
| Prompts fire in CI / a script | Set `TUTOR_NONINTERACTIVE=1` (defaults to "no") or `PYTHON_TUTOR_ASSUME_YES=1` |
| Ollama installer prompts for sudo | That's the upstream Linux installer; decline the prompt or install via your package manager |

For the design rationale behind the two-script flow (and the five flows
we evaluated), see
Expand Down
115 changes: 83 additions & 32 deletions docs/install-runtime-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,47 +154,89 @@ flowchart TD

## Decision

We ship **Candidate D, blended with one ergonomic touch from C**.
We ship **Candidate D, blended with the consent-gated ergonomics of 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.
Ollama and the model rather than installing them silently; 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**: when something host-level is missing — the Ollama binary,
the `ollama serve` daemon, or the default model — `install.sh`
*offers* to handle it. The user types `y` to accept. The default
answer is **no**, so pressing Enter never installs a system binary.

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).
- Zero hidden system-level installs. Every system-touching action is
preceded by an explicit y/N confirmation.
- A single non-interactive entry-point for CI
(`TUTOR_NONINTERACTIVE=1` defaults all prompts to "no";
`PYTHON_TUTOR_ASSUME_YES=1` defaults them to "yes" for pre-approved
unattended setup).
- A web UI that loads even when the LLM is unreachable — so the learner
always gets *something* to interact with.

```mermaid
flowchart TD
Clone[git clone repo] --> Install[./install.sh]
Install --> Py[Python 3.10+ check]
Py --> Venv[Create / reuse backend/.venv]
Venv --> Pip[pip install backend deps]
Pip --> HasOllama{ollama on PATH?}
HasOllama -- no --> AskInstall{Install Ollama now? y/N}
AskInstall -- y --> RunInstaller[Run upstream installer]
AskInstall -- N --> HintInstall[Print manual install hint]
RunInstaller --> Daemon{Daemon on :11434?}
HasOllama -- yes --> Daemon
HintInstall --> NextBanner
Daemon -- yes --> Model{Model present?}
Daemon -- no --> AskStart{Start 'ollama serve' now? y/N}
AskStart -- y --> Spawn[nohup ollama serve & probe up to 10s]
AskStart -- N --> HintStart[Print 'run: ollama serve' hint]
Spawn --> Model
HintStart --> NextBanner
Model -- yes --> NextBanner
Model -- no --> AskPull{"Pull model? y/N"}
AskPull -- y --> Pull[ollama pull TUTOR_MODEL]
AskPull -- N --> HintPull[Print 'ollama pull' hint]
Pull --> NextBanner
HintPull --> NextBanner
NextBanner --> AskLaunch{Launch ./run.sh? y/N}
AskLaunch -- y --> Run[./run.sh → uvicorn]
AskLaunch -- N --> Done[Print next-step banner]
Run --> Browser[Open http://localhost:8001]
```

## 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`.
3. `pip install -r backend/requirements-dev.txt` (idempotent;
not gated by a prompt — those changes live inside the repo).
4. Check `ollama` on `PATH`. If missing, **prompt** to install via the
OS-appropriate upstream installer (`brew install ollama` on macOS,
`curl https://ollama.com/install.sh | sh` on Linux). Decline and
the script prints the manual command and continues.
5. If `ollama` is present, probe `http://localhost:11434/api/tags`.
If the daemon is down, **prompt** to start `ollama serve` in the
background (`nohup`, logged to `/tmp/ollama-serve.log`).
6. If the daemon is up, check for the model
(`TUTOR_MODEL`, default `gemma3:4b`). If absent, **prompt** to
`ollama pull` it.
7. After setup, **prompt** "Launch the tutor now (./run.sh)?".
`PYTHON_TUTOR_AUTOLAUNCH=1` or `PYTHON_TUTOR_ASSUME_YES=1` answers
yes without asking.

### `run.sh`

1. Ensure venv exists (re-run `install.sh` if not).
2. Probe Ollama; warn if unreachable but continue.
1. Ensure venv exists (re-run `install.sh` in non-interactive,
skip-Ollama mode if not — this never installs system binaries).
2. Probe Ollama; if installed but the daemon is down, **prompt** to
start `ollama serve`. If declined, warn and 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/`.
Expand All @@ -206,25 +248,34 @@ This blend has:
- `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.
- `TUTOR_NONINTERACTIVE=1` — never prompt; auto-answer **no**.
- `PYTHON_TUTOR_NONINTERACTIVE=1` — alias for the above.
- `PYTHON_TUTOR_ASSUME_YES=1` — never prompt; auto-answer **yes**.
- `PYTHON_TUTOR_AUTOLAUNCH=1` — `exec ./run.sh` after install
without asking.

## What the user does

Default interactive flow:

```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/
./install.sh # ~2 min cold; prompts y/N for each system action
# answer 'y' to install Ollama, start the daemon,
# pull the model, and launch the app
```

If Ollama is missing, `install.sh` will tell them exactly what to type:
If you'd rather drive it yourself, decline every prompt and the script
still finishes successfully — only the Python side is set up, with
clear hints for what to run next.

Unattended:

```bash
# macOS
brew install ollama && ollama serve &
# Pre-approved: install Ollama, start it, pull the model, exec run.sh.
PYTHON_TUTOR_ASSUME_YES=1 ./install.sh

# Linux
curl -fsSL https://ollama.com/install.sh | sh && ollama serve &
# CI: do not touch Ollama at all.
TUTOR_SKIP_OLLAMA=1 TUTOR_NONINTERACTIVE=1 ./install.sh
```

Then `./install.sh && ./run.sh` again.
Loading
Loading