├── .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 | ""
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 | 
2 |
3 | # AI Multi-Agent Architecture 3 or 5 days POC: Build Intelligent Agents with Azure Services
4 |
5 | [](https://codespaces.new/MSUSAzureAccelerators/Azure-Cognitive-Search-Azure-OpenAI-Accelerator?quickstart=1)
6 | [](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 | 
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 | [](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 | [](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 | [](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