/// <summary> /// Requests a list of popular commands from service. These commands are used as fall back suggestion /// if none of the predictions fit for the current input. This method should be called once per session. /// </summary> protected virtual void RequestAllPredictiveCommands() { // We don't need to block on the task. We send the HTTP request and update commands and predictions list at the background. Task.Run(async() => { var hasSentHttpRequest = false; Exception exception = null; try { AzPredictorService.SetHttpRequestHeader(_client.DefaultRequestHeaders, _azContext.HashUserId, _telemetryClient.RequestId); var httpResponseMessage = await _client.GetAsync(_commandsEndpoint); hasSentHttpRequest = true; httpResponseMessage.EnsureSuccessStatusCode(); var reply = await httpResponseMessage.Content.ReadAsStringAsync(); var commandsReply = JsonSerializer.Deserialize <IList <PredictiveCommand> >(reply, JsonUtilities.DefaultSerializerOptions); SetFallbackPredictor(commandsReply); } catch (Exception e) when(!(e is OperationCanceledException)) { exception = e; } finally { _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(AzPredictorConstants.DefaultClientId, new List <string>(), hasSentHttpRequest, exception)); } // Initialize predictions hasSentHttpRequest = false; var placeholderCommands = new string[] { AzPredictorConstants.CommandPlaceholder, AzPredictorConstants.CommandPlaceholder }; try { hasSentHttpRequest = await RequestPredictionsAsync(placeholderCommands, CancellationToken.None); } catch (ServiceRequestException e) { hasSentHttpRequest = e.IsRequestSent; exception = e.InnerException; } catch (Exception e) { exception = e; } finally { _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(AzPredictorConstants.DefaultClientId, placeholderCommands, hasSentHttpRequest, (exception is OperationCanceledException ? null : exception))); } }); }
cmd.IndexOf(AzPredictorConstants.AzCommandMoniker) > 0; // This is the Az cmdlet. /// <summary> /// Requests a list of popular commands from service. These commands are used as fall back suggestion /// if none of the predictions fit for the current input. This method should be called once per session. /// </summary> protected virtual void RequestAllPredictiveCommands() { // We don't need to block on the task. We send the HTTP request and update commands and predictions list at the background. Task.Run(async() => { var hasSentHttpRequest = false; Exception exception = null; var requestId = Guid.NewGuid().ToString(); try { AzPredictorService.SetHttpRequestHeader(_client.DefaultRequestHeaders, _azContext.HashUserId, requestId); var httpResponseMessage = await _client.GetAsync(_commandsEndpoint); hasSentHttpRequest = true; httpResponseMessage.EnsureSuccessStatusCode(); var reply = await httpResponseMessage.Content.ReadAsStringAsync(); var commandsReply = JsonSerializer.Deserialize <List <PredictiveCommand> >(reply, JsonUtilities.DefaultSerializerOptions); commandsReply.AddRange(_surveyCmdlets); SetFallbackPredictor(commandsReply); } catch (Exception e) when(!(e is OperationCanceledException)) { exception = e; } finally { _telemetryClient.RequestId = requestId; _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(null, new List <string>(), hasSentHttpRequest, exception)); } // Initialize predictions var placeholderCommands = new string[] { AzPredictorConstants.CommandPlaceholder, AzPredictorConstants.CommandPlaceholder }; return(AzPredictorUtilities.RequestPredictionAndCollectTelemetryAync(this, _telemetryClient, null, placeholderCommands, null, CancellationToken.None)); }); }
/// <summary> /// Requests preditions and collects telemetry event. /// </summary> /// <param name="azPredictorService">The service to send the request.</param> /// <param name="telemetryClient">The telemetry client to collect the data.</param> /// <param name="predictionClient">The client that initiate the telemetry event.</param> /// <param name="commands">A list of commands.</param> /// <param name="telemetryWaitTask">The task to wait before we collect the telemetry data.</param> /// <param name="cancellationToken">The cancellation token.</param> public static async Task RequestPredictionAndCollectTelemetryAync(IAzPredictorService azPredictorService, ITelemetryClient telemetryClient, PredictionClient predictionClient, IEnumerable <string> commands, TaskCompletionSource telemetryWaitTask, CancellationToken cancellationToken) { var requestId = Guid.NewGuid().ToString(); bool? hasSentHttpRequest = default; Exception exception = null; try { hasSentHttpRequest = await azPredictorService.RequestPredictionsAsync(commands, requestId, cancellationToken); } catch (ServiceRequestException e) { hasSentHttpRequest = e.IsRequestSent; exception = e.InnerException; } catch (Exception e) when(!(e is OperationCanceledException)) { exception = e; } finally { if (telemetryWaitTask != null) { await telemetryWaitTask.Task; } if (hasSentHttpRequest.HasValue) { telemetryClient.RequestId = requestId; telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(predictionClient, commands, hasSentHttpRequest.Value, exception)); } } }
/// <inheritdoc/> public virtual void RequestPredictions(IEnumerable <string> commands) { Validation.CheckArgument(commands, $"{nameof(commands)} cannot be null."); var localCommands = string.Join(AzPredictorConstants.CommandConcatenator, commands); bool postSuccess = false; Exception exception = null; bool startRequestTask = false; try { if (string.Equals(localCommands, _commandToRequestPrediction, StringComparison.Ordinal)) { // It's the same history we've already requested the prediction for last time, skip it. return; } if (commands.Any()) { SetCommandToRequestPrediction(localCommands); // When it's called multiple times, we only need to keep the one for the latest command. _predictionRequestCancellationSource?.Cancel(); _predictionRequestCancellationSource = new CancellationTokenSource(); var cancellationToken = _predictionRequestCancellationSource.Token; // We don't need to block on the task. We send the HTTP request and update prediction list at the background. startRequestTask = true; Task.Run(async() => { try { AzPredictorService.SetHttpRequestHeader(_client?.DefaultRequestHeaders, _azContext.UserId, _telemetryClient.CorrelationId); var requestContext = new PredictionRequestBody.RequestContext() { VersionNumber = this._azContext.AzVersion }; var requestBody = new PredictionRequestBody(commands) { Context = requestContext, }; var requestBodyString = JsonSerializer.Serialize(requestBody, JsonUtilities.DefaultSerializerOptions); var httpResponseMessage = await _client.PostAsync(_predictionsEndpoint, new StringContent(requestBodyString, Encoding.UTF8, "application/json"), cancellationToken); postSuccess = true; httpResponseMessage.EnsureSuccessStatusCode(); var reply = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken); var suggestionsList = await JsonSerializer.DeserializeAsync <IList <PredictiveCommand> >(reply, JsonUtilities.DefaultSerializerOptions); SetCommandBasedPreditor(localCommands, suggestionsList); } catch (Exception e) when(!(e is OperationCanceledException)) { exception = e; } finally { _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(localCommands, postSuccess, exception)); } }, cancellationToken); } } catch (Exception e) { exception = e; } finally { if (!startRequestTask) { _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(localCommands, hasSentHttpRequest: false, exception: exception)); } } }
/// <inhericdoc /> public void StartEarlyProcessing(string clientId, IReadOnlyList <string> history) { // The context only changes when the user executes the corresponding command. _azContext?.UpdateContext(); if (history.Count > 0) { // We try to find the commands to request predictions for. // We should only have "start_of_snippet" when there are no enough Az commands for prediction. // We then ignore that when there are new "start_of_snippet". // This is the scenario. // 1. New-AzResourceGroup -Name **** // 2. $resourceName="Test" // 3. $resourceLocation="westus2" // 4. New-AzVM -Name $resourceName -Location $resourceLocation // // We'll replace 2 and 3 with "start_of_snippet" but if we request prediction using 2 and 3, that'll reset the // workflow. We want to predict only by Az commands. That's to use commands 1 and 4. bool isLastTwoCommandsChanged = false; if (_lastTwoMaskedCommands.Count == 0) { // This is the first time we populate our record. Push the second to last command in history to the // queue. If there is only one command in history, push the command placeholder. if (history.Count() > 1) { string secondToLastLine = history.TakeLast(AzPredictorConstants.CommandHistoryCountToProcess).First(); var secondToLastCommand = GetAstAndMaskedCommandLine(secondToLastLine); _lastTwoMaskedCommands.Enqueue(secondToLastCommand.IsSupported ? secondToLastCommand.MaskedValue : AzPredictorConstants.CommandPlaceholder); if (secondToLastCommand.IsSupported) { _service.RecordHistory(secondToLastCommand.Ast); } } else { _lastTwoMaskedCommands.Enqueue(AzPredictorConstants.CommandPlaceholder); // We only extract parameter values from the command line in _service.RecordHistory. // So we don't need to do that for a placeholder. } isLastTwoCommandsChanged = true; } string lastLine = history.Last(); var lastCommand = GetAstAndMaskedCommandLine(lastLine); bool isLastCommandSupported = lastCommand.IsSupported; if (isLastCommandSupported) { if (_lastTwoMaskedCommands.Count == 2) { // There are already two commands, dequeue the oldest one. _lastTwoMaskedCommands.Dequeue(); } _lastTwoMaskedCommands.Enqueue(lastCommand.Item2); isLastTwoCommandsChanged = true; _service.RecordHistory(lastCommand.Item1); } else if (_lastTwoMaskedCommands.Count == 1) { isLastTwoCommandsChanged = true; var existingInQueue = _lastTwoMaskedCommands.Dequeue(); _lastTwoMaskedCommands.Enqueue(AzPredictorConstants.CommandPlaceholder); _lastTwoMaskedCommands.Enqueue(existingInQueue); } _telemetryClient.OnHistory(new HistoryTelemetryData(clientId, lastCommand.MaskedValue ?? AzPredictorConstants.CommandPlaceholder)); if (isLastTwoCommandsChanged) { // When it's called multiple times, we only need to keep the one for the latest command. _predictionRequestCancellationSource?.Cancel(); _predictionRequestCancellationSource = new CancellationTokenSource(); // Need to create a new object to hold the string. They're used in a seperate thread the the contents in // _lastTwoMaskedCommands may change when the method is called again. var lastTwoMaskedCommands = new List <string>(_lastTwoMaskedCommands); Exception exception = null; var hasSentHttpRequest = false; // We don't need to block on the task. It sends the HTTP request and update prediction list. That can run at the background. Task.Run(async() => { try { await _service.RequestPredictionsAsync(lastTwoMaskedCommands, _predictionRequestCancellationSource.Token); } catch (ServiceRequestException e) { hasSentHttpRequest = e.IsRequestSent; exception = e.InnerException; } catch (Exception e) { exception = e; } finally { _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(clientId, lastTwoMaskedCommands, hasSentHttpRequest, (exception is OperationCanceledException ? null : exception))); } }, _predictionRequestCancellationSource.Token); } } (CommandAst Ast, string MaskedValue, bool IsSupported) GetAstAndMaskedCommandLine(string commandLine) { var asts = Parser.ParseInput(commandLine, out _, out _); var allNestedAsts = asts?.FindAll((ast) => ast is CommandAst, true); var commandAst = allNestedAsts?.LastOrDefault() as CommandAst; var commandName = commandAst?.CommandElements?.FirstOrDefault().ToString(); bool isSupported = _service.IsSupportedCommand(commandName); string maskedCommandLine = CommandLineUtilities.MaskCommandLine(commandAst); return(commandAst, maskedCommandLine, isSupported); } }
/// <inhericdoc /> public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList <string> history) { _commandLineExecutedCompletion = new TaskCompletionSource(); if (history.Count > 0) { // We try to find the commands to request predictions for. // We should only have "start_of_snippet" when there are no enough Az commands for prediction. // We only send the requests when Az commands are changed. So we'll never add "start_of_snippet" again // once we have enough Az commands. // This is the scenario. // 1. New-AzResourceGroup -Name **** // 2. $resourceName="Test" // 3. $resourceLocation="westus2" // 4. New-AzVM -Name $resourceName -Location $resourceLocation // // If the history only contains 1, we'll add "start_of_snippet" to the request. // We'll replace 2 and 3 with "start_of_snippet". But if we request prediction using 2 and 3, that'll reset the // workflow. We want to predict only by Az commands. So we don't send the request until the command 4. // That's to use commands 1 and 4 to request prediction. bool ShouldRequestPrediction = false; if (_lastTwoMaskedCommands.Count == 0) { // This is the first time we populate our record. Push the second to last command in history to the // queue. If there is only one command in history, push the command placeholder. if (history.Count() > 1) { string secondToLastLine = history.TakeLast(AzPredictorConstants.CommandHistoryCountToProcess).First(); var secondToLastCommand = GetAstAndMaskedCommandLine(secondToLastLine); _lastTwoMaskedCommands.Enqueue(secondToLastCommand.IsSupported ? secondToLastCommand.MaskedCommandLine : AzPredictorConstants.CommandPlaceholder); if (secondToLastCommand.IsSupported) { _service.RecordHistory(secondToLastCommand.Ast); } } else { _lastTwoMaskedCommands.Enqueue(AzPredictorConstants.CommandPlaceholder); // We only extract parameter values from the command line in _service.RecordHistory. // So we don't need to do that for a placeholder. } ShouldRequestPrediction = true; } string lastLine = history.Last(); var lastCommand = GetAstAndMaskedCommandLine(lastLine); bool isLastCommandSupported = lastCommand.IsSupported; _parsedCommandLineHistory.TryAdd(lastLine, lastCommand); if (isLastCommandSupported) { if (_lastTwoMaskedCommands.Count == 2) { // There are already two commands, dequeue the oldest one. _lastTwoMaskedCommands.Dequeue(); } _lastTwoMaskedCommands.Enqueue(lastCommand.MaskedCommandLine); ShouldRequestPrediction = true; _service.RecordHistory(lastCommand.Ast); } else if (_lastTwoMaskedCommands.Count == 1) { ShouldRequestPrediction = true; var existingInQueue = _lastTwoMaskedCommands.Dequeue(); _lastTwoMaskedCommands.Enqueue(AzPredictorConstants.CommandPlaceholder); _lastTwoMaskedCommands.Enqueue(existingInQueue); } if (ShouldRequestPrediction) { // When it's called multiple times, we only need to keep the one for the latest command. _predictionRequestCancellationSource?.Cancel(); _predictionRequestCancellationSource = new CancellationTokenSource(); // Need to create a new object to hold the string. They're used in a seperate thread the the contents in // _lastTwoMaskedCommands may change when the method is called again. var lastTwoMaskedCommands = new List <string>(_lastTwoMaskedCommands); Exception exception = null; var hasSentHttpRequest = false; // We don't need to block on the task. It sends the HTTP request and update prediction list. That can run at the background. Task.Run(async() => { var localCommandLineExecutedCompletion = _commandLineExecutedCompletion; var requestId = Guid.NewGuid().ToString(); try { hasSentHttpRequest = await _service.RequestPredictionsAsync(lastTwoMaskedCommands, requestId, _predictionRequestCancellationSource.Token); } catch (ServiceRequestException e) { hasSentHttpRequest = e.IsRequestSent; exception = e.InnerException; } catch (Exception e) { exception = e; } finally { await localCommandLineExecutedCompletion.Task; _telemetryClient.RequestId = requestId; _telemetryClient.OnRequestPrediction(new RequestPredictionTelemetryData(client, lastTwoMaskedCommands, hasSentHttpRequest, (exception is OperationCanceledException ? null : exception))); } }, _predictionRequestCancellationSource.Token); } } }