diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 165c996..2f87f74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,42 +14,32 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + strategy: 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" - name: Install dependencies run: | - python -m pip install --upgrade pip - grep -vf req.filters.txt requirements.txt | pip install -r /dev/stdin - ./code/tools/lint.sh deps - - - name: Check Python syntax with Pylint - run: | - ./code/tools/lint.sh pylint - - - name: Check Type Hints with MyPy - run: | - ./code/tools/lint.sh mypy - - - name: Check code formatting with Black - run: | - ./code/tools/lint.sh black - - - name: Ensure Notebooks have no output - run: | - ./code/tools/lint.sh ipynb - - - - - + python -m pip install --upgrade pip wheel + pip install -r requirements.txt + pip install ruff mypy + + - name: Check code formatting + run: make format + - name: Run code linters + run: make lint + - name: Check typehints + run: make typecheck + - name: Ensure notebooks have no output + run: make check-notebooks diff --git a/.github/workflows/generate-documentation.yml b/.github/workflows/generate-documentation.yml deleted file mode 100644 index ce140d1..0000000 --- a/.github/workflows/generate-documentation.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Generate README - -on: - push: - paths: - - 'docs/**' # Trigger when files under the docs folder change - workflow_dispatch: # Also allows manual triggering from the GitHub Actions UI - -jobs: - build-docs: - runs-on: ubuntu-latest - - steps: - # Step 1: Checkout the repository - - name: Checkout repository - uses: actions/checkout@v2 - - # Step 2: Set up Python - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.11' - - # Step 3: Install Dependencies (Pandoc, Make, etc.) - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y make wget - PANDOC_VERSION=3.1.8 - wget https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-amd64.deb - sudo dpkg -i pandoc-${PANDOC_VERSION}-1-amd64.deb - python -m pip install --upgrade pip - pip install -r .github/workflows/generate-documentation_requirements.txt - - # Step 4: Build Documentation - - name: Build documentation - working-directory: docs - run: make all - - # Step 5: Upload README.md as an artifact for manual review - #- name: Upload README.md as artifact - # uses: actions/upload-artifact@v4 - # with: - # name: generated-readme - # path: README.md # Adjust the path if necessary - - # Step 6: Display the diff for review in logs - - name: Show diff for README.md - run: | - echo "Comparing differences for the updated README" - git diff README.md || echo "No differences to display" - - # Step 7: Commit changes automatically - - name: Commit and push changes - run: | - git config --local user.name "GitHub Actions" - git config --local user.email "actions@github.com" - git add README.md .static - git diff --cached --exit-code || git commit -m "Auto-generated: README.md and related content" - git push - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Use GitHub token for authentication \ No newline at end of file diff --git a/.github/workflows/generate-documentation_requirements.txt b/.github/workflows/generate-documentation_requirements.txt deleted file mode 100644 index d17b333..0000000 --- a/.github/workflows/generate-documentation_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -confz==2.0.1 -Jinja2>=3.0.0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c7fc6b..e155146 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Ignore generated or temporary files managed by the Workbench .project/* +!.project/compose.yaml !.project/spec.yaml !.project/configpacks @@ -56,6 +57,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +.ruff_cache # Workbench Project Layout data/scratch/* diff --git a/compose.yaml b/.project/compose.yaml similarity index 100% rename from compose.yaml rename to .project/compose.yaml diff --git a/.project/spec.yaml b/.project/spec.yaml index e80cd4b..7c2d376 100644 --- a/.project/spec.yaml +++ b/.project/spec.yaml @@ -13,7 +13,7 @@ layout: - path: code/ type: code storage: git - - path: docs/ + - path: libs/ type: code storage: git - path: data/ @@ -25,8 +25,8 @@ layout: environment: base: registry: nvcr.io - image: nvidia/ai-workbench/python-cuda122:1.0.3 - build_timestamp: "20231214221614" + image: nvidia/ai-workbench/python-cuda122:1.0.6 + build_timestamp: "20250205043304" name: Python with CUDA 12.2 supported_architectures: [] cuda_version: "12.2" @@ -53,8 +53,8 @@ environment: url_command: jupyter lab list | head -n 2 | tail -n 1 | cut -f1 -d' ' | grep -v 'Currently' programming_languages: - python3 - icon_url: "" - image_version: 1.0.3 + icon_url: https://workbench.download.nvidia.com/static/img/ai-workbench-icon-rectangle.jpg + image_version: 1.0.6 os: linux os_distro: ubuntu os_distro_release: "22.04" @@ -75,17 +75,14 @@ environment: - python3-dev - python3-pip - vim - - less - - jq - - ssh - name: pip - binary_path: /usr/local/bin/pip + binary_path: /usr/bin/pip installed_packages: - - jupyterlab==4.0.7 + - jupyterlab==4.2.5 package_manager_environment: name: "" target: "" - compose_file_path: "" + compose_file_path: .project/compose.yaml execution: apps: - name: Visual Studio Code diff --git a/.static/_static/generate_personal_key.png b/.static/_static/generate_personal_key.png deleted file mode 100644 index 8786a22..0000000 Binary files a/.static/_static/generate_personal_key.png and /dev/null differ diff --git a/.static/_static/mac_dmg_drag.png b/.static/_static/mac_dmg_drag.png deleted file mode 100644 index ed4f494..0000000 Binary files a/.static/_static/mac_dmg_drag.png and /dev/null differ diff --git a/.static/_static/na_frontend.png b/.static/_static/na_frontend.png deleted file mode 100644 index bbf7dee..0000000 Binary files a/.static/_static/na_frontend.png and /dev/null differ diff --git a/.static/_static/nim-anywhere.png b/.static/_static/nim-anywhere.png deleted file mode 100644 index 77981e2..0000000 Binary files a/.static/_static/nim-anywhere.png and /dev/null differ diff --git a/.static/_static/nvwb_clone.png b/.static/_static/nvwb_clone.png deleted file mode 100644 index 29a8f2c..0000000 Binary files a/.static/_static/nvwb_clone.png and /dev/null differ diff --git a/.static/_static/nvwb_left_menu.png b/.static/_static/nvwb_left_menu.png deleted file mode 100644 index ecc5007..0000000 Binary files a/.static/_static/nvwb_left_menu.png and /dev/null differ diff --git a/.static/_static/nvwb_locations.png b/.static/_static/nvwb_locations.png deleted file mode 100644 index 957a3b4..0000000 Binary files a/.static/_static/nvwb_locations.png and /dev/null differ diff --git a/.static/_static/nvwb_logs.png b/.static/_static/nvwb_logs.png deleted file mode 100644 index ac033af..0000000 Binary files a/.static/_static/nvwb_logs.png and /dev/null differ diff --git a/.static/_static/nvwb_projects.png b/.static/_static/nvwb_projects.png deleted file mode 100644 index 217935d..0000000 Binary files a/.static/_static/nvwb_projects.png and /dev/null differ diff --git a/.static/_static/personal_key.png b/.static/_static/personal_key.png deleted file mode 100644 index 87802a2..0000000 Binary files a/.static/_static/personal_key.png and /dev/null differ diff --git a/.static/_static/personal_key_form.png b/.static/_static/personal_key_form.png deleted file mode 100644 index a0c6eb0..0000000 Binary files a/.static/_static/personal_key_form.png and /dev/null differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2f9cfc6..785f2c8 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,11 +2,8 @@ "recommendations": [ "ms-python.vscode-pylance", "ms-python.python", - "ms-python.debugpy", "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "ms-python.isort", - "ms-python.pylint", - "ms-toolsai.jupyter" + "ms-toolsai.jupyter", + "charliermarsh.ruff" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 7233115..ecfc2db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,7 @@ { // file explorer configuration "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, + ".git": true, "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true, @@ -11,27 +9,25 @@ "**/__pycache__": true, "**/.mypy_cache": true, "**/.ipynb_checkpoints": true, - "**/.terraform": true, + ".project": true, + ".vscode": true, + ".github": true, + "**/.ruff_cache": true, }, - // global editor settings "files.eol": "\n", "editor.tabSize": 4, "editor.insertSpaces": true, "files.insertFinalNewline": true, - // remove this line to automatically forward ports - // in general, workbench will manage this already "remote.autoForwardPorts": false, - // bash scripting configuration "[shellscript]": { "editor.tabSize": 4, - "editor.insertSpaces": false, + "editor.insertSpaces": false }, - // css style sheet configuration "[css]": { "editor.suggest.insertMode": "replace", @@ -41,31 +37,29 @@ // js configuration "[javascript]": { "editor.maxTokenizationLineLength": 2500, - "editor.tabSize": 2, + "editor.tabSize": 2 }, // Python environment configuration "python.terminal.activateEnvironment": true, "python.defaultInterpreterPath": "/usr/bin/python3", - "isort.args":["--profile", "black"], - "isort.check": true, + + // Ruff as formatter "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.organizeImports": "explicit", + "source.fixAll": "explicit" }, - // Comment out this settings to disable auto-formatting "editor.formatOnSave": true }, - "black-formatter.serverTransport": "stdio", - "black-formatter.args": [ - "--line-length", - "120" + "ruff.enable": true, + "ruff.exclude": [], + // tutorial apps use a different python environment + "python.analysis.exclude": [ + "code/tutorial_app/**", + "libs/**" ], - "pylint.severity": { - "refactor": "Information", - }, - - "python.analysis.ignore": ["code/tutorial_app/**"] + "mypy-type-checker.ignorePatterns": ["**/answers/*"], } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..001b310 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Help command +default: help +.PHONY: help +help: # Show help for each of the Makefile recipes. + @echo "Available targets:" + @grep -E '^[a-zA-Z0-9 -]+:.*#' Makefile | sort | while read -r l; do printf "\033[1;32m$$(echo $$l | cut -f 1 -d':')\033[00m:$$(echo $$l | cut -f 2- -d'#')\n"; done + +## CI Pipeline logic +.PHONY: ci lint format typecheck test check-notebooks +ci: lint format typecheck check-notebooks test # Run the ci pipeline locally + +lint: # Examing the code with linters + ruff check . + +format: # Check the code formatting + ruff format --check . + +format-fix: # Autofix the code formatting where it is possible + +typecheck: # Check the type hints in the code + mypy . + +test: # Run unit tests + # pytest --quiet + +check-notebooks: # Ensure the jupyter notebooks have no saved + find . -name "*.ipynb" -exec jupyter nbconvert --clear-output --inplace {} + + git diff --exit-code || (echo "Notebooks have output. Clear it before committing." && exit 1) diff --git a/README.md b/README.md index 2583b49..59c6ef6 100644 --- a/README.md +++ b/README.md @@ -1,354 +1,30 @@ -# NVIDIA NIM Anywhere [![Clone Me with AI Workbench](https://img.shields.io/badge/Open_In-AI_Workbench-76B900)](https://ngc.nvidia.com/open-ai-workbench/aHR0cHM6Ly9naXRodWIuY29tL05WSURJQS9uaW0tYW55d2hlcmUK) - -[![NVIDIA: LLM NIM](https://img.shields.io/badge/NVIDIA-LLM%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#large-language-models) -[![NVIDIA: Embedding NIM](https://img.shields.io/badge/NVIDIA-Embedding%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#nemo-retriever) -[![NVIDIA: Reranker NIM](https://img.shields.io/badge/NVIDIA-Reranker%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#nemo-retriever) -[![CI Pipeline Status](https://github.com/nvidia/nim-anywhere/actions/workflows/ci.yml/badge.svg?query=branch%3Amain)](https://github.com/NVIDIA/nim-anywhere/actions/workflows/ci.yml?query=branch%3Amain) -![Python: 3.10 | 3.11 | 3.12](https://img.shields.io/badge/Python-3.10%20|%203.11%20|%203.12-yellow?logo=python&logoColor=white&color=%23ffde57) - - - -Please join \#cdd-nim-anywhere slack channel if you are a internal user, -open an issue if you are external for any question and feedback. - -One of the primary benefit of using AI for Enterprises is their ability -to work with and learn from their internal data. Retrieval-Augmented -Generation -([RAG](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/)) -is one of the best ways to do so. NVIDIA has developed a set of -micro-services called [NIM -micro-service](https://docs.nvidia.com/nim/large-language-models/latest/introduction.html) -to help our partners and customers build effective RAG pipeline with -ease. - -NIM Anywhere contains all the tooling required to start integrating NIMs -for RAG. It natively scales out to full-sized labs and up to production -environments. This is great news for building a RAG architecture and -easily adding NIMs as needed. If you're unfamiliar with RAG, it -dynamically retrieves relevant external information during inference -without modifying the model itself. Imagine you're the tech lead of a -company with a local database containing confidential, up-to-date -information. You don’t want OpenAI to access your data, but you need the -model to understand it to answer questions accurately. The solution is -to connect your language model to the database and feed them with the -information. - -To learn more about why RAG is an excellent solution for boosting the -accuracy and reliability of your generative AI models, [read this -blog](https://developer.nvidia.com/blog/enhancing-rag-applications-with-nvidia-nim/). - -Get started with NIM Anywhere now with the [quick-start](#quick-start) -instructions and build your first RAG application using NIMs! - -![NIM Anywhere Screenshot](.static/_static/nim-anywhere.png) - -- [Quick-start](#quick-start) - - [Generate your NGC Personal Key](#generate-your-ngc-personal-key) - - [Authenticate with Docker](#authenticate-with-docker) - - [Install AI Workbench](#install-ai-workbench) - - [Download this project](#download-this-project) - - [Configure this project](#configure-this-project) - - [Start This Project](#start-this-project) - - [Populating the Knowledge Base](#populating-the-knowledge-base) -- [Developing Your Own Applications](#developing-your-own-applications) -- [Application Configuration](#application-configuration) - - [Config from a file](#config-from-a-file) - - [Config from a custom file](#config-from-a-custom-file) - - [Config from env vars](#config-from-env-vars) - - [Chain Server config schema](#chain-server-config-schema) - - [Chat Frontend config schema](#chat-frontend-config-schema) -- [Contributing](#contributing) - - [Code Style](#code-style) - - [Updating the frontend](#updating-the-frontend) - - [Updating documentation](#updating-documentation) -- [Managing your Development - Environment](#managing-your-development-environment) - - [Environment Variables](#environment-variables) - - [Python Environment Packages](#python-environment-packages) - - [Operating System Configuration](#operating-system-configuration) - - [Updating Dependencies](#updating-dependencies) - -# Quick-start - -## Generate your NGC Personal Key - -To allow AI Workbench to access NVIDIA’s cloud resources, you’ll need to -provide it with a Personal Key. These keys begin with `nvapi-`. +# NVIDIA NIM Anywhere -
- -Expand this section for instructions for creating this key. - - -1. Go to the [NGC Personal Key - Manager](https://org.ngc.nvidia.com/setup/personal-keys). If you are - prompted to, then register for a new account and sign in. - - > **HINT** You can find this tool by logging into - > [ngc.nvidia.com](https://ngc.nvidia.com), expanding your profile - > menu on the top right, selecting *Setup*, and then selecting - > *Generate Personal Key*. - -2. Select *Generate Personal Key*. - - ![Generate Personal Key](.static/_static/generate_personal_key.png) - -3. Enter any value as the Key name, an expiration of 12 months is fine, - and select all the services. Press *Generate Personal Key* when you - are finished. - - ![Personal Key Form](.static/_static/personal_key_form.png) - -4. Save your personal key for later. Workbench will need it and there - is no way to retrieve it later. If the key is lost, a new one must - be created. Protect this key as if it were a password. - - ![Personal Key](.static/_static/personal_key.png) - -
- -## Authenticate with Docker - -Workbench will use your system's Docker client to pull NVIDIA NIM -containers, so before continuing, make sure to follow these steps to -authenticate your Docker client with your NGC Personal Key. - -1. Run the following Docker login command - - ``` bash - docker login nvcr.io - ``` - -2. When prompted for your credentials, use the following values: - - - Username: `$oauthtoken` - - Password: Use your NGC Personal key beggining with `nv-api` - -## Install AI Workbench - -This project is designed to be used with [NVIDIA AI -Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/). -While this is not a requirement, running this demo without AI Workbench -will require manual work as the pre-configured automation and -integrations may not be available. - -This quick start guide will assume a remote lab machine is being used -for development and the local machine is a thin-client for remotely -accessing the development machine. This allows for compute resources to -stay centrally located and for developers to be more portable. Note, the -remote lab machine must run Ubuntu, but the local client can run -Windows, MacOS, or Ubuntu. To install this project local only, simply -skip the remote install. - -``` mermaid -flowchart LR - local - subgraph lab environment - remote-lab-machine - end - - local <-.ssh.-> remote-lab-machine -``` - -### Client Machine Install - -Ubuntu is required if the local client will also be used for developent. -When using a remote lab machine, this can be Windows, MacOS, or Ubuntu. - -
- -Expand this section for a Windows install. - - -For full instructions, see the [NVIDIA AI Workbench User -Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/windows.html). - -1. Install Prerequisite Software - - 1. If this machine has an NVIDIA GPU, ensure the GPU drivers are - installed. It is recommended to use the [GeForce - Experience](https://www.nvidia.com/en-us/geforce/geforce-experience/) - tooling to manage the GPU drivers. - 2. Install [Docker - Desktop](https://www.docker.com/products/docker-desktop/) for - local container support. Please be mindful of Docker Desktop's - licensing for enterprise use. [Rancher - Desktop](https://rancherdesktop.io/) may be a viable - alternative. - 3. *\[OPTIONAL\]* If Visual Studio Code integration is desired, - install [Visual Studio Code](https://code.visualstudio.com/). - -2. Download the [NVIDIA AI - Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) - installer and execute it. Authorize Windows to allow the installer - to make changes. +[![Clone Me with AI Workbench](https://img.shields.io/badge/Open_In-AI_Workbench-76B900?logo=nvidia)](https://build.nvidia.com/open-ai-workbench/aHR0cHM6Ly9naXRodWIuY29tL05WSURJQS9uaW0tYW55d2hlcmU=) +[![Work on your own fork](https://img.shields.io/badge/Create_your_own-Fork-4078c0?logo=github)](https://github.com/NVIDIA/nim-anywhere/fork) -3. Follow the instructions in the installation wizard. If you need to - install WSL2, authorize Windows to make the changes and reboot local - machine when requested. When the system restarts, the NVIDIA AI - Workbench installer should automatically resume. +NIM Anywhere is a starting point into discovering enterprise AI. This branch is currently under heavy construction. -4. Select Docker as your container runtime. +
-5. Log into your GitHub Account by using the *Sign in through - GitHub.com* option. - -6. Enter your git author information if requested. - -
- -
- -Expand this section for a MacOS install. - +# NEEDS TO BE UPDATED -For full instructions, see the [NVIDIA AI Workbench User -Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/macos.html). - -1. Install Prerequisite Software - - 1. Install [Docker - Desktop](https://www.docker.com/products/docker-desktop/) for - local container support. Please be mindful of Docker Desktop's - licensing for enterprise use. [Rancher - Desktop](https://rancherdesktop.io/) may be a viable - alternative. - 2. *\[OPTIONAL\]* If Visual Studio Code integration is desired, - install [Visual Studio Code](https://code.visualstudio.com/). - When using VSCode on a Mac, an a[dditional step must be - performed](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) - to install the VSCode CLI interface used by Workbench. - -2. Download the [NVIDIA AI - Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) - disk image (*.dmg* file) and open it. - -3. Drag AI Workbench into the Applications folder and run *NVIDIA AI - Workbench* from the application launcher. ![Mac DMG Install - Interface](.static/_static/mac_dmg_drag.png) - -4. Select Docker as your container runtime. - -5. Log into your GitHub Account by using the *Sign in through - GitHub.com* option. - -6. Enter your git author information if requested. - -
- -
- -Expand this section for an Ubuntu install. - +## Get Started -For full instructions, see the [NVIDIA AI Workbench User -Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/ubuntu-local.html). -Run this installation as the user who will be user Workbench. Do not run -these steps as `root`. +robot wave -1. Install Prerequisite Software +Hello world! - 1. *\[OPTIONAL\]* If Visual Studio Code integration is desired, - install [Visual Studio Code](https://code.visualstudio.com/). -2. Download the [NVIDIA AI - Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) - installer, make it executable, and then run it. You can make the - file executable with the following command: +### Prerequisites - ``` bash - chmod +x NVIDIA-AI-Workbench-*.AppImage - ``` +### Generate your NGC Personal Key -3. AI Workbench will install the NVIDIA drivers for you (if needed). - You will need to reboot your local machine after the drivers are - installed and then restart the AI Workbench installation by - double-clicking the NVIDIA AI Workbench icon on your desktop. +#### Install AI Workbench -4. Select Docker as your container runtime. +### Installing -5. Log into your GitHub Account by using the *Sign in through - GitHub.com* option. - -6. Enter your git author information if requested. - -
- -### Remote Machine Install - -Only Ubuntu is supported for remote machines. - -
- -Expand this section for a remote Ubuntu install. - - -For full instructions, see the [NVIDIA AI Workbench User -Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/ubuntu-remote.html). -Run this installation as the user who will be using Workbench. Do not -run these steps as `root`. - -1. Ensure SSH Key based authentication is enabled from the local - machine to the remote machine. If this is not currently enabled, the - following commands will enable this is most situations. Change - `REMOTE_USER` and `REMOTE-MACHINE` to reflect your remote address. - - - From a Windows local client, use the following PowerShell: - ``` powershell - ssh-keygen -f "C:\Users\local-user\.ssh\id_rsa" -t rsa -N '""' - type $env:USERPROFILE\.ssh\id_rsa.pub | ssh REMOTE_USER@REMOTE-MACHINE "cat >> .ssh/authorized_keys" - ``` - - From a MacOS or Linux local client, use the following shell: - ``` bash - if [ ! -e ~/.ssh/id_rsa ]; then ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ""; fi - ssh-copy-id REMOTE_USER@REMOTE-MACHINE - ``` - -2. SSH into the remote host. Then, use the following commands to - download and execute the NVIDIA AI Workbench Installer. - - ``` bash - mkdir -p $HOME/.nvwb/bin && \ - curl -L https://workbench.download.nvidia.com/stable/workbench-cli/$(curl -L -s https://workbench.download.nvidia.com/stable/workbench-cli/LATEST)/nvwb-cli-$(uname)-$(uname -m) --output $HOME/.nvwb/bin/nvwb-cli && \ - chmod +x $HOME/.nvwb/bin/nvwb-cli && \ - sudo -E $HOME/.nvwb/bin/nvwb-cli install - ``` - -3. AI Workbench will install the NVIDIA drivers for you (if needed). - You will need to reboot your remote machine after the drivers are - installed and then restart the AI Workbench installation by - re-running the commands in the previous step. - -4. Select Docker as your container runtime. - -5. Log into your GitHub Account by using the *Sign in through - GitHub.com* option. - -6. Enter your git author information if requested. - -7. Once the remote installation is complete, the Remote Location can be - added to the local AI Workbench instance. Open the AI Workbench - application, click *Add Remote Location*, and then enter the - required information. When finished, click *Add Location*. - - - \*Location Name: \* Any short name for this new location - - \*Description: \* Any brief metadata for this location. - - \*Hostname or IP Address: \* The hostname or address used to - remotely SSH. If step 1 was followed, this should be the same as - `REMOTE-MACHINE`. - - \*SSH Port: \* Usually left blank. If a nonstandard SSH port is - used, it can be configured here. - - \*SSH Username: \* The username used for making an SSH connection. - If step 1 was followed, this should be the same as `REMOTE_USER`. - - \*SSH Key File: \* The path to the private key for making SSH - connections. If step 1 was followed, this should be: - `/home/USER/.ssh/id_rsa`. - - \*Workbench Directory: \* Usually left blank. This is where - Workbench will remotely save state. - -
- -## Download this project +#### Download this project There are two ways to download this project for local use: Cloning and Forking. @@ -397,7 +73,7 @@ section. -## Configure this project +### Configure this project The project must be configured to use your NGC personal key. @@ -418,7 +94,7 @@ The project must be configured to use your NGC personal key. -## Start This Project +### Start This Project Even the most basic of LLM Chains depend on a few additional microservices. These can be ignored during development for in-memory @@ -511,7 +187,7 @@ without GPUs. -## Populating the Knowledge Base +### Populating the Knowledge Base To get started developing demos, a sample dataset is provided along with a Jupyter Notebook showing how data is ingested into a Vector Database. @@ -526,420 +202,23 @@ a Jupyter Notebook showing how data is ingested into a Vector Database. 3. If using a custom dataset, upload it to the `data/` directory in Jupyter and modify the provided notebook as necessary. -# Developing Your Own Applications - -This project contains applications for a few demo services as well as -integrations with external services. These are all orchestrated by -[NVIDIA AI -Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/). - -The demo services are all in the `code` folder. The root level of the -code folder has a few interactive notebooks meant for technical deep -dives. The Chain Server is a sample application utilizing NIMs with -LangChain. (Note that the Chain Server here gives you the option to -experiment with and without RAG). The Chat Frontend folder contains an -interactive UI server for exercising the chain server. Finally, sample -notebooks are provided in the Evaluation directory to demonstrate -retrieval scoring and validation. - -``` mermaid -mindmap - root((AI Workbench)) - Demo Services - Chain Server
LangChain + NIMs - Frontend
Interactive Demo UI - Evaluation
Validate the results - Notebooks
Advanced usage - - Integrations - Redis
Conversation History - Milvus
Vector Database - LLM NIM
Optimized LLMs -``` - -# Application Configuration - -The Chain Server can be configured with either a configuration file or -environment variables. - -## Config from a file - -By default, the application will search for a configuration file in all -of the following locations. If multiple configuration files are found, -values from lower files in the list will take precedence. - -- ./config.yaml -- ./config.yml -- ./config.json -- ~/app.yaml -- ~/app.yml -- ~/app.json -- /etc/app.yaml -- /etc/app.yml -- /etc/app.json - -## Config from a custom file - -An additional config file path can be specified through an environment -variable named `APP_CONFIG`. The value in this file will take precedence -over all the default file locations. - -``` bash -export APP_CONFIG=/etc/my_config.yaml -``` - -## Config from env vars - -Configuration can also be set using environment variables. The variable -names will be in the form: `APP_FIELD__SUB_FIELD` Values specified as -environment variables will take precedence over all values from files. - -## Chain Server config schema - -``` yaml -# Your API key for authentication to AI Foundation. -# ENV Variables: NGC_API_KEY, NVIDIA_API_KEY, APP_NVIDIA_API_KEY -# Type: string, null -nvidia_api_key: ~ - -# The Data Source Name for your Redis DB. -# ENV Variables: APP_REDIS_DSN -# Type: string -redis_dsn: redis://localhost:6379/0 - -llm_model: - # The name of the model to request. - # ENV Variables: APP_LLM_MODEL__NAME - # Type: string - name: meta/llama3-8b-instruct - - # The URL to the model API. - # ENV Variables: APP_LLM_MODEL__URL - # Type: string - url: https://integrate.api.nvidia.com/v1 - - -embedding_model: - # The name of the model to request. - # ENV Variables: APP_EMBEDDING_MODEL__NAME - # Type: string - name: nvidia/nv-embedqa-e5-v5 - - # The URL to the model API. - # ENV Variables: APP_EMBEDDING_MODEL__URL - # Type: string - url: https://integrate.api.nvidia.com/v1 - - -reranking_model: - # The name of the model to request. - # ENV Variables: APP_RERANKING_MODEL__NAME - # Type: string - name: nv-rerank-qa-mistral-4b:1 - - # The URL to the model API. - # ENV Variables: APP_RERANKING_MODEL__URL - # Type: string - url: https://integrate.api.nvidia.com/v1 - - -milvus: - # The host machine running Milvus vector DB. - # ENV Variables: APP_MILVUS__URL - # Type: string - url: http://localhost:19530 - - # The name of the Milvus collection. - # ENV Variables: APP_MILVUS__COLLECTION_NAME - # Type: string - collection_name: collection_1 - - -log_level: - -``` - -## Chat Frontend config schema - -The chat frontend has a few configuration options as well. They can be -set in the same manner as the chain server. - -``` yaml -# The URL to the chain on the chain server. -# ENV Variables: APP_CHAIN_URL -# Type: string -chain_url: http://localhost:3030/ - -# The url prefix when this is running behind a proxy. -# ENV Variables: PROXY_PREFIX, APP_PROXY_PREFIX -# Type: string -proxy_prefix: / - -# Path to the chain server's config. -# ENV Variables: APP_CHAIN_CONFIG_FILE -# Type: string -chain_config_file: ./config.yaml - -log_level: - -``` - # Contributing -All feedback and contributions to this project are welcome. When making -changes to this project, either for personal use or for contributing, it -is recommended to work on a fork on this project. Once the changes have -been completed on the fork, a Merge Request should be opened. - -## Code Style - -This project has been configured with Linters that have been tuned to -help the code remain consistent while not being overly burdensome. We -use the following Linters: - -- Bandit is used for security scanning -- Pylint is used for Python Syntax Linting -- MyPy is used for type hint linting -- Black is configured for code styling -- A custom check is run to ensure Jupyter Notebooks do not have any - output -- Another custom check is run to ensure the README.md file is up to date - -The embedded VSCode environment is configured to run the linting and -checking in realtime. - -To manually run the linting that is done by the CI pipelines, execute -`/project/code/tools/lint.sh`. Individual tests can be run be specifying -them by name: -`/project code/tools/lint.sh [deps|pylint|mypy|black|docs|fix]`. Running -the lint tool in fix mode will automatically correct what it can by -running Black, updating the README, and clearing the cell output on all -Jupyter Notebooks. - -## Updating the frontend - -The frontend has been designed in an effort to minimize the required -HTML and Javascript development. A branded and styled Application Shell -is provided that has been created with vanilla HTML, Javascript, and -CSS. It is designed to be easy to customize, but it should never be -required. The interactive components of the frontend are all created in -Gradio and mounted in the app shell using iframes. - -Along the top of the app shell is a menu listing the available views. -Each view may have its own layout consisting of one or a few pages. - -### Creating a new page - -Pages contain the interactive components for a demo. The code for the -pages is in the `code/frontend/pages` directory. To create a new page: - -1. Create a new folder in the pages directory -2. Create an `__init__.py` file in the new directory that uses Gradio - to define the UI. The Gradio Blocks layout should be defined in a - variable called `page`. -3. It is recommended that any CSS and JS files needed for this view be - saved in the same directory. See the `chat` page for an example. -4. Open the `code/frontend/pages/__init__.py` file, import the new - page, and add the new page to the `__all__` list. - -> **NOTE:** Creating a new page will not add it to the frontend. It must -> be added to a view to appear on the Frontend. - -### Adding a view - -View consist of one or a few pages and should function independently of -each other. Views are all defined in the `code/frontend/server.py` -module. All declared views will automatically be added to the Frontend's -menu bar and made available in the UI. - -To define a new view, modify the list named `views`. This is a list of -`View` objects. The order of the objects will define their order in the -Frontend menu. The first defined view will be the default. - -View objects describe the view name and layout. They can be declared as -follow: - -``` python -my_view = frontend.view.View( - name="My New View", # the name in the menu - left=frontend.pages.sample_page, # the page to show on the left - right=frontend.pages.another_page, # the page to show on the right -) -``` - -All of the page declarations, `View.left` or `View.right`, are optional. -If they are not declared, then the associated iframes in the web layout -will be hidden. The other iframes will expand to fill the gaps. The -following diagrams show the various layouts. - -- All pages are defined - -``` mermaid -block-beta - columns 1 - menu["menu bar"] - block - columns 2 - left right - end -``` - -- Only left is defined - -``` mermaid -block-beta - columns 1 - menu["menu bar"] - block - columns 1 - left:1 - end -``` - -### Frontend branding - -The frontend contains a few branded assets that can be customized for -different use cases. - -#### Logo - -The frontend contains a logo on the top left of the page. To modify the -logo, an SVG of the desired logo is required. The app shell can then be -easily modified to use the new SVG by modifying the -`code/frontend/_assets/index.html` file. There is a single `div` with an -ID of `logo`. This box contains a single SVG. Update this to the desired -SVG definition. - -``` html - -``` - -#### Color scheme - -The styling of the App Shell is defined in -`code/frontend/_static/css/style.css`. The colors in this file may be -safely modified. - -The styling of the various pages are defined in -`code/frontend/pages/*/*.css`. These files may also require modification -for custom color schemes. - -#### Gradio theme - -The Gradio theme is defined in the file -`code/frontend/_assets/theme.json`. The colors in this file can safely -be modified to the desired branding. Other styles in this file may also -be changed, but may cause breaking changes to the frontend. The [Gradio -documentation](https://www.gradio.app/guides/theming-guide) contains -more information on Gradio theming. - -### Messaging between pages - -> **NOTE:** This is an advanced topic that most developers will never -> require. - -Occasionally, it may be necessary to have multiple pages in a view that -communicate with each other. For this purpose, Javascript's -`postMessage` messaging framework is used. Any trusted message posted to -the application shell will be forwarded to each iframe where the pages -can handle the message as desired. The `control` page uses this feature -to modify the configuration of the `chat` page. - -The following will post a message to the app shell (`window.top`). The -message will contain a dictionary with the key `use_kb` and a value of -true. Using Gradio, this Javascript can be executed by [any Gradio -event](https://www.gradio.app/guides/custom-CSS-and-JS#adding-custom-java-script-to-your-demo). - -``` javascript -window.top.postMessage({"use_kb": true}, '*'); -``` - -This message will automatically be sent to all pages by the app shell. -The following sample code will consume the message on another page. This -code will run asynchronously when a `message` event is received. If the -message is trusted, a Gradio component with the `elem_id` of `use_kb` -will be updated to the value specified in the message. In this way, the -value of a Gradio component can be duplicated across pages. - -``` javascript -window.addEventListener( - "message", - (event) => { - if (event.isTrusted) { - use_kb = gradio_config.components.find((element) => element.props.elem_id == "use_kb"); - use_kb.props.value = event.data["use_kb"]; - }; - }, - false); -``` - -## Updating documentation - -The README is rendered automatically; direct edits will be overwritten. -In order to modify the README you will need to edit the files for each -section separately. All of these files will be combined and the README -will be automatically generated. You can find all of the related files -in the `docs` folder. - -Documentation is written in Github Flavored Markdown and then rendered -to a final Markdown file by Pandoc. The details for this process are -defined in the Makefile. The order of files generated are defined in -`docs/_TOC.md`. The documentation can be previewed in the Workbench file -browser window. - -### Header file - -The header file is the first file used to compile the documentation. -This file can be found at `docs/_HEADER.md`. The contents of this file -will be written verbatim, without any manipulation, to the README before -anything else. - -### Summary file +## Running the tests -The summary file contains quick description and graphic that describe -this project. The contents of this file will be added to the README -immediately after the header and just before the table of contents. This -file is processed by Pandoc to embed images before writing to the -README. - -### Table of Contents file - -The most important file for the documentation is the table of contents -file at `docs/_TOC.md`. This file defines a list of files that should be -concatenated in order to generate the final README manual. Files must be -on this list to be included. - -### Static Content +- lint +- ci -Save all static content, including images, to the `_static` folder. This -will help with organization. +## Managing your Development Environment -### Dynamic documentation - -It may be helpful to have documents that update and write themselves. To -create a dynamic document, simply create an executable file that writes -the Markdown formatted document to stdout. During build time, if an -entry in the table of contents file is executable, it will be executed -and its stdout will be used in its place. - -### Rendering documentation - -When a documentation related commit is pushed, a GitHub Action will -render the documentation. Any changes to the README will be automatially -committed. - -# Managing your Development Environment - -## Environment Variables +### Environment Variables Most of the configuration for the development environment happens with Environment Variables. To make permanent changes to environment variables, modify [`variables.env`](./variables.env) or use the Workbench UI. -## Python Environment Packages +### Python Environment Packages This project uses one Python environment at `/usr/bin/python3` and dependencies are managed with `pip`. Because all development is done @@ -947,7 +226,7 @@ inside a container, any changes to the Python environment will be ephemeral. To permanently install a Python package, add it to the [`requirements.txt`](./requirements.txt) file or use the Workbench UI. -## Operating System Configuration +### Operating System Configuration The development environment is based on Ubuntu 22.04. The primary user has password-less sudo access, but all changes to the system will be @@ -990,3 +269,7 @@ update. 7. **Regression testing:** Run through the entire demo, from document ingesting to the frontend, and ensure it is still functional and that the GUI looks correct. + +# License + +This project is licensed under the Apache 2.0 License - see the [LICENSE.txt](LICENSE.txt) file for details. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..d49e410 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,19 @@ +# NIM Anywhere Next + +## Basic Retrieval Lessons +- [ ] Getting started with llms +- [ ] singe doc rag +- [ ] naive rag +- [ ] advanced rag +- [ ] enterprise rag -> point to the rag 2.0 blueprint + +## AI Agent Lessons +- [x] Build an agent the hard way +- [ ] Build your first agent + +## CI/CD +- [ ] Look for dead links in tutorials and sidebar +- [ ] run all labs against answers + +## Misc +- [ ] Update readme diff --git a/apt.txt b/apt.txt index 76cd1b2..88c3d31 100644 --- a/apt.txt +++ b/apt.txt @@ -1,12 +1,9 @@ ca-certificates curl -gettext jq less make -pandoc pipx python3.10-venv -skopeo unzip wget diff --git a/code/chain_server/configuration.py b/code/chain_server/configuration.py index e13fecf..bb88a59 100644 --- a/code/chain_server/configuration.py +++ b/code/chain_server/configuration.py @@ -48,12 +48,13 @@ Values specified as environment variables will take precedence over all values from files. """ + import logging import os from enum import Enum -from typing import Annotated, Any, Callable, Optional, cast +from typing import Annotated, Any, Callable, ClassVar, Optional, cast -from confz import BaseConfig, EnvSource, FileSource +from confz import BaseConfig, ConfigSource, EnvSource, FileSource from pydantic import ( BaseModel, Field, @@ -189,7 +190,7 @@ class Configuration(BaseConfig): log_level: Annotated[LogLevels, Field(LogLevels.WARNING, description=LogLevels.__doc__)] # sources where config is looked for - CONFIG_SOURCES = [ + CONFIG_SOURCES: ClassVar[list[ConfigSource]] = [ FileSource(file="./config.yaml", optional=True), FileSource(file="./config.yml", optional=True), FileSource(file="./config.json", optional=True), diff --git a/code/frontend/configuration.py b/code/frontend/configuration.py index 5d22c54..57249d5 100644 --- a/code/frontend/configuration.py +++ b/code/frontend/configuration.py @@ -18,9 +18,9 @@ import logging import os from enum import Enum -from typing import Annotated +from typing import Annotated, ClassVar -from confz import BaseConfig, EnvSource, FileSource +from confz import BaseConfig, ConfigSource, EnvSource, FileSource from pydantic import Field, FilePath, HttpUrl, field_validator _ENV_VAR_PREFIX = "APP_" @@ -77,7 +77,7 @@ def _check_proxy_prefix(cls, val: str) -> str: log_level: Annotated[LogLevels, Field(LogLevels.INFO, description=LogLevels.__doc__)] # sources where config is looked for - CONFIG_SOURCES = [ + CONFIG_SOURCES: ClassVar[list[ConfigSource]] = [ FileSource(file="./frontend-config.yaml", optional=True), FileSource(file="./frontend-config.yml", optional=True), FileSource(file="./frontend-config.json", optional=True), diff --git a/code/frontend/pages/__init__.py b/code/frontend/pages/__init__.py index e6b358f..b0ad137 100644 --- a/code/frontend/pages/__init__.py +++ b/code/frontend/pages/__init__.py @@ -15,8 +15,6 @@ """A collection of pages available to views.""" -import gradio as gr - from .chat import page as chat from .control import page as control diff --git a/code/frontend/pages/chat/__init__.py b/code/frontend/pages/chat/__init__.py index 823637d..5049d9d 100644 --- a/code/frontend/pages/chat/__init__.py +++ b/code/frontend/pages/chat/__init__.py @@ -17,7 +17,7 @@ import uuid from pathlib import Path -from typing import AsyncGenerator, Any +from typing import Any, AsyncGenerator import gradio as gr from httpx import ConnectError, HTTPStatusError diff --git a/code/frontend/pages/control/__init__.py b/code/frontend/pages/control/__init__.py index 20cb05b..0e76661 100644 --- a/code/frontend/pages/control/__init__.py +++ b/code/frontend/pages/control/__init__.py @@ -15,24 +15,19 @@ """The control panel web app.""" -from pathlib import Path -import shutil -import glob -from typing import List +import os import time +from pathlib import Path -import os import gradio as gr import jinja2 import yaml - -from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings -from langchain_milvus.vectorstores.milvus import Milvus -from langchain_community.document_loaders import PyPDFLoader -from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain.schema import Document from chain_server.configuration import Configuration as ChainConfiguration from chain_server.configuration import config as chain_config +from langchain.schema import Document +from langchain_community.document_loaders import PyPDFLoader +from langchain_milvus.vectorstores.milvus import Milvus +from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings from ... import mermaid from ...common import IMG_DIR, THEME, USE_KB_INITIAL, USE_RERANKER_INITIAL @@ -101,10 +96,8 @@ # web ui definition with gr.Blocks(theme=THEME, css=_CSS, head=mermaid.HEAD) as page: - # %% contrl panel tab with gr.Tab("Control Panel", elem_id="cp-tab", elem_classes=["invert-bg"]): - # %% architecture control box with gr.Accordion(label="Retrieval Configuration"): with gr.Row(elem_id="kb-row"): @@ -137,7 +130,6 @@ # %% knowledge base tab with gr.Tab("Knowledge Base", elem_id="kb-tab", elem_classes=["invert-bg"]): - # upload file button upload_btn = gr.UploadButton("Upload PDFs", icon=str(_UPLOAD_IMG), file_types=[".pdf"], file_count="multiple") diff --git a/code/tools/audit.sh b/code/tools/audit.sh deleted file mode 100755 index dc05baa..0000000 --- a/code/tools/audit.sh +++ /dev/null @@ -1,24 +0,0 @@ -# auditing -function _audit_venv() { - echo "Auditing production python dependencies." - - # make a minimal python env - venv_dir=$(mktemp -d) - cd $venv_dir - python3 -m venv "$venv_dir" - "$venv_dir/bin/pip" install --upgrade pip - # Install pipdeptree - "$venv_dir/bin/pip" install pipdeptree - grep -vf /project/req.filters.txt /project/requirements.txt | \ - "$venv_dir/bin/pip" install -r /dev/stdin - - # run the audit on the venv - "$venv_dir/bin/python" /project/code/tools/audit_venv.py --ignore-healthy - rm -rf "$venv_dir" -} - -function main() { - _audit_venv -} - -main diff --git a/code/tools/audit_venv.py b/code/tools/audit_venv.py deleted file mode 100644 index a7a8772..0000000 --- a/code/tools/audit_venv.py +++ /dev/null @@ -1,154 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Check the version of all the dependencies.""" - -# pylint: disable=bad-builtin,global-statement - -import argparse -import json -import subprocess -import sys -from collections import defaultdict -from datetime import datetime -from pprint import pprint -from typing import Any, Dict, List, Tuple - -import requests -from pip._vendor.packaging.specifiers import SpecifierSet -from tqdm import tqdm - -NOW = datetime.now() -VULN_MIRROR = "https://pyup.io/aws/safety/free/insecure_full.json" -VULN_DB = requests.get(VULN_MIRROR, timeout=15).json() -EXIT_CODE = 0 - - -def _parse_arguments() -> argparse.Namespace: - """Parse the CLI arguments.""" - parser = argparse.ArgumentParser("Dependency Auditor") - parser.add_argument( - "--ignore-healthy", - action="store_true", - default=False, - help="If set, only dependencies at yellow, red, or unknown are displayed.", - ) - parser.add_argument( - "--max-age", - type=int, - default=30, - help="Maximum number of days for a dependency to be considered old.", - ) - return parser.parse_args() - - -def _read_dependencies() -> Tuple[List[Tuple[str, str]], Dict[str, List[str]]]: - """Find all the dependencies of this project.""" - dep_tree = json.loads( - subprocess.run(["pipdeptree", "--json", "--python", sys.executable], check=True, capture_output=True).stdout - ) - - all_deps = [(row["package"]["key"], row["package"]["installed_version"]) for row in dep_tree] - dep_map: Dict[str, List[str]] = defaultdict(list) - for row in dep_tree: - for dep in row["dependencies"]: - dep_map[dep["key"]].append(row["package"]["key"]) - - return all_deps, dep_map - - -def _latest_ver(package: str, report: Dict[str, Any]) -> None: - """Lookup the latest version of a package.""" - # lookup package in pip - req = requests.get(f"https://pypi.org/pypi/{package}/json", timeout=5) - if req.status_code != 200: - return - metadata = req.json() - - # calculate age - installed = report.get("installed", "0") - installed_meta = metadata["releases"].get(installed) - released = datetime.fromisoformat(installed_meta[0]["upload_time"]) - report["age_days"] = (NOW - released).days - - # find latest - report["latest"] = metadata["info"]["version"] - - -def _security(package: str, report: Dict[str, Any], max_age: int) -> None: - """Lookup the safety of a package.""" - global EXIT_CODE - - installed = report.get("installed", "0") - latest = report.get("latest", "") - age_days = report.get("age_days", "-1") - - # check for cves - active_cves: List[Tuple[str, str]] = [] - db_records: List[Dict[str, Any]] = VULN_DB.get(package, []) - for cve in db_records: - for vuln_spec in cve["specs"]: - if SpecifierSet(vuln_spec).contains(installed): - active_cves.append( - ( - cve["cve"], - f"https://nvd.nist.gov/vuln/detail/{cve['cve']}", - ) - ) - - # assume an out of date copy - report["health"] = "🟑" - - # the installed version is comprimised - if active_cves: - report["health"] = "πŸ”΄" - report["cves"] = active_cves - EXIT_CODE = 1 - - # package was not found in pypi - if age_days == -1: - report["health"] = "⁇" - - # the installed version is not comprimised and (latest or relatively new-ish) - if latest == installed or age_days < max_age: - report["health"] = "🟒" - - -def main() -> None: - """Execute main routine.""" - # lookup package information - args = _parse_arguments() - full_report = {} - print("Reading project dependencies...") - all_packages, depdency_map = _read_dependencies() - - print("Processing and scoring dependencies...") - for pkg, installed in tqdm(all_packages): - pkg_report = {"installed": installed} - if depdency_map.get(pkg): - pkg_report["required_by"] = depdency_map[pkg] - _latest_ver(pkg, pkg_report) # add age_days and latest - _security(pkg, pkg_report, args.max_age) # add cves and health score - - if not args.ignore_healthy or pkg_report.get("health", "🟒") != "🟒": - full_report[pkg] = pkg_report - - print("\nFinal Report:") - pprint(full_report, sort_dicts=False) - - sys.exit(EXIT_CODE) - - -if __name__ == "__main__": - main() diff --git a/code/tools/bump.sh b/code/tools/bump.sh deleted file mode 100755 index 3a34258..0000000 --- a/code/tools/bump.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -set -e -cd /project - - -# helpers -function _divider() { - echo -e "\n\n\n\n\n" -} - -# python deps -function _bump_reqs() { - echo "Updating Python Dependencies" - pip install bumper --upgrade > /dev/null - bump -} - -# nims -function _bump_nims() { - echo "Updating NIMs" - for app in apps/*.sh; do - # get application image - export IMAGE=$(env -i /bin/bash $app image 2> /dev/null) - export TAG=$(env -i /bin/bash $app tag 2> /dev/null) - - # ensure this is a nim - if [[ "$IMAGE" != "nvcr.io/nim/"* ]]; then - continue - fi - - # find the latest tag - LATEST=$( \ - skopeo list-tags docker://$IMAGE | \ - jq '.Tags' | \ - jq -r '.[] | select(contains("sha256") | not)' | \ - grep -v 'latest' | \ - tail -n 1) - - # check if an update is required - if [ "${LATEST}x" != "${TAG}x" ]; then - echo "Updating: $app [ $TAG -> $LATEST ]" - sed -i -r 's/(^TAG=.*)"'$TAG'"\)/\1"'$LATEST'")/g' "$app" - git diff "$app" - else - echo "NO CHANGE" - echo "$TAG == $LATEST" - fi - echo "" - done -} - -main() { - _bump_reqs - _divider - _bump_nims -} - -main diff --git a/code/tools/lint.sh b/code/tools/lint.sh deleted file mode 100755 index 3ebe474..0000000 --- a/code/tools/lint.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -set -e - -cd $(dirname $0)/../.. - -if [ -z "$1" ] || [ "$1" = "deps" ]; then - echo "Installing dependencies..." - which pylint || pip install pylint - which mypy || pip install mypy - which black || pip install black - echo -e "\n\n\n" -fi - -if [ -z "$1" ] || [ "$1" = "pylint" ]; then - echo "Checking Python syntax with Pylint." - pylint --rc-file pyproject.toml $(git ls-files 'code/*.py' | grep -v tutorial_app) - echo -e "\n\n\n" -fi - -if [ -z "$1" ] || [ "$1" = "mypy" ]; then - echo "Checking Type Hints with MyPy" - mypy --config-file pyproject.toml $(git ls-files 'code/*.py') - echo -e "\n\n\n" -fi - -if [ -z "$1" ] || [ "$1" = "black" ]; then - echo "Checking code formatting with Black" - black --check . --line-length 120 $(git ls-files 'code/*.py') - echo -e "\n\n\n" -fi - -if [ -z "$1" ] || [ "$1" = "ipynb" ]; then - echo "Checking Notebooks for cells with output." - fail_count=0 - for file in $(git ls-files 'code/*.ipynb'); do - echo -en "$file\t" - # filter the ipynb json to get only the cell output, remove empty values - outputs=$(cat $file | jq '.cells[].outputs' | grep -Pv '(null|\[\])' | cat) - if [ "$outputs" == "" ]; then - echo "pass" - else - echo "fail" - echo "$outputs" - fail_count=$(expr $fail_count + 1) - fi - done - - if [ $fail_count > 0 ]; then - exit $fail_count - fi - echo -e "\n\n\n" -fi - -if [ -z "$1" ] || [ "$1" = "docs" ]; then - echo "Checking if the Documentation is up to date." - cd docs; - if ! make -q; then - make -qd | grep -v '^ ' | grep -v 'is older than' - echo 'fail: in the docs folder run `make all` to update the readme' >&2 - exit 1 - fi - echo "pass" - cd .. - echo -e "\n\n\n" -fi - -if [ "$1" = "fix" ]; then - echo "Fixing code formatting with Black" - black . --line-length 120 $(git ls-files 'code/*.py') - echo -e "\n\n\n" - - echo "Ensuring the README.md is up to date" - cd docs - make - cd .. - - echo "Clearing Jupyter Notebook cell output." - for ipynb in $(git ls-files 'code/*.ipynb'); do - if cat "$ipynb" | jq '.cells[].outputs' | grep -Pv '(null|\[\])' > /dev/null ; then - echo "$ipynb" - jupyter nbconvert "$ipynb" --ClearOutputPreprocessor.enabled=True --to=notebook --inplace --log-level=ERROR - fi - done - - echo -e "\n\n\n" -fi - -echo -e "Success." - diff --git a/code/tutorial_app/.vscode/settings.json b/code/tutorial_app/.vscode/settings.json new file mode 100644 index 0000000..58d2322 --- /dev/null +++ b/code/tutorial_app/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "git.openRepositoryInParentFolders": "always" +} diff --git a/code/tutorial_app/answers/agents_the_hard_way/caching.py b/code/tutorial_app/answers/agents_the_hard_way/caching.py new file mode 100644 index 0000000..34aa44f --- /dev/null +++ b/code/tutorial_app/answers/agents_the_hard_way/caching.py @@ -0,0 +1,43 @@ +"""Some helpers for caching LLM calls during development.""" + +from collections import OrderedDict +from typing import Any + +from cachier import cachier +from openai import OpenAI + +CACHE_DIR = "/project/data/scratch" + + +def _call_llm_cached_hash(_: Any, kwds: OrderedDict[str, Any]) -> int: + """Safely create a hash used for the cache.""" + model_name = kwds.get("model_name", "---") + message_history = str(kwds.get("message_history")) + tool_list = str(kwds.get("tool_list")) + return hash(model_name + message_history + tool_list) + + +@cachier(cache_dir=CACHE_DIR, allow_none=False, hash_func=_call_llm_cached_hash) +def call_llm_cached( + model_client: OpenAI, + model_name: str, + message_history: list[dict[str, str]], + tool_list: None | list[dict[str, Any]] = None, +) -> None | dict[str, Any]: + """Create OpenAI completions request.""" + if tool_list: + response = model_client.chat.completions.create( + model=model_name, + messages=message_history, + tools=tool_list, + tool_choice="auto", + ) + else: + response = model_client.chat.completions.create( + model=model_name, + messages=message_history, + ) + if response.choices: + return response.choices[0].message.to_dict() + else: + return None diff --git a/code/tutorial_app/answers/agents_the_hard_way/single_agent.py b/code/tutorial_app/answers/agents_the_hard_way/single_agent.py new file mode 100644 index 0000000..d436249 --- /dev/null +++ b/code/tutorial_app/answers/agents_the_hard_way/single_agent.py @@ -0,0 +1,72 @@ +"""An example agent built from scratch.""" +# type: ignore + +import json +import os + +from caching import call_llm_cached +from openai import OpenAI + +API_KEY = os.environ.get("NGC_API_KEY", "---") +MODEL_URL = "https://integrate.api.nvidia.com/v1" +MODEL_NAME = "meta/llama-3.3-70b-instruct" + + +# Connect to the model server +client = OpenAI(base_url=MODEL_URL, api_key=API_KEY) + + +# Create a tool for your agent +def add(a, b): + return a + b + + +# Create a list of all the available tools +tools = [ + { + "type": "function", + "function": { + "name": "add", + "description": "Add two integers.", + "parameters": { + "type": "object", + "properties": { + "a": {"type": "integer", "description": "First integer"}, + "b": {"type": "integer", "description": "Second integer"}, + }, + "required": ["a", "b"], + }, + }, + } +] + + +# Initilialize some short term memory +messages = [{"role": "user", "content": "What is 3 plus 12?"}] + + +# Prompt the model for a response to the question and update the memory +llm_response = call_llm_cached(client, MODEL_NAME, messages, tools) +messages.append(llm_response) + + +# Extract tool request +tool_call = messages[-1]["tool_calls"][0] +tool_name = tool_call["function"]["name"] +tool_args = json.loads(tool_call["function"]["arguments"]) +tool_id = tool_call["id"] + + +# Run the tool +if tool_name == "add": + tool_out = add(**tool_args) + + +# Save the tool output into the memory +tool_result = {"role": "tool", "tool_call_id": tool_id, "name": tool_name, "content": str(tool_out)} +messages.append(tool_result) + + +# Prompt the model again, this time with the tool output +llm_response = call_llm_cached(client, messages, tools) +messages.append(llm_response) diff --git a/code/tutorial_app/new_page.sh b/code/tutorial_app/new_page.sh deleted file mode 100755 index d59d7a3..0000000 --- a/code/tutorial_app/new_page.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -cd $(dirname $0) - - -TEMPLATE_NAME="template" -export PAGE_NAME=$1 - -envsubst < pages_templates/${TEMPLATE_NAME}.py.envsub > pages/${PAGE_NAME}.py -envsubst < pages_templates/${TEMPLATE_NAME}.en_US.yaml.envsub > pages/${PAGE_NAME}.en_US.yaml -envsubst < pages_templates/${TEMPLATE_NAME}_tests.py.envsub > pages/${PAGE_NAME}_tests.py - -cat <> pages/sidebar.yaml - - label: "$PAGE_NAME" - target: $PAGE_NAME -EOM diff --git a/code/tutorial_app/pages/__init__.py b/code/tutorial_app/pages/__init__.py index e69de29..a08b2c2 100644 --- a/code/tutorial_app/pages/__init__.py +++ b/code/tutorial_app/pages/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/code/tutorial_app/pages/agents_the_hard_way.en_US.yaml b/code/tutorial_app/pages/agents_the_hard_way.en_US.yaml new file mode 100644 index 0000000..e60e025 --- /dev/null +++ b/code/tutorial_app/pages/agents_the_hard_way.en_US.yaml @@ -0,0 +1,388 @@ +# header data +title: Agents the Hard Way +waiting_msg: Let me know when you are ready. +testing_msg: Your turn! Edit the code in the editor. +next: Let's go! + +welcome_msg: | + In this excercise, we will be building a simple AI agent from scratch. + Later, we will replace much of this logic with prebuilt tools, but building it ourselves helps + us understand the fundamentals. + + ![intro picture](app/static/robots/assembly.png) + +tasks: + - name: What is an agent? + msg: | + An agent is an application that uses an LLM to make decisions and interact with the outside world. + + Agents have four key parts: + 1. **MODEL:** An LLM that decides which tools to use and how to respond + 1. **TOOLS:** Functions that let the LLM perform actions like math or database queries + 1. **MEMORY:** Information available to the LLM during and between conversations + 1. **ROUTING:** The LLM will make decisions about what to do next, we need to route messages accordingly + + In this exercise, we'll build a basic agent by implementing these basic components from scratch. + + - name: Setup the environment + msg: | + To get us started, I'll drop is some code and explain as I go. + + Like most Python code, we start by importing the necessary modules. + - The `openai` module is maintained by OpenAI and is used for talking to models running remotely. + - The `caching` module helps us cache LLM responses and conserve tokens. + + The caching module is custom code, but it makes use of the `cachier` Python library. + Feel free to check it out by opening Visual Studio Code or JupyterLab from the Workbench interface. + response: | + Let's keep going! + prep: prep_imports + + - name: Load the configuration + msg: | + Now I'll load our configuration as contants: + - `API_KEY` loads our credentials from an environment variable + - `MODEL_URL` points the the server hosting your model + - `MODEL_NAME` is the model we are going to use + + Why these values? + + This `MODEL_URL` points to NVIDIA's hosted Model API Catalog. + + Because we are starting with NVIDIA's hosted service, we have a lot of models to choose from. + Picking where to start can be difficult. + + - Start with a newer open source model from a team you recognize. + - Start with a moderate sized model (~70b parameters). You'll work on optimizing to a smaller model later. + - If you need features like function calling, make sure the model supports it! (More on that later). + response: | + Let's keep going! + prep: prep_api_key + + - name: Part 1 - The Model + msg: | + The first of four critical parts for an agent is the AI model. + + We will talk to the AI models on build.nvidia.com using the OpenAI API. + This API is the *language* that most model providers use. + This means we can use the `OpenAI` class to connect to most model providers. Neat! + + Using the `MODEL_URL` and `API_KEY` defined above, create a new model client named `client`. + + #### Resources + - [Docs: OpenAI Quick Start](https://github.com/openai/openai-python?tab=readme-ov-file#usage) + - [Docs: OpenAI Custom base URL](https://github.com/openai/openai-python?tab=readme-ov-file#configuring-the-http-client) + - [Example: NVIDIA hosted models](https://build.nvidia.com/nvidia/llama-3_3-nemotron-super-49b-v1) + +
+ Need some help? + + ```python + client = OpenAI( + base_url=MODEL_URL, + api_key=API_KEY + ) + ``` +
+ response: | + Great! We are now connected to the models! + prep: prep_define_client + test: test_define_client + + - name: Part 2 - Tools + msg: | + Every agent has access to some tools. + Tools are how the model is able to interact with the world. + + Tools are created by developers to connect to external services. + The model cannot *do* anything that it doesn't have a tool for. + + Tools are simply code that is executed at the LLM's request. + So lets write our first tool! + + Create a function, called `add`, that adds two integers, called `a` and `b`. + +
+ Need some help? + + Here is a simple Python function for adding two numbers. + + ```python + def add(a, b): + return a + b + ``` + + We can use this function as a tool by returning it from our function. +
+ response: ~ + prep: prep_adding_tool + test: test_adding_tool + + - name: Describe the tools + msg: | + Before moving on, we need create a description of the tools that the model can understand. + Think of this as the LLM's menu of possible helpers. + + The syntax on this is tricky and picky, so I'll do this part. + The good news is this is usualy handled for you by agent frameworks. + This syntax is defined as part of the OpenAI API spec. + + #### Resources + - [Docs: Defining functions format](https://platform.openai.com/docs/guides/function-calling?api-mode=responses&example=get-weather#defining-functions) + response: Your turn again! + prep: prep_tools_list + + - name: Part 3 - Memory + msg: | + The topic of memory is complex, and we will only scratch the surface. + There are two types of memory, short term and long term. + For now, lets focus on short term memory. + + Short term memory starts at the beginining of the conversation + and ends at the end of the conversation. + Put simply, short term memory is a log of the conversation. + + For this, we will use a humble list. + Every line in our list will be a message in the conversation, stored in a dictionary. + The messages can come from the user, the assistant, or from tools. + + Create an initial list called `memory`. + Initialize it with this message from the user: + + ```python + {"role": "user", "content": "What is 3 plus 12?"} + ``` + + #### Reference + - [Docs: Chat completion docs](https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages) + +
+ Need some help? + + ```python + messages = [ + {"role": "user", "content": "What is 3 plus 12?"} + ] + ``` +
+ response: Looking good. Now let's have some fun! + prep: prep_messages + test: test_messages + + - name: Run the agent + msg: | + We now have three of the four pieces required of an agent. + The last missing piece is the routing. + For the time being, we will forgo this piece and manually route this mesasge. + + The agent starts by giving the memory and tool list to the model. + The model will reply by either requesting a tool or answering the question. + + Based on the model's response, we will decide how to proceed. + + Call the model using the `call_llm_cached` function. + The function takes four arguments: + - `model_client` - the OpenAI client + - `model_name` - the name of the model to use (check the constants from before) + - `message_history` - your short term memory + - `tool_list` - the menu of tools the model can access + + Use this function to call the llm. + Save the result by appending it to the end of `messages`. + +
+ Need some help? + + ```python + llm_response = call_llm_cached( + client, + MODEL_NAME, + messages, + tools + ) + messages.append(llm_response) + ``` +
+ response: | + It's alive! + + πŸ‘‡ This is what we got back. πŸ‘‡ + + ```json + {{ result }} + + | eval | tojson(2) }} + ``` + prep: prep_run_agent + test: test_run_agent + + - name: Part 4 - Routing + msg: | + Looks like the model chose to request a tool instead of answering. + We can tell because `content` is `null` and a function is defined under `tool_calls`. + +
+ Tools vs Function Calling? + + These terms are often used interchangeably. + Technically, **Function Calling** is a feature of a model. + This feature allows the model to request that the developer run the **Tools**. + + But most of the time, these terms are simply referring to an agents ability to run functions. +
+ + We can see that the model has requested that we run the `add` function with the arguments 3 and 12. + + Write some code to extract the requested function's name, arguments, and id from `messages[-1]`. + Store those values in variables called `tool_name`, `tool_args`, and `tool_id` respectively. + + :blue-badge[TIP] The value of `arguments` is a string. Use the `json` library to read it. + + ```python + tool_args = json.loads(tool_args_str) + ``` + +
+ Need some help? + + ```python + tool_call = messages[-1]["tool_calls"][0] + tool_name = tool_call["function"]["name"] + tool_args = json.loads(tool_call["function"]["arguments"]) + tool_id = tool_call["id"] + ``` +
+ prep: prep_extract_tool + test: test_extract_tool + + - name: Tool Calling + msg: | + If you haven't noticed by now, even though the feature is called tool calling... + The model doesn't actually call the tool! + + So let's write the code to run the tools as requested. + + Check if the tool name is equal to `add`. + If it is, then run the add function with the requested arguments. + + Save the output from the tool call to a variable called `tool_out`. + +
+ Need some help? + + ```python + if tool_name == "add": + tool_out = add(**tool_args) + ``` +
+ response: | + Excellent! This is a portion of the heavy lifting usually handled by agent frameworks. + prep: prep_execute_tool + test: test_execute_tool + + - name: Update the memory + msg: | + We just got `tool_out` back from the tool. + Now we can update the memory with the tool output. + + Next time we call the model, it will see the prompt from the user, + it's own request for a tool call, and the result of that tool call. + + This is another one where the syntax is tricky and picky. + I'll type this one out, but this is the standard message format for a tool call result. + This format is also defined by OpenAI. + + #### Reference + - [Docs: Chat completion docs](https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages) + response: | + Our memory now looks like this: + + ```json + [ + {"role": "user", "content": "What is 3 plus 12?"}, + {"role": "assistant", "tool_calls": ... }, + {"role": "tool", "tool_call_id": "...", "name": "add", "content": "15"} + ] + ``` + prep: prep_update_memory + + - name: Loop back to the model + msg: | + Now, we call the model again and save the response in memory. + + :blue-badge[HINT] It is the exact same code as last time. + +
+ Need some help? + + ```python + llm_response = call_llm_cached( + client, + MODEL_NAME, + messages, + tools + ) + messages.append(llm_response) + ``` +
+ response: | + You got it! + + Here is what the model had to say: + + ```json + {{ result | eval | tojson(2) }} + ``` + prep: prep_call_model_again + test: test_call_model_again + +closing_header: You did it! +closing_msg: | + Your very first agent! Congratulations. + + This simple example demonstrates the basic components of an agent and how they work together. + + Of course... we made a few *convenient assumptions* along the way. + + For example. What if: + - the agent wants more than one tool call? + - the agent doesn't want any? + - we need additional feedback from the user? + - we want build complex multi-agent systems? + + The list goes on. This is the value of agentic frameworks. + All of these worries are abstracted away and you focus on writing and wiring tools. + There are many agentic frameworks to choose from, and many people choose to make their own. + + In the next example, we will be using [LangGraph](https://www.langchain.com/langgraph) as our framework. + +# Test time error messages +info_test_nonzero_exit_code: "Uh oh! Looks like your code isn't running. Check the *Terminal Output* tab." +info_no_client: "Create the variable named `client`." +info_wrong_client_type: "`client` needs to be an instance of the OpenAI class." +info_client_bad_request: | + We got a Bad Request error. This usually happens if you give the wrong args to the `chat.completions.create` method. +info_client_bad_auth: | + You API Key was rejected. + Check the value you set in the workbench interface in the Project Container --> Variables configuration. +info_test_bad_url: "No models were found at the provided URL. Make sure we are pointing to a working endpoint." +info_no_add: "Create a function named `add`." +info_add_not_fun: "`add` needs to be a funtion." +info_bad_add_args: "Check the arguments to your `add` function." +info_add_not_working: "The `add` function doesn't seem to be adding." +info_no_messages: "Create a `messages` variable to be a list of messages." +info_messages_not_correct: "Check the contents of your `messages` variable." +info_messages_too_short: | + The chat history should have a langth of two. One message from the user, + and the reply from the model. +info_bad_message_order: | + `messages` appears to be in the wrong order. The oldest messages should be first (lowest index). +info_no_tool_out: | + Waiting to see the tool output in `tool_out`. +info_tool_out_not_num: | + The output from your tool doesn't seem to be a number. +info_no_tool_name: "`tool_name` should be set to equal the function name from the LLM response." +info_no_tool_args: "`tool_args` should be set to equal the function arguments. Make sure to decode them with the `json` library." +info_no_tool_id: "`tool_id` should be the tool call's id from the LLM response." +info_messages_len_3: "Call the llm with `call_llm_cached` and append the response to `messages`." +info_messages_all_wrong: "Make sure you append to the messages list." diff --git a/code/tutorial_app/pages/editor_test.py b/code/tutorial_app/pages/agents_the_hard_way.py similarity index 89% rename from code/tutorial_app/pages/editor_test.py rename to code/tutorial_app/pages/agents_the_hard_way.py index cf44a1e..5070821 100644 --- a/code/tutorial_app/pages/editor_test.py +++ b/code/tutorial_app/pages/agents_the_hard_way.py @@ -19,19 +19,19 @@ import live_labs import streamlit as st -from pages import editor_test_tests as TESTS +from pages import agents_the_hard_way_tests as TESTS MESSAGES = live_labs.MessageCatalog.from_page(__file__) NAME = Path(__file__).stem EDITOR_DIR = Path("/project/code").joinpath(NAME) -EDITOR_FILES = ["file1.py", "file2.py"] +EDITOR_FILES = ["single_agent.py"] +# name of the editor file with live_labs.Worksheet(name=NAME, autorefresh=0).with_editor(EDITOR_DIR, EDITOR_FILES) as worksheet: # Header st.title(MESSAGES.get("title")) st.write(MESSAGES.get("welcome_msg")) - st.header(MESSAGES.get("header"), divider="gray") # Print Tasks worksheet.live_lab(MESSAGES, TESTS) diff --git a/code/tutorial_app/pages/agents_the_hard_way_tests.py b/code/tutorial_app/pages/agents_the_hard_way_tests.py new file mode 100644 index 0000000..871e461 --- /dev/null +++ b/code/tutorial_app/pages/agents_the_hard_way_tests.py @@ -0,0 +1,335 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for auto continuing associated tasks.""" + +import shutil +from pathlib import Path + +from live_labs.editor import send_keys +from live_labs.testing import isolate + +NAME = "agents_the_hard_way" +ANSWER_DIR = Path(__file__).parent.parent.joinpath("answers", NAME) +EDITOR_DIR = Path("/project/code").joinpath(NAME) +PYTHON_EXE = "/usr/bin/python" + + +## Setup the environment +def prep_imports() -> None: + """Prepare the imports and api key.""" + cache_answer = ANSWER_DIR.joinpath("caching.py") + cache_lib = EDITOR_DIR.joinpath("caching.py") + shutil.copy(cache_answer, cache_lib) + + send_keys( + r''' + """An example agent built from scratch.""" + + import json + import os + + from caching import call_llm_cached + from openai import OpenAI + ''' + ) + + +## Load the configuration +def prep_api_key() -> None: + """Prepare the imports and api key.""" + send_keys( + r""" + + API_KEY = os.environ.get("NGC_API_KEY", "---") + MODEL_URL = "https://integrate.api.nvidia.com/v1" + MODEL_NAME = "meta/llama-3.3-70b-instruct" + """ + ) + + +## Part 1 - The Model +def prep_define_client() -> None: + """Add some comments.""" + send_keys( + r""" + + + # Connect to the model server + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_define_client(): + """define the client""" + import caching # pyright: ignore[reportMissingImports] + import openai # pyright: ignore[reportMissingImports] + import single_agent # pyright: ignore[reportMissingImports] + + MODEL_NAME = "meta/llama-3.3-70b-instruct" + + # look for the value + if not hasattr(single_agent, "client"): + print(":TestFail: info_no_client") + return + + # ensure the correct type + if not isinstance(single_agent.client, openai.OpenAI): + print(":TestFail: info_wrong_client_type") + return + + # ensure it works + messages = [{"role": "user", "content": "Hello!"}] + try: + _ = caching.call_llm_cached(single_agent.client, MODEL_NAME, messages) + except openai.BadRequestError: + print(":TestFail: info_client_bad_request") + except openai.AuthenticationError: + print(":TestFail: info_client_bad_auth") + except openai.NotFoundError: + print(":TestFail: info_test_bad_url") + + +## Part 2 - Tools +def prep_adding_tool() -> None: + """Add some comments.""" + send_keys( + r""" + + + # Create a tool for your agents + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_adding_tool(): + """make sure its an add function""" + import inspect + + import single_agent # pyright: ignore[reportMissingImports]s + + if not hasattr(single_agent, "add"): + print(":TestFail: info_no_add") + return + + if not callable(single_agent.add): + print(":TestFail: info_add_not_fun") + + signature = inspect.signature(single_agent.add) + args = list(signature.parameters.keys()) + if args != ["a", "b"]: + print(":TestFail: info_bad_add_args") + + if single_agent.add(7, 8) != 7 + 8: + print(":TestFail: info_add_not_working") + return + + +## Describe the Tools +def prep_tools_list(): + """Write out the tools list.""" + send_keys( + r""" + + + # A list of tools for the LLM + tools = [ + { + "type": "function", + "function": { + "name": "add", + "description": "Add two integers.", + "parameters": { + "type": "object", + "properties": { + "a": {"type": "integer", "description": "First integer"}, + "b": {"type": "integer", "description": "Second integer"}, + }, + "required": ["a", "b"], + }, + }, + } + ] + """ + ) + + +## Part 3 - Memory +def prep_messages(): + """Write a comment.""" + send_keys( + r""" + + + # Initilialize some short term memory + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_messages(): + """create a message list""" + import single_agent # pyright: ignore[reportMissingImports] + + if not hasattr(single_agent, "messages"): + print(":TestFail: info_no_messages") + return + + if single_agent.messages != [{"role": "user", "content": "What is 3 plus 12?"}]: + print(":TestFail: info_messages_not_correct") + return + + +## Run the agent +def prep_run_agent(): + """Write a comment.""" + send_keys( + r""" + + + # Prompt the model for a response to the question and update the memory + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_run_agent(): + """Wait for llm response.""" + import single_agent # pyright: ignore[reportMissingImports] + + if not hasattr(single_agent, "messages"): + print(":TestFail: info_no_messages") + return + + if len(single_agent.messages) < 2: # noqa + print(":TestFail: info_messages_too_short") + return + + if single_agent.messages[1]["role"] != "assistant": + print(":TestFail: info_bad_message_order") + return + + print(single_agent.messages[1]) + + +## Part 4 - Routing +def prep_extract_tool(): + """Write some comments.""" + send_keys( + r""" + + + # Extract tool request + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_extract_tool(): + """Wait for tool execution.""" + import single_agent # pyright: ignore[reportMissingImports] + + # check for variable + for var, var_type in {"tool_name": str, "tool_args": dict, "tool_id": str}.items(): + if not hasattr(single_agent, var): + print(f":TestFail: info_no_{var}") + return + + var_val = getattr(single_agent, var) + + if not isinstance(var_val, var_type): + print(f":TestFail: info_no_{var}") + return + + +## Tool Calling +def prep_execute_tool(): + """Write some comments.""" + send_keys( + r""" + + + # Run the requested tool + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_execute_tool(): + """Wait for tool execution.""" + import numbers + + import single_agent # pyright: ignore[reportMissingImports] + + # check for variable + if not hasattr(single_agent, "tool_out"): + print(":TestFail: info_no_tool_out") + return + + # check variable type + if not isinstance(single_agent.tool_out, numbers.Number): + print(":TestFail: info_tool_out_not_num") + return + + +## Update the memory +def prep_update_memory(): + """Write some comments.""" + send_keys( + r""" + + + # Save the tool output into the memory + tool_result = {"role": "tool", "tool_call_id": tool_id, "name": tool_name, "content": str(tool_out)} + messages.append(tool_result) + """ + ) + + +## Loop back to the model +def prep_call_model_again(): + """Write some comments.""" + send_keys( + r""" + + + # Call the model again with the tool output + """ + ) + + +@isolate(EDITOR_DIR, PYTHON_EXE) +def test_call_model_again(): + """call the model again""" + import single_agent # pyright: ignore[reportMissingImports] + + if not hasattr(single_agent, "messages"): + print(":TestFail: info_no_messages") + return + + if not isinstance(single_agent.messages, list): + print(":TestFail: info_messages_all_wrong") + return + + if len(single_agent.messages) == 3: # noqa + print(":TestFail: info_messages_len_3") + return + + if len(single_agent.messages) != 4: # noqa + print(":TestFail: info_messages_all_wrong") + return + + print(single_agent.messages[3]) diff --git a/code/tutorial_app/pages/editor_test.en_US.yaml b/code/tutorial_app/pages/editor_test.en_US.yaml deleted file mode 100644 index 21a0e5b..0000000 --- a/code/tutorial_app/pages/editor_test.en_US.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# header data -title: Write some code -welcome_msg: | - In this example, we are going to write some code. -header: | - Sometimes, you have to write code. - - here, we will teach you how to do that. - -# general strings -waiting_msg: Let me know when you are ready. -testing_msg: Waiting for you to complete the task. -next: Next - -# task script -tasks: - - name: Make a string - msg: | - In test1.py, define a string called `my_string` that equals `five`. - -
- Need a hint? - - Try this: - - ```python - my_string = "five" - ``` -
- response: | - Nicely done! I checked and here is what I saw: - {{ result | indent(4) }} - test: test_my_string - -# footer data -closing_msg: "Congratulations! You have completed this exercise." - -# testing messages -# the helpers in the testing module may return one of these errors -info_test_nonzero_exit_code: "Uh oh! I'm not able to run your code. Check the **Terminal Output** tab for details." -info_test_timeout: "Your code seems to be taking too long to run." - -# custom testing messages -# if you add manual tests, you can add your own messages here -info_no_my_string: "`my_string` does not exist." -info_my_string_not_five: "`my_string` exists, but isn't set to the correct value." diff --git a/code/tutorial_app/pages/editor_test_tests.py b/code/tutorial_app/pages/editor_test_tests.py deleted file mode 100644 index ccff212..0000000 --- a/code/tutorial_app/pages/editor_test_tests.py +++ /dev/null @@ -1,58 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Tests for auto continuing associated tasks.""" - -import sys -from pathlib import Path - -from live_labs.testing import TestFail, isolate - -NAME = "editor_test" -EDITOR_DIR = Path("/project/code").joinpath(NAME) - - -def sample_test(): - """Test a project using built in helpers.""" - # The testing module contains some test compatible helpers. - # These helpers can either get an object or ensure the state of an object. - # If there is an error in these helpers, they will raise TestFail automatically. - raise TestFail("info_test_test") - - -@isolate(EDITOR_DIR) -def test_my_string(): - """Wait for my_string to be ready.""" - import file1 # pyright: ignore[reportMissingImports] - - print("Looking for my_string.") - - if not hasattr(file1, "my_string"): - print(":TestFail: info_no_my_string") - return - - print("Looking for five.") - - if file1.my_string != "five": - print(":TestFail: info_my_string_not_five") - return - - print("Looks good!") - - -if __name__ == "__main__": - sys.stdout.write("---------------\n") - # you can use this space for testing while you are - # developing your tests - test_my_string() diff --git a/code/tutorial_app/pages/overview.en_US.yaml b/code/tutorial_app/pages/overview.en_US.yaml index 18ad1d5..3446145 100644 --- a/code/tutorial_app/pages/overview.en_US.yaml +++ b/code/tutorial_app/pages/overview.en_US.yaml @@ -1,128 +1,17 @@ # header data -title: NVIDIA AI Workbench Live Labs -waiting_msg: Click `Next` to continue. -testing_msg: Go do the task! -next: Next - -header: "Orientation" +title: NIM Anywhere Live Labs welcome_msg: | - ![](app/static/hero.png) + ![hero picture](app/static/robots/strong.png) #### Welcome - This is the AI Workbench Live Labs, an interactive guide to the features of NVIDIA AI Workbench. + This is the NIM Anywhere Live Labs, an interactive guide to using NVIDIA solutions. It will help you learn by doing instead of only reading the docs. - However, you should feel free to work through the [docs](https://docs.nvidia.com/ai-workbench/user-guide/latest/overview/introduction.html) - on your own or with the assistance of an LLM. - It is organized into **exercises** that are divided into **tasks**. - Completing each **task** unlocks content for the exercise and will give you feedback. - - - -tasks: - - - name: Guide Overview - msg: | - ###### Use the Sidebar to navigate this guide (click the :arrow_forward: icon in the top left to expand it) - There are three sections of exercises, and each exercise is organized into tasks. Completing - tasks unlocks more content in the exercise. - - 1. **Basic** explores core concepts and the Desktop App UI. (15 mins) - 2. **Advanced** digs deeper to show example workflows. (20 mins - 1 hour) - 3. **Sneak Peek** previews "in-early-development" features that will be stable soon. - - response: Ok. Let's get started. **Make sure the Desktop App is running with the view for this project visible.** - - - name: The AI Workbench Desktop App - msg: | - ##### AI Workbench is a free development platform that helps you: - * **Connect** to and work on remote systems as if they were local. - * **Develop** in containerized environments. - * **Manage** development environments across local and remote systems. - - ##### The user experience is handled through views and tabs. - - - **Main Locations View**: For managing and accessing remote systems - - **Single Location View**: For managing and accessing different projects on a single system - - **Project Tab**: For managing and working with a single project - - ![overview](app/static/overview/cascade.png) - ##### There are three basics steps to get working: - 1. You select a **location** to work in (*local* is default) - 2. You create or select a **project** to work on - 3. You start a **web app** or an **IDE** to do your work in the project - - ###### If you've gotten this far, you've already done all of these steps. - - response: | - Let's get started. **Make sure the Desktop App is running with the view for this project visible.** - - - - name: Starting JupyterLab - msg: | - ###### Let's begin with the most basic thing possible in a Workbench project, starting JupyterLab. - - Go to the Desktop App and the Project View for this project - - Find the large green button at the top right and click the drop down - - Select JupyterLab - - Wait for JupyterLab to open in your default browser. - - ![dropdown](app/static/overview/dropdown.png) - - response: Great. JupyterLab is running. - - - name: Stopping JupyterLab - msg: | - ###### Now let's do the second most basic thing possible, stopping JupyterLab. - - Go to the project view for this project and click the **Project Dashboard** - - Scroll down a little to see the **Project Container** section - - You will see that both this Tutorial app and JupyterLab are running - - Click the JupyterLab toggle to stop it. - - response: You stopped JupyterLab. - - - name: Finishing Up the Overview - msg: | - ###### Objective sources for help and validation are important, so don't be shy about asking for help. - - If something goes wrong or you are confused, work through the following steps: - - 1. Click the green :material/book_4: button to see our troubleshooting guide. - 2. Click the green :material/help: button to see crowd sourced answers in the NVIDIA Developer Forum. - 3. If you find a bug, click the :material/bug_report: button in the sidebar to create a report on GitHub. - 4. If all else fails, email us at `aiworkbench-support@nvidia.com`. - - response: Awesome. Let's get started! - - - - -# footer data -closing_msg: | - ##### Ok. Now you are ready to get started for real. + As you complete the requested tasks, the excercise will advance itself. -# testing messages -# the helpers in the testing module may return one of these errors -info_wait_for_project: "Waiting for the project to exist." -info_build_ready: ~ -info_build_needed: "It looks like your project needs you to start a new build. Please do that in the environment tab." -info_build_running: "Your project's build is currently running." -info_buid_error: "Uh oh! There was an error building your project. Please check the logs." -info_container_not_running: ~ -info_container_running: ~ -info_container_paused: "The container has been manually paused." -info_container_dead: "Uh oh! The container does not seem healthy. Please check the Workbench logs." -info_wait_for_app: "Waiting for the application to exist." -info_app_is_running: ~ -info_app_not_running: "Waiting for you to start the application." -info_app_starting: "The application is starting up! Just a few more seconds." -info_compose_is_running: "Docker Compose is running." -info_compose_not_running: "Start Docker Compose from the environment tab." -info_compose_starting: "Docker Compose is starting. This can take a while the first time." -info_compose_error: "Uh oh! Docker Compose had an error. Please check the logs." -info_wait_for_package: "Waiting for you to configure the necessary package." -info_wait_for_file: "Waiting for you to create the requested file." + Let's make some AI! diff --git a/code/tutorial_app/pages/overview.py b/code/tutorial_app/pages/overview.py index d19c774..6e08d53 100644 --- a/code/tutorial_app/pages/overview.py +++ b/code/tutorial_app/pages/overview.py @@ -28,7 +28,3 @@ # Header st.title(MESSAGES.get("title")) st.write(MESSAGES.get("welcome_msg")) - st.header(MESSAGES.get("header"), divider="gray") - - # Print Tasks - worksheet.live_lab(MESSAGES, TESTS) diff --git a/code/tutorial_app/pages/sidebar.yaml b/code/tutorial_app/pages/sidebar.yaml index 612b35b..203e6b0 100644 --- a/code/tutorial_app/pages/sidebar.yaml +++ b/code/tutorial_app/pages/sidebar.yaml @@ -14,5 +14,7 @@ navbar: - label: "🏠 Overview" target: "overview" show_progress: False - - label: "πŸ“ŽEditor Test" - target: editor_test + - label: Build Agents with NVIDIA + children: + - label: "πŸ“ŽAgents the Hard Way" + target: agents_the_hard_way diff --git a/code/tutorial_app/pages_templates/template.en_US.yaml.envsub b/code/tutorial_app/pages_templates/template.en_US.yaml.envsub deleted file mode 100644 index f780efd..0000000 --- a/code/tutorial_app/pages_templates/template.en_US.yaml.envsub +++ /dev/null @@ -1,61 +0,0 @@ -# header data -title: $PAGE_NAME -welcome_msg: | - An example welcome message. - - You **should** update this. GitHub Flavored Markdown is accepted with some additional extensions. - - Check out the [formatting docs here](https://docs.streamlit.io/develop/api-reference/text/st.markdown). -header: | - This is a new page! - Edit the content to make your own tutorial. - -# general strings -waiting_msg: Let me know when you are ready. -testing_msg: Waiting for task to complete. -next: Next - -# task script -tasks: - - name: The first task - msg: | - This message should explain the task to the user. - - Again, Markdown is accepted. - response: | - After the user completes their task, this message will be shown in a green bubble. - test: ~ - # some tasks are able to check themselves and automatically continue when they are complete. - # to do this, add a function to ${PAGE_NAME}_tests.py to test if the step has completed. - # then add the function name to test. - # - # if this task does not have a test, you can omit this line or set it to the null value (~) - -# footer data -closing_msg: "Congratulations! You have completed this exercise." - -# testing messages -# the helpers in the testing module may return one of these errors -info_wait_for_project: "Waiting for the project to exist." -info_build_ready: ~ -info_build_needed: "It looks like your project needs you to start a new build. Please do that in the environment tab." -info_build_running: "Your project's build is currently running." -info_buid_error: "Uh oh! There was an error building your project. Please check the logs." -info_container_not_running: ~ -info_container_running: ~ -info_container_paused: "The container has been manually paused." -info_container_dead: "Uh oh! The container does not seem healthy. Please check the Workbench logs." -info_wait_for_app: "Waiting for the application to exist." -info_app_is_running: ~ -info_app_not_running: "Waiting for you to start the application." -info_app_starting: "The application is starting up! Just a few more seconds." -info_compose_is_running: "Docker Compose is running." -info_compose_not_running: "Start Docker Compose from the environment tab." -info_compose_starting: "Docker Compose is starting. This can take a while the first time." -info_compose_error: "Uh oh! Docker Compose had an error. Please check the logs." -info_wait_for_package: "Waiting for you to configure the necessary package." -info_wait_for_file: "Waiting for you to create the requested file." - -# custom testing messages -# if you add manual tests, you can add your own messages here -info_test_test: "This is a test!" diff --git a/code/tutorial_app/pages_templates/template_tests.py.envsub b/code/tutorial_app/pages_templates/template_tests.py.envsub deleted file mode 100644 index 35afd8b..0000000 --- a/code/tutorial_app/pages_templates/template_tests.py.envsub +++ /dev/null @@ -1,45 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Tests for auto continuing associated tasks.""" - -import sys - -from .editor_test import EDITOR_DIR -from live_labs.testing import TestFail, isolate - - -def sample_test(): - """Test a project using built in helpers.""" - # The testing module contains some test compatible helpers. - # These helpers can either get an object or ensure the state of an object. - # If there is an error in these helpers, they will raise TestFail automatically. - raise testing.TestFail("info_test_test") - - -@isolate(EDITOR_DIR) -def isolated_test(): - """Run test in another Python process.""" - import sys - - sys.stdout.write("info_isolated_test") - sys.exit(1) - - -if __name__ == "__main__": - sys.stdout.write("---------------\n") - # you can use this space for testing while you are - # developing your tests - sample_test() - isolated_test() diff --git a/code/tutorial_app/static/hero.png b/code/tutorial_app/static/hero.png deleted file mode 100644 index 77981e2..0000000 Binary files a/code/tutorial_app/static/hero.png and /dev/null differ diff --git a/code/tutorial_app/static/robots/assembly.png b/code/tutorial_app/static/robots/assembly.png new file mode 100644 index 0000000..4f9081a Binary files /dev/null and b/code/tutorial_app/static/robots/assembly.png differ diff --git a/code/tutorial_app/static/robots/hero.png b/code/tutorial_app/static/robots/hero.png new file mode 100644 index 0000000..a1546b5 Binary files /dev/null and b/code/tutorial_app/static/robots/hero.png differ diff --git a/code/tutorial_app/static/robots/strong.png b/code/tutorial_app/static/robots/strong.png new file mode 100644 index 0000000..03d3cd3 Binary files /dev/null and b/code/tutorial_app/static/robots/strong.png differ diff --git a/code/tutorial_app/static/robots/wave.png b/code/tutorial_app/static/robots/wave.png new file mode 100644 index 0000000..8ed22b5 Binary files /dev/null and b/code/tutorial_app/static/robots/wave.png differ diff --git a/code/tutorial_app/tutorial_app.code-workspace b/code/tutorial_app/tutorial_app.code-workspace index f00adf4..035d628 100644 --- a/code/tutorial_app/tutorial_app.code-workspace +++ b/code/tutorial_app/tutorial_app.code-workspace @@ -5,13 +5,9 @@ } ], "settings": { - "pylint.importStrategy": "useBundled", - "python.analysis.extraPaths": ["/project/libs/live-labs"], - // file explorer configuration - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, + // file explorer configuration + "files.exclude": { + ".git": true, "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true, @@ -19,64 +15,70 @@ "**/__pycache__": true, "**/.mypy_cache": true, "**/.ipynb_checkpoints": true, - "**/.terraform": true, - }, - + ".project": true, + ".vscode": true, + ".github": true, + "**/.ruff_cache": true, + }, - // global editor settings - "files.eol": "\n", - "editor.tabSize": 4, - "editor.insertSpaces": true, - "files.insertFinalNewline": true, - // remove this line to automatically forward ports - // in general, workbench will manage this already - "remote.autoForwardPorts": false, + // global editor settings + "files.eol": "\n", + "editor.tabSize": 4, + "editor.insertSpaces": true, + "files.insertFinalNewline": true, + "remote.autoForwardPorts": false, - - // bash scripting configuration - "[shellscript]": { + // bash scripting configuration + "[shellscript]": { "editor.tabSize": 4, - "editor.insertSpaces": false, - }, - + "editor.insertSpaces": false + }, - // css style sheet configuration - "[css]": { + // css style sheet configuration + "[css]": { "editor.suggest.insertMode": "replace", "editor.tabSize": 2 - }, + }, - // js configuration - "[javascript]": { + // js configuration + "[javascript]": { "editor.maxTokenizationLineLength": 2500, - "editor.tabSize": 2, - }, + "editor.tabSize": 2 + }, + + // Python environment configuration + "python.terminal.activateEnvironment": true, + // "python.defaultInterpreterPath": "/usr/bin/python3", - // Python environment configuration - "python.terminal.activateEnvironment": true, - "python.defaultInterpreterPath": ".venv/bin/python", - "isort.args":["--profile", "black"], - "isort.check": true, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + // Ruff as formatter + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.organizeImports": "explicit", + "source.fixAll": "explicit" }, - // Comment out this settings to disable auto-formatting "editor.formatOnSave": true - }, + }, + "ruff.enable": true, + "ruff.exclude": [], - "black-formatter.args": [ - "--line-length", - "120" - ], - "pylint.severity": { - "refactor": "Information", - }, - "explorer.fileNesting.enabled": true, - "explorer.fileNesting.patterns": { + "mypy-type-checker.ignorePatterns": ["**/answers/*"], + + // Virtual environment specific + "python.analysis.extraPaths": ["/project/libs/live-labs"], + "python.defaultInterpreterPath": "/opt/live-labs/bin/python", + "ruff.configuration": "../../pyproject.toml", + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { "*.py": "${capture}.*.yaml, ${capture}_tests.py" - } + }, + "python.analysis.exclude": [ + "answers/**", + ], + "workbench.colorCustomizations": { + "titleBar.activeBackground": "#2fe274" + }, + "window.title": "code/tutorial_app ${dirty}${activeEditorShort}${separator}${rootName}${separator}${profileName}${separator}${appName}" } } diff --git a/code/upload-pdfs.ipynb b/code/upload-pdfs.ipynb index 0c36ef1..5b6980c 100644 --- a/code/upload-pdfs.ipynb +++ b/code/upload-pdfs.ipynb @@ -41,9 +41,7 @@ "cell_type": "code", "execution_count": null, "id": "114728f1-6ee9-4851-9042-32248fa39318", - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "!unzip -n ../data/corp-comms-dataset.zip -d ../data/" diff --git a/docs/.puppeteer.json b/docs/.puppeteer.json deleted file mode 100644 index 2c60f5e..0000000 --- a/docs/.puppeteer.json +++ /dev/null @@ -1 +0,0 @@ -{ "args": ["--no-sandbox"] } diff --git a/docs/0_0_quick_start.md b/docs/0_0_quick_start.md deleted file mode 100644 index 95ad18d..0000000 --- a/docs/0_0_quick_start.md +++ /dev/null @@ -1 +0,0 @@ -# Quick-start diff --git a/docs/0_1_personal_key.md b/docs/0_1_personal_key.md deleted file mode 100644 index b9e9706..0000000 --- a/docs/0_1_personal_key.md +++ /dev/null @@ -1,26 +0,0 @@ -## Generate your NGC Personal Key - -To allow AI Workbench to access NVIDIA’s cloud resources, you’ll need to provide it with a Personal Key. These keys begin with `nvapi-`. - -
- -Expand this section for instructions for creating this key. - - -1. Go to the [NGC Personal Key Manager](https://org.ngc.nvidia.com/setup/personal-keys). If you are prompted to, then register for a new account and sign in. - - > **HINT** You can find this tool by logging into [ngc.nvidia.com](https://ngc.nvidia.com), expanding your profile menu on the top right, selecting *Setup*, and then selecting *Generate Personal Key*. - -1. Select *Generate Personal Key*. - - ![Generate Personal Key](_static/generate_personal_key.png) - -1. Enter any value as the Key name, an expiration of 12 months is fine, and select all the services. Press *Generate Personal Key* when you are finished. - - ![Personal Key Form](_static/personal_key_form.png) - -1. Save your personal key for later. Workbench will need it and there is no way to retrieve it later. If the key is lost, a new one must be created. Protect this key as if it were a password. - - ![Personal Key](_static/personal_key.png) - -
diff --git a/docs/0_2_docker_auth.md b/docs/0_2_docker_auth.md deleted file mode 100644 index 89e8671..0000000 --- a/docs/0_2_docker_auth.md +++ /dev/null @@ -1,14 +0,0 @@ -## Authenticate with Docker - -Workbench will use your system's Docker client to pull NVIDIA NIM containers, so before continuing, make sure to follow these steps to authenticate your Docker client with your NGC Personal Key. - -1. Run the following Docker login command - - ```bash - docker login nvcr.io - ``` - -1. When prompted for your credentials, use the following values: - - - Username: `$oauthtoken` - - Password: Use your NGC Personal key beggining with `nv-api` diff --git a/docs/0_3_0_install_nvwb.md b/docs/0_3_0_install_nvwb.md deleted file mode 100644 index 7cf5cc0..0000000 --- a/docs/0_3_0_install_nvwb.md +++ /dev/null @@ -1,19 +0,0 @@ -## Install AI Workbench - -This project is designed to be used with [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/). While this is not a requirement, running this demo without AI Workbench will require manual work as the pre-configured automation and integrations may not be available. - -This quick start guide will assume a remote lab machine is being used for development and the local machine is a thin-client for remotely accessing the development machine. This allows for compute resources to stay centrally located and for developers to be more portable. Note, the remote lab machine must run Ubuntu, but the local client can run Windows, MacOS, or Ubuntu. To install this project local only, simply skip the remote install. - -```mermaid -flowchart LR - local - subgraph lab environment - remote-lab-machine - end - - local <-.ssh.-> remote-lab-machine -``` - -### Client Machine Install - -Ubuntu is required if the local client will also be used for developent. When using a remote lab machine, this can be Windows, MacOS, or Ubuntu. diff --git a/docs/0_3_1_windows.md b/docs/0_3_1_windows.md deleted file mode 100644 index bda2f55..0000000 --- a/docs/0_3_1_windows.md +++ /dev/null @@ -1,23 +0,0 @@ -
- -Expand this section for a Windows install. - - -For full instructions, see the [NVIDIA AI Workbench User Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/windows.html). - -1. Install Prerequisite Software - 1. If this machine has an NVIDIA GPU, ensure the GPU drivers are installed. It is recommended to use the [GeForce Experience](https://www.nvidia.com/en-us/geforce/geforce-experience/) tooling to manage the GPU drivers. - 1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) for local container support. Please be mindful of Docker Desktop's licensing for enterprise use. [Rancher Desktop](https://rancherdesktop.io/) may be a viable alternative. - 1. *[OPTIONAL]* If Visual Studio Code integration is desired, install [Visual Studio Code](https://code.visualstudio.com/). - -1. Download the [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) installer and execute it. Authorize Windows to allow the installer to make changes. - -1. Follow the instructions in the installation wizard. If you need to install WSL2, authorize Windows to make the changes and reboot local machine when requested. When the system restarts, the NVIDIA AI Workbench installer should automatically resume. - -1. Select Docker as your container runtime. - -1. Log into your GitHub Account by using the *Sign in through GitHub.com* option. - -1. Enter your git author information if requested. - -
diff --git a/docs/0_3_2_macos.md b/docs/0_3_2_macos.md deleted file mode 100644 index 5f552cf..0000000 --- a/docs/0_3_2_macos.md +++ /dev/null @@ -1,23 +0,0 @@ -
- -Expand this section for a MacOS install. - - -For full instructions, see the [NVIDIA AI Workbench User Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/macos.html). - -1. Install Prerequisite Software - 1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) for local container support. Please be mindful of Docker Desktop's licensing for enterprise use. [Rancher Desktop](https://rancherdesktop.io/) may be a viable alternative. - 1. *[OPTIONAL]* If Visual Studio Code integration is desired, install [Visual Studio Code](https://code.visualstudio.com/). When using VSCode on a Mac, an a[dditional step must be performed](https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line) to install the VSCode CLI interface used by Workbench. - -1. Download the [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) disk image (*.dmg* file) and open it. - -1. Drag AI Workbench into the Applications folder and run *NVIDIA AI Workbench* from the application launcher. - ![Mac DMG Install Interface](_static/mac_dmg_drag.png) - -1. Select Docker as your container runtime. - -1. Log into your GitHub Account by using the *Sign in through GitHub.com* option. - -1. Enter your git author information if requested. - -
diff --git a/docs/0_3_3_ubuntu.md b/docs/0_3_3_ubuntu.md deleted file mode 100644 index a7dc069..0000000 --- a/docs/0_3_3_ubuntu.md +++ /dev/null @@ -1,25 +0,0 @@ -
- -Expand this section for an Ubuntu install. - - -For full instructions, see the [NVIDIA AI Workbench User Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/ubuntu-local.html). Run this installation as the user who will be user Workbench. Do not run these steps as `root`. - -1. Install Prerequisite Software - 1. *[OPTIONAL]* If Visual Studio Code integration is desired, install [Visual Studio Code](https://code.visualstudio.com/). - -1. Download the [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/) installer, make it executable, and then run it. You can make the file executable with the following command: - - ```bash - chmod +x NVIDIA-AI-Workbench-*.AppImage - ``` - -1. AI Workbench will install the NVIDIA drivers for you (if needed). You will need to reboot your local machine after the drivers are installed and then restart the AI Workbench installation by double-clicking the NVIDIA AI Workbench icon on your desktop. - -1. Select Docker as your container runtime. - -1. Log into your GitHub Account by using the *Sign in through GitHub.com* option. - -1. Enter your git author information if requested. - -
diff --git a/docs/0_3_4_remote_ubuntu.md b/docs/0_3_4_remote_ubuntu.md deleted file mode 100644 index 4b687ad..0000000 --- a/docs/0_3_4_remote_ubuntu.md +++ /dev/null @@ -1,55 +0,0 @@ - - -### Remote Machine Install - -Only Ubuntu is supported for remote machines. - -
- -Expand this section for a remote Ubuntu install. - - -For full instructions, see the [NVIDIA AI Workbench User Guide](https://docs.nvidia.com/ai-workbench/user-guide/latest/installation/ubuntu-remote.html). Run this installation as the user who will be using Workbench. Do not run these steps as `root`. - -1. Ensure SSH Key based authentication is enabled from the local machine to the remote machine. If this is not currently enabled, the following commands will enable this is most situations. Change `REMOTE_USER` and `REMOTE-MACHINE` to reflect your remote address. - - - From a Windows local client, use the following PowerShell: - ```powershell - ssh-keygen -f "C:\Users\local-user\.ssh\id_rsa" -t rsa -N '""' - type $env:USERPROFILE\.ssh\id_rsa.pub | ssh REMOTE_USER@REMOTE-MACHINE "cat >> .ssh/authorized_keys" - ``` - - From a MacOS or Linux local client, use the following shell: - ```bash - if [ ! -e ~/.ssh/id_rsa ]; then ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ""; fi - ssh-copy-id REMOTE_USER@REMOTE-MACHINE - ``` - -1. SSH into the remote host. Then, use the following commands to download and execute the NVIDIA AI Workbench Installer. - - ```bash - mkdir -p $HOME/.nvwb/bin && \ - curl -L https://workbench.download.nvidia.com/stable/workbench-cli/$(curl -L -s https://workbench.download.nvidia.com/stable/workbench-cli/LATEST)/nvwb-cli-$(uname)-$(uname -m) --output $HOME/.nvwb/bin/nvwb-cli && \ - chmod +x $HOME/.nvwb/bin/nvwb-cli && \ - sudo -E $HOME/.nvwb/bin/nvwb-cli install - ``` - -1. AI Workbench will install the NVIDIA drivers for you (if needed). You will need to reboot your remote machine after the drivers are installed and then restart the AI Workbench installation by re-running the commands in the previous step. - -1. Select Docker as your container runtime. - -1. Log into your GitHub Account by using the *Sign in through GitHub.com* option. - -1. Enter your git author information if requested. - -1. Once the remote installation is complete, the Remote Location can be added to the local AI Workbench instance. Open the AI Workbench application, click *Add Remote Location*, and then enter the required information. When finished, click *Add Location*. - - - *Location Name: * Any short name for this new location - - *Description: * Any brief metadata for this location. - - *Hostname or IP Address: * The hostname or address used to remotely SSH. If step 1 was followed, this should be the same as `REMOTE-MACHINE`. - - *SSH Port: * Usually left blank. If a nonstandard SSH port is used, it can be configured here. - - *SSH Username: * The username used for making an SSH connection. If step 1 was followed, this should be the same as `REMOTE_USER`. - - *SSH Key File: * The path to the private key for making SSH connections. If step 1 was followed, this should be: `/home/USER/.ssh/id_rsa`. - - *Workbench Directory: * Usually left blank. This is where Workbench will remotely save state. - - -
diff --git a/docs/0_4_0_download.md b/docs/0_4_0_download.md deleted file mode 100644 index d93a7c4..0000000 --- a/docs/0_4_0_download.md +++ /dev/null @@ -1,31 +0,0 @@ -## Download this project - -There are two ways to download this project for local use: Cloning and Forking. - -Cloning this repository is the recommended way to start. This will not allow for local modifications, but is the fastest to get started. This also allows for the easiest way to pull updates. - -Forking this repository is recommended for development as changes will be able to be saved. However, to get updates, the fork maintainer will have to regularly pull from the upstream repo. To work from a fork, follow [GitHub's instructions](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and then reference the URL to your personal fork in the rest of this section. - -
- -Expand this section for a details on downloading this project. - - -1. Open the local NVIDIA AI Workbench window. From the list of locations displayed, select either the remote one you just set up, or local if you're going to work locally. - - ![AI Workbench Locations Menu](_static/nvwb_locations.png) - -1. Once inside the location, select *Clone Project*. - - ![AI Workbench Projects Menu](_static/nvwb_projects.png) - -1. In the 'Clone Project' pop up window, set the Repository URL to `https://github.com/NVIDIA/nim-anywhere.git`. You can leave the Path as the default of `/home/REMOTE_USER/nvidia-workbench/nim-anywhere.git`. Click *Clone*.` - - ![AI Workbench Clone Project Menu](_static/nvwb_clone.png) - -1. You will be redirected to the new project’s page. Workbench will automatically bootstrap the development environment. You can view real-time progress by expanding the Output from the bottom of the window. - - ![AI Workbench Log Viewer](_static/nvwb_logs.png) - -
- diff --git a/docs/0_4_1_configure.md b/docs/0_4_1_configure.md deleted file mode 100644 index f7dba40..0000000 --- a/docs/0_4_1_configure.md +++ /dev/null @@ -1,15 +0,0 @@ -## Configure this project -The project must be configured to use your NGC personal key. - -
- -Expand this section for a details on configuring this project. - - -1. Before running for the first time, your NGC personal key must be configured in Workbench. This is done using the *Environment* tab from the left-hand panel. - - ![AI Workbench Side Menu](_static/nvwb_left_menu.png) - -1. Scroll down to the **Secrets** section and find the *NGC_API_KEY* entry. Press *Configure* and provide the personal key for NGC that was generated earlier. - -
diff --git a/docs/0_5_start.md b/docs/0_5_start.md deleted file mode 100644 index b52cc71..0000000 --- a/docs/0_5_start.md +++ /dev/null @@ -1,51 +0,0 @@ -## Start This Project - -Even the most basic of LLM Chains depend on a few additional microservices. These can be ignored during development for in-memory alternatives, but then code changes are required to go to production. Thankfully, Workbench manages those additional microservices for development environments. - -
- -Expand this section for details on starting the demo application. - - -> **HINT:** For each application, the debug output can be monitored in the UI by clicking the Output link in the lower left corner, selecting the dropdown menu, and choosing the application of interest (or **Compose** for applications started via compose). - -Since you can either pull NIMs and run them locally, or utilize the endpoints from *ai.nvidia.com* you can run this project with *or* without GPUs. - -1. The applications bundled in this workspace can be controlled by navigating to two tabs: - - - **Environment** > **Compose** - - **Environment** > **Applications** - -1. First, navigate to the **Environment** > **Compose** tab. If you're not working in an environment with GPUs, you can just click **Start** to run the project using a lightweight deployment. This default configuration will run the following containers: - - - *Milvus Vector DB*: An unstructured knowledge base - - - *Redis*: Used to store conversation histories - -1. If you have access to GPU resources and want to run any NIMs locally, use the dropdown menu under **Compose** and select which set of NIMs you want to run locally. Note that you *must* have at least 1 available GPU per NIM you plan to run locally. Below is an outline of the available configurations: - - - Local LLM (min 1 GPU required) - - The first time the LLM NIM is started, it will take some time to download the image and the optimized models. - - During a long start, to confirm the LLM NIM is starting, the progress can be observed by viewing the logs by using the *Output* pane on the bottom left of the UI. - - - If the logs indicate an authentication error, that means the provided *NGC_API_KEY* does not have access to the NIMs. Please verify it was generated correctly and in an NGC organization that has NVIDIA AI Enterprise support or trial. - - - If the logs appear to be stuck on `..........: Pull complete`. `..........: Verifying complete`, or `..........: Download complete`; this is all normal output from Docker that the various layers of the container image have been downloaded. - - - Any other failures here need to be addressed. - - Local LLM + Embedding (min 2 GPUs required) - - - Local LLM + Embedding + Reranking (min 3 GPUs required) - - - > **NOTE:** - > - Each profile will also run *Milvus Vector DB* and *Redis* - > - Due to the nature of Docker Compose profiles, the UI will let you select multiple profiles at the same time. In the context of this project, selecting multiple profiles does not make sense. It will not cause any errors, however we recommend only selecting one profile at a time for simplicity. - -1. Once the compose services have been started, navigate to the **Environment** > **Applications** tab. Now, the *Chain Server* can safely be started. This contains the custom LangChain code for performing our reasoning chain. By default, it will use the local Milvus and Redis, but use *ai.nvidia.com* for LLM, Embedding, and Reranking model inferencing. - -1. Once the *Chain Server* is up, the *Chat Frontend* can be started. Starting the interface will automatically open it in a browser window. If you are running any local NIMs, you can edit the config to connect to them via the *Chat Frontend* - - ![NIM Anywhere Frontend](_static/na_frontend.png) - -
diff --git a/docs/0_6_knowledgebase.md b/docs/0_6_knowledgebase.md deleted file mode 100644 index 8b0edf9..0000000 --- a/docs/0_6_knowledgebase.md +++ /dev/null @@ -1,9 +0,0 @@ -## Populating the Knowledge Base - -To get started developing demos, a sample dataset is provided along with a Jupyter Notebook showing how data is ingested into a Vector Database. - - 1. To import PDF documentation into the vector Database, open Jupyter using the app launcher in AI Workbench. - - 1. Use the Jupyter Notebook at `code/upload-pdfs.ipynb` to ingest the default dataset. If using the default dataset, no changes are necessary. - - 1. If using a custom dataset, upload it to the `data/` directory in Jupyter and modify the provided notebook as necessary. diff --git a/docs/1_develop.md b/docs/1_develop.md deleted file mode 100644 index c21241e..0000000 --- a/docs/1_develop.md +++ /dev/null @@ -1,20 +0,0 @@ -# Developing Your Own Applications - -This project contains applications for a few demo services as well as integrations with external services. These are all orchestrated by [NVIDIA AI Workbench](https://www.nvidia.com/en-us/deep-learning-ai/solutions/data-science/workbench/). - -The demo services are all in the `code` folder. The root level of the code folder has a few interactive notebooks meant for technical deep dives. The Chain Server is a sample application utilizing NIMs with LangChain. (Note that the Chain Server here gives you the option to experiment with and without RAG). The Chat Frontend folder contains an interactive UI server for exercising the chain server. Finally, sample notebooks are provided in the Evaluation directory to demonstrate retrieval scoring and validation. - -``` mermaid -mindmap - root((AI Workbench)) - Demo Services - Chain Server
LangChain + NIMs - Frontend
Interactive Demo UI - Evaluation
Validate the results - Notebooks
Advanced usage - - Integrations - Redis
Conversation History - Milvus
Vector Database - LLM NIM
Optimized LLMs -``` diff --git a/docs/2_configuration.md.py b/docs/2_configuration.md.py deleted file mode 100755 index 40f26f7..0000000 --- a/docs/2_configuration.md.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 - -import jinja2 -from chain_server import configuration -from frontend import configuration as fe_configuration - - -def resolve_child(schema, pinfo): - ref = pinfo.get("$ref", "") - if ref.startswith("#/$defs"): - child = ref.split("/")[-1] - return schema["$defs"][child] - return None - - -def to_yaml(schema, level=0, env_var_prefixes=[("APP_", "__")]): - indent = " " * 4 * level - out = "" - - if schema["type"] == "object": - for prop_name, prop in schema["properties"].items(): - prop_type = prop.get("anyOf", [{"type": prop.get("type")}]) - prop_desc = prop.get("description", "") - prop_child = resolve_child(schema, prop) - prop_default = "" if prop_child else (prop.get("default") or "~") - - # print the property description - if prop_desc: - out += f"{indent}# {prop_desc}\n" - - # print the property environment variables - env_vars = prop.get("extra_env_vars", []) + [ - f"{prefix[0]}{prop_name.upper()}" for prefix in env_var_prefixes - ] - if not prop_child and env_vars: - out += f"{indent}# ENV Variables: " - out += ", ".join(env_vars) - out += "\n" - - # print variable type - if prop_type[0].get("type"): - out += f"{indent}# Type: " - out += ", ".join([t["type"] for t in prop_type]) - out += "\n" - - # print out the property - out += f"{indent}{prop_name}: {prop_default}\n" - - # if the property references a child, print the child - if prop_child: - new_env_var_prefixes = [ - (f"{prefix[0]}{prop_name.upper()}{prefix[1]}", prefix[1]) for prefix in env_var_prefixes - ] - out += to_yaml(prop_child, level=level + 1, env_var_prefixes=new_env_var_prefixes) - - out += "\n" - - return out - - -environment = jinja2.Environment(loader=jinja2.BaseLoader, autoescape=True) -environment.filters["to_yaml"] = to_yaml - -doc_page = environment.from_string( - """ - -# {{docstring}} - -## Chain Server config schema - -```yaml -{{ cs_schema | to_yaml }} -``` - -## Chat Frontend config schema - -The chat frontend has a few configuration options as well. They can be set in the same manner as the chain server. - -```yaml -{{ fe_schema | to_yaml }} -``` - -""" -) - -env_var_prefixes = [ - (source.prefix, source.nested_separator) - for source in configuration.config.CONFIG_SOURCES - if hasattr(source, "prefix") -] -docs = doc_page.render( - docstring=configuration.__doc__, - cs_schema=configuration.config.model_json_schema(), - fe_schema=fe_configuration.config.model_json_schema(), -) -print(docs) diff --git a/docs/3.0_contributing.md b/docs/3.0_contributing.md deleted file mode 100644 index 37ed02a..0000000 --- a/docs/3.0_contributing.md +++ /dev/null @@ -1,18 +0,0 @@ -# Contributing - -All feedback and contributions to this project are welcome. When making changes to this project, either for personal use or for contributing, it is recommended to work on a fork on this project. Once the changes have been completed on the fork, a Merge Request should be opened. - -## Code Style - -This project has been configured with Linters that have been tuned to help the code remain consistent while not being overly burdensome. We use the following Linters: - -- Bandit is used for security scanning -- Pylint is used for Python Syntax Linting -- MyPy is used for type hint linting -- Black is configured for code styling -- A custom check is run to ensure Jupyter Notebooks do not have any output -- Another custom check is run to ensure the README.md file is up to date - -The embedded VSCode environment is configured to run the linting and checking in realtime. - -To manually run the linting that is done by the CI pipelines, execute `/project/code/tools/lint.sh`. Individual tests can be run be specifying them by name: `/project code/tools/lint.sh [deps|pylint|mypy|black|docs|fix]`. Running the lint tool in fix mode will automatically correct what it can by running Black, updating the README, and clearing the cell output on all Jupyter Notebooks. diff --git a/docs/3.1_frontend.md b/docs/3.1_frontend.md deleted file mode 100644 index 9d15d8a..0000000 --- a/docs/3.1_frontend.md +++ /dev/null @@ -1,107 +0,0 @@ -## Updating the frontend - -The frontend has been designed in an effort to minimize the required HTML and Javascript development. A branded and styled Application Shell is provided that has been created with vanilla HTML, Javascript, and CSS. It is designed to be easy to customize, but it should never be required. The interactive components of the frontend are all created in Gradio and mounted in the app shell using iframes. - -Along the top of the app shell is a menu listing the available views. Each view may have its own layout consisting of one or a few pages. - -### Creating a new page - -Pages contain the interactive components for a demo. The code for the pages is in the `code/frontend/pages` directory. To create a new page: - -1. Create a new folder in the pages directory -1. Create an `__init__.py` file in the new directory that uses Gradio to define the UI. The Gradio Blocks layout should be defined in a variable called `page`. -1. It is recommended that any CSS and JS files needed for this view be saved in the same directory. See the `chat` page for an example. -1. Open the `code/frontend/pages/__init__.py` file, import the new page, and add the new page to the `__all__` list. - -> **NOTE:** Creating a new page will not add it to the frontend. It must be added to a view to appear on the Frontend. - -### Adding a view - -View consist of one or a few pages and should function independently of each other. Views are all defined in the `code/frontend/server.py` module. All declared views will automatically be added to the Frontend's menu bar and made available in the UI. - -To define a new view, modify the list named `views`. This is a list of `View` objects. The order of the objects will define their order in the Frontend menu. The first defined view will be the default. - -View objects describe the view name and layout. They can be declared as follow: -```python -my_view = frontend.view.View( - name="My New View", # the name in the menu - left=frontend.pages.sample_page, # the page to show on the left - right=frontend.pages.another_page, # the page to show on the right -) -``` - -All of the page declarations, `View.left` or `View.right`, are optional. If they are not declared, then the associated iframes in the web layout will be hidden. The other iframes will expand to fill the gaps. The following diagrams show the various layouts. - - - All pages are defined - -```mermaid -block-beta - columns 1 - menu["menu bar"] - block - columns 2 - left right - end -``` - - - Only left is defined - -```mermaid -block-beta - columns 1 - menu["menu bar"] - block - columns 1 - left:1 - end -``` - -### Frontend branding - -The frontend contains a few branded assets that can be customized for different use cases. - -#### Logo - -The frontend contains a logo on the top left of the page. To modify the logo, an SVG of the desired logo is required. The app shell can then be easily modified to use the new SVG by modifying the `code/frontend/_assets/index.html` file. There is a single `div` with an ID of `logo`. This box contains a single SVG. Update this to the desired SVG definition. - -```html - -``` - -#### Color scheme - -The styling of the App Shell is defined in `code/frontend/_static/css/style.css`. The colors in this file may be safely modified. - -The styling of the various pages are defined in `code/frontend/pages/*/*.css`. These files may also require modification for custom color schemes. - -#### Gradio theme - -The Gradio theme is defined in the file `code/frontend/_assets/theme.json`. The colors in this file can safely be modified to the desired branding. Other styles in this file may also be changed, but may cause breaking changes to the frontend. The [Gradio documentation](https://www.gradio.app/guides/theming-guide) contains more information on Gradio theming. - -### Messaging between pages - -> **NOTE:** This is an advanced topic that most developers will never require. - -Occasionally, it may be necessary to have multiple pages in a view that communicate with each other. For this purpose, Javascript's `postMessage` messaging framework is used. Any trusted message posted to the application shell will be forwarded to each iframe where the pages can handle the message as desired. The `control` page uses this feature to modify the configuration of the `chat` page. - -The following will post a message to the app shell (`window.top`). The message will contain a dictionary with the key `use_kb` and a value of true. Using Gradio, this Javascript can be executed by [any Gradio event](https://www.gradio.app/guides/custom-CSS-and-JS#adding-custom-java-script-to-your-demo). - -```javascript -window.top.postMessage({"use_kb": true}, '*'); -``` - -This message will automatically be sent to all pages by the app shell. The following sample code will consume the message on another page. This code will run asynchronously when a `message` event is received. If the message is trusted, a Gradio component with the `elem_id` of `use_kb` will be updated to the value specified in the message. In this way, the value of a Gradio component can be duplicated across pages. - -```javascript -window.addEventListener( - "message", - (event) => { - if (event.isTrusted) { - use_kb = gradio_config.components.find((element) => element.props.elem_id == "use_kb"); - use_kb.props.value = event.data["use_kb"]; - }; - }, - false); -``` diff --git a/docs/3.2_docs.md b/docs/3.2_docs.md deleted file mode 100644 index 4edffc1..0000000 --- a/docs/3.2_docs.md +++ /dev/null @@ -1,30 +0,0 @@ - -## Updating documentation - -The README is rendered automatically; direct edits will be overwritten. In order to modify the README you will need to edit the files for each section separately. All of these files will be combined and the README will be automatically generated. You can find all of the related files in the `docs` folder. - -Documentation is written in Github Flavored Markdown and then rendered to a final Markdown file by Pandoc. The details for this process are defined in the Makefile. The order of files generated are defined in `docs/_TOC.md`. The documentation can be previewed in the Workbench file browser window. - -### Header file - -The header file is the first file used to compile the documentation. This file can be found at `docs/_HEADER.md`. The contents of this file will be written verbatim, without any manipulation, to the README before anything else. - -### Summary file - -The summary file contains quick description and graphic that describe this project. The contents of this file will be added to the README immediately after the header and just before the table of contents. This file is processed by Pandoc to embed images before writing to the README. - -### Table of Contents file - -The most important file for the documentation is the table of contents file at `docs/_TOC.md`. This file defines a list of files that should be concatenated in order to generate the final README manual. Files must be on this list to be included. - -### Static Content - -Save all static content, including images, to the `_static` folder. This will help with organization. - -### Dynamic documentation - -It may be helpful to have documents that update and write themselves. To create a dynamic document, simply create an executable file that writes the Markdown formatted document to stdout. During build time, if an entry in the table of contents file is executable, it will be executed and its stdout will be used in its place. - -### Rendering documentation - -When a documentation related commit is pushed, a GitHub Action will render the documentation. Any changes to the README will be automatially committed. \ No newline at end of file diff --git a/docs/3.3_environment.md b/docs/3.3_environment.md deleted file mode 100644 index af5b497..0000000 --- a/docs/3.3_environment.md +++ /dev/null @@ -1,13 +0,0 @@ -# Managing your Development Environment - -## Environment Variables - -Most of the configuration for the development environment happens with Environment Variables. To make permanent changes to environment variables, modify [`variables.env`](./variables.env) or use the Workbench UI. - -## Python Environment Packages - -This project uses one Python environment at `/usr/bin/python3` and dependencies are managed with `pip`. Because all development is done inside a container, any changes to the Python environment will be ephemeral. To permanently install a Python package, add it to the [`requirements.txt`](./requirements.txt) file or use the Workbench UI. - -## Operating System Configuration - -The development environment is based on Ubuntu 22.04. The primary user has password-less sudo access, but all changes to the system will be ephemeral. To make permanent changes to installed packages, add them to the [`apt.txt`] file. To make other changes to the operating system such as manipulating files, adding environment variables, etc; use the [`postBuild.bash`](./postBuild.bash) and [`preBuild.bash`](./preBuild.bash) files. diff --git a/docs/3.4_bumping_dependencies.md b/docs/3.4_bumping_dependencies.md deleted file mode 100644 index 3c3da55..0000000 --- a/docs/3.4_bumping_dependencies.md +++ /dev/null @@ -1,11 +0,0 @@ -## Updating Dependencies - -It is typically good practice to update dependencies monthly to ensure no CVEs are exposed through misused dependencies. The following process can be used to patch this project. It is recommended to run the regression testing after the patch to ensure nothing has broken in the update. - -1. **Update Environment:** In the workbench GUI, open the project and navigate to the Environment pane. Check if there is an update available for the base image. If an updated base image is available, apply the update and rebuild the environment. Address any build errors. Ensure that all of the applications can start. -1. **Update Python Packages and NIMs:** The Python dependencies and NIM applications can be updated automatically by running the `/project/code/tools/bump.sh` script. -1. **Update Remaining applications:** For the remaining applications, manually check their default tag and compare to the latest. Update where appropriate and ensure that the applications still start up successfully. -1. **Restart and rebuild the environment.** -1. **Audit Python Environment:** It is now best to check the installed versions of ALL Python packages, not just the direct dependencies. To accomplish this, run `/project/code/tools/audit.sh`. This script will print out a report of all Python packages in a warning state and all packages in an error state. Anything in an error state must be resolved as it will have active CVEs and known vulnerabilities. -1. **Check Dependabot Alerts:** Check all of the [Dependabot](https://github.com/NVIDIA/nim-anywhere/security/dependabot) alerts and ensure they should be resolved. -1. **Regression testing:** Run through the entire demo, from document ingesting to the frontend, and ensure it is still functional and that the GUI looks correct. diff --git a/docs/9_footer.md b/docs/9_footer.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index bcdeacc..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specifi -SHELL=/bin/bash -PANDOC_HEADER_OPTS =--from gfm --to gfm --extract-media=.static --embed-resources --standalone -PANDOC_OPTS =--from gfm --to gfm --standalone --toc --toc-depth=2 --extract-media=.static --embed-resources --standalone - -HEADER_FILE=_HEADER.md -SUMMARY_FILE=_SUMMARY.md -TOC_FILE=_TOC.md - -export PYTHONPATH := $(PYTHONPATH):../code - -../README.md: *.md* ../code/*/configuration.py - make --silent _render > ../README.md - rm -rf ../.static; mv .static ..; - - -.PHONY: _render _markdown -.SILENT: _render _markdown -.ONESHELL: _markdown -_render: export MERMAID_FILTER_FORMAT=png -_render: export MERMAID_FILTER_WIDTH=800 -_render: - cat $(HEADER_FILE); echo " " - pandoc $(PANDOC_HEADER_OPTS) $(SUMMARY_FILE); echo " " - set -eo pipefail; $(MAKE) --silent _markdown | pandoc $(PANDOC_OPTS) -_markdown: - for page in $$(cat $(TOC_FILE)); do - if [[ $$page == *.py ]]; then - ./$$page 2> /dev/null || cat $$page || exit 1; - else - cat $$page || exit 1; - fi - echo " " - done - - -.PHONY: clean all -clean: - rm -rf ../README.md ../.static -all: clean ../README.md diff --git a/docs/_HEADER.md b/docs/_HEADER.md deleted file mode 100644 index a79507c..0000000 --- a/docs/_HEADER.md +++ /dev/null @@ -1,9 +0,0 @@ -# NVIDIA NIM Anywhere [![Clone Me with AI Workbench](https://img.shields.io/badge/Open_In-AI_Workbench-76B900)](https://ngc.nvidia.com/open-ai-workbench/aHR0cHM6Ly9naXRodWIuY29tL05WSURJQS9uaW0tYW55d2hlcmUK) - -[![NVIDIA: LLM NIM](https://img.shields.io/badge/NVIDIA-LLM%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#large-language-models) -[![NVIDIA: Embedding NIM](https://img.shields.io/badge/NVIDIA-Embedding%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#nemo-retriever) -[![NVIDIA: Reranker NIM](https://img.shields.io/badge/NVIDIA-Reranker%20NIM-green?logo=nvidia&logoColor=white&color=%2376B900)](https://docs.nvidia.com/nim/#nemo-retriever) -[![CI Pipeline Status](https://github.com/nvidia/nim-anywhere/actions/workflows/ci.yml/badge.svg?query=branch%3Amain)](https://github.com/NVIDIA/nim-anywhere/actions/workflows/ci.yml?query=branch%3Amain) -![Python: 3.10 | 3.11 | 3.12](https://img.shields.io/badge/Python-3.10%20|%203.11%20|%203.12-yellow?logo=python&logoColor=white&color=%23ffde57) - - diff --git a/docs/_SUMMARY.md b/docs/_SUMMARY.md deleted file mode 100644 index 0c83b1b..0000000 --- a/docs/_SUMMARY.md +++ /dev/null @@ -1,13 +0,0 @@ -Please join #cdd-nim-anywhere slack channel if you are a internal user, open an issue if you are external for any question and feedback. - -One of the primary benefit of using AI for Enterprises is their ability to work with and learn from their internal data. Retrieval-Augmented Generation ([RAG](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/)) is one of the best ways to do so. NVIDIA has developed a set of micro-services called [NIM micro-service](https://docs.nvidia.com/nim/large-language-models/latest/introduction.html) to help our partners and customers build effective RAG pipeline with ease. - -NIM Anywhere contains all the tooling required to start integrating NIMs for RAG. It natively scales out to full-sized labs and up to production environments. This is great news for building a RAG architecture and easily adding NIMs as needed. If you're unfamiliar with RAG, it dynamically retrieves relevant -external information during inference without modifying the model -itself. Imagine you're the tech lead of a company with a local database containing confidential, up-to-date information. You don’t want OpenAI to access your data, but you need the model to understand it to answer questions accurately. The solution is to connect your language model to the database and feed them with the information. - -To learn more about why RAG is an excellent solution for boosting the accuracy and reliability of your generative AI models, [read this blog](https://developer.nvidia.com/blog/enhancing-rag-applications-with-nvidia-nim/). - -Get started with NIM Anywhere now with the [quick-start](#quick-start) instructions and build your first RAG application using NIMs! - -![NIM Anywhere Screenshot](_static/nim-anywhere.png) diff --git a/docs/_TOC.md b/docs/_TOC.md deleted file mode 100644 index 2f64092..0000000 --- a/docs/_TOC.md +++ /dev/null @@ -1,20 +0,0 @@ -0_0_quick_start.md -0_1_personal_key.md -0_2_docker_auth.md -0_3_0_install_nvwb.md -0_3_1_windows.md -0_3_2_macos.md -0_3_3_ubuntu.md -0_3_4_remote_ubuntu.md -0_4_0_download.md -0_4_1_configure.md -0_5_start.md -0_6_knowledgebase.md -1_develop.md -2_configuration.md.py -3.0_contributing.md -3.1_frontend.md -3.2_docs.md -3.3_environment.md -3.4_bumping_dependencies.md -9_footer.md diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/_static/generate_personal_key.png b/docs/_static/generate_personal_key.png deleted file mode 100644 index 8786a22..0000000 Binary files a/docs/_static/generate_personal_key.png and /dev/null differ diff --git a/docs/_static/mac_dmg_drag.png b/docs/_static/mac_dmg_drag.png deleted file mode 100644 index ed4f494..0000000 Binary files a/docs/_static/mac_dmg_drag.png and /dev/null differ diff --git a/docs/_static/na_frontend.png b/docs/_static/na_frontend.png deleted file mode 100644 index bbf7dee..0000000 Binary files a/docs/_static/na_frontend.png and /dev/null differ diff --git a/docs/_static/nim-anywhere.png b/docs/_static/nim-anywhere.png deleted file mode 100644 index 77981e2..0000000 Binary files a/docs/_static/nim-anywhere.png and /dev/null differ diff --git a/docs/_static/nvwb_clone.png b/docs/_static/nvwb_clone.png deleted file mode 100644 index 29a8f2c..0000000 Binary files a/docs/_static/nvwb_clone.png and /dev/null differ diff --git a/docs/_static/nvwb_left_menu.png b/docs/_static/nvwb_left_menu.png deleted file mode 100644 index ecc5007..0000000 Binary files a/docs/_static/nvwb_left_menu.png and /dev/null differ diff --git a/docs/_static/nvwb_locations.png b/docs/_static/nvwb_locations.png deleted file mode 100644 index 957a3b4..0000000 Binary files a/docs/_static/nvwb_locations.png and /dev/null differ diff --git a/docs/_static/nvwb_logs.png b/docs/_static/nvwb_logs.png deleted file mode 100644 index ac033af..0000000 Binary files a/docs/_static/nvwb_logs.png and /dev/null differ diff --git a/docs/_static/nvwb_mount_nim.png b/docs/_static/nvwb_mount_nim.png deleted file mode 100644 index bfbe2e7..0000000 Binary files a/docs/_static/nvwb_mount_nim.png and /dev/null differ diff --git a/docs/_static/nvwb_mount_varrun.png b/docs/_static/nvwb_mount_varrun.png deleted file mode 100644 index 7eba8c8..0000000 Binary files a/docs/_static/nvwb_mount_varrun.png and /dev/null differ diff --git a/docs/_static/nvwb_projects.png b/docs/_static/nvwb_projects.png deleted file mode 100644 index 217935d..0000000 Binary files a/docs/_static/nvwb_projects.png and /dev/null differ diff --git a/docs/_static/personal_key.png b/docs/_static/personal_key.png deleted file mode 100644 index 87802a2..0000000 Binary files a/docs/_static/personal_key.png and /dev/null differ diff --git a/docs/_static/personal_key_form.png b/docs/_static/personal_key_form.png deleted file mode 100644 index a0c6eb0..0000000 Binary files a/docs/_static/personal_key_form.png and /dev/null differ diff --git a/docs/_static/screenshot.png b/docs/_static/screenshot.png deleted file mode 100644 index 0ef9c14..0000000 Binary files a/docs/_static/screenshot.png and /dev/null differ diff --git a/docs/.gitkeep b/libs/.gitkeep similarity index 100% rename from docs/.gitkeep rename to libs/.gitkeep diff --git a/libs/live-labs/Makefile b/libs/live-labs/Makefile index d8a8056..e9a39c9 100644 --- a/libs/live-labs/Makefile +++ b/libs/live-labs/Makefile @@ -27,3 +27,7 @@ clean: clean-venv: rm -rf .venv clean-all: clean clean-venv + +inject-into-tutorial: + sudo /opt/live-labs/bin/pip uninstall -y live-labs + sudo /opt/live-labs/bin/pip install -e /project/libs/live-labs/ diff --git a/libs/live-labs/live-labs.code-workspace b/libs/live-labs/live-labs.code-workspace index f00adf4..804c353 100644 --- a/libs/live-labs/live-labs.code-workspace +++ b/libs/live-labs/live-labs.code-workspace @@ -5,13 +5,9 @@ } ], "settings": { - "pylint.importStrategy": "useBundled", - "python.analysis.extraPaths": ["/project/libs/live-labs"], - // file explorer configuration - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, + // file explorer configuration + "files.exclude": { + ".git": true, "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true, @@ -19,64 +15,62 @@ "**/__pycache__": true, "**/.mypy_cache": true, "**/.ipynb_checkpoints": true, - "**/.terraform": true, - }, + ".project": true, + ".vscode": true, + ".github": true, + "**/.ruff_cache": true, + }, + // global editor settings + "files.eol": "\n", + "editor.tabSize": 4, + "editor.insertSpaces": true, + "files.insertFinalNewline": true, + "remote.autoForwardPorts": false, - // global editor settings - "files.eol": "\n", - "editor.tabSize": 4, - "editor.insertSpaces": true, - "files.insertFinalNewline": true, - // remove this line to automatically forward ports - // in general, workbench will manage this already - "remote.autoForwardPorts": false, - - - // bash scripting configuration - "[shellscript]": { + // bash scripting configuration + "[shellscript]": { "editor.tabSize": 4, - "editor.insertSpaces": false, - }, - + "editor.insertSpaces": false + }, - // css style sheet configuration - "[css]": { + // css style sheet configuration + "[css]": { "editor.suggest.insertMode": "replace", "editor.tabSize": 2 - }, + }, - // js configuration - "[javascript]": { + // js configuration + "[javascript]": { "editor.maxTokenizationLineLength": 2500, - "editor.tabSize": 2, - }, + "editor.tabSize": 2 + }, + + // Python environment configuration + "python.terminal.activateEnvironment": true, + // "python.defaultInterpreterPath": "/usr/bin/python3", - // Python environment configuration - "python.terminal.activateEnvironment": true, - "python.defaultInterpreterPath": ".venv/bin/python", - "isort.args":["--profile", "black"], - "isort.check": true, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + // Ruff as formatter + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.organizeImports": "explicit", + "source.fixAll": "explicit" }, - // Comment out this settings to disable auto-formatting "editor.formatOnSave": true - }, - - "black-formatter.args": [ - "--line-length", - "120" - ], - "pylint.severity": { - "refactor": "Information", - }, + }, + "ruff.enable": true, + "ruff.exclude": [], - "explorer.fileNesting.enabled": true, - "explorer.fileNesting.patterns": { + // Virtual environment specific + "python.defaultInterpreterPath": ".venv/bin/python", + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { "*.py": "${capture}.*.yaml, ${capture}_tests.py" - } + }, + "workbench.colorCustomizations": { + "titleBar.activeBackground": "#e0218a" + }, + "window.title": "lib/live_labs ${dirty}${activeEditorShort}${separator}${rootName}${separator}${profileName}${separator}${appName}" } } diff --git a/libs/live-labs/live_labs/__init__.py b/libs/live-labs/live_labs/__init__.py index 66fa975..791325d 100644 --- a/libs/live-labs/live_labs/__init__.py +++ b/libs/live-labs/live_labs/__init__.py @@ -16,33 +16,11 @@ The modules in here contain the basic building blocks of a live lab page. """ -import shutil -from pathlib import Path -import streamlit as st - -from .lab import DEFAULT_STATE_FILE, Worksheet +from . import testing +from .helpers import reset_all_progress, scroll_to +from .lab import Worksheet from .localization import MessageCatalog from .shell import AppShell -__all__ = ["AppShell", "MessageCatalog", "Worksheet", "reset_all_progress"] - - -def reset_all_progress(): - """Remove all files and reset cached progress.""" - # remove artifacts - for artifact in st.session_state.get("artifacts", []): - shutil.rmtree(artifact, ignore_errors=True) - - # remove the cached state - try: - Path(DEFAULT_STATE_FILE).unlink() - except FileNotFoundError: - pass - - # clear the state - keys = list(st.session_state.keys()) - for key in keys: - st.session_state.pop(key) - - st.rerun() +__all__ = ["AppShell", "MessageCatalog", "Worksheet", "reset_all_progress", "scroll_to", "testing"] diff --git a/libs/live-labs/live_labs/css/editor.css b/libs/live-labs/live_labs/css/editor.css index 2a7b26b..b74f6d0 100644 --- a/libs/live-labs/live_labs/css/editor.css +++ b/libs/live-labs/live_labs/css/editor.css @@ -28,50 +28,47 @@ In the editor column, the tabs are styled to be a bit easier on the eyes. [data-testid="stMainBlockContainer"] { height: 100%; - /* overflow: hidden; */ >[data-testid="stVerticalBlockBorderWrapper"] { height: 100%; - >div { + >[data-testid="stVerticalBlock"] { height: 100%; - >[data-testid="stVerticalBlock"] { + >[data-testid="stHorizontalBlock"] { height: 100%; - >[data-testid="stHorizontalBlock"] { - height: 100%; + overflow: hidden; - /* style the worksheet side */ - >[data-testid="stColumn"]:first-child { + /* style the worksheet side */ + >[data-testid="stColumn"]:first-child { + height: 100%; + >[data-testid="stVerticalBlockBorderWrapper"] { + height: 100%; height: 100%; - >[data-testid="stVerticalBlockBorderWrapper"] { + >[data-testid="stVerticalBlock"] { height: 100%; - >div { - height: 100%; - >[data-testid="stVerticalBlock"] { - height: 100%; - overflow: scroll; - margin-bottom: 1.5em; - } - } + overflow: scroll; + scroll-behavior: smooth; + margin-bottom: 1.5em; } } + } - /* style the editor side */ - >[data-testid="stColumn"]:last-child { - [data-testid="stTabs"]:first-of-type { + /* style the editor side */ + >[data-testid="stColumn"]:last-child { + [data-testid="stTabs"]:first-of-type { + p { + font-size: 18px; + } + [data-baseweb="tab"]:last-of-type { + color: #5d1682; + margin-left: auto; p { - font-size: 18px; - } - [data-baseweb="tab"]:last-of-type { - color: #5d1682; - margin-left: auto; - p { - font-weight: 900; - } + font-weight: 900; } } } - } + } } + } } diff --git a/libs/live-labs/live_labs/css/style.css b/libs/live-labs/live_labs/css/style.css index e9c77fe..9f770af 100644 --- a/libs/live-labs/live_labs/css/style.css +++ b/libs/live-labs/live_labs/css/style.css @@ -17,19 +17,63 @@ /* Custom css necessary when using the live labs app shell layout. */ +/* smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* customize fonts */ +p, ol, ul, dl { + font-size: 20px !important; +} + +/* custom details drop downs */ +details { + padding-left: 50px; + padding-bottom: 15px; +} +details > summary { + font-size: 20px; + font-style: italic; + font-weight: 100; + text-transform: uppercase; + list-style-type: 'β–Ί '; +} +details[open] > summary { + list-style-type: 'β–Ό '; +} +details > summary::marker { + color: #76b900; +} +details > summary::after { + content: "πŸ™‹"; + font-size: 32px; + position: absolute; + left: 0; +} + + /* limit image size */ .stMarkdown{ img { max-height: 500px; max-width: 500px; + width: 100%; } } /* remove padding at the end of the document */ [data-testid="stMainBlockContainer"] { - padding-bottom: 0px; + padding-bottom: 5px; } +/* style the sidebar collaplse button */ +[data-testid="stSidebarCollapseButton"] svg { + color: #76b900; + background: #eee; + border: black 0px solid; + border-radius: 5px; +} /* nvidia themed sidebar */ [data-testid="stSidebarContent"] { diff --git a/libs/live-labs/live_labs/editor.py b/libs/live-labs/live_labs/editor.py index 818cbbd..83c76e8 100644 --- a/libs/live-labs/live_labs/editor.py +++ b/libs/live-labs/live_labs/editor.py @@ -20,7 +20,10 @@ It can only be called once per page and should probably not be called outside of the Worksheet class. """ +import json +from collections.abc import Generator from pathlib import Path +from textwrap import dedent import streamlit as st from streamlit.delta_generator import DeltaGenerator @@ -28,6 +31,7 @@ from streamlit_javascript import st_javascript _JS_CODE = Path(__file__).parent.joinpath("js", "editor.js").read_text("UTF-8").strip() +_JS_SEND_KEYS_CODE = Path(__file__).parent.joinpath("js", "editor.send_keys.js").read_text("UTF-8").strip() _CSS = Path(__file__).parent.joinpath("css", "editor.css").read_text("UTF-8").strip() @@ -56,10 +60,6 @@ def _new_ace_ide(base_dir: Path, key: str, value: str) -> None: def st_editor(base_dir: Path, files: list[str], init_data: list[str]) -> DeltaGenerator: """Add an editor to the page.""" - with st.container(height=1, border=False): - st_javascript(_JS_CODE) - st.html(f"") - with st.container(): tab_names = [f":material/data_object: {fname}" for fname in files] + [":material/terminal: Terminal Output"] editor_tabs = st.tabs(tab_names) @@ -67,4 +67,32 @@ def st_editor(base_dir: Path, files: list[str], init_data: list[str]) -> DeltaGe with file_tab: _new_ace_ide(base_dir, files[file_idx], init_data[file_idx]) + with st.container(height=1, border=False): + st_javascript(_JS_CODE) + st.html(f"") + return editor_tabs[-1] + + +def _sanitize_text(text: str) -> Generator[str]: + """Internal generator for removing happy accidents from strings.""" + text = dedent(text) + + for idx, line in enumerate(text.splitlines()): + clean_line = line.rstrip() + if idx > 0 or line: + yield clean_line + + +def send_keys(text: str) -> bool: + """Write the text to the on screen editor.""" + if isinstance(text, bytes): + text = text.decode("UTF-8") + + text = "\n".join(_sanitize_text(text)) + + code = _JS_SEND_KEYS_CODE.replace("ARG", json.dumps(text)) + with st.container(height=1, border=False): + st_javascript(code) + + return True diff --git a/libs/live-labs/live_labs/helpers.py b/libs/live-labs/live_labs/helpers.py new file mode 100644 index 0000000..0356d56 --- /dev/null +++ b/libs/live-labs/live_labs/helpers.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Live Labs Helpers functions and constants.""" + +import json +import re +import shutil +from contextlib import suppress +from pathlib import Path + +import streamlit as st +from streamlit_javascript import st_javascript + +_JS_SCROLL_TO_CODE = Path(__file__).parent.joinpath("js", "helpers.scroll_to.js").read_text("UTF-8").strip() + +DEFAULT_STATE_FILE = Path("/project/data/scratch/tutorial_state.json") + + +def scroll_to(header_title: str): + """Scroll the document to the requested header.""" + anchor = slugify(header_title, "-") + code = _JS_SCROLL_TO_CODE.replace("ARG", json.dumps(anchor)) + with st.container(height=1, border=False): + st_javascript(code, key=anchor) + + +def slugify(text: str, space: str = "_") -> str: + """Convert text to a valid slug.""" + text = text.lower() + text = re.sub(r"[^a-z0-9\s]", "", text) + return re.sub(r"\s+", space, text) + + +def reset_all_progress(): + """Remove all files and reset cached progress.""" + # remove artifacts + for artifact in st.session_state.get("artifacts", []): + shutil.rmtree(artifact) + + # remove the cached state + with suppress(FileNotFoundError): + Path(DEFAULT_STATE_FILE).unlink() + + # clear the state + keys = list(st.session_state.keys()) + for key in keys: + st.session_state.pop(key) + + st.rerun() diff --git a/libs/live-labs/live_labs/js/editor.js b/libs/live-labs/live_labs/js/editor.js index 645796d..b3b9395 100644 --- a/libs/live-labs/live_labs/js/editor.js +++ b/libs/live-labs/live_labs/js/editor.js @@ -1,4 +1,4 @@ -function setupUpdateEditorHeight() { +function main() { // SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // @@ -14,23 +14,111 @@ function setupUpdateEditorHeight() { // See the License for the specific language governing permissions and // limitations under the License. - /* Frontend code for ensuring the editor stays full height. */ - function updateEditorHeight() { + /* initialize the editor */ + function init() { + + /* Find the ace editor object and dom element */ + const { editor, editorDiv } = function () { + // find the iframe with the ace editor + const frames = window.parent.frames; + for (let idx = 0; idx < frames.length; idx++) { + let editorDiv = frames[idx].document.getElementById("ace-editor"); + if (editorDiv) { + const editor = frames[idx].ace.edit("ace-editor"); + return {"editor": editor, "editorDiv": editorDiv} + } + } + return {"editor": null, "editorDiv": null} + }(); + + /* If there is no editor, wait and try again. */ + if ( ! editor ) { + setTimeout(init, 10); + return + } + + /* Helper function that will make the editor full height. */ + function updateEditorHeight() { + if (!editorDiv) { + return null; + } const stApp = window.parent.document.querySelector('div[data-testid="stApp"]'); const stMainBlock = window.parent.document.querySelector('div[data-testid="stMainBlockContainer"]'); const appHeight = stApp.clientHeight; const mainPaddingTop = parseInt(getComputedStyle(stMainBlock).paddingTop, 10) || 0; const editorHeight = appHeight - mainPaddingTop; + editorDiv.style.height = editorHeight - 150 + "px"; + } - const frames = window.parent.frames; - for (let idx = 0; idx < frames.length; idx++) { - let editor = frames[idx].document.getElementById("ace-editor"); - if (editor) { - editor.style.height = editorHeight - 150 + "px"; + // Resize the editor now and when the window is resized + window.parent.addEventListener('resize', updateEditorHeight); + updateEditorHeight(); + + // Helper function to pop the first word off of a string, helpful for simulated streaming + function wordPop(input) { + if (!input) { + return { word: '', newInput: '', last: true }; + } + + const firstChar = input[0]; + + // Newline special case + if (firstChar === '\n') { + return { word: '\n', newInput: input.slice(1), last: input.length === 1 }; + } + + // Group either spaces or non-spaces + const isSpace = firstChar === ' '; + let i = 0; + while (i < input.length && (input[i] === ' ') === isSpace) { + i++; + } + + const word = input.slice(0, i); + const newInput = input.slice(i); + return { word, newInput, last: newInput.length === 0 }; + } + + + // Helper function to simulate typing in the editor + async function editorSendKeys(input) { + if (!editor) return; + + const scrollbar = editor.container.getElementsByClassName("ace_scrollbar")[0]; + let newInput = input; + + while (newInput.length > 0) { + let { word, newInput: updatedInput, last } = wordPop(newInput); + + if (last) { + word += "\n"; + } + + editor.setValue(editor.getValue() + word); + scrollbar.scrollTop = scrollbar.scrollHeight; + editor.clearSelection(); + + newInput = updatedInput; + + if (!last) { + // sleep + await new Promise(resolve => setTimeout(resolve, 125)); } } + editor.setValue(editor.getValue() + word); + scrollbar.scrollTop = scrollbar.scrollHeight; + editor.clearSelection(); + } + + + // Save the helper function to the global scope + window.parent.editor = editor; + window.parent.editorSendKeys = editorSendKeys; } - window.addEventListener('resize', updateEditorHeight); - setTimeout(updateEditorHeight, 500); -}({}); + /* Start trying to initialize in the background. */ + setTimeout(init, 1); +}( {} ); + + + diff --git a/libs/live-labs/live_labs/js/editor.send_keys.js b/libs/live-labs/live_labs/js/editor.send_keys.js new file mode 100644 index 0000000..4741696 --- /dev/null +++ b/libs/live-labs/live_labs/js/editor.send_keys.js @@ -0,0 +1,22 @@ + async function main() { + // SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // SPDX-License-Identifier: Apache-2.0 + // + // Licensed under the Apache License, Version 2.0 (the "License"); + // you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // http://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software + // distributed under the License is distributed on an "AS IS" BASIS, + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + // See the License for the specific language governing permissions and + // limitations under the License. + + async function send() { + await window.parent.editorSendKeys(ARG); + } + + await send(); +}({}); diff --git a/libs/live-labs/live_labs/js/helpers.scroll_to.js b/libs/live-labs/live_labs/js/helpers.scroll_to.js new file mode 100644 index 0000000..a0d1d86 --- /dev/null +++ b/libs/live-labs/live_labs/js/helpers.scroll_to.js @@ -0,0 +1,70 @@ +await async function main() { + // SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // SPDX-License-Identifier: Apache-2.0 + // + // Licensed under the Apache License, Version 2.0 (the "License"); + // you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // http://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software + // distributed under the License is distributed on an "AS IS" BASIS, + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + // See the License for the specific language governing permissions and + // limitations under the License. + + // helper function to find the previous streamlit element + function findPreviousStElementContainer(elementId) { + const element = window.parent.document.getElementById(elementId); + if (!element) { + console.error(`Element with ID '${elementId}' not found.`); + return null; + } + + // Find nearest container upward + let container = element.closest('div[data-testid="stElementContainer"]'); + if (!container) { + console.error(`No parent with data-testid="stElementContainer" found for ID '${elementId}'.`); + return null; + } + + // Find all containers in parent document order + const containers = Array.from(window.parent.document.querySelectorAll('div[data-testid="stElementContainer"]')); + const index = containers.indexOf(container); + + if (index > 0) { + return containers[index - 1]; + } else { + console.warn(`No previous stElementContainer found before ID '${elementId}'.`); + return null; + } + } + + // helper function to find the alert container in a streamlit element + function findAlertInContainer(container) { + if (!container) { + console.error("Container not provided."); + return null; + } + + // Search inside the container for the first stAlert div + const alertDiv = container.querySelector('div[data-testid="stAlert"]'); + + if (!alertDiv) { + console.warn("No stAlert found inside the given container."); + return null; + } + + return alertDiv; + } + + const element = window.parent.document.getElementById(ARG) + const prevElement = findPreviousStElementContainer(ARG); + const alert = findAlertInContainer(prevElement); + const target = alert ?? element; + + + target.scrollIntoView({behavior: "smooth"}); + +}({}); diff --git a/libs/live-labs/live_labs/lab.py b/libs/live-labs/live_labs/lab.py index e110cce..130a6b5 100644 --- a/libs/live-labs/live_labs/lab.py +++ b/libs/live-labs/live_labs/lab.py @@ -46,31 +46,26 @@ """ import json +import random +from collections.abc import Callable from pathlib import Path from types import ModuleType -from typing import Any, Optional +from typing import Any, TypeVar import streamlit as st -from jinja2 import BaseLoader, Environment from pydantic import BaseModel, Field, PrivateAttr from streamlit.delta_generator import DeltaGenerator from streamlit_autorefresh import st_autorefresh +from streamlit_extras.add_vertical_space import add_vertical_space +from streamlit_extras.let_it_rain import rain from streamlit_extras.stateful_button import button -from live_labs import editor, localization, testing +from live_labs import editor, localization, templates, testing +from live_labs.helpers import DEFAULT_STATE_FILE, scroll_to, slugify -DEFAULT_STATE_FILE = Path("/project/data/scratch/tutorial_state.json") - - -def _slugify(name: str) -> str: - """Convert a name into a slugged string.""" - - def _is_valid(char: str) -> bool: - """Only pass lowercase and underscores.""" - return (ord(char) > 96 and ord(char) < 123) or ord(char) == 95 - - filtered_name = [x for x in name.lower().replace(" ", "_") if _is_valid(x)] - return "".join(filtered_name) +_TYPE = TypeVar("_TYPE") +_FREE_SCROLL_LINES = 50 +_END_EMOJIS = "πŸŽ‰πŸŽŠπŸ₯³πŸŽˆπŸŽ‚πŸŽπŸΎπŸ₯‚πŸŽ†πŸŽ‡βœ¨πŸͺ©πŸŽΆπŸ†πŸ₯‡πŸ…πŸŽ―🎀🍻πŸ’₯πŸš€πŸ‘‘πŸ•ΊπŸ’ƒπŸ€©πŸŒ»" class Worksheet(BaseModel): @@ -84,11 +79,13 @@ class Worksheet(BaseModel): completed_tasks: int = Field(0, init=False) total_tasks: int = Field(0, init=False) - _body: Optional[DeltaGenerator] = PrivateAttr(None) - _base_dir: Optional[Path] = PrivateAttr(None) - _files: Optional[list[str]] = PrivateAttr(None) - _files_data_init: Optional[list[str]] = PrivateAttr(None) - _stdout: Optional[DeltaGenerator] = PrivateAttr(None) + make_it_rain: bool = Field(False, init=False) + + _body: DeltaGenerator | None = PrivateAttr(None) + _base_dir: Path | None = PrivateAttr(None) + _files: list[str] | None = PrivateAttr(None) + _files_data_init: list[str] | None = PrivateAttr(None) + _stdout: DeltaGenerator | None = PrivateAttr(None) @property def stdout(self) -> DeltaGenerator: @@ -119,14 +116,18 @@ def __enter__(self) -> "Worksheet": return self - def __exit__(self, _, __, ___): + def __exit__(self, _: object, __: object, ___: object): """Cache data.""" + add_vertical_space(_FREE_SCROLL_LINES) if self._body: self._body.__exit__(None, None, None) if not self.ephemeral: self.save_state() - def with_editor(self, base_dir: Path, files: list[str]): + if self.make_it_rain: + rain(random.choice(_END_EMOJIS), animation_length=1) + + def with_editor(self, base_dir: Path, files: list[str]) -> "Worksheet": """Enable the in page code editor.""" self._base_dir = base_dir self._files = files @@ -134,7 +135,8 @@ def with_editor(self, base_dir: Path, files: list[str]): if not base_dir.exists(): base_dir.mkdir() - artifacts = st.session_state.get("artifacts", []) + [str(base_dir)] + artifacts = st.session_state.get("artifacts", []) + artifacts.append(str(base_dir)) st.session_state["artifacts"] = artifacts for idx, file in enumerate(self._files): @@ -152,7 +154,7 @@ def load_state(self): try: with self.state_file.open("r", encoding="UTF-8") as ptr: loaded_state = json.load(ptr) - except (IOError, OSError): + except OSError: loaded_state = {} st.session_state.update(loaded_state) @@ -165,7 +167,7 @@ def save_state(self): last_state_json = state_dict.pop("last_state", "{}") # dont recurse and save last state # dont save autorefresh runtime var # dont save session scoped variables (*_derived) - remove_keys = ["autorefresh"] + [key for key in state_dict.keys() if key.endswith("_derived")] + remove_keys = ["autorefresh"] + [key for key in state_dict if key.endswith("_derived")] _ = [state_dict.pop(key, None) for key in remove_keys] state_json = json.dumps(state_dict) @@ -175,7 +177,7 @@ def save_state(self): ptr.write(state_json) st.session_state["last_state"] = state_json - def run_test(self, fun) -> tuple[bool, None | str, None | Any]: + def run_test(self, fun: Callable[[], _TYPE]) -> tuple[bool, None | str, None | _TYPE]: """Cache the state of a test once it passes.""" cached_state: tuple[bool, None | str, None | Any] state: tuple[bool, None | str, None | Any] @@ -200,19 +202,30 @@ def run_test(self, fun) -> tuple[bool, None | str, None | Any]: return state def print_task( - self, task: localization.Task, test_suite: None | ModuleType, messages: localization.MessageCatalog + self, + task: localization.Task, + test_suite: None | ModuleType, + messages: localization.MessageCatalog, ) -> bool: """Write tasks out to screen. Returns boolean to indicate if task printing should continue.""" st.write("### " + task.name) + st.markdown(task.msg, unsafe_allow_html=True) # html is allowed to enable
blocks # Lookup a test from the test module. test = task.get_test(test_suite) + prep = task.get_prep(test_suite) result: str | None = "" + slug = slugify(task.name) + + # run prep function + if prep and not st.session_state.get(f"{self.name}_task_{slug}_prep"): + result = prep() + st.session_state[f"{self.name}_task_{slug}_prep"] = True if test: # continue task based on test function @@ -227,20 +240,23 @@ def print_task( else: # continue task based on user input - slug = _slugify(task.name) - col1, col2 = st.columns([3, 1]) - with col1: - st.write("**" + messages.get("waiting_msg", "") + "**") - with col2: - done = button(messages.get("next"), key=f"{self.name}_task_{slug}") - if not done: - return False + st.write("") + with st.empty(): + col1, col2 = st.columns([3, 1]) + with col1: + st.write("**" + messages.get("waiting_msg", "") + "**") + with col2: + done = button(messages.get("next"), key=f"{self.name}_task_{slug}") + if not done: + return False + st.write("") # Hide the "next" button # show success message after completion scs_msg = task.response if scs_msg is not None: st.write(" ") - rtemplate = Environment(loader=BaseLoader()).from_string(task.response or "") + rtemplate = templates.ENVIRONMENT.from_string(task.response or "") + rtemplate.render(result=result) st.success(rtemplate.render(result=result)) return True @@ -250,13 +266,16 @@ def live_lab(self, messages: localization.MessageCatalog, test_suite: None | Mod self.total_tasks += len(messages.tasks) for task in messages.tasks: if not self.print_task(task, test_suite, messages): + if self.completed_tasks > 0: + scroll_to(task.name) break self.completed_tasks += 1 else: # Print footer after last task - msg = messages.get("closing_msg", None) - if msg: - st.success(msg) + st.header(messages.get("closing_header")) + scroll_to(messages.get("closing_header")) + st.markdown(messages.get("closing_msg")) + self.make_it_rain = True st.session_state[f"{self.name}_completed"] = self.completed_tasks st.session_state[f"{self.name}_total"] = self.total_tasks diff --git a/libs/live-labs/live_labs/localization.py b/libs/live-labs/live_labs/localization.py index ef9e71e..99a1a76 100644 --- a/libs/live-labs/live_labs/localization.py +++ b/libs/live-labs/live_labs/localization.py @@ -25,8 +25,9 @@ ``` """ +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable, cast +from types import ModuleType import streamlit as st from pydantic import BaseModel, ConfigDict, Field @@ -42,19 +43,29 @@ class Task(BaseModel): msg: str response: None | str = None test: None | str = None + prep: None | str = None - def get_test(self, tests: Any) -> None | Callable[[], str]: + def get_test(self, tests: None | ModuleType) -> None | Callable[[], str]: """Find a test for this task.""" if self.name and self.test and tests: - out = cast(Callable[[], str], getattr(tests, self.test, None)) - return out + func = getattr(tests, self.test, None) + if callable(func): + return func # type: ignore[return-value] + return None + + def get_prep(self, tests: None | ModuleType) -> None | Callable[[], str]: + """Find a prep function for this task.""" + if self.name and self.prep and tests: + func = getattr(tests, self.prep, None) + if callable(func): + return func # type: ignore[return-value] return None class MessageCatalog(BaseModel): """Representation of a localization catalog files.""" - __pydantic_extra__: dict[str, None | str | list[Task]] = Field(init=False) # type: ignore + __pydantic_extra__: dict[str, None | str | list[Task]] = Field(init=False) model_config = ConfigDict(extra="allow") tasks: list[Task] = [] @@ -62,7 +73,7 @@ class MessageCatalog(BaseModel): @classmethod def from_yaml(cls, path: Path) -> "MessageCatalog": """Load the message catalog data from yaml.""" - with open(path, "r", encoding="UTF-8") as ptr: + with path.open(encoding="UTF-8") as ptr: yml = ptr.read() return parse_yaml_raw_as(cls, yml) @@ -81,8 +92,10 @@ def from_page(cls, page_path: Path | str) -> "MessageCatalog": return cls.from_yaml(catalog_path) return cls() - def get(self, key: str, default_value: Any = None) -> Any: + def get(self, key: str, default_value: None | str = None) -> str: """Get a value from this class.""" + if default_value is None: + default_value = f":red-badge[{key}]" try: return getattr(self, key) except AttributeError: diff --git a/libs/live-labs/live_labs/pages/settings.en_US.yaml b/libs/live-labs/live_labs/pages/settings.en_US.yaml index b7639d0..12b20bb 100644 --- a/libs/live-labs/live_labs/pages/settings.en_US.yaml +++ b/libs/live-labs/live_labs/pages/settings.en_US.yaml @@ -14,6 +14,9 @@ # limitations under the License. title: Settings +reset_header: Reset your progress +reset_warning: This will erase the code you've written and your saved progress. It might not be recoverable. + bundle_header: Logs and Support Bundles bundle_msg: | A **Support Bundle** is a collection of logs that capture the state of your AI Workbench. To create a support bundle to send in for troubleshooting, first open the AI Workbench CLI (use the WSL distro if on Windows) diff --git a/libs/live-labs/live_labs/pages/settings.py b/libs/live-labs/live_labs/pages/settings.py index 8bd58bf..8971c4e 100644 --- a/libs/live-labs/live_labs/pages/settings.py +++ b/libs/live-labs/live_labs/pages/settings.py @@ -13,11 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. """A generic settings page.""" + from pathlib import Path import streamlit as st from streamlit_extras.stateful_button import button as st_toggle_btn -from streamlit_file_browser import st_file_browser from live_labs import MessageCatalog, Worksheet, reset_all_progress @@ -25,31 +25,16 @@ NAME = Path(__file__).stem with Worksheet(name=NAME, ephemeral=True) as worksheet: - - # file browser - with st.container(border=True): - st.markdown("## Lab Results") - globs = [path + "/**" for path in st.session_state.get("artifacts", [])] - file_browser_event = st_file_browser( - "/project/code", - glob_patterns=globs, - extentions=["py", "yaml", "txt"], - show_preview=False, - ) - - if file_browser_event and file_browser_event.get("type", "") == "SELECT_FILE": - file = Path("/project/code").joinpath(file_browser_event["target"]["path"]) - with file.open("rb") as ptr: - st.download_button(label="Download File", data=ptr, file_name=file.name) - # reset progress with st.container(border=True): + st.header(MESSAGES.get("reset_header")) + st.warning(MESSAGES.get("reset_warning")) col_1, col_2, col_3 = st.columns([1, 1, 1]) with col_1: - reset = st_toggle_btn("Reset your progress.", key="reset") + reset = st_toggle_btn("⚠️ Reset your progress.", key="reset") if reset: with col_2: - verify_reset = st.button("Are you sure?") + verify_reset = st.button("πŸ›‘ Are you sure?") if verify_reset: reset_all_progress() @@ -57,13 +42,11 @@ col_1, col_2 = st.columns([1, 1]) # information on support logs - with col_1: - with st.container(border=True): - st.header(MESSAGES.get("bundle_header"), divider="gray") - st.markdown(MESSAGES.get("bundle_msg")) + with col_1, st.container(border=True): + st.header(MESSAGES.get("bundle_header"), divider="gray") + st.markdown(MESSAGES.get("bundle_msg")) # information on developer forum - with col_2: - with st.container(border=True): - st.header(MESSAGES.get("forum_header"), divider="gray") - st.markdown(MESSAGES.get("forum_msg")) + with col_2, st.container(border=True): + st.header(MESSAGES.get("forum_header"), divider="gray") + st.markdown(MESSAGES.get("forum_msg")) diff --git a/libs/live-labs/live_labs/shell.py b/libs/live-labs/live_labs/shell.py index 180e6d0..99c00fa 100644 --- a/libs/live-labs/live_labs/shell.py +++ b/libs/live-labs/live_labs/shell.py @@ -120,7 +120,7 @@ def _icon(name: str) -> str: 'style="display: inline-block; font-family: "Material Symbols Rounded"; font-weight: 400; ' 'user-select: none; vertical-align: bottom; white-space: nowrap; overflow-wrap: normal;"' ) - return f'{name}' + return f'{name}' class AppShell(BaseModel): @@ -146,7 +146,7 @@ def model_post_init(self, _: Any, /) -> None: @classmethod def from_yaml(cls, path: Path) -> "AppShell": """Load the sidebar data from yaml.""" - with open(path, "r", encoding="UTF-8") as ptr: + with path.open(encoding="UTF-8") as ptr: yml = ptr.read() return parse_yaml_raw_as(cls, yml) @@ -190,8 +190,7 @@ def _render_links(self): ) if self.links.settings: html += ( - '' - f'{_icon("settings")}' + f'{_icon("settings")}' ) html += "" @@ -200,7 +199,7 @@ def _render_links(self): def _render_stylesheet(self): """Load and apply the stylesheet.""" - with open(DEFAULT_CSS, "r", encoding="UTF-8") as ptr: + with DEFAULT_CSS.open(encoding="UTF-8") as ptr: style = ptr.read() with st.container(height=1, border=False): st.html(f"") @@ -215,5 +214,4 @@ def sidebar(self): def navigation(self) -> "StreamlitPage": """Run the streamlit multipage router.""" - pg = st.navigation(self.page_list) - return pg + return st.navigation(self.page_list) diff --git a/code/tutorial_app/pages_templates/template.py.envsub b/libs/live-labs/live_labs/templates.py similarity index 53% rename from code/tutorial_app/pages_templates/template.py.envsub rename to libs/live-labs/live_labs/templates.py index 2169bd0..f0b4c6c 100644 --- a/code/tutorial_app/pages_templates/template.py.envsub +++ b/libs/live-labs/live_labs/templates.py @@ -12,26 +12,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Excercise page layout.""" +"""Custom filters for Jinja parsing.""" -from pathlib import Path +import ast +import json +from typing import Any -import streamlit as st -import live_labs +from jinja2 import BaseLoader, Environment -from pages import ${PAGE_NAME}_tests as TESTS -MESSAGES = localization.load_messages(__file__) -NAME = Path(__file__).stem +def from_json(value: str) -> Any: + """Convert a JSON string to an object.""" + return json.loads(str(value)) -EDITOR_DIR = Path("/project/code").joinpath(NAME) -EDITOR_FILES = ["file1.py", "file2.py"] -with live_labs.Worksheet(autorefresh=0).with_editor(EDITOR_DIR, EDITOR_FILES) as worksheet: - # Header - st.title(MESSAGES.get("title")) - st.write(MESSAGES.get("welcome_msg")) - st.header(MESSAGES.get("header"), divider="gray") +def eval(value: str) -> Any: + """Parse the string and return the value.""" + return ast.literal_eval(value) - # Print Tasks - worksheet.live_lab(NAME, MESSAGES, TESTS) + +ENVIRONMENT = Environment(loader=BaseLoader()) +ENVIRONMENT.filters["from_json"] = from_json +ENVIRONMENT.filters["eval"] = eval diff --git a/libs/live-labs/live_labs/testing.py b/libs/live-labs/live_labs/testing.py index a72d681..156ec80 100644 --- a/libs/live-labs/live_labs/testing.py +++ b/libs/live-labs/live_labs/testing.py @@ -46,15 +46,19 @@ def test_my_string(): print("Looks good!") ``` """ + import functools import inspect +import os +import re import selectors import subprocess import sys import time +from collections.abc import Callable, Iterator from functools import partial from pathlib import Path -from typing import Any, Callable, Iterator, TextIO, cast +from typing import Any, TextIO, cast import streamlit as st @@ -114,6 +118,7 @@ def execute(self) -> Iterator[str]: stderr=subprocess.PIPE, text=True, cwd=self.cwd, + env=os.environ, ) as proc: # start the test assert proc.stdin is not None @@ -131,7 +136,7 @@ def execute(self) -> Iterator[str]: sel.register(proc.stderr, selectors.EVENT_READ) # iterate through stdout + stderr - yield "```" + yield "```\n" start_time = time.monotonic() while time.monotonic() - start_time < _TIMEOUT: for key, _ in sel.select(timeout=0.1): @@ -159,7 +164,7 @@ def execute(self) -> Iterator[str]: else: self._testfail = "info_test_timeout" - yield "```" + yield "```\n\n" # update the status self._rc = proc.returncode @@ -179,8 +184,8 @@ def __call__(self) -> str: raise TestFail("info_test_nonzero_exit_code") # remove the markdown code block indicators - output_text = "\n".join(output_text.splitlines()[1:-1]) - return output_text + match = re.search(r"```(.*?)```", output_text, re.DOTALL) + return match.group(1) if match else "" def isolate(cwd: Path | None = None, exec: str | Path | None = None) -> Callable[[Callable[[], None]], Runner]: diff --git a/libs/live-labs/pyproject.toml b/libs/live-labs/pyproject.toml index e3bca5d..fcecea2 100644 --- a/libs/live-labs/pyproject.toml +++ b/libs/live-labs/pyproject.toml @@ -16,7 +16,6 @@ dependencies = [ "streamlit-ace", "streamlit-bridge", "streamlit-javascript", - "streamlit_file_browser", ] [project.optional-dependencies] @@ -32,41 +31,51 @@ requires = [ [tool.setuptools.package-data] live_labs = ["css/*.css", "js/*.js"] -# Pylint Configuration -[tool.pylint.MAIN] -load-plugins = "pylint.extensions.bad_builtin" +[tool.ruff] +line-length = 120 +target-version = "py310" -[tool.pylint.'MESSAGES CONTROL'] -disable=["raw-checker-failed", - "bad-inline-option", - "locally-disabled", - "file-ignored", - "suppressed-message", - "useless-suppression", - "deprecated-pragma", - "use-symbolic-message-instead", - "wrong-spelling-in-comment", - "redefined-builtin", - "unsupported-binary-operation", - "duplicate-code", - "no-member"] -enable=["c-extension-no-member", "broad-exception-caught"] +exclude = [ + "**/*.ipynb" +] -[tool.pylint.FORMAT] -max-line-length=120 -max-module-lines=500 -max-args=8 +[tool.ruff.lint] +select = [ + "E", # pycodestyle (formatting errors) + "F", # pyflakes (logical errors, like unused imports) + "B", # bugbear (common Python gotchas) + "I", # isort (import sorting) + "UP", # pyupgrade (modernize Python syntax) + "C4", # comprehensions (bad comprehensions, dict/set/list comprehensions issues) + "RET", # flake8-return checks + "T10", # flake8-pdb (ban interactive debugger in real code) + # "T20", # flake8-print (ban print statements in real code) + "SIM", # flake8-simplify (simplify if-else, etc) + "PTH", # flake8-pathlib (encourage pathlib usage) + "PYI", # flake8-pyi + "PL", # pylint rules subset (e.g., overly complex functions) + "RUF", # Ruff-specific rules + "ANN", # Type annotations + "ARG", # Function arguments + "FIX", # Forbid common FIXME comments +] +ignore = [ + "ANN401", # Allow the use of the Any type +] +unfixable = [ + "F401", # Dont remove unused imports +] -[tool.pylint.DEPRECATED_BUILTINS] -bad-functions=["print", "input"] +[tool.ruff.lint.flake8-annotations] +allow-star-arg-any = true # suppress ANN401 for dynamically typed *args and **kwargs arguments. +mypy-init-return = true # allow the omission of a return type hint for __init__ if at least one argument is annotated. +suppress-none-returning = true # allow the return type to implicitly be None. -# Flake8 Configuration -[tool.flake8] -max-line-length = 120 -ignore = ["E203", "W503"] -enable = ["W504"] +[tool.ruff.lint.isort] +force-single-line = false +known-first-party = [] +known-third-party = [] -# mypy Configuration [tool.mypy] strict = false implicit_optional = true diff --git a/postBuild.bash b/postBuild.bash index 1760672..64aa40e 100644 --- a/postBuild.bash +++ b/postBuild.bash @@ -42,10 +42,20 @@ EOM # setup the tutorial app cd /opt sudo python3 -m venv live-labs -sudo ./live-labs/bin/pip install wheel -sudo ./live-labs/bin/pip install git+https://github.com/NVIDIA/nim-anywhere.git#subdirectory=libs/live-labs +sudo ./live-labs/bin/pip install --upgrade pip wheel +sudo ./live-labs/bin/pip install git+https://github.com/NVIDIA/nim-anywhere.git@rkraus/simplify#subdirectory=libs/live-labs +# TODO: swith branch back to main sudo ln -s /opt/live-labs/bin/streamlit /home/workbench/.local/bin/streamlit +# install pipx packages +if [ -f /project/pipx.txt ]; then + while IFS= read -r line; do + pipx install "$line" + done < /project/pipx.txt +fi + + + # clean up sudo apt-get autoremove -y sudo rm -rf /var/cache/apt diff --git a/pyproject.toml b/pyproject.toml index bf42818..4232f72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,60 @@ -# Pylint Configuration -[tool.pylint.MAIN] -load-plugins = "pylint.extensions.bad_builtin" +[tool.ruff] +line-length = 120 +target-version = "py310" -[tool.pylint.'MESSAGES CONTROL'] -disable=["raw-checker-failed", - "bad-inline-option", - "locally-disabled", - "file-ignored", - "suppressed-message", - "useless-suppression", - "deprecated-pragma", - "use-symbolic-message-instead", - "wrong-spelling-in-comment", - "redefined-builtin", - "unsupported-binary-operation", - "duplicate-code", - "no-member"] -enable=["c-extension-no-member", "broad-exception-caught"] +exclude = [ + "**/*.ipynb", + "code/frontend/*", # legacy code + "code/chain_server/*", # legacy code +] -[tool.pylint.FORMAT] -max-line-length=120 -max-module-lines=500 -max-args=8 +[tool.ruff.lint] +select = [ + "E", # pycodestyle (formatting errors) + "F", # pyflakes (logical errors, like unused imports) + "B", # bugbear (common Python gotchas) + "I", # isort (import sorting) + "UP", # pyupgrade (modernize Python syntax) + "C4", # comprehensions (bad comprehensions, dict/set/list comprehensions issues) + "T10", # flake8-pdb (ban interactive debugger in real code) + # "T20", # flake8-print (ban print statements in real code) + "SIM", # flake8-simplify (simplify if-else, etc) + "PTH", # flake8-pathlib (encourage pathlib usage) + "PYI", # flake8-pyi + "PL", # pylint rules subset (e.g., overly complex functions) + "RUF", # Ruff-specific rules + "ANN", # Type annotations + "ARG", # Function arguments + "FIX", # Forbid common FIXME comments +] +ignore = [ + "ANN401", # Allow the use of the Any type +] +unfixable = [ + "F401", # Dont remove unused imports +] -[tool.pylint.DEPRECATED_BUILTINS] -bad-functions=["print", "input"] +[tool.ruff.lint.per-file-ignores] +"**/answers/*" = ["ANN001", "ANN201"] -# Flake8 Configuration -[tool.flake8] -max-line-length = 120 -ignore = ["E203", "W503"] -enable = ["W504"] +[tool.ruff.lint.flake8-annotations] +allow-star-arg-any = true # suppress ANN401 for dynamically typed *args and **kwargs arguments. +mypy-init-return = true # allow the omission of a return type hint for __init__ if at least one argument is annotated. +suppress-none-returning = true # allow the return type to implicitly be None. + +[tool.ruff.lint.isort] +force-single-line = false +known-first-party = [] +known-third-party = [] -# mypy Configuration [tool.mypy] strict = false implicit_optional = true follow_imports = "silent" ignore_missing_imports = true show_column_numbers = true -disable_error_code = ["no-untyped-call", "override", "misc", "import-untyped"] +disable_error_code = ["no-untyped-call", "override", "import-untyped"] +exclude = "**/answers/*" +[tool.pylint] +max-line-length = 120 diff --git a/req.filters.txt b/req.filters.txt deleted file mode 100644 index eae30c1..0000000 --- a/req.filters.txt +++ /dev/null @@ -1,4 +0,0 @@ -# these are packages that will be excluded from the built container. -jupyterlab -grandalf -watchfiles diff --git a/requirements.txt b/requirements.txt index dac87e6..5b2626d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ redis==5.2.1 sse-starlette==2.2.1 uvicorn==0.34.0 watchfiles==1.0.3 +cachier