diff --git a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui.cs b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui.cs index 3d0721cc..3958434a 100644 --- a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui.cs +++ b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui.cs @@ -3,33 +3,53 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -using System; -using System.Linq; -using System.Threading.Tasks; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using Azure.AI.Details.Common.CLI.ConsoleGui; using System.Text.Json; -using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Azure.AI.Details.Common.CLI { - public struct AiHubResourceInfo + public readonly struct AiHubResourceInfo { - public string Id; - public string Group; - public string Name; - public string RegionLocation; + [JsonProperty("id")] + public string Id { get; init; } + + [JsonProperty("resource_group")] + public string Group { get; init; } + + [JsonProperty("name")] + public string Name { get; init; } + + [JsonProperty("location")] + public string RegionLocation { get; init; } + + [JsonProperty("display_name")] + public string DisplayName { get; init; } + + public override string ToString() => $"{DisplayName ?? Name} ({RegionLocation})"; } - public struct AiHubProjectInfo + public readonly struct AiHubProjectInfo { - public string Id; - public string Group; - public string Name; - public string DisplayName; - public string RegionLocation; - public string HubId; + [JsonProperty("id")] + public string Id { get; init; } + + [JsonProperty("resource_group")] + public string Group{ get; init; } + + [JsonProperty("name")] + public string Name{ get; init; } + + [JsonProperty("display_name")] + public string DisplayName{ get; init; } + + [JsonProperty("location")] + public string RegionLocation{ get; init; } + + [JsonProperty("workspace_hub")] + public string HubId{ get; init; } + + public override string ToString() => $"{DisplayName} ({RegionLocation})"; } public partial class AiSdkConsoleGui diff --git a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreateAiHubResource.cs b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreateAiHubResource.cs index e7145346..e265569a 100644 --- a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreateAiHubResource.cs +++ b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreateAiHubResource.cs @@ -40,27 +40,23 @@ private static async Task PickOrCreateAiHubResource(bool allo var json = PythonSDKWrapper.ListResources(values, subscription); if (Program.Debug) Console.WriteLine(json); - var parsed = !string.IsNullOrEmpty(json) ? JToken.Parse(json) : null; - var items = parsed?.Type == JTokenType.Object ? parsed["resources"] : new JArray(); + var items = JsonHelpers.DeserializePropertyValueOrDefault>(json, "resources") + ?.OrderBy(res => res.DisplayName + " " + res.Name) + .ThenBy(res => res.RegionLocation) + .ToArray() + ?? Array.Empty(); var choices = new List(); - foreach (var item in items) - { - var name = item["name"].Value(); - var location = item["location"].Value(); - var displayName = item["display_name"].Value(); - - choices.Add(string.IsNullOrEmpty(displayName) - ? $"{name} ({location})" - : $"{displayName} ({location})"); - } - if (allowCreate) { - choices.Insert(0, "(Create w/ integrated Open AI + AI Services)"); - choices.Insert(1, "(Create w/ standalone Open AI resource)"); + choices.Add("(Create w/ integrated Open AI + AI Services)"); + choices.Add("(Create w/ standalone Open AI resource)"); } + choices.AddRange(items + .Select(item => + $"{(string.IsNullOrEmpty(item.DisplayName) ? item.Name : item.DisplayName)} ({item.RegionLocation})")); + if (choices.Count == 0) { throw new ApplicationException($"CANCELED: No resources found"); @@ -74,9 +70,9 @@ private static async Task PickOrCreateAiHubResource(bool allo } Console.WriteLine($"\rName: {choices[picked]}"); - var resource = allowCreate - ? (picked >= 2 ? items.ToArray()[picked - 2] : null) - : items.ToArray()[picked]; + AiHubResourceInfo resource = allowCreate + ? (picked >= 2 ? items[picked - 2] : default) + : items[picked]; var byoServices = allowCreate && picked == 1; if (byoServices) @@ -109,7 +105,7 @@ private static async Task PickOrCreateAiHubResource(bool allo return FinishPickOrCreateAiHubResource(values, resource); } - private static async Task TryCreateAiHubResourceInteractive(ICommandValues values, string subscription) + private static async Task TryCreateAiHubResourceInteractive(ICommandValues values, string subscription) { var locationName = values.GetOrDefault("service.resource.region.name", ""); var groupName = ResourceGroupNameToken.Data().GetOrDefault(values); @@ -125,25 +121,17 @@ private static async Task TryCreateAiHubResourceInteractive(ICommandValu return await TryCreateAiHubResourceInteractive(values, subscription, locationName, groupName, displayName, description, openAiResourceId, openAiResourceKind, smartName, smartNameKind); } - private static AiHubResourceInfo FinishPickOrCreateAiHubResource(ICommandValues values, JToken resource) + private static AiHubResourceInfo FinishPickOrCreateAiHubResource(ICommandValues values, AiHubResourceInfo resource) { - var aiHubResource = new AiHubResourceInfo - { - Id = resource["id"].Value(), - Group = resource["resource_group"].Value(), - Name = resource["name"].Value(), - RegionLocation = resource["location"].Value(), - }; - - ResourceIdToken.Data().Set(values, aiHubResource.Id); - ResourceNameToken.Data().Set(values, aiHubResource.Name); - ResourceGroupNameToken.Data().Set(values, aiHubResource.Group); - RegionLocationToken.Data().Set(values, aiHubResource.RegionLocation); - - return aiHubResource; + ResourceIdToken.Data().Set(values, resource.Id); + ResourceNameToken.Data().Set(values, resource.Name); + ResourceGroupNameToken.Data().Set(values, resource.Group); + RegionLocationToken.Data().Set(values, resource.RegionLocation); + + return resource; } - private static async Task TryCreateAiHubResourceInteractive(ICommandValues values, string subscription, string locationName, string groupName, string displayName, string description, string openAiResourceId, string openAiResourceKind, string smartName = null, string smartNameKind = null) + private static async Task TryCreateAiHubResourceInteractive(ICommandValues values, string subscription, string locationName, string groupName, string displayName, string description, string openAiResourceId, string openAiResourceKind, string smartName = null, string smartNameKind = null) { var sectionHeader = $"\n`CREATE AZURE AI RESOURCE`"; ConsoleHelpers.WriteLineWithHighlight(sectionHeader); @@ -178,8 +166,7 @@ private static async Task TryCreateAiHubResourceInteractive(ICommandValu Console.WriteLine("\r*** CREATED *** "); - var parsed = !string.IsNullOrEmpty(json) ? JToken.Parse(json) : null; - return parsed["resource"]; + return JsonHelpers.DeserializePropertyValueOrDefault(json, "resource"); } } } diff --git a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreate_AiHubProject.cs b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreate_AiHubProject.cs index 1fa0529e..daa23646 100644 --- a/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreate_AiHubProject.cs +++ b/src/common/details/ai_python_generative_sdk/AiSdkConsoleGui_PickOrCreate_AiHubProject.cs @@ -95,8 +95,7 @@ public static AiHubProjectInfo PickOrCreateAiHubProject(ICommandValues values, s public static AiHubProjectInfo CreateAiHubProject(ICommandValues values, string subscription, string resourceId) { - var project = TryCreateAiHubProjectInteractive(values, subscription, resourceId); - return AiHubProjectInfoFromToken(values, project); + return TryCreateAiHubProjectInteractive(values, subscription, resourceId); } private static AiHubProjectInfo PickOrCreateAiHubProject(bool allowCreate, ICommandValues values, string subscription, string resourceId, out bool createNew) @@ -107,34 +106,23 @@ private static AiHubProjectInfo PickOrCreateAiHubProject(bool allowCreate, IComm var json = PythonSDKWrapper.ListProjects(values, subscription); if (Program.Debug) Console.WriteLine(json); - var parsed = !string.IsNullOrEmpty(json) ? JToken.Parse(json) : null; - var items = parsed?.Type == JTokenType.Object ? parsed["projects"] : new JArray(); + var items = JsonHelpers.DeserializePropertyValueOrDefault>(json, "projects") + ?.Where(proj => string.IsNullOrEmpty(resourceId) || proj.HubId == resourceId) + .OrderBy(item => item.DisplayName + " " + item.Name) + .ThenBy(item => item.RegionLocation) + .ToArray() + ?? Array.Empty(); var choices = new List(); - var itemJTokens = new List(); - foreach (var item in items) - { - var hub = item["workspace_hub"]?.Value(); - - var hubOk = string.IsNullOrEmpty(resourceId) || hub == resourceId; - if (!hubOk) continue; - - itemJTokens.Add(item); - - var name = item["name"].Value(); - var location = item["location"].Value(); - var displayName = item["display_name"].Value(); - - choices.Add(string.IsNullOrEmpty(displayName) - ? $"{name} ({location})" - : $"{displayName} ({location})"); - } - if (allowCreate) { - choices.Insert(0, "(Create new)"); + choices.Add("(Create new)"); } + choices.AddRange(items + .Select(proj => + $"{(string.IsNullOrEmpty(proj.DisplayName) ? proj.Name : proj.DisplayName)} ({proj.RegionLocation})")); + if (choices.Count == 0) { throw new ApplicationException($"CANCELED: No projects found"); @@ -148,20 +136,27 @@ private static AiHubProjectInfo PickOrCreateAiHubProject(bool allowCreate, IComm } Console.WriteLine($"\rName: {choices[picked]}"); - var project = allowCreate - ? (picked > 0 ? itemJTokens[picked - 1] : null) - : itemJTokens[picked]; - createNew = allowCreate && picked == 0; - if (createNew) + createNew = false; + AiHubProjectInfo project; + if (allowCreate && picked == 0) { + createNew = true; project = TryCreateAiHubProjectInteractive(values, subscription, resourceId); } + else if (allowCreate && picked > 0) + { + project = items[picked - 1]; + } + else + { + project = items[picked]; + } - return AiHubProjectInfoFromToken(values, project); + return project; } - private static JToken TryCreateAiHubProjectInteractive(ICommandValues values, string subscription, string resourceId) + private static AiHubProjectInfo TryCreateAiHubProjectInteractive(ICommandValues values, string subscription, string resourceId) { var group = ResourceGroupNameToken.Data().GetOrDefault(values); var location = RegionLocationToken.Data().GetOrDefault(values, ""); @@ -174,22 +169,7 @@ private static JToken TryCreateAiHubProjectInteractive(ICommandValues values, st return TryCreateAiHubProjectInteractive(values, subscription, resourceId, group, location, ref displayName, ref description, smartName, smartNameKind); } - private static AiHubProjectInfo AiHubProjectInfoFromToken(ICommandValues values, JToken project) - { - var aiHubProject = new AiHubProjectInfo - { - Id = project["id"].Value(), - Group = project["resource_group"].Value(), - Name = project["name"].Value(), - DisplayName = project["display_name"].Value(), - RegionLocation = project["location"].Value(), - HubId = project["workspace_hub"].Value(), - }; - - return aiHubProject; - } - - private static JToken TryCreateAiHubProjectInteractive(ICommandValues values, string subscription, string resourceId, string group, string location, ref string displayName, ref string description, string smartName = null, string smartNameKind = null) + private static AiHubProjectInfo TryCreateAiHubProjectInteractive(ICommandValues values, string subscription, string resourceId, string group, string location, ref string displayName, ref string description, string smartName = null, string smartNameKind = null) { ConsoleHelpers.WriteLineWithHighlight($"\n`CREATE AZURE AI PROJECT`"); @@ -208,8 +188,7 @@ private static JToken TryCreateAiHubProjectInteractive(ICommandValues values, st Console.WriteLine("\r*** CREATED *** "); - var parsed = !string.IsNullOrEmpty(json) ? JToken.Parse(json) : null; - return parsed["project"]; + return JsonHelpers.DeserializePropertyValueOrDefault(json, "project"); } public static void GetOrCreateAiHubProjectConnections(ICommandValues values, bool create, string subscription, string groupName, string projectName, string openAiEndpoint, string openAiKey, string searchEndpoint, string searchKey) diff --git a/src/common/details/azcli/AzCli.cs b/src/common/details/azcli/AzCli.cs index 4126481d..a8f8ae1e 100644 --- a/src/common/details/azcli/AzCli.cs +++ b/src/common/details/azcli/AzCli.cs @@ -181,18 +181,7 @@ public static async Task> ListAccoun var accounts = parsed.Payload; var x = new ParsedJsonProcessOutput(parsed.Output); - x.Payload = new SubscriptionInfo[accounts.Count]; - - var i = 0; - foreach (var account in accounts) - { - x.Payload[i].Id = account["Id"].Value(); - x.Payload[i].Name = account["Name"].Value(); - x.Payload[i].IsDefault = account["IsDefault"].Value(); - x.Payload[i].UserName = account["UserName"].Value(); - i++; - } - + x.Payload = accounts.ToObject(); return x; } diff --git a/src/common/details/azcli/AzCliConsoleGui_SubscriptionPicker.cs b/src/common/details/azcli/AzCliConsoleGui_SubscriptionPicker.cs index b5ea398a..1e23e351 100644 --- a/src/common/details/azcli/AzCliConsoleGui_SubscriptionPicker.cs +++ b/src/common/details/azcli/AzCliConsoleGui_SubscriptionPicker.cs @@ -155,19 +155,22 @@ public static async Task PickSubscriptionIdAsync(bool allowInteractiveLo private static AzCli.SubscriptionInfo? ListBoxPickSubscription(AzCli.SubscriptionInfo[] subscriptions) { - var list = subscriptions.Select(x => x.Name).ToArray(); - var defaultIndex = subscriptions.Select((x, i) => new { x, i }).Where(x => x.x.IsDefault).Select(x => x.i).FirstOrDefault(); - - var picked = ListBoxPicker.PickIndexOf(list, defaultIndex); - if (picked < 0) + var selected = ListBoxPicker.PickValue( + subscriptions + .Select(s => new ListBoxPickerChoice() + { + IsDefault = s.IsDefault, + DisplayName = s.Name, + Value = s + })); + if (selected == null) { throw new OperationCanceledException("User canceled"); } - var subscription = subscriptions[picked]; - DisplayNameAndId(subscription); - CacheSubscriptionUserName(subscription); - return subscription; + DisplayNameAndId(selected.Value); + CacheSubscriptionUserName(selected.Value); + return selected.Value; } private static bool MatchSubscriptionFilter(AzCli.SubscriptionInfo subscription, string subscriptionFilter) diff --git a/src/common/details/console/gui/controls/ListBoxPicker.cs b/src/common/details/console/gui/controls/ListBoxPicker.cs index 4cecb8e9..659e68fd 100644 --- a/src/common/details/console/gui/controls/ListBoxPicker.cs +++ b/src/common/details/console/gui/controls/ListBoxPicker.cs @@ -3,13 +3,15 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -using System; -using System.Linq; -using System.Text; -using Azure.AI.Details.Common.CLI; - namespace Azure.AI.Details.Common.CLI.ConsoleGui { + public interface IListBoxPickerChoice + { + bool IsDefault { get; } + + string DisplayName { get; } + } + public class ListBoxPicker : SpeedSearchListBoxControl { public static int PickIndexOf(string[] choices, int select = 0) @@ -60,6 +62,30 @@ public static string PickString(string[] lines, int width, int height, Colors no : null; } + public static TVal PickValue(IEnumerable items) where TVal : IListBoxPickerChoice + { + if (items == null) + { + return default; + } + + int? select = null; + var displayTexts = items + .Select((item, index) => + { + if (item.IsDefault && !select.HasValue) + { + select = index; + } + + return item.DisplayName; + }) + .ToArray(); + + int selected = PickIndexOf(displayTexts, select ?? 0); + return items.ElementAtOrDefault(selected); // this handles negatives and values out of bound + } + public override bool ProcessKey(ConsoleKeyInfo key) { var processed = ProcessSpeedSearchKey(key); @@ -101,4 +127,49 @@ protected ListBoxPicker(Window parent, Rect rect, Colors colorNormal, Colors col #endregion } + + public class ListBoxPickerChoice : IListBoxPickerChoice + { + public ListBoxPickerChoice() + { } + + public ListBoxPickerChoice(string displayName, TVal value, bool isDefault = false) + { + IsDefault = isDefault; + DisplayName = displayName; + Value = value; + } + + public bool IsDefault { get; set; } + + public string DisplayName { get; set; } + + public TVal Value { get; set; } + + public override string ToString() => DisplayName; + } + + public class ListBoxPickerChoice : ListBoxPickerChoice + { + public ListBoxPickerChoice() + { } + + public ListBoxPickerChoice(string displayName, TVal value, TMeta metadata = default, bool isDefault = false) + : base(displayName, value, isDefault) + { + Metadata = metadata; + } + + public TMeta Metadata { get; set; } + } + + public class ListBoxPickerChoice : ListBoxPickerChoice + { + public ListBoxPickerChoice() + { } + + public ListBoxPickerChoice(string displayName, string value, string metadata = null, bool isDefault = false) + : base(displayName, value, metadata, isDefault) + { } + } } diff --git a/src/common/details/helpers/json_helpers.cs b/src/common/details/helpers/json_helpers.cs index c8cb8ccf..f62eb395 100644 --- a/src/common/details/helpers/json_helpers.cs +++ b/src/common/details/helpers/json_helpers.cs @@ -214,6 +214,39 @@ public static void PrintJson(string text, string indent = " ", bool naked = fal } } + /// + /// Helper method to deserialize the value of a specific named property from a JSON string + /// + /// The type of the value to deserialize + /// The JSON string to deserialize + /// The name of the property to deserialize + /// The deserialized value, or the default value in the case of errors (e.g. null string, + /// property didn't exist, etc...) + public static TValue DeserializePropertyValueOrDefault(string json, string propertyName) + { + TValue value = default; + + try + { + JToken token = null; + if (!string.IsNullOrWhiteSpace(json)) + { + token = JToken.Parse(json); + } + + JToken property = token?[propertyName ?? string.Empty]; + if (property != null) + { + value = property.ToObject(); + } + } + catch (Exception) + { + } + + return value; + } + private static void PrintJson(JToken token, string indent = " ", bool naked = false) { var print = !naked