diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 796b64d2965..c16763a1962 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -72,6 +72,8 @@ /src/ip-group/ @haroldrandom +/src/ai-examples/ @mirdaki + /src/notification-hub/ @fengzhou-msft /src/connection-monitor-preview/ @haroldrandom diff --git a/src/ai-examples/HISTORY.rst b/src/ai-examples/HISTORY.rst new file mode 100644 index 00000000000..38acb6f6677 --- /dev/null +++ b/src/ai-examples/HISTORY.rst @@ -0,0 +1,10 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++ +* Initial release. +* Add AI generated examples to the "--help" examples +* Check to see if the client can connect to the AI example service diff --git a/src/ai-examples/README.rst b/src/ai-examples/README.rst new file mode 100644 index 00000000000..cf3eb509bea --- /dev/null +++ b/src/ai-examples/README.rst @@ -0,0 +1,6 @@ +Microsoft Azure CLI 'AI Examples' Extension +========================================== + +Improve user experince by adding AI powered examples to command help content. + +This extension changes the default examples provided when calling `-h` or `--help` on a command, such as `az vm create -h`, with ones selected by an AI powered service. The service provides examples based on Azure usage, internet sources, and other factors. \ No newline at end of file diff --git a/src/ai-examples/azext_ai_examples/__init__.py b/src/ai-examples/azext_ai_examples/__init__.py new file mode 100644 index 00000000000..4c678e05fdc --- /dev/null +++ b/src/ai-examples/azext_ai_examples/__init__.py @@ -0,0 +1,38 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from azure.cli.core import AzCommandsLoader + +from azext_ai_examples._help import helps # pylint: disable=unused-import + + +def inject_functions_into_core(): + # Replace the default examples from help calls + from azure.cli.core._help import AzCliHelp + from azext_ai_examples.custom import provide_examples + AzCliHelp.example_provider = provide_examples + + +class AiExamplesCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + ai_examples_custom = CliCommandType( + operations_tmpl='azext_ai_examples.custom#{}') + super(AiExamplesCommandsLoader, self).__init__(cli_ctx=cli_ctx, + custom_command_type=ai_examples_custom) + inject_functions_into_core() + + def load_command_table(self, args): + from azext_ai_examples.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + pass + + +COMMAND_LOADER_CLS = AiExamplesCommandsLoader diff --git a/src/ai-examples/azext_ai_examples/_help.py b/src/ai-examples/azext_ai_examples/_help.py new file mode 100644 index 00000000000..c281a5a2756 --- /dev/null +++ b/src/ai-examples/azext_ai_examples/_help.py @@ -0,0 +1,19 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from knack.help_files import helps # pylint: disable=unused-import + + +helps['ai-examples'] = """ + type: group + short-summary: Add AI powered examples to help content. +""" + +helps['ai-examples check-connection'] = """ + type: command + short-summary: Check if the client can connect to the AI example service. +""" diff --git a/src/ai-examples/azext_ai_examples/azext_metadata.json b/src/ai-examples/azext_ai_examples/azext_metadata.json new file mode 100644 index 00000000000..bb2003f649f --- /dev/null +++ b/src/ai-examples/azext_ai_examples/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.81" +} \ No newline at end of file diff --git a/src/ai-examples/azext_ai_examples/commands.py b/src/ai-examples/azext_ai_examples/commands.py new file mode 100644 index 00000000000..ebfc5473c07 --- /dev/null +++ b/src/ai-examples/azext_ai_examples/commands.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def load_command_table(self, _): + + with self.command_group('ai-examples') as g: + g.custom_command('check-connection', 'check_connection_aladdin') + + with self.command_group('ai-examples', is_preview=True): + pass diff --git a/src/ai-examples/azext_ai_examples/custom.py b/src/ai-examples/azext_ai_examples/custom.py new file mode 100644 index 00000000000..a7a82cbefd4 --- /dev/null +++ b/src/ai-examples/azext_ai_examples/custom.py @@ -0,0 +1,139 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +import json +import re +import requests +from pkg_resources import parse_version + +from azure.cli.core import telemetry as telemetry_core +from azure.cli.core import __version__ as core_version +from azure.cli.core._help import HelpExample + + +# Commands +def check_connection_aladdin(): + response = ping_aladdin_service() + if response.status_code == 200: + print('Connection was successful') + else: + print('Connection failed') + + +# Replacements for core functions +def provide_examples(help_file): + return replace_examples(help_file) + + +# Provide two options for changing the examples + +# Replace built in examples wirh Aladdin ones +def replace_examples(help_file): + # Specify az to coerce the examples to be for the exact command + lookup_term = "az " + help_file.command + return get_generated_examples(lookup_term) + + +# Append Aladdin examples to the built in ones +def append_examples(help_file): + # Specify az to coerce the examples to be for the exact command + lookup_term = "az " + help_file.command + aladdin_examples = get_generated_examples(lookup_term) + return concat_unique_examples(help_file.examples, aladdin_examples) + + +# Support functions +def get_generated_examples(cli_term): + examples = [] + response = call_aladdin_service(cli_term) + + if response.status_code == 200: + for answer in json.loads(response.content): + examples.append(clean_from_http_answer(answer)) + + return examples + + +def concat_unique_examples(first_list, second_list): + for first_item in first_list: + for second_item in second_list: + if are_examples_equal(first_item, second_item): + second_list.remove(second_item) + return first_list + second_list + + +def are_examples_equal(first, second): + return clean_string(first.short_summary) == clean_string(second.short_summary) \ + or clean_string(first.command) == clean_string(second.command) + + +def clean_string(text): + return text.strip() + + +def clean_from_http_answer(http_answer): + example = HelpExample() + example.short_summary = http_answer['title'].strip() + example.command = http_answer['snippet'].strip() + if example.short_summary.startswith("az "): + example.short_summary, example.command = example.command, example.short_summary + example.short_summary = example.short_summary.split('\r\n')[0] + elif '```azurecli\r\n' in example.command: + start_index = example.command.index('```azurecli\r\n') + len('```azurecli\r\n') + example.command = example.command[start_index:] + example.command = example.command.replace('```', '').replace(example.short_summary, '').strip() + example.command = re.sub(r'\[.*\]', '', example.command).strip() + # Add a '\n' to comply with the existing examples format + example.command = example.command + '\n' + return example + + +# HTTP calls +def ping_aladdin_service(): + api_url = 'https://app.aladdin.microsoft.com/api/v1.0/monitor' + headers = {'Content-Type': 'application/json'} + + response = requests.get( + api_url, + headers=headers) + + return response + + +def call_aladdin_service(query): + client_request_id = '' + if telemetry_core._session.application: # pylint: disable=protected-access + client_request_id = telemetry_core._session.application.data['headers']['x-ms-client-request-id'] # pylint: disable=protected-access + + session_id = telemetry_core._session._get_base_properties()['Reserved.SessionId'] # pylint: disable=protected-access + subscription_id = telemetry_core._get_azure_subscription_id() # pylint: disable=protected-access + client_request_id = client_request_id # pylint: disable=protected-access + installation_id = telemetry_core._get_installation_id() # pylint: disable=protected-access + version = str(parse_version(core_version)) + + context = { + "sessionId": session_id, + "subscriptionId": subscription_id, + "clientRequestId": client_request_id, + "installationId": installation_id, + "versionNumber": version + } + + api_url = 'https://app.aladdin.microsoft.com/api/v1.0/examples' + headers = {'Content-Type': 'application/json'} + + response = requests.get( + api_url, + params={ + 'query': query, + 'clientType': 'AzureCli', + 'context': json.dumps(context), + 'commandOnly': True, + 'numberOfExamples': 5 + }, + headers=headers) + + return response diff --git a/src/ai-examples/azext_ai_examples/tests/__init__.py b/src/ai-examples/azext_ai_examples/tests/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/ai-examples/azext_ai_examples/tests/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/ai-examples/azext_ai_examples/tests/latest/__init__.py b/src/ai-examples/azext_ai_examples/tests/latest/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/ai-examples/azext_ai_examples/tests/latest/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/ai-examples/azext_ai_examples/tests/latest/test_ai_examples_scenario.py b/src/ai-examples/azext_ai_examples/tests/latest/test_ai_examples_scenario.py new file mode 100644 index 00000000000..262357ae57b --- /dev/null +++ b/src/ai-examples/azext_ai_examples/tests/latest/test_ai_examples_scenario.py @@ -0,0 +1,107 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +import json +import unittest +import mock +import requests + +from azure_devtools.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) +from azext_ai_examples.custom import (call_aladdin_service, ping_aladdin_service, + clean_from_http_answer, get_generated_examples) + + +def create_valid_http_response(): + mock_response = requests.Response() + mock_response.status_code = 200 + data = [{ + 'title': 'RunTestAutomation', + 'snippet': 'az find' + }, { + 'title': 'az test', + 'snippet': 'The title' + }] + mock_response._content = json.dumps(data) + return mock_response + + +def create_empty_http_response(): + mock_response = requests.Response() + mock_response.status_code = 200 + data = [] + mock_response._content = json.dumps(data) + return mock_response + + +def create_failed_http_response(): + mock_response = requests.Response() + mock_response.status_code = 500 + data = [] + mock_response._content = json.dumps(data) + return mock_response + + +class AiExamplesCustomCommandTest(unittest.TestCase): + + # Test the Aladdin check connection command + def test_ai_examples_ping_aladdin_service_success(self): + mock_response = create_empty_http_response() + + with mock.patch('requests.get', return_value=(mock_response)): + response = ping_aladdin_service() + + self.assertEqual(200, response.status_code) + + def test_ai_examples_ping_aladdin_service_failed(self): + mock_response = create_failed_http_response() + + with mock.patch('requests.get', return_value=(mock_response)): + response = ping_aladdin_service() + + self.assertEqual(500, response.status_code) + + # Test the Aladdin examples + def test_ai_examples_call_aladdin_service(self): + mock_response = create_valid_http_response() + + with mock.patch('requests.get', return_value=(mock_response)): + response = call_aladdin_service('RunTestAutomation') + self.assertEqual(200, response.status_code) + self.assertEqual(2, len(json.loads(response.content))) + + def test_ai_examples_example_clean_from_http_answer(self): + cleaned_responses = [] + mock_response = create_valid_http_response() + + for response in json.loads(mock_response.content): + cleaned_responses.append(clean_from_http_answer(response)) + + self.assertEqual('RunTestAutomation', cleaned_responses[0].short_summary) + self.assertEqual('az find\n', cleaned_responses[0].command) + self.assertEqual('The title', cleaned_responses[1].short_summary) + self.assertEqual('az test\n', cleaned_responses[1].command) + + def test_ai_examples_get_generated_examples_full(self): + examples = [] + mock_response = create_valid_http_response() + + with mock.patch('requests.get', return_value=(mock_response)): + examples = get_generated_examples('RunTestAutomation') + + self.assertEqual('RunTestAutomation', examples[0].short_summary) + self.assertEqual('az find\n', examples[0].command) + self.assertEqual('The title', examples[1].short_summary) + self.assertEqual('az test\n', examples[1].command) + + def test_ai_examples_get_generated_examples_empty(self): + examples = [] + mock_response = create_empty_http_response() + + with mock.patch('requests.get', return_value=(mock_response)): + examples = get_generated_examples('RunTestAutomation') + + self.assertEqual(0, len(examples)) diff --git a/src/ai-examples/setup.cfg b/src/ai-examples/setup.cfg new file mode 100644 index 00000000000..3c6e79cf31d --- /dev/null +++ b/src/ai-examples/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/ai-examples/setup.py b/src/ai-examples/setup.py new file mode 100644 index 00000000000..26bbd6139a1 --- /dev/null +++ b/src/ai-examples/setup.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from codecs import open +from setuptools import setup, find_packages +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + +# HISTORY.rst entry. +VERSION = '0.1.0' + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: MIT License', +] + +DEPENDENCIES = [] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='ai_examples', + version=VERSION, + description='Add AI powered examples to help content.', + author='Matthew Booe', + author_email='mabooe@microsoft.com', + url='https://github.com/Azure/azure-cli-extensions/tree/master/src/ai-examples', + long_description='Improve user experince by adding AI powered examples to command help content.', + license='MIT', + classifiers=CLASSIFIERS, + packages=find_packages(), + install_requires=DEPENDENCIES, + package_data={'azext_ai_examples': ['azext_metadata.json']}, +) diff --git a/src/index.json b/src/index.json index 2e01315137a..464299efa27 100644 --- a/src/index.json +++ b/src/index.json @@ -46,6 +46,54 @@ "sha256Digest": "4ac7b8a4a89eda68d9d1a07cc5edd9b1a2b88421e2aa9a9e5b86a241f127775f" } ], + "ai-examples": [ + { + "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/ai_examples-0.1.0-py2.py3-none-any.whl", + "filename": "ai_examples-0.1.0-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.81", + "classifiers": [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: MIT License" + ], + "extensions": { + "python.details": { + "contacts": [ + { + "email": "mabooe@microsoft.com", + "name": "Matthew Booe", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions/tree/master/src/ai-examples" + } + } + }, + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "ai-examples", + "summary": "Add AI powered examples to help content.", + "version": "0.1.0" + }, + "sha256Digest": "3bf63937122345abe28f6d6ddcac8c76491ae992910a6516bcb506e099e59f8b" + } + ], "aks-preview": [ { "downloadUrl": "https://azurecliaks.blob.core.windows.net/azure-cli-extension/aks_preview-0.4.31-py2.py3-none-any.whl",