diff --git a/labs/function-calling/function-calling.ipynb b/labs/function-calling/function-calling.ipynb index c9624c2..629776c 100644 --- a/labs/function-calling/function-calling.ipynb +++ b/labs/function-calling/function-calling.ipynb @@ -19,16 +19,6 @@ "### Result\n", "![result](result.png)\n", "\n", - "### TOC\n", - "- [0️⃣ Initialize notebook variables](#0)\n", - "- [1️⃣ Create the Azure Resource Group](#1)\n", - "- [2️⃣ Create deployment using 🦾 Bicep](#2)\n", - "- [3️⃣ Get the deployment outputs](#3)\n", - "- [4️⃣ Deploy the function](#4)\n", - "- [🧪 Test the Function API](#function)\n", - "- [🧪 Test OpenAI function calling](#functioncalling)\n", - "- [🗑️ Clean up resources](#clean)\n", - "\n", "### Prerequisites\n", "- [Python 3.12 or later version](https://www.python.org/) installed\n", "- [Pandas Library](https://pandas.pydata.org/) installed\n", @@ -61,33 +51,37 @@ }, "outputs": [], "source": [ - "import os\n", - "import json\n", - "import datetime\n", - "import requests\n", + "import os, sys, json\n", + "sys.path.insert(1, '../../shared') # add the shared directory to the Python path\n", + "import utils\n", "\n", "deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n", "resource_group_name = f\"lab-{deployment_name}\" # change the name to match your naming style\n", "resource_group_location = \"westeurope\"\n", + "\n", "apim_resource_name = \"apim\"\n", "apim_resource_location = \"westeurope\"\n", "apim_resource_sku = \"Basicv2\"\n", "openai_resources = [ {\"name\": \"openai1\", \"location\": \"swedencentral\"} ] # list of OpenAI resources to deploy. Clear this list to use only the mock resources\n", "openai_resources_sku = \"S0\"\n", - "openai_model_name = \"gpt-35-turbo\"\n", - "openai_model_version = \"1106\"\n", - "openai_deployment_name = \"gpt-35-turbo\"\n", - "openai_api_version = \"2024-03-01-preview\"\n", + "\n", + "openai_model_name = \"gpt-4o-mini\"\n", + "openai_model_version = \"2024-07-18\"\n", + "openai_model_sku = \"GlobalStandard\"\n", + "openai_model_capacity = 20\n", + "openai_deployment_name = \"gpt-4o-mini\"\n", + "openai_api_version = \"2024-10-21\"\n", + "\n", "openai_specification_url='https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/data-plane/AzureOpenAI/inference/stable/2024-02-01/inference.json'\n", "openai_backend_pool = \"openai-backend-pool\"\n", - "mock_backend_pool = \"mock-backend-pool\"\n", - "mock_webapps = [ ]\n", "\n", "log_analytics_name = \"workspace\"\n", "app_insights_name = 'insights'\n", "\n", "function_app_name = \"function\"\n", - "storage_account_name = \"storage\"\n" + "storage_account_name = \"storage\"\n", + "\n", + "utils.print_ok('Notebook initialized')" ] }, { @@ -95,8 +89,9 @@ "metadata": {}, "source": [ "\n", - "### 1️⃣ Create the Azure Resource Group\n", - "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." + "### 1️⃣ Verify the Azure CLI and the connected Azure subscription\n", + "\n", + "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." ] }, { @@ -105,11 +100,16 @@ "metadata": {}, "outputs": [], "source": [ - "resource_group_stdout = ! az group create --name {resource_group_name} --location {resource_group_location}\n", - "if resource_group_stdout.n.startswith(\"ERROR\"):\n", - " print(resource_group_stdout)\n", - "else:\n", - " print(\"✅ Azure Resource Group \", resource_group_name, \" created ⌚ \", datetime.datetime.now().time())" + "output = utils.run(\"az account show\", \"Retrieved az account\", \"Failed to get the current az account\")\n", + "\n", + "if output.success and output.json_data:\n", + " current_user = output.json_data['user']['name']\n", + " tenant_id = output.json_data['tenantId']\n", + " subscription_id = output.json_data['id']\n", + "\n", + " utils.print_info(f\"Current user: {current_user}\")\n", + " utils.print_info(f\"Tenant ID: {tenant_id}\")\n", + " utils.print_info(f\"Subscription ID: {subscription_id}\")" ] }, { @@ -119,7 +119,7 @@ "\n", "### 2️⃣ Create deployment using 🦾 Bicep\n", "\n", - "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. " + "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" ] }, { @@ -128,29 +128,21 @@ "metadata": {}, "outputs": [], "source": [ - "if len(openai_resources) > 0:\n", - " backend_id = openai_backend_pool if len(openai_resources) > 1 else openai_resources[0].get(\"name\")\n", - "elif len(mock_webapps) > 0:\n", - " backend_id = mock_backend_pool if len(mock_backend_pool) > 1 else mock_webapps[0].get(\"name\")\n", - "\n", - "with open(\"policy.xml\", 'r') as policy_xml_file:\n", - " policy_template_xml = policy_xml_file.read()\n", - " policy_xml = policy_template_xml.replace(\"{backend-id}\", backend_id)\n", - " policy_xml_file.close()\n", - "open(\"policy.xml\", 'w').write(policy_xml)\n", + "# Create the resource group if doesn't exist\n", + "utils.create_resource_group(resource_group_name, resource_group_location)\n", "\n", + "# Define the Bicep parameters\n", "bicep_parameters = {\n", " \"$schema\": \"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#\",\n", " \"contentVersion\": \"1.0.0.0\",\n", " \"parameters\": {\n", - " \"mockWebApps\": { \"value\": mock_webapps },\n", - " \"mockBackendPoolName\": { \"value\": mock_backend_pool },\n", " \"openAIBackendPoolName\": { \"value\": openai_backend_pool },\n", " \"openAIConfig\": { \"value\": openai_resources },\n", " \"openAIDeploymentName\": { \"value\": openai_deployment_name },\n", " \"openAISku\": { \"value\": openai_resources_sku },\n", " \"openAIModelName\": { \"value\": openai_model_name },\n", " \"openAIModelVersion\": { \"value\": openai_model_version },\n", + " \"openAIModelSKU\": { \"value\": openai_model_sku },\n", " \"openAIAPISpecURL\": { \"value\": openai_specification_url },\n", " \"apimResourceName\": { \"value\": apim_resource_name},\n", " \"apimResourceLocation\": { \"value\": apim_resource_location},\n", @@ -161,12 +153,14 @@ " \"storageAccountName\": { \"value\": storage_account_name }\n", " }\n", "}\n", + "\n", + "# Write the parameters to the params.json file\n", "with open('params.json', 'w') as bicep_parameters_file:\n", " bicep_parameters_file.write(json.dumps(bicep_parameters))\n", "\n", - "! az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file \"main.bicep\" --parameters \"params.json\"\n", - "\n", - "open(\"policy.xml\", 'w').write(policy_template_xml)\n" + "# Run the deployment\n", + "output = utils.run(f\"az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file main.bicep --parameters params.json\",\n", + " f\"Deployment '{deployment_name}' succeeded\", f\"Deployment '{deployment_name}' failed\")" ] }, { @@ -174,7 +168,9 @@ "metadata": {}, "source": [ "\n", - "### 3️⃣ Get the deployment outputs\n" + "### 3️⃣ Get the deployment outputs\n", + "\n", + "Retrieve the required outputs from the Bicep deployment." ] }, { @@ -183,23 +179,13 @@ "metadata": {}, "outputs": [], "source": [ - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.apimSubscriptionKey.value -o tsv\n", - "apim_subscription_key = deployment_stdout.n\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.apimResourceGatewayURL.value -o tsv\n", - "apim_resource_gateway_url = deployment_stdout.n\n", - "print(\"👉🏻 API Gateway URL: \", apim_resource_gateway_url)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.logAnalyticsWorkspaceId.value -o tsv\n", - "workspace_id = deployment_stdout.n\n", - "print(\"👉🏻 Workspace ID: \", workspace_id)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.applicationInsightsAppId.value -o tsv\n", - "app_id = deployment_stdout.n\n", - "print(\"👉🏻 App ID: \", app_id)\n", - "\n", - "deployment_stdout = ! az deployment group show --name {deployment_name} -g {resource_group_name} --query properties.outputs.functionAppResourceName.value -o tsv\n", - "function_app_resource_name = deployment_stdout.n\n", - "print(\"👉🏻 Function Name: \", function_app_resource_name)\n" + "# Obtain all of the outputs from the deployment\n", + "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", + "\n", + "if output.success and output.json_data:\n", + " apim_resource_gateway_url = utils.get_deployment_output(output, 'apimResourceGatewayURL', 'APIM API Gateway URL')\n", + " apim_subscription_key = utils.get_deployment_output(output, 'apimSubscriptionKey', 'APIM Subscription Key (masked)', True)\n", + " function_app_resource_name = utils.get_deployment_output(output, 'functionAppResourceName', 'Function App Resource Name')\n" ] }, { @@ -235,6 +221,8 @@ "metadata": {}, "outputs": [], "source": [ + "import requests\n", + "\n", "request = { \"location\": \"London\", \"unit\": \"celsius\" }\n", "url = apim_resource_gateway_url + \"/weather\"\n", "response = requests.post(url, headers = {'api-key':apim_subscription_key}, json = request)\n", @@ -265,6 +253,7 @@ "metadata": {}, "outputs": [], "source": [ + "# type: ignore\n", "from openai import AzureOpenAI\n", "import uuid\n", "\n", @@ -390,7 +379,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/labs/function-calling/main.bicep b/labs/function-calling/main.bicep index d8c56a8..58fbc59 100644 --- a/labs/function-calling/main.bicep +++ b/labs/function-calling/main.bicep @@ -1,11 +1,3 @@ -@description('List of Mock webapp names used to simulate OpenAI behavior.') -param mockWebApps array = [] - -@description('The name of the OpenAI mock backend pool') -param mockBackendPoolName string = 'openai-backend-pool' - -@description('The description of the OpenAI mock backend pool') -param mockBackendPoolDescription string = 'Load balancer for multiple OpenAI Mocking endpoints' @description('List of OpenAI resources to create. Add pairs of name and location.') param openAIConfig array = [] @@ -25,6 +17,9 @@ param openAIModelName string @description('Model Version') param openAIModelVersion string +@description('Model SKU') +param openAIModelSKU string + @description('Model Capacity') param openAIModelCapacity int = 20 @@ -162,6 +157,10 @@ param functionAPIDescription string = 'Weather API' var resourceSuffix = uniqueString(subscription().id, resourceGroup().id) +var policyXml = loadTextContent('policy.xml') +var updatedPolicyXml = replace(policyXml, '{backend-id}', (length(openAIConfig) > 1) ? 'openai-backend-pool' : openAIConfig[0].name) + + resource cognitiveServices 'Microsoft.CognitiveServices/accounts@2021-10-01' = [for config in openAIConfig: if(length(openAIConfig) > 0) { name: '${config.name}-${resourceSuffix}' location: config.location @@ -188,7 +187,7 @@ resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01 } } sku: { - name: 'Standard' + name: openAIModelSKU capacity: openAIModelCapacity } }] @@ -247,7 +246,7 @@ resource apiPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-12-01-pre parent: api properties: { format: 'rawxml' - value: loadTextContent('policy.xml') + value: updatedPolicyXml } } @@ -282,45 +281,14 @@ resource backendOpenAI 'Microsoft.ApiManagement/service/backends@2023-05-01-prev } }] -resource backendMock 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = [for (mock, i) in mockWebApps: if(length(openAIConfig) == 0 && length(mockWebApps) > 0) { - name: mock.name - parent: apimService - properties: { - description: 'backend description' - url: '${mock.endpoint}/openai' - protocol: 'http' - circuitBreaker: { - rules: [ - { - failureCondition: { - count: 3 - errorReasons: [ - 'Server errors' - ] - interval: 'PT5M' - statusCodeRanges: [ - { - min: 429 - max: 429 - } - ] - } - name: 'mockBreakerRule' - tripDuration: 'PT1M' - } - ] - } - } -}] - resource backendPoolOpenAI 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = if(length(openAIConfig) > 1) { name: openAIBackendPoolName parent: apimService + // BCP035: protocol and url are not needed in the Pool type. This is an incorrect error. + #disable-next-line BCP035 properties: { description: openAIBackendPoolDescription type: 'Pool' -// protocol: 'http' // the protocol is not needed in the Pool type -// url: '${cognitiveServices[0].properties.endpoint}/openai' // the url is not needed in the Pool type pool: { services: [for (config, i) in openAIConfig: { id: '/backends/${backendOpenAI[i].name}' @@ -330,23 +298,6 @@ resource backendPoolOpenAI 'Microsoft.ApiManagement/service/backends@2023-05-01- } } -resource backendPoolMock 'Microsoft.ApiManagement/service/backends@2023-05-01-preview' = if(length(openAIConfig) == 0 && length(mockWebApps) > 1) { - name: mockBackendPoolName - parent: apimService - properties: { - description: mockBackendPoolDescription - type: 'Pool' -// protocol: 'http' // the protocol is not needed in the Pool type -// url: '${mockWebApps[0].endpoint}/openai' // the url is not needed in the Pool type - pool: { - services: [for (webApp, i) in mockWebApps: { - id: '/backends/${backendMock[i].name}' - } - ] - } - } -} - resource apimSubscription 'Microsoft.ApiManagement/service/subscriptions@2023-05-01-preview' = { name: openAISubscriptionName parent: apimService