├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── azure-dev.yml │ ├── build.yaml │ └── validate-infra.yaml ├── .gitignore ├── .nvmrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── app ├── api │ ├── chat │ │ ├── config │ │ │ └── route.ts │ │ ├── engine │ │ │ ├── chat.ts │ │ │ ├── file-storage.ts │ │ │ ├── generate.ts │ │ │ ├── index.ts │ │ │ ├── loader.ts │ │ │ ├── queryFilter.ts │ │ │ ├── settings.ts │ │ │ ├── shared.ts │ │ │ └── tools │ │ │ │ ├── __AzureDynamicSessionTool.node--patched.ts │ │ │ │ ├── __azure-code-interpreter.ts │ │ │ │ └── index.ts │ │ ├── llamaindex │ │ │ ├── documents │ │ │ │ ├── helper.ts │ │ │ │ ├── pipeline.ts │ │ │ │ └── upload.ts │ │ │ └── streaming │ │ │ │ ├── annotations.ts │ │ │ │ ├── events.ts │ │ │ │ ├── file.ts │ │ │ │ ├── stream.ts │ │ │ │ └── suggestion.ts │ │ ├── route.ts │ │ └── upload │ │ │ └── route.ts │ └── files │ │ └── [...slug] │ │ └── route.ts ├── components │ ├── chat-section.tsx │ ├── header.tsx │ └── ui │ │ ├── README.md │ │ ├── button.tsx │ │ ├── chat │ │ ├── chat-actions.tsx │ │ ├── chat-input.tsx │ │ ├── chat-message │ │ │ ├── chat-agent-events.tsx │ │ │ ├── chat-avatar.tsx │ │ │ ├── chat-events.tsx │ │ │ ├── chat-files.tsx │ │ │ ├── chat-image.tsx │ │ │ ├── chat-sources.tsx │ │ │ ├── chat-suggestedQuestions.tsx │ │ │ ├── chat-tools.tsx │ │ │ ├── codeblock.tsx │ │ │ ├── index.tsx │ │ │ └── markdown.tsx │ │ ├── chat-messages.tsx │ │ ├── chat.interface.ts │ │ ├── hooks │ │ │ ├── use-config.ts │ │ │ ├── use-copy-to-clipboard.tsx │ │ │ └── use-file.ts │ │ ├── index.ts │ │ └── widgets │ │ │ ├── PdfDialog.tsx │ │ │ └── WeatherCard.tsx │ │ ├── collapsible.tsx │ │ ├── document-preview.tsx │ │ ├── drawer.tsx │ │ ├── file-uploader.tsx │ │ ├── hover-card.tsx │ │ ├── icons │ │ ├── docx.svg │ │ ├── pdf.svg │ │ ├── sheet.svg │ │ └── txt.svg │ │ ├── input.tsx │ │ ├── lib │ │ └── utils.ts │ │ ├── select.tsx │ │ └── upload-image-preview.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── markdown.css └── page.tsx ├── azure.yaml ├── cache └── vector_store.json ├── config └── tools.json ├── docs ├── assets │ ├── llamaindex-code-interpreter-azure-dynamic-session-architecture.png │ ├── llamaindex-code-interpreter-azure-dynamic-session-full.png │ └── llamaindex-code-interpreter-azure-dynamic-session-small.png └── readme.md ├── infra ├── abbreviations.json ├── app │ └── llamaindex-azure-dynamic-session.bicep ├── main.bicep ├── main.parameters.json ├── modules │ ├── dynamic-sessions.bicep │ └── fetch-container-image.bicep ├── readme.md └── shared │ ├── apps-env.bicep │ ├── cognitive-services.bicep │ ├── dashboard-web.bicep │ ├── keyvault.bicep │ ├── monitoring.bicep │ ├── registry.bicep │ ├── security-role.bicep │ └── storage-account.bicep ├── next.config.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── llama.png └── sample-data │ └── gdppercap.csv ├── tailwind.config.ts ├── tsconfig.json └── webpack.config.mjs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:dev-20-bullseye", 3 | "features": { 4 | "ghcr.io/devcontainers-contrib/features/turborepo-npm:1": {}, 5 | "ghcr.io/devcontainers-contrib/features/typescript:2": {}, 6 | "ghcr.io/devcontainers/features/python:1": { 7 | "version": "3.11", 8 | "toolsToInstall": [ 9 | "flake8", 10 | "black", 11 | "mypy", 12 | "poetry" 13 | ] 14 | } 15 | }, 16 | "customizations": { 17 | "codespaces": { 18 | "openFiles": [ 19 | "README.md" 20 | ] 21 | }, 22 | "vscode": { 23 | "extensions": [ 24 | "ms-vscode.typescript-language-features", 25 | "esbenp.prettier-vscode", 26 | "ms-python.python", 27 | "ms-python.black-formatter", 28 | "ms-python.vscode-flake8", 29 | "ms-python.vscode-pylance" 30 | ], 31 | "settings": { 32 | "python.formatting.provider": "black", 33 | "python.languageServer": "Pylance", 34 | "python.analysis.typeCheckingMode": "basic" 35 | } 36 | } 37 | }, 38 | "containerEnv": { 39 | "POETRY_VIRTUALENVS_CREATE": "false" 40 | }, 41 | "forwardPorts": [ 42 | 3000, 43 | 8000 44 | ], 45 | "postCreateCommand": "npm install" 46 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"], 3 | "rules": { 4 | "max-params": ["error", 4], 5 | "prefer-const": "error" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Azure 2 | 3 | # Run when commits are pushed to main 4 | on: 5 | workflow_dispatch: 6 | push: 7 | # Run when commits are pushed to mainline branch (main or master) 8 | # Set this to the mainline branch you are using 9 | branches: 10 | - main 11 | 12 | # Set up permissions for deploying with secretless Azure federated credentials 13 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 14 | permissions: 15 | id-token: write 16 | contents: read 17 | 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | env: 23 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 24 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 25 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 26 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 27 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 28 | AZURE_DEV_COLLECT_TELEMETRY: 'no' 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Install azd 33 | uses: azure/setup-azd@v2 34 | - name: Log in with Azure (Federated Credentials) 35 | run: | 36 | azd auth login ` 37 | --client-id "$Env:AZURE_CLIENT_ID" ` 38 | --federated-credential-provider "github" ` 39 | --tenant-id "$Env:AZURE_TENANT_ID" 40 | shell: pwsh 41 | 42 | 43 | - name: Provision Infrastructure 44 | run: azd provision --no-prompt 45 | env: 46 | AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} 47 | 48 | - name: Deploy Application 49 | run: azd deploy --no-prompt 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Runs on any open or reopened pull request 9 | pull_request: 10 | types: [opened, reopened] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | jobs: 22 | # Build job 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Setup Node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: "20" 32 | - name: Restore cache 33 | uses: actions/cache@v4 34 | with: 35 | path: | 36 | .next/cache 37 | # Generate a new cache whenever packages or source files change. 38 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 39 | # If source files changed but packages didn't, rebuild from a prior cache. 40 | restore-keys: | 41 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- 42 | - name: Install dependencies 43 | run: npm install 44 | - name: Build 45 | run: npm run build 46 | env: 47 | # Provide this env variable so that the build does not fail 48 | AZURE_OPENAI_ENDPOINT: "foo-bar" -------------------------------------------------------------------------------- /.github/workflows/validate-infra.yaml: -------------------------------------------------------------------------------- 1 | name: Validate AZD template 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - 'infra/**' 7 | pull_request: 8 | branches: [main] 9 | paths: 10 | - 'infra/**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Build Bicep for linting 22 | uses: azure/CLI@v2 23 | with: 24 | inlineScript: az config set bicep.use_binary_from_path=false && az bicep build -f infra/main.bicep --stdout 25 | 26 | - name: Run Microsoft Security DevOps Analysis 27 | uses: microsoft/security-devops-action@v1 28 | id: msdo 29 | continue-on-error: true 30 | with: 31 | tools: templateanalyzer 32 | 33 | - name: Upload alerts to Security tab 34 | if: github.repository_owner == 'Azure-Samples' 35 | uses: github/codeql-action/upload-sarif@v3 36 | with: 37 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | output/ 39 | .azure 40 | public/tools/azure-dynamic-sessions/* 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase main -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine as build 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json package-lock.* ./ 6 | RUN npm install 7 | 8 | # Build the application 9 | COPY . . 10 | RUN npm run build 11 | 12 | # ==================================== 13 | FROM build as release 14 | 15 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 22 | # Serverless RAG application with LlamaIndex and Azure Dyamic Sessions Tool 23 | 24 | [![Open project in GitHub Codespaces](https://img.shields.io/badge/Codespaces-Open-black?style=flat-square&logo=github)](https://codespaces.new/Azure-Samples/llama-index-azure-code-interpreter?hide_repo_select=true&ref=main&quickstart=true) 25 | [![Open project in Dev Containers](https://img.shields.io/badge/Dev_Containers-Open-blue?style=flat-square)](https://codespaces.new/Azure-Samples/llama-index-azure-code-interpreter?hide_repo_select=true&ref=main&quickstart=true) 26 | [![Build Status](https://img.shields.io/github/actions/workflow/status/Azure-Samples/llama-index-azure-code-interpreter/build.yaml?style=flat-square&label=Build)](https://github.com/Azure-Samples/llama-index-azure-code-interpreter/actions) 27 | ![Node version](https://img.shields.io/badge/Node.js->=20-3c873a?style=flat-square) 28 | [![License](https://img.shields.io/badge/License-MIT-pink?style=flat-square)](LICENSE) 29 | 30 | This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). It uses Azure Container Apps as a serverless deployment platform and Azure Dymanic Session as a tool for code interpretation. 31 | 32 | 33 | [![Features](https://img.shields.io/badge/🚀%20Features-blue?style=flat-square)](#features) 34 | [![Architecture Diagram](https://img.shields.io/badge/🌐%20Architecture%20Diagram-blue?style=flat-square)](#architecture-diagram) 35 | [![Demo Video](https://img.shields.io/badge/📺%20Demo%20Video-blue?style=flat-square)](#demo-video-optional) 36 | [![Getting Started](https://img.shields.io/badge/🚀%20Getting%20Started-blue?style=flat-square)](#getting-started) 37 | [![Contributing](https://img.shields.io/badge/🤝%20Contributing-blue?style=flat-square)](#contributing) 38 | [![Give us a star](https://img.shields.io/badge/⭐%20Give%20us%20a%20star-blue?style=flat-square)](https://github.com/Azure-Samples/llama-index-javascript/stargazers) 39 |
40 | 41 | ![Screenshot showing the LlamaIndex app in action](./docs/assets/llamaindex-code-interpreter-azure-dynamic-session-small.png) 42 | 43 | ## Important Security Notice 44 | 45 | This template, the application code and configuration it contains, has been built to showcase Microsoft Azure specific services and tools. We strongly advise our customers not to make this code part of their production environments without implementing or enabling additional security features. 46 | 47 | ## Features 48 | 49 | * A full-stack chat application written in [Next.js](https://nextjs.org/) 50 | * Support for file uploads, code highlighting, and pdf rendering 51 | * [Azure OpenAI](https://azure.microsoft.com/products/ai-services/openai-service) using gpt-4o-mini (by default) 52 | * [Azure Container Apps](https://azure.microsoft.com/products/container-apps) for deployment 53 | * [Azure Dynamic Sessions](https://learn.microsoft.com/azure/container-apps/sessions?tabs=azure-cli) Tool for code interpretation (Python runtime) 54 | * [Azure Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for secure access to Azure services 55 | 56 | ### Architecture Diagram 57 | 58 | ![Screenshot showing the chatgpt app high level diagram](./docs/assets/llamaindex-code-interpreter-azure-dynamic-session-architecture.png) 59 | 60 | ### Azure account requirements 61 | 62 | To deploy this template, you need an Azure subscription. If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/) before you begin. 63 | 64 | ## Getting Started 65 | 66 | You have a few options for getting started with this template. The quickest way to get started is [GitHub Codespaces](#github-codespaces), since it will setup all the tools for you, but you can also [set it up locally](#local-environment). You can also use a [VS Code dev container](#vs-code-dev-containers) 67 | 68 | This template uses `gpt-4o-mini` which may not be available in all Azure regions. Check for [up-to-date region availability](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#standard-deployment-model-availability) and select a region during deployment accordingly 69 | 70 | * We recommend using `eastus` 71 | 72 | ### GitHub Codespaces 73 | 74 | You can run this template virtually by using GitHub Codespaces. The button will open a web-based VS Code instance in your browser: 75 | 76 | 1. Open the template (this may take several minutes) 77 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/Azure-Samples/llama-index-azure-code-interpreter.git) 78 | 2. Open a terminal window 79 | 3. Sign into your Azure account: 80 | 81 | ```shell 82 | azd auth login --use-device-code 83 | ``` 84 | 5. Provision the Azure resources and deploy your code: 85 | 86 | ```shell 87 | azd up 88 | ``` 89 | 90 | You will be prompted to select some details about your deployed resources, including location. As a reminder we recommend `eastus` as the region for this project. Once the deployment is complete you should be able to scroll up in your terminal and see the url that the app has been deployed to. It should look similar to this `Ingress Updated. Access your app at https://env-name.codespacesname.eastus.azurecontainerapps.io/`. Navigate to the link to try out the app straight away! 91 | 92 | ### VS Code Dev Containers 93 | 94 | A related option is VS Code Dev Containers, which will open the project in your local VS Code using the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): 95 | 96 | 1. Start Docker Desktop (install it if not already installed) 97 | 2. Open the project: 98 | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)]() 99 | 3. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. 100 | 4. Sign into your Azure account: 101 | 102 | ```shell 103 | azd auth login 104 | ``` 105 | 6. Provision the Azure resources and deploy your code: 106 | 107 | ```shell 108 | azd up 109 | ``` 110 | 6. Install the app dependencies: 111 | 112 | ```bash 113 | npm install 114 | ``` 115 | 8. Configure a CI/CD pipeline: 116 | 117 | ```shell 118 | azd pipeline config 119 | ``` 120 | To start the web app, run the following command: 121 | 122 | ```bash 123 | npm run dev 124 | ``` 125 | 126 | Open the URL `http://localhost:3000` in your browser to interact with the bot. 127 | 128 | ### Local Environment 129 | 130 | #### Prerequisites 131 | 132 | You need to install following tools to work on your local machine: 133 | 134 | * Install [azd](https://aka.ms/install-azd) 135 | * Windows: `winget install microsoft.azd` 136 | * Linux: `curl -fsSL https://aka.ms/install-azd.sh | bash` 137 | * MacOS: `brew tap azure/azd && brew install azd` 138 | * Docker Desktop (for Mac M1 M2 M3, use [Docker Desktop for Apple Silicon](https://docs.docker.com/docker-for-mac/apple-silicon/) 4.34.2 or later) 139 | * Node.js (v20 LTS) 140 | * Git 141 | 142 | Then you can get the project code: 143 | 144 | 1. [**Fork**](https://github.com/Azure-Samples/llama-index-azure-code-interpreter/fork) the project to create your own copy of this repository. 145 | 2. On your forked repository, select the **Code** button, then the **Local** tab, and copy the URL of your forked repository. 146 | 3. Open a terminal and run this command to clone the repo: git clone <your-repo-url> 147 | 148 | #### Quickstart 149 | 150 | 1. Bring down the template code: 151 | 152 | ```shell 153 | azd init --template llama-index-azure-code-interpreter 154 | ``` 155 | 156 | This will perform a git clone 157 | 158 | 2. Sign into your Azure account: 159 | 160 | ```shell 161 | azd auth login 162 | ``` 163 | 164 | 3. Install all dependencies: 165 | 166 | ```bash 167 | npm install 168 | ``` 169 | 5. Provision and deploy the project to Azure: 170 | 171 | ```shell 172 | azd up 173 | ``` 174 | 175 | 7. Configure a CI/CD pipeline: 176 | 177 | ```shell 178 | azd pipeline config 179 | ``` 180 | 181 | Once your deployment is complete, you should see a `.env` file at the root of the project. This file contains the environment variables needed to run the application using Azure resources. 182 | 183 | #### Local Development 184 | 185 | First, install the dependencies: 186 | 187 | ``` 188 | npm install 189 | ``` 190 | 191 | Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step): 192 | 193 | ``` 194 | npm run generate 195 | ``` 196 | 197 | Third, run the development server: 198 | 199 | ``` 200 | npm run dev 201 | ``` 202 | 203 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 204 | 205 | ##### Local Development (Using Docker) 206 | 207 | 1. Build an image for the Next.js app: 208 | 209 | ``` 210 | docker build -t . 211 | ``` 212 | 213 | 2. Generate embeddings: 214 | 215 | Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step: 216 | 217 | ``` 218 | docker run \ 219 | --rm \ 220 | -v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system 221 | -v $(pwd)/config:/app/config \ 222 | -v $(pwd)/data:/app/data \ 223 | -v $(pwd)/cache:/app/cache \ # Use your file system to store the vector database 224 | \ 225 | npm run generate 226 | ``` 227 | 228 | 3. Start the app: 229 | 230 | ``` 231 | docker run \ 232 | --rm \ 233 | -v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system 234 | -v $(pwd)/config:/app/config \ 235 | -v $(pwd)/cache:/app/cache \ # Use your file system to store gea vector database 236 | -p 3000:3000 \ 237 | 238 | ``` 239 | 240 | ## Guidance 241 | 242 | ### Region Availability 243 | 244 | This template uses `gpt-4o-mini` which may not be available in all Azure regions. Check for [up-to-date region availability](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#standard-deployment-model-availability) and select a region during deployment accordingly 245 | * We recommend using `eastus` 246 | 247 | ### Costs 248 | 249 | You can estimate the cost of this project's architecture with [Azure's pricing calculator](https://azure.microsoft.com/pricing/calculator/) 250 | 251 | - Azure Container Apps: Consumption plan, Free for the first 2M executions. Pricing per execution and memory used. [Pricing](https://azure.microsoft.com/pricing/details/container-apps/) 252 | - Azure OpenAI: Standard tier, GPT and Ada models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/) 253 | 254 | > [!WARNING] 255 | > To avoid unnecessary costs, remember to take down your app if it's no longer in use, either by deleting the resource group in the Portal or running `azd down --purge`. 256 | 257 | 258 | ### Security 259 | 260 | This template has [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) built in to eliminate the need for developers to manage these credentials. Applications can use managed identities to obtain Microsoft Entra tokens without having to manage any credentials. Additionally, we have added a [GitHub Action tool](https://github.com/microsoft/security-devops-action) that scans the infrastructure-as-code files and generates a report containing any detected issues. To ensure best practices in your repo we recommend anyone creating solutions based on our templates ensure that the [Github secret scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning) setting is enabled in your repos. 261 | 262 | ## Resources 263 | 264 | Here are some resources to learn more about the technologies used in this sample: 265 | 266 | - [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features). 267 | - [Generative AI For Beginners](https://github.com/microsoft/generative-ai-for-beginners) 268 | - [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/overview) 269 | - [Azure OpenAI Assistant Builder](https://github.com/Azure-Samples/azure-openai-assistant-builder) 270 | - [Chat + Enterprise data with Azure OpenAI and Azure AI Search](https://github.com/Azure-Samples/azure-search-openai-javascript) 271 | 272 | You can also find [more Azure AI samples here](https://github.com/Azure-Samples/azureai-samples). 273 | 274 | ## Troubleshooting 275 | 276 | If you can't find a solution to your problem, please [open an issue](https://github.com/Azure-Samples/llama-index-azure-code-interpreter/issues) in this repository. 277 | 278 | ## Contributing 279 | 280 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 281 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 282 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 283 | 284 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 285 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 286 | provided by the bot. You will only need to do this once across all repos using our CLA. 287 | 288 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 289 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 290 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 291 | 292 | ## Trademarks 293 | 294 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 295 | trademarks or logos is subject to and must follow 296 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). 297 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 298 | Any use of third-party trademarks or logos are subject to those third-party's policies. 299 | -------------------------------------------------------------------------------- /app/api/chat/config/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | /** 4 | * This API is to get config from the backend envs and expose them to the frontend 5 | */ 6 | export async function GET() { 7 | const config = { 8 | starterQuestions: process.env.CONVERSATION_STARTERS?.trim().split("\n"), 9 | }; 10 | return NextResponse.json(config, { status: 200 }); 11 | } 12 | -------------------------------------------------------------------------------- /app/api/chat/engine/chat.ts: -------------------------------------------------------------------------------- 1 | import { BaseToolWithCall, QueryEngineTool, ReActAgent } from "llamaindex"; 2 | import fs from "node:fs/promises"; 3 | import path from "node:path"; 4 | import { getDataSource } from "./index"; 5 | import { generateFilters } from "./queryFilter"; 6 | import { createTools } from "./tools"; 7 | 8 | export async function createChatEngine(documentIds?: string[], params?: any) { 9 | const tools: BaseToolWithCall[] = []; 10 | 11 | // Add a query engine tool if we have a data source 12 | // Delete this code if you don't have a data source 13 | const index = await getDataSource(params); 14 | if (index) { 15 | tools.push( 16 | new QueryEngineTool({ 17 | queryEngine: index.asQueryEngine({ 18 | preFilters: generateFilters(documentIds || []), 19 | }), 20 | metadata: { 21 | name: "data_query_engine", 22 | description: `A query engine for documents from your data source.`, 23 | }, 24 | }), 25 | ); 26 | } 27 | 28 | const configFile = path.join("config", "tools.json"); 29 | let toolConfig: any = {}; 30 | try { 31 | // add tools from config file if it exists 32 | toolConfig = JSON.parse(await fs.readFile(configFile, "utf8")); 33 | } catch (e) { 34 | console.info(`Could not read ${configFile} file. Using no tools.`); 35 | } 36 | if (toolConfig) { 37 | tools.push(...(await createTools(toolConfig))); 38 | } 39 | 40 | return new ReActAgent({ 41 | tools, 42 | systemPrompt: process.env.SYSTEM_PROMPT, 43 | verbose: true, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /app/api/chat/engine/file-storage.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | import { DefaultAzureCredential } from "@azure/identity"; 5 | import { BlobServiceClient, BlockBlobUploadHeaders } from "@azure/storage-blob"; 6 | import { getEnv } from "@llamaindex/env"; 7 | 8 | export async function uploadFileToAzureStorage( 9 | fileName: string, 10 | fileContentsAsBuffer: Buffer, 11 | ): Promise { 12 | const accountName = getEnv("AZURE_STORAGE_ACCOUNT") ?? ""; 13 | 14 | if (!accountName) { 15 | throw new Error("AZURE_STORAGE_ACCOUNT must be defined."); 16 | } 17 | 18 | const blobServiceClient = new BlobServiceClient( 19 | `https://${accountName}.blob.core.windows.net`, 20 | new DefaultAzureCredential(), 21 | ); 22 | 23 | const containerName = getEnv("AZURE_STORAGE_CONTAINER") || "files"; 24 | 25 | const containerClient = blobServiceClient.getContainerClient(containerName); 26 | const blobClient = containerClient.getBlockBlobClient(fileName); 27 | 28 | // Get file url - available before contents are uploaded 29 | console.log(`blob.url: ${blobServiceClient.url}`); 30 | 31 | // Upload file contents 32 | const result: BlockBlobUploadHeaders = 33 | await blobClient.uploadData(fileContentsAsBuffer); 34 | 35 | if (result.errorCode) throw Error(result.errorCode); 36 | 37 | // Get results 38 | return `${blobServiceClient.url}${containerName}/${fileName}`; 39 | } 40 | -------------------------------------------------------------------------------- /app/api/chat/engine/generate.ts: -------------------------------------------------------------------------------- 1 | import { VectorStoreIndex } from "llamaindex"; 2 | import { storageContextFromDefaults } from "llamaindex/storage/StorageContext"; 3 | 4 | import * as dotenv from "dotenv"; 5 | 6 | import { getDocuments } from "./loader"; 7 | import { initSettings } from "./settings"; 8 | import { STORAGE_CACHE_DIR } from "./shared"; 9 | 10 | // Load environment variables from local .env file 11 | dotenv.config(); 12 | 13 | async function getRuntime(func: any) { 14 | const start = Date.now(); 15 | await func(); 16 | const end = Date.now(); 17 | return end - start; 18 | } 19 | 20 | async function generateDatasource() { 21 | console.log(`Generating storage context...`); 22 | // Split documents, create embeddings and store them in the storage context 23 | const ms = await getRuntime(async () => { 24 | const storageContext = await storageContextFromDefaults({ 25 | persistDir: STORAGE_CACHE_DIR, 26 | }); 27 | const documents = await getDocuments(); 28 | 29 | await VectorStoreIndex.fromDocuments(documents, { 30 | storageContext, 31 | }); 32 | }); 33 | console.log(`Storage context successfully generated in ${ms / 1000}s.`); 34 | } 35 | 36 | (async () => { 37 | initSettings(); 38 | await generateDatasource(); 39 | console.log("Finished generating storage."); 40 | })(); 41 | -------------------------------------------------------------------------------- /app/api/chat/engine/index.ts: -------------------------------------------------------------------------------- 1 | import { SimpleDocumentStore, VectorStoreIndex } from "llamaindex"; 2 | import { storageContextFromDefaults } from "llamaindex/storage/StorageContext"; 3 | import { STORAGE_CACHE_DIR } from "./shared"; 4 | 5 | export async function getDataSource(params?: any) { 6 | const storageContext = await storageContextFromDefaults({ 7 | persistDir: `${STORAGE_CACHE_DIR}`, 8 | }); 9 | 10 | const numberOfDocs = Object.keys( 11 | (storageContext.docStore as SimpleDocumentStore).toDict(), 12 | ).length; 13 | if (numberOfDocs === 0) { 14 | return null; 15 | } 16 | return await VectorStoreIndex.init({ 17 | storageContext, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /app/api/chat/engine/loader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FILE_EXT_TO_READER, 3 | SimpleDirectoryReader, 4 | } from "llamaindex/readers/SimpleDirectoryReader"; 5 | 6 | export const DATA_DIR = "./data"; 7 | 8 | export function getExtractors() { 9 | return FILE_EXT_TO_READER; 10 | } 11 | 12 | export async function getDocuments() { 13 | const documents = await new SimpleDirectoryReader().loadData({ 14 | directoryPath: DATA_DIR, 15 | }); 16 | // Set private=false to mark the document as public (required for filtering) 17 | for (const document of documents) { 18 | document.metadata = { 19 | ...document.metadata, 20 | private: "false", 21 | }; 22 | } 23 | return documents; 24 | } 25 | -------------------------------------------------------------------------------- /app/api/chat/engine/queryFilter.ts: -------------------------------------------------------------------------------- 1 | import { MetadataFilter, MetadataFilters } from "llamaindex"; 2 | 3 | export function generateFilters(documentIds: string[]): MetadataFilters { 4 | // filter all documents have the private metadata key set to true 5 | const publicDocumentsFilter: MetadataFilter = { 6 | key: "private", 7 | value: "true", 8 | operator: "!=", 9 | }; 10 | 11 | // if no documentIds are provided, only retrieve information from public documents 12 | if (!documentIds.length) return { filters: [publicDocumentsFilter] }; 13 | 14 | const privateDocumentsFilter: MetadataFilter = { 15 | key: "doc_id", 16 | value: documentIds, 17 | operator: "in", 18 | }; 19 | 20 | // if documentIds are provided, retrieve information from public and private documents 21 | return { 22 | filters: [publicDocumentsFilter, privateDocumentsFilter], 23 | condition: "or", 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/api/chat/engine/settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultAzureCredential, 3 | getBearerTokenProvider, 4 | } from "@azure/identity"; 5 | import { OpenAI, OpenAIEmbedding, Settings } from "llamaindex"; 6 | 7 | const CHUNK_SIZE = 512; 8 | const CHUNK_OVERLAP = 20; 9 | const AZURE_AD_SCOPE = "https://cognitiveservices.azure.com/.default"; 10 | 11 | export const initSettings = async () => { 12 | initAzureOpenAI(); 13 | 14 | Settings.chunkSize = CHUNK_SIZE; 15 | Settings.chunkOverlap = CHUNK_OVERLAP; 16 | }; 17 | 18 | function initAzureOpenAI() { 19 | const credential = new DefaultAzureCredential(); 20 | const azureADTokenProvider = getBearerTokenProvider( 21 | credential, 22 | AZURE_AD_SCOPE, 23 | ); 24 | 25 | const azure = { 26 | apiVersion: "2023-03-15-preview", 27 | azureADTokenProvider, 28 | deployment: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-4o-mini", 29 | }; 30 | 31 | Settings.llm = new OpenAI({azure}); 32 | Settings.embedModel = new OpenAIEmbedding({ 33 | azure, 34 | model: process.env.EMBEDDING_MODEL, 35 | dimensions: process.env.EMBEDDING_DIM 36 | ? parseInt(process.env.EMBEDDING_DIM) 37 | : undefined, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /app/api/chat/engine/shared.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_CACHE_DIR = "./cache"; 2 | -------------------------------------------------------------------------------- /app/api/chat/engine/tools/__azure-code-interpreter.ts: -------------------------------------------------------------------------------- 1 | import {AzureDynamicSessionTool as AzureDynamicSessionToolPython } from "./__AzureDynamicSessionTool.node--patched"; 2 | export class AzureDynamicSessionTool extends AzureDynamicSessionToolPython { 3 | constructor(config?: any) { 4 | super(config); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/api/chat/engine/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseToolWithCall } from "llamaindex"; 2 | import { AzureDynamicSessionTool } from "./__azure-code-interpreter"; 3 | 4 | type ToolCreator = (config: unknown) => Promise; 5 | 6 | export async function createTools(toolConfig: { 7 | local: Record; 8 | llamahub: any; 9 | }): Promise { 10 | // add local tools from the 'tools' folder (if configured) 11 | return await createLocalTools(toolConfig.local); 12 | } 13 | 14 | const toolFactory: Record = { 15 | interpreter: async (config: unknown) => { 16 | return [new AzureDynamicSessionTool(config as any)]; 17 | } 18 | }; 19 | 20 | async function createLocalTools( 21 | localConfig: Record, 22 | ): Promise { 23 | const tools: BaseToolWithCall[] = []; 24 | 25 | for (const [key, toolConfig] of Object.entries(localConfig)) { 26 | if (key in toolFactory) { 27 | const newTools = await toolFactory[key](toolConfig); 28 | tools.push(...newTools); 29 | } 30 | } 31 | 32 | return tools; 33 | } 34 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/documents/helper.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { getExtractors } from "../../engine/loader"; 3 | 4 | const MIME_TYPE_TO_EXT: Record = { 5 | "application/pdf": "pdf", 6 | "text/plain": "txt", 7 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document": 8 | "docx", 9 | }; 10 | 11 | const UPLOADED_FOLDER = "output/uploaded"; 12 | 13 | export async function storeAndParseFile( 14 | filename: string, 15 | fileBuffer: Buffer, 16 | mimeType: string, 17 | ) { 18 | const documents = await loadDocuments(fileBuffer, mimeType); 19 | await saveDocument(filename, fileBuffer, mimeType); 20 | for (const document of documents) { 21 | document.metadata = { 22 | ...document.metadata, 23 | file_name: filename, 24 | private: "true", // to separate private uploads from public documents 25 | }; 26 | } 27 | return documents; 28 | } 29 | 30 | async function loadDocuments(fileBuffer: Buffer, mimeType: string) { 31 | const extractors = getExtractors(); 32 | const reader = extractors[MIME_TYPE_TO_EXT[mimeType]]; 33 | 34 | if (!reader) { 35 | throw new Error(`Unsupported document type: ${mimeType}`); 36 | } 37 | console.log(`Processing uploaded document of type: ${mimeType}`); 38 | return await reader.loadDataAsContent(fileBuffer); 39 | } 40 | 41 | async function saveDocument( 42 | filename: string, 43 | fileBuffer: Buffer, 44 | mimeType: string, 45 | ) { 46 | const fileExt = MIME_TYPE_TO_EXT[mimeType]; 47 | if (!fileExt) throw new Error(`Unsupported document type: ${mimeType}`); 48 | 49 | const filepath = `${UPLOADED_FOLDER}/${filename}`; 50 | const fileurl = `${process.env.FILESERVER_URL_PREFIX}/${filepath}`; 51 | 52 | if (!fs.existsSync(UPLOADED_FOLDER)) { 53 | fs.mkdirSync(UPLOADED_FOLDER, { recursive: true }); 54 | } 55 | await fs.promises.writeFile(filepath, fileBuffer); 56 | 57 | console.log(`Saved document file to ${filepath}.\nURL: ${fileurl}`); 58 | return { 59 | filename, 60 | filepath, 61 | fileurl, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/documents/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Document, 3 | IngestionPipeline, 4 | Settings, 5 | SimpleNodeParser, 6 | VectorStoreIndex, 7 | } from "llamaindex"; 8 | 9 | export async function runPipeline( 10 | currentIndex: VectorStoreIndex, 11 | documents: Document[], 12 | ) { 13 | // Use ingestion pipeline to process the documents into nodes and add them to the vector store 14 | const pipeline = new IngestionPipeline({ 15 | transformations: [ 16 | new SimpleNodeParser({ 17 | chunkSize: Settings.chunkSize, 18 | chunkOverlap: Settings.chunkOverlap, 19 | }), 20 | Settings.embedModel, 21 | ], 22 | }); 23 | const nodes = await pipeline.run({ documents }); 24 | await currentIndex.insertNodes(nodes); 25 | currentIndex.storageContext.docStore.persist(); 26 | console.log("Added nodes to the vector store."); 27 | return documents.map((document) => document.id_); 28 | } 29 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/documents/upload.ts: -------------------------------------------------------------------------------- 1 | import { VectorStoreIndex } from "llamaindex"; 2 | import { storeAndParseFile } from "./helper"; 3 | import { runPipeline } from "./pipeline"; 4 | 5 | export async function uploadDocument( 6 | index: VectorStoreIndex, 7 | filename: string, 8 | raw: string, 9 | ): Promise { 10 | const [header, content] = raw.split(","); 11 | const mimeType = header.replace("data:", "").replace(";base64", ""); 12 | const fileBuffer = Buffer.from(content, "base64"); 13 | 14 | // run the pipeline for other vector store indexes 15 | const documents = await storeAndParseFile(filename, fileBuffer, mimeType); 16 | return runPipeline(index, documents); 17 | } 18 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/streaming/annotations.ts: -------------------------------------------------------------------------------- 1 | import { JSONValue } from "ai"; 2 | import { MessageContent, MessageContentDetail } from "llamaindex"; 3 | 4 | export type DocumentFileType = "csv" | "pdf" | "txt" | "docx"; 5 | 6 | export type DocumentFileContent = { 7 | type: "ref" | "text"; 8 | value: string[] | string; 9 | }; 10 | 11 | export type DocumentFile = { 12 | id: string; 13 | filename: string; 14 | filesize: number; 15 | filetype: DocumentFileType; 16 | content: DocumentFileContent; 17 | }; 18 | 19 | type Annotation = { 20 | type: string; 21 | data: object; 22 | }; 23 | 24 | export function retrieveDocumentIds(annotations?: JSONValue[]): string[] { 25 | if (!annotations) return []; 26 | 27 | const ids: string[] = []; 28 | 29 | for (const annotation of annotations) { 30 | const { type, data } = getValidAnnotation(annotation); 31 | if ( 32 | type === "document_file" && 33 | "files" in data && 34 | Array.isArray(data.files) 35 | ) { 36 | const files = data.files as DocumentFile[]; 37 | for (const file of files) { 38 | if (Array.isArray(file.content.value)) { 39 | // it's an array, so it's an array of doc IDs 40 | for (const id of file.content.value) { 41 | ids.push(id); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | return ids; 49 | } 50 | 51 | export function convertMessageContent( 52 | content: string, 53 | annotations?: JSONValue[], 54 | ): MessageContent { 55 | if (!annotations) return content; 56 | return [ 57 | { 58 | type: "text", 59 | text: content, 60 | }, 61 | ...convertAnnotations(annotations), 62 | ]; 63 | } 64 | 65 | function convertAnnotations(annotations: JSONValue[]): MessageContentDetail[] { 66 | const content: MessageContentDetail[] = []; 67 | annotations.forEach((annotation: JSONValue) => { 68 | const { type, data } = getValidAnnotation(annotation); 69 | // convert image 70 | if (type === "image" && "url" in data && typeof data.url === "string") { 71 | content.push({ 72 | type: "image_url", 73 | image_url: { 74 | url: data.url, 75 | }, 76 | }); 77 | } 78 | // convert the content of files to a text message 79 | if ( 80 | type === "document_file" && 81 | "files" in data && 82 | Array.isArray(data.files) 83 | ) { 84 | // get all CSV files and convert their whole content to one text message 85 | // currently CSV files are the only files where we send the whole content - we don't use an index 86 | const csvFiles: DocumentFile[] = data.files.filter( 87 | (file: DocumentFile) => file.filetype === "csv", 88 | ); 89 | if (csvFiles && csvFiles.length > 0) { 90 | const csvContents = csvFiles.map((file: DocumentFile) => { 91 | const fileContent = Array.isArray(file.content.value) 92 | ? file.content.value.join("\n") 93 | : file.content.value; 94 | return "```csv\n" + fileContent + "\n```"; 95 | }); 96 | const text = 97 | "Use the following CSV content:\n" + csvContents.join("\n\n"); 98 | content.push({ 99 | type: "text", 100 | text, 101 | }); 102 | } 103 | } 104 | }); 105 | 106 | return content; 107 | } 108 | 109 | function getValidAnnotation(annotation: JSONValue): Annotation { 110 | if ( 111 | !( 112 | annotation && 113 | typeof annotation === "object" && 114 | "type" in annotation && 115 | typeof annotation.type === "string" && 116 | "data" in annotation && 117 | annotation.data && 118 | typeof annotation.data === "object" 119 | ) 120 | ) { 121 | throw new Error("Client sent invalid annotation. Missing data and type"); 122 | } 123 | return { type: annotation.type, data: annotation.data }; 124 | } 125 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/streaming/events.ts: -------------------------------------------------------------------------------- 1 | import { StreamData } from "ai"; 2 | import { 3 | CallbackManager, 4 | LLamaCloudFileService, 5 | Metadata, 6 | MetadataMode, 7 | NodeWithScore, 8 | ToolCall, 9 | ToolOutput, 10 | } from "llamaindex"; 11 | import path from "node:path"; 12 | import { DATA_DIR } from "../../engine/loader"; 13 | import { downloadFile } from "./file"; 14 | 15 | const LLAMA_CLOUD_DOWNLOAD_FOLDER = "output/llamacloud"; 16 | 17 | export function appendSourceData( 18 | data: StreamData, 19 | sourceNodes?: NodeWithScore[], 20 | ) { 21 | if (!sourceNodes?.length) return; 22 | try { 23 | const nodes = sourceNodes.map((node) => ({ 24 | metadata: node.node.metadata, 25 | id: node.node.id_, 26 | score: node.score ?? null, 27 | url: getNodeUrl(node.node.metadata), 28 | text: node.node.getContent(MetadataMode.NONE), 29 | })); 30 | data.appendMessageAnnotation({ 31 | type: "sources", 32 | data: { 33 | nodes, 34 | }, 35 | }); 36 | } catch (error) { 37 | console.error("Error appending source data:", error); 38 | } 39 | } 40 | 41 | export function appendEventData(data: StreamData, title?: string) { 42 | if (!title) return; 43 | data.appendMessageAnnotation({ 44 | type: "events", 45 | data: { 46 | title, 47 | }, 48 | }); 49 | } 50 | 51 | export function appendToolData( 52 | data: StreamData, 53 | toolCall: ToolCall, 54 | toolOutput: ToolOutput, 55 | ) { 56 | data.appendMessageAnnotation({ 57 | type: "tools", 58 | data: { 59 | toolCall: { 60 | id: toolCall.id, 61 | name: toolCall.name, 62 | input: toolCall.input, 63 | }, 64 | toolOutput: { 65 | output: toolOutput.output, 66 | isError: toolOutput.isError, 67 | }, 68 | }, 69 | }); 70 | } 71 | 72 | export function createStreamTimeout(stream: StreamData) { 73 | const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes 74 | const t = setTimeout(() => { 75 | appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`); 76 | stream.close(); 77 | }, timeout); 78 | return t; 79 | } 80 | 81 | export function createCallbackManager(stream: StreamData) { 82 | const callbackManager = new CallbackManager(); 83 | 84 | callbackManager.on("retrieve-end", (data) => { 85 | const { nodes, query } = data.detail; 86 | appendSourceData(stream, nodes); 87 | appendEventData(stream, `Retrieving context for query: '${query}'`); 88 | appendEventData( 89 | stream, 90 | `Retrieved ${nodes.length} sources to use as context for the query`, 91 | ); 92 | downloadFilesFromNodes(nodes); // don't await to avoid blocking chat streaming 93 | }); 94 | 95 | callbackManager.on("llm-tool-call", (event) => { 96 | const { name, input } = event.detail.toolCall; 97 | const inputString = Object.entries(input) 98 | .map(([key, value]) => `${key}: ${value}`) 99 | .join(", "); 100 | appendEventData( 101 | stream, 102 | `Using tool: '${name}' with inputs: '${inputString}'`, 103 | ); 104 | }); 105 | 106 | callbackManager.on("llm-tool-result", (event) => { 107 | const { toolCall, toolResult } = event.detail; 108 | appendToolData(stream, toolCall, toolResult); 109 | }); 110 | 111 | return callbackManager; 112 | } 113 | 114 | function getNodeUrl(metadata: Metadata) { 115 | if (!process.env.FILESERVER_URL_PREFIX) { 116 | console.warn( 117 | "FILESERVER_URL_PREFIX is not set. File URLs will not be generated.", 118 | ); 119 | } 120 | const fileName = metadata["file_name"]; 121 | if (fileName && process.env.FILESERVER_URL_PREFIX) { 122 | // file_name exists and file server is configured 123 | const pipelineId = metadata["pipeline_id"]; 124 | if (pipelineId) { 125 | const name = toDownloadedName(pipelineId, fileName); 126 | return `${process.env.FILESERVER_URL_PREFIX}/${LLAMA_CLOUD_DOWNLOAD_FOLDER}/${name}`; 127 | } 128 | const isPrivate = metadata["private"] === "true"; 129 | if (isPrivate) { 130 | return `${process.env.FILESERVER_URL_PREFIX}/output/uploaded/${fileName}`; 131 | } 132 | const filePath = metadata["file_path"]; 133 | const dataDir = path.resolve(DATA_DIR); 134 | 135 | if (filePath && dataDir) { 136 | const relativePath = path.relative(dataDir, filePath); 137 | return `${process.env.FILESERVER_URL_PREFIX}/data/${relativePath}`; 138 | } 139 | } 140 | // fallback to URL in metadata (e.g. for websites) 141 | return metadata["URL"]; 142 | } 143 | 144 | async function downloadFilesFromNodes(nodes: NodeWithScore[]) { 145 | try { 146 | const files = nodesToLlamaCloudFiles(nodes); 147 | for (const { pipelineId, fileName, downloadedName } of files) { 148 | const downloadUrl = await LLamaCloudFileService.getFileUrl( 149 | pipelineId, 150 | fileName, 151 | ); 152 | if (downloadUrl) { 153 | await downloadFile( 154 | downloadUrl, 155 | downloadedName, 156 | LLAMA_CLOUD_DOWNLOAD_FOLDER, 157 | ); 158 | } 159 | } 160 | } catch (error) { 161 | console.error("Error downloading files from nodes:", error); 162 | } 163 | } 164 | 165 | function nodesToLlamaCloudFiles(nodes: NodeWithScore[]) { 166 | const files: Array<{ 167 | pipelineId: string; 168 | fileName: string; 169 | downloadedName: string; 170 | }> = []; 171 | for (const node of nodes) { 172 | const pipelineId = node.node.metadata["pipeline_id"]; 173 | const fileName = node.node.metadata["file_name"]; 174 | if (!pipelineId || !fileName) continue; 175 | const isDuplicate = files.some( 176 | (f) => f.pipelineId === pipelineId && f.fileName === fileName, 177 | ); 178 | if (!isDuplicate) { 179 | files.push({ 180 | pipelineId, 181 | fileName, 182 | downloadedName: toDownloadedName(pipelineId, fileName), 183 | }); 184 | } 185 | } 186 | return files; 187 | } 188 | 189 | function toDownloadedName(pipelineId: string, fileName: string) { 190 | return `${pipelineId}$${fileName}`; 191 | } 192 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/streaming/file.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import https from "node:https"; 3 | import path from "node:path"; 4 | 5 | export async function downloadFile( 6 | urlToDownload: string, 7 | filename: string, 8 | folder = "output/uploaded", 9 | ) { 10 | try { 11 | const downloadedPath = path.join(folder, filename); 12 | 13 | // Check if file already exists 14 | if (fs.existsSync(downloadedPath)) return; 15 | 16 | const file = fs.createWriteStream(downloadedPath); 17 | https 18 | .get(urlToDownload, (response) => { 19 | response.pipe(file); 20 | file.on("finish", () => { 21 | file.close(() => { 22 | console.log("File downloaded successfully"); 23 | }); 24 | }); 25 | }) 26 | .on("error", (err) => { 27 | fs.unlink(downloadedPath, () => { 28 | console.error("Error downloading file:", err); 29 | throw err; 30 | }); 31 | }); 32 | } catch (error) { 33 | throw new Error(`Error downloading file: ${error}`); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/streaming/stream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StreamData, 3 | createCallbacksTransformer, 4 | createStreamDataTransformer, 5 | trimStartOfStreamHelper, 6 | type AIStreamCallbacksAndOptions, 7 | } from "ai"; 8 | import { ChatMessage, EngineResponse } from "llamaindex"; 9 | import { generateNextQuestions } from "./suggestion"; 10 | 11 | export function LlamaIndexStream( 12 | response: ReadableStream, 13 | data: StreamData, 14 | chatHistory: ChatMessage[], 15 | opts?: { 16 | callbacks?: AIStreamCallbacksAndOptions; 17 | }, 18 | ): ReadableStream { 19 | return createParser(response, data, chatHistory) 20 | .pipeThrough(createCallbacksTransformer(opts?.callbacks)) 21 | .pipeThrough(createStreamDataTransformer()); 22 | } 23 | 24 | function createParser( 25 | res: ReadableStream, 26 | data: StreamData, 27 | chatHistory: ChatMessage[], 28 | ) { 29 | const it = res.getReader(); 30 | const trimStartOfStream = trimStartOfStreamHelper(); 31 | let llmTextResponse = ""; 32 | 33 | return new ReadableStream({ 34 | async pull(controller): Promise { 35 | const { value, done } = await it.read(); 36 | if (done) { 37 | controller.close(); 38 | // LLM stream is done, generate the next questions with a new LLM call 39 | chatHistory.push({ role: "assistant", content: llmTextResponse }); 40 | const questions: string[] = await generateNextQuestions(chatHistory); 41 | if (questions.length > 0) { 42 | data.appendMessageAnnotation({ 43 | type: "suggested_questions", 44 | data: questions, 45 | }); 46 | } 47 | data.close(); 48 | return; 49 | } 50 | const text = trimStartOfStream(value.delta ?? ""); 51 | if (text) { 52 | llmTextResponse += text; 53 | controller.enqueue(text); 54 | } 55 | }, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /app/api/chat/llamaindex/streaming/suggestion.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage, Settings } from "llamaindex"; 2 | 3 | export async function generateNextQuestions(conversation: ChatMessage[]) { 4 | const llm = Settings.llm; 5 | const NEXT_QUESTION_PROMPT = process.env.NEXT_QUESTION_PROMPT; 6 | if (!NEXT_QUESTION_PROMPT) { 7 | return []; 8 | } 9 | 10 | // Format conversation 11 | const conversationText = conversation 12 | .map((message) => `${message.role}: ${message.content}`) 13 | .join("\n"); 14 | const message = NEXT_QUESTION_PROMPT.replace( 15 | "{conversation}", 16 | conversationText, 17 | ); 18 | 19 | try { 20 | const response = await llm.complete({ prompt: message }); 21 | const questions = extractQuestions(response.text); 22 | return questions; 23 | } catch (error) { 24 | console.error("Error when generating the next questions: ", error); 25 | return []; 26 | } 27 | } 28 | 29 | // TODO: instead of parsing the LLM's result we can use structured predict, once LITS supports it 30 | function extractQuestions(text: string): string[] { 31 | // Extract the text inside the triple backticks 32 | // @ts-ignore 33 | const contentMatch = text.match(/```(.*?)```/s); 34 | const content = contentMatch ? contentMatch[1] : ""; 35 | 36 | // Split the content by newlines to get each question 37 | const questions = content 38 | .split("\n") 39 | .map((question) => question.trim()) 40 | .filter((question) => question !== ""); 41 | 42 | return questions; 43 | } 44 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { JSONValue, Message, StreamData, StreamingTextResponse } from "ai"; 2 | import { ChatMessage, Settings } from "llamaindex"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { createChatEngine } from "./engine/chat"; 5 | import { initSettings } from "./engine/settings"; 6 | import { 7 | convertMessageContent, 8 | retrieveDocumentIds, 9 | } from "./llamaindex/streaming/annotations"; 10 | import { 11 | createCallbackManager, 12 | createStreamTimeout, 13 | } from "./llamaindex/streaming/events"; 14 | import { LlamaIndexStream } from "./llamaindex/streaming/stream"; 15 | 16 | initSettings(); 17 | 18 | export async function POST(request: NextRequest) { 19 | // Init Vercel AI StreamData and timeout 20 | const vercelStreamData = new StreamData(); 21 | const streamTimeout = createStreamTimeout(vercelStreamData); 22 | 23 | try { 24 | const body = await request.json(); 25 | const { messages, data }: { messages: Message[]; data?: any } = body; 26 | const userMessage = messages.pop(); 27 | if (!messages || !userMessage || userMessage.role !== "user") { 28 | return NextResponse.json( 29 | { 30 | error: 31 | "messages are required in the request body and the last message must be from the user", 32 | }, 33 | { status: 400 }, 34 | ); 35 | } 36 | 37 | let annotations = userMessage.annotations; 38 | if (!annotations) { 39 | // the user didn't send any new annotations with the last message 40 | // so use the annotations from the last user message that has annotations 41 | // REASON: GPT4 doesn't consider MessageContentDetail from previous messages, only strings 42 | annotations = messages 43 | .slice() 44 | .reverse() 45 | .find( 46 | (message) => message.role === "user" && message.annotations, 47 | )?.annotations; 48 | } 49 | 50 | // retrieve document Ids from the annotations of all messages (if any) and create chat engine with index 51 | const allAnnotations: JSONValue[] = [...messages, userMessage].flatMap( 52 | (message) => { 53 | return message.annotations ?? []; 54 | }, 55 | ); 56 | const ids = retrieveDocumentIds(allAnnotations); 57 | const chatEngine = await createChatEngine(ids, data); 58 | 59 | // Convert message content from Vercel/AI format to LlamaIndex/OpenAI format 60 | const userMessageContent = convertMessageContent( 61 | userMessage.content, 62 | annotations, 63 | ); 64 | 65 | // Setup callbacks 66 | const callbackManager = createCallbackManager(vercelStreamData); 67 | 68 | // Calling LlamaIndex's ChatEngine to get a streamed response 69 | const response = await Settings.withCallbackManager(callbackManager, () => { 70 | return chatEngine.chat({ 71 | message: userMessageContent, 72 | chatHistory: messages as ChatMessage[], 73 | stream: true, 74 | }); 75 | }); 76 | 77 | // Transform LlamaIndex stream to Vercel/AI format 78 | const stream = LlamaIndexStream( 79 | response, 80 | vercelStreamData, 81 | messages as ChatMessage[], 82 | ); 83 | 84 | // Return a StreamingTextResponse, which can be consumed by the Vercel/AI client 85 | return new StreamingTextResponse(stream, {}, vercelStreamData); 86 | } catch (error) { 87 | console.error("[LlamaIndex]", error); 88 | return NextResponse.json( 89 | { 90 | detail: (error as Error).message, 91 | }, 92 | { 93 | status: 500, 94 | }, 95 | ); 96 | } finally { 97 | clearTimeout(streamTimeout); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/api/chat/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getDataSource } from "../engine"; 3 | import { initSettings } from "../engine/settings"; 4 | import { uploadDocument } from "../llamaindex/documents/upload"; 5 | import { AzureDynamicSessionTool } from "../engine/tools/__azure-code-interpreter"; 6 | 7 | initSettings(); 8 | 9 | export const runtime = "nodejs"; 10 | export const dynamic = "force-dynamic"; 11 | 12 | export async function POST(request: NextRequest) { 13 | try { 14 | const { 15 | filename, 16 | base64, 17 | params, 18 | }: { filename: string; base64: string; params?: any } = 19 | await request.json(); 20 | if (!base64 || !filename) { 21 | return NextResponse.json( 22 | { error: "base64 and filename is required in the request body" }, 23 | { status: 400 }, 24 | ); 25 | } 26 | 27 | console.log("[Upload API] Uploading document", {params}); 28 | 29 | const index = await getDataSource(params); 30 | if (index) { 31 | // throw new Error( 32 | // `StorageContext is empty - call 'npm run generate' to generate the storage first`, 33 | // ); 34 | return NextResponse.json(await uploadDocument(index, filename, base64)); 35 | } 36 | else { 37 | return NextResponse.json(new AzureDynamicSessionTool().uploadFile({ 38 | remoteFilename: filename, 39 | base64, 40 | })); 41 | } 42 | } catch (error) { 43 | console.error("[Upload API]", error); 44 | return NextResponse.json( 45 | { error: (error as Error).message }, 46 | { status: 500 }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/api/files/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import path from "path"; 4 | import { DATA_DIR } from "../../chat/engine/loader"; 5 | 6 | /** 7 | * This API is to get file data from allowed folders 8 | * It receives path slug and response file data like serve static file 9 | */ 10 | export async function GET( 11 | _request: NextRequest, 12 | { params }: { params: { slug: string[] } }, 13 | ) { 14 | const slug = params.slug; 15 | 16 | if (!slug) { 17 | return NextResponse.json({ detail: "Missing file slug" }, { status: 400 }); 18 | } 19 | 20 | if (slug.includes("..") || path.isAbsolute(path.join(...slug))) { 21 | return NextResponse.json({ detail: "Invalid file path" }, { status: 400 }); 22 | } 23 | 24 | const [folder, ...pathTofile] = params.slug; // data, file.pdf 25 | const allowedFolders = ["data", "output"]; 26 | 27 | if (!allowedFolders.includes(folder)) { 28 | return NextResponse.json({ detail: "No permission" }, { status: 400 }); 29 | } 30 | 31 | try { 32 | const filePath = path.join( 33 | process.cwd(), 34 | folder === "data" ? DATA_DIR : folder, 35 | path.join(...pathTofile), 36 | ); 37 | const blob = await readFile(filePath); 38 | 39 | return new NextResponse(blob, { 40 | status: 200, 41 | statusText: "OK", 42 | headers: { 43 | "Content-Length": blob.byteLength.toString(), 44 | }, 45 | }); 46 | } catch (error) { 47 | console.error(error); 48 | return NextResponse.json({ detail: "File not found" }, { status: 404 }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/components/chat-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useChat } from "ai/react"; 4 | import { useState } from "react"; 5 | import { ChatInput, ChatMessages } from "./ui/chat"; 6 | import { useClientConfig } from "./ui/chat/hooks/use-config"; 7 | 8 | export default function ChatSection() { 9 | const { backend } = useClientConfig(); 10 | const [requestData, setRequestData] = useState(); 11 | const { 12 | messages, 13 | input, 14 | isLoading, 15 | handleSubmit, 16 | handleInputChange, 17 | reload, 18 | stop, 19 | append, 20 | setInput, 21 | } = useChat({ 22 | body: { data: requestData }, 23 | api: `${backend}/api/chat`, 24 | headers: { 25 | "Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26 26 | }, 27 | onError: (error: unknown) => { 28 | if (!(error instanceof Error)) throw error; 29 | const message = JSON.parse(error.message); 30 | alert(message.detail); 31 | }, 32 | sendExtraMessageFields: true, 33 | }); 34 | 35 | return ( 36 |
37 | 44 | 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function Header() { 4 | return ( 5 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/ui/README.md: -------------------------------------------------------------------------------- 1 | Using the chat component from https://github.com/marcusschiesser/ui (based on https://ui.shadcn.com/) 2 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "./lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-actions.tsx: -------------------------------------------------------------------------------- 1 | import { PauseCircle, RefreshCw } from "lucide-react"; 2 | 3 | import { Button } from "../button"; 4 | import { ChatHandler } from "./chat.interface"; 5 | 6 | export default function ChatActions( 7 | props: Pick & { 8 | showReload?: boolean; 9 | showStop?: boolean; 10 | }, 11 | ) { 12 | return ( 13 |
14 | {props.showStop && ( 15 | 19 | )} 20 | {props.showReload && ( 21 | 25 | )} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-input.tsx: -------------------------------------------------------------------------------- 1 | import { JSONValue } from "ai"; 2 | import { Button } from "../button"; 3 | import { DocumentPreview } from "../document-preview"; 4 | import FileUploader from "../file-uploader"; 5 | import { Input } from "../input"; 6 | import UploadImagePreview from "../upload-image-preview"; 7 | import { ChatHandler } from "./chat.interface"; 8 | import { useFile } from "./hooks/use-file"; 9 | 10 | const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"]; 11 | 12 | export default function ChatInput( 13 | props: Pick< 14 | ChatHandler, 15 | | "isLoading" 16 | | "input" 17 | | "onFileUpload" 18 | | "onFileError" 19 | | "handleSubmit" 20 | | "handleInputChange" 21 | | "messages" 22 | | "setInput" 23 | | "append" 24 | > & { 25 | requestParams?: any; 26 | setRequestData?: React.Dispatch; 27 | }, 28 | ) { 29 | const { 30 | imageUrl, 31 | setImageUrl, 32 | uploadFile, 33 | files, 34 | removeDoc, 35 | reset, 36 | getAnnotations, 37 | } = useFile(); 38 | 39 | // default submit function does not handle including annotations in the message 40 | // so we need to use append function to submit new message with annotations 41 | const handleSubmitWithAnnotations = ( 42 | e: React.FormEvent, 43 | annotations: JSONValue[] | undefined, 44 | ) => { 45 | e.preventDefault(); 46 | props.append!({ 47 | content: props.input, 48 | role: "user", 49 | createdAt: new Date(), 50 | annotations, 51 | }); 52 | props.setInput!(""); 53 | }; 54 | 55 | const onSubmit = (e: React.FormEvent) => { 56 | const annotations = getAnnotations(); 57 | if (annotations.length) { 58 | handleSubmitWithAnnotations(e, annotations); 59 | return reset(); 60 | } 61 | props.handleSubmit(e); 62 | }; 63 | 64 | const handleUploadFile = async (file: File) => { 65 | if (imageUrl || files.length > 0) { 66 | alert("You can only upload one file at a time."); 67 | return; 68 | } 69 | try { 70 | await uploadFile(file, props.requestParams); 71 | props.onFileUpload?.(file); 72 | } catch (error: any) { 73 | const onFileUploadError = props.onFileError || window.alert; 74 | onFileUploadError(error.message); 75 | } 76 | }; 77 | 78 | return ( 79 |
83 | {imageUrl && ( 84 | setImageUrl(null)} /> 85 | )} 86 | {files.length > 0 && ( 87 |
88 | {files.map((file) => ( 89 | removeDoc(file)} 93 | /> 94 | ))} 95 |
96 | )} 97 |
98 | 106 | 114 | 117 |
118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-agent-events.tsx: -------------------------------------------------------------------------------- 1 | import { icons, LucideIcon } from "lucide-react"; 2 | import { useMemo } from "react"; 3 | import { Button } from "../../button"; 4 | import { 5 | Drawer, 6 | DrawerClose, 7 | DrawerContent, 8 | DrawerHeader, 9 | DrawerTitle, 10 | DrawerTrigger, 11 | } from "../../drawer"; 12 | import { AgentEventData } from "../index"; 13 | import Markdown from "./markdown"; 14 | 15 | const AgentIcons: Record = { 16 | bot: icons.Bot, 17 | researcher: icons.ScanSearch, 18 | writer: icons.PenLine, 19 | reviewer: icons.MessageCircle, 20 | }; 21 | 22 | type MergedEvent = { 23 | agent: string; 24 | texts: string[]; 25 | icon: LucideIcon; 26 | }; 27 | 28 | export function ChatAgentEvents({ 29 | data, 30 | isFinished, 31 | }: { 32 | data: AgentEventData[]; 33 | isFinished: boolean; 34 | }) { 35 | const events = useMemo(() => mergeAdjacentEvents(data), [data]); 36 | return ( 37 |
38 |
39 | {events.map((eventItem, index) => ( 40 | 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | 52 | const MAX_TEXT_LENGTH = 150; 53 | 54 | function AgentEventContent({ 55 | event, 56 | isLast, 57 | isFinished, 58 | }: { 59 | event: MergedEvent; 60 | isLast: boolean; 61 | isFinished: boolean; 62 | }) { 63 | const { agent, texts } = event; 64 | const AgentIcon = event.icon; 65 | return ( 66 |
67 |
68 |
69 | {isLast && !isFinished && ( 70 |
71 | 72 | 73 | 74 | 75 |
76 | )} 77 | 78 |
79 | {agent} 80 |
81 |
    82 | {texts.map((text, index) => ( 83 |
  • 84 | {text.length <= MAX_TEXT_LENGTH && {text}} 85 | {text.length > MAX_TEXT_LENGTH && ( 86 |
    87 | {text.slice(0, MAX_TEXT_LENGTH)}... 88 | 92 | 93 | Show more 94 | 95 | 96 |
    97 | )} 98 |
  • 99 | ))} 100 |
101 |
102 | ); 103 | } 104 | 105 | type AgentEventDialogProps = { 106 | title: string; 107 | content: string; 108 | children: React.ReactNode; 109 | }; 110 | 111 | function AgentEventDialog(props: AgentEventDialogProps) { 112 | return ( 113 | 114 | {props.children} 115 | 116 | 117 |
118 | {props.title} 119 |
120 | 121 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | ); 130 | } 131 | 132 | function mergeAdjacentEvents(events: AgentEventData[]): MergedEvent[] { 133 | const mergedEvents: MergedEvent[] = []; 134 | 135 | for (const event of events) { 136 | const lastMergedEvent = mergedEvents[mergedEvents.length - 1]; 137 | 138 | if (lastMergedEvent && lastMergedEvent.agent === event.agent) { 139 | // If the last event in mergedEvents has the same non-null agent, add the title to it 140 | lastMergedEvent.texts.push(event.text); 141 | } else { 142 | // Otherwise, create a new merged event 143 | mergedEvents.push({ 144 | agent: event.agent, 145 | texts: [event.text], 146 | icon: AgentIcons[event.agent] ?? icons.Bot, 147 | }); 148 | } 149 | } 150 | 151 | return mergedEvents; 152 | } 153 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { User2 } from "lucide-react"; 2 | import Image from "next/image"; 3 | 4 | export default function ChatAvatar({ role }: { role: string }) { 5 | if (role === "user") { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | return ( 14 |
15 | Llama Logo 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-events.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDown, ChevronRight, Loader2 } from "lucide-react"; 2 | import { useState } from "react"; 3 | import { Button } from "../../button"; 4 | import { 5 | Collapsible, 6 | CollapsibleContent, 7 | CollapsibleTrigger, 8 | } from "../../collapsible"; 9 | import { EventData } from "../index"; 10 | 11 | export function ChatEvents({ 12 | data, 13 | isLoading, 14 | }: { 15 | data: EventData[]; 16 | isLoading: boolean; 17 | }) { 18 | const [isOpen, setIsOpen] = useState(false); 19 | 20 | const buttonLabel = isOpen ? "Hide events" : "Show events"; 21 | 22 | const EventIcon = isOpen ? ( 23 | 24 | ) : ( 25 | 26 | ); 27 | 28 | return ( 29 |
30 | 31 | 32 | 37 | 38 | 39 |
40 | {data.map((eventItem, index) => ( 41 |
42 | {eventItem.title} 43 |
44 | ))} 45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-files.tsx: -------------------------------------------------------------------------------- 1 | import { DocumentPreview } from "../../document-preview"; 2 | import { DocumentFileData } from "../index"; 3 | 4 | export function ChatFiles({ data }: { data: DocumentFileData }) { 5 | if (!data.files.length) return null; 6 | return ( 7 |
8 | {data.files.map((file) => ( 9 | 10 | ))} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { type ImageData } from "../index"; 3 | 4 | export function ChatImage({ data }: { data: ImageData }) { 5 | return ( 6 |
7 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-sources.tsx: -------------------------------------------------------------------------------- 1 | import { Check, Copy, FileText } from "lucide-react"; 2 | import Image from "next/image"; 3 | import { useMemo } from "react"; 4 | import { Button } from "../../button"; 5 | import { FileIcon } from "../../document-preview"; 6 | import { 7 | HoverCard, 8 | HoverCardContent, 9 | HoverCardTrigger, 10 | } from "../../hover-card"; 11 | import { cn } from "../../lib/utils"; 12 | import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard"; 13 | import { DocumentFileType, SourceData, SourceNode } from "../index"; 14 | import PdfDialog from "../widgets/PdfDialog"; 15 | 16 | type Document = { 17 | url: string; 18 | sources: SourceNode[]; 19 | }; 20 | 21 | export function ChatSources({ data }: { data: SourceData }) { 22 | const documents: Document[] = useMemo(() => { 23 | // group nodes by document (a document must have a URL) 24 | const nodesByUrl: Record = {}; 25 | data.nodes.forEach((node) => { 26 | const key = node.url; 27 | nodesByUrl[key] ??= []; 28 | nodesByUrl[key].push(node); 29 | }); 30 | 31 | // convert to array of documents 32 | return Object.entries(nodesByUrl).map(([url, sources]) => ({ 33 | url, 34 | sources, 35 | })); 36 | }, [data.nodes]); 37 | 38 | if (documents.length === 0) return null; 39 | 40 | return ( 41 |
42 |
Sources:
43 |
44 | {documents.map((document) => { 45 | return ; 46 | })} 47 |
48 |
49 | ); 50 | } 51 | 52 | export function SourceInfo({ 53 | node, 54 | index, 55 | }: { 56 | node?: SourceNode; 57 | index: number; 58 | }) { 59 | if (!node) return ; 60 | return ( 61 | 62 | { 65 | e.preventDefault(); 66 | e.stopPropagation(); 67 | }} 68 | > 69 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | export function SourceNumberButton({ 82 | index, 83 | className, 84 | }: { 85 | index: number; 86 | className?: string; 87 | }) { 88 | return ( 89 | 95 | {index + 1} 96 | 97 | ); 98 | } 99 | 100 | function DocumentInfo({ document }: { document: Document }) { 101 | if (!document.sources.length) return null; 102 | const { url, sources } = document; 103 | const fileName = sources[0].metadata.file_name as string | undefined; 104 | const fileExt = fileName?.split(".").pop(); 105 | const fileImage = fileExt ? FileIcon[fileExt as DocumentFileType] : null; 106 | 107 | const DocumentDetail = ( 108 |
112 |

119 | {fileName ?? url} 120 |

121 |
122 |
123 | {sources.map((node: SourceNode, index: number) => { 124 | return ( 125 |
126 | 127 |
128 | ); 129 | })} 130 |
131 | {fileImage ? ( 132 |
133 | Icon 139 |
140 | ) : ( 141 | 142 | )} 143 |
144 |
145 | ); 146 | 147 | if (url.endsWith(".pdf")) { 148 | // open internal pdf dialog for pdf files when click document card 149 | return ; 150 | } 151 | // open external link when click document card for other file types 152 | return
window.open(url, "_blank")}>{DocumentDetail}
; 153 | } 154 | 155 | function NodeInfo({ nodeInfo }: { nodeInfo: SourceNode }) { 156 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 }); 157 | 158 | const pageNumber = 159 | // XXX: page_label is used in Python, but page_number is used by Typescript 160 | (nodeInfo.metadata?.page_number as number) ?? 161 | (nodeInfo.metadata?.page_label as number) ?? 162 | null; 163 | 164 | return ( 165 |
166 |
167 | 168 | {pageNumber ? `On page ${pageNumber}:` : "Node content:"} 169 | 170 | {nodeInfo.text && ( 171 | 186 | )} 187 |
188 | 189 | {nodeInfo.text && ( 190 |
191 |           “{nodeInfo.text}”
192 |         
193 | )} 194 |
195 | ); 196 | } 197 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-suggestedQuestions.tsx: -------------------------------------------------------------------------------- 1 | import { ChatHandler, SuggestedQuestionsData } from ".."; 2 | 3 | export function SuggestedQuestions({ 4 | questions, 5 | append, 6 | isLastMessage, 7 | }: { 8 | questions: SuggestedQuestionsData; 9 | append: Pick["append"]; 10 | isLastMessage: boolean; 11 | }) { 12 | const showQuestions = isLastMessage && questions.length > 0; 13 | return ( 14 | showQuestions && 15 | append !== undefined && ( 16 | 29 | ) 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/chat-tools.tsx: -------------------------------------------------------------------------------- 1 | import { ToolData } from "../index"; 2 | import { WeatherCard, WeatherData } from "../widgets/WeatherCard"; 3 | 4 | // TODO: If needed, add displaying more tool outputs here 5 | export default function ChatTools({ data }: { data: ToolData }) { 6 | if (!data) return null; 7 | const { toolCall, toolOutput } = data; 8 | 9 | if (toolOutput.isError) { 10 | return ( 11 |
12 | There was an error when calling the tool {toolCall.name} with input:{" "} 13 |
14 | {JSON.stringify(toolCall.input)} 15 |
16 | ); 17 | } 18 | 19 | switch (toolCall.name) { 20 | case "get_weather_information": 21 | const weatherData = toolOutput.output as unknown as WeatherData; 22 | return ; 23 | default: 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/codeblock.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Check, Copy, Download } from "lucide-react"; 4 | import { FC, memo } from "react"; 5 | import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter"; 6 | import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; 7 | 8 | import { Button } from "../../button"; 9 | import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard"; 10 | 11 | // TODO: Remove this when @type/react-syntax-highlighter is updated 12 | const SyntaxHighlighter = Prism as unknown as FC; 13 | 14 | interface Props { 15 | language: string; 16 | value: string; 17 | } 18 | 19 | interface languageMap { 20 | [key: string]: string | undefined; 21 | } 22 | 23 | export const programmingLanguages: languageMap = { 24 | javascript: ".js", 25 | python: ".py", 26 | java: ".java", 27 | c: ".c", 28 | cpp: ".cpp", 29 | "c++": ".cpp", 30 | "c#": ".cs", 31 | ruby: ".rb", 32 | php: ".php", 33 | swift: ".swift", 34 | "objective-c": ".m", 35 | kotlin: ".kt", 36 | typescript: ".ts", 37 | go: ".go", 38 | perl: ".pl", 39 | rust: ".rs", 40 | scala: ".scala", 41 | haskell: ".hs", 42 | lua: ".lua", 43 | shell: ".sh", 44 | sql: ".sql", 45 | html: ".html", 46 | css: ".css", 47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component 48 | }; 49 | 50 | export const generateRandomString = (length: number, lowercase = false) => { 51 | const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0 52 | let result = ""; 53 | for (let i = 0; i < length; i++) { 54 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 55 | } 56 | return lowercase ? result.toLowerCase() : result; 57 | }; 58 | 59 | const CodeBlock: FC = memo(({ language, value }) => { 60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); 61 | 62 | const downloadAsFile = () => { 63 | if (typeof window === "undefined") { 64 | return; 65 | } 66 | const fileExtension = programmingLanguages[language] || ".file"; 67 | const suggestedFileName = `file-${generateRandomString( 68 | 3, 69 | true, 70 | )}${fileExtension}`; 71 | const fileName = window.prompt("Enter file name", suggestedFileName); 72 | 73 | if (!fileName) { 74 | // User pressed cancel on prompt. 75 | return; 76 | } 77 | 78 | const blob = new Blob([value], { type: "text/plain" }); 79 | const url = URL.createObjectURL(blob); 80 | const link = document.createElement("a"); 81 | link.download = fileName; 82 | link.href = url; 83 | link.style.display = "none"; 84 | document.body.appendChild(link); 85 | link.click(); 86 | document.body.removeChild(link); 87 | URL.revokeObjectURL(url); 88 | }; 89 | 90 | const onCopy = () => { 91 | if (isCopied) return; 92 | copyToClipboard(value); 93 | }; 94 | 95 | return ( 96 |
97 |
98 | {language} 99 |
100 | 104 | 112 |
113 |
114 | 132 | {value} 133 | 134 |
135 | ); 136 | }); 137 | CodeBlock.displayName = "CodeBlock"; 138 | 139 | export { CodeBlock }; 140 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/index.tsx: -------------------------------------------------------------------------------- 1 | import { Check, Copy } from "lucide-react"; 2 | 3 | import { Message } from "ai"; 4 | import { Fragment } from "react"; 5 | import { Button } from "../../button"; 6 | import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard"; 7 | import { 8 | AgentEventData, 9 | ChatHandler, 10 | DocumentFileData, 11 | EventData, 12 | ImageData, 13 | MessageAnnotation, 14 | MessageAnnotationType, 15 | SuggestedQuestionsData, 16 | ToolData, 17 | getAnnotationData, 18 | getSourceAnnotationData, 19 | } from "../index"; 20 | import { ChatAgentEvents } from "./chat-agent-events"; 21 | import ChatAvatar from "./chat-avatar"; 22 | import { ChatEvents } from "./chat-events"; 23 | import { ChatFiles } from "./chat-files"; 24 | import { ChatImage } from "./chat-image"; 25 | import { ChatSources } from "./chat-sources"; 26 | import { SuggestedQuestions } from "./chat-suggestedQuestions"; 27 | import ChatTools from "./chat-tools"; 28 | import Markdown from "./markdown"; 29 | 30 | type ContentDisplayConfig = { 31 | order: number; 32 | component: JSX.Element | null; 33 | }; 34 | 35 | function ChatMessageContent({ 36 | message, 37 | isLoading, 38 | append, 39 | isLastMessage, 40 | }: { 41 | message: Message; 42 | isLoading: boolean; 43 | append: Pick["append"]; 44 | isLastMessage: boolean; 45 | }) { 46 | const annotations = message.annotations as MessageAnnotation[] | undefined; 47 | if (!annotations?.length) return ; 48 | 49 | const imageData = getAnnotationData( 50 | annotations, 51 | MessageAnnotationType.IMAGE, 52 | ); 53 | const contentFileData = getAnnotationData( 54 | annotations, 55 | MessageAnnotationType.DOCUMENT_FILE, 56 | ); 57 | const eventData = getAnnotationData( 58 | annotations, 59 | MessageAnnotationType.EVENTS, 60 | ); 61 | const agentEventData = getAnnotationData( 62 | annotations, 63 | MessageAnnotationType.AGENT_EVENTS, 64 | ); 65 | 66 | const sourceData = getSourceAnnotationData(annotations); 67 | 68 | const toolData = getAnnotationData( 69 | annotations, 70 | MessageAnnotationType.TOOLS, 71 | ); 72 | const suggestedQuestionsData = getAnnotationData( 73 | annotations, 74 | MessageAnnotationType.SUGGESTED_QUESTIONS, 75 | ); 76 | 77 | const contents: ContentDisplayConfig[] = [ 78 | { 79 | order: 1, 80 | component: imageData[0] ? : null, 81 | }, 82 | { 83 | order: -3, 84 | component: 85 | eventData.length > 0 ? ( 86 | 87 | ) : null, 88 | }, 89 | { 90 | order: -2, 91 | component: 92 | agentEventData.length > 0 ? ( 93 | 97 | ) : null, 98 | }, 99 | { 100 | order: 2, 101 | component: contentFileData[0] ? ( 102 | 103 | ) : null, 104 | }, 105 | { 106 | order: -1, 107 | component: toolData[0] ? : null, 108 | }, 109 | { 110 | order: 0, 111 | component: , 112 | }, 113 | { 114 | order: 3, 115 | component: sourceData[0] ? : null, 116 | }, 117 | { 118 | order: 4, 119 | component: suggestedQuestionsData[0] ? ( 120 | 125 | ) : null, 126 | }, 127 | ]; 128 | 129 | return ( 130 |
131 | {contents 132 | .sort((a, b) => a.order - b.order) 133 | .map((content, index) => ( 134 | {content.component} 135 | ))} 136 |
137 | ); 138 | } 139 | 140 | export default function ChatMessage({ 141 | chatMessage, 142 | isLoading, 143 | append, 144 | isLastMessage, 145 | }: { 146 | chatMessage: Message; 147 | isLoading: boolean; 148 | append: Pick["append"]; 149 | isLastMessage: boolean; 150 | }) { 151 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); 152 | return ( 153 |
154 | 155 |
156 | 162 | 174 |
175 |
176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-message/markdown.tsx: -------------------------------------------------------------------------------- 1 | import "katex/dist/katex.min.css"; 2 | import { FC, memo } from "react"; 3 | import ReactMarkdown, { Options } from "react-markdown"; 4 | import rehypeKatex from "rehype-katex"; 5 | import remarkGfm from "remark-gfm"; 6 | import remarkMath from "remark-math"; 7 | 8 | import { SourceData } from ".."; 9 | import { SourceNumberButton } from "./chat-sources"; 10 | import { CodeBlock } from "./codeblock"; 11 | 12 | const MemoizedReactMarkdown: FC = memo( 13 | ReactMarkdown, 14 | (prevProps, nextProps) => 15 | prevProps.children === nextProps.children && 16 | prevProps.className === nextProps.className, 17 | ); 18 | 19 | const preprocessLaTeX = (content: string) => { 20 | // Replace block-level LaTeX delimiters \[ \] with $$ $$ 21 | const blockProcessedContent = content.replace( 22 | /\\\[([\s\S]*?)\\\]/g, 23 | (_, equation) => `$$${equation}$$`, 24 | ); 25 | // Replace inline LaTeX delimiters \( \) with $ $ 26 | const inlineProcessedContent = blockProcessedContent.replace( 27 | /\\\[([\s\S]*?)\\\]/g, 28 | (_, equation) => `$${equation}$`, 29 | ); 30 | return inlineProcessedContent; 31 | }; 32 | 33 | const preprocessMedia = (content: string) => { 34 | // Remove `sandbox:` from the beginning of the URL 35 | // to fix OpenAI's models issue appending `sandbox:` to the relative URL 36 | return content.replace(/(sandbox|attachment|snt):/g, ""); 37 | }; 38 | 39 | /** 40 | * Update the citation flag [citation:id]() to the new format [citation:index](url) 41 | */ 42 | const preprocessCitations = (content: string, sources?: SourceData) => { 43 | if (sources) { 44 | const citationRegex = /\[citation:(.+?)\]\(\)/g; 45 | let match; 46 | // Find all the citation references in the content 47 | while ((match = citationRegex.exec(content)) !== null) { 48 | const citationId = match[1]; 49 | // Find the source node with the id equal to the citation-id, also get the index of the source node 50 | const sourceNode = sources.nodes.find((node) => node.id === citationId); 51 | // If the source node is found, replace the citation reference with the new format 52 | if (sourceNode !== undefined) { 53 | content = content.replace( 54 | match[0], 55 | `[citation:${sources.nodes.indexOf(sourceNode)}]()`, 56 | ); 57 | } else { 58 | // If the source node is not found, remove the citation reference 59 | content = content.replace(match[0], ""); 60 | } 61 | } 62 | } 63 | return content; 64 | }; 65 | 66 | const preprocessContent = (content: string, sources?: SourceData) => { 67 | return preprocessCitations( 68 | preprocessMedia(preprocessLaTeX(content)), 69 | sources, 70 | ); 71 | }; 72 | 73 | export default function Markdown({ 74 | content, 75 | sources, 76 | }: { 77 | content: string; 78 | sources?: SourceData; 79 | }) { 80 | const processedContent = preprocessContent(content, sources); 81 | 82 | return ( 83 | {children}

; 90 | }, 91 | code({ node, inline, className, children, ...props }) { 92 | if (children.length) { 93 | if (children[0] == "▍") { 94 | return ( 95 | 96 | ); 97 | } 98 | 99 | children[0] = (children[0] as string).replace("`▍`", "▍"); 100 | } 101 | 102 | const match = /language-(\w+)/.exec(className || ""); 103 | 104 | if (inline) { 105 | return ( 106 | 107 | {children} 108 | 109 | ); 110 | } 111 | 112 | return ( 113 | 119 | ); 120 | }, 121 | a({ href, children }) { 122 | // If a text link starts with 'citation:', then render it as a citation reference 123 | if ( 124 | Array.isArray(children) && 125 | typeof children[0] === "string" && 126 | children[0].startsWith("citation:") 127 | ) { 128 | const index = Number(children[0].replace("citation:", "")); 129 | if (!isNaN(index)) { 130 | return ; 131 | } else { 132 | // citation is not looked up yet, don't render anything 133 | return <>; 134 | } 135 | } 136 | return {children}; 137 | }, 138 | }} 139 | > 140 | {processedContent} 141 |
142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat-messages.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | import { useEffect, useRef, useState } from "react"; 3 | 4 | import { Button } from "../button"; 5 | import ChatActions from "./chat-actions"; 6 | import ChatMessage from "./chat-message"; 7 | import { ChatHandler } from "./chat.interface"; 8 | import { useClientConfig } from "./hooks/use-config"; 9 | 10 | export default function ChatMessages( 11 | props: Pick< 12 | ChatHandler, 13 | "messages" | "isLoading" | "reload" | "stop" | "append" 14 | >, 15 | ) { 16 | const { backend } = useClientConfig(); 17 | const [starterQuestions, setStarterQuestions] = useState(); 18 | 19 | const scrollableChatContainerRef = useRef(null); 20 | const messageLength = props.messages.length; 21 | const lastMessage = props.messages[messageLength - 1]; 22 | 23 | const scrollToBottom = () => { 24 | if (scrollableChatContainerRef.current) { 25 | scrollableChatContainerRef.current.scrollTop = 26 | scrollableChatContainerRef.current.scrollHeight; 27 | } 28 | }; 29 | 30 | const isLastMessageFromAssistant = 31 | messageLength > 0 && lastMessage?.role !== "user"; 32 | const showReload = 33 | props.reload && !props.isLoading && isLastMessageFromAssistant; 34 | const showStop = props.stop && props.isLoading; 35 | 36 | // `isPending` indicate 37 | // that stream response is not yet received from the server, 38 | // so we show a loading indicator to give a better UX. 39 | const isPending = props.isLoading && !isLastMessageFromAssistant; 40 | 41 | useEffect(() => { 42 | scrollToBottom(); 43 | }, [messageLength, lastMessage]); 44 | 45 | useEffect(() => { 46 | if (!starterQuestions) { 47 | fetch(`${backend}/api/chat/config`) 48 | .then((response) => response.json()) 49 | .then((data) => { 50 | if (data?.starterQuestions) { 51 | setStarterQuestions(data.starterQuestions); 52 | } 53 | }) 54 | .catch((error) => console.error("Error fetching config", error)); 55 | } 56 | }, [starterQuestions, backend]); 57 | 58 | return ( 59 |
63 |
64 | {props.messages.map((m, i) => { 65 | const isLoadingMessage = i === messageLength - 1 && props.isLoading; 66 | return ( 67 | 74 | ); 75 | })} 76 | {isPending && ( 77 |
78 | 79 |
80 | )} 81 |
82 | {(showReload || showStop) && ( 83 |
84 | 90 |
91 | )} 92 | {!messageLength && starterQuestions?.length && props.append && ( 93 |
94 |
95 | {starterQuestions.map((question, i) => ( 96 | 105 | ))} 106 |
107 |
108 | )} 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /app/components/ui/chat/chat.interface.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "ai"; 2 | 3 | export interface ChatHandler { 4 | messages: Message[]; 5 | input: string; 6 | isLoading: boolean; 7 | handleSubmit: ( 8 | e: React.FormEvent, 9 | ops?: { 10 | data?: any; 11 | }, 12 | ) => void; 13 | handleInputChange: (e: React.ChangeEvent) => void; 14 | reload?: () => void; 15 | stop?: () => void; 16 | onFileUpload?: (file: File) => Promise; 17 | onFileError?: (errMsg: string) => void; 18 | setInput?: (input: string) => void; 19 | append?: ( 20 | message: Message | Omit, 21 | ops?: { 22 | data: any; 23 | }, 24 | ) => Promise; 25 | } 26 | -------------------------------------------------------------------------------- /app/components/ui/chat/hooks/use-config.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export interface ChatConfig { 4 | backend?: string; 5 | } 6 | 7 | function getBackendOrigin(): string { 8 | const chatAPI = process.env.NEXT_PUBLIC_CHAT_API; 9 | if (chatAPI) { 10 | return new URL(chatAPI).origin; 11 | } else { 12 | if (typeof window !== "undefined") { 13 | // Use BASE_URL from window.ENV 14 | return (window as any).ENV?.BASE_URL || ""; 15 | } 16 | return ""; 17 | } 18 | } 19 | 20 | export function useClientConfig(): ChatConfig { 21 | return { 22 | backend: getBackendOrigin(), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /app/components/ui/chat/hooks/use-copy-to-clipboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | export interface useCopyToClipboardProps { 6 | timeout?: number; 7 | } 8 | 9 | export function useCopyToClipboard({ 10 | timeout = 2000, 11 | }: useCopyToClipboardProps) { 12 | const [isCopied, setIsCopied] = React.useState(false); 13 | 14 | const copyToClipboard = (value: string) => { 15 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) { 16 | return; 17 | } 18 | 19 | if (!value) { 20 | return; 21 | } 22 | 23 | navigator.clipboard.writeText(value).then(() => { 24 | setIsCopied(true); 25 | 26 | setTimeout(() => { 27 | setIsCopied(false); 28 | }, timeout); 29 | }); 30 | }; 31 | 32 | return { isCopied, copyToClipboard }; 33 | } 34 | -------------------------------------------------------------------------------- /app/components/ui/chat/hooks/use-file.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { JSONValue } from "llamaindex"; 4 | import { useState } from "react"; 5 | import { v4 as uuidv4 } from "uuid"; 6 | import { 7 | DocumentFile, 8 | DocumentFileType, 9 | MessageAnnotation, 10 | MessageAnnotationType, 11 | } from ".."; 12 | import { useClientConfig } from "./use-config"; 13 | 14 | const docMineTypeMap: Record = { 15 | "text/csv": "csv", 16 | "application/pdf": "pdf", 17 | "text/plain": "txt", 18 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document": 19 | "docx", 20 | }; 21 | 22 | export function useFile() { 23 | const { backend } = useClientConfig(); 24 | const [imageUrl, setImageUrl] = useState(null); 25 | const [files, setFiles] = useState([]); 26 | 27 | const docEqual = (a: DocumentFile, b: DocumentFile) => { 28 | if (a.id === b.id) return true; 29 | if (a.filename === b.filename && a.filesize === b.filesize) return true; 30 | return false; 31 | }; 32 | 33 | const addDoc = (file: DocumentFile) => { 34 | const existedFile = files.find((f) => docEqual(f, file)); 35 | if (!existedFile) { 36 | setFiles((prev) => [...prev, file]); 37 | return true; 38 | } 39 | return false; 40 | }; 41 | 42 | const removeDoc = (file: DocumentFile) => { 43 | setFiles((prev) => prev.filter((f) => f.id !== file.id)); 44 | }; 45 | 46 | const reset = () => { 47 | imageUrl && setImageUrl(null); 48 | files.length && setFiles([]); 49 | }; 50 | 51 | const uploadContent = async ( 52 | file: File, 53 | requestParams: any = {}, 54 | ): Promise => { 55 | const base64 = await readContent({ file, asUrl: true }); 56 | const uploadAPI = `${backend}/api/chat/upload`; 57 | const response = await fetch(uploadAPI, { 58 | method: "POST", 59 | headers: { 60 | "Content-Type": "application/json", 61 | }, 62 | body: JSON.stringify({ 63 | ...requestParams, 64 | base64, 65 | filename: file.name, 66 | }), 67 | }); 68 | if (!response.ok) throw new Error("Failed to upload document."); 69 | return await response.json(); 70 | }; 71 | 72 | const getAnnotations = () => { 73 | const annotations: MessageAnnotation[] = []; 74 | if (imageUrl) { 75 | annotations.push({ 76 | type: MessageAnnotationType.IMAGE, 77 | data: { url: imageUrl }, 78 | }); 79 | } 80 | if (files.length > 0) { 81 | annotations.push({ 82 | type: MessageAnnotationType.DOCUMENT_FILE, 83 | data: { files }, 84 | }); 85 | } 86 | return annotations as JSONValue[]; 87 | }; 88 | 89 | const readContent = async (input: { 90 | file: File; 91 | asUrl?: boolean; 92 | }): Promise => { 93 | const { file, asUrl } = input; 94 | const content = await new Promise((resolve, reject) => { 95 | const reader = new FileReader(); 96 | if (asUrl) { 97 | reader.readAsDataURL(file); 98 | } else { 99 | reader.readAsText(file); 100 | } 101 | reader.onload = () => resolve(reader.result as string); 102 | reader.onerror = (error) => reject(error); 103 | }); 104 | return content; 105 | }; 106 | 107 | const uploadFile = async (file: File, requestParams: any = {}) => { 108 | if (file.type.startsWith("image/")) { 109 | const base64 = await readContent({ file, asUrl: true }); 110 | return setImageUrl(base64); 111 | } 112 | 113 | const filetype = docMineTypeMap[file.type]; 114 | if (!filetype) throw new Error("Unsupported document type."); 115 | const newDoc: Omit = { 116 | id: uuidv4(), 117 | filetype, 118 | filename: file.name, 119 | filesize: file.size, 120 | }; 121 | switch (file.type) { 122 | case "text/csv": { 123 | const content = await readContent({ file }); 124 | return addDoc({ 125 | ...newDoc, 126 | content: { 127 | type: "text", 128 | value: content, 129 | }, 130 | }); 131 | } 132 | default: { 133 | const ids = await uploadContent(file, requestParams); 134 | return addDoc({ 135 | ...newDoc, 136 | content: { 137 | type: "ref", 138 | value: ids, 139 | }, 140 | }); 141 | } 142 | } 143 | }; 144 | 145 | return { 146 | imageUrl, 147 | setImageUrl, 148 | files, 149 | removeDoc, 150 | reset, 151 | getAnnotations, 152 | uploadFile, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /app/components/ui/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONValue } from "ai"; 2 | import ChatInput from "./chat-input"; 3 | import ChatMessages from "./chat-messages"; 4 | 5 | export { type ChatHandler } from "./chat.interface"; 6 | export { ChatInput, ChatMessages }; 7 | 8 | export enum MessageAnnotationType { 9 | IMAGE = "image", 10 | DOCUMENT_FILE = "document_file", 11 | SOURCES = "sources", 12 | EVENTS = "events", 13 | TOOLS = "tools", 14 | SUGGESTED_QUESTIONS = "suggested_questions", 15 | AGENT_EVENTS = "agent", 16 | } 17 | 18 | export type ImageData = { 19 | url: string; 20 | }; 21 | 22 | export type DocumentFileType = "csv" | "pdf" | "txt" | "docx"; 23 | 24 | export type DocumentFileContent = { 25 | type: "ref" | "text"; 26 | value: string[] | string; 27 | }; 28 | 29 | export type DocumentFile = { 30 | id: string; 31 | filename: string; 32 | filesize: number; 33 | filetype: DocumentFileType; 34 | content: DocumentFileContent; 35 | }; 36 | 37 | export type DocumentFileData = { 38 | files: DocumentFile[]; 39 | }; 40 | 41 | export type SourceNode = { 42 | id: string; 43 | metadata: Record; 44 | score?: number; 45 | text: string; 46 | url: string; 47 | }; 48 | 49 | export type SourceData = { 50 | nodes: SourceNode[]; 51 | }; 52 | 53 | export type EventData = { 54 | title: string; 55 | }; 56 | 57 | export type AgentEventData = { 58 | agent: string; 59 | text: string; 60 | }; 61 | 62 | export type ToolData = { 63 | toolCall: { 64 | id: string; 65 | name: string; 66 | input: { 67 | [key: string]: JSONValue; 68 | }; 69 | }; 70 | toolOutput: { 71 | output: JSONValue; 72 | isError: boolean; 73 | }; 74 | }; 75 | 76 | export type SuggestedQuestionsData = string[]; 77 | 78 | export type AnnotationData = 79 | | ImageData 80 | | DocumentFileData 81 | | SourceData 82 | | EventData 83 | | AgentEventData 84 | | ToolData 85 | | SuggestedQuestionsData; 86 | 87 | export type MessageAnnotation = { 88 | type: MessageAnnotationType; 89 | data: AnnotationData; 90 | }; 91 | 92 | const NODE_SCORE_THRESHOLD = 0.25; 93 | 94 | export function getAnnotationData( 95 | annotations: MessageAnnotation[], 96 | type: MessageAnnotationType, 97 | ): T[] { 98 | return annotations.filter((a) => a.type === type).map((a) => a.data as T); 99 | } 100 | 101 | export function getSourceAnnotationData( 102 | annotations: MessageAnnotation[], 103 | ): SourceData[] { 104 | const data = getAnnotationData( 105 | annotations, 106 | MessageAnnotationType.SOURCES, 107 | ); 108 | if (data.length > 0) { 109 | const sourceData = data[0] as SourceData; 110 | if (sourceData.nodes) { 111 | sourceData.nodes = preprocessSourceNodes(sourceData.nodes); 112 | } 113 | } 114 | return data; 115 | } 116 | 117 | function preprocessSourceNodes(nodes: SourceNode[]): SourceNode[] { 118 | // Filter source nodes has lower score 119 | nodes = nodes 120 | .filter((node) => (node.score ?? 1) > NODE_SCORE_THRESHOLD) 121 | .filter((node) => node.url && node.url.trim() !== "") 122 | .sort((a, b) => (b.score ?? 1) - (a.score ?? 1)) 123 | .map((node) => { 124 | // remove trailing slash for node url if exists 125 | node.url = node.url.replace(/\/$/, ""); 126 | return node; 127 | }); 128 | return nodes; 129 | } 130 | -------------------------------------------------------------------------------- /app/components/ui/chat/widgets/PdfDialog.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { Button } from "../../button"; 3 | import { 4 | Drawer, 5 | DrawerClose, 6 | DrawerContent, 7 | DrawerDescription, 8 | DrawerHeader, 9 | DrawerTitle, 10 | DrawerTrigger, 11 | } from "../../drawer"; 12 | 13 | export interface PdfDialogProps { 14 | documentId: string; 15 | url: string; 16 | trigger: React.ReactNode; 17 | } 18 | 19 | // Dynamic imports for client-side rendering only 20 | const PDFViewer = dynamic( 21 | () => import("@llamaindex/pdf-viewer").then((module) => module.PDFViewer), 22 | { ssr: false }, 23 | ); 24 | 25 | const PdfFocusProvider = dynamic( 26 | () => 27 | import("@llamaindex/pdf-viewer").then((module) => module.PdfFocusProvider), 28 | { ssr: false }, 29 | ); 30 | 31 | export default function PdfDialog(props: PdfDialogProps) { 32 | return ( 33 | 34 | {props.trigger} 35 | 36 | 37 |
38 | PDF Content 39 | 40 | File URL:{" "} 41 | 46 | {props.url} 47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 |
55 | 56 | 62 | 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/components/ui/chat/widgets/WeatherCard.tsx: -------------------------------------------------------------------------------- 1 | export interface WeatherData { 2 | latitude: number; 3 | longitude: number; 4 | generationtime_ms: number; 5 | utc_offset_seconds: number; 6 | timezone: string; 7 | timezone_abbreviation: string; 8 | elevation: number; 9 | current_units: { 10 | time: string; 11 | interval: string; 12 | temperature_2m: string; 13 | weather_code: string; 14 | }; 15 | current: { 16 | time: string; 17 | interval: number; 18 | temperature_2m: number; 19 | weather_code: number; 20 | }; 21 | hourly_units: { 22 | time: string; 23 | temperature_2m: string; 24 | weather_code: string; 25 | }; 26 | hourly: { 27 | time: string[]; 28 | temperature_2m: number[]; 29 | weather_code: number[]; 30 | }; 31 | daily_units: { 32 | time: string; 33 | weather_code: string; 34 | }; 35 | daily: { 36 | time: string[]; 37 | weather_code: number[]; 38 | }; 39 | } 40 | 41 | // Follow WMO Weather interpretation codes (WW) 42 | const weatherCodeDisplayMap: Record< 43 | string, 44 | { 45 | icon: JSX.Element; 46 | status: string; 47 | } 48 | > = { 49 | "0": { 50 | icon: ☀️, 51 | status: "Clear sky", 52 | }, 53 | "1": { 54 | icon: 🌤️, 55 | status: "Mainly clear", 56 | }, 57 | "2": { 58 | icon: ☁️, 59 | status: "Partly cloudy", 60 | }, 61 | "3": { 62 | icon: ☁️, 63 | status: "Overcast", 64 | }, 65 | "45": { 66 | icon: 🌫️, 67 | status: "Fog", 68 | }, 69 | "48": { 70 | icon: 🌫️, 71 | status: "Depositing rime fog", 72 | }, 73 | "51": { 74 | icon: 🌧️, 75 | status: "Drizzle", 76 | }, 77 | "53": { 78 | icon: 🌧️, 79 | status: "Drizzle", 80 | }, 81 | "55": { 82 | icon: 🌧️, 83 | status: "Drizzle", 84 | }, 85 | "56": { 86 | icon: 🌧️, 87 | status: "Freezing Drizzle", 88 | }, 89 | "57": { 90 | icon: 🌧️, 91 | status: "Freezing Drizzle", 92 | }, 93 | "61": { 94 | icon: 🌧️, 95 | status: "Rain", 96 | }, 97 | "63": { 98 | icon: 🌧️, 99 | status: "Rain", 100 | }, 101 | "65": { 102 | icon: 🌧️, 103 | status: "Rain", 104 | }, 105 | "66": { 106 | icon: 🌧️, 107 | status: "Freezing Rain", 108 | }, 109 | "67": { 110 | icon: 🌧️, 111 | status: "Freezing Rain", 112 | }, 113 | "71": { 114 | icon: ❄️, 115 | status: "Snow fall", 116 | }, 117 | "73": { 118 | icon: ❄️, 119 | status: "Snow fall", 120 | }, 121 | "75": { 122 | icon: ❄️, 123 | status: "Snow fall", 124 | }, 125 | "77": { 126 | icon: ❄️, 127 | status: "Snow grains", 128 | }, 129 | "80": { 130 | icon: 🌧️, 131 | status: "Rain showers", 132 | }, 133 | "81": { 134 | icon: 🌧️, 135 | status: "Rain showers", 136 | }, 137 | "82": { 138 | icon: 🌧️, 139 | status: "Rain showers", 140 | }, 141 | "85": { 142 | icon: ❄️, 143 | status: "Snow showers", 144 | }, 145 | "86": { 146 | icon: ❄️, 147 | status: "Snow showers", 148 | }, 149 | "95": { 150 | icon: ⛈️, 151 | status: "Thunderstorm", 152 | }, 153 | "96": { 154 | icon: ⛈️, 155 | status: "Thunderstorm", 156 | }, 157 | "99": { 158 | icon: ⛈️, 159 | status: "Thunderstorm", 160 | }, 161 | }; 162 | 163 | const displayDay = (time: string) => { 164 | return new Date(time).toLocaleDateString("en-US", { 165 | weekday: "long", 166 | }); 167 | }; 168 | 169 | export function WeatherCard({ data }: { data: WeatherData }) { 170 | const currentDayString = new Date(data.current.time).toLocaleDateString( 171 | "en-US", 172 | { 173 | weekday: "long", 174 | month: "long", 175 | day: "numeric", 176 | }, 177 | ); 178 | 179 | return ( 180 |
181 |
182 |
183 |
{currentDayString}
184 |
185 | 186 | {data.current.temperature_2m} {data.current_units.temperature_2m} 187 | 188 | {weatherCodeDisplayMap[data.current.weather_code].icon} 189 |
190 |
191 | 192 | {weatherCodeDisplayMap[data.current.weather_code].status} 193 | 194 |
195 |
196 | {data.daily.time.map((time, index) => { 197 | if (index === 0) return null; // skip the current day 198 | return ( 199 |
200 | {displayDay(time)} 201 |
202 | {weatherCodeDisplayMap[data.daily.weather_code[index]].icon} 203 |
204 | 205 | {weatherCodeDisplayMap[data.daily.weather_code[index]].status} 206 | 207 |
208 | ); 209 | })} 210 |
211 |
212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /app/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleContent, CollapsibleTrigger }; 12 | -------------------------------------------------------------------------------- /app/components/ui/document-preview.tsx: -------------------------------------------------------------------------------- 1 | import { XCircleIcon } from "lucide-react"; 2 | import Image from "next/image"; 3 | import DocxIcon from "../ui/icons/docx.svg"; 4 | import PdfIcon from "../ui/icons/pdf.svg"; 5 | import SheetIcon from "../ui/icons/sheet.svg"; 6 | import TxtIcon from "../ui/icons/txt.svg"; 7 | import { Button } from "./button"; 8 | import { DocumentFile, DocumentFileType } from "./chat"; 9 | import { 10 | Drawer, 11 | DrawerClose, 12 | DrawerContent, 13 | DrawerDescription, 14 | DrawerHeader, 15 | DrawerTitle, 16 | DrawerTrigger, 17 | } from "./drawer"; 18 | import { cn } from "./lib/utils"; 19 | 20 | export interface DocumentPreviewProps { 21 | file: DocumentFile; 22 | onRemove?: () => void; 23 | } 24 | 25 | export function DocumentPreview(props: DocumentPreviewProps) { 26 | const { filename, filesize, content, filetype } = props.file; 27 | 28 | if (content.type === "ref") { 29 | return ( 30 |
31 | 32 |
33 | ); 34 | } 35 | 36 | return ( 37 | 38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 |
46 | {filetype.toUpperCase()} Raw Content 47 | 48 | {filename} ({inKB(filesize)} KB) 49 | 50 |
51 | 52 | 53 | 54 |
55 |
56 | {content.type === "text" && ( 57 |
 58 |               {content.value as string}
 59 |             
60 | )} 61 |
62 |
63 |
64 | ); 65 | } 66 | 67 | export const FileIcon: Record = { 68 | csv: SheetIcon, 69 | pdf: PdfIcon, 70 | docx: DocxIcon, 71 | txt: TxtIcon, 72 | }; 73 | 74 | function PreviewCard(props: DocumentPreviewProps) { 75 | const { onRemove, file } = props; 76 | return ( 77 |
83 |
84 |
85 | Icon 91 |
92 |
93 |
94 | {file.filename} ({inKB(file.filesize)} KB) 95 |
96 |
97 | {file.filetype.toUpperCase()} File 98 |
99 |
100 |
101 | {onRemove && ( 102 |
107 | 111 |
112 | )} 113 |
114 | ); 115 | } 116 | 117 | function inKB(size: number) { 118 | return Math.round((size / 1024) * 10) / 10; 119 | } 120 | -------------------------------------------------------------------------------- /app/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "./lib/utils"; 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ); 17 | Drawer.displayName = "Drawer"; 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger; 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal; 22 | 23 | const DrawerClose = DrawerPrimitive.Close; 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )); 56 | DrawerContent.displayName = "DrawerContent"; 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ); 67 | DrawerHeader.displayName = "DrawerHeader"; 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ); 78 | DrawerFooter.displayName = "DrawerFooter"; 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )); 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )); 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 106 | 107 | export { 108 | Drawer, 109 | DrawerClose, 110 | DrawerContent, 111 | DrawerDescription, 112 | DrawerFooter, 113 | DrawerHeader, 114 | DrawerOverlay, 115 | DrawerPortal, 116 | DrawerTitle, 117 | DrawerTrigger, 118 | }; 119 | -------------------------------------------------------------------------------- /app/components/ui/file-uploader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader2, Paperclip } from "lucide-react"; 4 | import { ChangeEvent, useState } from "react"; 5 | import { buttonVariants } from "./button"; 6 | import { cn } from "./lib/utils"; 7 | 8 | export interface FileUploaderProps { 9 | config?: { 10 | inputId?: string; 11 | fileSizeLimit?: number; 12 | allowedExtensions?: string[]; 13 | checkExtension?: (extension: string) => string | null; 14 | disabled: boolean; 15 | }; 16 | onFileUpload: (file: File) => Promise; 17 | onFileError?: (errMsg: string) => void; 18 | } 19 | 20 | const DEFAULT_INPUT_ID = "fileInput"; 21 | const DEFAULT_FILE_SIZE_LIMIT = 1024 * 1024 * 50; // 50 MB 22 | 23 | export default function FileUploader({ 24 | config, 25 | onFileUpload, 26 | onFileError, 27 | }: FileUploaderProps) { 28 | const [uploading, setUploading] = useState(false); 29 | 30 | const inputId = config?.inputId || DEFAULT_INPUT_ID; 31 | const fileSizeLimit = config?.fileSizeLimit || DEFAULT_FILE_SIZE_LIMIT; 32 | const allowedExtensions = config?.allowedExtensions; 33 | const defaultCheckExtension = (extension: string) => { 34 | if (allowedExtensions && !allowedExtensions.includes(extension)) { 35 | return `Invalid file type. Please select a file with one of these formats: ${allowedExtensions!.join( 36 | ",", 37 | )}`; 38 | } 39 | return null; 40 | }; 41 | const checkExtension = config?.checkExtension ?? defaultCheckExtension; 42 | 43 | const isFileSizeExceeded = (file: File) => { 44 | return file.size > fileSizeLimit; 45 | }; 46 | 47 | const resetInput = () => { 48 | const fileInput = document.getElementById(inputId) as HTMLInputElement; 49 | fileInput.value = ""; 50 | }; 51 | 52 | const onFileChange = async (e: ChangeEvent) => { 53 | const file = e.target.files?.[0]; 54 | if (!file) return; 55 | 56 | setUploading(true); 57 | await handleUpload(file); 58 | resetInput(); 59 | setUploading(false); 60 | }; 61 | 62 | const handleUpload = async (file: File) => { 63 | const onFileUploadError = onFileError || window.alert; 64 | const fileExtension = file.name.split(".").pop() || ""; 65 | const extensionFileError = checkExtension(fileExtension); 66 | if (extensionFileError) { 67 | return onFileUploadError(extensionFileError); 68 | } 69 | 70 | if (isFileSizeExceeded(file)) { 71 | return onFileUploadError( 72 | `File size exceeded. Limit is ${fileSizeLimit / 1024 / 1024} MB`, 73 | ); 74 | } 75 | 76 | await onFileUpload(file); 77 | }; 78 | 79 | return ( 80 |
81 | 89 | 103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /app/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "./lib/utils"; 7 | 8 | const HoverCard = HoverCardPrimitive.Root; 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 26 | )); 27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 28 | 29 | export { HoverCard, HoverCardContent, HoverCardTrigger }; 30 | -------------------------------------------------------------------------------- /app/components/ui/icons/docx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/components/ui/icons/pdf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/components/ui/icons/sheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | Sheets-icon 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /app/components/ui/icons/txt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 13 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "./lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /app/components/ui/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /app/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SelectPrimitive from "@radix-ui/react-select"; 4 | import { Check, ChevronDown, ChevronUp } from "lucide-react"; 5 | import * as React from "react"; 6 | import { cn } from "./lib/utils"; 7 | 8 | const Select = SelectPrimitive.Root; 9 | 10 | const SelectGroup = SelectPrimitive.Group; 11 | 12 | const SelectValue = SelectPrimitive.Value; 13 | 14 | const SelectTrigger = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, children, ...props }, ref) => ( 18 | span]:line-clamp-1", 22 | className, 23 | )} 24 | {...props} 25 | > 26 | {children} 27 | 28 | 29 | 30 | 31 | )); 32 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 33 | 34 | const SelectScrollUpButton = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | 47 | 48 | )); 49 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 50 | 51 | const SelectScrollDownButton = React.forwardRef< 52 | React.ElementRef, 53 | React.ComponentPropsWithoutRef 54 | >(({ className, ...props }, ref) => ( 55 | 63 | 64 | 65 | )); 66 | SelectScrollDownButton.displayName = 67 | SelectPrimitive.ScrollDownButton.displayName; 68 | 69 | const SelectContent = React.forwardRef< 70 | React.ElementRef, 71 | React.ComponentPropsWithoutRef 72 | >(({ className, children, position = "popper", ...props }, ref) => ( 73 | 74 | 85 | 86 | 93 | {children} 94 | 95 | 96 | 97 | 98 | )); 99 | SelectContent.displayName = SelectPrimitive.Content.displayName; 100 | 101 | const SelectLabel = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 112 | 113 | const SelectItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, children, ...props }, ref) => ( 117 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | {children} 132 | 133 | )); 134 | SelectItem.displayName = SelectPrimitive.Item.displayName; 135 | 136 | const SelectSeparator = React.forwardRef< 137 | React.ElementRef, 138 | React.ComponentPropsWithoutRef 139 | >(({ className, ...props }, ref) => ( 140 | 145 | )); 146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 147 | 148 | export { 149 | Select, 150 | SelectContent, 151 | SelectGroup, 152 | SelectItem, 153 | SelectLabel, 154 | SelectScrollDownButton, 155 | SelectScrollUpButton, 156 | SelectSeparator, 157 | SelectTrigger, 158 | SelectValue, 159 | }; 160 | -------------------------------------------------------------------------------- /app/components/ui/upload-image-preview.tsx: -------------------------------------------------------------------------------- 1 | import { XCircleIcon } from "lucide-react"; 2 | import Image from "next/image"; 3 | import { cn } from "./lib/utils"; 4 | 5 | export default function UploadImagePreview({ 6 | url, 7 | onRemove, 8 | }: { 9 | url: string; 10 | onRemove: () => void; 11 | }) { 12 | return ( 13 |
14 | Uploaded image 20 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/llama-index-azure-code-interpreter/6b9ba09d407ae65987f117239fdad8313c6d5fd7/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 47.4% 11.2%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 47.4% 11.2%; 15 | 16 | --border: 214.3 31.8% 91.4%; 17 | --input: 214.3 31.8% 91.4%; 18 | 19 | --card: 0 0% 100%; 20 | --card-foreground: 222.2 47.4% 11.2%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 100% 50%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 215 20.2% 65.1%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 224 71% 4%; 41 | --foreground: 213 31% 91%; 42 | 43 | --muted: 223 47% 11%; 44 | --muted-foreground: 215.4 16.3% 56.9%; 45 | 46 | --accent: 216 34% 17%; 47 | --accent-foreground: 210 40% 98%; 48 | 49 | --popover: 224 71% 4%; 50 | --popover-foreground: 215 20.2% 65.1%; 51 | 52 | --border: 216 34% 17%; 53 | --input: 216 34% 17%; 54 | 55 | --card: 224 71% 4%; 56 | --card-foreground: 213 31% 91%; 57 | 58 | --primary: 210 40% 98%; 59 | --primary-foreground: 222.2 47.4% 1.2%; 60 | 61 | --secondary: 222.2 47.4% 11.2%; 62 | --secondary-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 63% 31%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 216 34% 17%; 68 | 69 | --radius: 0.5rem; 70 | } 71 | } 72 | 73 | @layer base { 74 | * { 75 | @apply border-border; 76 | } 77 | html { 78 | @apply h-full; 79 | } 80 | body { 81 | @apply bg-background text-foreground h-full; 82 | font-feature-settings: 83 | "rlig" 1, 84 | "calt" 1; 85 | } 86 | .background-gradient { 87 | background-color: #fff; 88 | background-image: radial-gradient( 89 | at 21% 11%, 90 | rgba(186, 186, 233, 0.53) 0, 91 | transparent 50% 92 | ), 93 | radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%), 94 | radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%), 95 | radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%); 96 | } 97 | 98 | @keyframes fadeIn { 99 | 0% { 100 | opacity: 0; 101 | } 102 | 100% { 103 | opacity: 1; 104 | } 105 | } 106 | 107 | .fadein-agent { 108 | animation-name: fadeIn; 109 | animation-duration: 1.5s; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import "./markdown.css"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Llama App", 10 | description: "Generated by create-llama", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/markdown.css: -------------------------------------------------------------------------------- 1 | /* Custom CSS for chat message markdown */ 2 | .custom-markdown ul { 3 | list-style-type: disc; 4 | margin-left: 20px; 5 | } 6 | 7 | .custom-markdown ol { 8 | list-style-type: decimal; 9 | margin-left: 20px; 10 | } 11 | 12 | .custom-markdown li { 13 | margin-bottom: 5px; 14 | } 15 | 16 | .custom-markdown ol ol { 17 | list-style: lower-alpha; 18 | } 19 | 20 | .custom-markdown ul ul, 21 | .custom-markdown ol ol { 22 | margin-left: 20px; 23 | } 24 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/app/components/header"; 2 | import ChatSection from "./components/chat-section"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: llama-index-azure-code-interpreter 4 | metadata: 5 | template: llama-index-azure-code-interpreter@0.1.0 6 | requiredVersions: 7 | azd: ">= 1.10.0" 8 | hooks: 9 | postprovision: 10 | posix: 11 | shell: sh 12 | run: azd env get-values > .env 13 | windows: 14 | shell: pwsh 15 | run: azd env get-values > .env 16 | 17 | services: 18 | llama-index-azure-code-interpreter: 19 | project: . 20 | host: containerapp 21 | language: ts 22 | docker: 23 | path: Dockerfile 24 | -------------------------------------------------------------------------------- /cache/vector_store.json: -------------------------------------------------------------------------------- 1 | {"embeddingDict":{},"textIdToRefDocId":{},"metadataDict":{}} -------------------------------------------------------------------------------- /config/tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "interpreter": {} 4 | }, 5 | "llamahub": {} 6 | } -------------------------------------------------------------------------------- /docs/assets/llamaindex-code-interpreter-azure-dynamic-session-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/llama-index-azure-code-interpreter/6b9ba09d407ae65987f117239fdad8313c6d5fd7/docs/assets/llamaindex-code-interpreter-azure-dynamic-session-architecture.png -------------------------------------------------------------------------------- /docs/assets/llamaindex-code-interpreter-azure-dynamic-session-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/llama-index-azure-code-interpreter/6b9ba09d407ae65987f117239fdad8313c6d5fd7/docs/assets/llamaindex-code-interpreter-azure-dynamic-session-full.png -------------------------------------------------------------------------------- /docs/assets/llamaindex-code-interpreter-azure-dynamic-session-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/llama-index-azure-code-interpreter/6b9ba09d407ae65987f117239fdad8313c6d5fd7/docs/assets/llamaindex-code-interpreter-azure-dynamic-session-small.png -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabases": "sqldb-", 120 | "sqlServersDatabasesStretch": "sqlstrdb-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "storSimpleManagers": "ssimp", 124 | "streamAnalyticsCluster": "asa-", 125 | "synapseWorkspaces": "syn", 126 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 127 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 128 | "synapseWorkspacesSqlPoolsSpark": "synsp", 129 | "timeSeriesInsightsEnvironments": "tsi-", 130 | "webServerFarms": "plan-", 131 | "webSitesAppService": "app-", 132 | "webSitesAppServiceEnvironment": "ase-", 133 | "webSitesFunctions": "func-", 134 | "webStaticSites": "stapp-" 135 | } 136 | -------------------------------------------------------------------------------- /infra/app/llamaindex-azure-dynamic-session.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param identityName string 6 | param containerRegistryName string 7 | param containerAppsEnvironmentName string 8 | param applicationInsightsName string 9 | param azureOpenAiDeploymentName string 10 | param azureOpenAiEndpoint string 11 | param azureOpenAiApiVersion string 12 | param dynamicSessionsName string 13 | param storageAccountName string 14 | param storageContainerName string 15 | 16 | @description('The name of the container image') 17 | param imageName string = '' 18 | 19 | @secure() 20 | param appDefinition object 21 | 22 | var appSettingsArray = filter(array(appDefinition.settings), i => i.name != '') 23 | var secrets = map(filter(appSettingsArray, i => i.?secret != null), i => { 24 | name: i.name 25 | value: i.value 26 | secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) 27 | }) 28 | var env = map(filter(appSettingsArray, i => i.?secret == null), i => { 29 | name: i.name 30 | value: i.value 31 | }) 32 | 33 | resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 34 | name: identityName 35 | location: location 36 | } 37 | 38 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 39 | name: containerRegistryName 40 | } 41 | 42 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { 43 | name: containerAppsEnvironmentName 44 | } 45 | 46 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 47 | name: applicationInsightsName 48 | } 49 | 50 | resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 51 | scope: containerRegistry 52 | name: guid(subscription().id, resourceGroup().id, identity.id, 'acrPullRole') 53 | properties: { 54 | roleDefinitionId: subscriptionResourceId( 55 | 'Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 56 | principalType: 'ServicePrincipal' 57 | principalId: identity.properties.principalId 58 | } 59 | } 60 | 61 | module dynamicSessions '../modules/dynamic-sessions.bicep' = { 62 | name: dynamicSessionsName 63 | scope: resourceGroup() 64 | params: { 65 | name: 'llamaindex-sessions' 66 | location: location 67 | tags: tags 68 | } 69 | } 70 | 71 | resource app 'Microsoft.App/containerApps@2023-05-02-preview' = { 72 | name: name 73 | location: location 74 | tags: union(tags, {'azd-service-name': 'llama-index-azure-code-interpreter' }) 75 | dependsOn: [ acrPullRole ] 76 | identity: { 77 | type: 'UserAssigned' 78 | userAssignedIdentities: { '${identity.id}': {} } 79 | } 80 | properties: { 81 | managedEnvironmentId: containerAppsEnvironment.id 82 | configuration: { 83 | ingress: { 84 | external: true 85 | targetPort: 3000 86 | transport: 'auto' 87 | } 88 | registries: [ 89 | { 90 | server: '${containerRegistryName}.azurecr.io' 91 | identity: identity.id 92 | } 93 | ] 94 | secrets: union([ 95 | ], 96 | map(secrets, secret => { 97 | name: secret.secretRef 98 | value: secret.value 99 | })) 100 | } 101 | template: { 102 | containers: [ 103 | { 104 | image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' 105 | name: 'main' 106 | env: union([ 107 | { 108 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 109 | value: applicationInsights.properties.ConnectionString 110 | } 111 | { 112 | name: 'PORT' 113 | value: '3000' 114 | } 115 | { 116 | name: 'OPENAI_API_TYPE' 117 | value: 'azure' 118 | } 119 | { 120 | name: 'OPENAI_API_VERSION' 121 | value: azureOpenAiApiVersion 122 | } 123 | { 124 | name: 'AZURE_OPENAI_ENDPOINT' 125 | value: azureOpenAiEndpoint 126 | } 127 | { 128 | name: 'AZURE_DEPLOYMENT_NAME' 129 | value: azureOpenAiDeploymentName 130 | } 131 | { 132 | name: 'AZURE_CLIENT_ID' 133 | value: identity.properties.clientId 134 | } 135 | { 136 | name: 'AZURE_TENANT_ID' 137 | value: identity.properties.tenantId 138 | } 139 | { 140 | name: 'AZURE_STORAGE_ACCOUNT' 141 | value: storageAccountName 142 | } 143 | { 144 | name: 'AZURE_STORAGE_CONTAINER' 145 | value: storageContainerName 146 | } 147 | ], 148 | env, 149 | map(secrets, secret => { 150 | name: secret.name 151 | secretRef: secret.secretRef 152 | })) 153 | resources: { 154 | cpu: json('1.0') 155 | memory: '2.0Gi' 156 | } 157 | } 158 | ] 159 | scale: { 160 | minReplicas: 1 161 | maxReplicas: 10 162 | } 163 | } 164 | } 165 | } 166 | 167 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 168 | output name string = app.name 169 | output uri string = 'https://${app.properties.configuration.ingress.fqdn}' 170 | output id string = app.id 171 | output principalId string = identity.properties.principalId 172 | output poolManagementEndpoint string = dynamicSessions.outputs.poolManagementEndpoint 173 | output registryLoginServer string = containerRegistry.properties.loginServer 174 | output registryName string = containerRegistry.name 175 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | @secure() 13 | param llamaIndexAzureDynamicSessionDefinition object 14 | 15 | @description('Id of the user or app to assign application roles') 16 | param principalId string 17 | 18 | @description('Whether the deployment is running on GitHub Actions') 19 | param CI string = '' 20 | 21 | @description('Public network access value for all deployed resources') 22 | @allowed(['Enabled', 'Disabled']) 23 | param publicNetworkAccess string = 'Enabled' 24 | 25 | @allowed(['None', 'AzureServices']) 26 | @description('If allowedIp is set, whether azure services are allowed to bypass the storage and AI services firewall.') 27 | param bypass string = 'AzureServices' 28 | 29 | param storageContainerName string = 'files' 30 | param storageSkuName string // Set in main.parameters.json 31 | 32 | param disableKeyBasedAuth bool = true 33 | param azureOpenAiDeploymentName string // See main.parameters.json 34 | param azureOpenAiApiVersion string // See main.parameters.json 35 | param azureOpenAiEmbeddingModel string // See main.parameters.json 36 | param azureOpenAiEmbeddingDim string // See main.parameters.json 37 | param azureOpenAiDeploymentCapacity int = 30 38 | 39 | // Tags that should be applied to all resources. 40 | // 41 | // Note that 'azd-service-name' tags should be applied separately to service host resources. 42 | // Example usage: 43 | // tags: union(tags, { 'azd-service-name': }) 44 | var tags = { 45 | 'azd-env-name': environmentName 46 | } 47 | 48 | var SYSTEM_PROMPT = ''' 49 | I want you to process the user's input using the code interpreter tool. 50 | After processing, ensure that the output image is always returned in base64 format and encoded as a PNG file. 51 | Include the base64 image using the following format: data:image/png;base64,. 52 | Additionally, upload the PNG file and render the image in the response using 53 | the following Markdown format: ![Rendered Image](https://.blob.core.windows.net/files/.png). Ensure the base64-encoded image is included as data:image/png;base64, 54 | ''' 55 | 56 | var abbrs = loadJsonContent('./abbreviations.json') 57 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 58 | var principalType = empty(CI) ? 'User' : 'ServicePrincipal' 59 | 60 | resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { 61 | name: 'rg-${environmentName}' 62 | location: location 63 | tags: tags 64 | } 65 | 66 | module llamaIndexAzureDynamicSession './app/llamaindex-azure-dynamic-session.bicep' = { 67 | name: 'aca-app' 68 | params: { 69 | name: '${abbrs.appContainerApps}llamaindex-${resourceToken}' 70 | location: location 71 | tags: tags 72 | identityName: '${abbrs.managedIdentityUserAssignedIdentities}llamaindex-${resourceToken}' 73 | applicationInsightsName: monitoring.outputs.applicationInsightsName 74 | containerAppsEnvironmentName: appsEnv.outputs.name 75 | containerRegistryName: registry.outputs.name 76 | dynamicSessionsName: '${abbrs.managedIdentityUserAssignedIdentities}llamaindex-session-pool-${resourceToken}' 77 | appDefinition: llamaIndexAzureDynamicSessionDefinition 78 | azureOpenAiDeploymentName: azureOpenAiDeploymentName 79 | azureOpenAiApiVersion: azureOpenAiApiVersion 80 | azureOpenAiEndpoint: azureOpenAi.outputs.endpoint 81 | storageAccountName: storage.outputs.name 82 | storageContainerName: storageContainerName 83 | } 84 | scope: rg 85 | } 86 | 87 | module monitoring './shared/monitoring.bicep' = { 88 | name: 'monitoring' 89 | params: { 90 | location: location 91 | tags: tags 92 | logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' 93 | applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' 94 | } 95 | scope: rg 96 | } 97 | 98 | module dashboard './shared/dashboard-web.bicep' = { 99 | name: 'dashboard' 100 | params: { 101 | name: '${abbrs.portalDashboards}${resourceToken}' 102 | applicationInsightsName: monitoring.outputs.applicationInsightsName 103 | location: location 104 | tags: tags 105 | } 106 | scope: rg 107 | } 108 | 109 | module registry './shared/registry.bicep' = { 110 | name: 'registry' 111 | params: { 112 | location: location 113 | tags: tags 114 | name: '${abbrs.containerRegistryRegistries}${resourceToken}' 115 | } 116 | scope: rg 117 | } 118 | 119 | module keyVault './shared/keyvault.bicep' = { 120 | name: 'keyvault' 121 | params: { 122 | location: location 123 | tags: tags 124 | name: '${abbrs.keyVaultVaults}${resourceToken}' 125 | principalId: principalId 126 | } 127 | scope: rg 128 | } 129 | 130 | module appsEnv './shared/apps-env.bicep' = { 131 | name: 'apps-env' 132 | params: { 133 | name: '${abbrs.appManagedEnvironments}${resourceToken}' 134 | location: location 135 | tags: tags 136 | applicationInsightsName: monitoring.outputs.applicationInsightsName 137 | logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName 138 | } 139 | scope: rg 140 | } 141 | 142 | module storage './shared/storage-account.bicep' = { 143 | name: 'storage' 144 | params: { 145 | name: '${abbrs.storageStorageAccounts}${resourceToken}' 146 | location: location 147 | tags: tags 148 | publicNetworkAccess: publicNetworkAccess 149 | bypass: bypass 150 | allowBlobPublicAccess: true 151 | allowSharedKeyAccess: false 152 | sku: { 153 | name: storageSkuName 154 | } 155 | deleteRetentionPolicy: { 156 | enabled: true 157 | days: 2 158 | } 159 | containers: [ 160 | { 161 | name: storageContainerName 162 | publicAccess: 'Blob' 163 | } 164 | ] 165 | } 166 | scope: rg 167 | } 168 | 169 | module azureOpenAi 'shared/cognitive-services.bicep' = { 170 | name: 'openai' 171 | params: { 172 | name: '${abbrs.cognitiveServicesAccounts}${resourceToken}' 173 | tags: tags 174 | disableLocalAuth: disableKeyBasedAuth 175 | sku: { 176 | name: 'S0' 177 | } 178 | deployments: [ 179 | { 180 | name: azureOpenAiDeploymentName 181 | model: { 182 | format: 'OpenAI' 183 | name: azureOpenAiDeploymentName 184 | version: azureOpenAiApiVersion 185 | } 186 | sku: { 187 | name: 'GlobalStandard' 188 | capacity: azureOpenAiDeploymentCapacity 189 | } 190 | } 191 | ] 192 | } 193 | scope: rg 194 | } 195 | 196 | module openAiRoleUser 'shared/security-role.bicep' = { 197 | name: 'openai-role-user' 198 | params: { 199 | principalId: principalId 200 | roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' 201 | principalType: principalType 202 | } 203 | scope: rg 204 | } 205 | 206 | module openAiRoleBackend 'shared/security-role.bicep' = { 207 | name: 'openai-role-backend' 208 | params: { 209 | principalId: llamaIndexAzureDynamicSession.outputs.principalId 210 | roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' 211 | principalType: 'ServicePrincipal' 212 | } 213 | scope: rg 214 | } 215 | 216 | module acaSessionExecutorRoleUser 'shared/security-role.bicep' = { 217 | name: 'aca-session-role-user' 218 | params: { 219 | principalId: principalId 220 | roleDefinitionId: '0fb8eba5-a2bb-4abe-b1c1-49dfad359bb0' 221 | principalType: principalType 222 | } 223 | scope: rg 224 | } 225 | 226 | module acaSessionExecutorRoleBackend 'shared/security-role.bicep' = { 227 | name: 'aca-session-role-backend' 228 | params: { 229 | principalId: llamaIndexAzureDynamicSession.outputs.principalId 230 | roleDefinitionId: '0fb8eba5-a2bb-4abe-b1c1-49dfad359bb0' 231 | principalType: 'ServicePrincipal' 232 | } 233 | scope: rg 234 | } 235 | 236 | module storageRoleUser 'shared/security-role.bicep' = { 237 | name: 'storage-role-user' 238 | params: { 239 | principalId: principalId 240 | roleDefinitionId: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' 241 | principalType: principalType 242 | } 243 | scope: rg 244 | } 245 | 246 | module storageContribRoleUser 'shared/security-role.bicep' = { 247 | name: 'storage-contrib-role-user' 248 | params: { 249 | principalId: principalId 250 | roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 251 | principalType: principalType 252 | } 253 | scope: rg 254 | } 255 | 256 | module storageRoleBackend 'shared/security-role.bicep' = { 257 | name: 'storage-role-backend' 258 | params: { 259 | principalId: llamaIndexAzureDynamicSession.outputs.principalId 260 | roleDefinitionId: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' 261 | principalType: 'ServicePrincipal' 262 | } 263 | scope: rg 264 | } 265 | 266 | output AZURE_STORAGE_ACCOUNT string = storage.outputs.name 267 | output AZURE_STORAGE_CONTAINER string = storageContainerName 268 | output AZURE_STORAGE_RESOURCE_GROUP string = rg.name 269 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = llamaIndexAzureDynamicSession.outputs.registryLoginServer 270 | output AZURE_CONTAINER_REGISTRY_NAME string = llamaIndexAzureDynamicSession.outputs.registryName 271 | output AZURE_POOL_MANAGEMENT_ENDPOINT string = llamaIndexAzureDynamicSession.outputs.poolManagementEndpoint 272 | output AZURE_OPENAI_ENDPOINT string = azureOpenAi.outputs.endpoint 273 | output OPENAI_API_VERSION string = azureOpenAiApiVersion 274 | output AZURE_DEPLOYMENT_NAME string = azureOpenAiDeploymentName 275 | output EMBEDDING_MODEL string = azureOpenAiEmbeddingModel 276 | output EMBEDDING_DIM string = azureOpenAiEmbeddingDim 277 | output SYSTEM_PROMPT string = SYSTEM_PROMPT 278 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION=eastus}" 10 | }, 11 | "llamaIndexAzureDynamicSessionDefinition": { 12 | "value": { 13 | "settings": [ 14 | { 15 | "name": "", 16 | "value": "${VAR}", 17 | "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", 18 | "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR} to use the value of 'VAR' from the current environment." 19 | }, 20 | { 21 | "name": "", 22 | "value": "${VAR_S}", 23 | "secret": true, 24 | "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", 25 | "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR_S} to use the value of 'VAR_S' from the current environment." 26 | } 27 | ] 28 | } 29 | }, 30 | "principalId": { 31 | "value": "${AZURE_PRINCIPAL_ID}" 32 | }, 33 | "disableKeyBasedAuth": { 34 | "value": "${DISABLE_KEY_BASED_AUTH=true}" 35 | }, 36 | "azureOpenAiEndpoint": { 37 | "value": "${AZURE_OPENAI_ENDPOINT}" 38 | }, 39 | "azureOpenAiDeploymentName": { 40 | "value": "${AZURE_DEPLOYMENT_NAME=gpt-4o-mini}" 41 | }, 42 | "azureOpenAiApiVersion": { 43 | "value": "${OPENAI_API_VERSION=2024-07-18}" 44 | }, 45 | "azureOpenAiEmbeddingModel": { 46 | "value": "${EMBEDDING_MODEL=text-embedding-3-large}" 47 | }, 48 | "azureOpenAiEmbeddingDim": { 49 | "value": "${EMBEDDING_DIM=1024}" 50 | }, 51 | "CI": { 52 | "value": "${GITHUB_ACTIONS}" 53 | }, 54 | "storageSkuName": { 55 | "value": "${AZURE_STORAGE_SKU=Standard_LRS}" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /infra/modules/dynamic-sessions.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource dynamicSessions 'Microsoft.App/sessionPools@2024-02-02-preview' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | properties: { 10 | poolManagementType: 'Dynamic' 11 | containerType: 'PythonLTS' 12 | scaleConfiguration: { 13 | maxConcurrentSessions: 100 14 | } 15 | dynamicPoolConfiguration: { 16 | executionType: 'Timed' 17 | cooldownPeriodInSeconds: 300 18 | } 19 | sessionNetworkConfiguration: { 20 | status: 'EgressEnabled' 21 | } 22 | } 23 | } 24 | 25 | output poolManagementEndpoint string = dynamicSessions.properties.poolManagementEndpoint 26 | output name string = dynamicSessions.name 27 | -------------------------------------------------------------------------------- /infra/modules/fetch-container-image.bicep: -------------------------------------------------------------------------------- 1 | param exists bool 2 | param name string 3 | 4 | resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { 5 | name: name 6 | } 7 | 8 | output containers array = exists ? existingApp.properties.template.containers : [] 9 | -------------------------------------------------------------------------------- /infra/readme.md: -------------------------------------------------------------------------------- 1 | # Next Steps after `azd init` 2 | 3 | ## Table of Contents 4 | 5 | 1. [Next Steps](#next-steps) 6 | 2. [What was added](#what-was-added) 7 | 3. [Billing](#billing) 8 | 4. [Troubleshooting](#troubleshooting) 9 | 10 | ## Next Steps 11 | 12 | ### Provision infrastructure and deploy application code 13 | 14 | Run `azd up` to provision your infrastructure and deploy to Azure (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! 15 | 16 | To troubleshoot any issues, see [troubleshooting](#troubleshooting). 17 | 18 | ### Configure environment variables for running services 19 | 20 | Configure environment variables for running services by updating `settings` in [main.parameters.json](./infra/main.parameters.json). 21 | 22 | ### Configure CI/CD pipeline 23 | 24 | 1. Create a workflow pipeline file locally. The following starters are available: 25 | - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) 26 | - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) 27 | 2. Run `azd pipeline config` to configure the deployment pipeline to connect securely to Azure. 28 | 29 | ## What was added 30 | 31 | ### Infrastructure configuration 32 | 33 | To describe the infrastructure and application, `azure.yaml` along with Infrastructure as Code files using Bicep were added with the following directory structure: 34 | 35 | ```yaml 36 | - azure.yaml # azd project configuration 37 | - infra/ # Infrastructure as Code (bicep) files 38 | - main.bicep # main deployment module 39 | - app/ # Application resource modules 40 | - shared/ # Shared resource modules 41 | - modules/ # Library modules 42 | ``` 43 | 44 | Each bicep file declares resources to be provisioned. The resources are provisioned when running `azd up` or `azd provision`. 45 | 46 | - [app/llama-index-azure-dynamic-session.bicep](./infra/app/llama-index-azure-dynamic-session.bicep) - Azure Container Apps resources to host the 'llama-index-azure-dynamic-session' service. 47 | - [shared/keyvault.bicep](./infra/shared/keyvault.bicep) - Azure KeyVault to store secrets. 48 | - [shared/monitoring.bicep](./infra/shared/monitoring.bicep) - Azure Log Analytics workspace and Application Insights to log and store instrumentation logs. 49 | - [shared/registry.bicep](./infra/shared/registry.bicep) - Azure Container Registry to store docker images. 50 | 51 | More information about [Bicep](https://aka.ms/bicep) language. 52 | 53 | ### Build from source (no Dockerfile) 54 | 55 | #### Build with Buildpacks using Oryx 56 | 57 | If your project does not contain a Dockerfile, we will use [Buildpacks](https://buildpacks.io/) using [Oryx](https://github.com/microsoft/Oryx/blob/main/doc/README.md) to create an image for the services in `azure.yaml` and get your containerized app onto Azure. 58 | 59 | To produce and run the docker image locally: 60 | 61 | 1. Run `azd package` to build the image. 62 | 2. Copy the *Image Tag* shown. 63 | 3. Run `docker run -it ` to run the image locally. 64 | 65 | #### Exposed port 66 | 67 | Oryx will automatically set `PORT` to a default value of `80` (port `8080` for Java). Additionally, it will auto-configure supported web servers such as `gunicorn` and `ASP .NET Core` to listen to the target `PORT`. If your application already listens to the port specified by the `PORT` variable, the application will work out-of-the-box. Otherwise, you may need to perform one of the steps below: 68 | 69 | 1. Update your application code or configuration to listen to the port specified by the `PORT` variable 70 | 1. (Alternatively) Search for `targetPort` in a .bicep file under the `infra/app` folder, and update the variable to match the port used by the application. 71 | 72 | ## Billing 73 | 74 | Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). 75 | 76 | ## Troubleshooting 77 | 78 | Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. 79 | 80 | A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: 81 | 82 | 1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. 83 | 2. Navigate to the specific Container App service that is failing to deploy. 84 | 3. Click on the failing revision under "Revisions with Issues". 85 | 4. Review "Status details" for more information about the type of failure. 86 | 5. Observe the log outputs from Console log stream and System log stream to identify any errors. 87 | 6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. 88 | 89 | For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). 90 | 91 | ### Additional information 92 | 93 | For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). 94 | -------------------------------------------------------------------------------- /infra/shared/apps-env.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param logAnalyticsWorkspaceName string 6 | param applicationInsightsName string = '' 7 | 8 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-10-01' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | properties: { 13 | appLogsConfiguration: { 14 | destination: 'log-analytics' 15 | logAnalyticsConfiguration: { 16 | customerId: logAnalyticsWorkspace.properties.customerId 17 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 18 | } 19 | } 20 | daprAIConnectionString: applicationInsights.properties.ConnectionString 21 | } 22 | } 23 | 24 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 25 | name: logAnalyticsWorkspaceName 26 | } 27 | 28 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 29 | name: applicationInsightsName 30 | } 31 | 32 | output name string = containerAppsEnvironment.name 33 | output domain string = containerAppsEnvironment.properties.defaultDomain 34 | -------------------------------------------------------------------------------- /infra/shared/cognitive-services.bicep: -------------------------------------------------------------------------------- 1 | 2 | metadata description = 'Creates an Azure Cognitive Services instance.' 3 | param name string 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | @description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') 7 | param customSubDomainName string = name 8 | param disableLocalAuth bool = false 9 | param deployments array = [] 10 | param kind string = 'OpenAI' 11 | 12 | @allowed([ 'Enabled', 'Disabled' ]) 13 | param publicNetworkAccess string = 'Enabled' 14 | param sku object = { 15 | name: 'S0' 16 | } 17 | 18 | param allowedIpRules array = [] 19 | param networkAcls object = empty(allowedIpRules) ? { 20 | defaultAction: 'Allow' 21 | } : { 22 | ipRules: allowedIpRules 23 | defaultAction: 'Deny' 24 | } 25 | 26 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 27 | name: name 28 | location: location 29 | tags: tags 30 | kind: kind 31 | properties: { 32 | customSubDomainName: customSubDomainName 33 | publicNetworkAccess: publicNetworkAccess 34 | networkAcls: networkAcls 35 | disableLocalAuth: disableLocalAuth 36 | } 37 | sku: sku 38 | } 39 | 40 | @batchSize(1) 41 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 42 | parent: account 43 | name: deployment.name 44 | properties: { 45 | model: deployment.model 46 | raiPolicyName: deployment.?raiPolicyName ?? null 47 | } 48 | sku: deployment.?sku ?? { 49 | name: 'Standard' 50 | capacity: 20 51 | } 52 | }] 53 | 54 | output endpoint string = account.properties.endpoint 55 | output endpoints object = account.properties.endpoints 56 | output id string = account.id 57 | output name string = account.name 58 | -------------------------------------------------------------------------------- /infra/shared/keyvault.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | @description('Service principal that should be granted read access to the KeyVault. If unset, no service principal is granted access by default') 6 | param principalId string = '' 7 | 8 | var defaultAccessPolicies = !empty(principalId) ? [ 9 | { 10 | objectId: principalId 11 | permissions: { secrets: [ 'get', 'list' ] } 12 | tenantId: subscription().tenantId 13 | } 14 | ] : [] 15 | 16 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { 17 | name: name 18 | location: location 19 | tags: tags 20 | properties: { 21 | tenantId: subscription().tenantId 22 | sku: { family: 'A', name: 'standard' } 23 | enabledForTemplateDeployment: true 24 | accessPolicies: union(defaultAccessPolicies, [ 25 | // define access policies here 26 | ]) 27 | } 28 | } 29 | 30 | output endpoint string = keyVault.properties.vaultUri 31 | output name string = keyVault.name 32 | -------------------------------------------------------------------------------- /infra/shared/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsName string 2 | param applicationInsightsName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 7 | name: logAnalyticsName 8 | location: location 9 | tags: tags 10 | properties: any({ 11 | retentionInDays: 30 12 | features: { 13 | searchVersion: 1 14 | } 15 | sku: { 16 | name: 'PerGB2018' 17 | } 18 | }) 19 | } 20 | 21 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 22 | name: applicationInsightsName 23 | location: location 24 | tags: tags 25 | kind: 'web' 26 | properties: { 27 | Application_Type: 'web' 28 | WorkspaceResourceId: logAnalytics.id 29 | } 30 | } 31 | 32 | output applicationInsightsName string = applicationInsights.name 33 | output logAnalyticsWorkspaceId string = logAnalytics.id 34 | output logAnalyticsWorkspaceName string = logAnalytics.name 35 | -------------------------------------------------------------------------------- /infra/shared/registry.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param adminUserEnabled bool = true 6 | param anonymousPullEnabled bool = false 7 | param dataEndpointEnabled bool = false 8 | param encryption object = { 9 | status: 'disabled' 10 | } 11 | param networkRuleBypassOptions string = 'AzureServices' 12 | param publicNetworkAccess string = 'Enabled' 13 | param sku object = { 14 | name: 'Standard' 15 | } 16 | param zoneRedundancy string = 'Disabled' 17 | 18 | // 2023-01-01-preview needed for anonymousPullEnabled 19 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { 20 | name: name 21 | location: location 22 | tags: tags 23 | sku: sku 24 | properties: { 25 | adminUserEnabled: adminUserEnabled 26 | anonymousPullEnabled: anonymousPullEnabled 27 | dataEndpointEnabled: dataEndpointEnabled 28 | encryption: encryption 29 | networkRuleBypassOptions: networkRuleBypassOptions 30 | publicNetworkAccess: publicNetworkAccess 31 | zoneRedundancy: zoneRedundancy 32 | } 33 | } 34 | 35 | output loginServer string = containerRegistry.properties.loginServer 36 | output name string = containerRegistry.name 37 | -------------------------------------------------------------------------------- /infra/shared/security-role.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a role assignment for a service principal.' 2 | param principalId string 3 | 4 | @allowed([ 5 | 'Device' 6 | 'ForeignGroup' 7 | 'Group' 8 | 'ServicePrincipal' 9 | 'User' 10 | ]) 11 | param principalType string = 'ServicePrincipal' 12 | param roleDefinitionId string 13 | 14 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 15 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 16 | properties: { 17 | principalId: principalId 18 | principalType: principalType 19 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /infra/shared/storage-account.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure storage account.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @allowed([ 7 | 'Cool' 8 | 'Hot' 9 | 'Premium' ]) 10 | param accessTier string = 'Hot' 11 | param allowBlobPublicAccess bool = true 12 | param allowCrossTenantReplication bool = true 13 | param allowSharedKeyAccess bool = true 14 | param containers array = [] 15 | param defaultToOAuthAuthentication bool = false 16 | param deleteRetentionPolicy object = {} 17 | @allowed([ 'AzureDnsZone', 'Standard' ]) 18 | param dnsEndpointType string = 'Standard' 19 | param isHnsEnabled bool = false 20 | param kind string = 'StorageV2' 21 | param minimumTlsVersion string = 'TLS1_2' 22 | param supportsHttpsTrafficOnly bool = true 23 | @allowed([ 'Enabled', 'Disabled' ]) 24 | param publicNetworkAccess string = 'Enabled' 25 | param sku object = { name: 'Standard_LRS' } 26 | @allowed([ 'None', 'AzureServices' ]) 27 | param bypass string = 'AzureServices' 28 | 29 | var networkAcls = (publicNetworkAccess == 'Enabled') ? { 30 | bypass: bypass 31 | defaultAction: 'Allow' 32 | } : { defaultAction: 'Deny' } 33 | 34 | resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { 35 | name: name 36 | location: location 37 | tags: tags 38 | kind: kind 39 | sku: sku 40 | properties: { 41 | accessTier: accessTier 42 | allowBlobPublicAccess: allowBlobPublicAccess 43 | allowCrossTenantReplication: allowCrossTenantReplication 44 | allowSharedKeyAccess: allowSharedKeyAccess 45 | defaultToOAuthAuthentication: defaultToOAuthAuthentication 46 | dnsEndpointType: dnsEndpointType 47 | isHnsEnabled: isHnsEnabled 48 | minimumTlsVersion: minimumTlsVersion 49 | networkAcls: networkAcls 50 | publicNetworkAccess: publicNetworkAccess 51 | supportsHttpsTrafficOnly: supportsHttpsTrafficOnly 52 | } 53 | 54 | resource blobServices 'blobServices' = if (!empty(containers)) { 55 | name: 'default' 56 | properties: { 57 | deleteRetentionPolicy: deleteRetentionPolicy 58 | } 59 | resource container 'containers' = [for container in containers: { 60 | name: container.name 61 | properties: { 62 | publicAccess: container.?publicAccess ?? 'None' 63 | } 64 | }] 65 | } 66 | } 67 | 68 | output id string = storage.id 69 | output name string = storage.name 70 | output primaryEndpoints object = storage.properties.primaryEndpoints 71 | -------------------------------------------------------------------------------- /next.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "experimental": { 3 | "outputFileTracingIncludes": { 4 | "/*": [ 5 | "./cache/**/*" 6 | ] 7 | }, 8 | "outputFileTracingExcludes": { 9 | "/api/files/*": [ 10 | ".next/**/*", 11 | "node_modules/**/*", 12 | "public/**/*", 13 | "app/**/*" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | import fs from "fs"; 3 | import withLlamaIndex from "llamaindex/next"; 4 | import webpack from "./webpack.config.mjs"; 5 | 6 | const nextConfig = JSON.parse(fs.readFileSync("./next.config.json", "utf-8")); 7 | nextConfig.webpack = webpack; 8 | 9 | // use withLlamaIndex to add necessary modifications for llamaindex library 10 | export default withLlamaIndex(nextConfig); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llama-index-code-interpreter-javascript", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "format": "prettier --ignore-unknown --cache --check .", 6 | "format:write": "prettier --ignore-unknown --write .", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "generate": "tsx app/api/chat/engine/generate.ts" 12 | }, 13 | "dependencies": { 14 | "@azure/identity": "^4.4.1", 15 | "@azure/storage-blob": "^12.25.0", 16 | "@llamaindex/pdf-viewer": "^1.1.3", 17 | "@radix-ui/react-collapsible": "^1.0.3", 18 | "@radix-ui/react-hover-card": "^1.0.7", 19 | "@radix-ui/react-select": "^2.1.1", 20 | "@radix-ui/react-slot": "^1.0.2", 21 | "ai": "3.0.21", 22 | "ajv": "^8.12.0", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.1", 25 | "dotenv": "^16.3.1", 26 | "duck-duck-scrape": "^2.2.5", 27 | "formdata-node": "^6.0.3", 28 | "got": "^14.4.1", 29 | "llamaindex": "0.8.31", 30 | "lucide-react": "^0.294.0", 31 | "next": "^14.2.26", 32 | "react": "^18.2.0", 33 | "react-dom": "^18.2.0", 34 | "react-markdown": "^8.0.7", 35 | "react-syntax-highlighter": "^15.5.0", 36 | "rehype-katex": "^7.0.0", 37 | "remark": "^14.0.3", 38 | "remark-code-import": "^1.2.0", 39 | "remark-gfm": "^3.0.1", 40 | "remark-math": "^5.1.1", 41 | "supports-color": "^8.1.1", 42 | "tailwind-merge": "^2.1.0", 43 | "tiktoken": "^1.0.15", 44 | "uuid": "^9.0.1", 45 | "vaul": "^0.9.1" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^20.10.3", 49 | "@types/react": "^18.2.42", 50 | "@types/react-dom": "^18.2.17", 51 | "@types/react-syntax-highlighter": "^15.5.11", 52 | "@types/uuid": "^9.0.8", 53 | "autoprefixer": "^10.4.16", 54 | "cross-env": "^7.0.3", 55 | "eslint": "^8.55.0", 56 | "eslint-config-next": "^14.2.4", 57 | "eslint-config-prettier": "^8.10.0", 58 | "postcss": "^8.4.32", 59 | "prettier": "^3.2.5", 60 | "prettier-plugin-organize-imports": "^3.2.4", 61 | "tailwindcss": "^3.3.6", 62 | "tsx": "^4.19.3", 63 | "typescript": "^5.6.2" 64 | }, 65 | "author": { 66 | "name": "Wassim Chegham", 67 | "email": "github@wassim.dev", 68 | "url": "https://wassim.dev" 69 | }, 70 | "engines": { 71 | "node": "20.x" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-organize-imports"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/llama-index-azure-code-interpreter/6b9ba09d407ae65987f117239fdad8313c6d5fd7/public/llama.png -------------------------------------------------------------------------------- /public/sample-data/gdppercap.csv: -------------------------------------------------------------------------------- 1 | "continent","1952","1957" 2 | "Africa",1252.57246582115,1385.23606225577 3 | "Americas",4079.0625522,4616.04373316 4 | "Asia",5195.48400403939,4003.13293994242 5 | "Europe",5661.05743476,6963.01281593333 6 | "Oceania",10298.08565,11598.522455 7 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | const config: Config = { 5 | darkMode: ["class"], 6 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"], 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: "2rem", 11 | screens: { 12 | "2xl": "1400px", 13 | }, 14 | }, 15 | extend: { 16 | colors: { 17 | border: "hsl(var(--border))", 18 | input: "hsl(var(--input))", 19 | ring: "hsl(var(--ring))", 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | primary: { 23 | DEFAULT: "hsl(var(--primary))", 24 | foreground: "hsl(var(--primary-foreground))", 25 | }, 26 | secondary: { 27 | DEFAULT: "hsl(var(--secondary))", 28 | foreground: "hsl(var(--secondary-foreground))", 29 | }, 30 | destructive: { 31 | DEFAULT: "hsl(var(--destructive) / )", 32 | foreground: "hsl(var(--destructive-foreground) / )", 33 | }, 34 | muted: { 35 | DEFAULT: "hsl(var(--muted))", 36 | foreground: "hsl(var(--muted-foreground))", 37 | }, 38 | accent: { 39 | DEFAULT: "hsl(var(--accent))", 40 | foreground: "hsl(var(--accent-foreground))", 41 | }, 42 | popover: { 43 | DEFAULT: "hsl(var(--popover))", 44 | foreground: "hsl(var(--popover-foreground))", 45 | }, 46 | card: { 47 | DEFAULT: "hsl(var(--card))", 48 | foreground: "hsl(var(--card-foreground))", 49 | }, 50 | }, 51 | borderRadius: { 52 | xl: `calc(var(--radius) + 4px)`, 53 | lg: `var(--radius)`, 54 | md: `calc(var(--radius) - 2px)`, 55 | sm: "calc(var(--radius) - 4px)", 56 | }, 57 | fontFamily: { 58 | sans: ["var(--font-sans)", ...fontFamily.sans], 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [], 77 | }; 78 | export default config; 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.mjs: -------------------------------------------------------------------------------- 1 | // webpack config must be a function in NextJS that is used to patch the default webpack config provided by NextJS, see https://nextjs.org/docs/pages/api-reference/next-config-js/webpack 2 | export default function webpack(config) { 3 | config.resolve.fallback = { 4 | aws4: false, 5 | }; 6 | 7 | return config; 8 | } 9 | --------------------------------------------------------------------------------