├── .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 | [](https://codespaces.new/Azure-Samples/llama-index-azure-code-interpreter?hide_repo_select=true&ref=main&quickstart=true)
25 | [](https://codespaces.new/Azure-Samples/llama-index-azure-code-interpreter?hide_repo_select=true&ref=main&quickstart=true)
26 | [](https://github.com/Azure-Samples/llama-index-azure-code-interpreter/actions)
27 | 
28 | [](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)
34 | [](#architecture-diagram)
35 | [](#demo-video-optional)
36 | [](#getting-started)
37 | [](#contributing)
38 | [](https://github.com/Azure-Samples/llama-index-javascript/stargazers)
39 |
40 |
41 | 
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 | 
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 | [](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 | []()
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 |
16 |
17 | Stop generating
18 |
19 | )}
20 | {props.showReload && (
21 |
22 |
23 | Regenerate
24 |
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 |
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 | Close
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 |
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 |
33 | {isLoading ? : null}
34 | {buttonLabel}
35 | {EventIcon}
36 |
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 |
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 | {
173 | e.stopPropagation();
174 | copyToClipboard(nodeInfo.text);
175 | }}
176 | size="icon"
177 | variant="ghost"
178 | className="h-12 w-12 shrink-0"
179 | >
180 | {isCopied ? (
181 |
182 | ) : (
183 |
184 | )}
185 |
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 |
101 |
102 | Download
103 |
104 |
105 | {isCopied ? (
106 |
107 | ) : (
108 |
109 | )}
110 | Copy code
111 |
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 | copyToClipboard(chatMessage.content)}
164 | size="icon"
165 | variant="ghost"
166 | className="h-8 w-8 opacity-0 group-hover:opacity-100"
167 | >
168 | {isCopied ? (
169 |
170 | ) : (
171 |
172 | )}
173 |
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 |
100 | props.append!({ role: "user", content: question })
101 | }
102 | >
103 | {question}
104 |
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 |
50 |
51 | Close
52 |
53 |
54 |
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 |
33 | );
34 | }
35 |
36 | return (
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 | {filetype.toUpperCase()} Raw Content
47 |
48 | {filename} ({inKB(filesize)} KB)
49 |
50 |
51 |
52 | Close
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 |
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 |
97 | {uploading ? (
98 |
99 | ) : (
100 |
101 | )}
102 |
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 |
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 |
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: . 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 |
--------------------------------------------------------------------------------