diff --git a/src/azure-cli-core/azure/cli/core/_help.py b/src/azure-cli-core/azure/cli/core/_help.py index e28f82ca980..74847354733 100644 --- a/src/azure-cli-core/azure/cli/core/_help.py +++ b/src/azure-cli-core/azure/cli/core/_help.py @@ -129,7 +129,6 @@ def _print_header(self, cli_name, help_file): def _print_detailed_help(self, cli_name, help_file): CLIPrintMixin._print_extensions_msg(help_file) super()._print_detailed_help(cli_name, help_file) - self._print_az_find_message(help_file.command) @staticmethod def _get_choices_defaults_sources_str(p): @@ -154,12 +153,6 @@ def _print_examples(help_file): _print_indent('{0}'.format(e.command), indent) print('') - @staticmethod - def _print_az_find_message(command): - indent = 0 - message = 'To search AI knowledge base for examples, use: az find "az {}"'.format(command) - _print_indent(message + '\n', indent) - @staticmethod def _process_value_sources(p): commands, strings, urls = [], [], [] @@ -401,10 +394,7 @@ def show_cached_help(self, help_data, args=None): self._print_cached_help_section(groups_items, "Subgroups:", max_line_len) self._print_cached_help_section(commands_items, "Commands:", max_line_len) - - # Use same az find message as non-cached path - print() # Blank line before the message - self._print_az_find_message('') + print() from azure.cli.core.util import show_updates_available show_updates_available(new_line_after=True) diff --git a/src/azure-cli-core/azure/cli/core/azclierror.py b/src/azure-cli-core/azure/cli/core/azclierror.py index 10f2f2b8097..8ad6598bd19 100644 --- a/src/azure-cli-core/azure/cli/core/azclierror.py +++ b/src/azure-cli-core/azure/cli/core/azclierror.py @@ -33,8 +33,8 @@ def __init__(self, error_msg, recommendation=None): self.recommendations = [] self.set_recommendation(recommendation) - # AI recommendations provided by Aladdin service, with tuple form: (recommendation, description) - self.aladdin_recommendations = [] + # example recommendations with tuple form: (recommendation, description) + self.example_recommendations = [] # exception trace for the error self.exception_trace = None @@ -50,11 +50,11 @@ def set_recommendation(self, recommendation): elif isinstance(recommendation, list): self.recommendations.extend(recommendation) - def set_aladdin_recommendation(self, recommendations): - """ Set aladdin recommendations for the error. + def set_example_recommendation(self, recommendations): + """ Set example recommendations for the error. One item should be a tuple with the form: (recommendation, description) """ - self.aladdin_recommendations.extend(recommendations) + self.example_recommendations.extend(recommendations) def set_exception_trace(self, exception_trace): self.exception_trace = exception_trace @@ -80,9 +80,12 @@ def print_error(self): for recommendation in self.recommendations: print(recommendation, file=sys.stderr) - if self.aladdin_recommendations: - print('\nExamples from AI knowledge base:', file=sys.stderr) - for recommendation, description in self.aladdin_recommendations: + if self.example_recommendations: + print(file=sys.stderr) + if len(self.example_recommendations) > 1: # contains help examples + print("Examples from command's help:", file=sys.stderr) + + for recommendation, description in self.example_recommendations: print_styled_text(recommendation, file=sys.stderr) print_styled_text(description, file=sys.stderr) diff --git a/src/azure-cli-core/azure/cli/core/cloud.py b/src/azure-cli-core/azure/cli/core/cloud.py index f8f90009835..9e740416a82 100644 --- a/src/azure-cli-core/azure/cli/core/cloud.py +++ b/src/azure-cli-core/azure/cli/core/cloud.py @@ -23,9 +23,6 @@ # Add names of clouds that don't allow telemetry data collection here such as some air-gapped clouds. CLOUDS_FORBIDDING_TELEMETRY = ['USSec', 'USNat'] -# Add names of clouds that don't allow Aladdin requests for command recommendations here -CLOUDS_FORBIDDING_ALADDIN_REQUEST = ['USSec', 'USNat'] - class CloudNotRegisteredException(Exception): def __init__(self, cloud_name): diff --git a/src/azure-cli-core/azure/cli/core/command_recommender.py b/src/azure-cli-core/azure/cli/core/command_recommender.py index bf2814e48f7..c5014d56195 100644 --- a/src/azure-cli-core/azure/cli/core/command_recommender.py +++ b/src/azure-cli-core/azure/cli/core/command_recommender.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from enum import Enum - from azure.cli.core import telemetry from knack.log import get_logger @@ -12,80 +10,9 @@ logger = get_logger(__name__) -class AladdinUserFaultType(Enum): - """Define the userfault types required by aladdin service - to get the command recommendations""" - - ExpectedArgument = 'ExpectedArgument' - UnrecognizedArguments = 'UnrecognizedArguments' - ValidationError = 'ValidationError' - UnknownSubcommand = 'UnknownSubcommand' - MissingRequiredParameters = 'MissingRequiredParameters' - MissingRequiredSubcommand = 'MissingRequiredSubcommand' - StorageAccountNotFound = 'StorageAccountNotFound' - Unknown = 'Unknown' - InvalidJMESPathQuery = 'InvalidJMESPathQuery' - InvalidOutputType = 'InvalidOutputType' - InvalidParameterValue = 'InvalidParameterValue' - UnableToParseCommandInput = 'UnableToParseCommandInput' - ResourceGroupNotFound = 'ResourceGroupNotFound' - InvalidDateTimeArgumentValue = 'InvalidDateTimeArgumentValue' - InvalidResourceGroupName = 'InvalidResourceGroupName' - AzureResourceNotFound = 'AzureResourceNotFound' - InvalidAccountName = 'InvalidAccountName' - - -def get_error_type(error_msg): - """The the error type of the failed command from the error message. - The error types are only consumed by aladdin service for better recommendations. - """ - - error_type = AladdinUserFaultType.Unknown - if not error_msg: - return error_type.value - - error_msg = error_msg.lower() - if 'unrecognized' in error_msg: - error_type = AladdinUserFaultType.UnrecognizedArguments - elif 'expected one argument' in error_msg or 'expected at least one argument' in error_msg \ - or 'value required' in error_msg: - error_type = AladdinUserFaultType.ExpectedArgument - elif 'misspelled' in error_msg: - error_type = AladdinUserFaultType.UnknownSubcommand - elif 'arguments are required' in error_msg or 'argument required' in error_msg: - error_type = AladdinUserFaultType.MissingRequiredParameters - if '_subcommand' in error_msg: - error_type = AladdinUserFaultType.MissingRequiredSubcommand - elif '_command_package' in error_msg: - error_type = AladdinUserFaultType.UnableToParseCommandInput - elif 'not found' in error_msg or 'could not be found' in error_msg \ - or 'resource not found' in error_msg: - error_type = AladdinUserFaultType.AzureResourceNotFound - if 'storage_account' in error_msg or 'storage account' in error_msg: - error_type = AladdinUserFaultType.StorageAccountNotFound - elif 'resource_group' in error_msg or 'resource group' in error_msg: - error_type = AladdinUserFaultType.ResourceGroupNotFound - elif 'pattern' in error_msg or 'is not a valid value' in error_msg or 'invalid' in error_msg: - error_type = AladdinUserFaultType.InvalidParameterValue - if 'jmespath_type' in error_msg: - error_type = AladdinUserFaultType.InvalidJMESPathQuery - elif 'datetime_type' in error_msg: - error_type = AladdinUserFaultType.InvalidDateTimeArgumentValue - elif '--output' in error_msg: - error_type = AladdinUserFaultType.InvalidOutputType - elif 'resource_group' in error_msg: - error_type = AladdinUserFaultType.InvalidResourceGroupName - elif 'storage_account' in error_msg: - error_type = AladdinUserFaultType.InvalidAccountName - elif "validation error" in error_msg: - error_type = AladdinUserFaultType.ValidationError - - return error_type.value - - class CommandRecommender: # pylint: disable=too-few-public-methods """Recommend a command for user when user's command fails. - It combines Aladdin recommendations and examples in help files.""" + It uses examples from help files to provide recommendations.""" def __init__(self, command, parameters, extension, error_msg, cli_ctx): """ @@ -107,8 +34,6 @@ def __init__(self, command, parameters, extension, error_msg, cli_ctx): self.cli_ctx = cli_ctx # the item is a dict with the form {'command': #, 'description': #} self.help_examples = [] - # the item is a dict with the form {'command': #, 'description': #, 'link': #} - self.aladdin_recommendations = [] def set_help_examples(self, examples): """Set help examples. @@ -119,89 +44,10 @@ def set_help_examples(self, examples): self.help_examples.extend(examples) - def _set_aladdin_recommendations(self): # pylint: disable=too-many-locals - """Set Aladdin recommendations. - Call the API, parse the response and set aladdin_recommendations. - """ - - import hashlib - import json - import requests - from requests import RequestException - from http import HTTPStatus - from azure.cli.core import __version__ as version - - api_url = 'https://app.aladdin.microsoft.com/api/v1.0/suggestions' - correlation_id = telemetry._session.correlation_id # pylint: disable=protected-access - subscription_id = telemetry._get_azure_subscription_id() # pylint: disable=protected-access - event_id = telemetry._session.event_id # pylint: disable=protected-access - # Used for DDOS protection and rate limiting - user_id = telemetry._get_user_azure_id() # pylint: disable=protected-access - hashed_user_id = hashlib.sha256(user_id.encode('utf-8')).hexdigest() - - headers = { - 'Content-Type': 'application/json', - 'X-UserId': hashed_user_id - } - context = { - 'versionNumber': version, - 'errorType': get_error_type(self.error_msg) - } - - if telemetry.is_telemetry_enabled(): - if correlation_id: - context['correlationId'] = correlation_id - if subscription_id: - context['subscriptionId'] = subscription_id - if event_id: - context['eventId'] = event_id - - parameters = self._normalize_parameters(self.parameters) - parameters = [item for item in set(parameters) if item not in ['--debug', '--verbose', '--only-show-errors']] - query = { - "command": self.command, - "parameters": ','.join(parameters) - } - - response = None - try: - response = requests.get( - api_url, - params={ - 'query': json.dumps(query), - 'clientType': 'AzureCli', - 'context': json.dumps(context) - }, - headers=headers, - timeout=1) - telemetry.set_debug_info('AladdinResponseTime', response.elapsed.total_seconds()) - - except RequestException as ex: - logger.debug('Recommendation requests.get() exception: %s', ex) - telemetry.set_debug_info('AladdinException', ex.__class__.__name__) - - recommendations = [] - if response and response.status_code == HTTPStatus.OK: - for result in response.json(): - # parse the response to get the raw command - raw_command = 'az {} '.format(result['command']) - for parameter, placeholder in zip(result['parameters'].split(','), result['placeholders'].split('♠')): - raw_command += '{} {}{}'.format(parameter, placeholder, ' ' if placeholder else '') - - # format the recommendation - recommendation = { - 'command': raw_command.strip(), - 'description': result['description'], - 'link': result['link'] - } - recommendations.append(recommendation) - - self.aladdin_recommendations.extend(recommendations) - def provide_recommendations(self): """Provide recommendations when a command fails. - The recommendations are either from Aladdin service or CLI help examples, + The recommendations are from CLI help examples, which include both commands and reference links along with their descriptions. :return: The decorated recommendations @@ -273,14 +119,7 @@ def replace_param_values(command): # pylint: disable=unused-variable if self.cli_ctx and self.cli_ctx.config.get('core', 'error_recommendation', 'on').upper() == 'OFF': return [] - # get recommendations from Aladdin service - if not self._disable_aladdin_service(): - self._set_aladdin_recommendations() - - # recommendations are either all from Aladdin or all from help examples - recommendations = self.aladdin_recommendations - if not recommendations: - recommendations = self.help_examples + recommendations = self.help_examples # sort the recommendations by parameter matching, get the top 3 recommended commands recommendations = sort_recommendations(recommendations)[:3] @@ -305,8 +144,6 @@ def replace_param_values(command): # pylint: disable=unused-variable # add reference link as a recommendation decorated_link = [(Style.HYPERLINK, OVERVIEW_REFERENCE)] - if self.aladdin_recommendations: - decorated_link = [(Style.HYPERLINK, self.aladdin_recommendations[0]['link'])] decorated_description = [(Style.SECONDARY, 'Read more about the command in reference docs')] decorated_recommendations.append((decorated_link, decorated_description)) @@ -319,49 +156,11 @@ def replace_param_values(command): # pylint: disable=unused-variable def _set_recommended_command_to_telemetry(self, raw_commands): """Set the recommended commands to Telemetry - Aladdin recommended commands and commands from CLI help examples are - set to different properties in Telemetry. - :param raw_commands: The recommended raw commands :type raw_commands: list """ - if self.aladdin_recommendations: - telemetry.set_debug_info('AladdinRecommendCommand', ';'.join(raw_commands)) - else: - telemetry.set_debug_info('ExampleRecommendCommand', ';'.join(raw_commands)) - - def _disable_aladdin_service(self): - """Decide whether to disable aladdin request when a command fails. - - The possible cases to disable it are: - 1. CLI context is missing - 2. In air-gapped clouds - 3. In testing environments - 4. In autocomplete mode - - :return: whether Aladdin service need to be disabled or not - :type: bool - """ - - from azure.cli.core.cloud import CLOUDS_FORBIDDING_ALADDIN_REQUEST - - # CLI is not started well - if not self.cli_ctx or not self.cli_ctx.cloud: - return True - - # for air-gapped clouds - if self.cli_ctx.cloud.name in CLOUDS_FORBIDDING_ALADDIN_REQUEST: - return True - - # for testing environments - if self.cli_ctx.__class__.__name__ == 'DummyCli': - return True - - if self.cli_ctx.data['completer_active']: - return True - - return False + telemetry.set_debug_info('ExampleRecommendCommand', ';'.join(raw_commands)) def _normalize_parameters(self, args): """Normalize a parameter list. diff --git a/src/azure-cli-core/azure/cli/core/parser.py b/src/azure-cli-core/azure/cli/core/parser.py index 2fba4d56922..82811ea92d7 100644 --- a/src/azure-cli-core/azure/cli/core/parser.py +++ b/src/azure-cli-core/azure/cli/core/parser.py @@ -169,7 +169,7 @@ def error(self, message): from azure.cli.core.util import QUERY_REFERENCE az_error.set_recommendation(QUERY_REFERENCE) elif recommendations: - az_error.set_aladdin_recommendation(recommendations) + az_error.set_example_recommendation(recommendations) az_error.print_error() az_error.send_telemetry() self.exit(2) @@ -324,7 +324,7 @@ def _check_value(self, action, value): recommender.set_help_examples(self.get_examples(command_name_inferred)) recommendations = recommender.provide_recommendations() if recommendations: - az_error.set_aladdin_recommendation(recommendations) + az_error.set_example_recommendation(recommendations) # remind user to check extensions if we can not find a command to recommend if isinstance(az_error, CommandNotFoundError) \ diff --git a/src/azure-cli-core/azure/cli/core/tests/test_command_recommender.py b/src/azure-cli-core/azure/cli/core/tests/test_command_recommender.py index 9cee12d07bc..2ce2a9965cf 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_command_recommender.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_command_recommender.py @@ -12,38 +12,6 @@ class TestCommandRecommender(unittest.TestCase): def sample_command(arg1, arg2): pass - def test_get_error_type(self): - from azure.cli.core.command_recommender import AladdinUserFaultType, get_error_type - - error_msg_pairs = [ - ('unrecognized', AladdinUserFaultType.UnrecognizedArguments), - ('expected one argument', AladdinUserFaultType.ExpectedArgument), - ('expected at least one argument', AladdinUserFaultType.ExpectedArgument), - ('misspelled', AladdinUserFaultType.UnknownSubcommand), - ('arguments are required', AladdinUserFaultType.MissingRequiredParameters), - ('argument required', AladdinUserFaultType.MissingRequiredParameters), - ('argument required: _subcommand', AladdinUserFaultType.MissingRequiredSubcommand), - ('argument required: _command_package', AladdinUserFaultType.UnableToParseCommandInput), - ('not found', AladdinUserFaultType.AzureResourceNotFound), - ('could not be found', AladdinUserFaultType.AzureResourceNotFound), - ('resource not found', AladdinUserFaultType.AzureResourceNotFound), - ('resource not found: storage_account', AladdinUserFaultType.StorageAccountNotFound), - ('resource not found: resource_group', AladdinUserFaultType.ResourceGroupNotFound), - ('pattern', AladdinUserFaultType.InvalidParameterValue), - ('is not a valid value', AladdinUserFaultType.InvalidParameterValue), - ('invalid', AladdinUserFaultType.InvalidParameterValue), - ('is not a valid value: jmespath_type', AladdinUserFaultType.InvalidJMESPathQuery), - ('is not a valid value: datetime_type', AladdinUserFaultType.InvalidDateTimeArgumentValue), - ('is not a valid value: --output', AladdinUserFaultType.InvalidOutputType), - ('is not a valid value: resource_group', AladdinUserFaultType.InvalidResourceGroupName), - ('is not a valid value: storage_account', AladdinUserFaultType.InvalidAccountName), - ('validation error', AladdinUserFaultType.ValidationError) - ] - - for error_msg, expected_error_type in error_msg_pairs: - result_error_type = get_error_type(error_msg) - self.assertEqual(result_error_type, expected_error_type.value) - def test_get_parameter_mappings(self): from unittest import mock from azure.cli.core import AzCommandsLoader diff --git a/src/azure-cli-core/azure/cli/core/tests/test_help.py b/src/azure-cli-core/azure/cli/core/tests/test_help.py index 3ec55758e90..51d20760c94 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_help.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_help.py @@ -739,7 +739,6 @@ def test_show_cached_help_output(self): self.assertIn('vm', output) self.assertIn('login', output) self.assertIn('version', output) - self.assertIn('az find', output) finally: sys.stdout = sys.__stdout__ diff --git a/src/azure-cli/azure/cli/command_modules/find/custom.py b/src/azure-cli/azure/cli/command_modules/find/custom.py index ed9722fdf55..c40e6aaa0c6 100644 --- a/src/azure-cli/azure/cli/command_modules/find/custom.py +++ b/src/azure-cli/azure/cli/command_modules/find/custom.py @@ -35,7 +35,12 @@ def process_query(cli_term): response = call_aladdin_service(cli_term) if response.status_code != 200: - logger.error('Unexpected Error: If it persists, please file a bug.') + logger.error( + "The `az find` command has been retired. A new experience is being developed to replace it. " + "In the meantime, please use `az --help` to explore commands and examples, " + "or visit https://aka.ms/cli_ref for reference documentation." + ) + else: if (platform.system() == 'Windows' and should_enable_styling()): colorama.init(convert=True)