/// <summary> /// If we have an amount then perform normalization. /// </summary> /// <param name="state">State.</param> /// <param name="change_sign_of_amount">Indicate whether to change sign on amount.</param> /// <returns>SettingAmount and relative indication.</returns> private (SettingAmount amount, bool isRelative) OptionalAmount(AutomotiveSkillState state, bool change_sign_of_amount) { if (state == null) { throw new ArgumentNullException(nameof(state)); } SettingAmount optional_amount = null; bool isRelative = false; if (state.Entities.TryGetValue("AMOUNT", out var amountEntityValues)) { foreach (var amount_entity_value in amountEntityValues) { var normalized_amount = this.amountNormalizer.NormalizeOrNull(amount_entity_value); if (normalized_amount != null) { optional_amount = new SettingAmount { Unit = "%" }; if ("+-".Equals(normalized_amount)) { if (change_sign_of_amount) { optional_amount.Amount = 0.0; } else { optional_amount.Amount = 100.0; } } else { optional_amount.Amount = double.Parse(normalized_amount, CultureInfo.InvariantCulture); } } else { foreach (var chunk in this.numberNormalizer.SplitNumbers(amount_entity_value)) { if (chunk.NumericValue != null) { optional_amount = new SettingAmount(); optional_amount.Amount = chunk.NumericValue.Value; // Deal with ASR error that transcribes "to 24" as "224" if (!state.Entities.ContainsKey("TYPE") && ToAs2Pattern.Match(amount_entity_value).Success) { optional_amount.Amount = optional_amount.Amount - 200; isRelative = false; } break; } } } if (optional_amount != null) { if (state.Entities.TryGetValue("TYPE", out var typeEntityValues)) { foreach (var typeEntityValue in typeEntityValues) { var normalized_type = this.typeNormalizer.NormalizeOrNull(typeEntityValue); if (normalized_type != null) { isRelative = "DELTA".Equals(normalized_type); break; } } } if (string.IsNullOrEmpty(optional_amount.Unit) && state.Entities.TryGetValue("UNIT", out var unitEntityValues)) { foreach (var unitEntityValue in unitEntityValues) { var normalized_unit = this.unitNormalizer.NormalizeOrNull(unitEntityValue); if (normalized_unit != null) { optional_amount.Unit = normalized_unit; } else { optional_amount.Unit = unitEntityValue; } break; } } break; } } } if (change_sign_of_amount && optional_amount != null && isRelative) { optional_amount.Amount = -optional_amount.Amount; } return(optional_amount, isRelative); }
private IList <T> ApplySelectionToSettings <T>(AutomotiveSkillState state, List <string> settingEntities, IList <T> changesOrStatuses) where T : SettingOperation { if (state == null) { throw new ArgumentNullException(nameof(state)); } if (settingEntities == null) { throw new ArgumentNullException(nameof(settingEntities)); } if (changesOrStatuses == null) { throw new ArgumentNullException(nameof(changesOrStatuses)); } var settingNames = state.GetUniqueSettingNames(); ISet <string> selectedSettingNames = new HashSet <string>(); if (settingEntities.Any() && settingNames.Any()) { IList <AvailableSetting> resolvedSettings = new List <AvailableSetting>(); foreach (var settingName in settingNames) { var setting = this.settingList.FindSetting(settingName); if (setting != null) { resolvedSettings.Add(setting); } else { setting = new AvailableSetting { CanonicalName = settingName }; resolvedSettings.Add(setting); } } IList <AvailableSetting> settings_to_select_from = Util.CopyList(resolvedSettings); foreach (var setting in resolvedSettings) { if (setting.IncludedSettings != null) { foreach (var included_setting_name in setting.IncludedSettings) { if (!settingNames.Contains(included_setting_name)) { var included_setting = this.settingList.FindSetting(included_setting_name); if (included_setting == null) { // Unreachable. throw new Exception("The included settings of setting \"" + setting.CanonicalName + "\" must be canonical names of other settings, but \"" + included_setting_name + "\" is not and this should already have been checked when loading the SettingList."); } settings_to_select_from.Add(included_setting); } } } } var setting_matcher = new SettingMatcher(this.settingList.CreateSubList(settings_to_select_from)); var selected_settings = setting_matcher.MatchSettingNamesExactly(settingEntities.First()); if (!selected_settings.Any()) { selected_settings = setting_matcher.MatchSettingNames( settingEntities, SettingNameScoreThreshold, SettingNameAntonymDisambPercentageOfMax, true); } foreach (var setting_info in selected_settings) { selectedSettingNames.Add(setting_info.CanonicalName); } } IList <T> newCandidates = new List <T>(); ISet <string> addedSettingNames = new HashSet <string>(); foreach (var candidate in changesOrStatuses) { if (candidate == null) { continue; } if (selectedSettingNames.Contains(candidate.SettingName)) { newCandidates.Add(candidate); addedSettingNames.Add(candidate.SettingName); } } // If NLP tells us to select something that isn't on the list, // it's because it's included in one of the settings on the list. foreach (var selectedName in selectedSettingNames) { if (!addedSettingNames.Contains(selectedName)) { // This search is inefficient, but the lists will be short, so it doesn't matter. foreach (var candidate in changesOrStatuses) { var supportedSetting = settingList.FindSetting(candidate.SettingName); if (supportedSetting != null && supportedSetting.IncludedSettings != null && supportedSetting.IncludedSettings.Contains(selectedName)) { var newCandidate = (T)candidate.Clone(); newCandidate.SettingName = selectedName; newCandidates.Add(newCandidate); break; } } } } if (!Util.IsNullOrEmpty(newCandidates)) { return(newCandidates); } return(changesOrStatuses); }
/// <summary> /// Apply the selecting setting value to the setting values. /// </summary> /// <param name="state">State object.</param> /// <param name="entityValues">List of entity values.</param> /// <returns>Setting.</returns> public IList <SettingChange> ApplySelectionToSettingValues(AutomotiveSkillState state, List <string> entityValues) { if (state == null) { throw new ArgumentNullException(nameof(state)); } if (entityValues == null) { throw new ArgumentNullException(nameof(entityValues)); } var settingValues = state.GetUniqueSettingValues(); ISet <string> selectedSettingValues = new HashSet <string>(); if (entityValues.Any() && settingValues.Any()) { IList <SelectableSettingValue> selectableSettingValues = new List <SelectableSettingValue>(); foreach (var change in state.Changes) { SelectableSettingValue selectable = new SelectableSettingValue { CanonicalSettingName = change.SettingName }; var availableValue = this.settingList.FindSettingValue(change.SettingName, change.Value); if (availableValue != null) { selectable.Value = availableValue; } else { availableValue = new AvailableSettingValue { CanonicalName = change.Value }; selectable.Value = availableValue; } selectableSettingValues.Add(selectable); } var selected_values = this.settingMatcher.DisambiguateSettingValues( entityValues, selectableSettingValues, SettingValueAntonymDisambThreshold, SettingValueAntonymDisambPercentageOfMax); foreach (var selected_value in selected_values) { selectedSettingValues.Add(selected_value.Value.CanonicalName); } } IList <SettingChange> newCandidates = new List <SettingChange>(); foreach (var candidate in state.Changes) { if (selectedSettingValues.Contains(candidate.Value)) { newCandidates.Add(candidate); } } if (!Util.IsNullOrEmpty(newCandidates)) { return(newCandidates); } return(state.Changes); }
/// <summary> /// Take the entities provided by LUIS (Setting and Value) to try and identify the vehicle setting we need to process. /// </summary> /// <param name="state">State object.</param> /// <param name="declarative">Indicates if special process for declarative utterances should be performed.</param> public void PostProcessSettingName(AutomotiveSkillState state, bool declarative = false) { if (state == null) { throw new ArgumentNullException(nameof(state)); } IList <SettingMatch> setting_matches = new List <SettingMatch>(); var has_matching_value_for_any_setting = false; ISet <string> setting_names_to_remove = new HashSet <string>(); // The Setting entity will contain any identified vehicle setting that was present in the utterance, e.g. front right airflow // The Value entity will contain any identified value relating to a vehicle setting that was present in the utterance, e.g. warm IList <AvailableSetting> selected_settings = new List <AvailableSetting>(); if (state.Entities.ContainsKey("SETTING")) { // If we have a Setting then try to find a match between the setting name provided and the available settings selected_settings = this.settingMatcher.MatchSettingNamesExactly(state.Entities["SETTING"].First()); // If we have not found an exact setting match but we have a value then combine Setting and Value together to identify a match if (!selected_settings.Any() && state.Entities.ContainsKey("VALUE")) { /* First try SETTING + VALUE entities combined to catch cases like "warm my seat", * where the value can help disambiguate which setting the user meant.*/ List <string> entityValuesToMatch = new List <string>(); entityValuesToMatch.AddRange(state.Entities["SETTING"]); entityValuesToMatch.AddRange(state.Entities["VALUE"]); selected_settings = this.settingMatcher.MatchSettingNames( entityValuesToMatch, SettingNameScoreThreshold, SettingNameAntonymDisambPercentageOfMax, false); } // If we still haven't found a match then try to match with just the setting but not exactly this time if (!selected_settings.Any()) { List <string> entityValuesToMatch = new List <string>(); entityValuesToMatch.AddRange(state.Entities["SETTING"]); selected_settings = this.settingMatcher.MatchSettingNames( entityValuesToMatch, SettingNameScoreThreshold, SettingNameAntonymDisambPercentageOfMax, false); } } // Do we have a selected setting name? if (selected_settings.Any()) { List <string> entityValuesToMatch = new List <string>(); List <string> entity_types_for_value_disamb = new List <string>(); if (state.Entities.ContainsKey("VALUE")) { entityValuesToMatch.AddRange(state.Entities["VALUE"]); } else if (state.Entities.ContainsKey("SETTING")) { // Sometimes the setting name itself is also a value, e.g., "defog" entityValuesToMatch.AddRange(state.Entities["SETTING"]); } foreach (var setting_info in selected_settings) { IList <SelectableSettingValue> selected_values = new List <SelectableSettingValue>(); if (entityValuesToMatch.Any()) { IList <SelectableSettingValue> selectable_values = new List <SelectableSettingValue>(); foreach (var value in setting_info.Values) { SelectableSettingValue selectable = new SelectableSettingValue { CanonicalSettingName = setting_info.CanonicalName, Value = value }; selectable_values.Add(selectable); } /* From the available setting values for the given setting name identify which one applies for this setting name * e.g. Set (when users says set temperature to 21 degrees * e.g. Increase (when user says increase temperature) * e.g. Decrease (when user says decrease temperature) * e.g. Off, Alert, Alert and Brake when user wants to control Park Assist */ selected_values = this.settingMatcher.DisambiguateSettingValues( entityValuesToMatch, selectable_values, SettingValueAntonymDisambThreshold, SettingValueAntonymDisambPercentageOfMax); // If we don't even have a VALUE entity, we can't match multiple values. // If the SETTING entity is really also a value, then it must match only one value. if (!state.Entities.ContainsKey("VALUE") && selected_values.Count() > 1) { selected_values.Clear(); } // For all selected values we return the canonical name for both the name and value foreach (var selected_value in selected_values) { SettingMatch match = new SettingMatch { SettingName = setting_info.CanonicalName, Value = selected_value.Value.CanonicalName }; setting_matches.Add(match); has_matching_value_for_any_setting = true; } } if (!selected_values.Any()) { SettingMatch match = new SettingMatch { SettingName = setting_info.CanonicalName }; setting_matches.Add(match); } AddAll(setting_names_to_remove, setting_info.IncludedSettings); } } else if (state.Entities.ContainsKey("VALUE") && !state.Entities.ContainsKey("SETTING")) { /* If we have no SETTING entity, match the VALUE entities against all the values of all the settings. * This handles queries like "make it warmer" or "defog", where the value implies the setting.*/ List <string> entityValuesToMatch = new List <string>(); entityValuesToMatch.AddRange(state.Entities["VALUE"]); setting_matches = this.settingMatcher.MatchSettingValues( entityValuesToMatch, SettingValueScoreThreshold, SettingValueAntonymDisambPercentageOfMax); has_matching_value_for_any_setting = true; foreach (var match in setting_matches) { var setting_info = this.settingList.FindSetting(match.SettingName); if (setting_info != null) { AddAll(setting_names_to_remove, setting_info.IncludedSettings); } } } // If at least one setting has a matching value, remove all settings with no matching value. // This effectively disambiguates the settings by their available values. // Also remove 'included' settings. IList <SettingMatch> new_setting_matches = new List <SettingMatch>(); foreach (var match in setting_matches) { if ((!has_matching_value_for_any_setting || !string.IsNullOrEmpty(match.Value)) && !setting_names_to_remove.Contains(match.SettingName)) { new_setting_matches.Add(match); } } setting_matches = new_setting_matches; var(opt_amount, isRelative) = OptionalAmount(state, false); foreach (var setting_match in setting_matches) { SettingChange setting_change = new SettingChange { SettingName = setting_match.SettingName }; var value_info = this.settingList.FindSettingValue(setting_match.SettingName, setting_match.Value); if (declarative) { // If the user makes a declarative statement, it means that they're unhappy with the status quo. // So, we use the antonym of the value to get the opposite of the thing they're unhappy with, // which should hopefully make them happy. // If there is no antonym listed, then we want to return an empty value because we were unable to find // the correct value. if (value_info != null) { setting_change.Value = value_info.Antonym; } } else { setting_change.Value = setting_match.Value; } if (opt_amount != null && value_info != null && value_info.ChangesSignOfAmount) { (opt_amount, isRelative) = OptionalAmount(state, true); } setting_change.Amount = opt_amount; setting_change.IsRelativeAmount = isRelative; state.Changes.Add(setting_change); } if (!setting_matches.Any() && opt_amount != null) { SettingChange setting_change = new SettingChange { Amount = opt_amount, IsRelativeAmount = isRelative }; state.Changes.Add(setting_change); } }
/// <summary> /// Further process the entities and remove those that are invalid based on the entity to ensure we prompt for values and don't accept incorrect values. /// </summary> /// <param name="state">State object.</param> public void ApplyContentLogic(AutomotiveSkillState state) { if (state == null) { throw new ArgumentNullException(nameof(state)); } if (state.Changes != null && state.Changes.Count > 0) { IList <SettingChange> validChanges = new List <SettingChange>(); IList <SettingChange> invalidChanges = new List <SettingChange>(); foreach (var change in state.Changes) { var validity = ValidateChange(change); if (validity == ValidationStatus.Valid) { validChanges.Add(change); } else if (ValueRelatedValidities.Contains(validity)) { var settingInfo = settingList.FindSetting(change.SettingName); if (settingInfo != null && !Util.IsNullOrEmpty(settingInfo.Values)) { IList <SettingChange> validReplacements = new List <SettingChange>(); ValidationStatus replacementValidity = ValidationStatus.None; foreach (var valueInfo in settingInfo.Values) { var newChange = (SettingChange)change.Clone(); newChange.Value = valueInfo.CanonicalName; validity = ValidateChange(newChange); if (validity == ValidationStatus.Valid) { validReplacements.Add(newChange); } else if (replacementValidity == ValidationStatus.None) { replacementValidity = validity; } } if (!Util.IsNullOrEmpty(validReplacements)) { state.Entities.Remove("VALUE"); foreach (var replacement in validReplacements) { validChanges.Add(replacement); } } else { invalidChanges.Add(change); } } else { invalidChanges.Add(change); } } else { invalidChanges.Add(change); } } if (validChanges.Any()) { state.Changes = validChanges; } else { state.Changes = invalidChanges; } } }