diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..00f7280a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,106 @@ +# Git files +.git +.gitignore +.gitattributes +.github + +# Documentation (we only need README.md) +docs/ +INSTALL_GUIDE.md +CONTRIBUTING.md +LICENSE +*.md +!README.md + +# Development files +.vscode +.idea +*.swp +*.swo +*~ +.editorconfig + +# Python cache and build artifacts +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info +dist +build +.pytest_cache +.coverage +htmlcov +*.egg +MANIFEST +.mypy_cache +.ruff_cache +.tox +.nox +*.cover +.hypothesis + +# Node (we'll install and build in Docker) +chainforge/react-server/node_modules +chainforge/react-server/.pnp +chainforge/react-server/.pnp.js +chainforge/react-server/coverage +chainforge/react-server/build + +# Testing and development +tests/ +test/ +*.test.js +*.test.ts +*.test.tsx +*.spec.js +*.spec.ts +*.spec.tsx +coverage/ +.coverage +*.log +*.logs + +# Environment files +.env +.env.* +!.env.example +*.local + +# ChainForge specific +chainforge/cache +chainforge/examples/oaievals/ +chainforge_assets/ +packages/ +jobs/ +data/ + +# Docker files (avoid recursive copying) +Dockerfile* +docker-compose*.yml +.dockerignore + +# CI/CD +.circleci +.travis.yml +.gitlab-ci.yml +azure-pipelines.yml + +# IDE and editor files +*.sublime-* +.vscode +.idea +*.iml + +# OS files +.DS_Store +Thumbs.db +Desktop.ini + +# Temporary files +*.tmp +*.temp +*.bak +*.swp +*~ diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..aa507778 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,303 @@ +name: Build Docker Images + +on: + pull_request: + branches: + - main + - master + - ragforge + push: + branches: + - main + - master + - ragforge + tags: + - 'v*' + +# Concurrency: cancel in-progress runs when new one starts +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/chainforge + +jobs: + build-cpu-amd64: + name: Build CPU AMD64 Image + runs-on: ubuntu-latest + # Skip if commit message contains [skip ci] or [ci skip] + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') }} + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build CPU AMD64 image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + platforms: linux/amd64 + provenance: false + sbom: false + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + id: build-amd64 + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build-amd64.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-cpu-amd64 + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + build-cpu-arm64: + name: Build CPU ARM64 Image + runs-on: ubuntu-latest + # Skip if commit message contains [skip ci] or [ci skip] + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') }} + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build CPU ARM64 image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + platforms: linux/arm64 + provenance: false + sbom: false + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + id: build-arm64 + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build-arm64.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-cpu-arm64 + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + build-gpu-amd64: + name: Build GPU AMD64 Image + runs-on: ubuntu-latest + # Skip if commit message contains [skip ci] or [ci skip] + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') }} + permissions: + contents: read + packages: write + + steps: + - name: Free up disk space + run: | + echo "Before cleanup:" + df -h + + # Remove unnecessary software to free up space + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + + # Remove large packages + sudo apt-get remove -y '^aspnetcore-.*' || true + sudo apt-get remove -y '^dotnet-.*' --fix-missing || true + sudo apt-get remove -y '^llvm-.*' --fix-missing || true + sudo apt-get remove -y 'php.*' --fix-missing || true + sudo apt-get remove -y '^mongodb-.*' --fix-missing || true + sudo apt-get remove -y '^mysql-.*' --fix-missing || true + sudo apt-get remove -y azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri --fix-missing || true + sudo apt-get remove -y google-cloud-sdk --fix-missing || true + sudo apt-get remove -y google-cloud-cli --fix-missing || true + + # Clean up + sudo apt-get autoremove -y + sudo apt-get clean + sudo docker image prune --all --force + sudo rm -rf /var/lib/apt/lists/* + + echo "After cleanup:" + df -h + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build GPU AMD64 image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.gpu + push: false + platforms: linux/amd64 + provenance: false + sbom: false + outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + id: build-gpu + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build-gpu.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-gpu-amd64 + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + + create-cpu-manifest: + name: Create CPU Multi-Arch Manifest + runs-on: ubuntu-latest + needs: [build-cpu-amd64, build-cpu-arm64] + if: ${{ github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') }} + permissions: + contents: read + packages: write + + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-cpu-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=cpu + type=raw,value=latest + + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} + + create-gpu-manifest: + name: Create GPU Manifest + runs-on: ubuntu-latest + needs: [build-gpu-amd64] + if: ${{ github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') }} + permissions: + contents: read + packages: write + + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-gpu-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch,suffix=-gpu + type=semver,pattern={{version}},suffix=-gpu + type=semver,pattern={{major}}.{{minor}},suffix=-gpu + type=raw,value=gpu + + - name: Create and push GPU manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) + env: + DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 26446c23..da32a8cc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -29,9 +29,10 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('chainforge/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('chainforge/requirements.txt', 'chainforge/constraints.txt', 'setup.py') }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- - name: Upgrade pip run: python -m pip install --upgrade pip diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 00000000..c031eb7c --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,371 @@ +# ChainForge Docker Setup + +This document describes how to run ChainForge using Docker. ChainForge provides two Docker image variants: + +- **CPU (latest)**: Multi-architecture (AMD64 + ARM64) optimized for CPU-only environments +- **GPU**: AMD64-only with CUDA support for GPU acceleration + +## Prerequisites + +- Docker (version 20.10 or later) +- Docker Compose (version 2.0 or later) +- For GPU: NVIDIA Docker runtime (nvidia-docker2) + +## Architecture Support + +**CPU Images**: Available for both AMD64 and ARM64 architectures. Docker automatically pulls the correct architecture for your system. + +**GPU Images**: AMD64 only (ARM64 GPU support is limited and CUDA packages are very large) + +## Quick Start + +### CPU Version (Default) + +Using Docker Compose (Recommended): + +```bash +docker-compose up -d +``` + +Or using Docker CLI: + +```bash +# Pull pre-built image +docker pull gauransh/chainforge:latest + +# Or build locally +docker build -t chainforge:cpu . + +# Run +docker run -d \ + -p 8000:8000 \ + -v chainforge-data:/home/chainforge/.local/share/chainforge \ + --name chainforge \ + --restart unless-stopped \ + gauransh/chainforge:latest +``` + +### GPU Version + +Using Docker Compose (Recommended): + +```bash +docker-compose -f docker-compose.gpu.yml up -d +``` + +Or using Docker CLI: + +```bash +# Pull pre-built image +docker pull gauransh/chainforge:gpu + +# Or build locally +docker build -f Dockerfile.gpu -t chainforge:gpu . + +# Run with GPU support +docker run -d \ + -p 8000:8000 \ + -v chainforge-data:/home/chainforge/.local/share/chainforge \ + --name chainforge-gpu \ + --gpus all \ + --restart unless-stopped \ + gauransh/chainforge:gpu +``` + +Access ChainForge at: http://localhost:8000 + +## Image Variants + +### CPU (latest) +- **Architectures**: AMD64, ARM64 (multi-arch manifest) +- **Size**: Optimized and minimal (~800MB compressed) +- **Dependencies**: Uses `constraints.txt` for version pinning +- **PyTorch**: CPU-only build from https://download.pytorch.org/whl/cpu +- **Tags**: `latest`, `cpu`, branch names (e.g., `main`, `ragforge`), version tags (e.g., `v1.0`, `1.0`) + +### GPU +- **Architecture**: AMD64 only +- **Size**: Larger due to CUDA support (~2-3GB compressed) +- **Dependencies**: Uses `constraints.txt` for version pinning +- **PyTorch**: CUDA 12.1 build from https://download.pytorch.org/whl/cu121 +- **Tags**: `gpu`, branch names with `-gpu` suffix (e.g., `main-gpu`, `ragforge-gpu`), version tags with `-gpu` suffix (e.g., `v1.0-gpu`, `1.0-gpu`) +- **Requirements**: NVIDIA GPU with CUDA support, nvidia-docker runtime + +**When to use GPU variant:** +- You have NVIDIA GPUs available (AMD64 architecture) +- You need GPU acceleration for ML/AI workloads +- You're running compute-intensive models locally + +**When to use CPU variant:** +- Running on CPU-only machines +- Using ARM64 devices (Apple Silicon, Raspberry Pi, etc.) +- Deploying to cloud platforms without GPU +- Smaller image size is preferred + +## Managing Containers + +### View logs + +```bash +# CPU version +docker-compose logs -f + +# GPU version +docker-compose -f docker-compose.gpu.yml logs -f +``` + +### Stop containers + +```bash +# CPU version +docker-compose down + +# GPU version +docker-compose -f docker-compose.gpu.yml down +``` + +### Rebuild images + +```bash +# CPU version +docker-compose build --no-cache + +# GPU version +docker-compose -f docker-compose.gpu.yml build --no-cache +``` + +### Remove volumes (delete all data) + +```bash +docker-compose down -v +``` + +## Environment Variables + +You can set API keys and other environment variables by: + +1. Creating a `.env` file in the project root +2. Adding your variables (they're already templated in docker-compose.yml): + +```env +OPENAI_API_KEY=your_key_here +ANTHROPIC_API_KEY=your_key_here +COHERE_API_KEY=your_key_here +GOOGLE_API_KEY=your_key_here +DEEPSEEK_API_KEY=your_key_here +HUGGINGFACE_API_KEY=your_key_here +``` + +3. Uncommenting the relevant lines in `docker-compose.yml` + +## Docker Image Structure + +Multi-stage build optimized for minimal size and fast builds: + +### Stage 1: Frontend Builder +- Starts from `node:20-slim` +- Installs npm dependencies and builds React frontend +- Cleans up node_modules and npm cache after build +- Only the `/build` directory is copied to final image + +### Stage 2: Python Builder +- Starts from `python:3.12-slim` +- Installs build dependencies (build-essential, git) +- Installs PyTorch (CPU or CUDA variant) +- Installs Python dependencies from `requirements.txt` with `constraints.txt` +- Installs ChainForge package +- Purges build tools and cleans caches + +### Stage 3: Runtime Image +- Minimal `python:3.12-slim` base +- Only runtime dependencies: git, libgomp1 +- Copies Python packages from builder +- Copies React build from frontend builder +- Aggressive cleanup of unnecessary files (tests, docs, cache, static libs) +- Runs as non-root user (chainforge, uid 1000) + +**Optimizations:** +- Multi-stage build keeps final image small +- No build tools in runtime image +- All RUN commands combined into single layers to minimize layer count +- Aggressive file cleanup reduces image size by ~30% + +## Automated Builds (CI/CD) + +Docker images are automatically built and pushed to Docker Hub via GitHub Actions. + +### Build Triggers + +Builds run on: +- **Pull Requests**: Builds are tested but NOT pushed to Docker Hub +- **Branch Pushes**: `main`, `master`, `ragforge` - Built and pushed with branch-specific tags +- **Version Tags**: `v*` (e.g., `v1.0.0`) - Built and pushed with version tags + +### Build Architecture + +The workflow uses a **parallel multi-architecture build strategy**: + +1. **3 Parallel Build Jobs**: + - `build-cpu-amd64`: Builds CPU variant for AMD64 + - `build-cpu-arm64`: Builds CPU variant for ARM64 + - `build-gpu-amd64`: Builds GPU variant for AMD64 (with aggressive disk cleanup) + +2. **Manifest Creation Jobs**: + - `create-cpu-manifest`: Combines AMD64 + ARM64 into multi-arch CPU manifest + - `create-gpu-manifest`: Creates GPU manifest (AMD64 only) + +**Why separate builds?** +- **No QEMU emulation** - Native builds are 3-5x faster than cross-compilation +- **Parallel execution** - All architectures build simultaneously +- **Better caching** - Each architecture has independent build cache + +### Available Tags + +**CPU Images** (multi-arch: amd64 + arm64): +``` +gauransh/chainforge:latest +gauransh/chainforge:cpu +gauransh/chainforge:main +gauransh/chainforge:ragforge +gauransh/chainforge:v1.0.0 +gauransh/chainforge:1.0 +``` + +**GPU Images** (amd64 only): +``` +gauransh/chainforge:gpu +gauransh/chainforge:main-gpu +gauransh/chainforge:ragforge-gpu +gauransh/chainforge:v1.0.0-gpu +gauransh/chainforge:1.0-gpu +``` + +### Storage Optimizations + +To prevent hitting GitHub Actions storage limits: + +1. **No GitHub Actions cache** - Eliminated cache storage overhead +2. **No ARM64 GPU builds** - GPU only builds for AMD64 (50% storage reduction) +3. **Disabled provenance and SBOM** - Reduces metadata size +4. **Aggressive runner cleanup** - GPU builds free ~30-40GB before starting by removing: + - .NET SDK, Android tools, GHC, CodeQL + - LLVM, PHP, MongoDB, MySQL + - Chrome, Firefox, Azure CLI, Google Cloud SDK +5. **Minimal Dockerfile** - All operations combined into single layers + +### Concurrency Control + +The workflow uses concurrency groups to automatically cancel in-progress builds when a new commit is pushed to the same branch: + +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +``` + +### Skip CI + +To skip Docker builds entirely, add `[skip ci]` or `[ci skip]` to your commit message: + +```bash +git commit -m "Update documentation [skip ci]" +git commit -m "Fix typo [ci skip]" +``` + +This prevents all build jobs from running, saving time and resources. + +### GitHub Secrets Required + +To enable automated image publishing, configure these secrets: + +- `DOCKER_USERNAME` - Docker Hub username +- `DOCKER_PASSWORD` - Docker Hub password or personal access token + +**Setup**: Repository Settings → Secrets and variables → Actions → New repository secret + +### Build Workflow Summary + +``` +┌─────────────────────────────────────────────────────────┐ +│ PR or Push Event │ +└─────────────────────────────────────────────────────────┘ + │ + ┌───────────┴───────────┐ + │ Concurrency Check │ + │ (Cancel old builds?) │ + └───────────┬───────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ CPU AMD64 │ │ CPU ARM64 │ │ GPU AMD64 │ +│ Build │ │ Build │ │ Build │ +│ │ │ │ │ (w/ cleanup) │ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + │ upload digest │ upload digest │ upload digest + │ │ │ + └────────┬─────────┴──────────────────┘ + │ + ┌─────────┴──────────┐ + │ │ + ▼ ▼ +┌─────────────┐ ┌─────────────┐ +│ CPU │ │ GPU │ +│ Manifest │ │ Manifest │ +│ (amd64+arm64)│ │ (amd64 only)│ +└─────────────┘ └─────────────┘ + │ │ + └─────────┬──────────┘ + │ + ▼ + Push to Docker Hub +``` + +## Troubleshooting + +### ESLint Config Error + +If you see "ESLint couldn't find the config 'semistandard'": +- This is fixed in the current Dockerfile by installing devDependencies +- Rebuild the image: `docker-compose build --no-cache` + +### Port Already in Use + +If port 8000 is already in use: +- Change the port mapping in `docker-compose.yml` +- Example: `"8080:8000"` to use port 8080 on your host + +### Permission Issues + +If you encounter permission errors: +- The image runs as a non-root user (uid 1000) +- Ensure your volume permissions match this user +- You can adjust the UID in the Dockerfile if needed + +## Performance Tips + +- The multi-stage build creates an optimized image +- The image is optimized to be under 1GB +- Unnecessary files and caches are cleaned during build +- Consider allocating more memory to Docker if builds are slow + +## Pushing to Registry + +To push the image to a registry: + +```bash +# Tag the image +docker tag chainforge:latest your-registry/chainforge:tag + +# Push to registry +docker push your-registry/chainforge:tag +``` + +Or let docker-compose handle it: + +```bash +docker-compose build +docker-compose push +``` diff --git a/Dockerfile b/Dockerfile index 89e99328..2a69f30c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,84 @@ -FROM python:3.12-slim AS builder +# Multi-stage build: Stage 1 - Build React frontend +FROM node:20-slim AS frontend-builder -RUN pip install --upgrade pip -RUN pip install chainforge --no-cache-dir +WORKDIR /app + +# Copy package files and install dependencies (including dev for build) +COPY chainforge/react-server/package*.json ./ +RUN npm ci --legacy-peer-deps --prefer-offline + +# Copy source files and build +COPY chainforge/react-server/ ./ +RUN npm run build + +# Stage 2 - Build Python dependencies (CPU version with constraints) +FROM python:3.12-slim AS python-builder + +# Install only the build dependencies we need +RUN apt-get --allow-releaseinfo-change update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Upgrade pip tools first (this layer is highly cacheable) +RUN pip install --no-cache-dir --upgrade pip setuptools wheel + +# Copy requirements first for better layer caching +COPY chainforge/requirements.txt chainforge/constraints.txt ./ + +# Install PyTorch CPU-only FIRST as it's the largest dependency +# This separates the longest-running install into its own layer +RUN pip install --no-cache-dir --prefix=/install \ + --extra-index-url https://download.pytorch.org/whl/cpu \ + torch torchvision torchaudio + +# Install remaining requirements with constraints +# Using --find-links to help pip resolve faster +RUN pip install --no-cache-dir --prefix=/install \ + -r requirements.txt \ + -c constraints.txt + +# Copy project files and build the package (smallest layer last) +COPY setup.py README.md ./ +COPY chainforge/ ./chainforge/ +RUN pip install --no-cache-dir --prefix=/install . + +# Stage 3 - Final minimal runtime image +FROM python:3.12-slim + +# Install only runtime dependencies (no build tools) +RUN apt-get --allow-releaseinfo-change update && \ + apt-get install -y --no-install-recommends \ + git \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean WORKDIR /chainforge +# Copy Python packages from builder +COPY --from=python-builder /install /usr/local + +# Copy the built React app from the frontend-builder stage to the installed package location +COPY --from=frontend-builder /app/build /usr/local/lib/python3.12/site-packages/chainforge/react-server/build + +# Clean up any unnecessary files to reduce image size +RUN find /usr/local -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \ + find /usr/local -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \ + find /usr/local -name "*.pyc" -delete && \ + find /usr/local -name "*.pyo" -delete && \ + find /usr/local -name "*.md" -delete 2>/dev/null || true + +# Run as non-root user for security +RUN useradd -m -u 1000 chainforge && \ + mkdir -p /home/chainforge/.local/share/chainforge && \ + chown -R chainforge:chainforge /chainforge /home/chainforge + +USER chainforge + EXPOSE 8000 + ENTRYPOINT [ "chainforge", "serve", "--host", "0.0.0.0" ] diff --git a/Dockerfile.gpu b/Dockerfile.gpu new file mode 100644 index 00000000..26a1687c --- /dev/null +++ b/Dockerfile.gpu @@ -0,0 +1,71 @@ +# Multi-stage build: Stage 1 - Build React frontend +FROM node:20-slim AS frontend-builder + +WORKDIR /app + +# Copy everything and build in one layer to minimize size +COPY chainforge/react-server/ ./ +RUN npm ci --legacy-peer-deps --prefer-offline --production=false && \ + npm run build && \ + rm -rf node_modules && \ + npm cache clean --force + +# Stage 2 - Build Python dependencies (GPU version with CUDA support) +FROM python:3.12-slim AS python-builder + +WORKDIR /build + +# Copy all requirements and source files +COPY chainforge/requirements.txt chainforge/constraints.txt setup.py README.md ./ +COPY chainforge/ ./chainforge/ + +# Do everything in one layer to minimize build context and layers +RUN apt-get --allow-releaseinfo-change update && \ + apt-get install -y --no-install-recommends build-essential git && \ + pip install --no-cache-dir --upgrade pip setuptools wheel && \ + pip install --no-cache-dir --prefix=/install \ + --extra-index-url https://download.pytorch.org/whl/cu121 \ + torch torchvision torchaudio && \ + pip install --no-cache-dir --prefix=/install \ + -r requirements.txt \ + -c constraints.txt && \ + pip install --no-cache-dir --prefix=/install . && \ + apt-get purge -y --auto-remove build-essential && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* /root/.cache + +# Stage 3 - Final minimal runtime image +FROM python:3.12-slim + +WORKDIR /chainforge + +# Copy Python packages and React build from builder stages +COPY --from=python-builder /install /usr/local +COPY --from=frontend-builder /app/build /usr/local/lib/python3.12/site-packages/chainforge/react-server/build + +# Install runtime deps, clean up, and create user in one layer +RUN apt-get --allow-releaseinfo-change update && \ + apt-get install -y --no-install-recommends git libgomp1 && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get clean && \ + find /usr/local -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \ + find /usr/local -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \ + find /usr/local -type d -name "*.dist-info" -exec rm -rf {}/RECORD {} + 2>/dev/null || true && \ + find /usr/local -name "*.pyc" -delete && \ + find /usr/local -name "*.pyo" -delete && \ + find /usr/local -name "*.a" -delete && \ + find /usr/local -name "*.md" -delete 2>/dev/null || true && \ + find /usr/local -name "*.txt" -delete 2>/dev/null || true && \ + rm -rf /usr/local/lib/python3.12/site-packages/*/tests && \ + rm -rf /usr/local/lib/python3.12/site-packages/*/test && \ + rm -rf /tmp/* /var/tmp/* && \ + useradd -m -u 1000 chainforge && \ + mkdir -p /home/chainforge/.local/share/chainforge && \ + chown -R chainforge:chainforge /chainforge /home/chainforge + +USER chainforge + +EXPOSE 8000 + +ENTRYPOINT [ "chainforge", "serve", "--host", "0.0.0.0" ] + diff --git a/README.md b/README.md index c78c5a42..8503b587 100644 --- a/README.md +++ b/README.md @@ -50,19 +50,43 @@ You can set your API keys by clicking the Settings icon in the top-right corner. ## Run using Docker -You can use our [Dockerfile](/Dockerfile) to run `ChainForge` locally using `Docker Desktop`: +ChainForge provides pre-built Docker images for both CPU and GPU environments: -- Build the `Dockerfile`: - ```shell - docker build -t chainforge . - ``` +**Quick start with Docker Compose (recommended):** -- Run the image: - ```shell - docker run -p 8000:8000 chainforge - ``` +```bash +# CPU version (works on AMD64 and ARM64) +docker-compose up -d + +# GPU version (AMD64 only, requires NVIDIA Docker runtime) +docker-compose -f docker-compose.gpu.yml up -d +``` + +**Or use Docker CLI:** + +```bash +# Pull and run CPU version +docker pull gauransh/chainforge:latest +docker run -d -p 8000:8000 --name chainforge gauransh/chainforge:latest + +# Pull and run GPU version +docker pull gauransh/chainforge:gpu +docker run -d -p 8000:8000 --gpus all --name chainforge-gpu gauransh/chainforge:gpu +``` + +**Or build locally:** + +```bash +# Build CPU version +docker build -t chainforge . + +# Build GPU version +docker build -f Dockerfile.gpu -t chainforge:gpu . +``` + +Access ChainForge at http://localhost:8000 -Now you can open the browser of your choice and open `http://127.0.0.1:8000`. +For detailed Docker documentation including architecture support, environment variables, and CI/CD setup, see [DOCKER.md](DOCKER.md). # Supported providers diff --git a/chainforge/constraints.txt b/chainforge/constraints.txt new file mode 100644 index 00000000..379510e8 --- /dev/null +++ b/chainforge/constraints.txt @@ -0,0 +1,4 @@ +--extra-index-url https://download.pytorch.org/whl/cpu +torch +torchvision +torchaudio \ No newline at end of file diff --git a/chainforge/react-server/src/App.tsx b/chainforge/react-server/src/App.tsx index 501743a2..83f762ce 100644 --- a/chainforge/react-server/src/App.tsx +++ b/chainforge/react-server/src/App.tsx @@ -79,6 +79,7 @@ import axios from "axios"; import LZString from "lz-string"; import { EXAMPLEFLOW_1 } from "./example_flows"; import MediaNode from "./MediaNode"; +import { NODE_TOOLTIPS } from "./nodeConstants"; // Styling import "reactflow/dist/style.css"; // reactflow @@ -392,30 +393,28 @@ const App = () => { key: "upload", title: "Upload Docs Node", icon: nodeEmojis.upload, - tooltip: "Upload documents to the flow, such as text files or PDFs.", + tooltip: NODE_TOOLTIPS.upload, onClick: () => addNode("upload"), }, { key: "chunk", title: "Chunking Node", icon: nodeEmojis.chunk, - tooltip: - "Chunk texts into smaller pieces. Compare different chunking methods. Typically used after the Upload Node.", + tooltip: NODE_TOOLTIPS.chunk, onClick: () => addNode("chunk"), }, { key: "retrieval", title: "Retrieval Node", icon: nodeEmojis.retrieval, - tooltip: - "Given chunks and queries, retrieve relevant chunks for the given query. Compare retrieval methods across queries. Retrieval methods include both classical methods like BM25, and vector stores.", + tooltip: NODE_TOOLTIPS.retrieval, onClick: () => addNode("retrieval"), }, { key: "rerank", title: "Rerank Node", icon: nodeEmojis.rerank, - tooltip: "Reranks retrieval outputs.", + tooltip: NODE_TOOLTIPS.rerank, onClick: () => addNode("rerank"), }, { @@ -433,23 +432,21 @@ const App = () => { key: "comment", title: "Comment Node", icon: nodeEmojis.comment, - tooltip: "Make a comment about your flow.", + tooltip: NODE_TOOLTIPS.comment, onClick: () => addNode("comment"), }, { key: "script", title: "Global Python Scripts", icon: nodeEmojis.script, - tooltip: - "Specify directories to load as local packages, so they can be imported in your Python evaluator nodes (add to sys path).", + tooltip: NODE_TOOLTIPS.script, onClick: () => addNode("scriptNode", "script"), }, { key: "selectvars", title: "Filter Variables Node", icon: , - tooltip: - "Filter which variables and metavariables to keep for the next steps.", + tooltip: NODE_TOOLTIPS.selectvars, onClick: () => addNode("selectVarsNode", "selectvars"), }, ]; @@ -464,31 +461,28 @@ const App = () => { key: "textfields", title: "Text Fields Node", icon: nodeEmojis.textfields, - tooltip: - "Specify input text to prompt or chat nodes. You can also declare variables in brackets {} to chain TextFields together.", + tooltip: NODE_TOOLTIPS.textfields, onClick: () => addNode("textFieldsNode", "textfields"), }, { key: "table", title: "Tabular Data Node", icon: nodeEmojis.table, - tooltip: - "Import or create a spreadhseet of data to use as input to prompt or chat nodes. Import accepts xlsx, csv, and jsonl.", + tooltip: NODE_TOOLTIPS.table, onClick: () => addNode("table"), }, { key: "csv", title: "Items Node", icon: nodeEmojis.csv, - tooltip: - "Specify inputs as a comma-separated list of items. Good for specifying lots of short text values. An alternative to TextFields node.", + tooltip: NODE_TOOLTIPS.csv, onClick: () => addNode("csvNode", "csv"), }, { key: "media", title: "Media Node", icon: nodeEmojis.media, - tooltip: "Add image data with corresponding metadata.", + tooltip: NODE_TOOLTIPS.media, onClick: () => addNode("media", "media"), }, { @@ -502,16 +496,14 @@ const App = () => { key: "prompt", title: "Prompt Node", icon: nodeEmojis.prompt, - tooltip: - "Prompt one or multiple LLMs. Specify prompt variables in brackets {}.", + tooltip: NODE_TOOLTIPS.prompt, onClick: () => addNode("promptNode", "prompt", { prompt: "" }), }, { key: "chat", title: "Chat Turn Node", icon: nodeEmojis.chat, - tooltip: - "Start or continue a conversation with chat models. Attach Prompt Node output as past context to continue chatting past the first turn.", + tooltip: NODE_TOOLTIPS.chat, onClick: () => addNode("chatTurn", "chat", { prompt: "" }), }, { @@ -530,15 +522,14 @@ const App = () => { key: "simpleval", title: "Simple Evaluator", icon: nodeEmojis.simpleval, - tooltip: - "Evaluate responses with a simple check (no coding required).", + tooltip: NODE_TOOLTIPS.simpleval, onClick: () => addNode("simpleEval", "simpleval"), }, { key: "evaluator-javascript", title: "JavaScript Evaluator", icon: nodeEmojis.evaluator, - tooltip: "Evaluate responses by writing JavaScript code.", + tooltip: NODE_TOOLTIPS["evaluator-javascript"], onClick: () => addNode("evalNode", "evaluator", { language: "javascript", @@ -549,7 +540,7 @@ const App = () => { key: "evaluator-python", title: "Python Evaluator", icon: nodeEmojis.evaluator, - tooltip: "Evaluate responses by writing Python code.", + tooltip: NODE_TOOLTIPS["evaluator-python"], onClick: () => addNode("evalNode", "evaluator", { language: "python", @@ -560,16 +551,14 @@ const App = () => { key: "llmeval", title: "LLM Evaluation", icon: nodeEmojis.llmeval, - tooltip: - "Evaluate responses with an LLM. (Note that LLM evaluators should be used with caution and always double-checked.)", + tooltip: NODE_TOOLTIPS.llmeval, onClick: () => addNode("llmeval"), }, { key: "multieval", title: "Multi-Evaluator", icon: nodeEmojis.multieval, - tooltip: - "Evaluate responses across multiple criteria (multiple code and/or LLM evaluators).", + tooltip: NODE_TOOLTIPS.multieval, onClick: () => addNode("multieval"), }, ], @@ -583,24 +572,21 @@ const App = () => { key: "join", title: "Join Node", icon: nodeEmojis.join, - tooltip: - "Concatenate responses or input data together before passing into later nodes, within or across variables and LLMs.", + tooltip: NODE_TOOLTIPS.join, onClick: () => addNode("join"), }, { key: "split", title: "Split Node", icon: nodeEmojis.split, - tooltip: - "Split responses or input data by some format. For instance, you can split a markdown list into separate items.", + tooltip: NODE_TOOLTIPS.split, onClick: () => addNode("split"), }, { key: "processor-javascript", title: "JavaScript Processor", icon: nodeEmojis.evaluator, - tooltip: - "Transform responses by mapping a JavaScript function over them.", + tooltip: NODE_TOOLTIPS["processor-javascript"], onClick: () => addNode("process", "processor", { language: "javascript", @@ -611,8 +597,7 @@ const App = () => { key: "processor-python", title: "Python Processor", icon: nodeEmojis.evaluator, - tooltip: - "Transform responses by mapping a Python function over them.", + tooltip: NODE_TOOLTIPS["processor-python"], onClick: () => addNode("process", "processor", { language: "python", @@ -632,16 +617,14 @@ const App = () => { key: "vis", title: "Vis Node", icon: nodeEmojis.vis, - tooltip: - "Plot evaluation results. (Attach an evaluator or scorer node as input.)", + tooltip: NODE_TOOLTIPS.vis, onClick: () => addNode("visNode", "vis", {}), }, { key: "inspect", title: "Inspect Node", icon: nodeEmojis.inspect, - tooltip: - "Used to inspect responses from prompter or evaluation nodes, without opening up the pop-up view.", + tooltip: NODE_TOOLTIPS.inspect, onClick: () => addNode("inspectNode", "inspect"), }, { diff --git a/chainforge/react-server/src/ChunkNode.tsx b/chainforge/react-server/src/ChunkNode.tsx index 59b2cb8c..bf8fee1d 100644 --- a/chainforge/react-server/src/ChunkNode.tsx +++ b/chainforge/react-server/src/ChunkNode.tsx @@ -17,6 +17,7 @@ import LLMResponseInspectorModal, { } from "./LLMResponseInspectorModal"; import InspectFooter from "./InspectFooter"; import { IconSearch } from "@tabler/icons-react"; +import DocumentationButton from "./DocumentationButton"; import ChunkMethodListContainer, { ChunkMethodSpec, @@ -271,6 +272,7 @@ const ChunkNode: React.FC = ({ data, id }) => { status={status} handleRunClick={runChunking} runButtonTooltip="Perform chunking on input text" + customButtons={[]} /> { const btns: React.ReactNode[] = []; + // Documentation button + btns.push( + , + ); + // If this is Python and we are running locally, the user has // two options ---whether to run code in sandbox with pyodide, or from Flask (unsafe): if (progLang === "python" && IS_RUNNING_LOCALLY) diff --git a/chainforge/react-server/src/CommentNode.tsx b/chainforge/react-server/src/CommentNode.tsx index 8971efe3..c192d231 100644 --- a/chainforge/react-server/src/CommentNode.tsx +++ b/chainforge/react-server/src/CommentNode.tsx @@ -3,6 +3,7 @@ import useStore from "./store"; import NodeLabel from "./NodeLabelComponent"; import BaseNode from "./BaseNode"; import { Textarea } from "@mantine/core"; +import DocumentationButton from "./DocumentationButton"; export interface CommentNodeProps { data: { @@ -28,7 +29,12 @@ const CommentNode: React.FC = ({ data, id }) => { return ( - + ]} + />