Skip to content

Commit 9c7d67b

Browse files
committed
feat: add omni app
1 parent eacc29f commit 9c7d67b

10 files changed

Lines changed: 907 additions & 43 deletions

File tree

README.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,48 @@ The Helm chart consists of the following components:
6868

6969
- A choice of frontend web-apps built using [Gradio](https://www.gradio.app) (see [web-apps](./web-apps/)). Each web interface is available as a pre-built container image [hosted on ghcr.io](https://github.com/orgs/stackhpc/packages?repo_name=azimuth-llm) and be configured for each Helm release by changing the `ui.image` section of the chart values.
7070

71-
<!-- ## Development
71+
## Development environment (devenv + direnv)
7272

73-
TODO: Update this
73+
This repo uses [devenv](https://devenv.sh) (Nix-backed) to pin the full developer toolchain — Python 3.11, `uv`, `ruff`, `black`, `kubectl`, `helm`, `kind`, `chart-testing`, `grype`, `jq`, `yq` — and [direnv](https://direnv.net) to load that environment automatically whenever you `cd` into the repo. The `devenv.nix`, `devenv.yaml`, `devenv.lock`, and `.envrc` files at the repo root are the source of truth; nothing needs to be installed globally except devenv and direnv themselves.
7474

75-
The GitHub repository includes a [tilt](https://tilt.dev) file for easier development. After installing tilt locally, simply run `tilt up` from the repo root to get started with development. This will trigger the following:
75+
### One-time setup
7676

77-
- Install the backend API components of the Helm chart on the remote k8s cluster specified by your current k8s context.
77+
1. Install Nix: <https://nixos.org/download> (or use the [Determinate Systems installer](https://github.com/DeterminateSystems/nix-installer)).
78+
2. Install devenv: <https://devenv.sh/getting-started/> (typically `nix profile install nixpkgs#devenv`).
79+
3. Install direnv: <https://direnv.net/docs/installation.html> and hook it into your shell (e.g. add `eval "$(direnv hook zsh)"` to `~/.zshrc`).
80+
4. From the repo root, allow the `.envrc` once:
7881

79-
- Create a port-forward from the remote cluster to `localhost:8080`
82+
```
83+
direnv allow
84+
```
8085

81-
- Create a local `tilt-dev-venv` in the repo root containing the required Python dependencies to run the frontend web app locally.
86+
The first activation will build the dev shell and may take several minutes. Subsequent `cd` into the directory is near-instant (cached).
8287

83-
- Launch the frontend web app locally on `127.0.0.1:7860`, configured to use `localhost:8080` as the backend API
88+
### What you get on shell entry
8489

85-
- Watch all components and only reload the minimal set of components needed when a file in the repo changes (e.g. modifying `chart/web-app/app.py` will restart the local web app instance only) -->
90+
Every time direnv activates the shell, `devenv.nix` prints the available commands. The most important:
8691

87-
<!-- ## Adding a new web interface
92+
| Command | What it does |
93+
| --- | --- |
94+
| `omni-run` | Creates/refreshes `web-apps/omni/.venv` from `web-apps/omni/requirements.txt` (via `uv`) and launches the Omni Gradio UI on <http://localhost:7860>. |
95+
| `build [component] [--tag TAG]` | Builds the container image for a web-app under `web-apps/` (omit `component` to build all). |
96+
| `scan [component] [--tag TAG] [--fail-on SEV]` | Builds (if needed) then runs a Grype vulnerability scan against the image. |
97+
| `prek -a` | Runs the configured pre-commit hooks (treefmt: `nixfmt`, `black`) across all files. |
8898

89-
TODO: Write these docs... -->
99+
Components are auto-discovered from any subdirectory of `web-apps/` that contains a `Dockerfile`, so adding a new web-app requires no changes to `devenv.nix`.
100+
101+
### Without direnv
102+
103+
If you'd rather not use direnv, you can enter the same environment manually from the repo root:
104+
105+
```
106+
devenv shell
107+
```
108+
109+
…and then run any of the commands above. Exit with `exit` or `Ctrl-D`.
110+
111+
### Notes
112+
113+
- Python dependencies for each web-app are pinned in that web-app's own `requirements.txt` and installed by `uv` into a local `.venv/`. devenv does **not** manage those packages — this guarantees the dev environment matches the container image built by the Dockerfile.
114+
- On NixOS, `LD_LIBRARY_PATH` is preset in `devenv.nix` so that PyPI wheels with native extensions (numpy, scipy, pillow, …) can find `libstdc++`, `libz`, `libjpeg`, `libpng`, and `libwebp` at runtime.
115+
- `devenv.lock` pins the exact `nixpkgs` revision; commit it alongside any change to `devenv.nix`/`devenv.yaml` so collaborators get the same toolchain.

devenv.nix

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
1-
{ pkgs, ... }:
1+
{
2+
pkgs,
3+
lib,
4+
...
5+
}:
26
let
3-
allComponents = "chat image-analysis flux-image-gen";
47
imagePrefix = "ghcr.io/stackhpc/azimuth-llm";
5-
webAppsDir = "./web-apps";
8+
webAppsDir = ./web-apps;
9+
10+
# Discover components: subdirs of web-apps/ that contain a Dockerfile.
11+
# Keeps `build`, `scan`, and the help text in sync with the filesystem
12+
# so adding a web-app requires no edits here.
13+
componentList =
14+
let
15+
entries = builtins.readDir webAppsDir;
16+
isComponent =
17+
name: entries.${name} == "directory" && builtins.pathExists (webAppsDir + "/${name}/Dockerfile");
18+
in
19+
lib.sort lib.lessThan (lib.filter isComponent (builtins.attrNames entries));
620

7-
# Resolves "all" or empty arg to the full list, validates otherwise.
8-
resolveComponents = ''
9-
ALL_COMPONENTS="${allComponents}"
21+
# Shell prelude shared by every script that operates on components.
22+
# Provides:
23+
# resolve_components <input> - echo input (validated) or all components
24+
# image_name <component> - echo the full image reference
25+
componentPrelude = ''
26+
ALL_COMPONENTS="${lib.concatStringsSep " " componentList}"
1027
1128
resolve_components() {
1229
local input="$1"
1330
if [ -z "$input" ] || [ "$input" = "all" ]; then
1431
echo "$ALL_COMPONENTS"
15-
else
16-
for c in $input; do
17-
if ! echo " $ALL_COMPONENTS " | grep -q " $c "; then
18-
echo "Unknown component: $c" >&2
19-
echo "Available: $ALL_COMPONENTS" >&2
20-
return 1
21-
fi
22-
done
23-
echo "$input"
32+
return 0
2433
fi
34+
for c in $input; do
35+
if ! echo " $ALL_COMPONENTS " | grep -q " $c "; then
36+
echo "Unknown component: $c" >&2
37+
echo "Available: $ALL_COMPONENTS" >&2
38+
return 1
39+
fi
40+
done
41+
echo "$input"
2542
}
2643
2744
image_name() {
@@ -44,12 +61,29 @@ in
4461
# CI tooling
4562
jq
4663
yq-go
47-
# Python
64+
curl
65+
# Python toolchain.
66+
# devenv is NOT the source of truth for Python deps: the pinned
67+
# requirements.txt in each web-app is, and uv installs from it.
68+
# This guarantees the devenv .venv matches the Dockerfile-built image.
69+
# The Dockerfile must use the same Python minor version.
4870
python311
71+
uv
4972
ruff
5073
black
5174
];
5275

76+
# PyPI wheels (numpy, scipy, pillow, ...) are linked against system libs
77+
# that don't exist on NixOS at standard FHS paths. Expose them via
78+
# LD_LIBRARY_PATH so the wheels' C extensions can load.
79+
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
80+
pkgs.stdenv.cc.cc.lib # libstdc++.so.6, libgcc_s.so.1
81+
pkgs.zlib # libz.so.1 (numpy, scipy, pillow)
82+
pkgs.libjpeg # libjpeg.so (pillow)
83+
pkgs.libpng # libpng.so (pillow)
84+
pkgs.libwebp # libwebp.so (pillow)
85+
];
86+
5387
treefmt = {
5488
enable = true;
5589
config.programs = {
@@ -69,7 +103,8 @@ in
69103

70104
scripts = {
71105
build.exec = ''
72-
${resolveComponents}
106+
${componentPrelude}
107+
73108
TAG="latest"
74109
COMPONENT=""
75110
while [ $# -gt 0 ]; do
@@ -78,20 +113,20 @@ in
78113
*) COMPONENT="$COMPONENT $1"; shift ;;
79114
esac
80115
done
81-
COMPONENT="''${COMPONENT## }"
82116
83-
TARGETS=$(resolve_components "$COMPONENT") || exit 1
117+
TARGETS=$(resolve_components "''${COMPONENT## }") || exit 1
84118
for c in $TARGETS; do
85119
echo "==> Building $c (tag: $TAG)"
86120
docker build \
87121
-t "$(image_name "$c"):$TAG" \
88-
-f ${webAppsDir}/"$c"/Dockerfile \
89-
${webAppsDir}/
122+
-f ./web-apps/"$c"/Dockerfile \
123+
./web-apps/
90124
done
91125
'';
92126

93127
scan.exec = ''
94-
${resolveComponents}
128+
${componentPrelude}
129+
95130
TAG="latest"
96131
FAIL_ON="critical"
97132
COMPONENT=""
@@ -102,32 +137,68 @@ in
102137
*) COMPONENT="$COMPONENT $1"; shift ;;
103138
esac
104139
done
105-
COMPONENT="''${COMPONENT## }"
106140
107-
TARGETS=$(resolve_components "$COMPONENT") || exit 1
141+
TARGETS=$(resolve_components "''${COMPONENT## }") || exit 1
108142
EXIT=0
109143
for c in $TARGETS; do
110144
build "$c" --tag "$TAG"
111-
112145
IMAGE="$(image_name "$c"):$TAG"
113146
echo ""
114147
echo "==> Scanning $IMAGE (fail-on: $FAIL_ON)"
115-
if ! grype "$IMAGE" --fail-on "$FAIL_ON" --only-fixed; then
116-
EXIT=1
117-
fi
148+
grype "$IMAGE" --fail-on "$FAIL_ON" --only-fixed || EXIT=1
118149
done
119150
exit $EXIT
120151
'';
152+
153+
# ---------------- omni helpers ----------------
154+
#
155+
# omni-venv installs omni's Python deps into web-apps/omni/.venv from
156+
# requirements.txt, re-running only when requirements.txt is newer than
157+
# the install stamp. The `../utils` line in requirements.txt is resolved
158+
# by uv as a local path dep (same mechanism the Dockerfile uses, modulo
159+
# the sed rewrite).
160+
#
161+
# omni-run ensures the venv is up to date, activates it, and launches
162+
# the Gradio UI.
163+
164+
omni-venv.exec = ''
165+
set -e
166+
cd ./web-apps/omni
167+
STAMP=.venv/.installed
168+
if [ ! -d .venv ] || [ requirements.txt -nt "$STAMP" ]; then
169+
echo "==> Creating/refreshing ./web-apps/omni/.venv (Python 3.11)"
170+
uv venv --python 3.11 .venv
171+
# shellcheck disable=SC1091
172+
. .venv/bin/activate
173+
echo "==> Installing dependencies from requirements.txt"
174+
uv pip install -r requirements.txt
175+
touch "$STAMP"
176+
fi
177+
'';
178+
179+
omni-run.exec = ''
180+
set -e
181+
omni-venv
182+
cd ./web-apps/omni
183+
# shellcheck disable=SC1091
184+
. .venv/bin/activate
185+
echo "==> Starting Omni interface on http://localhost:7860"
186+
exec python app.py "$@"
187+
'';
121188
};
122189

123190
enterShell = ''
124191
echo "$GREET"
125192
echo ""
126-
echo "Commands (component = chat | image-analysis | flux-image-gen | omit for all):"
193+
echo "Commands (component = ${lib.concatStringsSep " | " componentList} | omit for all):"
127194
echo ""
128195
echo " prek -a Format/lint all files"
129196
echo " build [component] [--tag TAG] Build container image(s)"
130197
echo " scan [component] [--tag TAG] [--fail-on SEV] Build if needed + Grype scan"
131198
echo ""
199+
echo "Omni (multimodal UI) helpers:"
200+
echo ""
201+
echo " omni-run Start the Omni Gradio UI on :7860"
202+
echo ""
132203
'';
133204
}

web-apps/chat/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
22

3-
RUN apt-get update && apt-get install -y libssl3=3.0.19-1~deb12u2 openssl=3.0.19-1~deb12u2 && rm -rf /var/lib/apt/lists/*
3+
RUN apt-get update && apt-get install -y libssl3=3.0.19-1~deb12u2 openssl=3.0.19-1~deb12u2 libgnutls30=3.7.9-2+deb12u7 && rm -rf /var/lib/apt/lists/*
44

55
ARG DIR=chat
66

web-apps/flux-image-gen/Dockerfile

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
11
FROM ghcr.io/astral-sh/uv:python3.11-trixie
22

3-
# https://stackoverflow.com/questions/55313610/importerror-libgl-so-1-cannot-open-shared-object-file-no-such-file-or-directo
3+
ARG IMAGEMAGICK_VERSION=8:7.1.1.43+dfsg1-1+deb13u9
4+
ARG LIBUNBOUND_VERSION=1.22.0-2+deb13u3
5+
ARG KRB5_VERSION=1.21.3-5+deb13u1
6+
ARG LIBGCRYPT_VERSION=1.11.0-7+deb13u1
47
RUN apt-get update && \
5-
apt-get install -y ffmpeg libsm6 libxext6 && \
8+
apt-get install -y --no-install-recommends \
9+
ffmpeg \
10+
libsm6 \
11+
libxext6 \
12+
"imagemagick=${IMAGEMAGICK_VERSION}" \
13+
"imagemagick-7-common=${IMAGEMAGICK_VERSION}" \
14+
"imagemagick-7.q16=${IMAGEMAGICK_VERSION}" \
15+
"libmagickcore-7-arch-config=${IMAGEMAGICK_VERSION}" \
16+
"libmagickcore-7-headers=${IMAGEMAGICK_VERSION}" \
17+
"libmagickcore-7.q16-10=${IMAGEMAGICK_VERSION}" \
18+
"libmagickcore-7.q16-10-extra=${IMAGEMAGICK_VERSION}" \
19+
"libmagickcore-7.q16-dev=${IMAGEMAGICK_VERSION}" \
20+
"libmagickcore-dev=${IMAGEMAGICK_VERSION}" \
21+
"libmagickwand-7-headers=${IMAGEMAGICK_VERSION}" \
22+
"libmagickwand-7.q16-10=${IMAGEMAGICK_VERSION}" \
23+
"libmagickwand-7.q16-dev=${IMAGEMAGICK_VERSION}" \
24+
"libmagickwand-dev=${IMAGEMAGICK_VERSION}" \
25+
"libunbound8=${LIBUNBOUND_VERSION}" \
26+
"krb5-multidev=${KRB5_VERSION}" \
27+
"libgssapi-krb5-2=${KRB5_VERSION}" \
28+
"libgssrpc4t64=${KRB5_VERSION}" \
29+
"libk5crypto3=${KRB5_VERSION}" \
30+
"libkadm5clnt-mit12=${KRB5_VERSION}" \
31+
"libkadm5srv-mit12=${KRB5_VERSION}" \
32+
"libkdb5-10t64=${KRB5_VERSION}" \
33+
"libkrb5-3=${KRB5_VERSION}" \
34+
"libkrb5-dev=${KRB5_VERSION}" \
35+
"libkrb5support0=${KRB5_VERSION}" \
36+
"libgcrypt20=${LIBGCRYPT_VERSION}" && \
37+
apt-get clean && \
638
rm -rf /var/lib/apt/lists/*
739

840

941
ARG DIR=flux-image-gen
1042

43+
RUN uv pip install --system --no-cache-dir --upgrade \
44+
pip \
45+
setuptools \
46+
wheel
47+
1148
COPY $DIR/requirements.txt requirements.txt
1249
RUN uv pip install --system --no-cache-dir -r requirements.txt
1350

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
flux[gradio] @ git+https://github.com/black-forest-labs/flux@478338d
22
fastapi[standard]
33
httpx
4+
urllib3>=2.7.0
5+
idna>=3.15
46
# ../utils

web-apps/image-analysis/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
22

33
RUN apt-get update && \
4-
apt-get install -y --only-upgrade libssl3=3.0.19-1~deb12u2 openssl=3.0.19-1~deb12u2 && \
4+
apt-get install -y --only-upgrade libssl3=3.0.19-1~deb12u2 openssl=3.0.19-1~deb12u2 \
5+
libgnutls30=3.7.9-2+deb12u7 && \
56
rm -rf /var/lib/apt/lists/*
67

78
ARG DIR=image-analysis

web-apps/omni/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
2+
3+
RUN apt-get update && apt-get install -y \
4+
libssl3=3.0.19-1~deb12u2 \
5+
openssl=3.0.19-1~deb12u2 \
6+
libgnutls30=3.7.9-2+deb12u7 \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
ARG DIR=omni
10+
11+
COPY $DIR/requirements.txt requirements.txt
12+
RUN sed -i s$../utils$./utils$ requirements.txt
13+
COPY utils utils
14+
RUN uv pip install --system --no-cache-dir -r requirements.txt
15+
16+
COPY purge-google-fonts.sh purge-google-fonts.sh
17+
RUN bash purge-google-fonts.sh
18+
19+
WORKDIR /app
20+
21+
COPY $DIR/*.py .
22+
23+
COPY $DIR/defaults.yml .
24+
25+
ENTRYPOINT ["python3", "app.py"]

0 commit comments

Comments
 (0)