├── .devcontainer ├── Dockerfile ├── devcontainer.json └── first-run-notice.txt ├── .funcignore ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── azure.yaml ├── function_app.py ├── host.json ├── infra ├── abbreviations.json ├── app │ ├── ai-Cog-Service-Access.bicep │ ├── api.bicep │ ├── eventgrid.bicep │ ├── storage-Access.bicep │ ├── storage-PrivateEndpoint.bicep │ └── vnet.bicep ├── core │ ├── ai │ │ └── openai.bicep │ ├── host │ │ ├── appserviceplan.bicep │ │ └── functions-flexconsumption.bicep │ ├── identity │ │ └── userAssignedIdentity.bicep │ ├── monitor │ │ ├── appinsights-access.bicep │ │ ├── applicationinsights.bicep │ │ ├── loganalytics.bicep │ │ └── monitoring.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── requirements.txt ├── test.http └── testdata.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/universal:latest 2 | 3 | # Copy custom first notice message. 4 | COPY first-run-notice.txt /tmp/staging/ 5 | RUN sudo mv -f /tmp/staging/first-run-notice.txt /usr/local/etc/vscode-dev-containers/ \ 6 | && sudo rm -rf /tmp/staging 7 | 8 | # Install PowerShell 7.x 9 | RUN sudo apt-get update \ 10 | && sudo apt-get install -y wget apt-transport-https software-properties-common \ 11 | && wget -q https://packages.microsoft.com/config/ubuntu/$(. /etc/os-release && echo $VERSION_ID)/packages-microsoft-prod.deb \ 12 | && sudo dpkg -i packages-microsoft-prod.deb \ 13 | && sudo apt-get update \ 14 | && sudo apt-get install -y powershell 15 | 16 | # Install Azure Functions Core Tools 17 | RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \ 18 | && sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg \ 19 | && sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' \ 20 | && sudo apt-get update \ 21 | && sudo apt-get install -y azure-functions-core-tools-4 22 | 23 | # Install Azure Developer CLI 24 | RUN curl -fsSL https://aka.ms/install-azd.sh | bash 25 | 26 | # Install mechanical-markdown for quickstart validations 27 | RUN pip install mechanical-markdown 28 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Functions Quickstarts Codespace", 3 | "dockerFile": "Dockerfile", 4 | "features": { 5 | "azure-cli": "latest" 6 | }, 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "ms-azuretools.vscode-bicep", 11 | "ms-azuretools.vscode-docker", 12 | "ms-azuretools.vscode-azurefunctions", 13 | "GitHub.copilot", 14 | "humao.rest-client" 15 | ] 16 | } 17 | }, 18 | "mounts": [ 19 | // Mount docker-in-docker library volume 20 | "source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume" 21 | ], 22 | // Always run image-defined docker-init.sh to enable docker-in-docker 23 | "overrideCommand": false, 24 | "remoteUser": "codespace", 25 | "runArgs": [ 26 | // Enable ptrace-based debugging for Go in container 27 | "--cap-add=SYS_PTRACE", 28 | "--security-opt", 29 | "seccomp=unconfined", 30 | 31 | // Enable docker-in-docker configuration 32 | "--init", 33 | "--privileged" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.devcontainer/first-run-notice.txt: -------------------------------------------------------------------------------- 1 | 👋 Welcome to the Functions Codespace! You are on the Functions Quickstarts image. 2 | It includes everything needed to run through our tutorials and quickstart applications. 3 | 4 | 📚 Functions docs can be found at: https://learn.microsoft.com/en-us/azure/azure-functions/ 5 | -------------------------------------------------------------------------------- /.funcignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | # Azurite artifacts 46 | __blobstorage__ 47 | __queuestorage__ 48 | __azurite_db*__.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Python Functions", 6 | "type": "debugpy", 7 | "request": "attach", 8 | "connect": { 9 | "host": "localhost", 10 | "port": 9091 11 | }, 12 | "preLaunchTask": "func: host start" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.scmDoBuildDuringDeployment": true, 4 | "azureFunctions.pythonVenv": ".venv", 5 | "azureFunctions.projectLanguage": "Python", 6 | "azureFunctions.projectRuntime": "~4", 7 | "debug.internalConsoleOptions": "neverOpen" 8 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-python-watch", 9 | "isBackground": true, 10 | "dependsOn": "pip install (functions)" 11 | }, 12 | { 13 | "label": "pip install (functions)", 14 | "type": "shell", 15 | "osx": { 16 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 17 | }, 18 | "windows": { 19 | "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" 20 | }, 21 | "linux": { 22 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 23 | }, 24 | "problemMatcher": [] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase 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) 2023 Azure Samples 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 | 23 | 24 | 25 | # Azure Functions 26 | ## Chat using Azure OpenAI (Python v2 Function) 27 | 28 | This sample shows simple ways to interact with Azure OpenAI & GPT-4 model to build an interactive using Azure Functions [Azure Open AI Triggers and Bindings extension](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-openai?tabs=isolated-process&pivots=programming-language-javascript). You can issue simple prompts and receive completions using the `ask` function, and you can send messages and perform a stateful session with a friendly ChatBot using the `chats` function. The app deploys easily to Azure Functions Flex Consumption hosting plan using `azd up`. 29 | 30 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/Azure-Samples/function-python-ai-openai-chatgpt) 31 | 32 | ## Run on your local environment 33 | 34 | ### Pre-reqs 35 | 1) [Python 3.8+](https://www.python.org/) 36 | 2) [Azure Functions Core Tools 4.0.6610 or higher](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cmacos%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools) 37 | 3) [Azurite](https://github.com/Azure/Azurite) 38 | 39 | The easiest way to install Azurite is using a Docker container or the support built into Visual Studio: 40 | ```bash 41 | docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite 42 | ``` 43 | 44 | 4) Once you have your Azure subscription, run the following in a new terminal window to create Azure OpenAI and other resources needed: 45 | ```bash 46 | azd provision 47 | ``` 48 | 49 | Take note of the value of `AZURE_OPENAI_ENDPOINT` which can be found in `./.azure//.env`. It will look something like: 50 | ```bash 51 | AZURE_OPENAI_ENDPOINT="https://cog-.openai.azure.com/" 52 | ``` 53 | 54 | Alternatively you can [create an OpenAI resource](https://portal.azure.com/#create/Microsoft.CognitiveServicesTextAnalytics) in the Azure portal to get your key and endpoint. After it deploys, click Go to resource and view the Endpoint value. You will also need to deploy a model, e.g. with name `chat` and model `gpt-4o`. 55 | 56 | 5) Add this `local.settings.json` file to the root of the repo folder to simplify local development. Replace `AZURE_OPENAI_ENDPOINT` with your value from step 4. Optionally you can choose a different model deployment in `CHAT_MODEL_DEPLOYMENT_NAME`. This file will be gitignored to protect secrets from committing to your repo, however by default the sample uses Entra identity (user identity and mananaged identity) so it is secretless. 57 | ```json 58 | { 59 | "IsEncrypted": false, 60 | "Values": { 61 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 62 | "FUNCTIONS_WORKER_RUNTIME": "python", 63 | "AZURE_OPENAI_ENDPOINT": "https://cog-.openai.azure.com/", 64 | "CHAT_MODEL_DEPLOYMENT_NAME": "chat", 65 | "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", 66 | "PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1" 67 | } 68 | } 69 | ``` 70 | 71 | ## Simple Prompting with Ask Function 72 | ### Using Functions CLI 73 | 1) Open a new terminal and do the following: 74 | 75 | ```bash 76 | pip3 install -r requirements.txt 77 | func start 78 | ``` 79 | 2) Using your favorite REST client, e.g. [RestClient in VS Code](https://marketplace.visualstudio.com/items?itemName=humao.rest-client), PostMan, curl, make a post. [test.http](test.http) has been provided to run this quickly. 80 | 81 | Terminal: 82 | ```bash 83 | curl -i -X POST http://localhost:7071/api/ask/ \ 84 | -H "Content-Type: text/json" \ 85 | --data-binary "@testdata.json" 86 | ``` 87 | 88 | testdata.json 89 | ```json 90 | { 91 | "prompt": "Write a poem about Azure Functions. Include two reasons why users love them." 92 | } 93 | ``` 94 | 95 | [test.http](test.http) 96 | ```http 97 | ### Simple Ask Completion 98 | POST http://localhost:7071/api/ask HTTP/1.1 99 | content-type: application/json 100 | 101 | { 102 | "prompt": "Tell me two most popular programming features of Azure Functions" 103 | } 104 | 105 | ### Simple Whois Completion 106 | GET http://localhost:7071/api/whois/Turing HTTP/1.1 107 | ``` 108 | 109 | ## Stateful Interaction with Chatbot using Chat Function 110 | 111 | We will use the [test.http](test.http) file again now focusing on the Chat function. We need to start the chat with `chats` and send messages with `PostChat`. We can also get state at any time with `GetChatState`. 112 | 113 | ```http 114 | ### Stateful Chatbot 115 | ### CreateChatBot 116 | PUT http://localhost:7071/api/chats/abc123 117 | Content-Type: application/json 118 | 119 | { 120 | "name": "Sample ChatBot", 121 | "description": "This is a sample chatbot." 122 | } 123 | 124 | ### PostChat 125 | POST http://localhost:7071/api/chats/abc123 126 | Content-Type: application/json 127 | 128 | { 129 | "message": "Hello, how can I assist you today?" 130 | } 131 | 132 | ### PostChat 133 | POST http://localhost:7071/api/chats/abc123 134 | Content-Type: application/json 135 | 136 | { 137 | "message": "Need help with directions from Redmond to SeaTac?" 138 | } 139 | 140 | ### GetChatState 141 | GET http://localhost:7071/api/chats/abc123?timestampUTC=2024-01-15T22:00:00 142 | Content-Type: application/json 143 | ``` 144 | 145 | ## Deploy to Azure 146 | 147 | The easiest way to deploy this app is using the [Azure Developer CLI](https://aka.ms/azd). If you open this repo in GitHub CodeSpaces the AZD tooling is already preinstalled. 148 | 149 | To provision and deploy: 150 | ```bash 151 | azd up 152 | ``` 153 | 154 | ## Source Code 155 | 156 | The key code that makes the prompting and completion work is as follows in [function_app.py](function_app.py). The `/api/ask` function and route expects a prompt to come in the POST body. The templating pattern is used to define the input binding for prompt and the underlying parameters for OpenAI models like the maxTokens and the AI model to use for chat. 157 | 158 | The whois function expects a name to be sent in the route `/api/whois/` and you get to see a different example of a route and parameter coming in via http GET. 159 | 160 | ### Simple prompting and completions with gpt 161 | 162 | ```python 163 | app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) 164 | 165 | 166 | # Simple ask http POST function that returns the completion based on prompt 167 | # This OpenAI completion input requires a {prompt} value in json POST body 168 | @app.function_name("ask") 169 | @app.route(route="ask", methods=["POST"]) 170 | @app.text_completion_input(arg_name="response", prompt="{prompt}", 171 | model="%CHAT_MODEL_DEPLOYMENT_NAME%") 172 | def ask(req: func.HttpRequest, response: str) -> func.HttpResponse: 173 | response_json = json.loads(response) 174 | return func.HttpResponse(response_json["content"], status_code=200) 175 | 176 | 177 | # Simple WhoIs http GET function that returns the completion based on name 178 | # This OpenAI completion input requires a {name} binding value. 179 | @app.function_name("whois") 180 | @app.route(route="whois/{name}", methods=["GET"]) 181 | @app.text_completion_input(arg_name="response", prompt="Who is {name}?", 182 | max_tokens="100", 183 | model="%CHAT_MODEL_DEPLOYMENT_NAME%") 184 | def whois(req: func.HttpRequest, response: str) -> func.HttpResponse: 185 | response_json = json.loads(response) 186 | return func.HttpResponse(response_json["content"], status_code=200) 187 | ``` 188 | 189 | ### Stateful ChatBots with gpt 190 | 191 | The stateful chatbot is shown in [function_app.py](function_app.py) routing to `/api/chats`. This is a stateful function meaning you can create or ask for a session by and continue where you left off with the same context and memories stored by the function binding (backed Table storage). This makes use of the Assistants feature of the Azure Functions OpenAI extension that has a set of inputs and outputs for this case. 192 | 193 | To create or look up a session we have the CreateChatBot as an http PUT function. Note how the code will reuse your AzureWebJobStorage connection. The output binding of `assistantCreate` will actually kick off the create. 194 | 195 | ```python 196 | # http PUT function to start ChatBot conversation based on a chatID 197 | @app.function_name("CreateChatBot") 198 | @app.route(route="chats/{chatId}", methods=["PUT"]) 199 | @app.assistant_create_output(arg_name="requests") 200 | def create_chat_bot(req: func.HttpRequest, 201 | requests: func.Out[str]) -> func.HttpResponse: 202 | chatId = req.route_params.get("chatId") 203 | input_json = req.get_json() 204 | logging.info( 205 | f"Creating chat ${chatId} from input parameters " + 206 | "${json.dumps(input_json)}") 207 | create_request = { 208 | "id": chatId, 209 | "instructions": input_json.get("instructions"), 210 | "chatStorageConnectionSetting": CHAT_STORAGE_CONNECTION, 211 | "collectionName": COLLECTION_NAME 212 | } 213 | requests.set(json.dumps(create_request)) 214 | response_json = {"chatId": chatId} 215 | return func.HttpResponse(json.dumps(response_json), status_code=202, 216 | mimetype="application/json") 217 | ``` 218 | 219 | Subsequent chat messages are sent to the chat as http POST, being careful to use the same chatId. This makes use of the `inputAssistant` input binding to take message as input and do the completion, while also automatically pulling context and memories for the session, and also saving your new state. 220 | ```python 221 | # http POST function for user to send a message to ChatBot with chatID 222 | @app.function_name("PostUserResponse") 223 | @app.route(route="chats/{chatId}", methods=["POST"]) 224 | @app.assistant_post_input( 225 | arg_name="state", id="{chatId}", 226 | user_message="{message}", 227 | model="%CHAT_MODEL_DEPLOYMENT_NAME%", 228 | chat_storage_connection_setting=CHAT_STORAGE_CONNECTION, 229 | collection_name=COLLECTION_NAME 230 | ) 231 | def post_user_response(req: func.HttpRequest, state: str) -> func.HttpResponse: 232 | # Parse the JSON string into a dictionary 233 | data = json.loads(state) 234 | 235 | # Extract the content of the recentMessage 236 | recent_message_content = data['recentMessages'][0]['content'] 237 | return func.HttpResponse(recent_message_content, status_code=200, 238 | mimetype="text/plain") 239 | ``` 240 | 241 | The [test.http](test.http) file is helpful to see how clients and APIs should call these functions, and to learn the typical flow. 242 | 243 | You can customize this or learn more using [Open AI Triggers and Bindings extension](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-openai?tabs=isolated-process&pivots=programming-language-javascript). 244 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: chatgpt-openai-py-ai-func 4 | metadata: 5 | template: chatgpt-openai-py-ai-func@1.0.0-beta 6 | services: 7 | api: 8 | project: ./ 9 | language: python 10 | host: function 11 | -------------------------------------------------------------------------------- /function_app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import azure.functions as func 4 | 5 | app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) 6 | 7 | 8 | # Simple ask http POST function that returns the completion based on prompt 9 | # This OpenAI completion input requires a {prompt} value in json POST body 10 | @app.function_name("ask") 11 | @app.route(route="ask", methods=["POST"]) 12 | @app.text_completion_input(arg_name="response", prompt="{prompt}", 13 | model="%CHAT_MODEL_DEPLOYMENT_NAME%") 14 | def ask(req: func.HttpRequest, response: str) -> func.HttpResponse: 15 | response_json = json.loads(response) 16 | return func.HttpResponse(response_json["content"], status_code=200) 17 | 18 | 19 | # Simple WhoIs http GET function that returns the completion based on name 20 | # This OpenAI completion input requires a {name} binding value. 21 | @app.function_name("whois") 22 | @app.route(route="whois/{name}", methods=["GET"]) 23 | @app.text_completion_input(arg_name="response", prompt="Who is {name}?", 24 | max_tokens="100", 25 | model="%CHAT_MODEL_DEPLOYMENT_NAME%") 26 | def whois(req: func.HttpRequest, response: str) -> func.HttpResponse: 27 | response_json = json.loads(response) 28 | return func.HttpResponse(response_json["content"], status_code=200) 29 | 30 | 31 | CHAT_STORAGE_CONNECTION = "AzureWebJobsStorage" 32 | COLLECTION_NAME = "ChatState" 33 | 34 | 35 | # http PUT function to start ChatBot conversation based on a chatID 36 | @app.function_name("CreateChatBot") 37 | @app.route(route="chats/{chatId}", methods=["PUT"]) 38 | @app.assistant_create_output(arg_name="requests") 39 | def create_chat_bot(req: func.HttpRequest, 40 | requests: func.Out[str]) -> func.HttpResponse: 41 | chatId = req.route_params.get("chatId") 42 | input_json = req.get_json() 43 | logging.info( 44 | f"Creating chat ${chatId} from input parameters " + 45 | "${json.dumps(input_json)}") 46 | create_request = { 47 | "id": chatId, 48 | "instructions": input_json.get("instructions"), 49 | "chatStorageConnectionSetting": CHAT_STORAGE_CONNECTION, 50 | "collectionName": COLLECTION_NAME 51 | } 52 | requests.set(json.dumps(create_request)) 53 | response_json = {"chatId": chatId} 54 | return func.HttpResponse(json.dumps(response_json), status_code=202, 55 | mimetype="application/json") 56 | 57 | 58 | # http GET function to get ChatBot conversation with chatID & timestamp 59 | @app.function_name("GetChatState") 60 | @app.route(route="chats/{chatId}", methods=["GET"]) 61 | @app.assistant_query_input( 62 | arg_name="state", 63 | id="{chatId}", 64 | timestamp_utc="{Query.timestampUTC}", 65 | chat_storage_connection_setting=CHAT_STORAGE_CONNECTION, 66 | collection_name=COLLECTION_NAME 67 | ) 68 | def get_chat_state(req: func.HttpRequest, state: str) -> func.HttpResponse: 69 | return func.HttpResponse(state, status_code=200, 70 | mimetype="application/json") 71 | 72 | 73 | # http POST function for user to send a message to ChatBot with chatID 74 | @app.function_name("PostUserResponse") 75 | @app.route(route="chats/{chatId}", methods=["POST"]) 76 | @app.assistant_post_input( 77 | arg_name="state", id="{chatId}", 78 | user_message="{message}", 79 | model="%CHAT_MODEL_DEPLOYMENT_NAME%", 80 | chat_storage_connection_setting=CHAT_STORAGE_CONNECTION, 81 | collection_name=COLLECTION_NAME 82 | ) 83 | def post_user_response(req: func.HttpRequest, state: str) -> func.HttpResponse: 84 | # Parse the JSON string into a dictionary 85 | data = json.loads(state) 86 | 87 | # Extract the content of the recentMessage 88 | recent_message_content = data['recentMessages'][0]['content'] 89 | return func.HttpResponse(recent_message_content, status_code=200, 90 | mimetype="text/plain") 91 | -------------------------------------------------------------------------------- /host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle.Preview", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabases": "sqldb-", 120 | "sqlServersDatabasesStretch": "sqlstrdb-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "storSimpleManagers": "ssimp", 124 | "streamAnalyticsCluster": "asa-", 125 | "synapseWorkspaces": "syn", 126 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 127 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 128 | "synapseWorkspacesSqlPoolsSpark": "synsp", 129 | "timeSeriesInsightsEnvironments": "tsi-", 130 | "webServerFarms": "plan-", 131 | "webSitesAppService": "app-", 132 | "webSitesAppServiceEnvironment": "ase-", 133 | "webSitesFunctions": "func-", 134 | "webStaticSites": "stapp-" 135 | } -------------------------------------------------------------------------------- /infra/app/ai-Cog-Service-Access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param principalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 3 | param roleDefinitionID string 4 | param aiResourceName string 5 | 6 | resource cognitiveService 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { 7 | name: aiResourceName 8 | } 9 | 10 | // Allow access from API to this resource using a managed identity and least priv role grants 11 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 12 | name: guid(cognitiveService.id, principalID, roleDefinitionID) 13 | scope: cognitiveService 14 | properties: { 15 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 16 | principalId: principalID 17 | principalType: principalType 18 | } 19 | } 20 | 21 | output ROLE_ASSIGNMENT_NAME string = roleAssignment.name 22 | -------------------------------------------------------------------------------- /infra/app/api.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param applicationInsightsName string = '' 5 | param appServicePlanId string 6 | param appSettings object = {} 7 | param runtimeName string 8 | param runtimeVersion string 9 | param serviceName string = 'api' 10 | param storageAccountName string 11 | param deploymentStorageContainerName string 12 | param virtualNetworkSubnetId string = '' 13 | param instanceMemoryMB int = 2048 14 | param maximumInstanceCount int = 100 15 | param identityId string = '' 16 | param identityClientId string = '' 17 | param aiServiceUrl string = '' 18 | 19 | var applicationInsightsIdentity = 'ClientId=${identityClientId};Authorization=AAD' 20 | 21 | module api '../core/host/functions-flexconsumption.bicep' = { 22 | name: '${serviceName}-functions-module' 23 | params: { 24 | name: name 25 | location: location 26 | tags: union(tags, { 'azd-service-name': serviceName }) 27 | identityType: 'UserAssigned' 28 | identityId: identityId 29 | identityClientId: identityClientId 30 | appSettings: union(appSettings, 31 | { 32 | AzureWebJobsStorage__clientId : identityClientId 33 | APPLICATIONINSIGHTS_AUTHENTICATION_STRING: applicationInsightsIdentity 34 | AZURE_OPENAI_ENDPOINT: aiServiceUrl 35 | AZURE_CLIENT_ID: identityClientId 36 | }) 37 | applicationInsightsName: applicationInsightsName 38 | appServicePlanId: appServicePlanId 39 | runtimeName: runtimeName 40 | runtimeVersion: runtimeVersion 41 | storageAccountName: storageAccountName 42 | deploymentStorageContainerName: deploymentStorageContainerName 43 | virtualNetworkSubnetId: virtualNetworkSubnetId 44 | instanceMemoryMB: instanceMemoryMB 45 | maximumInstanceCount: maximumInstanceCount 46 | } 47 | } 48 | 49 | output SERVICE_API_NAME string = api.outputs.name 50 | output SERVICE_API_URI string = api.outputs.uri 51 | output SERVICE_API_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId 52 | -------------------------------------------------------------------------------- /infra/app/eventgrid.bicep: -------------------------------------------------------------------------------- 1 | param location string = resourceGroup().location 2 | param tags object = {} 3 | param storageAccountId string 4 | 5 | resource unprocessedPdfSystemTopic 'Microsoft.EventGrid/systemTopics@2024-06-01-preview' = { 6 | name: 'unprocessed-pdf-topic' 7 | location: location 8 | tags: tags 9 | properties: { 10 | source: storageAccountId 11 | topicType: 'Microsoft.Storage.StorageAccounts' 12 | } 13 | } 14 | 15 | // The actual event grid subscription will be created in the post deployment script as it needs the function to be deployed first 16 | 17 | // resource unprocessedPdfSystemTopicSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2024-06-01-preview' = { 18 | // parent: unprocessedPdfSystemTopic 19 | // name: 'unprocessed-pdf-topic-subscription' 20 | // properties: { 21 | // destination: { 22 | // endpointType: 'WebHook' 23 | // properties: { 24 | // //Will be set on post-deployment script once the function is created and the blobs extension code is available 25 | // //endpointUrl: 'https://${function_app_blob_event_grid_name}.azurewebsites.net/runtime/webhooks/blobs?functionName=Host.Functions.Trigger_BlobEventGrid&code=${blobs_extension}' 26 | // } 27 | // } 28 | // filter: { 29 | // includedEventTypes: [ 30 | // 'Microsoft.Storage.BlobCreated' 31 | // ] 32 | // subjectBeginsWith: '/blobServices/default/containers/${unprocessedPdfContainerName}/' 33 | // } 34 | // } 35 | // } 36 | 37 | output unprocessedPdfSystemTopicId string = unprocessedPdfSystemTopic.id 38 | output unprocessedPdfSystemTopicName string = unprocessedPdfSystemTopic.name 39 | -------------------------------------------------------------------------------- /infra/app/storage-Access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param principalType string = 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 3 | param roleDefinitionID string 4 | param storageAccountName string 5 | 6 | resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = { 7 | name: storageAccountName 8 | } 9 | 10 | // Allow access from API to storage account using a managed identity and least priv Storage roles 11 | resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 12 | name: guid(storageAccount.id, principalID, roleDefinitionID) 13 | scope: storageAccount 14 | properties: { 15 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 16 | principalId: principalID 17 | principalType: principalType 18 | } 19 | } 20 | 21 | output ROLE_ASSIGNMENT_NAME string = storageRoleAssignment.name 22 | -------------------------------------------------------------------------------- /infra/app/storage-PrivateEndpoint.bicep: -------------------------------------------------------------------------------- 1 | // Parameters 2 | @description('Specifies the name of the virtual network.') 3 | param virtualNetworkName string 4 | 5 | @description('Specifies the name of the subnet which contains the virtual machine.') 6 | param subnetName string 7 | 8 | @description('Specifies the resource name of the Storage resource with an endpoint.') 9 | param resourceName string 10 | 11 | @description('Specifies the location.') 12 | param location string = resourceGroup().location 13 | 14 | param tags object = {} 15 | 16 | // Virtual Network 17 | resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' existing = { 18 | name: virtualNetworkName 19 | } 20 | 21 | resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { 22 | name: resourceName 23 | } 24 | 25 | var blobPrivateDNSZoneName = format('privatelink.blob.{0}', environment().suffixes.storage) 26 | var blobPrivateDnsZoneVirtualNetworkLinkName = format('{0}-link-{1}', resourceName, take(toLower(uniqueString(resourceName, virtualNetworkName)), 4)) 27 | 28 | // Private DNS Zones 29 | resource blobPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { 30 | name: blobPrivateDNSZoneName 31 | location: 'global' 32 | tags: tags 33 | properties: {} 34 | dependsOn: [ 35 | vnet 36 | ] 37 | } 38 | 39 | // Virtual Network Links 40 | resource blobPrivateDnsZoneVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { 41 | parent: blobPrivateDnsZone 42 | name: blobPrivateDnsZoneVirtualNetworkLinkName 43 | location: 'global' 44 | tags: tags 45 | properties: { 46 | registrationEnabled: false 47 | virtualNetwork: { 48 | id: vnet.id 49 | } 50 | } 51 | } 52 | 53 | // Private Endpoints 54 | resource blobPrivateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { 55 | name: 'blob-private-endpoint' 56 | location: location 57 | tags: tags 58 | properties: { 59 | privateLinkServiceConnections: [ 60 | { 61 | name: 'blobPrivateLinkConnection' 62 | properties: { 63 | privateLinkServiceId: storageAccount.id 64 | groupIds: [ 65 | 'blob' 66 | ] 67 | } 68 | } 69 | ] 70 | subnet: { 71 | id: '${vnet.id}/subnets/${subnetName}' 72 | } 73 | } 74 | } 75 | 76 | resource blobPrivateDnsZoneGroupName 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-01-01' = { 77 | parent: blobPrivateEndpoint 78 | name: 'blobPrivateDnsZoneGroup' 79 | properties: { 80 | privateDnsZoneConfigs: [ 81 | { 82 | name: 'storageBlobARecord' 83 | properties: { 84 | privateDnsZoneId: blobPrivateDnsZone.id 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /infra/app/vnet.bicep: -------------------------------------------------------------------------------- 1 | @description('Specifies the name of the virtual network.') 2 | param vNetName string 3 | 4 | @description('Specifies the location.') 5 | param location string = resourceGroup().location 6 | 7 | @description('Specifies the name of the subnet for the Service Bus private endpoint.') 8 | param peSubnetName string = 'private-endpoints-subnet' 9 | 10 | @description('Specifies the name of the subnet for Function App virtual network integration.') 11 | param appSubnetName string = 'app' 12 | 13 | param tags object = {} 14 | 15 | resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = { 16 | name: vNetName 17 | location: location 18 | tags: tags 19 | properties: { 20 | addressSpace: { 21 | addressPrefixes: [ 22 | '10.0.0.0/16' 23 | ] 24 | } 25 | encryption: { 26 | enabled: false 27 | enforcement: 'AllowUnencrypted' 28 | } 29 | subnets: [ 30 | { 31 | name: peSubnetName 32 | id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'private-endpoints-subnet') 33 | properties: { 34 | addressPrefixes: [ 35 | '10.0.1.0/24' 36 | ] 37 | delegations: [] 38 | privateEndpointNetworkPolicies: 'Disabled' 39 | privateLinkServiceNetworkPolicies: 'Enabled' 40 | } 41 | type: 'Microsoft.Network/virtualNetworks/subnets' 42 | } 43 | { 44 | name: appSubnetName 45 | id: resourceId('Microsoft.Network/virtualNetworks/subnets', vNetName, 'app') 46 | properties: { 47 | addressPrefixes: [ 48 | '10.0.2.0/24' 49 | ] 50 | delegations: [ 51 | { 52 | name: 'delegation' 53 | id: resourceId('Microsoft.Network/virtualNetworks/subnets/delegations', vNetName, 'app', 'delegation') 54 | properties: { 55 | //Microsoft.App/environments is the correct delegation for Flex Consumption VNet integration 56 | serviceName: 'Microsoft.App/environments' 57 | } 58 | type: 'Microsoft.Network/virtualNetworks/subnets/delegations' 59 | } 60 | ] 61 | privateEndpointNetworkPolicies: 'Disabled' 62 | privateLinkServiceNetworkPolicies: 'Enabled' 63 | } 64 | type: 'Microsoft.Network/virtualNetworks/subnets' 65 | } 66 | ] 67 | virtualNetworkPeerings: [] 68 | enableDdosProtection: false 69 | } 70 | } 71 | 72 | output peSubnetName string = virtualNetwork.properties.subnets[0].name 73 | output peSubnetID string = virtualNetwork.properties.subnets[0].id 74 | output appSubnetName string = virtualNetwork.properties.subnets[1].name 75 | output appSubnetID string = virtualNetwork.properties.subnets[1].id 76 | -------------------------------------------------------------------------------- /infra/core/ai/openai.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param customSubDomainName string = name 6 | param deployments array = [] 7 | param kind string = 'OpenAI' 8 | param publicNetworkAccess string = 'Enabled' 9 | param sku object = { 10 | name: 'S0' 11 | } 12 | 13 | resource account 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { 14 | name: name 15 | location: location 16 | tags: tags 17 | kind: kind 18 | properties: { 19 | customSubDomainName: name 20 | networkAcls : { 21 | defaultAction: publicNetworkAccess == 'Enabled' ? 'Allow' : 'Deny' 22 | virtualNetworkRules: [] 23 | ipRules: [] 24 | } 25 | publicNetworkAccess: publicNetworkAccess 26 | } 27 | sku: sku 28 | } 29 | 30 | @batchSize(1) 31 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 32 | parent: account 33 | name: deployment.name 34 | sku: { 35 | name: 'Standard' 36 | capacity: deployment.capacity 37 | } 38 | properties: { 39 | model: deployment.model 40 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 41 | } 42 | }] 43 | 44 | output endpoint string = account.properties.endpoint 45 | output id string = account.id 46 | output name string = account.name 47 | output location string = account.location 48 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param kind string = '' 6 | param reserved bool = true 7 | param sku object 8 | 9 | resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | sku: sku 14 | kind: kind 15 | properties: { 16 | reserved: reserved 17 | } 18 | } 19 | 20 | output id string = appServicePlan.id 21 | -------------------------------------------------------------------------------- /infra/core/host/functions-flexconsumption.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | // Reference Properties 6 | param applicationInsightsName string = '' 7 | param appServicePlanId string 8 | param storageAccountName string 9 | param virtualNetworkSubnetId string = '' 10 | @allowed(['SystemAssigned', 'UserAssigned']) 11 | param identityType string 12 | @description('User assigned identity name') 13 | param identityId string 14 | @description('User assigned identity client id') 15 | param identityClientId string 16 | 17 | // Runtime Properties 18 | @allowed([ 19 | 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 20 | ]) 21 | param runtimeName string 22 | @allowed(['3.10', '3.11', '7.4', '8.0', '10', '11', '17', '20']) 23 | param runtimeVersion string 24 | param kind string = 'functionapp,linux' 25 | 26 | // Microsoft.Web/sites/config 27 | param appSettings object = {} 28 | param instanceMemoryMB int = 2048 29 | param maximumInstanceCount int = 100 30 | param deploymentStorageContainerName string 31 | 32 | resource stg 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { 33 | name: storageAccountName 34 | } 35 | 36 | resource functions 'Microsoft.Web/sites@2023-12-01' = { 37 | name: name 38 | location: location 39 | tags: tags 40 | kind: kind 41 | identity: { 42 | type: identityType 43 | userAssignedIdentities: { 44 | '${identityId}': {} 45 | } 46 | } 47 | properties: { 48 | serverFarmId: appServicePlanId 49 | functionAppConfig: { 50 | deployment: { 51 | storage: { 52 | type: 'blobContainer' 53 | value: '${stg.properties.primaryEndpoints.blob}${deploymentStorageContainerName}' 54 | authentication: { 55 | type: identityType == 'SystemAssigned' ? 'SystemAssignedIdentity' : 'UserAssignedIdentity' 56 | userAssignedIdentityResourceId: identityType == 'UserAssigned' ? identityId : '' 57 | } 58 | } 59 | } 60 | scaleAndConcurrency: { 61 | instanceMemoryMB: instanceMemoryMB 62 | maximumInstanceCount: maximumInstanceCount 63 | } 64 | runtime: { 65 | name: runtimeName 66 | version: runtimeVersion 67 | } 68 | } 69 | virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null 70 | } 71 | 72 | resource configAppSettings 'config' = { 73 | name: 'appsettings' 74 | properties: union(appSettings, 75 | { 76 | AzureWebJobsStorage__blobServiceUri: stg.properties.primaryEndpoints.blob 77 | AzureWebJobsStorage__tableServiceUri: stg.properties.primaryEndpoints.table 78 | AzureWebJobsStorage__queueServiceUri: stg.properties.primaryEndpoints.queue 79 | AzureWebJobsStorage__credential : 'managedidentity' 80 | AzureWebJobsStorage__clientId : identityClientId 81 | APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString 82 | }) 83 | } 84 | } 85 | 86 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 87 | name: applicationInsightsName 88 | } 89 | 90 | output name string = functions.name 91 | output uri string = 'https://${functions.properties.defaultHostName}' 92 | output identityPrincipalId string = identityType == 'SystemAssigned' ? functions.identity.principalId : '' 93 | -------------------------------------------------------------------------------- /infra/core/identity/userAssignedIdentity.bicep: -------------------------------------------------------------------------------- 1 | param identityName string 2 | param location string 3 | param tags object = {} 4 | 5 | resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { 6 | name: identityName 7 | location: location 8 | tags: tags 9 | } 10 | 11 | output identityId string = userAssignedIdentity.id 12 | output identityName string = userAssignedIdentity.name 13 | output identityPrincipalId string = userAssignedIdentity.properties.principalId 14 | output identityClientId string = userAssignedIdentity.properties.clientId 15 | -------------------------------------------------------------------------------- /infra/core/monitor/appinsights-access.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param roleDefinitionID string 3 | param appInsightsName string 4 | 5 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { 6 | name: appInsightsName 7 | } 8 | 9 | // Allow access from API to app insights using a managed identity and least priv role 10 | resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = { 11 | name: guid(applicationInsights.id, principalID, roleDefinitionID) 12 | scope: applicationInsights 13 | properties: { 14 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionID) 15 | principalId: principalID 16 | principalType: 'ServicePrincipal' // Workaround for https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-template#new-service-principal 17 | } 18 | } 19 | 20 | output ROLE_ASSIGNMENT_NAME string = appInsightsRoleAssignment.name 21 | 22 | -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param logAnalyticsWorkspaceId string 6 | param disableLocalAuth bool = false 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | DisableLocalAuth: disableLocalAuth 17 | } 18 | } 19 | 20 | output connectionString string = applicationInsights.properties.ConnectionString 21 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 22 | output name string = applicationInsights.name 23 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | properties: any({ 10 | retentionInDays: 30 11 | features: { 12 | searchVersion: 1 13 | } 14 | sku: { 15 | name: 'PerGB2018' 16 | } 17 | }) 18 | } 19 | 20 | output id string = logAnalytics.id 21 | output name string = logAnalytics.name 22 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param logAnalyticsName string 2 | param applicationInsightsName string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | param disableLocalAuth bool = false 6 | 7 | module logAnalytics 'loganalytics.bicep' = { 8 | name: 'loganalytics' 9 | params: { 10 | name: logAnalyticsName 11 | location: location 12 | tags: tags 13 | } 14 | } 15 | 16 | module applicationInsights 'applicationinsights.bicep' = { 17 | name: 'applicationinsights' 18 | params: { 19 | name: applicationInsightsName 20 | location: location 21 | tags: tags 22 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 23 | disableLocalAuth: disableLocalAuth 24 | } 25 | } 26 | 27 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 28 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 29 | output applicationInsightsName string = applicationInsights.outputs.name 30 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 31 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 32 | -------------------------------------------------------------------------------- /infra/core/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param allowBlobPublicAccess bool = false 6 | param containers array = [] 7 | param kind string = 'StorageV2' 8 | param minimumTlsVersion string = 'TLS1_2' 9 | param sku object = { name: 'Standard_LRS' } 10 | param networkAcls object = { 11 | bypass: 'AzureServices' 12 | defaultAction: 'Allow' 13 | } 14 | 15 | resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = { 16 | name: name 17 | location: location 18 | tags: tags 19 | kind: kind 20 | sku: sku 21 | properties: { 22 | minimumTlsVersion: minimumTlsVersion 23 | allowBlobPublicAccess: allowBlobPublicAccess 24 | allowSharedKeyAccess: false 25 | networkAcls: networkAcls 26 | } 27 | 28 | resource blobServices 'blobServices' = if (!empty(containers)) { 29 | name: 'default' 30 | resource container 'containers' = [for container in containers: { 31 | name: container.name 32 | properties: { 33 | publicAccess: container.?publicAccess ?? 'None' 34 | } 35 | }] 36 | } 37 | } 38 | 39 | output name string = storage.name 40 | output primaryEndpoints object = storage.properties.primaryEndpoints 41 | output id string = storage.id 42 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | @allowed(['australiaeast', 'eastasia', 'eastus', 'northeurope', 'southcentralus', 'southeastasia', 'uksouth', 'westus2']) 11 | @metadata({ 12 | azd: { 13 | type: 'location' 14 | } 15 | }) 16 | param location string 17 | param skipVnet bool = true 18 | param apiServiceName string = '' 19 | param apiUserAssignedIdentityName string = '' 20 | param applicationInsightsName string = '' 21 | param appServicePlanName string = '' 22 | param logAnalyticsName string = '' 23 | param resourceGroupName string = '' 24 | param storageAccountName string = '' 25 | param vNetName string = '' 26 | param disableLocalAuth bool = true 27 | 28 | @allowed([ 'consumption', 'flexconsumption' ]) 29 | param azFunctionHostingPlanType string = 'flexconsumption' 30 | 31 | param openAiServiceName string = '' 32 | 33 | param openAiSkuName string 34 | @allowed([ 'azure', 'openai', 'azure_custom' ]) 35 | param openAiHost string // Set in main.parameters.json 36 | 37 | param chatModelName string = '' 38 | param chatDeploymentName string = '' 39 | param chatDeploymentVersion string = '' 40 | param chatDeploymentCapacity int = 0 41 | 42 | var chatModel = { 43 | modelName: !empty(chatModelName) ? chatModelName : startsWith(openAiHost, 'azure') ? 'gpt-4o' : 'gpt-4o' 44 | deploymentName: !empty(chatDeploymentName) ? chatDeploymentName : 'chat' 45 | deploymentVersion: !empty(chatDeploymentVersion) ? chatDeploymentVersion : '2024-08-06' 46 | deploymentCapacity: chatDeploymentCapacity != 0 ? chatDeploymentCapacity : 40 47 | } 48 | 49 | @description('Id of the user or app to assign application roles') 50 | param principalId string = '' 51 | 52 | var abbrs = loadJsonContent('./abbreviations.json') 53 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 54 | var tags = { 'azd-env-name': environmentName } 55 | var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' 56 | var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' 57 | 58 | // Organize resources in a resource group 59 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 60 | name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' 61 | location: location 62 | tags: tags 63 | } 64 | 65 | // User assigned managed identity to be used by the function app to reach storage and service bus 66 | module apiUserAssignedIdentity './core/identity/userAssignedIdentity.bicep' = { 67 | name: 'apiUserAssignedIdentity' 68 | scope: rg 69 | params: { 70 | location: location 71 | tags: tags 72 | identityName: !empty(apiUserAssignedIdentityName) ? apiUserAssignedIdentityName : '${abbrs.managedIdentityUserAssignedIdentities}api-${resourceToken}' 73 | } 74 | } 75 | 76 | // The application backend is a function app 77 | module appServicePlan './core/host/appserviceplan.bicep' = { 78 | name: 'appserviceplan' 79 | scope: rg 80 | params: { 81 | name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' 82 | location: location 83 | tags: tags 84 | sku: { 85 | name: 'FC1' 86 | tier: 'FlexConsumption' 87 | } 88 | } 89 | } 90 | 91 | module api './app/api.bicep' = { 92 | name: 'api' 93 | scope: rg 94 | params: { 95 | name: functionAppName 96 | location: location 97 | tags: tags 98 | applicationInsightsName: monitoring.outputs.applicationInsightsName 99 | appServicePlanId: appServicePlan.outputs.id 100 | runtimeName: 'python' 101 | runtimeVersion: '3.11' 102 | storageAccountName: storage.outputs.name 103 | deploymentStorageContainerName: deploymentStorageContainerName 104 | identityId: apiUserAssignedIdentity.outputs.identityId 105 | identityClientId: apiUserAssignedIdentity.outputs.identityClientId 106 | appSettings: { 107 | CHAT_MODEL_DEPLOYMENT_NAME: chatModel.deploymentName 108 | } 109 | virtualNetworkSubnetId: skipVnet ? '' : serviceVirtualNetwork.outputs.appSubnetID 110 | aiServiceUrl: ai.outputs.endpoint 111 | } 112 | } 113 | 114 | module ai 'core/ai/openai.bicep' = { 115 | name: 'openai' 116 | scope: rg 117 | params: { 118 | name: !empty(openAiServiceName) ? openAiServiceName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' 119 | location: location 120 | tags: tags 121 | publicNetworkAccess: skipVnet == 'false' ? 'Disabled' : 'Enabled' 122 | sku: { 123 | name: openAiSkuName 124 | } 125 | deployments: [ 126 | { 127 | name: chatModel.deploymentName 128 | capacity: chatModel.deploymentCapacity 129 | model: { 130 | format: 'OpenAI' 131 | name: chatModel.modelName 132 | version: chatModel.deploymentVersion 133 | } 134 | scaleSettings: { 135 | scaleType: 'Standard' 136 | } 137 | } 138 | ] 139 | } 140 | } 141 | 142 | // Backing storage for Azure functions backend processor 143 | module storage 'core/storage/storage-account.bicep' = { 144 | name: 'storage' 145 | scope: rg 146 | params: { 147 | name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' 148 | location: location 149 | tags: tags 150 | containers: [ 151 | {name: deploymentStorageContainerName} 152 | ] 153 | networkAcls: skipVnet ? {} : { 154 | defaultAction: 'Deny' 155 | } 156 | } 157 | } 158 | 159 | var storageRoleDefinitionId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b' // Storage Blob Data Owner role 160 | 161 | // Allow access from api to storage account using a managed identity 162 | module storageRoleAssignmentApi 'app/storage-Access.bicep' = { 163 | name: 'storageRoleAssignmentapi' 164 | scope: rg 165 | params: { 166 | storageAccountName: storage.outputs.name 167 | roleDefinitionID: storageRoleDefinitionId 168 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 169 | principalType: 'ServicePrincipal' 170 | } 171 | } 172 | 173 | module storageRoleAssignmentUserIdentityApi 'app/storage-Access.bicep' = { 174 | name: 'storageRoleAssignmentUserIdentityApi' 175 | scope: rg 176 | params: { 177 | storageAccountName: storage.outputs.name 178 | roleDefinitionID: storageRoleDefinitionId 179 | principalID: principalId 180 | principalType: 'User' 181 | } 182 | } 183 | 184 | var storageQueueDataContributorRoleDefinitionId = '974c5e8b-45b9-4653-ba55-5f855dd0fb88' // Storage Queue Data Contributor 185 | 186 | module storageQueueDataContributorRoleAssignmentprocessor 'app/storage-Access.bicep' = { 187 | name: 'storageQueueDataContributorRoleAssignmentprocessor' 188 | scope: rg 189 | params: { 190 | storageAccountName: storage.outputs.name 191 | roleDefinitionID: storageQueueDataContributorRoleDefinitionId 192 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 193 | principalType: 'ServicePrincipal' 194 | } 195 | } 196 | 197 | module storageQueueDataContributorRoleAssignmentUserIdentityprocessor 'app/storage-Access.bicep' = { 198 | name: 'storageQueueDataContributorRoleAssignmentUserIdentityprocessor' 199 | scope: rg 200 | params: { 201 | storageAccountName: storage.outputs.name 202 | roleDefinitionID: storageQueueDataContributorRoleDefinitionId 203 | principalID: principalId 204 | principalType: 'User' 205 | } 206 | } 207 | 208 | var storageTableDataContributorRoleDefinitionId = '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3' // Storage Table Data Contributor 209 | 210 | module storageTableDataContributorRoleAssignmentprocessor 'app/storage-Access.bicep' = { 211 | name: 'storageTableDataContributorRoleAssignmentprocessor' 212 | scope: rg 213 | params: { 214 | storageAccountName: storage.outputs.name 215 | roleDefinitionID: storageTableDataContributorRoleDefinitionId 216 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 217 | principalType: 'ServicePrincipal' 218 | } 219 | } 220 | 221 | module storageTableDataContributorRoleAssignmentUserIdentityprocessor 'app/storage-Access.bicep' = { 222 | name: 'storageTableDataContributorRoleAssignmentUserIdentityprocessor' 223 | scope: rg 224 | params: { 225 | storageAccountName: storage.outputs.name 226 | roleDefinitionID: storageTableDataContributorRoleDefinitionId 227 | principalID: principalId 228 | principalType: 'User' 229 | } 230 | } 231 | 232 | var cogRoleDefinitionId = 'a97b65f3-24c7-4388-baec-2e87135dc908' // Cognitive Services User 233 | 234 | // Allow access from api to storage account using a managed identity 235 | module cogRoleAssignmentApi 'app/ai-Cog-Service-Access.bicep' = { 236 | name: 'cogRoleAssignmentapi' 237 | scope: rg 238 | params: { 239 | aiResourceName: ai.outputs.name 240 | roleDefinitionID: cogRoleDefinitionId 241 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 242 | principalType: 'ServicePrincipal' 243 | } 244 | } 245 | 246 | module cogRoleAssignmentUserIdentityApi 'app/ai-Cog-Service-Access.bicep' = { 247 | name: 'cogRoleAssignmentUserIdentityApi' 248 | scope: rg 249 | params: { 250 | aiResourceName: ai.outputs.name 251 | roleDefinitionID: cogRoleDefinitionId 252 | principalID: principalId 253 | principalType: 'User' 254 | } 255 | } 256 | 257 | // Virtual Network & private endpoint to blob storage 258 | module serviceVirtualNetwork 'app/vnet.bicep' = if (!skipVnet) { 259 | name: 'serviceVirtualNetwork' 260 | scope: rg 261 | params: { 262 | location: location 263 | tags: tags 264 | vNetName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' 265 | } 266 | } 267 | 268 | module storagePrivateEndpoint 'app/storage-PrivateEndpoint.bicep' = if (!skipVnet) { 269 | name: 'servicePrivateEndpoint' 270 | scope: rg 271 | params: { 272 | location: location 273 | tags: tags 274 | virtualNetworkName: !empty(vNetName) ? vNetName : '${abbrs.networkVirtualNetworks}${resourceToken}' 275 | subnetName: skipVnet ? '' : serviceVirtualNetwork.outputs.peSubnetName 276 | resourceName: storage.outputs.name 277 | } 278 | } 279 | 280 | // Monitor application with Azure Monitor 281 | module monitoring './core/monitor/monitoring.bicep' = { 282 | name: 'monitoring' 283 | scope: rg 284 | params: { 285 | location: location 286 | tags: tags 287 | logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' 288 | applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' 289 | disableLocalAuth: disableLocalAuth 290 | } 291 | } 292 | 293 | var monitoringRoleDefinitionId = '3913510d-42f4-4e42-8a64-420c390055eb' // Monitoring Metrics Publisher role ID 294 | 295 | // Allow access from api to application insights using a managed identity 296 | module appInsightsRoleAssignmentApi './core/monitor/appinsights-access.bicep' = { 297 | name: 'appInsightsRoleAssignmentapi' 298 | scope: rg 299 | params: { 300 | appInsightsName: monitoring.outputs.applicationInsightsName 301 | roleDefinitionID: monitoringRoleDefinitionId 302 | principalID: apiUserAssignedIdentity.outputs.identityPrincipalId 303 | } 304 | } 305 | 306 | // App outputs 307 | output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString 308 | output AZURE_LOCATION string = location 309 | output AZURE_TENANT_ID string = tenant().tenantId 310 | output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME 311 | output SERVICE_API_URI string = api.outputs.SERVICE_API_URI 312 | output AZURE_FUNCTION_APP_NAME string = api.outputs.SERVICE_API_NAME 313 | output RESOURCE_GROUP string = rg.name 314 | output AZURE_OPENAI_ENDPOINT string = ai.outputs.endpoint 315 | -------------------------------------------------------------------------------- /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 | "skipVnet": { 6 | "value": "${SKIP_VNET}=true" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "location": { 12 | "value": "${AZURE_LOCATION}" 13 | }, 14 | "principalId": { 15 | "value": "${AZURE_PRINCIPAL_ID}" 16 | }, 17 | "openAiSkuName": { 18 | "value": "S0" 19 | }, 20 | "openAiServiceName": { 21 | "value": "${AZURE_OPENAI_SERVICE}" 22 | }, 23 | "openAiHost": { 24 | "value": "${OPENAI_HOST=azure}" 25 | }, 26 | "openAiResourceGroupName": { 27 | "value": "${AZURE_OPENAI_RESOURCE_GROUP}" 28 | }, 29 | "chatGptDeploymentName": { 30 | "value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT=chat}" 31 | }, 32 | "chatGptDeploymentCapacity":{ 33 | "value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY=40}" 34 | }, 35 | "chatGptDeploymentVersion":{ 36 | "value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION=2024-08-06}" 37 | }, 38 | "chatGptModelName":{ 39 | "value": "${AZURE_OPENAI_CHATGPT_MODEL=gpt-4o}" 40 | }, 41 | "embeddingDeploymentName": { 42 | "value": "${AZURE_OPENAI_EMB_DEPLOYMENT=embedding}" 43 | }, 44 | "embeddingModelName":{ 45 | "value": "${AZURE_OPENAI_EMB_MODEL_NAME=text-embedding-3-small}" 46 | }, 47 | "embeddingDeploymentVersion":{ 48 | "value": "${AZURE_OPENAI_EMB_DEPLOYMENT_VERSION}" 49 | }, 50 | "embeddingDeploymentCapacity":{ 51 | "value": "${AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY}" 52 | }, 53 | "searchServiceName": { 54 | "value": "${AZURE_SEARCH_SERVICE}" 55 | }, 56 | "searchServiceResourceGroupName": { 57 | "value": "${AZURE_SEARCH_SERVICE_RESOURCE_GROUP}" 58 | }, 59 | "searchServiceIndexName": { 60 | "value": "${AZURE_SEARCH_INDEX=openai-index}" 61 | }, 62 | "searchServiceSkuName": { 63 | "value": "${AZURE_SEARCH_SERVICE_SKU=standard}" 64 | }, 65 | "storageAccountName": { 66 | "value": "${AZURE_STORAGE_ACCOUNT}" 67 | }, 68 | "storageResourceGroupName": { 69 | "value": "${AZURE_STORAGE_RESOURCE_GROUP}" 70 | }, 71 | "azFunctionHostingPlanType": { 72 | "value": "flexconsumption" 73 | }, 74 | "systemPrompt": { 75 | "value": "${SYSTEM_PROMPT}=You are a helpful assistant. You are responding to requests from a user about internal emails and documents. You can and should refer to the internal documents to help respond to requests. If a user makes a request thats not covered by the documents provided in the query, you must say that you do not have access to the information and not try and get information from other places besides the documents provided. The following is a list of documents that you can refer to when answering questions. The documents are in the format [filename]: [text] and are separated by newlines. If you answer a question by referencing any of the documents, please cite the document in your answer. For example, if you answer a question by referencing info.txt, you should add \"Reference: info.txt\" to the end of your answer on a separate line." 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Do not include azure-functions-worker in this file 2 | # The Python Worker is managed by the Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions>=1.22.0b2 6 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | ### Simple Whois Completion(Local) 2 | GET http://localhost:7071/api/whois/Turing HTTP/1.1 3 | 4 | ### Simple Ask Completion (Local) 5 | POST http://localhost:7071/api/ask HTTP/1.1 6 | content-type: application/json 7 | 8 | { 9 | "prompt": "Tell me two most popular programming features of Azure Functions" 10 | } 11 | 12 | ### Simple Ask Completion (Cloud) 13 | ### .gitignore this file if and when you set key 14 | POST https://.azurewebsites.net/api/ask HTTP/1.1 15 | content-type: application/json 16 | x-functions-key: 17 | 18 | { 19 | "prompt": "Tell me two most popular programming features of Azure Functions" 20 | } 21 | 22 | ### Stateful Chatbot 23 | 24 | ### CreateChatBot 25 | PUT http://localhost:7071/api/chats/abc123 26 | Content-Type: application/json 27 | 28 | { 29 | "name": "Sample ChatBot", 30 | "description": "This is a sample chatbot." 31 | } 32 | 33 | ### PostChat 34 | POST http://localhost:7071/api/chats/abc123 35 | Content-Type: application/json 36 | 37 | { 38 | "message": "Hello, how can I assist you today?" 39 | } 40 | 41 | ### PostChat 42 | POST http://localhost:7071/api/chats/abc123 43 | Content-Type: application/json 44 | 45 | { 46 | "message": "Need help with directions from Redmond to SeaTac?" 47 | } 48 | 49 | ### GetChatState 50 | GET http://localhost:7071/api/chats/abc123?timestampUTC=2024-01-15T22:00:00 51 | Content-Type: application/json 52 | -------------------------------------------------------------------------------- /testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "prompt": "Write a poem about Azure Functions. Include two reasons why users love them." 3 | } --------------------------------------------------------------------------------