├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── bicep-to-arm.yml │ └── main_gptsmartsearch_apps.yml ├── .gitignore ├── 01-Load-Data-ACogSearch.ipynb ├── 02-LoadCSVOneToMany-ACogSearch.ipynb ├── 03-Quering-AOpenAI.ipynb ├── 04-Complex-Docs.ipynb ├── 05-Adding_Memory.ipynb ├── 06-First-RAG.ipynb ├── 07-TabularDataQA.ipynb ├── 08-SQLDB_QA.ipynb ├── 09-BingChatClone.ipynb ├── 10-API-Search.ipynb ├── 11-Adding_Multi-modality.ipynb ├── 12-Smart_Agent.ipynb ├── 13-Building-Apps.ipynb ├── 14-BotService-API.ipynb ├── 15-FastAPI-API.ipynb ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GPT-Smart-Search-Architecture.vsdx ├── Intro AOAI GPT Azure Smart Search Engine Accelerator.pptx ├── LICENSE.txt ├── Latest_Release_Notes.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── apps ├── .gitignore ├── backend │ ├── botservice │ │ ├── README.md │ │ ├── app │ │ │ ├── app.py │ │ │ ├── bot.py │ │ │ ├── config.py │ │ │ └── runserver.sh │ │ ├── azuredeploy-backend.bicep │ │ ├── azuredeploy-backend.json │ │ └── backend.zip │ └── fastapi │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ ├── __init__.py │ │ ├── runserver.sh │ │ └── server.py │ │ └── backend.zip └── frontend │ ├── README.md │ ├── app │ ├── Home.py │ ├── __init__.py │ ├── helpers │ │ └── streamlit_helpers.py │ └── pages │ │ ├── 1_Search.py │ │ ├── 2_BotService_Chat.py │ │ └── 3_FastAPI_Chat.py │ ├── azuredeploy-frontend.bicep │ ├── azuredeploy-frontend.json │ └── frontend.zip ├── azure.yaml ├── azuredeploy.bicep ├── azuredeploy.json ├── common ├── __init__.py ├── audio_utils.py ├── cosmosdb_checkpointer.py ├── graph.py ├── prompts.py ├── requirements.txt └── utils.py ├── credentials.env ├── data ├── all-states-history.csv ├── books.zip ├── cord19mini.zip ├── friends_transcripts.zip └── openapi_kraken.json ├── download_odbc_driver.sh ├── download_odbc_driver_dev_container.sh ├── images ├── Bot-Framework.png ├── Cog-Search-Enrich.png ├── GPT-Smart-Search-Architecture.jpg ├── architecture.png ├── cosmos-chathistory.png ├── error-authorize-github.jpeg ├── github-actions-pipeline-success.png └── memory_diagram.png └── infra ├── README.md ├── core └── ai │ └── cognitiveservices.bicep ├── main.bicep ├── main.parameters.json └── scripts ├── CreateAppRegistration.ps1 ├── CreatePrerequisites.ps1 ├── UpdateSecretsInApps.ps1 └── loadenv.ps1 /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:0-3.10-bullseye", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | "postCreateCommand": "pip install -r ./common/requirements.txt", 16 | 17 | // Configure tool-specific properties. 18 | "customizations": { 19 | "vscode": { 20 | "extensions": [ 21 | "ms-toolsai.jupyter", 22 | "ms-python.python" 23 | ] 24 | } 25 | } 26 | 27 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 28 | // "remoteUser": "root" 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/bicep-to-arm.yml: -------------------------------------------------------------------------------- 1 | name: Compile bicep to ARM 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - '**.bicep' 8 | 9 | jobs: 10 | deploy: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Install Bicep build 18 | run: | 19 | curl -Lo bicepinstall https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 20 | chmod +x ./bicepinstall 21 | sudo mv ./bicepinstall /usr/local/bin/bicep 22 | bicep --help 23 | 24 | - name: Run Bicep build 25 | run: | 26 | bicep build azuredeploy.bicep 27 | bicep build apps/backend/azuredeploy-backend.bicep 28 | bicep build apps/frontend/azuredeploy-frontend.bicep 29 | ls -l *.json 30 | 31 | - uses: EndBug/add-and-commit@v7.0.0 32 | with: 33 | author_name: github-actions 34 | author_email: '41898282+github-actions[bot]@users.noreply.github.com' 35 | message: Update ARM template to match Bicep 36 | -------------------------------------------------------------------------------- /.github/workflows/main_gptsmartsearch_apps.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | # More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions 4 | 5 | name: GPTSmartSearch Apps Deployment 6 | env: 7 | DO_BUILD_DURING_DEPLOYMENT: true 8 | 9 | on: 10 | push: 11 | branches: 12 | - github-actions-apps 13 | - main 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | outputs: 20 | env-name: ${{steps.set-deploy-env.outputs.DEPLOY_ENVIRONMENT}} 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up Python version 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: '3.10' 28 | 29 | - name: Set environment for branch 30 | id: set-deploy-env 31 | run: | 32 | if [[ $GITHUB_REF_NAME == 'refs/heads/main' ]]; then 33 | echo "DEPLOY_ENVIRONMENT=Development" >> "$GITHUB_OUTPUT" 34 | elif [[ $GITHUB_REF_NAME == 'refs/heads/develop' ]]; then 35 | echo "DEPLOY_ENVIRONMENT=Development" >> "$GITHUB_OUTPUT" 36 | elif [[ $GITHUB_REF_NAME == 'refs/heads/release' ]]; then 37 | echo "DEPLOY_ENVIRONMENT=Development" >> "$GITHUB_OUTPUT" 38 | else 39 | echo "DEPLOY_ENVIRONMENT=Development" >> "$GITHUB_OUTPUT" 40 | fi 41 | - name: merge backend and common folder 42 | run: | 43 | echo "Prepare backend source for enviroment [${{ steps.set-deploy-env.outputs.DEPLOY_ENVIRONMENT }}]" 44 | mkdir -p ./target/backend 45 | cp -r ./apps/backend ./target 46 | cp -r ./common/*.* ./target/backend 47 | 48 | - name: Create and start virtual environment in backend folder 49 | if: ${{ !env.DO_BUILD_DURING_DEPLOYMENT }} 50 | run: | 51 | python -m venv ./target/backend/venv 52 | source ./target/backend/venv/bin/activate 53 | 54 | - name: Install backend dependencies 55 | if: ${{ !env.DO_BUILD_DURING_DEPLOYMENT }} 56 | run: pip install -r ./target/backend/requirements.txt 57 | 58 | - name: merge frontend and common folder 59 | run: | 60 | echo "Prepare frontend source for enviroment [${{ steps.set-deploy-env.outputs.DEPLOY_ENVIRONMENT }}]" 61 | mkdir -p ./target/frontend 62 | cp -r ./apps/frontend ./target 63 | cp -r ./common/*.* ./target/frontend 64 | 65 | - name: Create and start virtual environment in frontend folder 66 | if: ${{ !env.DO_BUILD_DURING_DEPLOYMENT }} 67 | run: | 68 | python -m venv ./target/frontend/venv 69 | source ./target/frontend/venv/bin/activate 70 | 71 | - name: Install frontend dependencies 72 | if: ${{ !env.DO_BUILD_DURING_DEPLOYMENT }} 73 | run: pip install -r ./target/frontend/requirements.txt 74 | # Optional: Add step to run tests here (PyTest, Django test suites, etc.) 75 | 76 | - name: Upload artifacts for backend deployment jobs 77 | uses: actions/upload-artifact@v2 78 | with: 79 | name: python-backend-app 80 | path: | 81 | ./target/backend 82 | 83 | - name: Upload artifacts for frontend deployment jobs 84 | uses: actions/upload-artifact@v2 85 | with: 86 | name: python-frontend-app 87 | path: | 88 | ./target/frontend 89 | 90 | deploy: 91 | runs-on: ubuntu-latest 92 | needs: build 93 | environment: 94 | name: ${{ needs.build.outputs.env-name}} 95 | url: ${{ steps.deploy-frontend-to-webapp.outputs.webapp-url }} 96 | 97 | steps: 98 | - name: Download backend artifact from build job 99 | uses: actions/download-artifact@v2 100 | with: 101 | name: python-backend-app 102 | path: ./backend 103 | 104 | - name: 'Deploy backend to Azure Web App' 105 | uses: azure/webapps-deploy@v2 106 | id: deploy-backend-to-webapp 107 | with: 108 | app-name: ${{ vars.AZURE_WEBAPP_BACKEND_NAME }} 109 | package: ./backend 110 | publish-profile: ${{ secrets.AZUREAPPSERVICE_BACKEND_PUBLISHPROFILE}} 111 | 112 | - name: Download frontend artifact from build job 113 | uses: actions/download-artifact@v2 114 | with: 115 | name: python-frontend-app 116 | path: ./frontend 117 | 118 | - name: 'Deploy frontend to Azure Web App' 119 | uses: azure/webapps-deploy@v2 120 | id: deploy-frontend-to-webapp 121 | with: 122 | app-name: ${{ vars.AZURE_WEBAPP_FRONTEND_NAME }} 123 | package: ./frontend 124 | publish-profile: ${{ secrets.AZUREAPPSERVICE_FRONTEND_PUBLISHPROFILE}} 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .amlignore 2 | .zip 3 | .chroma/ 4 | .ipynb_checkpoints/ 5 | .ipynb_aml_checkpoints/ 6 | __pycache__/ 7 | common/__pycache__/ 8 | .streamlit/ 9 | *.amltmp 10 | *.amltemp 11 | credentials.env 12 | .azure/ 13 | .vscode/ 14 | infra/target/ 15 | **/__pycache__/ 16 | -------------------------------------------------------------------------------- /13-Building-Apps.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1ccf7ea5-1abe-4401-a8c7-64bbfc057425", 6 | "metadata": {}, 7 | "source": [ 8 | "# Building the Bot Service Backend and Frontend Applications" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "78574a83-1d13-4e99-be84-ddcc5f2c011e", 14 | "metadata": {}, 15 | "source": [ 16 | "In the previous notebook, we assembled all the functions and code required to create a robust Agentic ChatBot. Depending on the user's question, this Agent/Bot searches for answers in the available sources and tools.\n", 17 | "\n", 18 | "However, the question arises: **\"How can we integrate this code into a Bot backend application capable of supporting multiple channel deployments?\"** Our ideal scenario involves building the bot once and deploying it across various channels such as MS Teams, Web Chat, Slack, Alexa, Outlook, WhatsApp, Line, Facebook, and more.\n", 19 | "\n", 20 | "\n", 21 | "To achieve this, we need a service that not only aids in building the bot as an API but also facilitates the exposure of this API to multiple channels. This service is known as Azure Bot Framework.\n", 22 | "\n", 23 | "In this notebook, you will learn how to deploy the code you have developed so far as a Bot API using the Bot Framework API and Service.
" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "id": "0a8858d8-c89c-4985-9164-b79cf9c530e3", 29 | "metadata": {}, 30 | "source": [ 31 | "## What is the Azure Bot Framework and Bot Service?" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "id": "3db318f3-f0f1-4328-a82e-9bb7f2a0eddf", 37 | "metadata": {}, 38 | "source": [ 39 | "Microsoft Bot Framework and Azure Bot Service are a collection of libraries, tools, and services that let you build, test, deploy, and manage intelligent bots.\n", 40 | "\n", 41 | "Bots are often implemented as a web application, hosted in Azure and using APIs to send and receive messages.\n", 42 | "\n", 43 | "Azure Bot Service and the Bot Framework include:\n", 44 | "\n", 45 | "- Bot Framework SDKs for developing bots in C#, JavaScript, Python, or Java.\n", 46 | "- CLI tools for help with end-to-end bot development.\n", 47 | "- Bot Connector Service, which relays messages and events between bots and channels.\n", 48 | "- Azure resources for bot management and configuration." 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "id": "e398cb34-3735-40ca-8dbf-3c50582e2213", 54 | "metadata": {}, 55 | "source": [ 56 | "So, in order to build our application we would use the **Bot Framework Python SDK to build the Web API**, and the **Bot Service to connect our API to mutiple channels**." 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "id": "f2905d00-c1c4-4fa8-8b4e-23c6fd0c1acc", 62 | "metadata": {}, 63 | "source": [ 64 | "## Architecture\n", 65 | "\n", 66 | "The image below shows:\n", 67 | "1) An Azure Web App hostoing the Bot API\n", 68 | "2) Azure Bot Service providing the connection between the Bot API, Channels and Application Insights" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "id": "25987e8c-c5fe-45c2-8547-b0f66b3faf0d", 74 | "metadata": {}, 75 | "source": [ 76 | "![Botframework](./images/Bot-Framework.png)" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "d31a7289-ca58-4bec-a977-aac3f755ea7f", 82 | "metadata": {}, 83 | "source": [ 84 | "# Backend - Azure Web App - Bot API" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "20c1936c-d084-4694-97eb-1ebd21fd5fe1", 90 | "metadata": {}, 91 | "source": [ 92 | "All the functions and prompts used in the prior notebook to create our brain Agent are located in `utils.py` and `prompts.py` respectively.\n", 93 | "So, what needs to be done is, basically, to do the same we did in the prior notebook but within the Bot Framework Python SDK classes.\n", 94 | "\n", 95 | "Within the `apps/backend/botservice/app/` folder, you will find three files: `config.py`, `app.py`, `graph.py` and `bot.py`.\n", 96 | "- `config.py`: declares the PORT the API will listen from and declares variables used in app.py\n", 97 | "- `app.py`: is the entrance main point to the application.\n", 98 | "- `graph.py`: all agent creation and logic\n", 99 | "- `bot.py`: compiles the graph (with a checkpointer) and runs it per conversation turn\n", 100 | "\n", 101 | "\n", 102 | "in `apps/backend/botservice/README.md` you will find all the instructions on how to:\n", 103 | "1) Deploy the Azure web services: Azure Web App and Azure Bot Service\n", 104 | "2) Zip the code and uploaded to the Azure Web App\n", 105 | "3) Test your Bot API using the Bot Service in the Azure portal\n", 106 | "\n", 107 | "GO AHEAD NOW AND FOLLOW THE INSTRUCTIONS in `apps/backend/botservice/README.md`\n" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "id": "8ba1f125-2cc7-48ca-a047-5054f2f4ed37", 113 | "metadata": {}, 114 | "source": [ 115 | "# Frontend - Azure Web App - Streamlit " 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "b9cb19fc-cd64-428c-8f2b-1963ff9fc4fb", 121 | "metadata": {}, 122 | "source": [ 123 | "Once you have the Backend Bot API app running and succesfully tested using the Bot Service Azure portal , we can proceed now to build a sample UI.\n", 124 | "\n", 125 | "In `apps/frontend/` folder you will find the files necesary to build a simple Streamlit application that will have:\n", 126 | "\n", 127 | "1) A Search Interface: Using `utils.py` and `prompts.py` and streamlit functions\n", 128 | "2) A BotService Chat Interface: Using the Bot Service Web Chat javascript library we can render the WebChat Channel inside Streamlit as an html component\n", 129 | "3) A FastAPI Chat Interface: Using a FastAPI as backend, we use streamlit components to provide a streaming chat interface (MORE ON THIS LATER)\n", 130 | "\n", 131 | "Notice that in (1) the logic code is running in the Frontend Web App, however in (2) and (3) the logic code is running in the Backend Bot API and the Frontend is just using the WebChat channel from the Bot Service.\n", 132 | "\n", 133 | "GO AHEAD NOW AND FOLLOW THE INSTRUCTIONS in `apps/frontend/README.md`" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "e0301fa7-1eb9-492a-918d-5c36ca5cce90", 139 | "metadata": {}, 140 | "source": [ 141 | "# Reference" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "id": "bdcdefab-7056-4990-b938-8e82b8dd9501", 147 | "metadata": {}, 148 | "source": [ 149 | "- https://learn.microsoft.com/en-us/azure/bot-service/bot-service-overview?view=azure-bot-service-4.0\n", 150 | "- https://github.com/microsoft/botbuilder-python/tree/main\n", 151 | "- https://github.com/microsoft/BotFramework-WebChat/tree/master" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "33ebcb0f-f620-4e1c-992c-c316466c3291", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [] 161 | } 162 | ], 163 | "metadata": { 164 | "kernelspec": { 165 | "display_name": "Python 3.10 - SDK v2", 166 | "language": "python", 167 | "name": "python310-sdkv2" 168 | }, 169 | "language_info": { 170 | "codemirror_mode": { 171 | "name": "ipython", 172 | "version": 3 173 | }, 174 | "file_extension": ".py", 175 | "mimetype": "text/x-python", 176 | "name": "python", 177 | "nbconvert_exporter": "python", 178 | "pygments_lexer": "ipython3", 179 | "version": "3.10.14" 180 | } 181 | }, 182 | "nbformat": 4, 183 | "nbformat_minor": 5 184 | } 185 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /GPT-Smart-Search-Architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/GPT-Smart-Search-Architecture.vsdx -------------------------------------------------------------------------------- /Intro AOAI GPT Azure Smart Search Engine Accelerator.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/Intro AOAI GPT Azure Smart Search Engine Accelerator.pptx -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. 2 | 3 | MIT License 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. 22 | -------------------------------------------------------------------------------- /Latest_Release_Notes.md: -------------------------------------------------------------------------------- 1 | ## Release Notes 2 | ### Version: 4.0.0 3 | 4 | - Updated API versions: 5 | - Azure AI Search: 2024-11-01-preview 6 | - Azure OpenAI: 2024-10-01-preview 7 | - Datasets are now included in the github repo 8 | - Dataset for Notebook 1 is now the diaglogues of each episode of the TV Show: Friends 9 | - This will make the VBD delivery more fun for the attendees 10 | - Added latest compression techniques to Indexes 11 | - Notebooks 1 and 2 now compress vector indexes size up to 90% 12 | - Every notebook and agents are updated to work with gpt-4o and gpt-4o-mini models. 13 | - Environments has been updated to use Python 3.12 14 | - All Notebooks has been updated to use agents with LangGraph 15 | - Added CosmosDB Checkpointer for LangGraph (in process to be added to Langchain official repo) 16 | - Bot Framework code updated to the latest version 17 | - Bot Service is now Single-Tenant (based on user's feedback regarding security) 18 | - Multi-Modality Notebook 11 Added. 19 | - audio_utils.py contains all the functions to add Whisper, TTS and Azure Speech Service capabilities 20 | - Images and Audio input are now included on the notebooks and on the supervisor architecture 21 | - Remove dependency of LangServe. Now it is FastAPI native only. 22 | - Based on customer's feedback. The FastAPI does not use LangServe code. It is only FastAPI code. 23 | - Implemented /stream endpoint using the Standard SSE Format (Servers side Events). 24 | - Web App frontend now allows for the user to speak to the Agent via the microphone 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/113465005/226238596-cc76039e-67c2-46b6-b0bb-35d037ae66e1.png) 2 | 3 | # AI Multi-Agent Architecture 3 or 5 days POC: Build Intelligent Agents with Azure Services 4 | 5 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator?quickstart=1) 6 | [![Open in VS Code Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator) 7 | 8 | 9 | Welcome to the **AI Multi-Agent Architecture Workshop**, designed for organizations seeking to unlock the power of AI-driven intelligent agents. Over this 3-to-5-day interactive workshop, Microsoft architects will guide you step-by-step to build a private, secure AI system tailored to your business needs. 10 | 11 | This workshop will teach you how to develop a **multi-agent system** capable of comprehending diverse datasets across various locations. These intelligent agents can answer questions with detailed explanations and source references, providing your organization with a powerful, ChatGPT-like experience designed for enterprise use. 12 | 13 | ## What You'll Build 14 | 15 | This hands-on workshop will walk you through creating a Proof of Concept (POC) for a **Generative AI Multi-Agent Architecture** using Azure Services. By the end of the workshop, you'll have built: 16 | 17 | 1. **A Scalable Backend** 18 | Developed with Bot Framework and FastAPI, the backend serves as the engine connecting AI logic to multiple communication channels, including: 19 | - Web Chat 20 | - Microsoft Teams 21 | - SMS 22 | - Email 23 | - Slack, and more! 24 | 25 | 2. **A User-Friendly Frontend** 26 | Build a web application that combines: 27 | - A **search engine** capable of querying your data intelligently. 28 | - A **bot UI** for seamless conversational experiences. 29 | 30 | 3. **A RAG-Based Multi-Agent Architecture** 31 | Leverage Retrieval-Augmented Generation (RAG) to enable your agents to retrieve precise information and generate accurate responses. 32 | 33 | ## Workshop Highlights 34 | 35 | - **Step-by-Step Guidance**: Each module builds upon the previous one, progressively introducing you to real-world AI architecture concepts. 36 | - **Custom Enterprise AI**: Create intelligent agents that understand your organization’s data while maintaining privacy and security. 37 | - **Multi-Channel Capabilities**: Deploy your agents across various platforms for broad accessibility. 38 | - **Practical Experience**: Learn by doing, with notebooks and code samples tailored for an enterprise setting. 39 | 40 | ## Why Attend? 41 | 42 | By the end of the workshop, you'll have a working knowledge of how to design, build, and deploy AI agents in a multi-agentic architecture. This hands-on experience will help you understand the value of Azure-powered Generative AI in solving real-world business problems. 43 | 44 | --- 45 | 46 | ## For Microsoft Employees 47 | 48 | This is a **customer-funded Value-Based Delivery (VBD)**. Below, you'll find all the assets and resources needed for a successful workshop delivery. 49 | 50 | 51 | | **Item** | **Description** | **Link** | 52 | |----------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| 53 | | VBD SKU Info and Datasheet | CSAM must dispatch it as "Customer Invested" against credits/hours of Unified Support Contract. Customer decides if 3 or 5 days. | [ESXP SKU page](https://esxp.microsoft.com/#/omexplanding/services/14486/geo/USA/details/1) | 54 | | VBD Accreditation for CSAs | Links for CSAs to get the Accreditation needed to deliver the workshop | [Link 1](https://learningplayer.microsoft.com/activity/s9261799/launch) , [Link 2](https://learningplayer.microsoft.com/activity/s9264662/launch) | 55 | | VBD 3-5 day POC Asset (IP) | The MVP to be delivered (this GitHub repo) | [Azure-Cognitive-Search-Azure-OpenAI-Accelerator](https://github.com/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator) | 56 | | VBD Workshop Deck | The deck introducing and explaining the workshop | [Intro AOAI GPT Azure Smart Search Engine Accelerator.pptx](https://github.com/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator/blob/main/Intro%20AOAI%20GPT%20Azure%20Smart%20Search%20Engine%20Accelerator.pptx) | 57 | | CSA Training Video | 2 Hour Training for Microsoft CSA's | [POC VBD Training Recording](https://microsoft-my.sharepoint.com/:v:/p/annagross/ETONCWUYCa5EtpmnYjYy9eABK1JV1yo49HDoYjnry1C8-A) | 58 | 59 | 60 | --- 61 | ## **Prerequisites Client 3-5 Days POC** 62 | * Azure subscription 63 | * Microsoft members preferably to be added as Guests in clients Azure AD. If not possible, then customers can issue corporate IDs to Microsoft members 64 | * A Resource Group (RG) needs to be set for this Workshop POC, in the customer Azure tenant 65 | * The customer team and the Microsoft team must have Contributor permissions to this resource group so they can set everything up 2 weeks prior to the workshop 66 | * Customer Data/Documents must be uploaded to the blob storage account, at least two weeks prior to the workshop date 67 | * A Single-Tenant App Registration (Service Principal) must be created by the customer (save the Client Id and Secret Value). 68 | * Customer must provide the Microsoft Team , 10-20 questions (easy to hard) that they want the Agent/Bot to respond correctly. 69 | * For IDE collaboration and standarization during workshop, AML compute instances with Jupyper Lab will be used, for this, Azure Machine Learning Workspace must be deployed in the RG 70 | * Note: Please ensure you have enough core compute quota in your Azure Machine Learning workspace 71 | 72 | --- 73 | ## Architecture 74 | ![Architecture](./images/GPT-Smart-Search-Architecture.jpg "Architecture") 75 | 76 | ## Flow 77 | 1. The user asks a question. 78 | 2. In the backend app, an Agent determines which source to use based on the user input 79 | 3. Five types of sources are available: 80 | * 3a. Azure SQL Database - contains COVID-related statistics in the US. 81 | * 3b. API Endpoints - RESTful OpenAPI 3.0 API from a online currency broker. 82 | * 3c. Azure Bing Search API - provides access to the internet allowing scenerios like: QnA on public websites . 83 | * 3d. Azure AI Search - contains AI-enriched documents from Blob Storage: 84 | - Transcripts of the dialogue of all the episodes of the TV Show: FRIENDS 85 | - 90,000 Covid publication abstracts 86 | - 4 lenghty PDF books 87 | * 3f. CSV Tabular File - contains COVID-related statistics in the US. 88 | 4. The Agent retrieves the result from the correct source and crafts the answer. 89 | 5. The Agent state is saved to CosmosDB as persistent memory and for further analysis. 90 | 6. The answer is delivered to the user. 91 | 92 | --- 93 | ## Demo 94 | 95 | [https://gptsmartsearch-frontend.azurewebsites.net](https://gptsmartsearch-frontend.azurewebsites.net) 96 | 97 | 98 | --- 99 | 100 | ## 🔧**Features** 101 | 102 | - 100% Python. 103 | - Uses [Azure Cognitive Services](https://azure.microsoft.com/en-us/products/cognitive-services/) to index and enrich unstructured documents: OCR over images, Chunking and automated vectorization. 104 | - Uses Hybrid Search Capabilities of Azure AI Search to provide the best semantic answer (Text and Vector search combined). 105 | - Uses [LangChain](https://langchain.readthedocs.io/en/latest/) as a wrapper for interacting with Azure OpenAI , vector stores, constructing prompts and creating agents. 106 | - Multi-Agentic Architecture using LangGraph. 107 | - Multi-Lingual (ingests, indexes and understand any language) 108 | - Multi-Index -> multiple search indexes 109 | - Multi-modal input and output (text and audio) 110 | - Tabular Data Q&A with CSV files and SQL flavor Databases 111 | - Uses [Azure AI Document Intelligence SDK (former Form Recognizer)](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/overview?view=doc-intel-3.0.0) to parse complex/large PDF documents 112 | - Uses [Bing Search API](https://www.microsoft.com/en-us/bing/apis) to power internet searches and Q&A over public websites. 113 | - Connects to API Data sources by converting natural language questions to API calls. 114 | - Uses CosmosDB as persistent memory to save user's conversations. 115 | - Uses [Streamlit](https://streamlit.io/) to build the Frontend web application in python. 116 | - Uses [Bot Framework](https://dev.botframework.com/) and [Bot Service](https://azure.microsoft.com/en-us/products/bot-services/) to Host the Bot API Backend and to expose it to multiple channels including MS Teams. 117 | - Uses also FastAPI to deploy an alternative backend API with streaming capabilites 118 | 119 | --- 120 | 121 | ## **Steps to Run the POC/Accelerator** 122 | 123 | ### **Pre-requisite** 124 | You must have an **Azure OpenAI Service** already created. 125 | 126 | ### **1. Fork the Repository** 127 | - Fork this repository to your GitHub account. 128 | 129 | ### **2. Deploy Required Models** 130 | In **Azure OpenAI Studio**, deploy the following models: 131 | *(Note: Older versions of these models will not work)* 132 | 133 | - `gpt-4o` 134 | - `gpt-4o-mini` 135 | - `text-embedding-3-large` 136 | - `tts` 137 | - `whisper` 138 | 139 | ### **3. Create a Resource Group** 140 | - Create a **Resource Group (RG)** to house all the assets for this accelerator. 141 | - Note: Azure OpenAI services can exist in a different RG or even a different subscription. 142 | 143 | ### **4. Deploy Azure Infrastructure** 144 | Click the button below to deploy all necessary Azure infrastructure (e.g., Azure AI Search, Cognitive Services, etc.): 145 | 146 | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fpablomarin%2FGPT-Azure-Search-Engine%2Fmain%2Fazuredeploy.json) 147 | 148 | **Important:** 149 | If this is your first time creating an **Azure AI Services Multi-Service Account**, do the following manually: 150 | 1. Go to the Azure portal. 151 | 2. Create the account. 152 | 3. Read and accept the **Responsible AI Terms**. 153 | Once done, delete this manually created account and then use the above deployment button. 154 | 155 | ### **5. Choose Your Development Environment** 156 | 157 | #### **Option A: Azure Machine Learning (Preferred)** 158 | 1. **Clone** your forked repository to your AML Compute Instance. 159 | - If your repository is private, refer to the **Troubleshooting** section for guidance on cloning private repos. 160 | 2. Install the dependencies in a Conda environment. Run the following commands on the **Python 3.12 Conda environment** you plan to use for the notebooks: 161 | 162 | ```bash 163 | conda create -n GPTSearch python=3.12 164 | conda activate GPTSearch 165 | pip install -r ./common/requirements.txt 166 | conda install ipykernel 167 | python -m ipykernel install --user --name=GPTSearch --display-name "GPTSearch (Python 3.12)" 168 | ``` 169 | 170 | #### **Option B: Visual Studio Code** 171 | 1. **Create a Python virtual environment (.venv):** 172 | - When creating the virtual environment, select the `./common/requirements.txt` file. 173 | - Alternatively, install dependencies manually: 174 | ```bash 175 | pip install -r ./common/requirements.txt 176 | ``` 177 | 2. **Activate the virtual environment:** 178 | ```bash 179 | .venv\scripts\activate 180 | ``` 181 | 3. Install `ipykernel`: 182 | ```bash 183 | pip install ipykernel 184 | ``` 185 | 186 | ### **6. Configure Credentials** 187 | Edit the `credentials.env` file with the appropriate values from the services created in Step 4. 188 | - To obtain `BLOB_SAS_TOKEN` and `BLOB_CONNECTION_STRING`, navigate to: 189 | **Storage Account > Security + Networking > Shared Access Signature > Generate SAS** 190 | 191 | ### **7. Run the Notebooks** 192 | - Execute the notebooks **in order**, as they build on top of each other. 193 | - Use the appropriate kernel: 194 | - For **AML**, select: `GPTSearch (Python 3.12)` 195 | - For **VS Code**, select the `.venv` kernel. 196 | 197 | ### **Troubleshooting** 198 | - If cloning a private repository: Refer to the detailed guide [here](#). 199 | - For issues with dependency installation: Ensure your Python version matches the required version. 200 | 201 | --- 202 | 203 | 204 |
205 | 206 | Troubleshooting 207 | 208 | ## Troubleshooting 209 | 210 | Steps to clone a private repo: 211 | - On your Terminal, Paste the text below, substituting in your GitHub email address. [Generate a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key). 212 | ```bash 213 | ssh-keygen -t ed25519 -C "your_email@example.com" 214 | ``` 215 | - Copy the SSH public key to your clipboard. [Add a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key). 216 | ```bash 217 | cat ~/.ssh/id_ed25519.pub 218 | # Then select and copy the contents of the id_ed25519.pub file 219 | # displayed in the terminal to your clipboard 220 | ``` 221 | - On GitHub, go to **Settings-> SSH and GPG Keys-> New SSH Key** 222 | - In the "Title" field, add a descriptive label for the new key. "AML Compute". In the "Key" field, paste your public key. 223 | - Clone your private repo 224 | ```bash 225 | git clone git@github.com:YOUR-USERNAME/YOUR-REPOSITORY.git 226 | ``` 227 |
228 | 229 | ## Contributing 230 | 231 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 232 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 233 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 234 | 235 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 236 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 237 | provided by the bot. You will only need to do this once across all repos using our CLA. 238 | 239 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 240 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 241 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 242 | 243 | ## Trademarks 244 | 245 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 246 | trademarks or logos is subject to and must follow 247 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 248 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 249 | Any use of third-party trademarks or logos are subject to those third-party's policies. 250 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 10 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 11 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 12 | 13 | ## Microsoft Support Policy 14 | 15 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 16 | -------------------------------------------------------------------------------- /apps/.gitignore: -------------------------------------------------------------------------------- 1 | # Local data 2 | data/local_data/ 3 | 4 | # Secrets 5 | .streamlit/secrets.toml 6 | 7 | # VSCode 8 | .vscode/ 9 | 10 | # TODO 11 | TODO.md 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pdm 98 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 99 | #pdm.lock 100 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 101 | # in version control. 102 | # https://pdm.fming.dev/#use-with-ide 103 | .pdm.toml 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | # PyCharm 149 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 150 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 151 | # and can be added to the global gitignore or merged into this file. For a more nuclear 152 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 153 | #.idea/ 154 | 155 | 156 | -------------------------------------------------------------------------------- /apps/backend/botservice/README.md: -------------------------------------------------------------------------------- 1 |

2 | Backend Web Application - Bot API + Bot Service 3 |

4 | 5 | This bot has been created using [Bot Framework](https://dev.botframework.com). 6 | 7 | Services and tools used: 8 | 9 | - Azure App Service (Web App) - Chatbot API Hosting 10 | - Azure Bot Service - A service for managing communication through various channels 11 | 12 | ## Deploy Bot To Azure Web App 13 | 14 | Below are the steps to run the Bot API as an Azure Wep App, connected with the Azure Bot Service that will expose the bot to multiple channels including: Web Chat, MS Teams, Twilio, SMS, Email, Slack, etc.. 15 | 16 | 1. In Azure Portal: In Azure Active Directory->App Registrations, Create an Single-Tenant App Registration (Service Principal), create a Secret (and take note of the value) 17 | 18 | 2. Deploy the Bot Web App and the Bot Service by clicking the Button below and type the App Registration ID and Secret Value that you got in Step 1 along with all the other ENV variables you used in the Notebooks 19 | 20 | [![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fpablomarin%2FGPT-Azure-Search-Engine%2Fmain%2Fapps%2Fbackend%2Fbotservice%2Fazuredeploy-backend.json) 21 | 22 | 3. Zip the code of the bot by executing the following command in the terminal (**you have to be inside the apps/backend/botservice/ folder**): 23 | ```bash 24 | (cd ../../../ && zip -r apps/backend/botservice/backend.zip common data/openapi_kraken.json data/all-states-history.csv) && zip -j backend.zip ../../../common/requirements.txt app/* 25 | ``` 26 | 4. Using the Azure CLI deploy the bot code to the Azure App Service created on Step 2 27 | ```bash 28 | az login -i 29 | az webapp deployment source config-zip --resource-group "" --name "" --src "backend.zip" 30 | ``` 31 | **Note**: If you get this error: `An error occured during deployment. Status Code: 401`. **Cause**: Some FDPO Azure Subscriptions disable Azure Web Apps Basic Authentication every minute (don't know why). **Solution**: before running the above `az webapp deployment` command, make sure that your backend azure web app has `Basic Authentication ON`. In the Azure Portal, you can find this settting in: `Configuration->General Settings`. 32 | Don't worry if after running the command it says retrying many times, the zip files already uploaded and is building. 33 | 34 | 5. In the Azure Portal: **Wait around 5 minutes** and test your bot by going to your Azure Bot Service created in Step 2 and clicking on: **Test in Web Chat** 35 | 36 | 6. In the Azure Portal: In your Bot Service , add multiple channels (Including Teams) by clicking in **Channels** 37 | 38 | 7. Go to apps/frontend folder and follow the steps in README.md to deploy a Frontend application that uses the bot. 39 | 40 | ## Reference documentation 41 | 42 | - [Bot Framework Documentation](https://docs.botframework.com) 43 | - [Bot Samples code](https://github.com/microsoft/BotBuilder-Samples) 44 | - [Bot Framework Python SDK](https://github.com/microsoft/botbuilder-python/tree/main) 45 | - [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) 46 | - [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) 47 | - [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) 48 | - [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) 49 | -------------------------------------------------------------------------------- /apps/backend/botservice/app/app.py: -------------------------------------------------------------------------------- 1 | # app.py 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. 4 | # Licensed under the MIT License. 5 | # ----------------------------------------------------------------------------- 6 | 7 | import os 8 | import sys 9 | import asyncio 10 | import traceback 11 | from datetime import datetime 12 | 13 | from aiohttp import web 14 | from aiohttp.web import Request, Response, json_response 15 | from botbuilder.core import TurnContext 16 | from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationBotFrameworkAuthentication 17 | from botbuilder.core.integration import aiohttp_error_middleware 18 | from botbuilder.schema import Activity, ActivityTypes 19 | 20 | from bot import MyBot 21 | from config import DefaultConfig 22 | 23 | # ---- Imports for CosmosDB checkpointer usage 24 | from common.cosmosdb_checkpointer import AsyncCosmosDBSaver 25 | from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer 26 | 27 | CONFIG = DefaultConfig() 28 | 29 | # Create adapter. 30 | # See https://aka.ms/about-bot-adapter to learn more about how bots work. 31 | ADAPTER = CloudAdapter(ConfigurationBotFrameworkAuthentication(CONFIG)) 32 | 33 | # Catch-all for errors 34 | async def on_error(context: TurnContext, error: Exception): 35 | # This check writes out errors to console log .vs. app insights. 36 | # NOTE: In production environment, you should consider logging this to Azure 37 | # application insights. 38 | print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr) 39 | traceback.print_exc() 40 | 41 | # Send a message to the user 42 | await context.send_activity("The bot encountered an error or bug.") 43 | await context.send_activity( 44 | "To continue to run this bot, please fix the bot source code." 45 | ) 46 | # Send a trace activity if we're talking to the Bot Framework Emulator 47 | if context.activity.channel_id == "emulator": 48 | # Create a trace activity that contains the error object 49 | trace_activity = Activity( 50 | label="TurnError", 51 | name="on_turn_error Trace", 52 | timestamp=datetime.utcnow(), 53 | type=ActivityTypes.trace, 54 | value=f"{error}", 55 | value_type="https://www.botframework.com/schemas/error", 56 | ) 57 | # Send a trace activity, which will be displayed in Bot Framework Emulator 58 | await context.send_activity(trace_activity) 59 | 60 | ADAPTER.on_turn_error = on_error 61 | 62 | # ----------------------------------------------------------------------------- 63 | # 1) Create a single, shared AsyncCosmosDBSaver instance for the entire service. 64 | # ----------------------------------------------------------------------------- 65 | 66 | checkpointer_async = AsyncCosmosDBSaver( 67 | endpoint=os.environ.get("AZURE_COSMOSDB_ENDPOINT"), 68 | key=os.environ.get("AZURE_COSMOSDB_KEY"), 69 | database_name=os.environ.get("AZURE_COSMOSDB_NAME"), 70 | container_name=os.environ.get("AZURE_COSMOSDB_CONTAINER_NAME"), 71 | serde=JsonPlusSerializer(), 72 | ) 73 | 74 | # Setup the checkpointer (async). We can do so using run_until_complete here: 75 | loop = asyncio.get_event_loop() 76 | loop.run_until_complete(checkpointer_async.setup()) 77 | 78 | # ----------------------------------------------------------------------------- 79 | # 2) Pass that single checkpointer to the bot. 80 | # ----------------------------------------------------------------------------- 81 | BOT = MyBot(cosmos_checkpointer=checkpointer_async) 82 | 83 | # Listen for incoming requests on /api/messages 84 | async def messages(req: Request) -> Response: 85 | return await ADAPTER.process(req, BOT) 86 | 87 | 88 | APP = web.Application(middlewares=[aiohttp_error_middleware]) 89 | APP.router.add_post("/api/messages", messages) 90 | 91 | if __name__ == "__main__": 92 | try: 93 | web.run_app(APP, host="localhost", port=CONFIG.PORT) 94 | except Exception as error: 95 | raise error -------------------------------------------------------------------------------- /apps/backend/botservice/app/bot.py: -------------------------------------------------------------------------------- 1 | # bot.py 2 | from botbuilder.core import ActivityHandler, TurnContext 3 | from botbuilder.schema import ChannelAccount, Activity, ActivityTypes 4 | from common.cosmosdb_checkpointer import AsyncCosmosDBSaver 5 | from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer 6 | 7 | from common.graph import build_async_workflow 8 | from common.prompts import WELCOME_MESSAGE 9 | 10 | 11 | class MyBot(ActivityHandler): 12 | def __init__(self, cosmos_checkpointer=None): 13 | super().__init__() 14 | self.checkpointer = cosmos_checkpointer 15 | 16 | csv_file_path = "data/all-states-history.csv" 17 | api_file_path = "data/openapi_kraken.json" 18 | 19 | # 1) Build the multi-agent workflow 20 | workflow = build_async_workflow(csv_file_path,api_file_path) 21 | 22 | # 2) Compile with the checkpointer 23 | self.graph_async = workflow.compile(checkpointer=self.checkpointer) 24 | 25 | 26 | # Function to show welcome message to new users 27 | async def on_members_added_activity(self, members_added: ChannelAccount, turn_context: TurnContext): 28 | for member_added in members_added: 29 | if member_added.id != turn_context.activity.recipient.id: 30 | await turn_context.send_activity(WELCOME_MESSAGE) 31 | 32 | 33 | # See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types. 34 | async def on_message_activity(self, turn_context: TurnContext): 35 | session_id = turn_context.activity.conversation.id 36 | user_text = turn_context.activity.text or "" 37 | 38 | await turn_context.send_activity(Activity(type=ActivityTypes.typing)) 39 | 40 | config_async = {"configurable": {"thread_id": session_id}} 41 | inputs = {"messages": [("human", user_text)]} 42 | 43 | # 3) Invoke the multi-agent workflow 44 | result = await self.graph_async.ainvoke(inputs, config=config_async) 45 | 46 | # 4) The final answer is in the last message 47 | final_answer = result["messages"][-1].content 48 | 49 | await turn_context.send_activity(final_answer) 50 | -------------------------------------------------------------------------------- /apps/backend/botservice/app/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. 4 | 5 | import os 6 | 7 | class DefaultConfig: 8 | """ Bot Configuration """ 9 | 10 | PORT = 3978 11 | APP_ID = os.environ.get("MicrosoftAppId", "") 12 | APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "") 13 | APP_TYPE = os.environ.get("MicrosoftAppType", "SingleTenant") 14 | APP_TENANTID = os.environ.get("MicrosoftAppTenantId", "") -------------------------------------------------------------------------------- /apps/backend/botservice/app/runserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start the BotService API on port 8000 4 | gunicorn --bind 0.0.0.0:8000 --worker-class aiohttp.worker.GunicornWebWorker --timeout 300 app:APP & 5 | 6 | # Wait for any processes to exit 7 | wait -n 8 | 9 | # Exit with the status of the process that exited first 10 | exit $? 11 | -------------------------------------------------------------------------------- /apps/backend/botservice/azuredeploy-backend.bicep: -------------------------------------------------------------------------------- 1 | @description('Required. Active Directory App ID.') 2 | param appId string 3 | 4 | @description('Required. Active Directory App Secret Value.') 5 | @secure() 6 | param appPassword string 7 | 8 | @description('Required. App Registration type SingleTenant, MultiTenant.') 9 | param appType string = 'SingleTenant' 10 | 11 | @description('Required. Microsoft Tenant ID.') 12 | param TenantId string 13 | 14 | @description('Required. The SAS token for the blob hosting your data.') 15 | @secure() 16 | param blobSASToken string 17 | 18 | @description('Required. The name of the resource group where the resources (Azure Search etc.) where deployed previously. Defaults to current resource group.') 19 | param resourceGroupSearch string = resourceGroup().name 20 | 21 | @description('Required. The name of the Azure Search service deployed previously.') 22 | param azureSearchName string 23 | 24 | @description('Required. The API version for the Azure Search service.') 25 | param azureSearchAPIVersion string = '2024-11-01-preview' 26 | 27 | @description('Required. The name of the Azure OpenAI resource deployed previously.') 28 | param azureOpenAIName string 29 | 30 | @description('Required. The API key of the Azure OpenAI resource deployed previously.') 31 | @secure() 32 | param azureOpenAIAPIKey string 33 | 34 | @description('Optional. The API version for the Azure OpenAI service.') 35 | param azureOpenAIAPIVersion string = '2024-10-01-preview' 36 | 37 | @description('Required. The deployment name for the GPT-4o-mini model.') 38 | param azureOpenAIGPT4oMiniModelName string = 'gpt-4o-mini' 39 | 40 | @description('Required. The deployment name for the GPT-4o-mini model..') 41 | param azureOpenAIGPT4oModelName string = 'gpt-4o' 42 | 43 | @description('Required. The deployment name for the Embedding model.') 44 | param azureOpenAIEmbeddingModelName string = 'text-embedding-3-large' 45 | 46 | @description('Required. The URL for the Bing Search service.') 47 | param bingSearchUrl string = 'https://api.bing.microsoft.com/v7.0/search' 48 | 49 | @description('Required. The name of the Bing Search service deployed previously.') 50 | param bingSearchName string 51 | 52 | @description('Required. The name of the SQL server deployed previously e.g. sqlserver.database.windows.net') 53 | param SQLServerName string 54 | 55 | @description('Required. The name of the SQL Server database.') 56 | param SQLServerDatabase string = 'SampleDB' 57 | 58 | @description('Required. The username for the SQL Server.') 59 | param SQLServerUsername string 60 | 61 | @description('Required. The password for the SQL Server.') 62 | @secure() 63 | param SQLServerPassword string 64 | 65 | @description('Required. The name of the Azure CosmosDB Account.') 66 | param cosmosDBAccountName string 67 | 68 | @description('Required. The name of the Azure CosmosDB container.') 69 | param cosmosDBContainerName string 70 | 71 | @description('Required. The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable.') 72 | param botId string = 'BotId-${uniqueString(resourceGroup().id)}' 73 | 74 | @description('Required, defaults to F0. The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1.') 75 | @allowed([ 76 | 'F0' 77 | 'S1' 78 | ]) 79 | param botSKU string = 'F0' 80 | 81 | @description('Required. The name of the new App Service Plan.') 82 | param appServicePlanName string = 'AppServicePlan-Backend-${uniqueString(resourceGroup().id)}' 83 | 84 | @description('Required, defaults to S3. The SKU of the App Service Plan. Acceptable values are B3, S3 and P2v3.') 85 | @allowed([ 86 | 'B3' 87 | 'S3' 88 | 'P2v3' 89 | ]) 90 | param appServicePlanSKU string = 'S3' 91 | 92 | @description('Required, defaults to resource group location. The location of the resources.') 93 | param location string = resourceGroup().location 94 | 95 | var publishingUsername = '$${botId}' 96 | var webAppName = 'webApp-Backend-${botId}' 97 | var siteHost = '${webAppName}.azurewebsites.net' 98 | var botEndpoint = 'https://${siteHost}/api/messages' 99 | 100 | // Existing Azure Search service. 101 | resource azureSearch 'Microsoft.Search/searchServices@2021-04-01-preview' existing = { 102 | name: azureSearchName 103 | scope: resourceGroup(resourceGroupSearch) 104 | } 105 | 106 | // Existing Bing Search resource. 107 | resource bingSearch 'Microsoft.Bing/accounts@2020-06-10' existing = { 108 | name: bingSearchName 109 | scope: resourceGroup(resourceGroupSearch) 110 | } 111 | 112 | // Existing Azure CosmosDB resource. 113 | resource cosmosDB 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = { 114 | name: cosmosDBAccountName 115 | scope: resourceGroup(resourceGroupSearch) 116 | } 117 | 118 | // Create a new Linux App Service Plan if no existing App Service Plan name was passed in. 119 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { 120 | name: appServicePlanName 121 | location: location 122 | sku: { 123 | name: appServicePlanSKU 124 | } 125 | kind: 'linux' 126 | properties: { 127 | reserved: true 128 | } 129 | } 130 | 131 | // Create a Web App using a Linux App Service Plan. 132 | resource webApp 'Microsoft.Web/sites@2022-09-01' = { 133 | name: webAppName 134 | location: location 135 | tags: { 'azd-service-name': 'backend' } 136 | kind: 'app,linux' 137 | properties: { 138 | enabled: true 139 | hostNameSslStates: [ 140 | { 141 | name: '${webAppName}.azurewebsites.net' 142 | sslState: 'Disabled' 143 | hostType: 'Standard' 144 | } 145 | { 146 | name: '${webAppName}.scm.azurewebsites.net' 147 | sslState: 'Disabled' 148 | hostType: 'Repository' 149 | } 150 | ] 151 | serverFarmId: appServicePlan.id 152 | reserved: true 153 | scmSiteAlsoStopped: false 154 | clientAffinityEnabled: false 155 | clientCertEnabled: false 156 | hostNamesDisabled: false 157 | containerSize: 0 158 | dailyMemoryTimeQuota: 0 159 | httpsOnly: false 160 | siteConfig: { 161 | appSettings: [ 162 | { 163 | name: 'MicrosoftAppId' 164 | value: appId 165 | } 166 | { 167 | name: 'MicrosoftAppPassword' 168 | value: appPassword 169 | } 170 | { 171 | name: 'MicrosoftAppTenantId' 172 | value: TenantId 173 | } 174 | { 175 | name: 'MicrosoftAppType' 176 | value: appType 177 | } 178 | { 179 | name: 'BLOB_SAS_TOKEN' 180 | value: blobSASToken 181 | } 182 | { 183 | name: 'AZURE_SEARCH_ENDPOINT' 184 | value: 'https://${azureSearchName}.search.windows.net' 185 | } 186 | { 187 | name: 'AZURE_SEARCH_KEY' 188 | value: azureSearch.listAdminKeys().primaryKey 189 | } 190 | { 191 | name: 'AZURE_SEARCH_API_VERSION' 192 | value: azureSearchAPIVersion 193 | } 194 | { 195 | name: 'AZURE_OPENAI_ENDPOINT' 196 | value: 'https://${azureOpenAIName}.openai.azure.com/' 197 | } 198 | { 199 | name: 'AZURE_OPENAI_API_KEY' 200 | value: azureOpenAIAPIKey 201 | } 202 | { 203 | name: 'AZURE_OPENAI_API_VERSION' 204 | value: azureOpenAIAPIVersion 205 | } 206 | { 207 | name: 'GPT4oMINI_DEPLOYMENT_NAME' 208 | value: azureOpenAIGPT4oMiniModelName 209 | } 210 | { 211 | name: 'GPT4o_DEPLOYMENT_NAME' 212 | value: azureOpenAIGPT4oModelName 213 | } 214 | { 215 | name: 'EMBEDDING_DEPLOYMENT_NAME' 216 | value: azureOpenAIEmbeddingModelName 217 | } 218 | { 219 | name: 'BING_SEARCH_URL' 220 | value: bingSearchUrl 221 | } 222 | { 223 | name: 'BING_SUBSCRIPTION_KEY' 224 | value: bingSearch.listKeys().key1 225 | } 226 | { 227 | name: 'SQL_SERVER_NAME' 228 | value: SQLServerName 229 | } 230 | { 231 | name: 'SQL_SERVER_DATABASE' 232 | value: SQLServerDatabase 233 | } 234 | { 235 | name: 'SQL_SERVER_USERNAME' 236 | value: SQLServerUsername 237 | } 238 | { 239 | name: 'SQL_SERVER_PASSWORD' 240 | value: SQLServerPassword 241 | } 242 | { 243 | name: 'AZURE_COSMOSDB_NAME' 244 | value: cosmosDBAccountName 245 | } 246 | { 247 | name: 'AZURE_COSMOSDB_CONTAINER_NAME' 248 | value: cosmosDBContainerName 249 | } 250 | { 251 | name: 'AZURE_COSMOSDB_ENDPOINT' 252 | value: cosmosDB.properties.documentEndpoint 253 | } 254 | { 255 | name: 'AZURE_COSMOSDB_KEY' 256 | value: cosmosDB.listKeys().primaryMasterKey 257 | } 258 | { 259 | name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' 260 | value: 'true' 261 | } 262 | ] 263 | cors: { 264 | allowedOrigins: [ 265 | 'https://botservice.hosting.portal.azure.net' 266 | 'https://hosting.onecloud.azure-test.net/' 267 | ] 268 | } 269 | } 270 | } 271 | } 272 | 273 | resource webAppConfig 'Microsoft.Web/sites/config@2022-09-01' = { 274 | parent: webApp 275 | name: 'web' 276 | properties: { 277 | numberOfWorkers: 1 278 | defaultDocuments: [ 279 | 'Default.htm' 280 | 'Default.html' 281 | 'Default.asp' 282 | 'index.htm' 283 | 'index.html' 284 | 'iisstart.htm' 285 | 'default.aspx' 286 | 'index.php' 287 | 'hostingstart.html' 288 | ] 289 | netFrameworkVersion: 'v4.0' 290 | phpVersion: '' 291 | pythonVersion: '' 292 | nodeVersion: '' 293 | linuxFxVersion: 'PYTHON|3.12' 294 | requestTracingEnabled: false 295 | remoteDebuggingEnabled: false 296 | remoteDebuggingVersion: 'VS2022' 297 | httpLoggingEnabled: true 298 | logsDirectorySizeLimit: 35 299 | detailedErrorLoggingEnabled: false 300 | publishingUsername: publishingUsername 301 | scmType: 'None' 302 | use32BitWorkerProcess: true 303 | webSocketsEnabled: false 304 | alwaysOn: true 305 | appCommandLine: 'runserver.sh' 306 | managedPipelineMode: 'Integrated' 307 | virtualApplications: [ 308 | { 309 | virtualPath: '/' 310 | physicalPath: 'site\\wwwroot' 311 | preloadEnabled: false 312 | virtualDirectories: null 313 | } 314 | ] 315 | loadBalancing: 'LeastRequests' 316 | experiments: { 317 | rampUpRules: [] 318 | } 319 | autoHealEnabled: false 320 | vnetName: '' 321 | minTlsVersion: '1.2' 322 | ftpsState: 'AllAllowed' 323 | } 324 | } 325 | 326 | resource bot 'Microsoft.BotService/botServices@2023-09-15-preview' = { 327 | name: botId 328 | location: 'global' 329 | kind: 'azurebot' 330 | sku: { 331 | name: botSKU 332 | } 333 | properties: { 334 | displayName: botId 335 | iconUrl: 'https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png' 336 | endpoint: botEndpoint 337 | msaAppId: appId 338 | msaAppTenantId: TenantId 339 | msaAppType: appType 340 | schemaTransformationVersion: '1.3' 341 | isCmekEnabled: false 342 | } 343 | dependsOn: [ 344 | webApp 345 | ] 346 | } 347 | 348 | output botServiceName string = bot.name 349 | output webAppName string = webApp.name 350 | output webAppUrl string = webApp.properties.defaultHostName 351 | -------------------------------------------------------------------------------- /apps/backend/botservice/azuredeploy-backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.26.54.24096", 8 | "templateHash": "6326872028797937962" 9 | } 10 | }, 11 | "parameters": { 12 | "appId": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "Required. Active Directory App ID." 16 | } 17 | }, 18 | "appPassword": { 19 | "type": "securestring", 20 | "metadata": { 21 | "description": "Required. Active Directory App Secret Value." 22 | } 23 | }, 24 | "appType": { 25 | "type": "string", 26 | "defaultValue": "SingleTenant", 27 | "metadata": { 28 | "description": "Required. App Registration type SingleTenant, MultiTenant" 29 | } 30 | }, 31 | "TenantId": { 32 | "type": "string", 33 | "metadata": { 34 | "description": "Required. Microsoft Tenant ID" 35 | } 36 | }, 37 | "blobSASToken": { 38 | "type": "securestring", 39 | "metadata": { 40 | "description": "Required. The SAS token for the blob hosting your data." 41 | } 42 | }, 43 | "resourceGroupSearch": { 44 | "type": "string", 45 | "defaultValue": "[resourceGroup().name]", 46 | "metadata": { 47 | "description": "Required. The name of the resource group where the resources (Azure Search etc.) where deployed previously. Defaults to current resource group." 48 | } 49 | }, 50 | "azureSearchName": { 51 | "type": "string", 52 | "metadata": { 53 | "description": "Required. The name of the Azure Search service deployed previously." 54 | } 55 | }, 56 | "azureSearchAPIVersion": { 57 | "type": "string", 58 | "defaultValue": "2024-11-01-preview", 59 | "metadata": { 60 | "description": "Required. The API version for the Azure Search service." 61 | } 62 | }, 63 | "azureOpenAIName": { 64 | "type": "string", 65 | "metadata": { 66 | "description": "Required. The name of the Azure OpenAI resource deployed previously." 67 | } 68 | }, 69 | "azureOpenAIAPIKey": { 70 | "type": "securestring", 71 | "metadata": { 72 | "description": "Required. The API key of the Azure OpenAI resource deployed previously." 73 | } 74 | }, 75 | "azureOpenAIAPIVersion": { 76 | "type": "string", 77 | "defaultValue": "2024-10-01-preview", 78 | "metadata": { 79 | "description": "Required. The API version for the Azure OpenAI service." 80 | } 81 | }, 82 | "azureOpenAIGPT4oMiniModelName": { 83 | "type": "string", 84 | "defaultValue": "gpt-4o-mini", 85 | "metadata": { 86 | "description": "Required. The deployment name for the GPT-4o-mini model." 87 | } 88 | }, 89 | "azureOpenAIGPT4oModelName": { 90 | "type": "string", 91 | "defaultValue": "gpt-4o", 92 | "metadata": { 93 | "description": "Required. The deployment name for the GPT-4o model." 94 | } 95 | }, 96 | "azureOpenAIEmbeddingModelName": { 97 | "type": "string", 98 | "defaultValue": "text-embedding-3-large", 99 | "metadata": { 100 | "description": "Required. The deployment name for the Embedding model." 101 | } 102 | }, 103 | "bingSearchUrl": { 104 | "type": "string", 105 | "defaultValue": "https://api.bing.microsoft.com/v7.0/search", 106 | "metadata": { 107 | "description": "Required. The URL for the Bing Search service." 108 | } 109 | }, 110 | "bingSearchName": { 111 | "type": "string", 112 | "metadata": { 113 | "description": "Required. The name of the Bing Search service deployed previously." 114 | } 115 | }, 116 | "SQLServerName": { 117 | "type": "string", 118 | "metadata": { 119 | "description": "Required. The name of the SQL server deployed previously e.g. sqlserver.database.windows.net" 120 | } 121 | }, 122 | "SQLServerDatabase": { 123 | "type": "string", 124 | "defaultValue": "SampleDB", 125 | "metadata": { 126 | "description": "Required. The name of the SQL Server database." 127 | } 128 | }, 129 | "SQLServerUsername": { 130 | "type": "string", 131 | "metadata": { 132 | "description": "Required. The username for the SQL Server." 133 | } 134 | }, 135 | "SQLServerPassword": { 136 | "type": "securestring", 137 | "metadata": { 138 | "description": "Required. The password for the SQL Server." 139 | } 140 | }, 141 | "cosmosDBAccountName": { 142 | "type": "string", 143 | "metadata": { 144 | "description": "Required. The name of the Azure CosmosDB Account." 145 | } 146 | }, 147 | "cosmosDBContainerName": { 148 | "type": "string", 149 | "metadata": { 150 | "description": "Required. The name of the Azure CosmosDB container." 151 | } 152 | }, 153 | "botId": { 154 | "type": "string", 155 | "defaultValue": "[format('BotId-{0}', uniqueString(resourceGroup().id))]", 156 | "metadata": { 157 | "description": "Required. The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." 158 | } 159 | }, 160 | "botSKU": { 161 | "type": "string", 162 | "defaultValue": "F0", 163 | "allowedValues": [ 164 | "F0", 165 | "S1" 166 | ], 167 | "metadata": { 168 | "description": "Required, defaults to F0. The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." 169 | } 170 | }, 171 | "appServicePlanName": { 172 | "type": "string", 173 | "defaultValue": "[format('AppServicePlan-Backend-{0}', uniqueString(resourceGroup().id))]", 174 | "metadata": { 175 | "description": "Required. The name of the new App Service Plan." 176 | } 177 | }, 178 | "appServicePlanSKU": { 179 | "type": "string", 180 | "defaultValue": "S3", 181 | "allowedValues": [ 182 | "B3", 183 | "S3", 184 | "P2v3" 185 | ], 186 | "metadata": { 187 | "description": "Required, defaults to S3. The SKU of the App Service Plan. Acceptable values are B3, S3 and P2v3." 188 | } 189 | }, 190 | "location": { 191 | "type": "string", 192 | "defaultValue": "[resourceGroup().location]", 193 | "metadata": { 194 | "description": "Optional, defaults to resource group location. The location of the resources." 195 | } 196 | } 197 | }, 198 | "variables": { 199 | "publishingUsername": "[format('${0}', parameters('botId'))]", 200 | "webAppName": "[format('webApp-Backend-{0}', parameters('botId'))]", 201 | "siteHost": "[format('{0}.azurewebsites.net', variables('webAppName'))]", 202 | "botEndpoint": "[format('https://{0}/api/messages', variables('siteHost'))]" 203 | }, 204 | "resources": [ 205 | { 206 | "type": "Microsoft.Web/serverfarms", 207 | "apiVersion": "2022-09-01", 208 | "name": "[parameters('appServicePlanName')]", 209 | "location": "[parameters('location')]", 210 | "sku": { 211 | "name": "[parameters('appServicePlanSKU')]" 212 | }, 213 | "kind": "linux", 214 | "properties": { 215 | "reserved": true 216 | } 217 | }, 218 | { 219 | "type": "Microsoft.Web/sites", 220 | "apiVersion": "2022-09-01", 221 | "name": "[variables('webAppName')]", 222 | "location": "[parameters('location')]", 223 | "tags": { 224 | "azd-service-name": "backend" 225 | }, 226 | "kind": "app,linux", 227 | "properties": { 228 | "enabled": true, 229 | "hostNameSslStates": [ 230 | { 231 | "name": "[format('{0}.azurewebsites.net', variables('webAppName'))]", 232 | "sslState": "Disabled", 233 | "hostType": "Standard" 234 | }, 235 | { 236 | "name": "[format('{0}.scm.azurewebsites.net', variables('webAppName'))]", 237 | "sslState": "Disabled", 238 | "hostType": "Repository" 239 | } 240 | ], 241 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", 242 | "reserved": true, 243 | "scmSiteAlsoStopped": false, 244 | "clientAffinityEnabled": false, 245 | "clientCertEnabled": false, 246 | "hostNamesDisabled": false, 247 | "containerSize": 0, 248 | "dailyMemoryTimeQuota": 0, 249 | "httpsOnly": false, 250 | "siteConfig": { 251 | "appSettings": [ 252 | { 253 | "name": "MicrosoftAppId", 254 | "value": "[parameters('appId')]" 255 | }, 256 | { 257 | "name": "MicrosoftAppPassword", 258 | "value": "[parameters('appPassword')]" 259 | }, 260 | { 261 | "name": "MicrosoftAppTenantId", 262 | "value": "[parameters('TenantId')]" 263 | }, 264 | { 265 | "name": "MicrosoftAppType", 266 | "value": "[parameters('appType')]" 267 | }, 268 | { 269 | "name": "BLOB_SAS_TOKEN", 270 | "value": "[parameters('blobSASToken')]" 271 | }, 272 | { 273 | "name": "AZURE_SEARCH_ENDPOINT", 274 | "value": "[format('https://{0}.search.windows.net', parameters('azureSearchName'))]" 275 | }, 276 | { 277 | "name": "AZURE_SEARCH_KEY", 278 | "value": "[listAdminKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupSearch')), 'Microsoft.Search/searchServices', parameters('azureSearchName')), '2021-04-01-preview').primaryKey]" 279 | }, 280 | { 281 | "name": "AZURE_SEARCH_API_VERSION", 282 | "value": "[parameters('azureSearchAPIVersion')]" 283 | }, 284 | { 285 | "name": "AZURE_OPENAI_ENDPOINT", 286 | "value": "[format('https://{0}.openai.azure.com/', parameters('azureOpenAIName'))]" 287 | }, 288 | { 289 | "name": "AZURE_OPENAI_API_KEY", 290 | "value": "[parameters('azureOpenAIAPIKey')]" 291 | }, 292 | { 293 | "name": "GPT4oMINI_DEPLOYMENT_NAME", 294 | "value": "[parameters('azureOpenAIGPT4oMiniModelName')]" 295 | }, 296 | { 297 | "name": "GPT4o_DEPLOYMENT_NAME", 298 | "value": "[parameters('azureOpenAIGPT4oModelName')]" 299 | }, 300 | { 301 | "name": "EMBEDDING_DEPLOYMENT_NAME", 302 | "value": "[parameters('azureOpenAIEmbeddingModelName')]" 303 | }, 304 | { 305 | "name": "AZURE_OPENAI_API_VERSION", 306 | "value": "[parameters('azureOpenAIAPIVersion')]" 307 | }, 308 | { 309 | "name": "BING_SEARCH_URL", 310 | "value": "[parameters('bingSearchUrl')]" 311 | }, 312 | { 313 | "name": "BING_SUBSCRIPTION_KEY", 314 | "value": "[listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupSearch')), 'Microsoft.Bing/accounts', parameters('bingSearchName')), '2020-06-10').key1]" 315 | }, 316 | { 317 | "name": "SQL_SERVER_NAME", 318 | "value": "[parameters('SQLServerName')]" 319 | }, 320 | { 321 | "name": "SQL_SERVER_DATABASE", 322 | "value": "[parameters('SQLServerDatabase')]" 323 | }, 324 | { 325 | "name": "SQL_SERVER_USERNAME", 326 | "value": "[parameters('SQLServerUsername')]" 327 | }, 328 | { 329 | "name": "SQL_SERVER_PASSWORD", 330 | "value": "[parameters('SQLServerPassword')]" 331 | }, 332 | { 333 | "name": "AZURE_COSMOSDB_ENDPOINT", 334 | "value": "[format('https://{0}.documents.azure.com:443/', parameters('cosmosDBAccountName'))]" 335 | }, 336 | { 337 | "name": "AZURE_COSMOSDB_NAME", 338 | "value": "[parameters('cosmosDBAccountName')]" 339 | }, 340 | { 341 | "name": "AZURE_COSMOSDB_CONTAINER_NAME", 342 | "value": "[parameters('cosmosDBContainerName')]" 343 | }, 344 | { 345 | "name": "AZURE_COSMOSDB_KEY", 346 | "value": "[listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupSearch')), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBAccountName')), '2023-04-15').primaryMasterKey]" 347 | }, 348 | { 349 | "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", 350 | "value": "true" 351 | } 352 | ], 353 | "cors": { 354 | "allowedOrigins": [ 355 | "https://botservice.hosting.portal.azure.net", 356 | "https://hosting.onecloud.azure-test.net/" 357 | ] 358 | } 359 | } 360 | }, 361 | "dependsOn": [ 362 | "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]" 363 | ] 364 | }, 365 | { 366 | "type": "Microsoft.Web/sites/config", 367 | "apiVersion": "2022-09-01", 368 | "name": "[format('{0}/{1}', variables('webAppName'), 'web')]", 369 | "properties": { 370 | "numberOfWorkers": 1, 371 | "defaultDocuments": [ 372 | "Default.htm", 373 | "Default.html", 374 | "Default.asp", 375 | "index.htm", 376 | "index.html", 377 | "iisstart.htm", 378 | "default.aspx", 379 | "index.php", 380 | "hostingstart.html" 381 | ], 382 | "netFrameworkVersion": "v4.0", 383 | "phpVersion": "", 384 | "pythonVersion": "", 385 | "nodeVersion": "", 386 | "linuxFxVersion": "PYTHON|3.12", 387 | "requestTracingEnabled": false, 388 | "remoteDebuggingEnabled": false, 389 | "remoteDebuggingVersion": "VS2022", 390 | "httpLoggingEnabled": true, 391 | "logsDirectorySizeLimit": 35, 392 | "detailedErrorLoggingEnabled": false, 393 | "publishingUsername": "[variables('publishingUsername')]", 394 | "scmType": "None", 395 | "use32BitWorkerProcess": true, 396 | "webSocketsEnabled": false, 397 | "alwaysOn": true, 398 | "appCommandLine": "runserver.sh", 399 | "managedPipelineMode": "Integrated", 400 | "virtualApplications": [ 401 | { 402 | "virtualPath": "/", 403 | "physicalPath": "site\\wwwroot", 404 | "preloadEnabled": false, 405 | "virtualDirectories": null 406 | } 407 | ], 408 | "loadBalancing": "LeastRequests", 409 | "experiments": { 410 | "rampUpRules": [] 411 | }, 412 | "autoHealEnabled": false, 413 | "vnetName": "", 414 | "minTlsVersion": "1.2", 415 | "ftpsState": "AllAllowed" 416 | }, 417 | "dependsOn": [ 418 | "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" 419 | ] 420 | }, 421 | { 422 | "type": "Microsoft.BotService/botServices", 423 | "apiVersion": "2023-09-15-preview", 424 | "name": "[parameters('botId')]", 425 | "location": "global", 426 | "kind": "azurebot", 427 | "sku": { 428 | "name": "[parameters('botSKU')]" 429 | }, 430 | "properties": { 431 | "displayName": "[parameters('botId')]", 432 | "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", 433 | "endpoint": "[variables('botEndpoint')]", 434 | "msaAppId": "[parameters('appId')]", 435 | "msaAppTenantId": "[parameters('TenantId')]", 436 | "msaAppType": "[parameters('appType')]", 437 | "schemaTransformationVersion": "1.3", 438 | "isCmekEnabled": false 439 | }, 440 | "dependsOn": [ 441 | "[resourceId('Microsoft.Web/sites', variables('webAppName'))]" 442 | ] 443 | } 444 | ], 445 | "outputs": { 446 | "botServiceName": { 447 | "type": "string", 448 | "value": "[format('{0}', parameters('botId'))]" 449 | } 450 | , 451 | "webAppName": { 452 | "type": "string", 453 | "value": "[variables('webAppName')]" 454 | }, 455 | "webAppUrl": { 456 | "type": "string", 457 | "value": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2022-09-01').defaultHostName]" 458 | } 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /apps/backend/botservice/backend.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/apps/backend/botservice/backend.zip -------------------------------------------------------------------------------- /apps/backend/fastapi/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /apps/backend/fastapi/README.md: -------------------------------------------------------------------------------- 1 |

2 | Backend Web Application - FastAPI 3 |

4 | 5 | ## Deploy Bot To Azure Web App 6 | 7 | Below are the steps to run the FastAPI Bot API as an Azure Wep App: 8 | 9 | 1. We don't need to deploy again the Azure infrastructure, we did that already for the Bot Service API (Notebook 13). We are going to use the same App Service, but we just need to add another SLOT to the service and have both APIs running at the same time. Note: the slot can have any name, we are using "staging".
In the terminal run: 10 | 11 | ```bash 12 | az login -i 13 | az webapp deployment slot create --name "" --resource-group "" --slot staging --configuration-source "" 14 | ``` 15 | 16 | 2. Zip the code of the bot by executing the following command in the terminal (**you have to be inside the apps/backend/fastapi/ folder**): 17 | 18 | ```bash 19 | (cd ../../../ && zip -r apps/backend/fastapi/backend.zip common data/openapi_kraken.json data/all-states-history.csv) && zip -j backend.zip ../../../common/requirements.txt app/* 20 | ``` 21 | 22 | 3. Using the Azure CLI deploy the bot code to the Azure App Service new SLOT created on Step 1: 23 | 24 | ```bash 25 | az webapp deployment source config-zip --resource-group "" --name "" --src "backend.zip" --slot staging 26 | ``` 27 | 28 | 4. Wait around 5 minutes and test your bot by running Notebook 15. Your Swagger (OpenAPI) definition should show here: 29 | 30 | ```html 31 | https://-staging.azurewebsites.net/ 32 | ``` 33 | 34 |

35 | 36 | ## (Optional) Run the FastAPI server Locally 37 | 38 | You can also run the server locally for testing. 39 | 40 | ### **Steps to Run Locally:** 41 | 42 | 1. Open `apps/backend/fastapi/app/server.py` and uncomment the following section: 43 | ```python 44 | ## Uncomment this section to run locally 45 | # current_file = Path(__file__).resolve() 46 | # library_path = current_file.parents[4] 47 | # data_path = library_path / "data" 48 | # sys.path.append(str(library_path)) # ensure we can import "common" etc. 49 | # load_dotenv(str(library_path) + "/credentials.env") 50 | # csv_file_path = data_path / "all-states-history.csv" 51 | # api_file_path = data_path / "openapi_kraken.json" 52 | 53 | ``` 54 | 2. Open a terminal, activate the right conda environment, then go to this folder `apps/backend/fastapi/app` and run this command: 55 | 56 | ```bash 57 | python server.py 58 | ``` 59 | 60 | This will run the backend server API in localhost port 8000. 61 | 62 | 3. If you are working on an Azure ML compute instance you can access the OpenAPI (Swagger) definition in this address: 63 | 64 | https:\-8000.\.instances.azureml.ms/ 65 | 66 | for example: 67 | https://pabmar1-8000.australiaeast.instances.azureml.ms/ 68 | 69 | 70 | -------------------------------------------------------------------------------- /apps/backend/fastapi/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/apps/backend/fastapi/app/__init__.py -------------------------------------------------------------------------------- /apps/backend/fastapi/app/runserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Starts the LangServe FastAPI on port 8000 4 | gunicorn --bind 0.0.0.0:8000 --worker-class uvicorn.workers.UvicornWorker --timeout 300 server:app & 5 | 6 | # Wait for any processes to exit 7 | wait -n 8 | 9 | # Exit with the status of the process that exited first 10 | exit $? 11 | -------------------------------------------------------------------------------- /apps/backend/fastapi/app/server.py: -------------------------------------------------------------------------------- 1 | # server.py 2 | import os 3 | import sys 4 | import uvicorn 5 | import asyncio 6 | import uuid 7 | import logging 8 | import json 9 | from typing import List, Optional 10 | 11 | from contextlib import asynccontextmanager 12 | from fastapi import FastAPI, Request, HTTPException, BackgroundTasks 13 | from fastapi.middleware.cors import CORSMiddleware 14 | from fastapi.responses import RedirectResponse, JSONResponse 15 | from pydantic import BaseModel 16 | from pathlib import Path 17 | from dotenv import load_dotenv 18 | 19 | csv_file_path = "data/all-states-history.csv" 20 | api_file_path = "data/openapi_kraken.json" 21 | 22 | ######### Uncomment this section to run locally########## 23 | # current_file = Path(__file__).resolve() 24 | # library_path = current_file.parents[4] 25 | # data_path = library_path / "data" 26 | # sys.path.append(str(library_path)) # ensure we can import "common" etc. 27 | # load_dotenv(str(library_path) + "/credentials.env") 28 | # csv_file_path = data_path / "all-states-history.csv" 29 | # api_file_path = data_path / "openapi_kraken.json" 30 | ########################################################## 31 | 32 | # from the graph module 33 | from common.graph import build_async_workflow 34 | 35 | # For CosmosDB checkpointer 36 | from common.cosmosdb_checkpointer import AsyncCosmosDBSaver 37 | from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer 38 | 39 | # SSE 40 | from sse_starlette.sse import EventSourceResponse 41 | 42 | # ----------------------------------------------------------------------------- 43 | # Logging setup 44 | # ----------------------------------------------------------------------------- 45 | logging.basicConfig( 46 | level=logging.DEBUG, # or logging.INFO if you want less verbosity 47 | format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" 48 | ) 49 | logger = logging.getLogger(__name__) 50 | 51 | 52 | # ----------------------------------------------------------------------------- 53 | # CosmosDB and Workflow Initialization 54 | # ----------------------------------------------------------------------------- 55 | checkpointer_async = AsyncCosmosDBSaver( 56 | endpoint=os.environ.get("AZURE_COSMOSDB_ENDPOINT", ""), 57 | key=os.environ.get("AZURE_COSMOSDB_KEY", ""), 58 | database_name=os.environ.get("AZURE_COSMOSDB_NAME", ""), 59 | container_name=os.environ.get("AZURE_COSMOSDB_CONTAINER_NAME", ""), 60 | serde=JsonPlusSerializer(), 61 | ) 62 | 63 | workflow = build_async_workflow(csv_file_path,api_file_path ) 64 | graph_async = None 65 | 66 | 67 | # ----------------------------------------------------------------------------- 68 | # Lifespan Event Handler 69 | # ----------------------------------------------------------------------------- 70 | @asynccontextmanager 71 | async def lifespan(app: FastAPI): 72 | global graph_async 73 | logger.info("Running checkpointer_async.setup() at startup.") 74 | await checkpointer_async.setup() 75 | 76 | logger.info("Compiling the graph with the cosmos checkpointer.") 77 | graph_async = workflow.compile(checkpointer=checkpointer_async) 78 | logger.info("Graph compilation complete.") 79 | 80 | yield # The app runs while execution is paused here 81 | 82 | logger.info("Shutting down application.") 83 | 84 | 85 | 86 | # ----------------------------------------------------------------------------- 87 | # FastAPI App Setup 88 | # ----------------------------------------------------------------------------- 89 | app = FastAPI( 90 | title="Multi-Agent GPT Assistant (FastAPI)", 91 | version="1.0", 92 | description="GPT Smart Search Engine - FastAPI Backend", 93 | lifespan=lifespan, 94 | ) 95 | 96 | app.add_middleware( 97 | CORSMiddleware, 98 | allow_origins=["*"], 99 | allow_credentials=True, 100 | allow_methods=["*"], 101 | allow_headers=["*"], 102 | ) 103 | 104 | @app.get("/") 105 | async def redirect_to_docs(): 106 | return RedirectResponse("/docs") 107 | 108 | 109 | # ----------------------------------------------------------------------------- 110 | # Define Pydantic Models 111 | # ----------------------------------------------------------------------------- 112 | class AskRequest(BaseModel): 113 | user_input: str 114 | thread_id: str = "" 115 | 116 | class AskResponse(BaseModel): 117 | final_answer: str 118 | 119 | class BatchRequest(BaseModel): 120 | questions: List[str] 121 | thread_id: str = "" 122 | 123 | class BatchResponse(BaseModel): 124 | answers: List[str] 125 | 126 | 127 | 128 | # ----------------------------------------------------------------------------- 129 | # Invoke Endpoint 130 | # ----------------------------------------------------------------------------- 131 | @app.post("/invoke", response_model=AskResponse) 132 | async def invoke(req: AskRequest): 133 | logger.info("[/invoke] Called with user_input=%s, thread_id=%s", req.user_input, req.thread_id) 134 | 135 | if not graph_async: 136 | logger.error("Graph not compiled yet.") 137 | raise HTTPException(status_code=500, detail="Graph not compiled yet.") 138 | 139 | config = {"configurable": {"thread_id": req.thread_id or str(uuid.uuid4())}} 140 | inputs = {"messages": [("human", req.user_input)]} 141 | 142 | try: 143 | logger.debug("[/invoke] Invoking graph_async with config=%s", config) 144 | result = await graph_async.ainvoke(inputs, config=config) 145 | final_answer = result["messages"][-1].content 146 | logger.info("[/invoke] Final answer: %s", final_answer) 147 | return AskResponse(final_answer=final_answer) 148 | except Exception as e: 149 | logger.exception("[/invoke] Exception while running the workflow") 150 | raise HTTPException(status_code=500, detail=str(e)) 151 | 152 | 153 | # ----------------------------------------------------------------------------- 154 | # Batch Endpoint 155 | # ----------------------------------------------------------------------------- 156 | @app.post("/batch", response_model=BatchResponse) 157 | async def batch(req: BatchRequest): 158 | logger.info("[/batch] Called with thread_id=%s, questions=%s", req.thread_id, req.questions) 159 | 160 | if not graph_async: 161 | logger.error("Graph not compiled yet.") 162 | raise HTTPException(status_code=500, detail="Graph not compiled yet.") 163 | 164 | answers = [] 165 | for question in req.questions: 166 | config = {"configurable": {"thread_id": req.thread_id or str(uuid.uuid4())}} 167 | inputs = {"messages": [("human", question)]} 168 | try: 169 | result = await graph_async.ainvoke(inputs, config=config) 170 | final_answer = result["messages"][-1].content 171 | answers.append(final_answer) 172 | except Exception as e: 173 | logger.exception("[/batch] Exception while running the workflow for question=%s", question) 174 | answers.append(f"Error: {str(e)}") 175 | 176 | return BatchResponse(answers=answers) 177 | 178 | 179 | # ----------------------------------------------------------------------------- 180 | # Streaming Endpoint 181 | # ----------------------------------------------------------------------------- 182 | @app.post("/stream") 183 | async def stream_endpoint(req: AskRequest): 184 | """ 185 | Stream partial chunks from the chain in SSE format. 186 | 187 | SSE event structure: 188 | - event: "metadata" (OPTIONAL) – any run-specific metadata 189 | - event: "data" – a chunk of text 190 | - event: "end" – signals no more data 191 | - event: "on_tool_start" - signals the begin of use of a tool 192 | - event: "on_tool_end" - signals the end of use of a tool 193 | - event: "error" – signals an error 194 | """ 195 | logger.info("[/stream] Called with user_input=%s, thread_id=%s", req.user_input, req.thread_id) 196 | 197 | if not graph_async: 198 | logger.error("Graph not compiled yet.") 199 | raise HTTPException(status_code=500, detail="Graph not compiled yet.") 200 | 201 | run_id = req.thread_id or str(uuid.uuid4()) 202 | config = {"configurable": {"thread_id": run_id}} 203 | inputs = {"messages": [("human", req.user_input)]} 204 | 205 | async def event_generator(): 206 | try: 207 | yield { 208 | "event": "metadata", 209 | "data": json.dumps({"run_id": run_id}) 210 | } 211 | 212 | accumulated_text = "" 213 | async for event in graph_async.astream_events(inputs, config, version="v2"): 214 | if event["event"] == "on_chat_model_stream": 215 | if event["metadata"].get("langgraph_node") == "agent": 216 | chunk_text = event["data"]["chunk"].content 217 | accumulated_text += chunk_text 218 | 219 | yield { 220 | "event": "data", 221 | "data": chunk_text # partial chunk 222 | } 223 | 224 | elif event["event"] == "on_tool_start": 225 | yield {"event": "on_tool_start", "data": f"Tool Start: {event.get('name', '')}"} 226 | 227 | elif event["event"] == "on_tool_end": 228 | yield {"event": "on_tool_end", "data": f"Tool End: {event.get('name', '')}"} 229 | 230 | elif event["event"] == "on_chain_end" and event.get("name") == "LangGraph": 231 | # If "FINISH" is the next step 232 | if event["data"]["output"].get("next") == "FINISH": 233 | yield {"event": "end", "data": accumulated_text} 234 | return # Stop iteration 235 | 236 | except Exception as ex: 237 | logger.exception("[/stream] Error streaming events") 238 | # SSE "error" event 239 | yield { 240 | "event": "error", 241 | "data": json.dumps({ 242 | "status_code": 500, 243 | "message": str(ex) 244 | }) 245 | } 246 | raise 247 | 248 | return EventSourceResponse(event_generator(), media_type="text/event-stream") 249 | 250 | 251 | 252 | # ----------------------------------------------------------------------------- 253 | # Main Entrypoint 254 | # ----------------------------------------------------------------------------- 255 | if __name__ == "__main__": 256 | logger.info("Starting server via uvicorn") 257 | uvicorn.run(app, host="127.0.0.1", port=8000) 258 | 259 | 260 | -------------------------------------------------------------------------------- /apps/backend/fastapi/backend.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/apps/backend/fastapi/backend.zip -------------------------------------------------------------------------------- /apps/frontend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Frontend Web Application - Search + Web Bot Channel 3 |

4 | 5 | Simple UI using Streamlit to expose the Bot Service Channel. 6 | Also includes a Search experience. 7 | 8 | ## Deploy in Azure Web App Service 9 | 10 | 1. Deploy the Frontend Azure Web Application by clicking the Button below 11 | 12 | [![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fpablomarin%2FGPT-Azure-Search-Engine%2Fmain%2Fapps%2Ffrontend%2Fazuredeploy-frontend.json) 13 | 14 | 2. Zip the code of the bot by executing the following command in the terminal (you have to be inside the folder: apps/frontend/ ): 15 | 16 | ```bash 17 | (zip frontend.zip ./app/* ./app/pages/* ./app/helpers/* && zip -j frontend.zip ../../common/*) && (cd ../../ && zip -r apps/frontend/frontend.zip common) 18 | ``` 19 | 20 | 3. Using the Azure CLI deploy the frontend code to the Azure App Service created on Step 2 21 | 22 | ```bash 23 | az login -i 24 | az webapp deployment source config-zip --resource-group "" --name "" --src "frontend.zip" 25 | ``` 26 | 27 | **Note**: Some FDPO Azure Subscriptions disable Azure Web Apps Basic Authentication every minute (don't know why). So before running the above `az webapp deployment` command, make sure that your frontend azure web app has Basic Authentication ON. In the Azure Portal, you can find this settting in: `Configuration->General Settings`. Don't worry if after running the command it says retrying many times, the zip files already uploaded and is building. 28 | 29 | 4. In a few minutes (5-10) your App should be working now. Go to the Azure Portal and get the URL. 30 | 31 | ## (Optional) Running the Frontend app Locally 32 | 33 | - Run the followin comand on the console to export the env variables (at the /frontend folder level) 34 | ```bash 35 | export FAST_API_SERVER = "" 36 | export $(grep -v '^#' ../../credentials.env | sed -E '/^\s*$/d;s/#.*//' | xargs) 37 | ``` 38 | - Run the stramlit server on port 8500 39 | ```bash 40 | python -m streamlit run app/Home.py --server.port 8500 --server.address 0.0.0.0 41 | ``` 42 | - If you are working on an AML compute instance you can accces the frontend here: 43 | ```bash 44 | https://-8500..instances.azureml.ms/ 45 | ``` 46 | 47 | 48 | ## Troubleshoot 49 | 50 | 1. If WebApp deployed succesfully but the Application didn't start 51 | 1. Go to Azure portal -> Your Webapp -> Settings -> Configuration -> General Settings 52 | 2. Make sure that StartUp Command has: python -m streamlit run Home.py --server.port 8000 --server.address 0.0.0.0 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /apps/frontend/app/Home.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | st.set_page_config(page_title="GPT Smart Search", page_icon="📖", layout="wide") 4 | 5 | st.image("https://user-images.githubusercontent.com/113465005/226238596-cc76039e-67c2-46b6-b0bb-35d037ae66e1.png") 6 | 7 | st.header("MSUS OpenAI Accelerator VBD - Web Frontend") 8 | 9 | 10 | st.markdown("---") 11 | st.markdown(""" 12 | This engine finds information from the following: 13 | - The entire Dialog from each episode and season of the TV Show "FRIENDS" 14 | - ~90k [COVID-19 research articles from the CORD19 dataset](https://github.com/allenai/cord19) 15 | - [Covid Tracking Project Dataset](https://covidtracking.com/). Azure SQL with information of Covid cases and hospitalizations in the US from 2020-2021. 16 | - 5 Books: "Azure_Cognitive_Search_Documentation.pdf", "Boundaries_When_to_Say_Yes_How_to_Say_No_to_Take_Control_of_Your_Life.pdf", "Fundamentals_of_Physics_Textbook.pdf", "Made_To_Stick.pdf", "Pere_Riche_Pere_Pauvre.pdf" (French version of Rich Dad Poor Dad). 17 | - [Kraken API](https://docs.kraken.com/rest/#tag/Market-Data). This API provides real-time data for currency and digital coins pricing. 18 | 19 | **👈 Select a demo from the sidebar** to see an example of a Search Interface, and a Bot Interface. 20 | 21 | ### Want to learn more? 22 | - Check out [Github Repo](https://github.com/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator/) 23 | - Ask a question or submit a [GitHub Issue!](https://github.com/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator/issues/new) 24 | 25 | 26 | """ 27 | ) 28 | st.markdown("---") 29 | -------------------------------------------------------------------------------- /apps/frontend/app/__init__.py: -------------------------------------------------------------------------------- 1 | # app/__init__.py 2 | 3 | import os 4 | 5 | def get_env_var(var_name, default_value=None, required=True): 6 | """ 7 | Retrieve an environment variable, optionally providing a default value. 8 | 9 | :param var_name: The name of the environment variable 10 | :param default_value: The fallback value if var_name is not found 11 | :param required: Whether to raise an error if the variable is missing 12 | :return: The value of the environment variable or default_value 13 | :raises EnvironmentError: If required=True and the variable is not set 14 | """ 15 | value = os.getenv(var_name, default_value) 16 | if required and value is None: 17 | raise EnvironmentError(f"Environment variable '{var_name}' is not set.") 18 | return value 19 | 20 | # ----------------------------------------------------------------------------- 21 | # 1) Endpoint & Model Config 22 | # ----------------------------------------------------------------------------- 23 | # The backend SSE endpoint, e.g. "http://localhost:8000" or your deployed URL. 24 | # We'll append "/stream" for SSE. 25 | api_url = get_env_var("FAST_API_SERVER", required=True).rstrip("/") + "/stream" 26 | 27 | # The primary model name or deployment name for OpenAI / Azure OpenAI 28 | model_name = get_env_var("GPT4o_DEPLOYMENT_NAME", required=True) 29 | 30 | # ----------------------------------------------------------------------------- 31 | # 2) OpenAI API Config 32 | # ----------------------------------------------------------------------------- 33 | openai_api_version = get_env_var("AZURE_OPENAI_API_VERSION", required=True) 34 | os.environ["OPENAI_API_VERSION"] = openai_api_version 35 | 36 | openai_endpoint = get_env_var("AZURE_OPENAI_ENDPOINT", required=True) 37 | openai_api_key = get_env_var("AZURE_OPENAI_API_KEY", required=True) 38 | 39 | # ----------------------------------------------------------------------------- 40 | # 3) Speech Engine Config 41 | # ----------------------------------------------------------------------------- 42 | speech_engine = get_env_var("SPEECH_ENGINE", required=True).lower() 43 | if speech_engine not in ["azure", "openai"]: 44 | raise EnvironmentError("Environment variable 'SPEECH_ENGINE' must be either 'azure' or 'openai'.") 45 | 46 | # For Azure-based TTS or STT 47 | if speech_engine == "azure": 48 | azure_speech_key = get_env_var("AZURE_SPEECH_KEY", required=True) 49 | azure_speech_region = get_env_var("AZURE_SPEECH_REGION", required=True) 50 | azure_speech_voice_name = get_env_var("AZURE_SPEECH_VOICE_NAME", default_value="en-US-AndrewMultilingualNeural", required=False) 51 | whisper_model_name = None 52 | tts_voice_name = None 53 | tts_model_name = None 54 | 55 | # For OpenAI-based Whisper + TTS 56 | elif speech_engine == "openai": 57 | azure_speech_key = None 58 | azure_speech_region = None 59 | azure_speech_voice_name = None 60 | whisper_model_name = get_env_var("AZURE_OPENAI_WHISPER_MODEL_NAME", default_value="whisper", required=True) 61 | tts_voice_name = get_env_var("AZURE_OPENAI_TTS_VOICE_NAME", default_value="nova", required=False) 62 | tts_model_name = get_env_var("AZURE_OPENAI_TTS_MODEL_NAME", default_value="tts", required=True) 63 | -------------------------------------------------------------------------------- /apps/frontend/app/helpers/streamlit_helpers.py: -------------------------------------------------------------------------------- 1 | # app/helpers/streamlit_helpers.py 2 | 3 | import json 4 | import uuid 5 | import os 6 | 7 | import requests 8 | import streamlit as st 9 | from sseclient import SSEClient # Import SSEClient from sseclient-py 10 | from langchain_core.messages import AIMessage, HumanMessage 11 | from langchain import hub 12 | 13 | try: 14 | from common.prompts import WELCOME_MESSAGE 15 | except Exception as e: 16 | from ..common.prompts import WELCOME_MESSAGE 17 | 18 | 19 | def get_logger(name): 20 | """ 21 | Retrieve a Streamlit logger instance. 22 | 23 | :param name: The name for the logger 24 | :return: Logger instance 25 | """ 26 | from streamlit.logger import get_logger 27 | return get_logger(name) 28 | 29 | logger = get_logger(__name__) 30 | 31 | def configure_page(title, icon): 32 | """ 33 | Configure the Streamlit page settings: page title, icon, and layout. 34 | Also applies minimal styling for spacing. 35 | 36 | :param title: The title of the page 37 | :param icon: The favicon/icon for the page 38 | """ 39 | st.set_page_config(page_title=title, page_icon=icon, layout="wide") 40 | st.markdown( 41 | """ 42 | 48 | """, 49 | unsafe_allow_html=True, 50 | ) 51 | 52 | def get_or_create_ids(): 53 | """ 54 | Generate or retrieve session and user IDs from Streamlit's session_state. 55 | 56 | :return: (session_id, user_id) 57 | """ 58 | if "session_id" not in st.session_state: 59 | st.session_state["session_id"] = str(uuid.uuid4()) 60 | logger.info("Created new session_id: %s", st.session_state["session_id"]) 61 | else: 62 | logger.info("Found existing session_id: %s", st.session_state["session_id"]) 63 | 64 | if "user_id" not in st.session_state: 65 | st.session_state["user_id"] = str(uuid.uuid4()) 66 | logger.info("Created new user_id: %s", st.session_state["user_id"]) 67 | else: 68 | logger.info("Found existing user_id: %s", st.session_state["user_id"]) 69 | 70 | return st.session_state["session_id"], st.session_state["user_id"] 71 | 72 | def consume_api(url, user_query, session_id, user_id): 73 | """ 74 | Send a POST request to the FastAPI backend at `url` (/stream endpoint), 75 | and consume the SSE stream using sseclient-py. 76 | 77 | The server is expected to return events like: 78 | {"event": "metadata", "data": "..."} 79 | {"event": "data", "data": "..."} 80 | {"event": "on_tool_start", "data": "..."} 81 | {"event": "on_tool_end", "data": "..."} 82 | {"event": "end", "data": "..."} 83 | {"event": "error", "data": "..."} 84 | """ 85 | headers = {"Content-Type": "application/json"} 86 | payload = { 87 | "user_input": user_query, 88 | "thread_id": session_id 89 | } 90 | 91 | logger.info( 92 | "Sending SSE request to %s with session_id=%s, user_id=%s", 93 | url, session_id, user_id 94 | ) 95 | logger.debug("Payload: %s", payload) 96 | 97 | try: 98 | with requests.post(url, json=payload, headers=headers, stream=True) as resp: 99 | resp.raise_for_status() 100 | logger.info("SSE stream opened with status code: %d", resp.status_code) 101 | 102 | client = SSEClient(resp) 103 | for event in client.events(): 104 | if not event.data: 105 | # Skip empty lines 106 | continue 107 | 108 | evt_type = event.event 109 | evt_data = event.data 110 | logger.debug("Received SSE event: %s, data: %s", evt_type, evt_data) 111 | 112 | if evt_type == "metadata": 113 | # Possibly parse run_id from the JSON 114 | # e.g. { "run_id": "...some uuid..." } 115 | info = json.loads(evt_data) 116 | run_id = info.get("run_id", "") 117 | # For streamlit, you might store it as session state, etc. 118 | # st.write(f"New run_id: {run_id}") 119 | 120 | elif evt_type == "data": 121 | # The server is sending partial tokens as "data" 122 | # We can yield them so Streamlit can display incrementally 123 | yield evt_data 124 | 125 | elif evt_type == "on_tool_start": 126 | # Optionally display: yield or do a Streamlit update 127 | # yield f"[Tool Start] {evt_data}" 128 | pass 129 | 130 | elif evt_type == "on_tool_end": 131 | # yield f"[Tool End] {evt_data}" 132 | pass 133 | 134 | elif evt_type == "end": 135 | # This is the final text. 136 | # Typically you might do a final display or update the UI 137 | yield evt_data 138 | 139 | elif evt_type == "error": 140 | # The server had an error 141 | yield f"[SSE Error] {evt_data}" 142 | 143 | else: 144 | yield f"[Unrecognized event: {evt_type}] {evt_data}" 145 | 146 | except requests.exceptions.HTTPError as err: 147 | logger.error("HTTP Error: %s", err) 148 | yield f"[HTTP Error] {err}" 149 | except Exception as e: 150 | logger.error("An error occurred during SSE consumption: %s", e) 151 | yield f"[Error] {e}" 152 | 153 | def initialize_chat_history(model): 154 | """ 155 | Initialize the chat history with a welcome message from the AI model. 156 | By default, attempts to pull a prompt from the prompts library if WELCOME_PROMPT_NAME is set, 157 | otherwise uses a fallback string. 158 | 159 | :param model: The name of the model (for logging or referencing) 160 | """ 161 | if "chat_history" not in st.session_state: 162 | st.session_state.chat_history = [AIMessage(content=WELCOME_MESSAGE)] 163 | logger.info("Chat history initialized for model: %s", model) 164 | else: 165 | logger.info("Chat history already exists for model: %s", model) 166 | 167 | def display_chat_history(): 168 | """ 169 | Render the existing chat history in Streamlit: 170 | - AI messages labeled "AI" 171 | - Human messages labeled "Human" 172 | """ 173 | for message in st.session_state.chat_history: 174 | if isinstance(message, AIMessage): 175 | with st.chat_message("AI"): 176 | st.write(message.content) 177 | logger.info("Displayed AI message: %s", message.content) 178 | elif isinstance(message, HumanMessage): 179 | with st.chat_message("Human"): 180 | st.write(message.content) 181 | logger.info("Displayed Human message: %s", message.content) 182 | 183 | def autoplay_audio(file_path): 184 | """ 185 | Play an audio file in the user's browser automatically using an tag. 186 | 187 | :param file_path: The path to the WAV file to play 188 | """ 189 | import base64 190 | if not os.path.exists(file_path): 191 | logger.error("Audio file does not exist: %s", file_path) 192 | return 193 | 194 | with open(file_path, "rb") as audio_file: 195 | audio_data = audio_file.read() 196 | 197 | audio_base64 = base64.b64encode(audio_data).decode("utf-8") 198 | audio_html = f""" 199 | 200 | 201 | 202 | """ 203 | st.markdown(audio_html, unsafe_allow_html=True) 204 | logger.info("Autoplayed audio: %s", file_path) 205 | -------------------------------------------------------------------------------- /apps/frontend/app/pages/1_Search.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import urllib 3 | import os 4 | import sys 5 | import re 6 | import time 7 | import random 8 | from operator import itemgetter 9 | from collections import OrderedDict 10 | from langchain_core.documents import Document 11 | from langchain_openai import AzureChatOpenAI 12 | from langchain_core.output_parsers import StrOutputParser 13 | from langchain_core.prompts import ChatPromptTemplate 14 | 15 | 16 | try: 17 | from utils import get_search_results 18 | from prompts import DOCSEARCH_PROMPT_TEXT 19 | except Exception as e: 20 | # Add the path four levels up 21 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../'))) 22 | from common.utils import get_search_results 23 | from common.prompts import DOCSEARCH_PROMPT_TEXT 24 | 25 | 26 | st.set_page_config(page_title="GPT Smart Search", page_icon="📖", layout="wide") 27 | # Add custom CSS styles to adjust padding 28 | st.markdown(""" 29 | 35 | """, unsafe_allow_html=True) 36 | 37 | st.header("GPT Smart Search Engine") 38 | 39 | 40 | def clear_submit(): 41 | st.session_state["submit"] = False 42 | 43 | 44 | with st.sidebar: 45 | st.markdown("""# Instructions""") 46 | st.markdown(""" 47 | 48 | Example questions: 49 | - Why Ross faked his death? 50 | - Who proposed first, Chandler or Monica? 51 | - What are the main risk factors for Covid-19? 52 | - What medicine reduces inflammation in the lungs? 53 | - Why Covid doesn't affect kids that much compared to adults? 54 | - What is the acronim of the book "Made to Stick" and what does it mean? give a short explanation of each letter. 55 | 56 | \nYou will notice that the answers to these questions are diferent from the open ChatGPT, since these papers are the only possible context. This search engine does not look at the open internet to answer these questions. If the context doesn't contain information, the engine will respond: I don't know. 57 | """) 58 | 59 | coli1, coli2= st.columns([3,1]) 60 | with coli1: 61 | query = st.text_input("Ask a question to your enterprise data lake", value= "What are the main risk factors for Covid-19?", on_change=clear_submit) 62 | 63 | button = st.button('Search') 64 | 65 | 66 | 67 | if (not os.environ.get("AZURE_SEARCH_ENDPOINT")) or (os.environ.get("AZURE_SEARCH_ENDPOINT") == ""): 68 | st.error("Please set your AZURE_SEARCH_ENDPOINT on your Web App Settings") 69 | elif (not os.environ.get("AZURE_SEARCH_KEY")) or (os.environ.get("AZURE_SEARCH_KEY") == ""): 70 | st.error("Please set your AZURE_SEARCH_ENDPOINT on your Web App Settings") 71 | elif (not os.environ.get("AZURE_OPENAI_ENDPOINT")) or (os.environ.get("AZURE_OPENAI_ENDPOINT") == ""): 72 | st.error("Please set your AZURE_OPENAI_ENDPOINT on your Web App Settings") 73 | elif (not os.environ.get("AZURE_OPENAI_API_KEY")) or (os.environ.get("AZURE_OPENAI_API_KEY") == ""): 74 | st.error("Please set your AZURE_OPENAI_API_KEY on your Web App Settings") 75 | elif (not os.environ.get("BLOB_SAS_TOKEN")) or (os.environ.get("BLOB_SAS_TOKEN") == ""): 76 | st.error("Please set your BLOB_SAS_TOKEN on your Web App Settings") 77 | 78 | else: 79 | os.environ["OPENAI_API_VERSION"] = os.environ["AZURE_OPENAI_API_VERSION"] 80 | 81 | MODEL = os.environ["GPT4o_DEPLOYMENT_NAME"] 82 | llm = AzureChatOpenAI(deployment_name=MODEL, temperature=0.5, max_tokens=1000) 83 | 84 | if button or st.session_state.get("submit"): 85 | if not query: 86 | st.error("Please enter a question!") 87 | else: 88 | # Azure Search 89 | 90 | try: 91 | indexes = ["srch-index-files", "srch-index-csv", "srch-index-books"] 92 | k = 10 93 | ordered_results = get_search_results(query, indexes, k=k, reranker_threshold=1, sas_token=os.environ['BLOB_SAS_TOKEN']) 94 | 95 | st.session_state["submit"] = True 96 | # Output Columns 97 | placeholder = st.empty() 98 | 99 | except Exception as e: 100 | st.markdown("Not data returned from Azure Search, check connection..") 101 | st.markdown(e) 102 | 103 | if "ordered_results" in locals(): 104 | try: 105 | top_docs = [] 106 | for key,value in ordered_results.items(): 107 | location = value["location"] if value["location"] is not None else "" 108 | document = {"source": location, 109 | "score": value["score"], 110 | "page_content": value["chunk"]} 111 | top_docs.append(document) 112 | 113 | add_text = "Reading the source documents to provide the best answer... ⏳" 114 | 115 | if "add_text" in locals(): 116 | with st.spinner(add_text): 117 | if(len(top_docs)>0): 118 | 119 | # Define prompt template 120 | DOCSEARCH_PROMPT = ChatPromptTemplate.from_messages( 121 | [ 122 | ("system", DOCSEARCH_PROMPT_TEXT + "\n\Retrieved Documents:\n{context}\n\n"), 123 | ("human", "{question}"), 124 | ] 125 | ) 126 | 127 | chain = ( 128 | DOCSEARCH_PROMPT 129 | | llm # Passes the finished prompt to the LLM 130 | | StrOutputParser() # converts the output (Runnable object) to the desired output (string) 131 | ) 132 | 133 | 134 | answer = chain.invoke({"question": query, "context":str(top_docs)}) 135 | 136 | else: 137 | answer = {"output_text":"No results found" } 138 | else: 139 | answer = {"output_text":"No results found" } 140 | 141 | 142 | with placeholder.container(): 143 | 144 | st.markdown("#### Answer") 145 | st.markdown(answer, unsafe_allow_html=True) 146 | st.markdown("---") 147 | st.markdown("#### Search Results") 148 | 149 | if(len(top_docs)>0): 150 | for key, value in ordered_results.items(): 151 | location = value["location"] if value["location"] is not None else "" 152 | title = str(value['title']) if (value['title']) else value['name'] 153 | score = str(round(value['score']*100/4,2)) 154 | st.markdown("[" + title + "](" + location + ")" + " (Score: " + score + "%)") 155 | st.markdown(value["caption"]) 156 | st.markdown("---") 157 | 158 | except Exception as e: 159 | st.error(e) -------------------------------------------------------------------------------- /apps/frontend/app/pages/2_BotService_Chat.py: -------------------------------------------------------------------------------- 1 | import os 2 | import streamlit as st 3 | import streamlit.components.v1 as components 4 | 5 | # From here down is all the StreamLit UI. 6 | st.set_page_config(page_title="BotService Backend Bot", page_icon="🤖", layout="wide") 7 | # Add custom CSS styles to adjust padding 8 | st.markdown(""" 9 | 15 | """, unsafe_allow_html=True) 16 | 17 | with st.sidebar: 18 | st.markdown("""# Instructions""") 19 | st.markdown(""" 20 | 21 | This Chatbot is hosted in an independent Backend Azure Web App and was created using the Bot Framework SDK. 22 | The Bot Interface is just a window to a Bot Service app hosted in Azure. 23 | 24 | It has access to the following tools/pluggins: 25 | 26 | - Bing Search (***use @websearch in your question***) 27 | - Azure SQL for covid statistics data (***use @sqlsearch in your question***) 28 | - Azure Search for documents knowledge - Friends Dialogs, Covid Papers, Books(***use @docsearch in your question***) 29 | - API Search for real-time currency and digital coins pricing (***use @apisearch in your question***) 30 | - CSV Analysis for covid statistics data (***use @csvsearch in your question***) 31 | 32 | Note: If you don't use any of the tool names beginning with @, the bot will try to use it's own knowledge or the websearch to answer the question. 33 | 34 | Example questions: 35 | 36 | - Hello, my name is Bob, what's yours? 37 | - @websearch, What's the main economic news of today? 38 | - @docsearch, what normally rich dad do that is different from poor dad? 39 | - @docsearch, Why Covid doesn't affect kids that much compared to adults? 40 | - @sqlsearch, How many people where hospitalized in Arkansas in June 2020? 41 | - @docsearch, Tell me about the "PIVOT" scene 42 | - @websearch, what movies are showing tonight in Seattle? 43 | """) 44 | 45 | st.markdown(""" 46 | 52 | """, unsafe_allow_html=True) 53 | 54 | 55 | BOT_DIRECTLINE_SECRET_KEY = os.environ.get("BOT_DIRECTLINE_SECRET_KEY") 56 | 57 | components.html( 58 | f""" 59 | 60 | 61 | 65 | 66 | 88 | 89 | 90 |

Bot Service + Azure OpenAI

91 |
92 | 152 | 153 | 154 | """, height=800) 155 | -------------------------------------------------------------------------------- /apps/frontend/app/pages/3_FastAPI_Chat.py: -------------------------------------------------------------------------------- 1 | # app/pages/Chat.py 2 | 3 | import os 4 | import sys 5 | 6 | # ----------------------------------------------------------------------------- 7 | # Imports 8 | # ----------------------------------------------------------------------------- 9 | # Import STT (speech-to-text) and TTS (text-to-speech) functions from audio_utils.py 10 | try: 11 | from audio_utils import ( 12 | speech_to_text_from_bytes as speech_to_text, 13 | text_to_speech, 14 | ) 15 | except Exception as e: 16 | # If local import fails, add the path four levels up and import from there 17 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../'))) 18 | from common.audio_utils import ( 19 | speech_to_text_from_bytes as speech_to_text, 20 | text_to_speech, 21 | ) 22 | 23 | import streamlit as st 24 | 25 | from app import ( 26 | model_name, 27 | api_url, 28 | get_env_var, 29 | ) 30 | from langchain_core.messages import AIMessage, HumanMessage 31 | from helpers.streamlit_helpers import ( 32 | configure_page, 33 | get_or_create_ids, 34 | consume_api, 35 | initialize_chat_history, 36 | display_chat_history, 37 | autoplay_audio, 38 | get_logger, 39 | ) 40 | 41 | from audio_recorder_streamlit import audio_recorder 42 | 43 | # ----------------------------------------------------------------------------- 44 | # Page Configuration 45 | # ----------------------------------------------------------------------------- 46 | page_title = get_env_var("AGENT_PAGE_TITLE", default_value="AI Agent", required=True) 47 | configure_page(page_title, "💬") 48 | 49 | logger = get_logger(__name__) 50 | logger.info(f"Page configured with title: {page_title}") 51 | 52 | # ----------------------------------------------------------------------------- 53 | # Initialize Session IDs and Chat History 54 | # ----------------------------------------------------------------------------- 55 | session_id, user_id = get_or_create_ids() 56 | initialize_chat_history(model_name) 57 | 58 | # ----------------------------------------------------------------------------- 59 | # Sidebar (Voice Input Option) 60 | # ----------------------------------------------------------------------------- 61 | with st.sidebar: 62 | st.header("Voice Input") 63 | voice_enabled = st.checkbox("Enable Voice Capabilities") 64 | 65 | # If voice is enabled, provide audio recorder 66 | audio_bytes = None 67 | if voice_enabled: 68 | audio_bytes = audio_recorder( 69 | text="Click to Talk", 70 | recording_color="red", 71 | neutral_color="#6aa36f", 72 | icon_size="2x", 73 | sample_rate=16000 74 | ) 75 | if audio_bytes: 76 | logger.info("Audio recorded from user microphone.") 77 | 78 | # ----------------------------------------------------------------------------- 79 | # Display Existing Chat Messages 80 | # ----------------------------------------------------------------------------- 81 | display_chat_history() 82 | logger.debug("Displayed existing chat history.") 83 | 84 | # ----------------------------------------------------------------------------- 85 | # Handle User Input (Text & Audio) 86 | # ----------------------------------------------------------------------------- 87 | new_user_message = False 88 | 89 | # Text query from the st.chat_input 90 | user_query = st.chat_input("Type your message here...") 91 | typed_query = user_query.strip() if user_query else None 92 | 93 | # 1) If voice is enabled, we allow typed OR voice input 94 | if voice_enabled: 95 | if typed_query: 96 | # A typed query takes priority if present 97 | st.session_state.chat_history.append(HumanMessage(content=typed_query)) 98 | with st.chat_message("Human"): 99 | st.markdown(typed_query) 100 | logger.info("User typed query added to chat history: %s", typed_query) 101 | new_user_message = True 102 | elif audio_bytes: 103 | # Only if there's no typed input, process recorded audio 104 | transcript = speech_to_text(audio_bytes) 105 | logger.debug(f"Transcript from STT: {transcript}") 106 | if transcript: 107 | st.session_state.chat_history.append(HumanMessage(content=transcript)) 108 | with st.chat_message("Human"): 109 | st.write(transcript) 110 | logger.info("Transcript added to chat history.") 111 | new_user_message = True 112 | 113 | # 2) If voice is disabled, we only process typed input 114 | else: 115 | if typed_query: 116 | st.session_state.chat_history.append(HumanMessage(content=typed_query)) 117 | with st.chat_message("Human"): 118 | st.markdown(typed_query) 119 | logger.info("User typed query added to chat history: %s", typed_query) 120 | new_user_message = True 121 | 122 | # ----------------------------------------------------------------------------- 123 | # Generate AI Response (If We Have a New User Message) 124 | # ----------------------------------------------------------------------------- 125 | if new_user_message: 126 | # The last message is now from a Human; let's call the AI 127 | with st.chat_message("AI"): 128 | try: 129 | user_text = st.session_state.chat_history[-1].content 130 | logger.info("Sending request to SSE /stream endpoint with user query.") 131 | 132 | # Stream the AI response using your SSE consumption function 133 | ai_response = st.write_stream( 134 | consume_api(api_url, user_text, session_id, user_id) 135 | ) 136 | logger.info("AI streaming complete. Final text aggregated.") 137 | except Exception as e: 138 | logger.error(f"Error during SSE consumption: {e}", exc_info=True) 139 | st.error("Failed to get a response from the AI.") 140 | ai_response = None 141 | 142 | # Append AI response to chat history 143 | if ai_response: 144 | st.session_state.chat_history.append(AIMessage(content=ai_response)) 145 | 146 | # If voice is enabled, convert AI response text to speech and auto-play 147 | if voice_enabled: 148 | try: 149 | audio_file_path = text_to_speech(ai_response) 150 | if audio_file_path: 151 | autoplay_audio(audio_file_path) 152 | logger.info("Audio response generated and played.") 153 | os.remove(audio_file_path) 154 | logger.info("Temporary audio file removed.") 155 | except Exception as ex: 156 | logger.error(f"Error generating or playing audio: {ex}", exc_info=True) 157 | -------------------------------------------------------------------------------- /apps/frontend/azuredeploy-frontend.bicep: -------------------------------------------------------------------------------- 1 | @description('Optional. Web app name must be between 2 and 60 characters.') 2 | @minLength(2) 3 | @maxLength(60) 4 | param webAppName string = 'webApp-Frontend-${uniqueString(resourceGroup().id)}' 5 | 6 | @description('Optional, defaults to S3. The SKU of App Service Plan. The allowed values are B3, S3 and P2v3.') 7 | @allowed([ 8 | 'B3' 9 | 'S3' 10 | 'P2v3' 11 | ]) 12 | param appServicePlanSKU string = 'S3' 13 | 14 | @description('Optional. The name of the App Service Plan.') 15 | param appServicePlanName string = 'AppServicePlan-Frontend-${uniqueString(resourceGroup().id)}' 16 | 17 | @description('Required. The name of your Bot Service.') 18 | param botServiceName string 19 | 20 | @description('Required. The key to the direct line channel of your bot.') 21 | @secure() 22 | param botDirectLineChannelKey string 23 | 24 | @description('Required. The SAS token for the Azure Storage Account hosting your data') 25 | @secure() 26 | param blobSASToken string 27 | 28 | @description('Optional. The name of the resource group where the resources (Azure Search etc.) where deployed previously. Defaults to current resource group.') 29 | param resourceGroupSearch string = resourceGroup().name 30 | 31 | @description('Required. The name of the Azure Search service deployed previously.') 32 | param azureSearchName string 33 | 34 | @description('Optional. The API version of the Azure Search.') 35 | param azureSearchAPIVersion string = '2024-11-01-preview' 36 | 37 | @description('Required. The name of the Azure OpenAI resource deployed previously.') 38 | param azureOpenAIName string 39 | 40 | @description('Required. The API key of the Azure OpenAI resource deployed previously.') 41 | @secure() 42 | param azureOpenAIAPIKey string 43 | 44 | @description('Required. The deployment name of the Azure OpenAI GPT-4o model.') 45 | param gpt4oDeploymentName string = 'gpt-4o' 46 | 47 | @description('Required. The deployment name of the Azure OpenAI GPT-4o-mini model.') 48 | param gpt4oMiniDeploymentName string = 'gpt-4o-mini' 49 | 50 | @description('Required. The API version of the Azure OpenAI.') 51 | param azureOpenAIAPIVersion string = '2024-10-01-preview' 52 | 53 | @description('Required. Select the Speech engine: openai or azure.') 54 | param speechEngine string = 'openai' 55 | 56 | @description('Required. Select deployment name for the AOAI whisper model') 57 | param openAIWhisperModelName string = 'whisper' 58 | 59 | @description('Required. Select deployment name for the AOAI TTS model') 60 | param openAITTSModelName string = 'tts' 61 | 62 | @description('Optional, defaults to resource group location. The location of the resources.') 63 | param location string = resourceGroup().location 64 | 65 | // Define Variables 66 | var backendWebAppName = 'webApp-Backend-${botServiceName}' 67 | var fastAPIsiteHost = 'https://${backendWebAppName}-staging.azurewebsites.net' 68 | 69 | // Existing Azure Search service. 70 | resource azureSearch 'Microsoft.Search/searchServices@2021-04-01-preview' existing = { 71 | name: azureSearchName 72 | scope: resourceGroup(resourceGroupSearch) 73 | } 74 | 75 | // Create a new Linux App Service Plan. 76 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { 77 | name: appServicePlanName 78 | location: location 79 | sku: { 80 | name: appServicePlanSKU 81 | } 82 | kind: 'linux' 83 | properties: { 84 | reserved: true 85 | } 86 | } 87 | 88 | // Create a Web App using a Linux App Service Plan. 89 | resource webApp 'Microsoft.Web/sites@2022-09-01' = { 90 | name: webAppName 91 | tags: { 'azd-service-name': 'frontend' } 92 | location: location 93 | properties: { 94 | serverFarmId: appServicePlan.id 95 | siteConfig: { 96 | appSettings: [ 97 | { 98 | name: 'BOT_SERVICE_NAME' 99 | value: botServiceName 100 | } 101 | { 102 | name: 'BOT_DIRECTLINE_SECRET_KEY' 103 | value: botDirectLineChannelKey 104 | } 105 | { 106 | name: 'BLOB_SAS_TOKEN' 107 | value: blobSASToken 108 | } 109 | { 110 | name: 'AZURE_SEARCH_ENDPOINT' 111 | value: 'https://${azureSearchName}.search.windows.net' 112 | } 113 | { 114 | name: 'AZURE_SEARCH_KEY' 115 | value: azureSearch.listAdminKeys().primaryKey 116 | } 117 | { 118 | name: 'AZURE_SEARCH_API_VERSION' 119 | value: azureSearchAPIVersion 120 | } 121 | { 122 | name: 'AZURE_OPENAI_ENDPOINT' 123 | value: 'https://${azureOpenAIName}.openai.azure.com/' 124 | } 125 | { 126 | name: 'AZURE_OPENAI_API_KEY' 127 | value: azureOpenAIAPIKey 128 | } 129 | { 130 | name: 'GPT4o_DEPLOYMENT_NAME' 131 | value: gpt4oDeploymentName 132 | } 133 | { 134 | name: 'GPT4oMINI_DEPLOYMENT_NAME' 135 | value: gpt4oMiniDeploymentName 136 | } 137 | { 138 | name: 'AZURE_OPENAI_API_VERSION' 139 | value: azureOpenAIAPIVersion 140 | } 141 | { 142 | name: 'SPEECH_ENGINE' 143 | value: speechEngine 144 | } 145 | { 146 | name: 'AZURE_OPENAI_WHISPER_MODEL_NAME' 147 | value: openAIWhisperModelName 148 | } 149 | { 150 | name: 'AZURE_OPENAI_TTS_MODEL_NAME' 151 | value: openAITTSModelName 152 | } 153 | { 154 | name: 'FAST_API_SERVER' 155 | value: fastAPIsiteHost 156 | } 157 | { 158 | name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' 159 | value: 'true' 160 | } 161 | ] 162 | } 163 | } 164 | } 165 | 166 | resource webAppConfig 'Microsoft.Web/sites/config@2022-09-01' = { 167 | parent: webApp 168 | name: 'web' 169 | properties: { 170 | linuxFxVersion: 'PYTHON|3.12' 171 | alwaysOn: true 172 | appCommandLine: 'python -m streamlit run app/Home.py --server.port 8000 --server.address 0.0.0.0' 173 | } 174 | } 175 | 176 | output webAppURL string = webApp.properties.defaultHostName 177 | output webAppName string = webApp.name 178 | -------------------------------------------------------------------------------- /apps/frontend/azuredeploy-frontend.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.26.54.24096", 8 | "templateHash": "17102531740028217408" 9 | } 10 | }, 11 | "parameters": { 12 | "webAppName": { 13 | "type": "string", 14 | "defaultValue": "[format('webApp-Frontend-{0}', uniqueString(resourceGroup().id))]", 15 | "minLength": 2, 16 | "maxLength": 60, 17 | "metadata": { 18 | "description": "Optional. Web app name must be between 2 and 60 characters." 19 | } 20 | }, 21 | "appServicePlanSKU": { 22 | "type": "string", 23 | "defaultValue": "S3", 24 | "allowedValues": [ 25 | "B3", 26 | "S3", 27 | "P2v3" 28 | ], 29 | "metadata": { 30 | "description": "Optional, defaults to S3. The SKU of App Service Plan. The allowed values are B3, S3 and P2v3." 31 | } 32 | }, 33 | "appServicePlanName": { 34 | "type": "string", 35 | "defaultValue": "[format('AppServicePlan-Frontend-{0}', uniqueString(resourceGroup().id))]", 36 | "metadata": { 37 | "description": "Optional. The name of the App Service Plan." 38 | } 39 | }, 40 | "botServiceName": { 41 | "type": "string", 42 | "metadata": { 43 | "description": "Required. The name of your Bot Service." 44 | } 45 | }, 46 | "botDirectLineChannelKey": { 47 | "type": "securestring", 48 | "metadata": { 49 | "description": "Required. The key to the direct line channel of your bot." 50 | } 51 | }, 52 | "blobSASToken": { 53 | "type": "securestring", 54 | "metadata": { 55 | "description": "Required. The SAS token for the Azure Storage Account hosting your data" 56 | } 57 | }, 58 | "resourceGroupSearch": { 59 | "type": "string", 60 | "defaultValue": "[resourceGroup().name]", 61 | "metadata": { 62 | "description": "Optional. The name of the resource group where the resources (Azure Search etc.) where deployed previously. Defaults to current resource group." 63 | } 64 | }, 65 | "azureSearchName": { 66 | "type": "string", 67 | "metadata": { 68 | "description": "Required. The name of the Azure Search service deployed previously." 69 | } 70 | }, 71 | "azureSearchAPIVersion": { 72 | "type": "string", 73 | "defaultValue": "2024-11-01-preview", 74 | "metadata": { 75 | "description": "Optional. The API version of the Azure Search." 76 | } 77 | }, 78 | "azureOpenAIName": { 79 | "type": "string", 80 | "metadata": { 81 | "description": "Required. The name of the Azure OpenAI resource deployed previously." 82 | } 83 | }, 84 | "azureOpenAIAPIKey": { 85 | "type": "securestring", 86 | "metadata": { 87 | "description": "Required. The API key of the Azure OpenAI resource deployed previously." 88 | } 89 | }, 90 | "gpt4oDeploymentName": { 91 | "type": "string", 92 | "defaultValue": "gpt-4o", 93 | "metadata": { 94 | "description": "Required. The deployment name of the Azure OpenAI GPT-4o model." 95 | } 96 | }, 97 | "gpt4oMiniDeploymentName": { 98 | "type": "string", 99 | "defaultValue": "gpt-4o-mini", 100 | "metadata": { 101 | "description": "Required. The deployment name of the Azure OpenAI GPT-4o-mini model." 102 | } 103 | }, 104 | "azureOpenAIAPIVersion": { 105 | "type": "string", 106 | "defaultValue": "2024-10-01-preview", 107 | "metadata": { 108 | "description": "Required. The API version of the Azure OpenAI." 109 | } 110 | }, 111 | "speechEngine": { 112 | "type": "string", 113 | "defaultValue": "openai", 114 | "metadata": { 115 | "description": "Required. Select the Speech engine: openai or azure" 116 | } 117 | }, 118 | "openAIWhisperModelName": { 119 | "type": "string", 120 | "defaultValue": "whisper", 121 | "metadata": { 122 | "description": "Required. Select deployment name for the AOAI whisper model" 123 | } 124 | }, 125 | "openAITTSModelName": { 126 | "type": "string", 127 | "defaultValue": "tts", 128 | "metadata": { 129 | "description": "Required. Select deployment name for the AOAI TTS model" 130 | } 131 | }, 132 | "location": { 133 | "type": "string", 134 | "defaultValue": "[resourceGroup().location]", 135 | "metadata": { 136 | "description": "Optional, defaults to resource group location. The location of the resources." 137 | } 138 | } 139 | }, 140 | "variables": { 141 | "backendWebAppName": "[format('webApp-Backend-{0}', parameters('botServiceName'))]", 142 | "fastAPIsiteHost": "[format('https://{0}-staging.azurewebsites.net', variables('backendWebAppName'))]" 143 | }, 144 | "resources": [ 145 | { 146 | "type": "Microsoft.Web/serverfarms", 147 | "apiVersion": "2022-09-01", 148 | "name": "[parameters('appServicePlanName')]", 149 | "location": "[parameters('location')]", 150 | "sku": { 151 | "name": "[parameters('appServicePlanSKU')]" 152 | }, 153 | "kind": "linux", 154 | "properties": { 155 | "reserved": true 156 | } 157 | }, 158 | { 159 | "type": "Microsoft.Web/sites", 160 | "apiVersion": "2022-09-01", 161 | "name": "[parameters('webAppName')]", 162 | "tags": { 163 | "azd-service-name": "frontend" 164 | }, 165 | "location": "[parameters('location')]", 166 | "properties": { 167 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", 168 | "siteConfig": { 169 | "appSettings": [ 170 | { 171 | "name": "BOT_SERVICE_NAME", 172 | "value": "[parameters('botServiceName')]" 173 | }, 174 | { 175 | "name": "BOT_DIRECTLINE_SECRET_KEY", 176 | "value": "[parameters('botDirectLineChannelKey')]" 177 | }, 178 | { 179 | "name": "BLOB_SAS_TOKEN", 180 | "value": "[parameters('blobSASToken')]" 181 | }, 182 | { 183 | "name": "AZURE_SEARCH_ENDPOINT", 184 | "value": "[format('https://{0}.search.windows.net', parameters('azureSearchName'))]" 185 | }, 186 | { 187 | "name": "AZURE_SEARCH_KEY", 188 | "value": "[listAdminKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupSearch')), 'Microsoft.Search/searchServices', parameters('azureSearchName')), '2021-04-01-preview').primaryKey]" 189 | }, 190 | { 191 | "name": "AZURE_SEARCH_API_VERSION", 192 | "value": "[parameters('azureSearchAPIVersion')]" 193 | }, 194 | { 195 | "name": "AZURE_OPENAI_ENDPOINT", 196 | "value": "[format('https://{0}.openai.azure.com/', parameters('azureOpenAIName'))]" 197 | }, 198 | { 199 | "name": "AZURE_OPENAI_API_KEY", 200 | "value": "[parameters('azureOpenAIAPIKey')]" 201 | }, 202 | { 203 | "name": "GPT4o_DEPLOYMENT_NAME", 204 | "value": "[parameters('gpt4oDeploymentName')]" 205 | }, 206 | { 207 | "name": "GPT4oMINI_DEPLOYMENT_NAME", 208 | "value": "[parameters('gpt4oMiniDeploymentName')]" 209 | }, 210 | { 211 | "name": "AZURE_OPENAI_API_VERSION", 212 | "value": "[parameters('azureOpenAIAPIVersion')]" 213 | }, 214 | { 215 | "name": "SPEECH_ENGINE", 216 | "value": "[parameters('speechEngine')]" 217 | }, 218 | { 219 | "name": "AZURE_OPENAI_WHISPER_MODEL_NAME", 220 | "value": "[parameters('openAIWhisperModelName')]" 221 | }, 222 | { 223 | "name": "AZURE_OPENAI_TTS_MODEL_NAME", 224 | "value": "[parameters('openAITTSModelName')]" 225 | }, 226 | { 227 | "name": "FAST_API_SERVER", 228 | "value": "[variables('fastAPIsiteHost')]" 229 | }, 230 | { 231 | "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", 232 | "value": "true" 233 | } 234 | ] 235 | } 236 | }, 237 | "dependsOn": [ 238 | "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]" 239 | ] 240 | }, 241 | { 242 | "type": "Microsoft.Web/sites/config", 243 | "apiVersion": "2022-09-01", 244 | "name": "[format('{0}/{1}', parameters('webAppName'), 'web')]", 245 | "properties": { 246 | "linuxFxVersion": "PYTHON|3.12", 247 | "alwaysOn": true, 248 | "appCommandLine": "python -m streamlit run app/Home.py --server.port 8000 --server.address 0.0.0.0" 249 | }, 250 | "dependsOn": [ 251 | "[resourceId('Microsoft.Web/sites', parameters('webAppName'))]" 252 | ] 253 | } 254 | ], 255 | "outputs": { 256 | "webAppURL": { 257 | "type": "string", 258 | "value": "[reference(resourceId('Microsoft.Web/sites', parameters('webAppName')), '2022-09-01').defaultHostName]" 259 | }, 260 | "webAppName": { 261 | "type": "string", 262 | "value": "[parameters('webAppName')]" 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /apps/frontend/frontend.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/apps/frontend/frontend.zip -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | # This is an example starter azure.yaml file containing several example services in comments below. 4 | # Make changes as needed to describe your application setup. 5 | # To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema 6 | 7 | # Name of the application. 8 | name: Azure-Cognitive-Search-Azure-OpenAI-Accelerator 9 | 10 | hooks: 11 | preup: 12 | windows: 13 | shell: pwsh 14 | run: ./infra/scripts/CreatePrerequisites.ps1 15 | interactive: true 16 | continueOnError: false 17 | posix: 18 | shell: pwsh 19 | run: ./infra/scripts/CreatePrerequisites.ps1 20 | interactive: true 21 | continueOnError: false 22 | preprovision: 23 | windows: 24 | shell: pwsh 25 | run: ./infra/scripts/CreateAppRegistration.ps1 26 | interactive: true 27 | continueOnError: false 28 | posix: 29 | shell: pwsh 30 | run: ./infra/scripts/CreateAppRegistration.ps1 31 | interactive: true 32 | continueOnError: false 33 | postprovision: 34 | windows: 35 | shell: pwsh 36 | run: ./infra/scripts/UpdateSecretsInApps.ps1 37 | interactive: true 38 | continueOnError: false 39 | posix: 40 | shell: pwsh 41 | run: ./infra/scripts/UpdateSecretsInApps.ps1 42 | interactive: true 43 | continueOnError: false 44 | 45 | services: 46 | backend: 47 | project: ./infra/target/backend 48 | language: py 49 | host: appservice 50 | hooks: 51 | prepackage: 52 | windows: 53 | shell: pwsh 54 | run: Remove-Item * -Recurse;Copy-Item -Path "../../../apps/backend/*" -Destination "./" -force;Copy-Item -Path "../../../common/*" -Destination "./" -Recurse -force;pip install -r requirements.txt 55 | interactive: true 56 | continueOnError: false 57 | posix: 58 | shell: pwsh 59 | run: 'Remove-Item * -Recurse;Copy-Item -Path "../../../apps/backend/*" -Destination "./" -force;Copy-Item -Path "../../../common/*" -Destination "./" -Recurse -force;pip install -r requirements.txt' 60 | interactive: true 61 | continueOnError: false 62 | frontend: 63 | project: ./infra/target/frontend 64 | language: py 65 | host: appservice 66 | hooks: 67 | prepackage: 68 | windows: 69 | shell: pwsh 70 | run: Remove-Item * -Recurse; Copy-Item -Path "../../../apps/frontend/*" -Destination "./" -Recurse -Force; Copy-Item -Path "../../../common/*" -Destination "./" -Recurse -force;pip install -r requirements.txt 71 | interactive: true 72 | continueOnError: false 73 | posix: 74 | shell: pwsh 75 | run: 'Remove-Item * -Recurse; Copy-Item -Path "../../../apps/frontend/*" -Destination "./" -Recurse -Force; Copy-Item -Path "../../../common/*" -Destination "./" -Recurse -force;pip install -r requirements.txt' 76 | interactive: true 77 | continueOnError: false 78 | -------------------------------------------------------------------------------- /azuredeploy.bicep: -------------------------------------------------------------------------------- 1 | @description('Optional. Service name must only contain lowercase letters, digits or dashes, cannot use dash as the first two or last one characters, cannot contain consecutive dashes, and is limited between 2 and 60 characters in length.') 2 | @minLength(2) 3 | @maxLength(60) 4 | param azureSearchName string = 'cog-search-${uniqueString(resourceGroup().id)}' 5 | 6 | @description('Optional, defaults to standard. The pricing tier of the search service you want to create (for example, basic or standard).') 7 | @allowed([ 8 | 'free' 9 | 'basic' 10 | 'standard' 11 | 'standard2' 12 | 'standard3' 13 | 'storage_optimized_l1' 14 | 'storage_optimized_l2' 15 | ]) 16 | param azureSearchSKU string = 'standard' 17 | 18 | @description('Optional, defaults to 1. Replicas distribute search workloads across the service. You need at least two replicas to support high availability of query workloads (not applicable to the free tier). Must be between 1 and 12.') 19 | @minValue(1) 20 | @maxValue(12) 21 | param azureSearchReplicaCount int = 1 22 | 23 | @description('Optional, defaults to 1. Partitions allow for scaling of document count as well as faster indexing by sharding your index over multiple search units. Allowed values: 1, 2, 3, 4, 6, 12.') 24 | @allowed([ 25 | 1 26 | 2 27 | 3 28 | 4 29 | 6 30 | 12 31 | ]) 32 | param azureSearchPartitionCount int = 1 33 | 34 | @description('Optional, defaults to default. Applicable only for SKUs set to standard3. You can set this property to enable a single, high density partition that allows up to 1000 indexes, which is much higher than the maximum indexes allowed for any other SKU.') 35 | @allowed([ 36 | 'default' 37 | 'highDensity' 38 | ]) 39 | param azureSearchHostingMode string = 'default' 40 | 41 | @description('Optional. The name of our application. It has to be unique. Type a name followed by your resource group name. (-)') 42 | param cognitiveServiceName string = 'cognitive-service-${uniqueString(resourceGroup().id)}' 43 | 44 | @description('Optional. The name of the SQL logical server.') 45 | param SQLServerName string = 'sql-server-${uniqueString(resourceGroup().id)}' 46 | 47 | @description('Optional. The name of the SQL Database.') 48 | param SQLDBName string = 'SampleDB' 49 | 50 | @description('Required. The administrator username of the SQL logical server.') 51 | param SQLAdministratorLogin string 52 | 53 | @description('Required. The administrator password of the SQL logical server.') 54 | @secure() 55 | param SQLAdministratorLoginPassword string 56 | 57 | @description('Optional. The name of the Bing Search API service') 58 | param bingSearchAPIName string = 'bing-search-${uniqueString(resourceGroup().id)}' 59 | 60 | @description('Optional. Cosmos DB account name, max length 44 characters, lowercase') 61 | param cosmosDBAccountName string = 'cosmosdb-account-${uniqueString(resourceGroup().id)}' 62 | 63 | @description('Optional. The name for the CosmosDB database') 64 | param cosmosDBDatabaseName string = 'cosmosdb-db-${uniqueString(resourceGroup().id)}' 65 | 66 | @description('Optional. The name for the CosmosDB database container') 67 | param cosmosDBContainerName string = 'cosmosdb-container-${uniqueString(resourceGroup().id)}' 68 | 69 | @description('Optional. The name of the Form Recognizer service') 70 | param formRecognizerName string = 'form-recognizer-${uniqueString(resourceGroup().id)}' 71 | 72 | @description('Optional. The name of the Azure Speech service') 73 | param speechServiceName string = 'speechservice-${uniqueString(resourceGroup().id)}' 74 | 75 | @description('Optional. The name of the Blob Storage account') 76 | param blobStorageAccountName string = 'blobstorage${uniqueString(resourceGroup().id)}' 77 | 78 | @description('Optional, defaults to resource group location. The location of the resources.') 79 | param location string = resourceGroup().location 80 | 81 | var cognitiveServiceSKU = 'S0' 82 | 83 | resource azureSearch 'Microsoft.Search/searchServices@2021-04-01-preview' = { 84 | name: azureSearchName 85 | location: location 86 | sku: { 87 | name: azureSearchSKU 88 | } 89 | properties: { 90 | replicaCount: azureSearchReplicaCount 91 | partitionCount: azureSearchPartitionCount 92 | hostingMode: azureSearchHostingMode 93 | semanticSearch: 'standard' 94 | } 95 | } 96 | 97 | resource cognitiveService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 98 | name: cognitiveServiceName 99 | location: location 100 | sku: { 101 | name: cognitiveServiceSKU 102 | } 103 | kind: 'CognitiveServices' 104 | properties: { 105 | apiProperties: { 106 | statisticsEnabled: false 107 | } 108 | } 109 | } 110 | 111 | resource SQLServer 'Microsoft.Sql/servers@2022-11-01-preview' = { 112 | name: SQLServerName 113 | location: location 114 | properties: { 115 | administratorLogin: SQLAdministratorLogin 116 | administratorLoginPassword: SQLAdministratorLoginPassword 117 | } 118 | } 119 | 120 | resource SQLDatabase 'Microsoft.Sql/servers/databases@2022-11-01-preview' = { 121 | parent: SQLServer 122 | name: SQLDBName 123 | location: location 124 | sku: { 125 | name: 'Standard' 126 | tier: 'Standard' 127 | } 128 | } 129 | 130 | resource SQLFirewallRules 'Microsoft.Sql/servers/firewallRules@2022-11-01-preview' = { 131 | parent: SQLServer 132 | name: 'AllowAllAzureIPs' 133 | properties: { 134 | startIpAddress: '0.0.0.0' 135 | endIpAddress: '255.255.255.255' 136 | } 137 | } 138 | 139 | resource cosmosDBAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { 140 | name: cosmosDBAccountName 141 | location: location 142 | kind: 'GlobalDocumentDB' 143 | properties: { 144 | databaseAccountOfferType: 'Standard' 145 | locations: [ 146 | { 147 | locationName: location 148 | } 149 | ] 150 | enableFreeTier: false 151 | isVirtualNetworkFilterEnabled: false 152 | publicNetworkAccess: 'Enabled' 153 | capabilities: [ 154 | { 155 | name: 'EnableServerless' 156 | } 157 | ] 158 | } 159 | } 160 | 161 | resource cosmosDBDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = { 162 | parent: cosmosDBAccount 163 | name: cosmosDBDatabaseName 164 | location: location 165 | properties: { 166 | resource: { 167 | id: cosmosDBDatabaseName 168 | } 169 | } 170 | } 171 | 172 | resource cosmosDBContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = { 173 | parent: cosmosDBDatabase 174 | name: cosmosDBContainerName 175 | location: location 176 | properties: { 177 | resource: { 178 | id: cosmosDBContainerName 179 | partitionKey: { 180 | paths: [ 181 | '/user_id' 182 | ] 183 | kind: 'Hash' 184 | version: 2 185 | } 186 | defaultTtl: 1000 187 | } 188 | } 189 | } 190 | 191 | resource bingSearchAccount 'Microsoft.Bing/accounts@2020-06-10' = { 192 | kind: 'Bing.Search.v7' 193 | name: bingSearchAPIName 194 | location: 'global' 195 | sku: { 196 | name: 'S1' 197 | } 198 | } 199 | 200 | resource formRecognizerAccount 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 201 | name: formRecognizerName 202 | location: location 203 | sku: { 204 | name: 'S0' 205 | } 206 | kind: 'FormRecognizer' 207 | properties: { 208 | apiProperties: { 209 | statisticsEnabled: false 210 | } 211 | } 212 | } 213 | 214 | resource speechService 'Microsoft.CognitiveServices/accounts@2024-06-01-preview' = { 215 | name: speechServiceName 216 | location: location 217 | sku: { 218 | name: 'S0' 219 | } 220 | kind: 'SpeechServices' 221 | properties: { 222 | networkAcls: { 223 | defaultAction: 'Allow' 224 | virtualNetworkRules: [] 225 | ipRules: [] 226 | } 227 | publicNetworkAccess: 'Enabled' 228 | } 229 | } 230 | 231 | resource blobStorageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { 232 | name: blobStorageAccountName 233 | location: location 234 | kind: 'StorageV2' 235 | sku: { 236 | name: 'Standard_LRS' 237 | } 238 | } 239 | 240 | resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { 241 | parent: blobStorageAccount 242 | name: 'default' 243 | properties: { 244 | deleteRetentionPolicy: { 245 | enabled: true 246 | days: 7 247 | } 248 | } 249 | } 250 | 251 | resource blobStorageContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = [for containerName in ['books', 'cord19', 'friends'] : { 252 | parent: blobServices 253 | name: containerName 254 | }] 255 | 256 | 257 | output azureSearchName string = azureSearchName 258 | output azureSearchEndpoint string = 'https://${azureSearchName}.search.windows.net' 259 | output azureSearchKey string = azureSearch.listAdminKeys().primaryKey 260 | output SQLServerName string = SQLServerName 261 | output SQLDatabaseName string = SQLDBName 262 | output cosmosDBAccountName string = cosmosDBAccountName 263 | output cosmosDBDatabaseName string = cosmosDBDatabaseName 264 | output cosmosDBContainerName string = cosmosDBContainerName 265 | output cosmosDBAccountEndpoint string = cosmosDBAccount.properties.documentEndpoint 266 | output cosmosDBConnectionString string = 'AccountEndpoint=${cosmosDBAccount.properties.documentEndpoint};AccountKey=${cosmosDBAccount.listKeys().primaryMasterKey}' 267 | output bingSearchAPIName string = bingSearchAPIName 268 | output bingServiceSearchKey string = bingSearchAccount.listKeys().key1 269 | output formRecognizerName string = formRecognizerName 270 | output formRecognizerEndpoint string = 'https://${formRecognizerName}.cognitiveservices.azure.com' 271 | output formRecognizerKey string = formRecognizerAccount.listKeys().key1 272 | output speechServiceName string = speechService.name 273 | output speechServiceEndpoint string = format('https://{0}.cognitiveservices.azure.com', speechService.name) 274 | output speechServiceKey string = listKeys(speechService.id, '2024-06-01-preview').key1 275 | output cognitiveServiceName string = cognitiveServiceName 276 | output cognitiveServiceKey string = cognitiveService.listKeys().key1 277 | output blobStorageAccountName string = blobStorageAccountName 278 | output blobConnectionString string = 'DefaultEndpointsProtocol=https;AccountName=${blobStorageAccountName};AccountKey=${blobStorageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net' 279 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/common/__init__.py -------------------------------------------------------------------------------- /common/audio_utils.py: -------------------------------------------------------------------------------- 1 | # audio_utils.py 2 | 3 | import os 4 | import logging 5 | import requests 6 | from io import BytesIO 7 | 8 | # LangChain Imports needed 9 | from langchain_openai import AzureChatOpenAI 10 | from langchain_core.prompts import ChatPromptTemplate 11 | from langchain_core.output_parsers import StrOutputParser 12 | 13 | import azure.cognitiveservices.speech as speechsdk 14 | from openai import AzureOpenAI 15 | try: 16 | from .prompts import SUMMARIZER_TEXT 17 | except Exception as e: 18 | from prompts import SUMMARIZER_TEXT 19 | 20 | 21 | ############################################################################### 22 | # Environment & Client Setup 23 | ############################################################################### 24 | 25 | def get_env_var(var_name, default_value=None, required=True): 26 | """ 27 | Retrieves an environment variable, optionally providing a default value. 28 | Raises an error if `required` is True and variable is not found. 29 | """ 30 | value = os.getenv(var_name, default_value) 31 | if required and value is None: 32 | raise EnvironmentError(f"Environment variable '{var_name}' is not set.") 33 | return value 34 | 35 | 36 | os.environ["OPENAI_API_VERSION"] = get_env_var("AZURE_OPENAI_API_VERSION") 37 | 38 | # For demonstration, pick which engine you want to use 39 | SPEECH_ENGINE = get_env_var("SPEECH_ENGINE", default_value="openai", required=False).lower() 40 | assert SPEECH_ENGINE in ["azure", "openai"], ( 41 | "SPEECH_ENGINE must be one of: azure, openai" 42 | ) 43 | 44 | # Setup for OpenAI or Azure keys 45 | openai_client = AzureOpenAI() 46 | 47 | if SPEECH_ENGINE == "azure": 48 | azure_speech_key = get_env_var("AZURE_SPEECH_KEY", required=True) 49 | azure_speech_region = get_env_var("AZURE_SPEECH_REGION", required=True) 50 | azure_speech_voice_name = get_env_var("AZURE_SPEECH_VOICE_NAME", default_value="en-US-AriaNeural", required=False) 51 | # not used for azure mode: 52 | whisper_model_name = None 53 | tts_model_name = None 54 | tts_voice_name = None 55 | 56 | elif SPEECH_ENGINE == "openai": 57 | whisper_model_name = get_env_var("AZURE_OPENAI_WHISPER_MODEL_NAME", default_value="whisper", required=True) 58 | tts_model_name = get_env_var("AZURE_OPENAI_TTS_MODEL_NAME", default_value="tts-hd", required=True) 59 | tts_voice_name = get_env_var("AZURE_OPENAI_TTS_VOICE_NAME", default_value="nova", required=False) 60 | # not used for openai mode: 61 | azure_speech_key = None 62 | azure_speech_region = None 63 | azure_speech_voice_name = None 64 | 65 | 66 | ############################################################################### 67 | # Speech-to-Text Functions 68 | ############################################################################### 69 | 70 | def recognize_whisper_api(audio_file, whisper_model: str): 71 | """ 72 | Call AzureOpenAI Whisper model to transcribe the audio. 73 | """ 74 | return openai_client.audio.transcriptions.create( 75 | model=whisper_model, 76 | response_format="text", 77 | file=audio_file 78 | ) 79 | 80 | def recognize_whisper_api_from_file(file_name: str, whisper_model: str): 81 | with open(file_name, "rb") as audio_file: 82 | transcript = recognize_whisper_api(audio_file, whisper_model) 83 | return transcript 84 | 85 | 86 | def recognize_azure_speech_to_text_from_file(file_path: str, key: str, region: str): 87 | """ 88 | Recognize speech from an audio file with automatic language detection 89 | across the top 6 spoken languages globally. 90 | 91 | Args: 92 | file_path (str): Path to the audio file. 93 | key (str): Azure Speech Service subscription key. 94 | region (str): Azure service region. 95 | 96 | Returns: 97 | string: Transcribed text. 98 | 99 | Raises: 100 | RuntimeError: If an error occurs during speech recognition. 101 | """ 102 | try: 103 | # Create a speech configuration with your subscription key and region 104 | speech_config = speechsdk.SpeechConfig(subscription=key, region=region) 105 | 106 | # Create an audio configuration pointing to the audio file 107 | audio_config = speechsdk.AudioConfig(filename=file_path) 108 | 109 | # Top 4 most spoken languages (ISO language codes) 110 | # SDK only supports 4 languages as options 111 | languages = ["en-US", "zh-CN", "hi-IN", "es-ES"] 112 | 113 | # Configure auto language detection with the specified languages 114 | auto_detect_source_language_config = speechsdk.languageconfig.AutoDetectSourceLanguageConfig(languages=languages) 115 | 116 | # Create a speech recognizer with the auto language detection configuration 117 | speech_recognizer = speechsdk.SpeechRecognizer( 118 | speech_config=speech_config, 119 | audio_config=audio_config, 120 | auto_detect_source_language_config=auto_detect_source_language_config 121 | ) 122 | 123 | # Perform speech recognition 124 | result = speech_recognizer.recognize_once_async().get() 125 | 126 | # Check the result 127 | if result.reason == speechsdk.ResultReason.RecognizedSpeech: 128 | # Retrieve the detected language 129 | detected_language = result.properties.get( 130 | speechsdk.PropertyId.SpeechServiceConnection_AutoDetectSourceLanguageResult, 131 | "Unknown" 132 | ) 133 | logging.debug("Detected Language %s", detected_language, exc_info=True) 134 | return result.text 135 | 136 | elif result.reason == speechsdk.ResultReason.NoMatch: 137 | raise RuntimeError("No speech could be recognized from the audio.") 138 | 139 | elif result.reason == speechsdk.ResultReason.Canceled: 140 | cancellation_details = speechsdk.CancellationDetails(result) 141 | raise RuntimeError(f"Speech Recognition canceled: {cancellation_details.reason}. " 142 | f"Error details: {cancellation_details.error_details}") 143 | 144 | else: 145 | raise RuntimeError("Unknown error occurred during speech recognition.") 146 | 147 | except Exception as e: 148 | raise RuntimeError(f"An error occurred during speech recognition: {e}") 149 | 150 | 151 | def speech_to_text_from_file(file_path: str): 152 | """ 153 | High-level function to transcribe audio from file, removing the file afterwards. 154 | """ 155 | try: 156 | if SPEECH_ENGINE == "openai": 157 | # Uses AzureOpenAI Whisper 158 | result = recognize_whisper_api_from_file(file_path, whisper_model_name) 159 | elif SPEECH_ENGINE == "azure": 160 | # Uses Azure speech 161 | result = recognize_azure_speech_to_text_from_file(file_path, azure_speech_key, azure_speech_region) 162 | else: 163 | result = None 164 | except Exception as e: 165 | print(f"Error in STT: {e}") 166 | result = None 167 | finally: 168 | if os.path.exists(file_path): 169 | os.remove(file_path) 170 | return result 171 | 172 | def speech_to_text_from_bytes(audio_bytes: BytesIO, temp_filename: str = "temp_audio_listen.wav"): 173 | """ 174 | Write a BytesIO object to disk, then call `speech_to_text_from_file`. 175 | """ 176 | with open(temp_filename, "wb") as audio_file: 177 | audio_file.write(audio_bytes) 178 | 179 | return speech_to_text_from_file(temp_filename) 180 | 181 | 182 | ############################################################################### 183 | # Text-to-Speech (TTS) Functions 184 | ############################################################################### 185 | 186 | import traceback 187 | 188 | def summarize_text(input_text: str) -> str: 189 | """ 190 | Converts the input text to a voice-ready short answer. 191 | 192 | This uses LangChain's AzureChatOpenAI with your custom summarization instructions. 193 | """ 194 | 195 | # For example, define how many tokens we allow for the completion 196 | COMPLETION_TOKENS = 1000 197 | 198 | # Build the prompt template for LangChain 199 | output_parser = StrOutputParser() 200 | prompt = ChatPromptTemplate.from_messages([ 201 | ("system", SUMMARIZER_TEXT), 202 | ("human", "Input Text: \n{input}") 203 | ]) 204 | 205 | try: 206 | # Create the LLM with AzureChatOpenAI using your deployment name 207 | llm = AzureChatOpenAI( 208 | deployment_name=os.environ["GPT4o_DEPLOYMENT_NAME"], 209 | temperature=0.5, 210 | max_tokens=COMPLETION_TOKENS 211 | ) 212 | 213 | # Build the chain 214 | chain = prompt | llm | output_parser 215 | 216 | # Run the chain with your input text 217 | summary = chain.invoke({"input": input_text}) 218 | 219 | # Return the summarized content 220 | return summary.strip() 221 | 222 | except Exception as e: 223 | logging.error("Error summarizing text with GPT-4o via LangChain: %s", str(e), exc_info=True) 224 | traceback.print_exc() 225 | # If summarization fails, just return the original text 226 | return input_text 227 | 228 | def text_to_speech_azure(input_text: str, output_filename="temp_audio_play.wav"): 229 | """ 230 | Use Azure TTS to synthesize speech from text, saving to `output_filename`. 231 | """ 232 | speech_config = speechsdk.SpeechConfig( 233 | subscription=azure_speech_key, 234 | region=azure_speech_region 235 | ) 236 | speech_config.speech_synthesis_voice_name = azure_speech_voice_name 237 | speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config) 238 | try: 239 | summarized_text = summarize_text(input_text) 240 | result = speech_synthesizer.speak_text_async(summarized_text).get() 241 | 242 | if result.reason != speechsdk.ResultReason.SynthesizingAudioCompleted: 243 | if result.reason == speechsdk.ResultReason.Canceled: 244 | cancellation_details = result.cancellation_details 245 | logging.error(f"Speech synthesis canceled: {cancellation_details.reason}") 246 | if cancellation_details.reason == speechsdk.CancellationReason.Error: 247 | logging.error(f"Error details: {cancellation_details.error_details}") 248 | raise Exception(f"Speech synthesis failed with reason: {result.reason}") 249 | 250 | audio_stream = speechsdk.AudioDataStream(result) 251 | audio_stream.save_to_wav_file(output_filename) 252 | return output_filename 253 | except Exception as e: 254 | logging.error("Azure TTS error: %s", e, exc_info=True) 255 | return None 256 | 257 | def text_to_speech_openai(input_text: str, output_filename="temp_audio_play.wav"): 258 | """ 259 | Use AzureOpenAI TTS. Adjust to reference openai_client usage for TTS if available. 260 | """ 261 | try: 262 | summarized_text = summarize_text(input_text) 263 | with openai_client.audio.speech.with_streaming_response.create( 264 | model=tts_model_name, 265 | voice=tts_voice_name, 266 | input=summarized_text 267 | ) as response: 268 | response.stream_to_file(output_filename) 269 | return output_filename 270 | except Exception as e: 271 | logging.error("OpenAI TTS error: %s", e, exc_info=True) 272 | return None 273 | 274 | 275 | def text_to_speech(input_text: str, engine=SPEECH_ENGINE, output_filename="temp_audio_play.wav"): 276 | """ 277 | High-level function to pick the correct TTS engine based on environment or parameter. 278 | 279 | :param input_text: Text to synthesize into speech 280 | :param engine: Which speech engine to use ("azure" or "openai") 281 | :param output_filename: Filename to save the synthesized speech 282 | :return: Path to the audio file or None if failed 283 | """ 284 | if engine == "azure": 285 | return text_to_speech_azure(input_text, output_filename=output_filename) 286 | elif engine == "openai": 287 | return text_to_speech_openai(input_text, output_filename=output_filename) 288 | else: 289 | logging.error("No valid speech engine specified.") 290 | return None 291 | 292 | 293 | -------------------------------------------------------------------------------- /common/graph.py: -------------------------------------------------------------------------------- 1 | # graph.py 2 | # ----------------------------------------------------------------------------- 3 | # All logic for building the multi-agent workflow in one place: 4 | # 1) Create the LLM 5 | # 2) Create specialized agents 6 | # 3) Define agent node and supervisor logic 7 | # 4) Build and return the async StateGraph 8 | # ----------------------------------------------------------------------------- 9 | 10 | import os 11 | import json 12 | import functools 13 | import operator 14 | import logging 15 | from pathlib import Path 16 | from typing_extensions import TypedDict 17 | from pydantic import BaseModel 18 | from typing import Annotated, Sequence, Literal 19 | 20 | from langchain_openai import AzureChatOpenAI 21 | from langchain_core.messages import AIMessage, BaseMessage 22 | from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 23 | from langgraph.graph import StateGraph, START, END 24 | 25 | # from your project 26 | from common.utils import ( 27 | create_docsearch_agent, 28 | create_csvsearch_agent, 29 | create_sqlsearch_agent, 30 | create_websearch_agent, 31 | create_apisearch_agent, 32 | reduce_openapi_spec 33 | ) 34 | from common.prompts import ( 35 | CUSTOM_CHATBOT_PREFIX, 36 | DOCSEARCH_PROMPT_TEXT, 37 | CSV_AGENT_PROMPT_TEXT, 38 | MSSQL_AGENT_PROMPT_TEXT, 39 | BING_PROMPT_TEXT, 40 | APISEARCH_PROMPT_TEXT, 41 | SUPERVISOR_PROMPT_TEXT 42 | ) 43 | 44 | # Set up a logger for this module 45 | logger = logging.getLogger(__name__) 46 | 47 | os.environ["OPENAI_API_VERSION"] = os.environ.get("AZURE_OPENAI_API_VERSION", "") 48 | 49 | # ----------------------------------------------------------------------------- 50 | # 1) The typed dictionary for the agent state 51 | # ----------------------------------------------------------------------------- 52 | class AgentState(TypedDict): 53 | """ 54 | The overall "conversation state" that flows through each node. 55 | messages: the running conversation (list of HumanMessage, AIMessage, etc.) 56 | next: indicates the next node to run 57 | """ 58 | messages: Annotated[Sequence[BaseMessage], operator.add] 59 | next: str 60 | 61 | 62 | # ----------------------------------------------------------------------------- 63 | # 2) Supervisor route structure 64 | # ----------------------------------------------------------------------------- 65 | class routeResponse(BaseModel): 66 | next: Literal[ 67 | "FINISH", 68 | "DocSearchAgent", 69 | "SQLSearchAgent", 70 | "CSVSearchAgent", 71 | "WebSearchAgent", 72 | "APISearchAgent", 73 | ] 74 | 75 | 76 | # ----------------------------------------------------------------------------- 77 | # 3) Specialized node calls 78 | # ----------------------------------------------------------------------------- 79 | async def agent_node_async(state: AgentState, agent, name: str): 80 | """ 81 | Invokes a specialized agent with the current conversation state. 82 | The agent returns a dictionary containing 'messages'. 83 | We then append its final message to the conversation with name=. 84 | """ 85 | logger.debug("agent_node_async: Called with agent=%s, name=%s, state=%s", agent, name, state) 86 | try: 87 | result = await agent.ainvoke(state) 88 | last_ai_content = result["messages"][-1].content 89 | logger.debug("agent_node_async: Agent '%s' responded with: %s", name, last_ai_content) 90 | return { 91 | "messages": [AIMessage(content=last_ai_content, name=name)] 92 | } 93 | except Exception as e: 94 | logger.exception("Exception in agent_node_async for agent=%s, name=%s", agent, name) 95 | raise 96 | 97 | 98 | async def supervisor_node_async(state: AgentState, llm: AzureChatOpenAI): 99 | """ 100 | Uses an LLM with structured output to figure out: 101 | -> which agent node should be invoked next, or 102 | -> FINISH the workflow. 103 | """ 104 | logger.debug("supervisor_node_async: Called with state=%s", state) 105 | supervisor_prompt = ChatPromptTemplate.from_messages( 106 | [ 107 | ("system", SUPERVISOR_PROMPT_TEXT), 108 | MessagesPlaceholder(variable_name="messages"), 109 | ( 110 | "system", 111 | "Given the conversation above, who should act next? Or should we FINISH?\n" 112 | "Select one of: ['DocSearchAgent','SQLSearchAgent','CSVSearchAgent'," 113 | "'WebSearchAgent','APISearchAgent','FINISH'].\n" 114 | ), 115 | ] 116 | ) 117 | 118 | chain = supervisor_prompt | llm.with_structured_output(routeResponse) 119 | 120 | try: 121 | result = await chain.ainvoke(state) 122 | logger.debug("supervisor_node_async: LLM routing result: %s", result) 123 | return result 124 | except Exception as e: 125 | logger.exception("Exception in supervisor_node_async.") 126 | raise 127 | 128 | 129 | # ----------------------------------------------------------------------------- 130 | # 4) Build the entire multi-agent workflow in a single function 131 | # ----------------------------------------------------------------------------- 132 | def build_async_workflow(csv_file_path: str ="all-states-history.csv", 133 | api_file_path: str ="openapi_kraken.json"): 134 | """ 135 | Creates the LLM, specialized agents, and the async StateGraph 136 | that orchestrates them with a supervisor node. Returns the 137 | not-yet-compiled workflow. You can then compile it with a checkpointer. 138 | """ 139 | logger.debug("build_async_workflow: Starting building workflow") 140 | 141 | # ------------------------------------ 142 | # A) Create LLM 143 | # ------------------------------------ 144 | model_name = os.environ.get("GPT4o_DEPLOYMENT_NAME", "") 145 | logger.debug("Creating LLM with deployment_name=%s", model_name) 146 | 147 | llm = AzureChatOpenAI( 148 | deployment_name=model_name, 149 | temperature=0, 150 | max_tokens=2000, 151 | streaming=True, # set True if you want partial streaming from the LLM 152 | ) 153 | 154 | # ------------------------------------ 155 | # B) Create specialized agents 156 | # ------------------------------------ 157 | logger.debug("Creating docsearch_agent, csvsearch_agent, sqlsearch_agent, websearch_agent, apisearch_agent") 158 | 159 | docsearch_agent = create_docsearch_agent( 160 | llm=llm, 161 | indexes=["srch-index-files", "srch-index-csv", "srch-index-books"], 162 | k=20, 163 | reranker_th=1.5, 164 | prompt=CUSTOM_CHATBOT_PREFIX + DOCSEARCH_PROMPT_TEXT, 165 | sas_token=os.environ.get("BLOB_SAS_TOKEN", "") 166 | ) 167 | 168 | csvsearch_agent = create_csvsearch_agent( 169 | llm=llm, 170 | prompt=CUSTOM_CHATBOT_PREFIX + CSV_AGENT_PROMPT_TEXT.format( 171 | file_url=str(csv_file_path) 172 | ) 173 | ) 174 | 175 | sqlsearch_agent = create_sqlsearch_agent( 176 | llm=llm, 177 | prompt=CUSTOM_CHATBOT_PREFIX + MSSQL_AGENT_PROMPT_TEXT 178 | ) 179 | 180 | websearch_agent = create_websearch_agent( 181 | llm=llm, 182 | prompt=CUSTOM_CHATBOT_PREFIX + BING_PROMPT_TEXT 183 | ) 184 | 185 | logger.debug("Reading API openapi_kraken.json from %s", api_file_path) 186 | with open(api_file_path, "r") as file: 187 | spec = json.load(file) 188 | reduced_api_spec = reduce_openapi_spec(spec) 189 | 190 | apisearch_agent = create_apisearch_agent( 191 | llm=llm, 192 | prompt=CUSTOM_CHATBOT_PREFIX + APISEARCH_PROMPT_TEXT.format( 193 | api_spec=reduced_api_spec 194 | ) 195 | ) 196 | 197 | # ------------------------------------ 198 | # C) Build the async LangGraph 199 | # ------------------------------------ 200 | logger.debug("Building the StateGraph for multi-agent workflow") 201 | workflow = StateGraph(AgentState) 202 | 203 | sup_node = functools.partial(supervisor_node_async, llm=llm) 204 | workflow.add_node("supervisor", sup_node) 205 | 206 | doc_node = functools.partial(agent_node_async, agent=docsearch_agent, name="DocSearchAgent") 207 | csv_node = functools.partial(agent_node_async, agent=csvsearch_agent, name="CSVSearchAgent") 208 | sql_node = functools.partial(agent_node_async, agent=sqlsearch_agent, name="SQLSearchAgent") 209 | web_node = functools.partial(agent_node_async, agent=websearch_agent, name="WebSearchAgent") 210 | api_node = functools.partial(agent_node_async, agent=apisearch_agent, name="APISearchAgent") 211 | 212 | workflow.add_node("DocSearchAgent", doc_node) 213 | workflow.add_node("CSVSearchAgent", csv_node) 214 | workflow.add_node("SQLSearchAgent", sql_node) 215 | workflow.add_node("WebSearchAgent", web_node) 216 | workflow.add_node("APISearchAgent", api_node) 217 | 218 | for agent_name in ["DocSearchAgent", "CSVSearchAgent", "SQLSearchAgent", "WebSearchAgent", "APISearchAgent"]: 219 | workflow.add_edge(agent_name, "supervisor") 220 | 221 | conditional_map = { 222 | "DocSearchAgent": "DocSearchAgent", 223 | "SQLSearchAgent": "SQLSearchAgent", 224 | "CSVSearchAgent": "CSVSearchAgent", 225 | "WebSearchAgent": "WebSearchAgent", 226 | "APISearchAgent": "APISearchAgent", 227 | "FINISH": END 228 | } 229 | workflow.add_conditional_edges("supervisor", lambda x: x["next"], conditional_map) 230 | workflow.add_edge(START, "supervisor") 231 | 232 | logger.debug("build_async_workflow: Workflow build complete") 233 | return workflow 234 | -------------------------------------------------------------------------------- /common/requirements.txt: -------------------------------------------------------------------------------- 1 | openai 2 | langchain 3 | langchain-openai 4 | langchain-community 5 | langchain-experimental 6 | langgraph 7 | botbuilder-integration-aiohttp 8 | tiktoken 9 | pypdf 10 | sqlalchemy 11 | pyodbc 12 | tabulate 13 | azure-cosmos 14 | streamlit 15 | audio-recorder-streamlit 16 | python-dotenv 17 | azure-ai-formrecognizer 18 | azure-storage-blob 19 | beautifulsoup4 20 | pydantic 21 | azure-cognitiveservices-speech 22 | fastapi 23 | sse_starlette 24 | sseclient-py 25 | uvicorn 26 | gunicorn 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /credentials.env: -------------------------------------------------------------------------------- 1 | # Don't mess with this unless you really know what you are doing 2 | AZURE_SEARCH_API_VERSION="2024-11-01-preview" 3 | AZURE_OPENAI_API_VERSION="2024-10-01-preview" 4 | BING_SEARCH_URL="https://api.bing.microsoft.com/v7.0/search" 5 | BOT_DIRECT_CHANNEL_ENDPOINT="https://directline.botframework.com/v3/directline" 6 | 7 | 8 | # Edit with your own azure services values 9 | BASE_CONTAINER_URL="ENTER YOUR VALUE HERE" # Example: https://.blob.core.windows.net/ 10 | BLOB_CONNECTION_STRING="ENTER YOUR VALUE HERE" 11 | BLOB_SAS_TOKEN="ENTER YOUR VALUE HERE" # Make sure that you put a ? at the beginning 12 | AZURE_SEARCH_ENDPOINT="ENTER YOUR VALUE HERE" 13 | AZURE_SEARCH_KEY="ENTER YOUR VALUE HERE" # Make sure is the MANAGEMENT KEY, not the query key 14 | COG_SERVICES_NAME="ENTER YOUR VALUE HERE" 15 | COG_SERVICES_KEY="ENTER YOUR VALUE HERE" 16 | FORM_RECOGNIZER_ENDPOINT="ENTER YOUR VALUE HERE" # Azure Document Intelligence API (former Form Recognizer) 17 | FORM_RECOGNIZER_KEY="ENTER YOUR VALUE HERE" 18 | AZURE_OPENAI_ENDPOINT="ENTER YOUR VALUE HERE" 19 | AZURE_OPENAI_API_KEY="ENTER YOUR VALUE HERE" 20 | GPT4o_DEPLOYMENT_NAME="ENTER YOUR VALUE HERE" 21 | GPT4oMINI_DEPLOYMENT_NAME="ENTER YOUR VALUE HERE" 22 | EMBEDDING_DEPLOYMENT_NAME="ENTER YOUR VALUE HERE" 23 | BING_SUBSCRIPTION_KEY="ENTER YOUR VALUE HERE" 24 | SQL_SERVER_NAME="ENTER YOUR VALUE HERE" # For Azure SQL, make sure it includes .database.windows.net at the end 25 | SQL_SERVER_DATABASE="ENTER YOUR VALUE HERE" 26 | SQL_SERVER_USERNAME="ENTER YOUR VALUE HERE" 27 | SQL_SERVER_PASSWORD="ENTER YOUR VALUE HERE" 28 | AZURE_COSMOSDB_ENDPOINT="ENTER YOUR VALUE HERE" 29 | AZURE_COSMOSDB_NAME="ENTER YOUR VALUE HERE" 30 | AZURE_COSMOSDB_CONTAINER_NAME="ENTER YOUR VALUE HERE" 31 | AZURE_COSMOSDB_KEY="ENTER YOUR VALUE HERE" 32 | 33 | # Voice env variables 34 | SPEECH_ENGINE="openai" 35 | AZURE_OPENAI_WHISPER_MODEL_NAME="ENTER YOUR VALUE HERE" # Normally "whisper" 36 | AZURE_OPENAI_TTS_VOICE_NAME="nova" 37 | AZURE_OPENAI_TTS_MODEL_NAME="ENTER YOUR VALUE HERE" # Normally "tts" or "tts-hd" 38 | AZURE_SPEECH_KEY="ENTER YOUR VALUE HERE" 39 | AZURE_SPEECH_REGION="ENTER YOUR VALUE HERE" 40 | AZURE_SPEECH_VOICE_NAME="en-US-AndrewMultilingualNeural" 41 | 42 | # These ENV Variables you will have to set it AFTER you deploy the Backend Infra (Notebook 13 Instructions) 43 | BOT_ID="ENTER YOUR VALUE HERE" # This is the name of your bot service created in Notebook 13 44 | BOT_SERVICE_DIRECT_LINE_SECRET="ENTER YOUR VALUE HERE" # Find this in Azure Bot Service -> Channels -> Direct Line -> Default Site 45 | 46 | -------------------------------------------------------------------------------- /data/books.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/data/books.zip -------------------------------------------------------------------------------- /data/cord19mini.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/data/cord19mini.zip -------------------------------------------------------------------------------- /data/friends_transcripts.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/data/friends_transcripts.zip -------------------------------------------------------------------------------- /download_odbc_driver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! [[ "18.04 20.04 22.04" == *"$(lsb_release -rs)"* ]]; 4 | then 5 | echo "Ubuntu $(lsb_release -rs) is not currently supported."; 6 | exit; 7 | fi 8 | 9 | echo "Downloading..." 10 | curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 11 | curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list > /etc/apt/sources.list.d/mssql-release.list 12 | 13 | echo "Updating apt-get.." 14 | apt-get update 15 | 16 | echo "Installing msodbcsql18.." 17 | ACCEPT_EULA=Y apt-get install -y msodbcsql17 18 | # optional: for bcp and sqlcmd 19 | ACCEPT_EULA=Y apt-get install -y mssql-tools 20 | echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc 21 | source ~/.bashrc 22 | # optional: for unixODBC development headers 23 | apt-get install -y unixodbc-dev 24 | echo "DONE" -------------------------------------------------------------------------------- /download_odbc_driver_dev_container.sh: -------------------------------------------------------------------------------- 1 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc 2 | 3 | #Debian 11 4 | curl https://packages.microsoft.com/config/debian/11/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list 5 | 6 | sudo apt-get update 7 | sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 8 | # optional: for bcp and sqlcmd 9 | sudo ACCEPT_EULA=Y apt-get install -y mssql-tools 10 | echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc 11 | source ~/.bashrc 12 | # optional: for unixODBC development headers 13 | sudo apt-get install -y unixodbc-dev 14 | # optional: kerberos library for debian-slim distributions 15 | sudo apt-get install -y libgssapi-krb5-2 -------------------------------------------------------------------------------- /images/Bot-Framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/Bot-Framework.png -------------------------------------------------------------------------------- /images/Cog-Search-Enrich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/Cog-Search-Enrich.png -------------------------------------------------------------------------------- /images/GPT-Smart-Search-Architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/GPT-Smart-Search-Architecture.jpg -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/architecture.png -------------------------------------------------------------------------------- /images/cosmos-chathistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/cosmos-chathistory.png -------------------------------------------------------------------------------- /images/error-authorize-github.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/error-authorize-github.jpeg -------------------------------------------------------------------------------- /images/github-actions-pipeline-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/github-actions-pipeline-success.png -------------------------------------------------------------------------------- /images/memory_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pablomarin/GPT-Azure-Search-Engine/5eca1b02f50d98d87db83d370f13c6de6641fef6/images/memory_diagram.png -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [Setup Local environment](#setup-local-environment) 4 | - [Installing PowerShell](#installing-powershell) 5 | - [Installing Azure Developer CLI (azd)](#installing-azure-developer-cli) 6 | - [Provision infrastructure](#provision-infrastructure) 7 | - [Deploying from scratch](#deploying-from-scratch) 8 | - [Deploying with existing Azure resources](#deploying-with-existing-azure-resources) 9 | - [Troubleshooting](#troubleshooting) 10 | 11 | ### Define environment variables for running services 12 | 13 | 1. Modify or add environment variables to configure the running application. Environment variables can be configured by updating the `settings` node(s) for each service in [main.parameters.json](./infra/main.parameters.json). 14 | 2. For services using a database, environment variables have been pre-configured under the `env` node in the following files to allow connection to the database. Modify the name of these variables as needed to match your application. 15 | - [app/common.bicep](./infra/app/common.bicep) 16 | 3. For services using Redis, environment variables will not show up under `env` explicitly, but are available as: `REDIS_ENDPOINT`, `REDIS_HOST`, `REDIS_PASSWORD`, and `REDIS_PORT`. 17 | 18 | ### Setup Local environment 19 | 20 | First install the required tools: 21 | 22 | - [Azure Developer CLI](https://aka.ms/azure-dev/install) 23 | - [Python 3.9, 3.10, or 3.11](https://www.python.org/downloads/) 24 | - **Important**: Python and the pip package manager must be in the path in Windows for the setup scripts to work. 25 | - **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`. 26 | - [Git](https://git-scm.com/downloads) 27 | - [Powershell 7+ (pwsh)](https://github.com/powershell/powershell) - For Windows users only. 28 | - **Important**: Ensure you can run `pwsh.exe` from a PowerShell terminal. If this fails, you likely need to upgrade PowerShell. 29 | 30 | #### Installing PowerShell 31 | 32 | PowerShell is a cross-platform task automation solution consisting of a command-line shell, a scripting language, and a configuration management framework. PowerShell runs on Windows, Linux, and macOS. 33 | 34 | ##### Windows 35 | 36 | - PowerShell comes pre-installed on Windows 10 and later. 37 | - To update or install the latest version, visit [Microsoft's PowerShell GitHub page](https://github.com/PowerShell/PowerShell). 38 | 39 | ##### Linux (PowerShell 7+) 40 | 41 | - For Ubuntu and other Linux distributions, follow the instructions in the [official guide](https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.3). 42 | 43 | ##### macOS (PowerShell 7+) 44 | 45 | - For macOS, use Homebrew to install PowerShell: 46 | 47 | ```bash 48 | brew install --cask powershell 49 | ``` 50 | 51 | #### Installing Azure Developer CLI 52 | 53 | The Azure Developer CLI (azd) is a command-line tool for building, deploying, and managing Azure resources in a repeatable and predictable manner. 54 | 55 | ##### Windows OS 56 | 57 | - Install using winget: 58 | 59 | ```bash 60 | winget install AzureDeveloperCLI 61 | ``` 62 | 63 | ##### macOS 64 | 65 | - Install using Homebrew: 66 | 67 | ```bash 68 | brew install azure-developer-cli 69 | ``` 70 | 71 | ##### Linux 72 | 73 | - Install using the script method: 74 | 75 | ```bash 76 | 77 | curl -fsSL https://aka.ms/install-azd.sh | bash 78 | ``` 79 | 80 | - For more details, refer to the [installation guide](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd?tabs=winget-windows%2Cbrew-mac%2Cscript-linux&pivots=os-linux). 81 | 82 | ### Provision infrastructure 83 | 84 | 1. Run `azd auth login` to conect to your azure tenant 85 | 86 | 2. Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! 87 | 88 | To troubleshoot any issues, see [troubleshooting](#troubleshooting). 89 | 90 | ### Deploying from scratch 91 | 92 | Execute the following command, if you don't have any pre-existing Azure services and want to start from a fresh deployment. 93 | 94 | 1. Open the terminal. 95 | 96 | 2. Run `azd auth login` to conect to your azure tenant 97 | 98 | * note : if your using Azure Machine Learning Compute to run the deployement you need to use `azd auth login --use-device-code` and follow the instruction to connect to Azure. 99 | - **Important**: for Microsoft FTE using fdpo tenant and Azure Machine Learning Studio Compute. This will not work as fdpo policies don't allow --use-device-code option. 100 | 101 | 3. Run `azd up` - This will provision Azure resources and deploy this sample to those resources. 102 | - **Important**: Beware that the resources created by this command will incur immediate costs, primarily from the Cognitive Search resource. These resources may accrue costs even if you interrupt the command before it is fully executed. You can run `azd down` or delete the resources manually to avoid unnecessary spending. 103 | - You will be prompted to select two locations, one for the majority of resources and one for the OpenAI resource, which is currently a short list. That location list is based on the [OpenAI model availability table](https://learn.microsoft.com/azure/cognitive-services/openai/concepts/models#model-summary-table-and-region-availability) and may become outdated as availability changes. 104 | 105 | 4. After the application has been successfully deployed you will see a backend and frontend URL printed to the console. Click that frontend URL to interact with the application in your browser. 106 | 107 | > NOTE: It may take 5 minutes for the application to be fully deployed. If you see a "Python Developer" welcome screen or an error page, then wait a bit and refresh the page. 108 | 109 | ### Deploying with existing Azure resources 110 | 111 | If you already have existing Azure resources, you can re-use those by setting `azd` environment values. 112 | 113 | #### Existing resource group 114 | 115 | 1. Run `azd env set AZURE_RESOURCE_GROUP {Name of existing resource group}` 116 | 1. Run `azd env set AZURE_LOCATION {Location of existing resource group}` 117 | 118 | #### Existing Azure OpenAI resource 119 | 120 | 1. Run `azd env set AZURE_OPENAI_SERVICE {Name of existing OpenAI service}` 121 | 1. Run `azd env set AZURE_OPENAI_RESOURCE_GROUP {Name of existing resource group that OpenAI service is provisioned to}` 122 | 1. Run `azd env set AZURE_OPENAI_CHATGPT_DEPLOYMENT {Name of existing ChatGPT deployment}`. Only needed if your ChatGPT deployment is not the default model name. 123 | 1. Run `azd env set AZURE_OPENAI_EMB_DEPLOYMENT {Name of existing GPT embedding deployment}`. Only needed if your embeddings deployment is not the default model name. 124 | 125 | When you run `azd up` after and are prompted to select a value for `openAiResourceGroupLocation`, make sure to select the same location as the existing OpenAI resource group. 126 | 127 | ## Troubleshooting 128 | 129 | Q: I visited the service endpoint listed, and I'm seeing a blank or error page. 130 | 131 | A: Your service may have failed to start or misconfigured. To investigate further: 132 | 133 | 1. Click on the resource group link shown to visit Azure Portal. 134 | 2. Navigate to the specific Azure Container App resource for the service. 135 | 3. Select _Monitoring -> Log stream_ under the navigation pane. 136 | 4. Observe the log output to identify any errors. 137 | 5. If logs are written to disk, examine the local logs or debug the application by using the _Console_ to connect to a shell within the running container. 138 | 139 | For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). 140 | -------------------------------------------------------------------------------- /infra/core/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cognitive Services instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | @description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') 6 | param customSubDomainName string = name 7 | param deployments array = [] 8 | param kind string = 'OpenAI' 9 | param publicNetworkAccess string = 'Enabled' 10 | param sku object = { 11 | name: 'S0' 12 | } 13 | 14 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 15 | name: name 16 | location: location 17 | tags: tags 18 | kind: kind 19 | properties: { 20 | customSubDomainName: customSubDomainName 21 | publicNetworkAccess: publicNetworkAccess 22 | } 23 | sku: sku 24 | } 25 | 26 | @batchSize(1) 27 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 28 | parent: account 29 | name: deployment.name 30 | properties: { 31 | model: deployment.model 32 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 33 | } 34 | sku: contains(deployment, 'sku') ? deployment.sku : { 35 | name: 'Standard' 36 | capacity: 20 37 | } 38 | }] 39 | 40 | output endpoint string = account.properties.endpoint 41 | output id string = account.id 42 | output name string = account.name 43 | var keys = account.listKeys() 44 | output key string = keys.key1 45 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | // The main bicep module to provision Azure resources. 4 | // For a more complete walkthrough to understand how this file works with azd, 5 | // see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create 6 | 7 | @minLength(1) 8 | @maxLength(64) 9 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 10 | param environmentName string 11 | 12 | @minLength(1) 13 | @description('Primary location for all resources') 14 | @metadata({ 15 | azd: { 16 | type: 'location' 17 | } 18 | }) 19 | param location string 20 | 21 | param resourceGroupName string = '' 22 | 23 | @description('Optional, defaults to S3. The SKU of the App Service Plan. Acceptable values are B3, S3 and P2v3.') 24 | @allowed([ 25 | 'B3' 26 | 'S3' 27 | 'P2v3' 28 | ]) 29 | param HostingPlanSku string = 'P2v3' 30 | 31 | @description('The location of the OpenAI resource group.') 32 | @allowed([ 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'switzerlandnorth', 'uksouth', 'japaneast' ]) 33 | @metadata({ 34 | azd: { 35 | type: 'location' 36 | } 37 | }) 38 | param openAiResourceGroupLocation string 39 | param openAiServiceName string // Set in main.parameters.json 40 | param openAiResourceGroupName string 41 | 42 | param openAiSkuName string = 'S0' 43 | 44 | @description('Optional. The API version of the Azure OpenAI.') 45 | param azureOpenAIAPIVersion string = '2023-05-15' 46 | 47 | param chatGptDeploymentName string = '' // Set in main.parameters.json 48 | param chatGptDeploymentCapacity int = 20 49 | param chatGptModelName string = (openAiHost == 'azure') ? 'gpt-4-32k' : 'gpt-3.5-turbo' 50 | param chatGptModelVersion string = '0613' 51 | param embeddingDeploymentName string = '' // Set in main.parameters.json 52 | param embeddingDeploymentCapacity int = 30 53 | param embeddingModelName string = 'text-embedding-ada-002' 54 | 55 | @allowed([ 'azure', 'openai' ]) 56 | param openAiHost string // Set in main.parameters.json 57 | 58 | // tags that should be applied to all resources. 59 | var tags = { 60 | // Tag all resources with the environment name. 61 | 'azd-env-name': environmentName 62 | } 63 | 64 | @description('Required. Active Directory App ID.') 65 | param appId string 66 | 67 | @description('Required. Active Directory App Secret Value.') 68 | @secure() 69 | param appSecret string 70 | 71 | // infrastructure resources 72 | @description('Required. The administrator username of the SQL logical server.') 73 | param SQLAdministratorLogin string 74 | 75 | @description('Required. The administrator password of the SQL logical server.') 76 | @secure() 77 | param SQLAdministratorLoginPassword string 78 | 79 | // Generate a unique token to be used in naming resources. 80 | // Remove linter suppression after using. 81 | #disable-next-line no-unused-vars 82 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 83 | 84 | // Organize resources in a resource group 85 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 86 | name: !empty(resourceGroupName) ? resourceGroupName : 'rg-${environmentName}' 87 | location: location 88 | tags: tags 89 | } 90 | 91 | resource openAiResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = if (!empty(openAiResourceGroupName)) { 92 | name: !empty(openAiResourceGroupName) ? openAiResourceGroupName : rg.name 93 | } 94 | 95 | // create opan ai account if openAiHost is azure 96 | module openAi 'core/ai/cognitiveservices.bicep' = if (openAiHost == 'azure') { 97 | name: 'openai' 98 | scope: openAiResourceGroup 99 | params: { 100 | name: !empty(openAiServiceName) ? openAiServiceName : 'openai-${resourceToken}' 101 | location: openAiResourceGroupLocation 102 | tags: tags 103 | sku: { 104 | name: openAiSkuName 105 | } 106 | deployments: [ 107 | { 108 | name: !empty(chatGptDeploymentName) ? chatGptDeploymentName : chatGptModelName 109 | model: { 110 | format: 'OpenAI' 111 | name: chatGptModelName 112 | version: chatGptModelVersion 113 | } 114 | sku: { 115 | name: 'Standard' 116 | capacity: chatGptDeploymentCapacity 117 | } 118 | } 119 | { 120 | name: !empty(embeddingDeploymentName) ? embeddingDeploymentName : embeddingModelName 121 | model: { 122 | format: 'OpenAI' 123 | name: embeddingModelName 124 | version: '2' 125 | } 126 | capacity: embeddingDeploymentCapacity 127 | } 128 | ] 129 | } 130 | } 131 | 132 | // Add resources to be provisioned below. 133 | // A full example that leverages azd bicep modules can be seen in the todo-python-mongo template: 134 | // https://github.com/Azure-Samples/todo-python-mongo/tree/main/infra 135 | module infra '../azuredeploy.bicep' = { 136 | name: 'infra' 137 | scope: rg 138 | params: { 139 | location: location 140 | SQLAdministratorLogin: SQLAdministratorLogin 141 | SQLAdministratorLoginPassword: SQLAdministratorLoginPassword 142 | } 143 | dependsOn: [ 144 | openAi 145 | ] 146 | } 147 | 148 | module backend '../apps/backend/azuredeploy-backend.bicep' = { 149 | name: 'backend' 150 | scope: rg 151 | params: { 152 | location: location 153 | appId: appId 154 | appPassword: appSecret 155 | azureOpenAIAPIKey: openAi.outputs.key 156 | azureOpenAIName: openAi.outputs.name 157 | azureSearchName: infra.outputs.azureSearchName 158 | bingSearchName: infra.outputs.bingSearchAPIName 159 | blobSASToken: '' // TODO: add blobSASToken 160 | cosmosDBAccountName: infra.outputs.cosmosDBAccountName 161 | cosmosDBContainerName: infra.outputs.cosmosDBContainerName 162 | SQLServerName: infra.outputs.SQLServerName 163 | SQLServerDatabase: infra.outputs.SQLDatabaseName 164 | SQLServerUsername: SQLAdministratorLogin 165 | SQLServerPassword: SQLAdministratorLoginPassword 166 | 167 | } 168 | dependsOn: [ 169 | infra 170 | openAi 171 | ] 172 | } 173 | 174 | module frontend '../apps/frontend/azuredeploy-frontend.bicep' = { 175 | name: 'frontend' 176 | scope: rg 177 | params: { 178 | appServicePlanSKU: HostingPlanSku 179 | location: location 180 | azureOpenAIName: openAi.outputs.name 181 | azureOpenAIAPIKey: openAi.outputs.key 182 | azureOpenAIModelName: chatGptModelName 183 | azureOpenAIAPIVersion: azureOpenAIAPIVersion 184 | azureSearchName: infra.outputs.azureSearchName 185 | blobSASToken: '' // TODO: to be added in post deployment 186 | botDirectLineChannelKey: '' // TODO: to be added in post deployment 187 | botServiceName: backend.outputs.botServiceName 188 | } 189 | dependsOn: [ 190 | openAi 191 | infra 192 | backend 193 | ] 194 | } 195 | 196 | // Outputs are automatically saved in the local azd environment .env file. 197 | // To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. 198 | #disable-next-line outputs-should-not-contain-secrets 199 | output AZURE_APP_SECRET string = appSecret 200 | output AZURE_APP_ID string = appId 201 | output AZURE_LOCATION string = location 202 | output AZURE_TENANT_ID string = tenant().tenantId 203 | output AZURE_SUBSCRIPTION_ID string = subscription().subscriptionId 204 | output AZURE_RESOURCE_GROUP string = rg.name 205 | output AZURE_OPENAI_SERVICE string = openAi.outputs.name 206 | output AZURE_OPENAI_RESOURCE_GROUP string = openAiResourceGroup.name 207 | output AZURE_OPENAI_HOST string = openAiHost 208 | output AZURE_OPENAI_MODEL_NAME string = !empty(chatGptDeploymentName) ? chatGptDeploymentName : chatGptModelName 209 | output AZURE_OPENAI_EMB_DEPLOYMENT string = !empty(embeddingDeploymentName) ? embeddingDeploymentName : embeddingModelName 210 | output AZURE_OPENAI_API_VERSION string = azureOpenAIAPIVersion 211 | 212 | output AZURE_BOT_SERVICE string = backend.outputs.botServiceName 213 | output AZURE_FRONTEND_WEBAPP_NAME string = frontend.outputs.webAppName 214 | output AZURE_BACKEND_WEBAPP_NAME string = backend.outputs.webAppName 215 | output AZURE_BLOB_STORAGE_ACCOUNT_NAME string = infra.outputs.blobStorageAccountName 216 | output AZURE_BACKEND_WEBAPP_URL string = backend.outputs.webAppUrl 217 | 218 | 219 | output BLOB_CONNECTION_STRING string = infra.outputs.blobConnectionString 220 | output BLOB_SAS_TOKEN string = '' // TODO: add blobSASToken 221 | 222 | // Key Variables to put in your Crenetials file 223 | output AZURE_SEARCH_ENDPOINT string = infra.outputs.azureSearchEndpoint 224 | output AZURE_SEARCH_KEY string = infra.outputs.azureSearchKey 225 | output COG_SERVICES_NAME string = infra.outputs.cognitiveServiceName 226 | output COG_SERVICES_KEY string = infra.outputs.cognitiveServiceKey 227 | output FORM_RECOGNIZER_ENDPOINT string = infra.outputs.formrecognizerEndpoint 228 | output FORM_RECOGNIZER_KEY string = infra.outputs.formRecognizerKey 229 | output AZURE_OPENAI_ENDPOINT string = 'https://${openAi.outputs.name}.openai.azure.com/' 230 | output AZURE_OPENAI_API_KEY string = openAi.outputs.key 231 | output BING_SUBSCRIPTION_KEY string = infra.outputs.bingServiceSearchKey 232 | output SQL_SERVER_NAME string = infra.outputs.SQLServerName 233 | output SQL_SERVER_DATABASE string = infra.outputs.SQLDatabaseName 234 | output SQL_SERVER_USERNAME string = SQLAdministratorLogin 235 | #disable-next-line outputs-should-not-contain-secrets 236 | output SQL_SERVER_PASSWORD string = SQLAdministratorLoginPassword 237 | output AZURE_COSMOSDB_ENDPOINT string = infra.outputs.cosmosDBAccountEndpoint 238 | output AZURE_COSMOSDB_NAME string = infra.outputs.cosmosDBAccountName 239 | output AZURE_COSMOSDB_CONTAINER_NAME string = infra.outputs.cosmosDBContainerName 240 | output AZURE_COMOSDB_CONNECTION_STRING string = infra.outputs.cosmosDBConnectionString 241 | -------------------------------------------------------------------------------- /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}" 10 | }, 11 | "resourceGroupName": { 12 | "value": "${AZURE_RESOURCE_GROUP}" 13 | }, 14 | "openAiResourceGroupLocation": { 15 | "value": "${AZURE_OPENAI_LOCATION}" 16 | }, 17 | "openAiServiceName": { 18 | "value": "${AZURE_OPENAI_SERVICE}" 19 | }, 20 | "openAiResourceGroupName": { 21 | "value": "${AZURE_OPENAI_RESOURCE_GROUP}" 22 | }, 23 | "openAiHost": { 24 | "value": "${OPENAI_HOST=azure}" 25 | }, 26 | "appId": { 27 | "value": "${AZURE_APP_ID}" 28 | }, 29 | "appSecret": { 30 | "value": "${AZURE_APP_SECRET}" 31 | }, 32 | "blobSASToken": { 33 | "value": "${AZURE_BLOB_SAS_TOKEN}" 34 | }, 35 | "chatGptDeploymentName": { 36 | "value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT}" 37 | }, 38 | "embeddingDeploymentName": { 39 | "value": "${AZURE_OPENAI_EMB_DEPLOYMENT}" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /infra/scripts/CreateAppRegistration.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | & $PSScriptRoot\loadenv.ps1 3 | 4 | $appName = $env:AZURE_ENV_NAME + '-app' 5 | $appURI = "http://$env:AZURE_ENV_NAME.fdpo.onmicrosoft.com" 6 | $appHomePageUrl = $appURI 7 | 8 | $myApp = az ad app list --filter "displayName eq '$appName'" --query "[0]" -o json | ConvertFrom-Json 9 | if ($null -eq $myApp) { 10 | $myApp = az ad app create --display-name $appName --query "{displayName: displayName, appId: appId}" -o json | ConvertFrom-Json 11 | } 12 | else { 13 | Write-Host "Application already exists" 14 | } 15 | 16 | $credential = az ad app credential reset --id $myApp.appId --display-name 'MSAPP_SECRET' --append --query "password" -o tsv 17 | 18 | 19 | azd env set AZURE_APP_ID $myApp.appId 20 | azd env set AZURE_APP_SECRET $credential 21 | Write-Host "Application ID: $($myApp.appId) and Secret Saved in azd environment variables" -ForegroundColor Green 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /infra/scripts/CreatePrerequisites.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | & $PSScriptRoot\loadenv.ps1 4 | 5 | 6 | New-Item -ItemType "directory" -Path ".\infra\target" -ErrorAction SilentlyContinue 7 | New-Item -ItemType "directory" -Path ".\infra\target\frontend" -ErrorAction SilentlyContinue 8 | New-Item -ItemType "directory" -Path ".\infra\target\backend" -ErrorAction SilentlyContinue 9 | -------------------------------------------------------------------------------- /infra/scripts/UpdateSecretsInApps.ps1: -------------------------------------------------------------------------------- 1 | 2 | $ErrorActionPreference = "Stop" 3 | 4 | & $PSScriptRoot\loadenv.ps1 5 | 6 | $jsonObject = az bot directline show --name $env:AZURE_BOT_SERVICE --resource-group $env:AZURE_RESOURCE_GROUP --with-secrets true | ConvertFrom-Json 7 | $defaultSite = $jsonObject.properties.properties.sites | Where-Object { $_.siteName -eq "Default Site" } 8 | $defaultSiteKey = $defaultSite.key.ToString() 9 | 10 | # Generate SAS for BlobStorage to access the container 11 | $expiry=(Get-Date).AddDays(7).ToString('yyyy-MM-ddTHH:mm:ssZ') 12 | $SAS_TOKEN=az storage container generate-sas --account-name $env:AZURE_BLOB_STORAGE_ACCOUNT_NAME --name 'cord19' --permissions dlrw --expiry $expiry --auth-mode login --as-user 13 | azd env set BLOB_SAS_TOKEN $SAS_TOKEN 14 | 15 | # Write value to Azure App Service configuration 16 | write-host "Update Frontend APP configuration" -ForegroundColor Gray 17 | az webapp config appsettings set --name $env:AZURE_FRONTEND_WEBAPP_NAME --resource-group $env:AZURE_RESOURCE_GROUP --settings BOT_DIRECTLINE_SECRET_KEY=$defaultSiteKey BLOB_SAS_TOKEN=$SAS_TOKEN 18 | # Write value to Azure App Service configuration 19 | write-host "Update Backend APP configuration" -ForegroundColor Gray 20 | az webapp config appsettings set --name $env:AZURE_BACKEND_WEBAPP_NAME --resource-group $env:AZURE_RESOURCE_GROUP --settings BLOB_SAS_TOKEN=$SAS_TOKEN 21 | 22 | Write-host "Generate Service Principal for Github actin deployment" -ForegroundColor Gray 23 | az ad sp create-for-rbac --name $env:AZURE_ENV_NAME --role contributor --scopes "/subscriptions/$env:AZURE_SUBSCRIPTION_ID/resourceGroups/$env:AZURE_RESOURCE_GROUP" 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /infra/scripts/loadenv.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Loading azd .env file from current environment" 2 | foreach ($line in (& azd env get-values)) { 3 | if ($line -match "([^=]+)=(.*)") { 4 | $key = $matches[1] 5 | $value = $matches[2] -replace '^"|"$' 6 | [Environment]::SetEnvironmentVariable($key, $value) 7 | } 8 | } 9 | 10 | $pythonCmd = Get-Command python -ErrorAction SilentlyContinue 11 | if (-not $pythonCmd) { 12 | # fallback to python3 if python not found 13 | $pythonCmd = Get-Command python3 -ErrorAction SilentlyContinue 14 | } 15 | 16 | # Write-Host 'Creating python virtual environment "scripts/.venv"' 17 | # Start-Process -FilePath ($pythonCmd).Source -ArgumentList "-m venv ./scripts/.venv" -Wait -NoNewWindow 18 | 19 | # $venvPythonPath = "./scripts/.venv/scripts/python.exe" 20 | # if (Test-Path -Path "/usr") { 21 | # # fallback to Linux venv path 22 | # $venvPythonPath = "./scripts/.venv/bin/python" 23 | # } 24 | 25 | # Write-Host 'Installing dependencies from "requirements.txt" into virtual environment' 26 | # Start-Process -FilePath $venvPythonPath -ArgumentList "-m pip install -r scripts/requirements.txt" -Wait -NoNewWindow 27 | --------------------------------------------------------------------------------