From 7f54bcc181fed08d06ee44fd2a44275b055511bf Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Wed, 19 Mar 2025 12:17:23 -0700 Subject: [PATCH 1/6] Added AI toolkit button --- .../Models/AIToolkitAction.cs | 15 ++++ AIDevGallery.SourceGenerator/Models/Model.cs | 3 + .../ModelsSourceGenerator.cs | 8 +- AIDevGallery/Models/Samples.cs | 12 +++ AIDevGallery/Pages/Models/ModelPage.xaml | 27 +++++-- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 81 ++++++++++++++++++- .../Models/languagemodels.modelgroup.json | 32 ++++++-- .../Models/multimodal.modelgroup.json | 8 +- .../Events/AIToolkitActionClickedEvent.cs | 35 ++++++++ AIDevGallery/Utils/AIToolkitHelper.cs | 71 ++++++++++++++++ 10 files changed, 272 insertions(+), 20 deletions(-) create mode 100644 AIDevGallery.SourceGenerator/Models/AIToolkitAction.cs create mode 100644 AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs create mode 100644 AIDevGallery/Utils/AIToolkitHelper.cs diff --git a/AIDevGallery.SourceGenerator/Models/AIToolkitAction.cs b/AIDevGallery.SourceGenerator/Models/AIToolkitAction.cs new file mode 100644 index 00000000..6851cfe0 --- /dev/null +++ b/AIDevGallery.SourceGenerator/Models/AIToolkitAction.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace AIDevGallery.SourceGenerator.Models; + +[JsonConverter(typeof(JsonStringEnumConverter))] +internal enum AIToolkitAction +{ + FineTuning, + PromptBuilder, + BulkRun, + Playground +} \ No newline at end of file diff --git a/AIDevGallery.SourceGenerator/Models/Model.cs b/AIDevGallery.SourceGenerator/Models/Model.cs index f6c56caf..4b41dd26 100644 --- a/AIDevGallery.SourceGenerator/Models/Model.cs +++ b/AIDevGallery.SourceGenerator/Models/Model.cs @@ -24,4 +24,7 @@ internal class Model [JsonConverter(typeof(SingleOrListOfStringConverter))] [JsonPropertyName("FileFilter")] public List? FileFilters { get; init; } + public List? AIToolkitActions { get; set; } + public string? AIToolkitId { get; set; } + public string? AIToolkitFinetuningId { get; set; } } \ No newline at end of file diff --git a/AIDevGallery.SourceGenerator/ModelsSourceGenerator.cs b/AIDevGallery.SourceGenerator/ModelsSourceGenerator.cs index 95ac4f1e..6cadd9e5 100644 --- a/AIDevGallery.SourceGenerator/ModelsSourceGenerator.cs +++ b/AIDevGallery.SourceGenerator/ModelsSourceGenerator.cs @@ -250,6 +250,9 @@ private void GenerateModelDetails(StringBuilder sourceBuilder, Dictionary $"\"{ff}\"")) : string.Empty; + var aiToolkitActions = modelDefinition.AIToolkitActions != null ? string.Join(", ", modelDefinition.AIToolkitActions.Select(action => $"AIToolkitAction.{action}")) : string.Empty; + var aiToolkitId = !string.IsNullOrEmpty(modelDefinition.AIToolkitId) ? $"\"{modelDefinition.AIToolkitId}\"" : "null"; + var aiToolkitFinetuningId = !string.IsNullOrEmpty(modelDefinition.AIToolkitFinetuningId) ? $"\"{modelDefinition.AIToolkitFinetuningId}\"" : "null"; sourceBuilder.AppendLine( $$"""" @@ -268,7 +271,10 @@ private void GenerateModelDetails(StringBuilder sourceBuilder, Dictionary? FileFilters { get; set; } + public List? AIToolkitActions { get; set; } + public string? AIToolkitId { get; set; } + public string? AIToolkitFinetuningId { get; set; } private ModelCompatibility? compatibility; [JsonIgnore(Condition = JsonIgnoreCondition.Always)] @@ -162,5 +165,14 @@ internal enum HardwareAccelerator WCRAPI } +[JsonConverter(typeof(JsonStringEnumConverter))] +internal enum AIToolkitAction +{ + FineTuning, + PromptBuilder, + BulkRun, + Playground +} + #pragma warning restore SA1402 // File may only contain a single type #pragma warning restore SA1649 // File name should match first type name \ No newline at end of file diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml b/AIDevGallery/Pages/Models/ModelPage.xaml index a9a70b75..b2872dec 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml +++ b/AIDevGallery/Pages/Models/ModelPage.xaml @@ -32,14 +32,27 @@ Margin="4,0,0,0" Style="{StaticResource SubtitleTextBlockStyle}" Text="{x:Bind ModelFamily.Name}" /> - + Spacing="8"> + + + + models = new(); public ModelPage() { @@ -41,12 +42,14 @@ protected override void OnNavigatedTo(NavigationEventArgs e) modelFamilyType = modelType; ModelFamily = modelFamilyDetails; - modelSelectionControl.SetModels(GetAllSampleDetails().ToList()); + models = GetAllSampleDetails().ToList(); + modelSelectionControl.SetModels(models); } else if (e.Parameter is ModelDetails details) { // this is likely user added model - modelSelectionControl.SetModels([details]); + models = [details]; + modelSelectionControl.SetModels(models); ModelFamily = new ModelFamily { @@ -70,6 +73,11 @@ protected override void OnNavigatedTo(NavigationEventArgs e) DocumentationCard.Visibility = Visibility.Collapsed; } + if(models.Count > 0) + { + BuildAIToolkitButton(); + } + EnableSampleListIfModelIsDownloaded(); App.ModelCache.CacheStore.ModelsChanged += CacheStore_ModelsChanged; } @@ -137,6 +145,75 @@ private IEnumerable GetAllSampleDetails() } } + private void BuildAIToolkitButton() + { + bool isAiToolkitActionAvailable = false; + Dictionary actionSubmenus = new(); + + foreach(ModelDetails modelDetails in models) + { + foreach(AIToolkitAction action in modelDetails.AIToolkitActions!) + { + if(!AIToolkitHelper.ValidateAction(modelDetails, action)) + { + continue; + } + + MenuFlyoutSubItem? actionFlyoutItem; + if (!actionSubmenus.TryGetValue(action, out actionFlyoutItem)) + { + actionFlyoutItem = new MenuFlyoutSubItem() + { + Text = AIToolkitHelper.AIToolkitActionInfos[action].DisplayName + }; + actionSubmenus.Add(action, actionFlyoutItem); + AIToolkitFlyout.Items.Add(actionFlyoutItem); + } + + isAiToolkitActionAvailable = true; + MenuFlyoutItem modelFlyoutItem = new MenuFlyoutItem() + { + Tag = (action, modelDetails), + Text = modelDetails.Name, + }; + + modelFlyoutItem.Click += ToolkitActionFlyoutItem_Click; + actionFlyoutItem.Items.Add(modelFlyoutItem); + } + } + + AIToolkitDropdown.Visibility = isAiToolkitActionAvailable ? Visibility.Visible : Visibility.Collapsed; + } + + private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) + { + if(sender is MenuFlyoutItem actionFlyoutItem) + { + (AIToolkitAction action, ModelDetails modelDetails) = ((AIToolkitAction, ModelDetails))actionFlyoutItem.Tag; + + AIToolkitActionClickedEvent.Log(AIToolkitHelper.AIToolkitActionInfos[action].QueryName, modelDetails.Name); + + string toolkitDeeplink = AIToolkitHelper.CreateAiToolkitDeeplink(action, modelDetails); + try + { + Process.Start(new ProcessStartInfo() + { + FileName = toolkitDeeplink, + UseShellExecute = true + }); + } + catch + { + Process.Start(new ProcessStartInfo() + { + FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", + UseShellExecute = true + }); + } + + } + } + private void ModelSelectionControl_SelectedModelChanged(object sender, ModelDetails? modelDetails) { // if we don't have a modelType, we are in a user added language model, use same samples as Phi diff --git a/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json b/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json index 74608d8e..7be048c3 100644 --- a/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json +++ b/AIDevGallery/Samples/Definitions/Models/languagemodels.modelgroup.json @@ -21,7 +21,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-4-mini-gpu-int4-rtn-block-32" }, "Phi4MiniCPU": { "Id": "69252aa6-0138-4bc0-b2d3-ef8d72f5380e", @@ -34,7 +36,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-4-mini-cpu-int4-rtn-block-32-acc-level-4-onnx" } }, "ReadmeUrl": "https://huggingface.co/microsoft/Phi-4-mini-instruct-onnx/blob/main/README.md" @@ -55,7 +59,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3.5-mini-cpu-int4-awq-block-128-acc-level-4-onnx" } }, "ReadmeUrl": "https://huggingface.co/microsoft/Phi-3.5-mini-instruct-onnx/blob/main/README.md" @@ -76,7 +82,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3-mini-4k-directml-int4-awq-block-128-onnx" }, "Phi3MiniCPU": { "Id": "69252aa6-0137-4bc0-b2d3-ef8d72f5380e", @@ -88,7 +96,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3-mini-4k-cpu-int4-rtn-block-32-onnx" }, "Phi3MiniCPUACC4": { "Id": "f4eba19c-b93f-4a4b-b003-0cfd1322ebad", @@ -100,7 +110,9 @@ "Icon": "Microsoft.svg", "ParameterSize": "3.8B", "PromptTemplate": "Phi3", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3-mini-4k-cpu-int4-rtn-block-32-acc-level-4-onnx" } }, "ReadmeUrl": "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/blob/main/README.md" @@ -154,7 +166,9 @@ "Icon": "Mistral.svg", "ParameterSize": "7B", "PromptTemplate": "Mistral", - "License": "apache-2.0" + "License": "apache-2.0", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "mistral-7b-v02-int4-directml" }, "Mistral7BInstruct02CPU": { "Id": "4e01634b-1c52-4e59-9f0f-0994a0730548", @@ -178,7 +192,9 @@ "Icon": "Mistral.svg", "ParameterSize": "7B", "PromptTemplate": "Mistral", - "License": "apache-2.0" + "License": "apache-2.0", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "mistral-7b-v02-int4-cpu" } }, "ReadmeUrl": "https://huggingface.co/microsoft/mistral-7b-instruct-v0.2-ONNX/blob/main/README.md" diff --git a/AIDevGallery/Samples/Definitions/Models/multimodal.modelgroup.json b/AIDevGallery/Samples/Definitions/Models/multimodal.modelgroup.json index b97fcfca..8e428fc1 100644 --- a/AIDevGallery/Samples/Definitions/Models/multimodal.modelgroup.json +++ b/AIDevGallery/Samples/Definitions/Models/multimodal.modelgroup.json @@ -20,7 +20,9 @@ "Size": 3220615084, "Icon": "Microsoft.svg", "ParameterSize": "4.2B", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3-vision-128k-cpu-int4-rtn-block-32-acc-level-4-onnx" } }, "ReadmeUrl": "https://huggingface.co/microsoft/Phi-3-vision-128k-instruct-onnx/blob/main/README.md" @@ -40,7 +42,9 @@ "Size": 3220612860, "Icon": "Microsoft.svg", "ParameterSize": "4.2B", - "License": "mit" + "License": "mit", + "AIToolkitActions": [ "BulkRun", "Playground", "PromptBuilder" ], + "AIToolkitId": "Phi-3.5-vision-cpu-int4-rtn-block-32-acc-level-4-onnx" } }, "ReadmeUrl": "https://huggingface.co/microsoft/Phi-3-vision-128k-instruct-onnx/blob/main/README.md" diff --git a/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs b/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs new file mode 100644 index 00000000..b9c2df3a --- /dev/null +++ b/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using System; +using System.Diagnostics.Tracing; + +namespace AIDevGallery.Telemetry.Events; + +[EventData] +internal class AIToolkitActionClickedEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public string ToolkitActionQueryName { get; } + + public string ModelName { get; } + + private AIToolkitActionClickedEvent(string toolkitActionQueryName, string modelName) + { + ToolkitActionQueryName = toolkitActionQueryName; + ModelName = modelName; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings + } + + public static void Log(string toolkitActionQueryName, string modelName) + { + TelemetryFactory.Get().Log("AIToolkitActionClicked_Event", LogLevel.Critical, new AIToolkitActionClickedEvent(toolkitActionQueryName, modelName)); + } +} \ No newline at end of file diff --git a/AIDevGallery/Utils/AIToolkitHelper.cs b/AIDevGallery/Utils/AIToolkitHelper.cs new file mode 100644 index 00000000..20f8700e --- /dev/null +++ b/AIDevGallery/Utils/AIToolkitHelper.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AIDevGallery.Models; +using System.Collections.Generic; +using System.Linq; + +namespace AIDevGallery.Utils; + +internal static class AIToolkitHelper +{ + private static Dictionary aiToolkitActionInfos = new() + { + { AIToolkitAction.FineTuning, new ToolkitActionInfo() { DisplayName = "Fine Tuning", QueryName = "open_fine_tuning" } }, + { AIToolkitAction.PromptBuilder, new ToolkitActionInfo() { DisplayName = "Prompt Builder", QueryName = "open_prompt_builder" } }, + { AIToolkitAction.BulkRun, new ToolkitActionInfo() { DisplayName = "Bulk Run", QueryName = "open_bulk_run" } }, + { AIToolkitAction.Playground, new ToolkitActionInfo() { DisplayName = "Playground", QueryName = "open_playground" } } + }; + + public static Dictionary AIToolkitActionInfos + { + get { return aiToolkitActionInfos; } + } + + public static string CreateAiToolkitDeeplink(AIToolkitAction action, ModelDetails modelDetails) + { + ToolkitActionInfo? actionInfo; + string deeplink = "vscode://ms-windows-ai-studio.windows-ai-studio/"; + string modelId = action == AIToolkitAction.FineTuning ? modelDetails.AIToolkitFinetuningId! : modelDetails.AIToolkitId!; + + if(aiToolkitActionInfos.TryGetValue(action, out actionInfo) && !string.IsNullOrEmpty(modelId)) + { + deeplink = deeplink + $"{actionInfo.QueryName}?model_id={modelId}&track_from=AIDevGallery"; + } + + return deeplink; + } + + public static bool ValidateForFineTuning(ModelDetails modelDetails) + { + return modelDetails.AIToolkitActions!.Contains(AIToolkitAction.FineTuning) && !string.IsNullOrEmpty(modelDetails.AIToolkitFinetuningId); + } + + public static bool ValidateForGeneralToolkit(ModelDetails modelDetails) + { + return modelDetails.AIToolkitActions!.Where(action => action != AIToolkitAction.FineTuning).ToList().Count > 0 && !string.IsNullOrEmpty(modelDetails.AIToolkitId); + } + + public static bool ValidateAction(ModelDetails modelDetails, AIToolkitAction action) + { + if(modelDetails.Compatibility.CompatibilityState == ModelCompatibilityState.NotCompatible) + { + return false; + } + + if(action == AIToolkitAction.FineTuning) + { + return ValidateForFineTuning(modelDetails); + } + else + { + return ValidateForGeneralToolkit(modelDetails); + } + } +} + +internal class ToolkitActionInfo +{ + public required string DisplayName { get; set; } + public required string QueryName { get; set; } +} \ No newline at end of file From f9ac970aefecdc0214c6e278a585daa6fb99d2fe Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Wed, 19 Mar 2025 12:21:10 -0700 Subject: [PATCH 2/6] fix warning --- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml.cs b/AIDevGallery/Pages/Models/ModelPage.xaml.cs index 35cd3a98..008abc39 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -228,7 +228,6 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) UseShellExecute = true }); } - } } From 59b0029ab11fa6f15146413be28dd55e00186d67 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Thu, 20 Mar 2025 13:40:42 +0100 Subject: [PATCH 3/6] Design tweaks --- AIDevGallery/Assets/AITKIcon.png | Bin 0 -> 16402 bytes AIDevGallery/Pages/Models/ModelPage.xaml | 39 ++++++++++++++------ AIDevGallery/Pages/Models/ModelPage.xaml.cs | 5 +++ 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 AIDevGallery/Assets/AITKIcon.png diff --git a/AIDevGallery/Assets/AITKIcon.png b/AIDevGallery/Assets/AITKIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5b67ea6cbc075488e8855a1ea845729428894c GIT binary patch literal 16402 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 - + + + + + + + + + + + + + + + - + Date: Thu, 20 Mar 2025 14:53:19 -0700 Subject: [PATCH 4/6] Address Alex feedback --- AIDevGallery.SourceGenerator/Models/Model.cs | 6 +++--- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 10 +++++++--- .../Events/AIToolkitActionClickedEvent.cs | 8 +++++--- AIDevGallery/Utils/AIToolkitHelper.cs | 14 +++++++------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/AIDevGallery.SourceGenerator/Models/Model.cs b/AIDevGallery.SourceGenerator/Models/Model.cs index 4b41dd26..e36653cc 100644 --- a/AIDevGallery.SourceGenerator/Models/Model.cs +++ b/AIDevGallery.SourceGenerator/Models/Model.cs @@ -24,7 +24,7 @@ internal class Model [JsonConverter(typeof(SingleOrListOfStringConverter))] [JsonPropertyName("FileFilter")] public List? FileFilters { get; init; } - public List? AIToolkitActions { get; set; } - public string? AIToolkitId { get; set; } - public string? AIToolkitFinetuningId { get; set; } + public List? AIToolkitActions { get; init; } + public string? AIToolkitId { get; init; } + public string? AIToolkitFinetuningId { get; init; } } \ No newline at end of file diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml.cs b/AIDevGallery/Pages/Models/ModelPage.xaml.cs index a9bfac05..a18676e0 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -173,7 +173,7 @@ private void BuildAIToolkitButton() { foreach(AIToolkitAction action in modelDetails.AIToolkitActions!) { - if(!AIToolkitHelper.ValidateAction(modelDetails, action)) + if(modelDetails.ValidateAction(action)) { continue; } @@ -214,9 +214,8 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) { (AIToolkitAction action, ModelDetails modelDetails) = ((AIToolkitAction, ModelDetails))actionFlyoutItem.Tag; - AIToolkitActionClickedEvent.Log(AIToolkitHelper.AIToolkitActionInfos[action].QueryName, modelDetails.Name); - string toolkitDeeplink = AIToolkitHelper.CreateAiToolkitDeeplink(action, modelDetails); + bool wasDeeplinkSuccesful = true; try { Process.Start(new ProcessStartInfo() @@ -232,6 +231,11 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) FileName = "https://learn.microsoft.com/en-us/windows/ai/toolkit/", UseShellExecute = true }); + wasDeeplinkSuccesful = false; + } + finally + { + AIToolkitActionClickedEvent.Log(AIToolkitHelper.AIToolkitActionInfos[action].QueryName, modelDetails.Name, wasDeeplinkSuccesful); } } } diff --git a/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs b/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs index b9c2df3a..0946fe6c 100644 --- a/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs +++ b/AIDevGallery/Telemetry/Events/AIToolkitActionClickedEvent.cs @@ -16,11 +16,13 @@ internal class AIToolkitActionClickedEvent : EventBase public string ToolkitActionQueryName { get; } public string ModelName { get; } + public bool WasDeeplinkSucessful { get; } - private AIToolkitActionClickedEvent(string toolkitActionQueryName, string modelName) + private AIToolkitActionClickedEvent(string toolkitActionQueryName, string modelName, bool wasDeeplinkSucessful) { ToolkitActionQueryName = toolkitActionQueryName; ModelName = modelName; + WasDeeplinkSucessful = wasDeeplinkSucessful; } public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) @@ -28,8 +30,8 @@ public override void ReplaceSensitiveStrings(Func replaceSensi // No sensitive strings } - public static void Log(string toolkitActionQueryName, string modelName) + public static void Log(string toolkitActionQueryName, string modelName, bool wasDeeplinkSuccesful) { - TelemetryFactory.Get().Log("AIToolkitActionClicked_Event", LogLevel.Critical, new AIToolkitActionClickedEvent(toolkitActionQueryName, modelName)); + TelemetryFactory.Get().Log("AIToolkitActionClicked_Event", LogLevel.Critical, new AIToolkitActionClickedEvent(toolkitActionQueryName, modelName, wasDeeplinkSuccesful)); } } \ No newline at end of file diff --git a/AIDevGallery/Utils/AIToolkitHelper.cs b/AIDevGallery/Utils/AIToolkitHelper.cs index 20f8700e..21750882 100644 --- a/AIDevGallery/Utils/AIToolkitHelper.cs +++ b/AIDevGallery/Utils/AIToolkitHelper.cs @@ -36,17 +36,17 @@ public static string CreateAiToolkitDeeplink(AIToolkitAction action, ModelDetail return deeplink; } - public static bool ValidateForFineTuning(ModelDetails modelDetails) + public static bool ValidateForFineTuning(this ModelDetails modelDetails) { - return modelDetails.AIToolkitActions!.Contains(AIToolkitAction.FineTuning) && !string.IsNullOrEmpty(modelDetails.AIToolkitFinetuningId); + return modelDetails.AIToolkitActions != null && modelDetails.AIToolkitActions.Contains(AIToolkitAction.FineTuning) && !string.IsNullOrEmpty(modelDetails.AIToolkitFinetuningId); } - public static bool ValidateForGeneralToolkit(ModelDetails modelDetails) + public static bool ValidateForGeneralToolkit(this ModelDetails modelDetails) { - return modelDetails.AIToolkitActions!.Where(action => action != AIToolkitAction.FineTuning).ToList().Count > 0 && !string.IsNullOrEmpty(modelDetails.AIToolkitId); + return modelDetails.AIToolkitActions != null && modelDetails.AIToolkitActions.Where(action => action != AIToolkitAction.FineTuning).ToList().Count > 0 && !string.IsNullOrEmpty(modelDetails.AIToolkitId); } - public static bool ValidateAction(ModelDetails modelDetails, AIToolkitAction action) + public static bool ValidateAction(this ModelDetails modelDetails, AIToolkitAction action) { if(modelDetails.Compatibility.CompatibilityState == ModelCompatibilityState.NotCompatible) { @@ -66,6 +66,6 @@ public static bool ValidateAction(ModelDetails modelDetails, AIToolkitAction act internal class ToolkitActionInfo { - public required string DisplayName { get; set; } - public required string QueryName { get; set; } + public required string DisplayName { get; init; } + public required string QueryName { get; init; } } \ No newline at end of file From 935c0dce97d404f754849eb065817ba60e905ebd Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Fri, 21 Mar 2025 13:33:01 -0700 Subject: [PATCH 5/6] Address Alex feedback pt 2 --- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 2 +- AIDevGallery/Utils/AIToolkitHelper.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml.cs b/AIDevGallery/Pages/Models/ModelPage.xaml.cs index a18676e0..3b99d6da 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -214,7 +214,7 @@ private void ToolkitActionFlyoutItem_Click(object sender, RoutedEventArgs e) { (AIToolkitAction action, ModelDetails modelDetails) = ((AIToolkitAction, ModelDetails))actionFlyoutItem.Tag; - string toolkitDeeplink = AIToolkitHelper.CreateAiToolkitDeeplink(action, modelDetails); + string toolkitDeeplink = modelDetails.CreateAiToolkitDeeplink(action); bool wasDeeplinkSuccesful = true; try { diff --git a/AIDevGallery/Utils/AIToolkitHelper.cs b/AIDevGallery/Utils/AIToolkitHelper.cs index 21750882..1a4d4595 100644 --- a/AIDevGallery/Utils/AIToolkitHelper.cs +++ b/AIDevGallery/Utils/AIToolkitHelper.cs @@ -22,7 +22,7 @@ public static Dictionary AIToolkitActionInfo get { return aiToolkitActionInfos; } } - public static string CreateAiToolkitDeeplink(AIToolkitAction action, ModelDetails modelDetails) + public static string CreateAiToolkitDeeplink(this ModelDetails modelDetails, AIToolkitAction action) { ToolkitActionInfo? actionInfo; string deeplink = "vscode://ms-windows-ai-studio.windows-ai-studio/"; @@ -55,11 +55,11 @@ public static bool ValidateAction(this ModelDetails modelDetails, AIToolkitActio if(action == AIToolkitAction.FineTuning) { - return ValidateForFineTuning(modelDetails); + return modelDetails.ValidateForFineTuning(); } else { - return ValidateForGeneralToolkit(modelDetails); + return modelDetails.ValidateForGeneralToolkit(); } } } From a28d38b4281f11b37ffd4587bec4335e0c6ff8a9 Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Fri, 21 Mar 2025 14:57:11 -0700 Subject: [PATCH 6/6] add null check --- AIDevGallery/Pages/Models/ModelPage.xaml.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AIDevGallery/Pages/Models/ModelPage.xaml.cs b/AIDevGallery/Pages/Models/ModelPage.xaml.cs index 3b99d6da..10c9e31b 100644 --- a/AIDevGallery/Pages/Models/ModelPage.xaml.cs +++ b/AIDevGallery/Pages/Models/ModelPage.xaml.cs @@ -171,7 +171,12 @@ private void BuildAIToolkitButton() foreach(ModelDetails modelDetails in models) { - foreach(AIToolkitAction action in modelDetails.AIToolkitActions!) + if(modelDetails.AIToolkitActions == null) + { + continue; + } + + foreach(AIToolkitAction action in modelDetails.AIToolkitActions) { if(modelDetails.ValidateAction(action)) {