Azure Command Companion
Published Dec 10 2023 12:01 AM 4,332 Views

Azure Command Companion: Harnessing the Power of OpenAI GPT-3.5 Turbo for Azure CLI Command Generation

 

Introduction

Welcome to the world of efficient and effective Azure resource management with the “Azure Command Companion”. This innovative tool, powered by Azure OpenAI’s GPT-3.5 Turbo model, is designed to simplify the complex task of provisioning, managing, and querying Azure resources.

The Azure Command Companion is more than just a tool; it’s your copilot in the cloud. By leveraging the advanced language understanding capabilities of GPT-3.5 Turbo, it can generate Azure CLI commands from natural language inputs. Simply provide your requirements in plain English, and the Azure Command Companion will translate them into the appropriate Azure CLI commands.

The backend API, built with Python, Semantic Kernel, and Flask, integrates with Azure OpenAI to generate, and execute the Azure CLI commands. The frontend, developed with AngularJS, provides a user-friendly interface for users to interact with the system.

The Azure Command Companion is not just about automation; it’s about making Azure resource management more accessible. Whether you’re a seasoned Azure user or a beginner, the Azure Command Companion can help you manage your Azure resources more efficiently and effectively.

In the following sections, we will delve deeper into the workings of the Azure Command Companion, exploring how it leverages the power of Azure OpenAI’s GPT-3.5 Turbo model to revolutionize Azure resource management. Stay tuned and happy reading!

 

Prerequisite

 

The next part of the sections will provide the prerequisites for this implementation. If you are new to these technologies, don’t worry please go through these prerequisite links to get started:

  1. Azure Open AI: Get started with Azure OpenAI Service - Training | Microsoft Learn
    • We are going to use Azure OpenAI chat completion models. So please use Azure Open AI GPT 3.5 Turbo (0613) model for this.
  2. Python coding: You will find multiple courses on the internet, you can refer this link: Learn Python - Free Interactive Python Tutorial
  3. Semantic Kernel Orchestrations: Orchestrate your AI with Semantic Kernel | Microsoft Learn
  4. Angular JS for frontend : Learn AngularJS Tutorial - javatpoint
  5. Flask tutorial to create python api : Flask Tutorial (tutorialspoint.com)
  1. Create a service principal to execute the az cli commands : Apps & service principals in Microsoft Entra ID - Microsoft identity platform | Microsoft Learn

The versions I have used to create these implementations:

  • Flask – 3.0.0
  • Flask-Cors – 4.0.0
  • openai – 1.3.7
  • python-dotenv – 1.0.0
  • semantic-kernel – 0.4.0.dev0
  • python – 3.11.6

Github location for the code base : Saby007/azurecommandcompanion (github.com)

 

Architecture

 

SabyasachiSamaddar_0-1702192750539.png

Figure 1: Architecture

This architecture follows a three-tier model, where:

  1. Presentation Tier: The frontend application serves as the presentation tier, responsible for user interaction and displaying information.
  2. Application Tier: The backend application acts as the application tier, handling user requests, managing logic, and interacting with Azure OpenAI and the Azure CLI.
  3. Data Tier: Azure OpenAI functions as the data tier, using pre-trained model to generate resource templates and CLI commands based on user requests.

The steps are explained below :

  1. User Request: The process begins when a user makes a request to provision or retrieve some details from Azure Cloud. This could be anything from requesting a new resource to be provisioned, to querying the status of an existing resource. 
  1. Frontend Acceptance: The request is accepted by the frontend of the application. The frontend is a web interface built using AngularJS. 
  1. Backend Processing: Once the frontend accepts the request, it is passed to the backend application for processing. The backend application is built using Flask, a Python-based micro web framework. Depending on the nature of the request, the backend application calls Azure OpenAI to generate a resource template and the appropriate Azure CLI (Command-Line Interface) commands. 
  1. Azure OpenAI Interaction: Azure OpenAI is a LLM AI service that can generate the appropriate Azure CLI commands based on the request. These commands are designed to interact with Azure resources. 
  1. Command Execution and Response: The Azure CLI commands are executed using a service principal with elevated access. A service principal is an identity that is used by a service or application to interact with Azure resources. Once the commands are executed, the result is sent back to the user. 

Key Architectural Features:

The architecture utilizes various features, including:

  • Separation of concerns: Distinct tiers enhance maintainability and scalability.
  • Model-driven provisioning: Azure OpenAI generates templates and commands, allowing for dynamic and flexible resource provisioning.
  • Automated provisioning: Service principals automate resource provisioning, eliminating manual intervention.
  • User-friendly interface: The frontend offers a user-friendly interface for interaction.

Advantages of this Architecture:

This architecture offers several benefits:

  • Increased efficiency: Automation saves time and effort compared to manual processes.
  • Reduced errors: Automated resource template and CLI command generation minimizes human error.
  • Flexibility: The architecture adapts to various user requests and model-driven generation.
  • Scalability: The architecture can be easily scaled to accommodate a larger user base. 

The next part of the document will explain the Backend (Flask Python app) and the Frontend (Angular JS) application , Some screen shots regarding a sample demo and finally the conclusion.

Backend (Flask Python App along with Semantic Kernel Plugins)

Creating the Folder Structure

Now I will suggest a folder structure for this application. It is not mandatory to follow and completely optional.

I have created 3 plugins for this application.

 

SabyasachiSamaddar_0-1702192914884.png

 

plugins: Folder to keep all my plugins for this application.

    • commandPlugin
      • config.json
      • skprompt.txt
      • command -> Semantic Function to create the az cli command from Azure Open AI model
    • deplpoyPlugin:
      • deploy.py
      • deploy  -> Native Function to execute the az cli command
    • templatePlugin:
      • config.json
      • skprompt.txt
      • template -> Semantic Function to create the resource template for validation before provisioning it.

Creating the Environment File

Now we will create our environment file, you can name it .env. Please fill in the details in the file in the below format: 

 

AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME="your_model_name"
AZURE_OPEN_AI__ENDPOINT="your_model_endpoint"
AZURE_OPEN_AI__API_KEY="your_model_key"
SERVICE_PRINCIPAL_USER="your_service_principal_user"
SERVICE_PRINCIPAL_PASSWORD="-your_service_principal_password"
TENANT_ID="your_tenant_id"

 

 

Explanation of the variables:

The code above is a configuration file named as .env file, which is used to set environment variables for your application. Environment variables are a universal mechanism for conveying configuration information. Let's break down what each line is doing:

  1. AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME="your_model_name": This sets an environment variable named AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME with the value of "your_model_name". This is likely the name of a specific deployment of an AI model in Azure that your application needs to reference. 
  1. AZURE_OPEN_AI__ENDPOINT="your_model_endpoint": This sets an environment variable named AZURE_OPEN_AI__ENDPOINT with the value of "your_model_endpoint". This is likely the endpoint (URL) of the Azure OpenAI service that your application will be making requests to. 
  1. AZURE_OPEN_AI__API_KEY="your_model_key": This sets an environment variable named AZURE_OPEN_AI__API_KEY with the value of "your_model_key". This is likely the API key that your application will use to authenticate its requests to the Azure OpenAI service. 
  1. SERVICE_PRINCIPAL_USER="your_service_principal_user": This sets an environment variable named SERVICE_PRINCIPAL_USER with the value of "your_service_principal_user". This is likely the user ID of a service principal in Azure. A service principal is an identity created for use with applications, hosted services, and automated tools to access Azure resources. 
  1. SERVICE_PRINCIPAL_PASSWORD="your_service_principal_password": This sets an environment variable named SERVICE_PRINCIPAL_PASSWORD with the value of "your_service_principal_password". This is likely the password for the service principal mentioned above. 
  1. TENANT_ID="your_tenant_id": This sets an environment variable named TENANT_ID with the value of "your_tenant_id". This is likely the ID of your Azure Active Directory tenant. A tenant represents an organization in Azure Active Directory . It's a dedicated instance of Azure AD that an organization receives and owns when it signs up for a Microsoft cloud service like Azure, Microsoft Intune, or Office 365.

You would replace "your_model_name", "your_model_endpoint", "your_model_key", "your_service_principal_user", "your_service_principal_password", and "your_tenant_id" with actual values that correspond to your Azure OpenAI deployment and service principal.

Using a .env file to manage environment variables is a common practice because it helps keep sensitive information like API keys out of your codebase. This is important for security reasons and makes it easier to manage configuration changes.

 

Creating the Plugins

Now as we have created three different types of plugins let use understand each of them in details.

Whenever user asks for anything to provision, the backend system first generate the resource template and the az cli commands to get a confirmation from the user. This activity is done through the template and command plugin.

When user asks for information or any kind of details then the template plugin is not used as we don’t need to validate anything. It uses the command plugin and then the deploy plugin to execute the az cli commands with a service principal.

Now let’s understand the details about each plugin

  • Template Plugin

For the Template Plugin, Create two more files in the appropriate folder:

  1. config.json
  2. skprompt.txt

In config.json add the following content: 

 

 

{
    "schema": 1,
    "description": "Create resource template json schema",
    "type": "completion",
    "completion": {
      "max_tokens": 1000,
      "temperature": 0.0,
      "top_p": 0.0,
      "presence_penalty": 0.0,
      "frequency_penalty": 0.0
    },
    "input": {
      "parameters": [
        {
          "name": "input",
          "description": "Create resource template json schema",
          "defaultValue": "{}"
        }        
      ]
    }
  }

 

In skprompt.txt 

 

You are a resource analyzer , List down all the important resource related details from the user statement in a single json file. Do not create nested json details. Even if user asks duplicate questions provide the answer again without prompting anything else. Always give the same output for the same input. Treat every chat as an isolated request with no previous history.
##
User: Create me a resourcegroup named as saby-rg in location centralindia 
Assistant: {"resourceGroupName":"saby-rg","location":"centralindia"}
User: Create a windows vm named as VM1 within saby-rg resource group, make the vm as small as possible. Password should be 'P@$$W0rd123456'. Location should be centralindia  . 128GB data disk and 128GB os disk
Assistant: {"resourceGroupName": "saby-rg", "vmName": "VM1", "location": "centralindia",  "osDiskSizeGB": 128, "dataDiskSizeGB": 128, "osType": "Windows", "vmSize": "Standard_B1ls", "adminUsername": "azureuser", "adminPassword": "P@$$W0rd123456"}
User: Create a storage account named as storageforsaby007 in resourcegroup saby-rg and location centralindia .
Assistant: {"resourceGroupName": "saby-rg", "storageAccountName": "storageforsaby007", "location": "centralindia"}
User: Create aks in resourcegroup saby-rg , name as myAKSCluster2, nodecount 3
Assistant: {"resourceGroupName": "saby-rg", "aksName": "myAKSCluster2", "nodeCount": 3}
User: Create a Virtual Network with the name myvnet in the centralindia region and saby-rg resourcegroup. The VNet should have a CIDR range of 10.0.0.0/16. Within this VNet, create one subnet named Subnet1. The CIDR range for Subnet1 should be 10.0.0.0/24 
Assistant: {"resourceGroupName": "saby-rg", "location": "centralindia",  "virtualNetworkName": "myvnet", "addressPrefix": "10.0.0.0/16", "subnet1Name": "Subnet1", "subnet1Prefix": "10.0.0.0/24"}
User: Create a Virtual Network with the name myvnet in the centralindia region and saby-rg resourcegroup. The VNet should have a CIDR range of /16 and a subnet with the range of /24
Assistant: {"action": "create", "resource": "Virtual Network", "name": "myvnet", "region": "centralindia", "resource_group": "saby-rg", "cidr_range": "10.0.0.0/16", "subnet_range": "10.0.0.0/24"}
User: Add a subnet named as subnet3 and CIDR as 10.0.2.0/24 in vnet myvnet within resourcegroup saby-rg 
Assistant: {"resourceGroupName": "saby-rg", "location": "centralindia",  "virtualNetworkName": "myvnet", "subnet3Name": "Subnet3", "subnet3Prefix": "10.0.2.0/24"}
User: Add a new CIDR range of 192.168.0.0/16 in vnet myvnet within resourcegroup saby-rg 
Assistant: {"resourceGroupName": "saby-rg", "location": "centralindia",  "virtualNetworkName": "myvnet", "newAddressPrefix": "192.168.0.0/16"}
User: Create a windows vm named as VM1 within saby-rg resource group, make the vm as small as possible. Password should be 'P@$$W0rd123456'. Location should be centralindia . 128GB data disk and 128GB os disk. Create it in vnet myvnet and subnet as subnet1
Assistant: {"resourceGroupName": "saby-rg", "location": "centralindia",  "vmName": "VM1", "vmSize": "Standard_B1ls", "adminUsername": "azureuser", "adminPassword": "P@$$W0rd123456", "osDiskSizeGB": 128, "dataDiskSizeGB": 128, "virtualNetworkName": "myvnet", "subnetName": "subnet1"}
User: Resize the data disk named as VM1_disk2_e20a15a63d95463c9c5e2ab505489748 of vm VM1 to 512 GB in resourcegroup saby-rg 
Assistant: {"resourceName": "VM1_disk2_e20a15a63d95463c9c5e2ab505489748", "vmName": "VM1", "newSize": "512 GB", "resourceGroup": "saby-rg"}
User: Add a new data disk named as datadisk3 of 64GB into vm VM1 within resource group saby-rg 
Assistant: {"resourceName": "datadisk3", "vmName": "VM1", "newSize": "64 GB", "resourceGroup": "saby-rg"}
User: Create me a mysql flexible server named as sabymysqlflexiserver , tier is GeneralPurpose , sku is Standard_D32ads_v5, public access none, storage is 32, in resourcegroup saby-rg 
Assistant: {"resourceGroupName": "saby-rg", "location": "centralindia",  "serverName": "sabymysqlflexiserver", "tier": "GeneralPurpose", "sku": "Standard_D32ads_v5", "storage": "32", "publicNetworkAccess": "None"}
User:{{$input}}
Assistant:

 

 

  • Command Plugin

For the Command Plugin, Create two more files in the appropriate folder:

  1. config.json
  2. skprompt.txt

In config.json add the following content:

 

 

{
    "schema": 1,
    "description": "Create az cli commands",
    "type": "completion",
    "completion": {
      "max_tokens": 1000,
      "temperature": 0.0,
      "top_p": 0.0,
      "presence_penalty": 0.0,
      "frequency_penalty": 0.0
    },
    "input": {
      "parameters": [
        {
          "name": "input",
          "description": "Create az cli commands",
          "defaultValue": ""
        }        
      ]
    }
  }

 

In skprompt.txt

 

You are a resource provision Azure CLI template creator. Please provide the Azure CLI command only. Do not interact with the user and act as a machine system that will only provide Azure CLI commands in one string and one line.  Even if user asks duplicate questions provide the answer again without prompting anything else. Always give the same output for the same input. Treat every chat as an isolated request with no previous history.
##
User: Create me a resourcegroup named as saby-rg in location centralindia 
Assistant: az group create --name saby-rg --location centralindia 
User: Create a windows vm named as VM1 within saby-rg resource group, make the vm as small as possible. Password should be 'P@$$W0rd123456'. Location should be centralindia . 128GB data disk and 128GB os disk
Assistant: az vm create --resource-group saby-rg --name VM1 --image Win2019Datacenter --admin-username azureuser --admin-password P@$$W0rd123456 --size Standard_B1ls --location centralindia  --data-disk-sizes-gb 128 --os-disk-size-gb 128
User: Create a storage account named as storageforsaby007 in resourcegroup saby-rg and location centralindia 
Assistant: az storage account create --name storageforsaby007 --resource-group saby-rg --location centralindia --sku Standard_LRS --kind StorageV2 
User: Create aks in resourcegroup saby-rg , name as myAKSCluster, nodecount 3  
Assistant: az aks create --resource-group saby-rg --name myAKSCluster --node-count 3 
User: Create a Virtual Network with the name myvnet in the centralindia region and saby-rg resourcegroup. The VNet should have a CIDR range of 10.0.0.0/16. Within this VNet, create one subnet named Subnet1. The CIDR range for Subnet1 should be 10.0.0.0/24 
Assistant: az network vnet create --resource-group saby-rg --name myvnet --address-prefixes 10.0.0.0/16 --location centralindia  --subnet-name Subnet1 --subnet-prefixes 10.0.0.0/24
User: Create a Virtual Network with the name myvnet in the centralindia region and saby-rg resourcegroup. The VNet should have a CIDR range of /16 and a subnet with the range of /24
Assistant: az network vnet create --resource-group saby-rg --name myvnet --address-prefixes 10.0.0.0/16 --location centralindia --subnet-name Subnet1 --subnet-prefixes 10.0.0.0/24
User: Add a subnet named as subnet3 and CIDR as 10.0.2.0/24 in vnet myvnet within resourcegroup saby-rg 
Assistant: az network vnet subnet create --resource-group saby-rg --vnet-name myvnet --name subnet3 --address-prefixes 10.0.2.0/24 
User: Add a new CIDR range of 192.168.0.0/16 in vnet myvnet within resourcegroup saby-rg 
Assistant: az network vnet update --resource-group saby-rg --name myvnet --add addressSpace.addressPrefixes 192.168.0.0/16 
User: Create a windows vm named as VM1 within saby-rg resource group, make the vm as small as possible. Password should be 'P@$$W0rd123456'. Location should be centralindia . 128GB data disk and 128GB os disk. Create it in vnet myvnet and subnet as subnet1
Assistant: az vm create --resource-group saby-rg --name VM1 --image Win2019Datacenter --admin-username azureuser --admin-password P@$$W0rd123456 --size Standard_B1ls --location centralindia  --data-disk-sizes-gb 128 --os-disk-size-gb 128 --vnet-name myvnet --subnet subnet1
User: Resize the data disk named as VM1_disk2_e20a15a63d95463c9c5e2ab505489748 of vm VM1 to 512 GB in resourcegroup saby-rg 
Assistant: az disk update --resource-group saby-rg --name VM1_disk2_e20a15a63d95463c9c5e2ab505489748 --size-gb 512 
User: Add a new data disk named as datadisk3 of 64GB into vm VM1 within resource group saby-rg
Assistant: az vm disk attach --resource-group saby-rg --vm-name VM1 --name datadisk3 --size-gb 64 --new
User: Create me a mysql flexible server named as sabymysqlflexiserver , tier is GeneralPurpose , sku is Standard_D32ads_v5, public access none, storage is 32, in resourcegroup saby-rg 
Assistant: az mysql flexible-server create --resource-group saby-rg --name sabymysqlflexiserver --location centralindia --sku-name Standard_D32ads_v5 --tier GeneralPurpose --storage-size 32 --public-access none 
User: show me the performance metrics of resource type VM , named as azresource-backend-vm in resource group Cloudxpdemo
Assistant: az monitor metrics list --resource azresource-backend-vm --resource-group Cloudxpdemo --resource-type Microsoft.Compute/virtualMachines --metric \"Percentage CPU\"
User: Get me all the Azure advisor recommendation for category HighAvailability
Assistant: az advisor recommendation list --category HighAvailability
User: {{$input}}
Assistant:

 

 

 

  • Deploy Plugin

Create another file named deploy.py and paste the below content.

 

 

import os
import subprocess
from semantic_kernel.skill_definition import (
    sk_function,
    sk_function_context_parameter,
)
from semantic_kernel.orchestration.sk_context import SKContext

# Deploy class is used to execute the az cli commands
class Deploy:
    _function(
        description="execute the az cli command",
        name="deploy_func",
        input_description="Execute the az cli command",
    )
    _function_context_parameter(
        name="command",
        description="Az cli command",
    )
    _function_context_parameter(
        name="user",
        description="Service principal user",
    )
    _function_context_parameter(
        name="password",
        description="Service principal password",
    )
    _function_context_parameter(
        name="tenantid",
        description="Tenant id",
    )
    def deploy_func(self, context: SKContext)-> str:
        # Login command
        login_command = "az login --service-principal -u "+ context["user"]+" -p="+context["password"]+ " --tenant "+ context["tenantid"]
        login_result = subprocess.run(login_command, capture_output=True, text=True, shell=True)

        if login_result.returncode != 0:
            return "Login Failed: " + login_result.stderr
        
        result = subprocess.run(context["command"], capture_output=True, text=True, shell=True)
        print(result)
        if(result.returncode == 0):
            return result.stdout
        else:
            return result.stderr
        

 

 

code explanation:

The Python code above is from a file named deploy.py, which defines a class Deploy that is used to execute Azure CLI commands. The class has a method deploy_func which is decorated with several decorators, sk_function and sk_function_context_parameter. These decorators are used to add metadata to the function, which could be used for various purposes such as documentation, type checking, or other runtime processing.

The sk_function decorator is used to add metadata about the function itself, such as its description, name, and input description. The sk_function_context_parameter decorator is used multiple times to add metadata about the parameters that the function expects in its context. In this case, the function expects a context with parameters command, user, password, and tenantid.

The deploy_func method takes a single parameter context, which is an instance of SKContext. This context is expected to contain the parameters defined by the sk_function_context_parameter decorators. The function constructs an Azure CLI login command using the user, password, and tenantid from the context, and then executes this command using the subprocess.run function. If the login command fails, the function returns an error message. If the login is successful, the function then executes the command provided in the context and returns the output of this command.

The subprocess.run function is a utility function for running a command in a subprocess and waiting for it to complete. It returns a CompletedProcess instance, which has attributes for the arguments used to run the command, the return code, and any output or errors produced by the command.

The print function is a built-in Python function that writes the specified message to the screen, or other standard output device. The message can be a string, or any other object, the object will be converted into a string before written to the screen. In this code, it's used to print the result of the command execution.

In summary, this code provides a way to execute Azure CLI commands using a service principal for authentication. The specific commands to be executed, and the details of the service principal, are provided in the context passed to the deploy_func method.

 

Final Orchestrator

Create a file named app.py to orchestrate the application flow.  Paste the below code to create the orchestration.

 

 

import os
import time
import semantic_kernel as sk
from flask_cors import CORS
from dotenv import load_dotenv, dotenv_values
from flask import Flask, request
from plugins.deployPlugin.deploy.deploy import Deploy
from semantic_kernel.connectors.ai.open_ai import (AzureChatCompletion)

# setting app and CORS
app = Flask(__name__)
CORS(app)

# Semantic functions are used to call the semantic skills
# 1. Creating the json schema of key attributes from user's input
# 2. Creating the az cli commands from user's input
def semanticFunctions(kernel, skills_directory,skill_plugin_name, skill_name,input):    
    functions = kernel.import_semantic_skill_from_directory(skills_directory,skill_plugin_name)
    semanticFunction = functions[skill_name]
    return semanticFunction(input)

# Native functions are used to call the native skills
# 1. Execute the az cli commands
def nativeFunctions(kernel, context, plugin_class,skill_name, function_name):
    native_plugin = kernel.import_skill(plugin_class, skill_name)
    function = native_plugin[function_name]    
    return function.invoke(context=context) 

# Process the info related input from the user
@app.route('/process_info', methods=['POST'])
def process_info():

    # Create kernel objects
    kernel = sk.Kernel()
    context = kernel.create_new_context()
    load_dotenv()
    kernel.add_chat_service("chat_completion", AzureChatCompletion(deployment_name=os.environ.get("AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME"),
                                                                   endpoint=os.environ.get("AZURE_OPEN_AI__ENDPOINT"),
                                                                   api_key=os.environ.get("AZURE_OPEN_AI__API_KEY", None)))

    #Getting user input
    user_input = request.data.decode('utf-8')
    
    skills_directory = "./plugins"
    print("Generating the command............... ")
    start = time.time()
    command = semanticFunctions(kernel, skills_directory,"commandPlugin","command",user_input).result
    print("Time taken(secs): ", time.time() - start)
    print("Command: ", command)

    #checking whether it is an info command or not
    if any(word in command for word in ["list", "show", "get"]):
        print("Generating the info............... ")
        context["command"] = command
        context["user"] = os.environ.get("SERVICE_PRINCIPAL_USER")
        context["password"] = os.environ.get("SERVICE_PRINCIPAL_PASSWORD")
        context["tenantid"] = os.environ.get("TENANT_ID")
        start = time.time()
        deployment_result = nativeFunctions(kernel, context, Deploy(),"deployPlugin","deploy_func").result
        print("Time taken(secs): ", time.time() - start)
        print("Info: ", deployment_result)
    else:
        deployment_result = "This is not an info command"

    return command + "|" + deployment_result

# Process the command from the user
@app.route('/process_command', methods=['POST'])
def process_command():
    # Create kernel objects
    kernel = sk.Kernel()
    context = kernel.create_new_context()
    load_dotenv()
    kernel.add_chat_service("chat_completion", AzureChatCompletion(deployment_name=os.environ.get("AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME"),
                                                                   endpoint=os.environ.get("AZURE_OPEN_AI__ENDPOINT"),
                                                                   api_key=os.environ.get("AZURE_OPEN_AI__API_KEY", None)))

    #Getting user input
    user_input = request.data.decode('utf-8')

    skills_directory = "./plugins"
    print("Generating the schema............... ")
    start = time.time()
    template = semanticFunctions(kernel, skills_directory,"templatePlugin","template",user_input).result
    print("Time taken(secs): ", time.time() - start)
    if template[0] == '{' or template[0] == '[':
        start = time.time()
        command = semanticFunctions(kernel, skills_directory,"commandPlugin","command",user_input).result
        print("Time taken(secs): ", time.time() - start)
        print("Command: ", command)
    else:
        command = ""
    return template + "|" + command

# Process the config related input from the user
@app.route('/process_config', methods=['POST'])
def process_config():
    # Create kernel objects
    kernel = sk.Kernel()
    context = kernel.create_new_context()
    load_dotenv()
    kernel.add_chat_service("chat_completion", AzureChatCompletion(deployment_name=os.environ.get("AZURE_OPEN_AI__CHAT_COMPLETION_DEPLOYMENT_NAME"),
                                                                   endpoint=os.environ.get("AZURE_OPEN_AI__ENDPOINT"),
                                                                   api_key=os.environ.get("AZURE_OPEN_AI__API_KEY", None)))

    #Getting user input
    user_input = request.data.decode('utf-8')
    
    skills_directory = "./plugins"
    print("Generating the command............... ")
    start = time.time()
    command = semanticFunctions(kernel, skills_directory,"commandPlugin","command",user_input).result
    print("Time taken(secs): ", time.time() - start)
    print("Command: ", command)

    #checking whether it is a config command or not
    if any(word in command for word in ["set"]):
        print("Generating the info............... ")
        context["command"] = command
        context["user"] = os.environ.get("SERVICE_PRINCIPAL_USER")
        context["password"] = os.environ.get("SERVICE_PRINCIPAL_PASSWORD")
        context["tenantid"] = os.environ.get("TENANT_ID")
        start = time.time()
        deployment_result = nativeFunctions(kernel, context, Deploy(),"deployPlugin","deploy_func").result
        print("Time taken(secs): ", time.time() - start)
        print("Info: ", deployment_result)
    else:
        deployment_result = "This is not a config command"

    return command 

# Process the deploy related input from the user
@app.route('/process_deploy', methods=['POST'])
def process_deploy():

    kernel = sk.Kernel()
    context = kernel.create_new_context()

    #Getting user input
    context["command"] = request.data.decode('utf-8')
    context["user"] = os.environ.get("SERVICE_PRINCIPAL_USER")
    context["password"] = os.environ.get("SERVICE_PRINCIPAL_PASSWORD")
    context["tenantid"] = os.environ.get("TENANT_ID")
    
    start = time.time()
    deployment_result = nativeFunctions(kernel, context, Deploy(),"deployPlugin","deploy_func").result
    print("Time taken(secs): ", time.time() - start)

    return deployment_result

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=8080)

 

 

If you run the app, the app will be hosted in your local machine.

Code Explanation:

The Python code above is part of a Flask application that uses the Semantic Kernel (SK) framework to process user inputs and execute commands. The application defines several routes that correspond to different types of user inputs and uses the SK framework to process these inputs and generate responses. 

The semanticFunctions function is used to process user inputs and generate commands. It takes five parameters: kernel, skills_directory, skill_plugin_name, skill_name, and input. The kernel parameter is an instance of the SK Kernel, which provides various utilities for processing user inputs. The skills_directory parameter is a string that specifies the directory where the semantic skills are located. The skill_plugin_name parameter is a string that specifies the name of the semantic skill plugin to use. The skill_name parameter is a string that specifies the name of the semantic skill to use. The input parameter is the user input to process.

The function begins by calling the import_semantic_skill_from_directory method of the kernel, passing the skills_directory and skill_plugin_name parameters. This method returns a dictionary of semantic skills. The function then retrieves the specified semantic skill from this dictionary and calls it with the user input, returning the result.

The nativeFunctions function is used to execute commands. It takes five parameters: kernel, context, plugin_class, skill_name, and function_name. The kernel parameter is an instance of the SK Kernel. The context parameter is a dictionary that contains the context for the command execution. The plugin_class parameter is a class that defines the native skill to use. The skill_name parameter is a string that specifies the name of the native skill to use. The function_name parameter is a string that specifies the name of the function to invoke on the native skill.

The function begins by calling the import_skill method of the kernel, passing the plugin_class and skill_name parameters. This method returns a dictionary of native skills. The function then retrieves the specified native skill from this dictionary, retrieves the specified function from the native skill, and invokes it with the context, returning the result.

DockerFile to deploy it in Kubernetes (Optional)

 

# Use an official Python runtime as a parent image
FROM python:3.11-slim-buster

# Set the working directory in the container to /app
WORKDIR /app

# Add the current directory contents into the container at /app
ADD . /app

# Install Azure CLI
RUN pip install azure-cli

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Run app.py when the container launches
CMD ["python", "app.py"]

 

 

requirements.txt

 

Flask==3.0.0
Flask-Cors==4.0.0
python-dotenv==1.0.0
openai==1.3.7
semantic-kernel==0.4.0.dev0

 

 

Frontend

This article will provide only the single page details. We can easily stitch it in any angular application. Do follow the tutorial if you are new in Angular JS.  Here I will explain two files.

  1. chat.component.html

 

<div class="container">
  <h1 class="title">Azure Command Companion</h1>
  <div class="chat-window">
    <div class="chat-header">Welcome to Azure Command Companion, your copilot in the cloud. This tool simplifies Azure resource management by translating your plain English requirements into Azure CLI commands. Whether you’re a seasoned Azure user or a beginner, Azure Command Companion makes managing your Azure resources more efficient and accessible.</div>
    <div class="chat-content">
      <div class="message" *ngFor="let msg of messages" [ngClass]="{ 'user-message': msg.isUser, 'system-message': !msg.isUser }">
        <div [innerHTML]="msg.text"></div>
      </div>
      <div *ngIf="isLoading" class="progress-bar"></div>
    </div>
    <div class="chat-input">
      <form class="form">
        <input [(ngModel)]="message" name="message" required class="input">
        <button (click)="sendMessage()" class="button">Send</button>
      </form>
    </div>
  </div>
</div>

 

       2. chat.component.ts

 

import { Component , OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SessionService } from '../session.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit {
  message = '';
  //messages: string[] = [];
  messages: { text: SafeHtml, isUser: boolean }[] = [];
  //provision_message: string[] = [];

  constructor(private http: HttpClient, private sessionService: SessionService, private sanitizer: DomSanitizer) {}

  ngOnInit() {
    this.messages.push({
      text: this.sanitizer.bypassSecurityTrustHtml("<b>Please enter your queries in the below format : <br><br>1. For provisioning commands use 'Command#' or 'command#' and then add your commands. <br>2. For getting generic information regarding resources use 'Info#' or 'info#' and then your query.<br>3. For setting configuration use 'Config#' or 'config#' and then add your requirements. <br> <br>*Rest all types of inputs will be ignored"),
      isUser: false
    });
  }

  isLoading = false;
  url_str = 'http://your_local_ip:8080'
  url_info = this.url_str +'/process_info'
  url_command = this.url_str +'/process_command'
  url_config = this.url_str +'/process_config'
  url_deploy = this.url_str +'/process_deploy'
  sendMessage() {
    console.log("sendMessage: " + this.message)
    let messageType = this.message.split('#')[0].trim();
    if(messageType == 'Command'|| messageType == 'command'){
      let command = this.message.split('#')[1].trim();
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(command), isUser: true})      
      this.isLoading = true;
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Processing the request .... "), isUser: false});      
      this.http.post(this.url_command, command, {responseType: 'text'}).subscribe(response => {
        this.messages.pop();
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Please find the resources & the command to be used for the provision:"), isUser: false});         
        console.log(response);
        let template = response.split('|')[0].trim()
        console.log(template);
        if(template[0]!='{' && template[0]!='['){
          this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(template), isUser: false});
          this.isLoading = false;
        }
        else{
          let jsonResponse = JSON.parse(template);
          this.isLoading = false;
          let htmlTable = this.jsonToTable(jsonResponse);
          let safeHtml = this.sanitizer.bypassSecurityTrustHtml(htmlTable);
          this.messages.push({text: safeHtml, isUser: false}); 
          let command_msg = response.split('|')[1].trim()
          this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("<b><code style=\"background-color: #000000; color: #FFFFFF; padding: 10px; font-family: 'Courier New', Courier, monospace;\">"+ command_msg +" </code></b>"), isUser: false});
          this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Confirm the resource details and type Yes to provision them. You can also hit the command button and say 'Yes' from your microphone."), isUser: false});    
          this.sessionService.provision_message = command_msg;   
          } 
      });
    }
    else if(messageType == 'Info'|| messageType == 'info' ){
      let command = this.message.split('#')[1].trim();
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(command), isUser: true})
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Processing the request .... "+ command), isUser: false});
      this.isLoading = true;
      this.http.post(this.url_info, command, {responseType: 'text'}).subscribe(response => {
        this.messages.pop();
        this.isLoading = false;
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Please find the details below & the command used:"), isUser: false}); 
        let command = response.split('|')[0].trim()
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("<b><code style=\"background-color: #000000; color: #FFFFFF; padding: 10px; font-family: 'Courier New', Courier, monospace;\">"+ command +" </code></b>"), isUser: false});
        //this.messages.push(response);
        console.log(response);
        if(response.split('|')[1].trim()[0]!='{' && response.split('|')[1].trim()[0]!='['){
          this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(response.split('|')[1].trim()), isUser: false});
          this.isLoading = false;
        }
        else{
          console.log(response.split('|')[1].trim());
          let jsonResponse = JSON.parse(response.split('|')[1].trim());
          let htmlTable = this.jsonToTable(jsonResponse);
          let safeHtml = this.sanitizer.bypassSecurityTrustHtml(htmlTable);
          this.messages.push({text: safeHtml, isUser: false}); 
          this.isLoading = false;
        }       
      });
    }
    else if(messageType == 'Config' || messageType == 'config'){
      let command = this.message.split('#')[1].trim();
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(command), isUser: true})
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Processing the request .... "+ command), isUser: false});
      this.isLoading = true;
      this.http.post(this.url_config, command, {responseType: 'text'}).subscribe(response => {
        this.messages.pop();
        this.isLoading = false;
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Please find the status below & the command used:"), isUser: false}); 
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("<b><code style=\"background-color: #000000; color: #FFFFFF; padding: 10px; font-family: 'Courier New', Courier, monospace;\">"+response+" </code></b>"), isUser: false});
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Successfully configured."), isUser: false});          
      });
    }
    else if(this.message == "Yes"){
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(this.message), isUser: true})
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Processing the request .... "), isUser: false});
      this.isLoading = true;
      this.http.post(this.url_deploy, this.sessionService.provision_message, {responseType: 'text'}).subscribe(response => {
        this.messages.pop();
        this.isLoading = false;
        this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("Please find the provision status below & the command used."), isUser: false}); 
        //this.messages.push(response);
        console.log(this.sessionService.provision_message)
        if(response[0]!='{' && response[0]!='['){
          this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml(response), isUser: false});
          this.isLoading = false;
        }
        else{
          console.log(response);
          let jsonResponse = JSON.parse(response);
          let htmlTable = this.jsonToTable(jsonResponse);
          let safeHtml = this.sanitizer.bypassSecurityTrustHtml(htmlTable);
          this.messages.push({text: safeHtml, isUser: false}); 
          this.isLoading = false;
        }       
      });      
    }
    else{
      this.messages.push({text: this.sanitizer.bypassSecurityTrustHtml("<b>Please enter your queries in the below format : <br><br>1. For provisioning commands use 'Command#' or 'command#' and then add your commands. <br>2. For getting generic information regarding resources use 'Info#' or 'info#' and then your query.<br>3. For setting configuration use 'Config#' or 'config#' and then add your requirements. <br> <br>*Rest all types of inputs will be ignored"), isUser: false});
    }
    this.message = '';    
  }

  

jsonToTable(json: any): string {
  let html = '<table style="width:100%;border:1px solid black;border-collapse:collapse;">';
  for (let key in json) {
      html += '<tr>';
      html += `<td style="border:1px solid black;padding:10px;">${key}</td>`;
      if (typeof json[key] === 'object' && !Array.isArray(json[key]) && json[key] !== null) {
          // If the value is an object, create a nested table
          html += `<td style="border:1px solid black;padding:10px;">${this.jsonToTable(json[key])}</td>`;
      } else if (Array.isArray(json[key])) {
          // If the value is an array, create a new row for each item
          html += '<td style="border:1px solid black;padding:10px;">';
          json[key].forEach((item: any) => {
              html += this.jsonToTable(item);
          });
          html += '</td>';
      } else {
          // If the value is a primitive, add it to the table
          html += `<td style="border:1px solid black;padding:10px;">${json[key]}</td>`;
      }
      html += '</tr>';
  }
  html += '</table>';
  return html;
}

}

 

 

Code Explanation:

The above TypeScript code is part of an Angular component named ChatComponent. This component is responsible for handling the chat functionality of the application.

The ChatComponent class is decorated with the @Component decorator, which is used to define a component in Angular. The decorator takes an object with several properties: selector (the name of the HTML tag where this component will be inserted), templateUrl (the location of the component's HTML template), and styleUrls (an array containing the locations of the component's CSS stylesheets).

The ChatComponent class has several properties. message is a string that holds the current message input by the user. messages is an array of objects, where each object represents a chat message. Each message has a text property (which is of type SafeHtml to allow HTML content) and an isUser property (a boolean indicating whether the message is from the user). isLoading is a boolean indicating whether the application is currently loading data. url_str, url_info, url_command, url_config, and url_deploy are strings that hold various URLs for the application's backend endpoints. 

The ChatComponent class has a constructor that injects three services: HttpClient (for making HTTP requests), SessionService (a custom service likely used for managing user sessions), and DomSanitizer (for sanitizing HTML strings).

The ngOnInit method is a lifecycle hook that is called after Angular has initialized all data-bound properties of the component. In this method, a welcome message is pushed to the messages array.

The sendMessage function in the ChatComponent class handles user messages. When a user sends a message, this function processes it based on the command type. Here's a detailed breakdown:

  • The function first logs the user's message to the console.
  • It then splits the user's message on the '#' character and checks the first part to determine the type of the message.
  • If the message type is 'Command' or 'command', it further splits the message to extract the command.
  • Depending on the command type, it makes an HTTP GET request to the appropriate backend endpoint:
    • If the command type is 'info', it sends a request to the url_info endpoint.
    • If the command type is 'config', it sends a request to the url_config endpoint.
    • If the command type is 'deploy', it sends a request to the url_deploy endpoint.
  • It processes the response from the HTTP request:
    • If the response is a JSON object, it converts it to an HTML table using the jsonToTable function.
    • If the response is not a JSON object, it simply uses the response as is.
  • It then pushes the response message to the messages array and sets isLoading to false.

The jsonToTable function is a utility function that converts a JSON object into an HTML table. Here's how it works:

  • It starts by initializing an HTML string with the opening tag for a table.
  • It then iterates over each key-value pair in the JSON object.
  • For each key-value pair, it creates a new row in the table and adds a cell containing the key.
  • It then checks the type of the value associated with the key:
  • If the value is an object, it recursively calls jsonToTable to convert this object into a table.
  • If the value is an array, it converts each element in the array into a table.
  • Otherwise, it simply adds a cell containing the value.
  • Finally, it adds the closing tag for the table and returns the HTML string.

Sample Demo

Now I will add some screenshots to show case the demo. I will show two use cases .

  1. Showing the default subscription

SabyasachiSamaddar_0-1702194695750.png

 

SabyasachiSamaddar_1-1702194695759.png

 

 

  1. Creating a resource group named as saby-rg

SabyasachiSamaddar_3-1702194695773.png

 

SabyasachiSamaddar_5-1702194764187.png

Conclusion

In conclusion, the Azure Command Companion, powered by Azure OpenAI’s GPT-3.5 Turbo model, is a revolutionary tool that simplifies the complex task of managing Azure resources. It’s more than just a tool; it’s a copilot in the cloud, capable of generating Azure CLI commands from natural language inputs. This means you can simply provide your requirements in plain English, and the Azure Command Companion will translate them into the appropriate Azure CLI commands.

The backend API, built with Python, Semantic Kernel, and Flask, integrates with Azure OpenAI to generate and execute the Azure CLI commands. The frontend, developed with AngularJS, provides a user-friendly interface for users to interact with the system. This makes Azure resource management more accessible, whether you’re a seasoned Azure user or a beginner.

One of the key features of the Azure Command Companion is its customizability. Users can play with the Semantic Kernel settings and change the behavior of the output by training it appropriately. This means the tool can be tailored to suit your specific needs and preferences, enhancing its efficiency and effectiveness.

While this blog focuses on Azure CLI commands, the Azure Command Companion’s capabilities extend beyond that. It can be extended to use Kusto queries, PowerShell scripts, ARM, terraform , biceps and more. This flexibility makes it a versatile tool that can cater to a wide range of Azure resource management tasks.

In essence, the Azure Command Companion is not just about automation; it’s about revolutionizing the way we manage Azure resources. By harnessing the power of Azure OpenAI’s GPT-3.5 Turbo model, it’s set to transform Azure resource management, making it more efficient, effective, and accessible to all. So, whether you’re a seasoned Azure user or a beginner, the Azure Command Companion is here to help you manage your Azure resources more efficiently and effectively. Stay tuned for more exciting developments in this space.

1 Comment
Version history
Last update:
‎Dec 10 2023 12:01 AM
Updated by: