├── .devcontainer └── devcontainer.json ├── .env_sample.txt ├── .github └── workflows │ ├── azure-dev.yml │ └── python.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── entrypoint.sh ├── fastapi_app │ ├── __init__.py │ ├── functions.py │ ├── main.py │ ├── models.py │ └── routes.py ├── pyproject.toml ├── requirements.txt └── setup_sql_database_role.py ├── azure.yaml ├── docs ├── limitations.md ├── screenshot_chat.png └── screenshot_chat_1.png ├── infra ├── core │ ├── ai │ │ └── openai.bicep │ ├── database │ │ └── sql-database.bicep │ ├── host │ │ ├── applicationinsights.bicep │ │ ├── appservice.bicep │ │ └── appserviceplan.bicep │ └── identity │ │ ├── roleAssignments.bicep │ │ └── user-mi.bicep ├── main.bicep └── main.parameters.json ├── pyproject.toml ├── requirements-dev.txt ├── scripts ├── create-sql-user.ps1 ├── create-sql-user.sh ├── fetch-principal-info.ps1 ├── fetch-principal-info.sh ├── load_python_env.sh ├── requirements.txt └── start.txt └── tests ├── test_functions,py ├── test_main.py └── test_routes.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "function-call-dynamic-query-demo", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.12-bullseye", 4 | "forwardPorts": [50505], 5 | "features": { 6 | "ghcr.io/azure/azure-dev/azd:latest": {}, 7 | "ghcr.io/devcontainers/features/azure-cli:latest": {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "ms-azuretools.azure-dev", 13 | "ms-azuretools.vscode-bicep", 14 | "ms-python.python", 15 | "GitHub.vscode-github-actions" 16 | ] 17 | } 18 | }, 19 | "postCreateCommand": "python3 -m pip install -r requirements-dev.txt && python3 -m pip install -e app", 20 | "remoteUser": "vscode", 21 | "hostRequirements": { 22 | "memory": "8gb" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.env_sample.txt: -------------------------------------------------------------------------------- 1 | SQL_SERVER= 2 | SQL_DATABASE= 3 | SQL_USERNAME= 4 | SQL_PASSWORD= 5 | AZURE_SQL_CONNECTIONSTRING= 'Driver={ODBC Driver 18 for SQL Server};Server=tcp:exampleserver.database.windows.net,1433;Database=exampledatabase;Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30' 6 | 7 | ######OpenAI Keys########## 8 | OPENAI_EMBED_HOST=azure 9 | DEPLOY_AZURE_OPENAI=true 10 | AZURE_OPENAI_ENDPOINT= 11 | AZURE_OPENAI_API_KEY= 12 | AZURE_OPENAI_VERSION = 13 | AZURE_OPENAI_CHAT_DEPLOYMENT= 14 | 15 | #######Managed Identity#3##### 16 | AZURE_CLIENT_ID = 17 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | name: Azure Deployment 2 | 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | # Run when commits are pushed to mainline branch (main or master) 8 | # Set this to the mainline branch you are using 9 | branches: 10 | -dev-testing 11 | 12 | # GitHub Actions workflow to deploy to Azure using azd 13 | # To configure required secrets for connecting to Azure, simply run `azd pipeline config` 14 | 15 | # Set up permissions for deploying with secretless Azure federated credentials 16 | # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication 17 | permissions: 18 | id-token: write 19 | contents: read 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | env: 25 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 26 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 27 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 28 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 29 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Install azd 35 | uses: Azure/setup-azd@v2.0.0 36 | 37 | - name: Log in with Azure (Federated Credentials) 38 | if: ${{ env.AZURE_CLIENT_ID != '' }} 39 | run: | 40 | azd auth login ` 41 | --client-id "$Env:AZURE_CLIENT_ID" ` 42 | --federated-credential-provider "github" ` 43 | --tenant-id "$Env:AZURE_TENANT_ID" 44 | shell: pwsh 45 | 46 | - name: Log in with Azure (Client Credentials) 47 | if: ${{ env.AZURE_CREDENTIALS != '' }} 48 | run: | 49 | $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; 50 | Write-Host "::add-mask::$($info.clientSecret)" 51 | 52 | azd auth login ` 53 | --client-id "$($info.clientId)" ` 54 | --client-secret "$($info.clientSecret)" ` 55 | --tenant-id "$($info.tenantId)" 56 | shell: pwsh 57 | env: 58 | AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} 59 | 60 | 61 | - name: Create azd env 62 | run: azd env new $AZURE_ENV_NAME 63 | env: 64 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 65 | 66 | - name: Provision Infrastructure 67 | run: azd provision --no-prompt 68 | env: 69 | AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} 70 | 71 | - name: Deploy Application 72 | run: azd deploy --no-prompt 73 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python checks 2 | 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | paths: 8 | - '**.py' 9 | 10 | pull_request: 11 | branches: [ main ] 12 | paths: 13 | - '**.py' 14 | 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | checks-format-and-lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python 3 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.12" 29 | cache: 'pip' 30 | - name: Install dependencies 31 | run: | 32 | python3 -m pip install --upgrade pip 33 | python3 -m pip install ruff 34 | - name: Lint with ruff 35 | run: ruff check . 36 | - name: Check formatting with ruff 37 | run: ruff format . --check 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | 165 | # testing code 166 | testing_code/ 167 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.1.0 10 | hooks: 11 | # Run the linter. 12 | - id: ruff 13 | args: [ --fix ] 14 | # Run the formatter. 15 | - id: ruff-format 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqltools.connections": [ 3 | { 4 | "mssqlOptions": { 5 | "appName": "SQLTools", 6 | "useUTC": true, 7 | "encrypt": true 8 | }, 9 | "previewLimit": 50, 10 | "server": "dwtrial.database.windows.net", 11 | "port": 1433, 12 | "driver": "MSSQL", 13 | "name": "sql_database", 14 | "database": "adventureworkstrial", 15 | "username": "zedan_10", 16 | "password": "Winter_123" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: (). 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/Azure-Samples/function-call-dynamic-query-demo/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text to Query for Azure SQL using OpenAI Function Call 2 | 3 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/rag-postgres-openai-python) 4 | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/rag-postgres-openai-python) 5 | 6 | This project creates a backend that can use OpenAI chat models that support the [function calling](https://platform.openai.com/docs/guides/function-calling) ability to answer questions about your Azure SQL Database. 7 | It does this by first identifying if the user query is asking about an internal data source (in this case, it is Azure SQL), if it does, then the application generates a SQL query from the users prompt, connects to the database via user assigned manage identity, executes that query, and relates it back to the user in JSON Format. The flow of this application can be seen using the below diagram. 8 | 9 | * [Features](#features) 10 | * [Getting started](#getting-started) 11 | * [GitHub Codespaces](#github-codespaces) 12 | * [VS Code Dev Containers](#vs-code-dev-containers) 13 | * [Local environment](#local-environment) 14 | * [Deployment](#deployment) 15 | * [Github Actions](#github-actions) 16 | * [Costs](#costs) 17 | * [Security guidelines](#security-guidelines) 18 | 19 | ![Diagramn of application flow](docs/screenshot_chat.png) 20 | 21 | ## Features 22 | 23 | This project is designed for deployment via the Azure Developer CLI, hosting the backend on Azure Web Apps, the database being Azure SQL, and the models that support function calling in Azure OpenAI. This demo leverges the ["AdventureWorks"](https://learn.microsoft.com/en-us/sql/samples/adventureworks-install-configure?view=sql-server-ver16&tabs=ssms) Sample Database. 24 | 25 | * Conversion of user queries into Azure SQL that can be executed 26 | * Generate results from your internal Azure SQL database based on user queries 27 | * Enforce only read queries to the database 28 | * Ask questions like "What are the top 3 products we have?", "What is the cost associated with product HL Road Frame - Black, 58?" , "How many red products do we have?" & more! 29 | 30 | ## Schema Detection & Understanding 31 | 32 | This project leverages GPT-4o to generate the SQL query for the database. The model has contextual understanding of the `SalesLT.Customer` & `SalesLT.Product` tables. This is done by injecting the schema information of these tables as part of the prompt. 33 | 34 | To have more understanding of the tables contents. Please login to your Azure SQL Database, and look through these tables. 35 | 36 | > [!NOTE] 37 | > Further developments of this repository will include automatic schema detections for accessible tables in the Azure SQL Database 38 | 39 | ## Getting Started 40 | 41 | ### GitHub Codespaces 42 | 43 | You can run this template virtually by using GitHub Codespaces. The button will open a web-based VS Code instance in your browser: 44 | 45 | 1. Open the template (this may take several minutes): 46 | 47 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/rag-postgres-openai-python) 48 | 49 | 2. Open a terminal window 50 | 3. Continue with the [deployment steps](#deployment) 51 | 52 | ### VS Code Dev Containers 53 | 54 | A related option is VS Code Dev Containers, which will open the project in your local VS Code using the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): 55 | 56 | 1. Start Docker Desktop (install it if not already installed) 57 | 2. Open the project: 58 | 59 | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Azure-Samples/function-call-dynamic-query-demo) 60 | 61 | 3. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. 62 | 4. Continue with the [deployment steps](#deployment) 63 | 64 | ### Local Environment 65 | 66 | 1. Make sure the following tools are installed: 67 | 68 | * [Azure Developer CLI (azd)](https://aka.ms/install-azd) 69 | * [Python 3.10+](https://www.python.org/downloads/) 70 | * [Git](https://git-scm.com/downloads) 71 | * [ODBC Driver 18](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver16) 72 | 73 | 2. Clone the repository to your local machine 74 | 75 | 3. Open the project folder 76 | 77 | 4. Create a Python virtual environment and activate it. 78 | 79 | 5. Install required Python packages and backend application: 80 | 81 | ```shell 82 | pip install -r requirements-dev.txt 83 | pip install -e app 84 | ``` 85 | 86 | 6. Continue with the [deployment steps](#deployment) below. 87 | 88 | ## Deployment 89 | 90 | Once you've opened the project, you can deploy it to Azure. 91 | 92 | 1. Sign in to your Azure account: 93 | 94 | ```shell 95 | azd auth login 96 | ``` 97 | 98 | If you have any issues with that command, you may also want to try `azd auth login --use-device-code`. 99 | 100 | 2. Create a new azd environment: 101 | 102 | ```shell 103 | azd env new 104 | ``` 105 | 106 | This will create a folder under `.azure/` in your project to store the configuration for this deployment. You may have multiple azd environments if desired. 107 | 108 | You will be asked to select the location of which the resource will be provisioned. You will have the option between 3 options due to model availability. 109 | 110 | 3. Configure your environment variables that will be used for deployment: 111 | 112 | > [!IMPORTANT] 113 | > This project code uses passwordless authentication with the Azure SQL server, but it doesn't currently turn off SQL password auth entirely, due to an issue with Bicep-based deployments. The username is set to a unique string, and the password is set to an auto-generated value. Once deployed, you can disable SQL password auth via the Azure portal. 114 | 115 | For the passwordless authentication to be properly set up, you must set the principal name of the external administrator (UPN). If you need help finding this value, please login with the Azure CLI, add an .env file to the root directory and run the script: [./scripts/fetch-principal-info.sh](./scripts/fetch-principal-info.sh) or [./scripts/fetch-principal-info.ps1](scripts/fetch-principal-info.ps1). The values should appear in the terminal and in the `.env`file in the root directory. 116 | 117 | Once you know your principal name, set it as an azd environment variable: 118 | 119 | ```shell 120 | azd env set AZURE_PRINCIPAL_NAME yourprincipalname 121 | ``` 122 | 123 | 4. Deploy the resources: 124 | 125 | ```shell 126 | azd up 127 | ``` 128 | 129 | > [!NOTE] 130 | > If you are running this project via Github Codespaces. You may encounter an error during the post provisioning step. 131 | > If this occurs, please run the following command `sudo apt --fix-broken install` 132 | 133 | ### Github Actions 134 | 135 | If you wish to deploy this project via Github Actions, you will find a working azure-dev.yaml file in the [.github\workflows](./.github/workflows/azure-dev.yml). 136 | More information on the limitations and workarounds for using this deployment method can be found in the [/docs](docs/limitations.md) 137 | 138 | ## Accessing the API documentation 139 | 140 | After all the resources have been provisioned and the deployment is complete, head to the endpoint the App Service created. 141 | You will be directed to a root entry point for the backend. 142 | 143 | ### Opening the API documentation 144 | 145 | To test the APIs, please add `docs` to the end of the url. 146 | 147 | > [!NOTE] 148 | > For example, if your endpoint is: `https://testing-function-call-demo-example-webapp.azurewebsites.net` 149 | > To test the endpoint, you must add `docs` at the end of this url, so the new url would be: 150 | > `https://testing-function-call-demo-example-webapp.azurewebsites.net/docs` 151 | 152 | ### Testing the APIs 153 | 154 | Use the Swagger UI to explore and test the available APIs. 155 | 156 | You will have the ability to test 2 APIs: 157 | 158 | 1) `execute_query` API which will take as input, a SQL command to execute on the Azure SQL Database. 159 | 2) `ask` API which will take a user message, convert it to a SQL Command using OpenAI, and execute the query against the database which will return the result in JSON format. 160 | 161 | ## Costs 162 | 163 | Pricing may vary per region and usage. Exact costs cannot be estimated. 164 | You may try the [Azure pricing calculator](https://azure.microsoft.com/pricing/calculator/) for the resources below: 165 | 166 | * Azure Web Apps: costs are based on the CPU, memory and storage resources you use. You can set the appServiceSkuName parameter in the main.parameters.json file to the sku of your choosing. Additional features like custom domains, SSL certificates and backups may incur additional charges.[Pricing](https://azure.microsoft.com/en-us/pricing/details/app-service/windows/) 167 | * Azure OpenAI: Standard tier, GPT and Ada models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/) 168 | * Azure SQL: This project leverage the “General Purpose - Serverless: Gen5, 1 vCore” sku with the adventureworks database. The cost depends on the compute costs and storage costs associated with the project. [Pricing](https://azure.microsoft.com/en-us/pricing/details/azure-sql-database/single/) 169 | * Azure Monitor: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/) 170 | 171 | ## Security guidelines 172 | 173 | This template uses [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for authenticating to the Azure services used (Azure OpenAI, Azure SQL Server). 174 | 175 | Additionally, we have added a [GitHub Action](https://github.com/microsoft/security-devops-action) that scans the infrastructure-as-code files and generates a report containing any detected issues. To ensure continued best practices in your own repository, we recommend that anyone creating solutions based on our templates ensure that the [Github secret scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning) setting is enabled. 176 | -------------------------------------------------------------------------------- /app/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | python3 -m pip install --upgrade pip 4 | python3 -m pip install -e . 5 | python3 -m gunicorn -k uvicorn.workers.UvicornWorker fastapi_app.main:app 6 | -------------------------------------------------------------------------------- /app/fastapi_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/function-call-dynamic-query-demo/321584c47919a583e178cf7b97474855fb820d68/app/fastapi_app/__init__.py -------------------------------------------------------------------------------- /app/fastapi_app/functions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pyodbc 4 | import struct 5 | import logging 6 | import threading 7 | from decimal import Decimal 8 | from uuid import UUID 9 | from datetime import datetime 10 | import base64 11 | from azure.identity import DefaultAzureCredential 12 | from dotenv import load_dotenv 13 | import sqlparse 14 | from sqlparse.tokens import DML 15 | 16 | 17 | # Setup logging 18 | logging.basicConfig(level=logging.INFO) 19 | logger = logging.getLogger(__name__) 20 | 21 | # Load environment variables 22 | load_dotenv() 23 | 24 | # Define the connection string 25 | connection_string = os.getenv("AZURE_SQL_CONNECTIONSTRING") 26 | 27 | # Timeout in seconds 28 | QUERY_TIMEOUT = 30 29 | 30 | 31 | # Function to handle query timeout 32 | def handler(signum, frame): 33 | raise TimeoutError( 34 | "This query took longer than the allotted time. Either the database server is experiencing performance issues or the generated query is too expensive. Please inspect the query or retry." 35 | ) 36 | 37 | 38 | # Function to get a database connection via pyodbc and entra ID 39 | def get_conn(): 40 | try: 41 | credential = DefaultAzureCredential( 42 | exclude_interactive_browser_credential=False, 43 | managed_identity_client_id=os.getenv("AZURE_CLIENT_ID"), 44 | ) 45 | token_bytes = credential.get_token("https://database.windows.net/.default").token.encode( 46 | "UTF-16-LE" 47 | ) 48 | token_struct = struct.pack(f" [!NOTE] 14 | > In practical situations, it is *not* recommended to give a UAMI exclusive admin rights to your database. 15 | -------------------------------------------------------------------------------- /docs/screenshot_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/function-call-dynamic-query-demo/321584c47919a583e178cf7b97474855fb820d68/docs/screenshot_chat.png -------------------------------------------------------------------------------- /docs/screenshot_chat_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/function-call-dynamic-query-demo/321584c47919a583e178cf7b97474855fb820d68/docs/screenshot_chat_1.png -------------------------------------------------------------------------------- /infra/core/ai/openai.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param chatgpt4oDeploymentCapacity int = 20 4 | param openaideploymentname string = 'gpt-4o' 5 | param openaimodelversion string = '2024-05-13' 6 | param openaiApiVersion string = '2024-05-01-preview' 7 | param customSubDomainName string = name 8 | 9 | 10 | resource openAI 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { 11 | name: name 12 | location: location 13 | kind: 'OpenAI' 14 | sku: { 15 | name: 'S0' 16 | tier: 'Standard' 17 | } 18 | properties: { 19 | customSubDomainName: customSubDomainName 20 | } 21 | resource gpt4o 'deployments' = { 22 | name: openaideploymentname 23 | sku: { 24 | name: 'Standard' 25 | capacity: chatgpt4oDeploymentCapacity 26 | } 27 | properties: { 28 | model: { 29 | format: 'OpenAI' 30 | name: openaideploymentname 31 | version: openaimodelversion 32 | } 33 | } 34 | } 35 | } 36 | 37 | output openAIResourceId string = openAI.id 38 | output openAIResourceName string = openAI.name 39 | output openAIEndpoint string = openAI.properties.endpoint 40 | output openAIDeploymentName string = openaideploymentname 41 | output openAIDeploymentVersion string = openaimodelversion 42 | output openAIAPIversion string = openaiApiVersion 43 | output openAPItestingVersion string = openAI.apiVersion 44 | -------------------------------------------------------------------------------- /infra/core/database/sql-database.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param serverName string 3 | param databaseName string 4 | param administratorLogin string 5 | @secure() 6 | param administratorPassword string 7 | // param administratorAADId string 8 | param tags object = {} 9 | 10 | // parameters for aad 11 | param aad_admin_type string 12 | param aad_only_auth bool 13 | @description('The name of the Azure AD admin for the SQL server.') 14 | param aad_admin_name string 15 | @description('The Tenant ID of the Azure Active Directory') 16 | param aad_admin_tenantid string = subscription().tenantId 17 | @description('The Object ID of the Azure AD admin.') 18 | param aad_admin_objectid string 19 | 20 | // referenced properties: 21 | // Reference Properties 22 | 23 | resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = { 24 | name: serverName 25 | location: location 26 | properties: { 27 | administratorLogin: administratorLogin 28 | administratorLoginPassword: administratorPassword 29 | version: '12.0' 30 | minimalTlsVersion: '1.2' 31 | publicNetworkAccess: 'Enabled' 32 | administrators: { 33 | administratorType: 'ActiveDirectory' 34 | login: aad_admin_name 35 | sid: aad_admin_objectid 36 | tenantId: aad_admin_tenantid 37 | principalType: aad_admin_type 38 | azureADOnlyAuthentication: aad_only_auth 39 | 40 | // sid: administratorAADId 41 | } 42 | } 43 | tags: tags 44 | } 45 | 46 | resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01-preview' = { 47 | parent: sqlServer 48 | name: databaseName 49 | location: location 50 | properties: { 51 | collation: 'SQL_Latin1_General_CP1_CI_AS' 52 | maxSizeBytes: 34359738368 53 | autoPauseDelay: 60 54 | readScale: 'Disabled' 55 | zoneRedundant: false 56 | sampleName: 'AdventureWorksLT' 57 | } 58 | sku: { 59 | name: 'GP_S_Gen5' 60 | tier: 'GeneralPurpose' 61 | family: 'Gen5' 62 | capacity: 1 63 | } 64 | tags: tags 65 | } 66 | 67 | resource firewallRule 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = { 68 | parent: sqlServer 69 | name: 'AllowAlLinternalAzureIps' 70 | properties: { 71 | endIpAddress: '0.0.0.0' 72 | startIpAddress: '0.0.0.0' 73 | } 74 | } 75 | 76 | resource firewallRule_Azure 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = { 77 | parent: sqlServer 78 | name: 'AllowAllIps' 79 | properties: { 80 | endIpAddress:'255.255.255.255' 81 | startIpAddress: '0.0.0.0' 82 | } 83 | } 84 | 85 | 86 | output sqlResourceId string = sqlServer.id 87 | output sqlResourcename string = sqlServer.name 88 | output sqlHostName string = sqlServer.properties.fullyQualifiedDomainName 89 | output sqlDatabaseName string = sqlDatabase.name 90 | output connectionString string = 'Driver={ODBC Driver 18 for SQL Server};Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${sqlDatabase.name};Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30' 91 | output sqlDatabaseuser string = administratorLogin 92 | -------------------------------------------------------------------------------- /infra/core/host/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | 5 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 6 | name: '${name}-appinsights' 7 | location: location 8 | kind: 'web' 9 | properties: { 10 | Application_Type: 'web' 11 | } 12 | tags: tags 13 | } 14 | -------------------------------------------------------------------------------- /infra/core/host/appservice.bicep: -------------------------------------------------------------------------------- 1 | 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | // Reference Properties 7 | param appServicePlanId string 8 | param managedIdentityId string 9 | 10 | // Runtime Properties 11 | @allowed([ 12 | 'python' 13 | ]) 14 | param runtimeName string 15 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 16 | param runtimeVersion string 17 | 18 | // GitHub Deployment Properties 19 | // param githubRepo string 20 | // param githubBranch string = 'main' 21 | 22 | 23 | //enableoryxbuild 24 | param enableOryxBuild bool = contains(kind, 'linux') 25 | 26 | 27 | // Microsoft.Web/sites Properties 28 | param kind string = 'app,linux' 29 | 30 | // Microsoft.Web/sites/config 31 | param allowedOrigins array = [] 32 | param alwaysOn bool = true 33 | param appCommandLine string = '' 34 | @secure() 35 | param appSettings object = {} 36 | param clientAffinityEnabled bool = false 37 | param scmDoBuildDuringDeployment bool = false 38 | param use32BitWorkerProcess bool = false 39 | param ftpsState string = 'FtpsOnly' 40 | param healthCheckPath string = '' 41 | @allowed([ 'Enabled', 'Disabled' ]) 42 | param publicNetworkAccess string = 'Enabled' 43 | 44 | var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ] 45 | 46 | 47 | 48 | var coreConfig = { 49 | linuxFxVersion: runtimeNameAndVersion 50 | alwaysOn: alwaysOn 51 | ftpsState: ftpsState 52 | http20Enabled: true 53 | httpLoggingEnabled: true 54 | numberOfWorkers: 1 55 | appCommandLine: appCommandLine 56 | minTlsVersion: '1.2' 57 | use32BitWorkerProcess: use32BitWorkerProcess 58 | healthCheckPath: healthCheckPath 59 | cors: { 60 | allowedOrigins: union(msftAllowedOrigins, allowedOrigins) 61 | } 62 | } 63 | 64 | var appServiceProperties = { 65 | serverFarmId: appServicePlanId 66 | siteConfig: coreConfig 67 | clientAffinityEnabled: clientAffinityEnabled 68 | httpsOnly: true 69 | publicNetworkAccess: publicNetworkAccess 70 | } 71 | 72 | 73 | resource appService 'Microsoft.Web/sites@2022-03-01' = { 74 | name: name 75 | location: location 76 | tags: tags 77 | kind: kind 78 | properties: appServiceProperties 79 | identity: { 80 | type: 'UserAssigned' 81 | userAssignedIdentities: { 82 | '${managedIdentityId}': {} 83 | } 84 | } 85 | 86 | resource configAppSettings 'config' = { 87 | name: 'appsettings' 88 | properties: union(appSettings, 89 | { 90 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) 91 | ENABLE_ORYX_BUILD: string(enableOryxBuild) 92 | }, 93 | runtimeName == 'python' ? { PYTHON_ENABLE_GUNICORN_MULTIWORKERS: 'true' } : {}) 94 | } 95 | 96 | resource configLogs 'config' = { 97 | name: 'logs' 98 | properties: { 99 | applicationLogs: { fileSystem: { level: 'Verbose' } } 100 | detailedErrorMessages: { enabled: true } 101 | failedRequestsTracing: { enabled: true } 102 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } 103 | } 104 | dependsOn: [ 105 | configAppSettings 106 | ] 107 | } 108 | 109 | resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { 110 | name: 'ftp' 111 | properties: { 112 | allow: false 113 | } 114 | } 115 | 116 | resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { 117 | name: 'scm' 118 | properties: { 119 | allow: false 120 | } 121 | } 122 | 123 | // resource configSourceControl 'sourcecontrols' = { 124 | // name: 'web' 125 | // properties: { 126 | // repoUrl: githubRepo 127 | // branch: githubBranch 128 | // isManualIntegration: false 129 | // isGitHubAction: true 130 | // deploymentRollbackEnabled: true 131 | // } 132 | // } 133 | } 134 | 135 | 136 | 137 | 138 | 139 | output SERVICE_WEB_NAME string = appService.name 140 | output SERVICE_WEB_URI string = appService.properties.defaultHostName 141 | 142 | 143 | output id string = appService.id 144 | output name string = appService.name 145 | output uri string = 'https://${appService.properties.defaultHostName}' 146 | output identityPrincipalId string = appService.identity.userAssignedIdentities[managedIdentityId].principalId 147 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure App Service plan.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param kind string = '' 7 | param reserved bool = true 8 | param sku object 9 | 10 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 11 | name: name 12 | location: location 13 | tags: tags 14 | sku: sku 15 | kind: kind 16 | properties: { 17 | reserved: reserved 18 | } 19 | } 20 | 21 | output id string = appServicePlan.id 22 | output name string = appServicePlan.name 23 | -------------------------------------------------------------------------------- /infra/core/identity/roleAssignments.bicep: -------------------------------------------------------------------------------- 1 | param openAIResourcename string 2 | param sqlResourcename string 3 | // param managedIdentityId string 4 | param managedIdentityPrincipalId string 5 | 6 | param roledefinitionopenai string = '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' 7 | param roledefinitionsql string = '9b7fa17d-e63e-47b0-bb0a-15c516ac86ec' 8 | 9 | 10 | resource openAIResource 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' existing = { 11 | name: openAIResourcename 12 | } 13 | 14 | resource sqlServerResource 'Microsoft.Sql/servers@2023-08-01-preview' existing = { 15 | name: sqlResourcename 16 | } 17 | 18 | resource openAIRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 19 | name: guid(subscription().id, resourceGroup().id, managedIdentityPrincipalId, roledefinitionopenai) 20 | scope: openAIResource 21 | properties: { 22 | principalType: 'ServicePrincipal' 23 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roledefinitionopenai) 24 | principalId: managedIdentityPrincipalId 25 | } 26 | } 27 | 28 | resource sqlRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 29 | name: guid(subscription().id, resourceGroup().id, managedIdentityPrincipalId, roledefinitionsql) 30 | scope: sqlServerResource 31 | properties: { 32 | 33 | principalType: 'ServicePrincipal' 34 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roledefinitionsql) 35 | principalId: managedIdentityPrincipalId 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /infra/core/identity/user-mi.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string 3 | param tags object = {} 4 | 5 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2021-09-30-preview' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | } 10 | 11 | output managedIdentityId string = managedIdentity.id 12 | output managedIdentityName string = managedIdentity.name 13 | output managedIdentityPrincipalId string = managedIdentity.properties.principalId 14 | output managedIdentityClientId string = managedIdentity.properties.clientId 15 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @allowed([ 4 | 'eastus' 5 | 'eastus2' 6 | 'westus' 7 | ]) 8 | param location string 9 | param name string 10 | param tags object = {} 11 | 12 | //sql param 13 | param administratorLogin string = uniqueString(name, location, subscription().tenantId, 'sqladmin') 14 | @secure() 15 | param administratorPassword string // Autogenerated in main.parameters.json 16 | 17 | // parameters for aad 18 | param aad_admin_type string = 'User' 19 | param aad_only_auth bool = false 20 | @description('The name of the Azure AD admin for the SQL server.') 21 | param aad_admin_name string 22 | @description('The Tenant ID of the Azure Active Directory') 23 | param aad_admin_tenantid string = subscription().tenantId 24 | @description('The Object ID of the Azure AD admin.') 25 | param aad_admin_objectid string 26 | 27 | 28 | // app service 29 | param appServicePlanName string = '' 30 | param appServiceSkuName string 31 | 32 | 33 | var resourceToken = toLower(uniqueString(subscription().id, name, location)) 34 | var prefix = '${name}-${resourceToken}' 35 | 36 | 37 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { 38 | name: '${name}-rg' 39 | location: location 40 | tags: tags 41 | } 42 | 43 | module appServicePlan 'core/host/appserviceplan.bicep' = { 44 | name: 'appserviceplan' 45 | scope: resourceGroup 46 | params: { 47 | name: !empty(appServicePlanName) ? appServicePlanName : '${prefix}-plan' 48 | location: location 49 | tags: tags 50 | sku: { 51 | name: appServiceSkuName 52 | capacity: 1 53 | } 54 | kind: 'linux' 55 | } 56 | } 57 | 58 | module appService 'core/host/appservice.bicep' = { 59 | name: 'appServiceDeployment' 60 | scope: resourceGroup 61 | params: { 62 | name: '${prefix}-webapp' 63 | location: location 64 | tags: union(tags, { 'azd-service-name': 'appdev' }) 65 | appServicePlanId: appServicePlan.outputs.id 66 | managedIdentityId: managedIdentity.outputs.managedIdentityId 67 | runtimeName: 'python' 68 | runtimeVersion: '3.10' 69 | appCommandLine: 'entrypoint.sh' 70 | scmDoBuildDuringDeployment: true 71 | // githubRepo:'https://github.com/abdulzedan/function-call-dynamic-query-demo.git' 72 | // githubBranch: 'main' 73 | appSettings: { 74 | SQL_SERVER: sqlDatabase.outputs.sqlHostName 75 | SQL_DATABASE: sqlDatabase.outputs.sqlDatabaseName 76 | SQL_USERNAME: sqlDatabase.outputs.sqlDatabaseuser 77 | SQL_PASSWORD: administratorPassword 78 | AZURE_SQL_CONNECTIONSTRING: sqlDatabase.outputs.connectionString 79 | AZURE_CLIENT_ID: managedIdentity.outputs.managedIdentityClientId 80 | AZURE_OPENAI_ENDPOINT: openAIService.outputs.openAIEndpoint 81 | AZURE_OPENAI_VERSION: openAIService.outputs.openAIAPIversion 82 | AZURE_OPENA_MODEL_VERSION: openAIService.outputs.openAIDeploymentVersion 83 | AZURE_OPENAI_CHAT_DEPLOYMENT: openAIService.outputs.openAIDeploymentName 84 | } 85 | } 86 | } 87 | 88 | 89 | 90 | 91 | module sqlDatabase 'core/database/sql-database.bicep' = { 92 | name: 'sqlDatabaseDeployment' 93 | scope: resourceGroup 94 | params: { 95 | aad_admin_name: aad_admin_name 96 | aad_admin_objectid: aad_admin_objectid 97 | aad_admin_tenantid: aad_admin_tenantid 98 | aad_only_auth: aad_only_auth 99 | aad_admin_type: aad_admin_type 100 | location: location 101 | serverName: '${prefix}-sql-server' 102 | databaseName: '${prefix}-database' 103 | administratorLogin: administratorLogin 104 | administratorPassword: administratorPassword 105 | // administratorAADId: 'aadId' 106 | } 107 | } 108 | 109 | module managedIdentity 'core/identity/user-mi.bicep' = { 110 | name: 'managedIdentityDeployment' 111 | scope: resourceGroup 112 | params: { 113 | name: '${prefix}-identity' 114 | location: location 115 | tags: tags 116 | } 117 | } 118 | 119 | module openAIService 'core/ai/openai.bicep' = { 120 | name: 'openAIDeployment' 121 | scope: resourceGroup 122 | params: { 123 | name: '${prefix}-openai' 124 | location: location 125 | } 126 | } 127 | 128 | module applicationinsights 'core/host/applicationinsights.bicep' = { 129 | name: 'applicationInsightsDeployment' 130 | scope: resourceGroup 131 | params: { 132 | name: name 133 | location: location 134 | tags: tags 135 | } 136 | } 137 | 138 | // Assign Roles 139 | module roleAssignments 'core/identity/roleAssignments.bicep' = { 140 | name: 'roleAssignmentsDeployment' 141 | scope: resourceGroup 142 | dependsOn: [ 143 | openAIService 144 | sqlDatabase 145 | managedIdentity 146 | ] 147 | params: { 148 | openAIResourcename: openAIService.outputs.openAIResourceName 149 | sqlResourcename: sqlDatabase.outputs.sqlResourcename 150 | // managedIdentityId: managedIdentity.outputs.managedIdentityId 151 | managedIdentityPrincipalId: managedIdentity.outputs.managedIdentityPrincipalId 152 | } 153 | } 154 | 155 | 156 | output resourceGroupName string = resourceGroup.name 157 | output managedIdentityId string = managedIdentity.outputs.managedIdentityId 158 | output openAIResourceId string = openAIService.outputs.openAIResourceId 159 | output SQL_SERVER string = sqlDatabase.outputs.sqlHostName 160 | output SQL_DATABASE string = sqlDatabase.outputs.sqlDatabaseName 161 | output AZURE_WEB_APP_NAME string = appService.outputs.SERVICE_WEB_NAME 162 | output ADMIN_USERNAME string = sqlDatabase.outputs.sqlDatabaseuser 163 | output MANAGED_IDENTITY_NAME string = managedIdentity.outputs.managedIdentityName 164 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "value": "${AZURE_LOCATION}" 7 | }, 8 | "name": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "tags": { 12 | "value": { 13 | "Environment": "development", 14 | "Department": "testingfastapi" 15 | } 16 | }, 17 | "administratorPassword": { 18 | "value": "$(secretOrRandomPassword)" 19 | }, 20 | "appServicePlanName": { 21 | "value": "${AZURE_APP_SERVICE_PLAN}" 22 | }, 23 | "appServiceSkuName": { 24 | "value": "${AZURE_APP_SERVICE_SKU=B1}" 25 | }, 26 | "aad_admin_name": { 27 | "value": "${AZURE_PRINCIPAL_NAME}" 28 | }, 29 | "aad_admin_objectid": { 30 | "value": "${AZURE_PRINCIPAL_ID}" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 100 3 | ignore = ["D203"] 4 | 5 | [tool.black] 6 | line-length = 100 7 | target-version = ['py310'] 8 | exclude = ''' 9 | ( 10 | ^/testing_code/ 11 | ) 12 | ''' 13 | 14 | [tool.pytest.ini_options] 15 | addopts = "-ra --cov=app" 16 | testpaths = ["tests"] 17 | pythonpath = ['.'] 18 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==24.3.0 2 | pytest==7.1.2 3 | coverage==6.4.1 4 | pytest-cov==3.0.0 5 | pre-commit 6 | ruff -------------------------------------------------------------------------------- /scripts/create-sql-user.ps1: -------------------------------------------------------------------------------- 1 | # Fetch environment variables 2 | Write-Host "Fetching environment variables..." 3 | $SQL_SERVER = azd env get-value SQL_SERVER 4 | if ($LASTEXITCODE -ne 0) { 5 | Write-Host "Failed to find a value for SQL_SERVER in your azd environment. Make sure you run azd up first." 6 | exit 1 7 | } 8 | 9 | $SQL_DATABASE = azd env get-value SQL_DATABASE 10 | $APP_IDENTITY_NAME = azd env get-value MANAGED_IDENTITY_NAME 11 | 12 | if (-not $SQL_SERVER -or -not $SQL_DATABASE -or -not $APP_IDENTITY_NAME) { 13 | Write-Host "Can't find SQL_SERVER, SQL_DATABASE, or AZURE_WEB_APP_NAME environment variables. Make sure you run azd up first." 14 | exit 1 15 | } 16 | 17 | Write-Host "Environment variables fetched successfully." 18 | Write-Host "SQL_SERVER: $SQL_SERVER" 19 | Write-Host "SQL_DATABASE: $SQL_DATABASE" 20 | Write-Host "APP_IDENTITY_NAME: $APP_IDENTITY_NAME" 21 | 22 | 23 | if (-Not (Test-Path -Path .\.venv)) { 24 | # Create Python virtual environment 25 | Write-Output 'Creating Python virtual environment in .venv...' 26 | python -m venv .venv 27 | 28 | # Activate the virtual environment 29 | Write-Output 'Activating the Python virtual environment...' 30 | . .\.venv\Scripts\Activate.ps1 31 | 32 | # Install dependencies from requirements.txt 33 | Write-Output 'Installing dependencies from "requirements.txt" into virtual environment...' 34 | .venv\Scripts\python -m pip install -r .\scripts\requirements.txt --quiet --disable-pip-version-check 35 | } else { 36 | Write-Output 'Using existing Python virtual environment in .venv...' 37 | 38 | # Activate the virtual environment 39 | Write-Output 'Activating the Python virtual environment...' 40 | . .\.venv\Scripts\Activate.ps1 41 | 42 | # Install dependencies from requirements.txt 43 | Write-Output 'Installing dependencies from "requirements.txt" into virtual environment...' 44 | .venv\Scripts\python -m pip install -r .\scripts\requirements.txt --quiet --disable-pip-version-check 45 | } 46 | 47 | 48 | # Run the Python script to assign roles 49 | Write-Host "Running the Python script to assign roles..." 50 | python ./app/setup_sql_database_role.py --server $SQL_SERVER --database $SQL_DATABASE --app-identity-name $APP_IDENTITY_NAME 51 | if ($LASTEXITCODE -ne 0) { 52 | Write-Host "Failed to run the Python script." 53 | exit 1 54 | } 55 | 56 | Write-Host "Python script executed successfully." 57 | -------------------------------------------------------------------------------- /scripts/create-sql-user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fetch environment variables 4 | echo "Fetching environment variables..." 5 | SQL_SERVER=$(azd env get-value SQL_SERVER) 6 | if [ $? -ne 0 ]; then 7 | echo "Failed to find a value for SQL_SERVER in your azd environment. Make sure you run azd up first." 8 | exit 1 9 | fi 10 | 11 | SQL_DATABASE=$(azd env get-value SQL_DATABASE) 12 | APP_IDENTITY_NAME=$(azd env get-value MANAGED_IDENTITY_NAME) 13 | 14 | if [ -z "$SQL_SERVER" ] || [ -z "$SQL_DATABASE" ] || [ -z "$APP_IDENTITY_NAME" ]; then 15 | echo "Can't find SQL_SERVER, SQL_DATABASE, or AZURE_WEB_APP_NAME environment variables. Make sure you run azd up first." 16 | exit 1 17 | fi 18 | 19 | echo "Environment variables fetched successfully." 20 | echo "SQL_SERVER: $SQL_SERVER" 21 | echo "SQL_DATABASE: $SQL_DATABASE" 22 | echo "APP_IDENTITY_NAME: $APP_IDENTITY_NAME" 23 | 24 | # Detect the OS version and install the appropriate Oracle driver 25 | OS=$(uname) 26 | if [ "$OS" == "Linux" ]; then 27 | OS_ID=$(lsb_release -is 2>/dev/null) 28 | OS_VERSION=$(lsb_release -rs 2>/dev/null) 29 | 30 | if [ "$OS_ID" == "Debian" ]; then 31 | echo "Detected OS: Debian $OS_VERSION" 32 | 33 | # Skip installing libldap-2.5-0 if dependencies cannot be met 34 | if ! dpkg -s libldap-2.5-0 &>/dev/null; then 35 | echo "Skipping installation of libldap-2.5-0 due to unmet dependencies." 36 | else 37 | echo "Installing libldap-2.5-0..." 38 | dpkg -i libldap-2.5-0_2.5.13+dfsg-5_amd64.deb 39 | if [ $? -ne 0 ]; then 40 | echo "Failed to install libldap-2.5-0. Skipping further libldap installation." 41 | else 42 | dpkg -i libldap-dev_2.5.13+dfsg-5_amd64.deb 43 | fi 44 | fi 45 | 46 | # Add GPG key and configure repository 47 | echo "Adding GPG key and configuring repository..." 48 | if [ "$OS_VERSION" == "9" ] || [ "$OS_VERSION" == "10" ] || [ "$OS_VERSION" == "11" ]; then 49 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc 50 | elif [ "$OS_VERSION" == "12" ]; then 51 | curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg 52 | else 53 | echo "Debian $OS_VERSION is not currently supported." 54 | exit 1 55 | fi 56 | 57 | # Configure repository based on Debian version 58 | echo "Configuring repository based on Debian version..." 59 | curl https://packages.microsoft.com/config/debian/$OS_VERSION/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list 60 | 61 | # Clear APT cache and update 62 | echo "Clearing APT cache and updating..." 63 | sudo rm -rf /var/lib/apt/lists/* 64 | sudo apt-get update 65 | 66 | # Install ODBC driver and tools 67 | echo "Installing ODBC driver and tools..." 68 | sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 69 | echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc 70 | source ~/.bashrc 71 | 72 | # Install unixODBC development headers and kerberos library 73 | echo "Installing unixODBC development headers and kerberos library..." 74 | sudo apt-get install -y unixodbc-dev libgssapi-krb5-2 75 | 76 | elif [ "$OS_ID" == "Ubuntu" ]; then 77 | echo "Detected OS: Ubuntu $OS_VERSION" 78 | if ! [[ "18.04 20.04 22.04 23.04 24.04" == *"$OS_VERSION"* ]]; then 79 | echo "Ubuntu $OS_VERSION is not currently supported." 80 | exit 1 81 | fi 82 | 83 | # Add GPG key and configure repository 84 | echo "Adding GPG key and configuring repository..." 85 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc 86 | curl https://packages.microsoft.com/config/ubuntu/$OS_VERSION/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list 87 | 88 | # Clear APT cache and update 89 | echo "Clearing APT cache and updating..." 90 | sudo rm -rf /var/lib/apt/lists/* 91 | sudo apt-get update 92 | 93 | # Install ODBC driver and tools 94 | echo "Installing ODBC driver and tools..." 95 | sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 96 | echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' >> ~/.bashrc 97 | source ~/.bashrc 98 | 99 | # Install unixODBC development headers and kerberos library 100 | echo "Installing unixODBC development headers and kerberos library..." 101 | sudo apt-get install -y unixodbc-dev libgssapi-krb5-2 102 | 103 | else 104 | echo "Unsupported Linux distribution." 105 | exit 1 106 | fi 107 | 108 | elif [ "$OS" == "Darwin" ]; then 109 | echo "Detected OS: macOS" 110 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 111 | brew tap microsoft/mssql-release https://github.com/Microsoft/homebrew-mssql-release 112 | brew update 113 | HOMEBREW_ACCEPT_EULA=Y brew install msodbcsql18 mssql-tools18 114 | echo 'export PATH="/usr/local/opt/msodbcsql18/bin:/usr/local/opt/mssql-tools18/bin:$PATH"' >> ~/.zshrc 115 | source ~/.zshrc 116 | else 117 | echo "Unsupported OS: $OS" 118 | exit 1 119 | fi 120 | 121 | # Load the Python environment (if using a virtual environment) 122 | echo "Loading Python environment..." 123 | . ./scripts/load_python_env.sh 124 | if [ $? -ne 0 ]; then 125 | echo "Failed to load Python environment." 126 | exit 1 127 | fi 128 | 129 | echo "Python environment loaded successfully." 130 | 131 | # Run the Python script to assign roles 132 | echo "Running the Python script to assign roles..." 133 | python3 ./app/setup_sql_database_role.py --server $SQL_SERVER --database $SQL_DATABASE --app-identity-name $APP_IDENTITY_NAME 134 | if [ $? -ne 0 ]; then 135 | echo "Failed to run the Python script." 136 | exit 1 137 | fi 138 | 139 | echo "Python script executed successfully." 140 | -------------------------------------------------------------------------------- /scripts/fetch-principal-info.ps1: -------------------------------------------------------------------------------- 1 | # Get the absolute path of the project root directory 2 | $projectRoot = Split-Path -Parent (Split-Path -Parent (Resolve-Path $MyInvocation.MyCommand.Path)) 3 | $envPath = "$projectRoot\.env" 4 | 5 | Write-Host "Fetching Azure Principal ID and Name..." 6 | 7 | # Fetch Principal ID 8 | $principalId = az ad signed-in-user show --query id -o tsv 9 | if (-not $principalId) { 10 | Write-Host "Error: Failed to fetch Principal ID. Ensure you are logged in to Azure and have the necessary permissions." 11 | exit 1 12 | } 13 | Write-Host "Fetched Principal ID: $principalId" 14 | 15 | # Fetch Principal Name 16 | $principalName = az ad signed-in-user show --query userPrincipalName -o tsv 17 | if (-not $principalName) { 18 | Write-Host "Error: Failed to fetch Principal Name. Ensure you are logged in to Azure and have the necessary permissions." 19 | exit 1 20 | } 21 | Write-Host "Fetched Principal Name: $principalName" 22 | 23 | # Append to .env file in the project root 24 | if (Test-Path $envPath) { 25 | Add-Content $envPath "AZURE_PRINCIPAL_ID=$principalId" 26 | Add-Content $envPath "AZURE_PRINCIPAL_NAME=$principalName" 27 | Write-Host "Principal ID and Name have been appended to the .env file in the root directory." 28 | } else { 29 | Write-Host "Error: The .env file does not exist or is not accessible." 30 | } 31 | -------------------------------------------------------------------------------- /scripts/fetch-principal-info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the absolute path of the project root directory 4 | PROJECT_ROOT=$(dirname "$(dirname "$(realpath "$0")")") 5 | ENV_PATH="$PROJECT_ROOT/.env" 6 | 7 | echo "Fetching Azure Principal ID and Name..." 8 | 9 | # Fetch Principal ID 10 | AZURE_PRINCIPAL_ID=$(az ad signed-in-user show --output tsv --query "id") 11 | if [ -z "$AZURE_PRINCIPAL_ID" ]; then 12 | echo "Error: Failed to fetch Principal ID. Ensure you are logged in to Azure and have the necessary permissions." 13 | exit 1 14 | fi 15 | echo "Fetched Principal ID: $AZURE_PRINCIPAL_ID" 16 | 17 | # Fetch Principal Name 18 | AZURE_PRINCIPAL_NAME=$(az ad signed-in-user show --query userPrincipalName -o tsv) 19 | if [ -z "$AZURE_PRINCIPAL_NAME" ]; then 20 | echo "Error: Failed to fetch Principal Name. Ensure you are logged in to Azure and have the necessary permissions." 21 | exit 1 22 | fi 23 | echo "Fetched Principal Name: $AZURE_PRINCIPAL_NAME" 24 | 25 | # Append to .env file in the project root 26 | if [ -e "$ENV_PATH" ] && [ -w "$ENV_PATH" ]; then 27 | echo "AZURE_PRINCIPAL_ID=$AZURE_PRINCIPAL_ID" >> "$ENV_PATH" 28 | echo "AZURE_PRINCIPAL_NAME=$AZURE_PRINCIPAL_NAME" >> "$ENV_PATH" 29 | echo "Principal ID and Name have been appended to the .env file in the root directory." 30 | else 31 | echo "Error: Cannot write to $ENV_PATH. Check file permissions." 32 | fi 33 | -------------------------------------------------------------------------------- /scripts/load_python_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'Creating Python virtual environment in .venv...' 4 | python3 -m venv .venv 5 | 6 | echo 'Installing dependencies from "requirements.txt" into virtual environment (in quiet mode)...' 7 | .venv/bin/python -m pip --quiet --disable-pip-version-check install -r ./scripts/requirements.txt 8 | 9 | echo 'Activating the virtual environment...' 10 | . .venv/bin/activate 11 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-identity==1.17.1 2 | pyodbc==5.1.0 3 | -------------------------------------------------------------------------------- /scripts/start.txt: -------------------------------------------------------------------------------- 1 | gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app -------------------------------------------------------------------------------- /tests/test_functions,py: -------------------------------------------------------------------------------- 1 | # tests/test_functions.py 2 | 3 | import pytest 4 | from unittest.mock import patch, MagicMock 5 | from app.functions import execute_query, get_conn 6 | 7 | # Mocking database connection 8 | @pytest.fixture 9 | def mock_db_conn(): 10 | with patch('app.functions.get_conn') as mock: 11 | conn = MagicMock() 12 | cursor = conn.cursor.return_value 13 | cursor.fetchall.return_value = [(1, 'red')] 14 | cursor.description = [('count',)] 15 | mock.return_value = conn 16 | yield mock 17 | 18 | def test_execute_query(mock_db_conn): 19 | query = "SELECT COUNT(*) FROM SalesLT.Product WHERE Color = 'red'" 20 | result = execute_query(query) 21 | assert result is not None 22 | assert "error" not in result 23 | assert '"count": 1' in result 24 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # tests/test_main.py 2 | 3 | from fastapi.testclient import TestClient 4 | from app.main import app 5 | 6 | client = TestClient(app) 7 | 8 | 9 | def test_root_endpoint(): 10 | response = client.get("/") 11 | assert response.status_code == 200 12 | assert response.json() == {"message": "Establishing Root Endpoint"} 13 | 14 | 15 | def test_ask_endpoint(): 16 | payload = {"message": "How many products with the color red do we have?"} 17 | response = client.post("/ask/", json=payload) 18 | assert response.status_code == 200 19 | # Check if the response contains expected keys 20 | assert "response" in response.json() or "error" in response.json() 21 | -------------------------------------------------------------------------------- /tests/test_routes.py: -------------------------------------------------------------------------------- 1 | # tests/test_routes.py 2 | 3 | from fastapi.testclient import TestClient 4 | from app.main import app 5 | 6 | client = TestClient(app) 7 | 8 | 9 | def test_health_check(): 10 | response = client.get("/health_check/") 11 | assert response.status_code == 200 12 | json_response = response.json() 13 | assert json_response["status"] in ["success", "error"] 14 | 15 | 16 | def test_execute_query_endpoint(): 17 | payload = {"query": "SELECT 1 AS Test"} 18 | response = client.post("/execute_query/", json=payload) 19 | assert response.status_code == 200 20 | assert "results" in response.json() 21 | --------------------------------------------------------------------------------