Skip to content

Commit 6ba329f

Browse files
authored
Merge pull request #87 from Azure-Samples/template-lab-improvs
Improvements in the backend pool load balancing lab to segregate apim…
2 parents 88d3f44 + 0225d33 commit 6ba329f

File tree

12 files changed

+501
-408
lines changed

12 files changed

+501
-408
lines changed

.github/workflows/nbchecks.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# to run locally, use the following command in the root folder of the repository:python .github/workflows/nbchecks.py
2+
3+
import json, sys, os
4+
from pathlib import Path
5+
6+
def has_outputs_stored(file):
7+
with open(file, 'r', encoding='utf-8') as f:
8+
notebook_content = json.load(f)
9+
10+
for cell in notebook_content.get('cells', []):
11+
if cell.get('cell_type') == 'code' and cell.get('outputs'):
12+
print(f"The notebook {filename} has outputs stored.", file=sys.stderr)
13+
return True
14+
return False
15+
16+
exit_code = 0
17+
for file in Path(".").glob('**/*.ipynb'):
18+
filename = os.fsdecode(file)
19+
if has_outputs_stored(file):
20+
exit_code = 1
21+
if exit_code == 0:
22+
print("All good. No stored output found.")
23+
sys.exit(exit_code)

.github/workflows/nbchecks.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: notebook-checks
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "**/*.ipynb"
7+
branches: [main]
8+
push:
9+
paths:
10+
- "**/*.ipynb"
11+
branches: [main]
12+
13+
permissions:
14+
contents: read
15+
pull-requests: write
16+
17+
jobs:
18+
nbchecks:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Set up Python 3.12
22+
uses: actions/setup-python@v1
23+
with:
24+
python-version: "3.12"
25+
- uses: actions/checkout@v1
26+
- name: nbchecks
27+
run: |
28+
python ./.github/workflows/nbchecks.py

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## What's new ✨
88

9-
<!-- ➕ the [**AI Foundry SDK**](labs/ai-foundry-sdk/ai-foundry-sdk.ipynb) lab. -->
9+
➕ the [**AI Foundry SDK**](labs/ai-foundry-sdk/ai-foundry-sdk.ipynb) lab.
1010
➕ the [**Content filtering**](labs/content-filtering/content-filtering.ipynb) and [**Prompt shielding**](labs/content-filtering/prompt-shielding.ipynb) labs.
1111
➕ the [**Model routing**](labs/model-routing/model-routing.ipynb) lab with OpenAI model based routing.
1212
➕ the [**Prompt flow**](labs/prompt-flow/prompt-flow.ipynb) lab to try the [Azure AI Studio Prompt Flow](https://learn.microsoft.com/azure/ai-studio/how-to/prompt-flow) with Azure API Management.

labs/backend-pool-load-balancing/backend-pool-load-balancing.ipynb

Lines changed: 56 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"\n",
2222
"### TOC\n",
2323
"- [0️⃣ Initialize notebook variables](#0)\n",
24-
"- [1️⃣ Create the Azure Resource Group](#1)\n",
24+
"- [1️⃣ Verify the Azure CLI and the connected Azure subscription](#1)\n",
2525
"- [2️⃣ Create deployment using 🦾 Bicep](#2)\n",
2626
"- [3️⃣ Get the deployment outputs](#3)\n",
2727
"- [🧪 Test the API using a direct HTTP call](#requests)\n",
@@ -53,35 +53,43 @@
5353
},
5454
{
5555
"cell_type": "code",
56-
"execution_count": 1,
56+
"execution_count": null,
5757
"metadata": {},
5858
"outputs": [],
5959
"source": [
60-
"import os\n",
60+
"import os, sys, json\n",
61+
"sys.path.insert(1, '../../shared') # add the shared directory to the Python path\n",
62+
"import utils\n",
6163
"\n",
6264
"deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n",
6365
"resource_group_name = f\"lab-{deployment_name}\" # change the name to match your naming style\n",
6466
"resource_group_location = \"westeurope\"\n",
6567
"\n",
68+
"apim_sku = 'Basicv2'\n",
69+
"\n",
6670
"openai_resources = [\n",
6771
" {\"name\": \"openai1\", \"location\": \"uksouth\", \"priority\": 1, \"weight\": 80},\n",
6872
" {\"name\": \"openai2\", \"location\": \"swedencentral\", \"priority\": 1, \"weight\": 10},\n",
6973
" {\"name\": \"openai3\", \"location\": \"francecentral\", \"priority\": 1, \"weight\": 10}\n",
7074
"]\n",
7175
"\n",
76+
"openai_deployment_name = \"gpt-35-turbo\"\n",
7277
"openai_model_name = \"gpt-35-turbo\"\n",
7378
"openai_model_version = \"0613\"\n",
74-
"openai_deployment_name = \"gpt-35-turbo\"\n",
75-
"openai_api_version = \"2024-02-01\"\n"
79+
"openai_model_capacity = 8\n",
80+
"openai_api_version = \"2024-02-01\"\n",
81+
"\n",
82+
"utils.print_ok('Notebook initiaized')"
7683
]
7784
},
7885
{
7986
"cell_type": "markdown",
8087
"metadata": {},
8188
"source": [
8289
"<a id='1'></a>\n",
83-
"### 1️⃣ Create the Azure Resource Group\n",
84-
"All resources deployed in this lab will be created in the specified resource group. Skip this step if you want to use an existing resource group."
90+
"### 1️⃣ Verify the Azure CLI and the connected Azure subscription\n",
91+
"\n",
92+
"The following commands ensure that you have the latest version of the Azure CLI and that the Azure CLI is connected to your Azure subscription."
8593
]
8694
},
8795
{
@@ -90,18 +98,11 @@
9098
"metadata": {},
9199
"outputs": [],
92100
"source": [
93-
"# %load ../../shared/snippets/create-az-resource-group.py\n",
94-
"\n",
95-
"# type: ignore\n",
96-
"\n",
97-
"import datetime\n",
98-
"\n",
99-
"resource_group_stdout = ! az group create --name {resource_group_name} --location {resource_group_location}\n",
100-
"\n",
101-
"if resource_group_stdout.n.startswith(\"ERROR\"):\n",
102-
" print(resource_group_stdout)\n",
103-
"else:\n",
104-
" print(f\"✅ Azure Resource Group {resource_group_name} created ⌚ {datetime.datetime.now().time()}\")\n"
101+
"output = utils.run(\"az account show\", \"Retrieved az account\", \"Failed to get the current az account\")\n",
102+
"if output.success and output.json_data:\n",
103+
" current_user = output.json_data['user']['name']\n",
104+
" subscription_id = output.json_data['id']\n",
105+
" tenant_id = output.json_data['tenantId']"
105106
]
106107
},
107108
{
@@ -111,9 +112,9 @@
111112
"<a id='2'></a>\n",
112113
"### 2️⃣ Create deployment using 🦾 Bicep\n",
113114
"\n",
114-
"This lab uses [Bicep](https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep) to declarative define all the resources that will be deployed. Change the parameters or the [main.bicep](main.bicep) directly to try different configurations. \n",
115+
"This lab uses [Bicep](https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep) to declarative define all the resources that will be deployed in the specified resource group. Change the parameters or the [main.bicep](main.bicep) directly to try different configurations. \n",
115116
"\n",
116-
"`openAIModelCapacity` is set intentionally low to `8` (8k tokens per minute) in _main.bicep_ to showcase the retry logic in the load balancer."
117+
"`openAIModelCapacity` is set intentionally low to `8` (8k tokens per minute) to showcase the retry logic in the load balancer."
117118
]
118119
},
119120
{
@@ -122,45 +123,43 @@
122123
"metadata": {},
123124
"outputs": [],
124125
"source": [
125-
"# %load ../../shared/snippets/create-az-deployment.py\n",
126-
"\n",
127-
"# type: ignore\n",
128-
"\n",
129-
"import json\n",
130-
"\n",
131-
"backend_id = \"openai-backend-pool\" if len(openai_resources) > 1 else openai_resources[0].get(\"name\")\n",
126+
"# create the resource group if doesn't exist\n",
127+
"utils.create_resource_group(True, resource_group_name, resource_group_location)\n",
132128
"\n",
129+
"# update the APIM policy file before the deployment\n",
130+
"policy_xml = None\n",
133131
"with open(\"policy.xml\", 'r') as policy_xml_file:\n",
134-
" policy_xml = policy_xml_file.read()\n",
135-
"\n",
136-
" if \"{backend-id}\" in policy_xml:\n",
137-
" policy_xml = policy_xml.replace(\"{backend-id}\", backend_id)\n",
138-
"\n",
139-
" if \"{aad-client-application-id}\" in policy_xml:\n",
140-
" policy_xml = policy_xml.replace(\"{aad-client-application-id}\", client_id)\n",
141-
"\n",
142-
" if \"{aad-tenant-id}\" in policy_xml:\n",
143-
" policy_xml = policy_xml.replace(\"{aad-tenant-id}\", tenant_id)\n",
144-
"\n",
132+
" policy_template_xml = policy_xml_file.read()\n",
133+
" if \"{backend-id}\" in policy_template_xml:\n",
134+
" policy_xml = policy_template_xml.replace(\"{backend-id}\", str(\"openai-backend-pool\" if len(openai_resources) > 1 else openai_resources[0].get(\"name\"))) \n",
145135
" policy_xml_file.close()\n",
146-
"open(\"policy-updated.xml\", 'w').write(policy_xml)\n",
136+
"if policy_xml is not None:\n",
137+
" open(\"policy.xml\", 'w').write(policy_xml)\n",
147138
"\n",
139+
"# define the BICEP parameters\n",
148140
"bicep_parameters = {\n",
149141
" \"$schema\": \"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#\",\n",
150142
" \"contentVersion\": \"1.0.0.0\",\n",
151143
" \"parameters\": {\n",
144+
" \"apimSku\": { \"value\": apim_sku },\n",
152145
" \"openAIConfig\": { \"value\": openai_resources },\n",
153146
" \"openAIDeploymentName\": { \"value\": openai_deployment_name },\n",
154147
" \"openAIModelName\": { \"value\": openai_model_name },\n",
155148
" \"openAIModelVersion\": { \"value\": openai_model_version },\n",
149+
" \"openAIModelCapacity\": { \"value\": openai_model_capacity },\n",
156150
" \"openAIAPIVersion\": { \"value\": openai_api_version }\n",
157151
" }\n",
158152
"}\n",
159153
"\n",
154+
"# write the parameters to a file \n",
160155
"with open('params.json', 'w') as bicep_parameters_file:\n",
161156
" bicep_parameters_file.write(json.dumps(bicep_parameters))\n",
162157
"\n",
163-
"! az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file \"main.bicep\" --parameters \"params.json\"\n"
158+
"# run the deployment\n",
159+
"output = utils.run(f\"az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file main.bicep --parameters params.json\", \n",
160+
" f\"Deployment '{deployment_name}' succeeded\", f\"Deployment '{deployment_name}' failed\")\n",
161+
"open(\"policy.xml\", 'w').write(policy_template_xml)\n",
162+
"\n"
164163
]
165164
},
166165
{
@@ -170,64 +169,22 @@
170169
"<a id='3'></a>\n",
171170
"### 3️⃣ Get the deployment outputs\n",
172171
"\n",
173-
"We are now at the stage where we only need to retrieve the gateway URL and the subscription before we are ready for testing."
172+
"Retrieve the required outputs from the Bicep deployment."
174173
]
175174
},
176175
{
177176
"cell_type": "code",
178-
"execution_count": 1,
177+
"execution_count": null,
179178
"metadata": {},
180179
"outputs": [],
181180
"source": [
182-
"# %load ../../shared/snippets/deployment-outputs.py\n",
183-
"# type: ignore\n",
184-
"\n",
185181
"# Obtain all of the outputs from the deployment\n",
186-
"stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs -o json\n",
187-
"outputs = json.loads(stdout.n)\n",
188-
"\n",
189-
"# Extract the individual properties\n",
190-
"apim_service_id = outputs.get('apimServiceId', {}).get('value', '')\n",
191-
"apim_subscription_key = outputs.get('apimSubscriptionKey', {}).get('value', '')\n",
192-
"apim_subscription1_key = outputs.get('apimSubscription1Key', {}).get('value', '')\n",
193-
"apim_subscription2_key = outputs.get('apimSubscription2Key', {}).get('value', '')\n",
194-
"apim_subscription3_key = outputs.get('apimSubscription3Key', {}).get('value', '')\n",
195-
"apim_resource_gateway_url = outputs.get('apimResourceGatewayURL', {}).get('value', '')\n",
196-
"workspace_id = outputs.get('logAnalyticsWorkspaceId', {}).get('value', '')\n",
197-
"app_id = outputs.get('applicationInsightsAppId', {}).get('value', '')\n",
198-
"function_app_resource_name = outputs.get('functionAppResourceName', {}).get('value', '')\n",
199-
"cosmosdb_connection_string = outputs.get('cosmosDBConnectionString', {}).get('value', '')\n",
200-
"\n",
201-
"# Print the extracted properties if they are not empty\n",
202-
"if apim_service_id:\n",
203-
" print(f\"👉🏻 APIM Service Id: {apim_service_id}\")\n",
204-
"\n",
205-
"if apim_subscription_key:\n",
206-
" print(f\"👉🏻 APIM Subscription Key (masked): ****{apim_subscription_key[-4:]}\")\n",
207-
"\n",
208-
"if apim_subscription1_key:\n",
209-
" print(f\"👉🏻 APIM Subscription Key 1 (masked): ****{apim_subscription1_key[-4:]}\")\n",
210-
"\n",
211-
"if apim_subscription2_key:\n",
212-
" print(f\"👉🏻 APIM Subscription Key 2 (masked): ****{apim_subscription2_key[-4:]}\")\n",
213-
"\n",
214-
"if apim_subscription3_key:\n",
215-
" print(f\"👉🏻 APIM Subscription Key 3 (masked): ****{apim_subscription3_key[-4:]}\")\n",
216-
"\n",
217-
"if apim_resource_gateway_url:\n",
218-
" print(f\"👉🏻 APIM API Gateway URL: {apim_resource_gateway_url}\")\n",
219-
"\n",
220-
"if workspace_id:\n",
221-
" print(f\"👉🏻 Workspace ID: {workspace_id}\")\n",
222-
"\n",
223-
"if app_id:\n",
224-
" print(f\"👉🏻 App ID: {app_id}\")\n",
225-
"\n",
226-
"if function_app_resource_name:\n",
227-
" print(f\"👉🏻 Function Name: {function_app_resource_name}\")\n",
228-
"\n",
229-
"if cosmosdb_connection_string:\n",
230-
" print(f\"👉🏻 Cosmos DB Connection String: {cosmosdb_connection_string}\")\n"
182+
"output = utils.run(f\"az deployment group show --name {deployment_name} -g {resource_group_name}\", f\"Retrieved deployment: {deployment_name}\", f\"Failed to retrieve deployment: {deployment_name}\")\n",
183+
"if output.success and output.json_data:\n",
184+
" apim_service_id = utils.get_deployment_output(output, 'apimServiceId', 'APIM Service Id')\n",
185+
" apim_subscription_key = utils.get_deployment_output(output, 'apimSubscriptionKey', 'APIM Subscription Key (masked)', True)\n",
186+
" apim_resource_gateway_url = utils.get_deployment_output(output, 'apimResourceGatewayURL', 'APIM API Gateway URL')\n",
187+
"\n"
231188
]
232189
},
233190
{
@@ -249,11 +206,7 @@
249206
"metadata": {},
250207
"outputs": [],
251208
"source": [
252-
"# %load ../../shared/snippets/api-http-requests.py\n",
253-
"\n",
254-
"import json\n",
255-
"import requests\n",
256-
"import time\n",
209+
"import requests, time\n",
257210
"\n",
258211
"runs = 10\n",
259212
"sleep_time_ms = 100\n",
@@ -267,7 +220,7 @@
267220
"# Initialize a session for connection pooling\n",
268221
"session = requests.Session()\n",
269222
"# Set default headers\n",
270-
"session.headers.update({'api-key': apim_subscription_key})\n",
223+
"session.headers.update({'api-key': apim_subscription_key}) # type: ignore\n",
271224
"\n",
272225
"try:\n",
273226
" for i in range(runs):\n",
@@ -288,7 +241,7 @@
288241
"\n",
289242
" # Print the response status with the appropriate formatting\n",
290243
" print(f\"Response status: {status_code_str}\")\n",
291-
" print(f\"Response headers: {json.dumps(dict(response.headers), indent = 4)}\")\n",
244+
" # print(f\"Response headers: {json.dumps(dict(response.headers), indent = 4)}\")\n",
292245
"\n",
293246
" if \"x-ms-region\" in response.headers:\n",
294247
" print(f\"x-ms-region: \\x1b[1;32m{response.headers.get(\"x-ms-region\")}\\x1b[0m\") # this header is useful to determine the region of the backend that served the request\n",
@@ -367,16 +320,14 @@
367320
"metadata": {},
368321
"outputs": [],
369322
"source": [
370-
"# %load ../../shared/snippets/openai-api-requests.py\n",
371-
"\n",
372323
"import time\n",
373324
"from openai import AzureOpenAI\n",
374325
"\n",
375326
"runs = 10\n",
376327
"sleep_time_ms = 100\n",
377328
"\n",
378329
"client = AzureOpenAI(\n",
379-
" azure_endpoint = apim_resource_gateway_url,\n",
330+
" azure_endpoint = apim_resource_gateway_url, # type: ignore\n",
380331
" api_key = apim_subscription_key,\n",
381332
" api_version = openai_api_version\n",
382333
")\n",
@@ -390,9 +341,11 @@
390341
" print(f\"▶️ Run {i+1}/{runs}:\")\n",
391342
"\n",
392343
" start_time = time.time()\n",
393-
" response = client.chat.completions.create(model = openai_model_name, messages = messages) # type: ignore\n",
344+
" raw_response = client.chat.completions.with_raw_response.create(model = openai_model_name, messages = messages) # type: ignore\n",
394345
" response_time = time.time() - start_time\n",
395346
" print(f\"⌚ {response_time:.2f} seconds\")\n",
347+
" print(f\"x-ms-region: \\x1b[1;32m{raw_response.headers.get(\"x-ms-region\")}\\x1b[0m\") # this header is useful to determine the region of the backend that served the request\n",
348+
" response = raw_response.parse()\n",
396349
" if response.usage:\n",
397350
" print(f\"Token usage: Total tokens: {response.usage.total_tokens} (Prompt tokens: {response.usage.prompt_tokens} & Completion tokens: {response.usage.completion_tokens})\")\n",
398351
" print(f\"💬 {response.choices[0].message.content}\\n\")\n",
@@ -414,7 +367,7 @@
414367
],
415368
"metadata": {
416369
"kernelspec": {
417-
"display_name": ".venv",
370+
"display_name": "Python 3",
418371
"language": "python",
419372
"name": "python3"
420373
},

labs/backend-pool-load-balancing/clean-up-resources.ipynb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@
1515
"metadata": {},
1616
"outputs": [],
1717
"source": [
18-
"# type: ignore\n",
19-
"\n",
20-
"import os\n",
21-
"import sys\n",
18+
"import os, sys\n",
2219
"sys.path.insert(1, '../../shared') # add the shared directory to the Python path\n",
23-
"from functions import cleanUpResources\n",
20+
"import utils\n",
2421
"\n",
2522
"deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n",
26-
"cleanUpResources(deployment_name)"
23+
"utils.cleanup_resources(deployment_name)"
2724
]
2825
}
2926
],
@@ -43,7 +40,7 @@
4340
"name": "python",
4441
"nbconvert_exporter": "python",
4542
"pygments_lexer": "ipython3",
46-
"version": "3.13.1"
43+
"version": "3.12.8"
4744
}
4845
},
4946
"nbformat": 4,

0 commit comments

Comments
 (0)