├── .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 | [](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 | }
--------------------------------------------------------------------------------