├── .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 | [](https://codespaces.new/Azure-Samples/rag-postgres-openai-python)
4 | [](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 | 
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 | [](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 | [](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 |
--------------------------------------------------------------------------------