From b581325fc3d0717d4284142330e0b08016c0dabf Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Wed, 5 Feb 2025 20:40:57 -0800 Subject: [PATCH 1/5] Semantic kernel added --- AIDevGallery/AIDevGallery.csproj | 1 + .../Language Models/SemanticKernelChat.xaml | 132 +++++++++ .../SemanticKernelChat.xaml.cs | 258 ++++++++++++++++++ .../ChatCompletionServiceFactory.cs | 21 ++ AIDevGallery/Samples/scenarios.json | 5 + Directory.Packages.props | 3 +- 6 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml create mode 100644 AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs create mode 100644 AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs diff --git a/AIDevGallery/AIDevGallery.csproj b/AIDevGallery/AIDevGallery.csproj index 86c1495..3ecd1cb 100644 --- a/AIDevGallery/AIDevGallery.csproj +++ b/AIDevGallery/AIDevGallery.csproj @@ -63,6 +63,7 @@ + diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml new file mode 100644 index 0000000..f917656 --- /dev/null +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs new file mode 100644 index 0000000..7e1d8c3 --- /dev/null +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using AIDevGallery.Models; +using AIDevGallery.Samples.Attributes; +using AIDevGallery.Samples.SharedCode; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using System; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; + +namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; + +[GallerySample( + Name = "Semantic Kernel Chat", + Model1Types = [ModelType.LanguageModels], + Id = "0d20e41d-d2d4-4e49-b55a-2a38a02db482", + Icon = "\uE8D4", + Scenario = ScenarioType.TextSemanticKernelChat, + NugetPackageReferences = [ + "CommunityToolkit.Mvvm", + "Microsoft.SemanticKernel.Connectors.Onnx", + ], + SharedCode = [ + SharedCodeEnum.ChatCompletionServiceFactory, + SharedCodeEnum.ChatTemplateSelector, + SharedCodeEnum.Message, + ])] +internal sealed partial class SemanticKernelChat : BaseSamplePage +{ + private const string _systemPrompt = "You are a helpful assistant."; + + private CancellationTokenSource? cts; + private IChatCompletionService? _chatCompletionService; + private Kernel? _semanticKernel; + private ChatHistory _chatHistory; + private bool _modelReady; + + public ObservableCollection Messages { get; } = []; + + public SemanticKernelChat() + { + this.Unloaded += (s, e) => CleanUp(); + this.Loaded += (s, e) => Page_Loaded(); // + _chatHistory = new ChatHistory(); + _chatHistory.AddSystemMessage(_systemPrompt); + this.InitializeComponent(); + } + + private ScrollViewer? scrollViewer; + + protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) + { + await InitModel(sampleParams.ModelPath); + sampleParams.NotifyCompletion(); + } + + private async Task InitModel(string modelPath) + { + (_chatCompletionService, _semanticKernel) = ChatCompletionServiceFactory.GetSemanticKernelChatCompletionService(modelPath); + + // Force service to load by sending it a dummy request + await _chatCompletionService.GetChatMessageContentAsync(string.Empty, null, _semanticKernel); + InputBox.IsEnabled = true; + _modelReady = true; + } + + // + private void Page_Loaded() + { + InputBox.Focus(FocusState.Programmatic); + } + + // + private void CleanUp() + { + CancelResponse(); + } + + private void CancelResponse() + { + StopBtn.Visibility = Visibility.Collapsed; + SendBtn.Visibility = Visibility.Visible; + EnableInputBoxWithPlaceholder(); + cts?.Cancel(); + cts?.Dispose(); + cts = null; + } + + private void TextBox_KeyUp(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Enter && + !Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift) + .HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down) && + sender is TextBox && + !string.IsNullOrWhiteSpace(InputBox.Text)) + { + var cursorPosition = InputBox.SelectionStart; + var text = InputBox.Text; + if (cursorPosition > 0 && (text[cursorPosition - 1] == '\n' || text[cursorPosition - 1] == '\r')) + { + text = text.Remove(cursorPosition - 1, 1); + InputBox.Text = text; + } + + InputBox.SelectionStart = cursorPosition - 1; + + SendMessage(); + } + } + + private void SendMessage() + { + if (InputBox.Text.Length > 0) + { + AddMessage(InputBox.Text); + InputBox.Text = string.Empty; + SendBtn.Visibility = Visibility.Collapsed; + } + } + + private void AddMessage(string text) + { + if (_chatCompletionService == null || _semanticKernel == null || !_modelReady) + { + return; + } + + Messages.Add(new Message(text.Trim(), DateTime.Now, ChatRole.User)); + _chatHistory.AddUserMessage(text); + var contentStartedBeingGenerated = false; // + NarratorHelper.Announce(InputBox, "Generating response, please wait.", "ChatWaitAnnouncementActivityId"); // > + + Task.Run(async () => + { + var responseMessage = new Message(string.Empty, DateTime.Now, ChatRole.Assistant); + + DispatcherQueue.TryEnqueue(() => + { + Messages.Add(responseMessage); + StopBtn.Visibility = Visibility.Visible; + InputBox.IsEnabled = false; + InputBox.PlaceholderText = "Please wait for the response to complete before entering a new prompt"; + }); + + cts = new CancellationTokenSource(); + string fullResponse = string.Empty; + + await foreach (var messagePart in _chatCompletionService.GetStreamingChatMessageContentsAsync(_chatHistory, null, _semanticKernel, cts.Token)) + { + fullResponse += messagePart.Content; + DispatcherQueue.TryEnqueue(() => + { + responseMessage.Content += messagePart.Content; + + // + if (!contentStartedBeingGenerated) + { + NarratorHelper.Announce(InputBox, "Response has started generating.", "ChatResponseAnnouncementActivityId"); + contentStartedBeingGenerated = true; + } + + // + }); + } + + _chatHistory.AddAssistantMessage(fullResponse); + + cts?.Dispose(); + cts = null; + + DispatcherQueue.TryEnqueue(() => + { + NarratorHelper.Announce(InputBox, "Content has finished generating.", "ChatDoneAnnouncementActivityId"); // + StopBtn.Visibility = Visibility.Collapsed; + SendBtn.Visibility = Visibility.Visible; + EnableInputBoxWithPlaceholder(); + }); + }); + } + + private void SendBtn_Click(object sender, RoutedEventArgs e) + { + SendMessage(); + } + + private void StopBtn_Click(object sender, RoutedEventArgs e) + { + CancelResponse(); + } + + private void InputBox_TextChanged(object sender, TextChangedEventArgs e) + { + SendBtn.IsEnabled = !string.IsNullOrWhiteSpace(InputBox.Text); + } + + private void EnableInputBoxWithPlaceholder() + { + InputBox.IsEnabled = true; + InputBox.PlaceholderText = "Enter your prompt (Press Shift + Enter to insert a newline)"; + } + + private void InvertedListView_Loaded(object sender, RoutedEventArgs e) + { + scrollViewer = FindElement(InvertedListView); + + ItemsStackPanel? itemsStackPanel = FindElement(InvertedListView); + if (itemsStackPanel != null) + { + itemsStackPanel.SizeChanged += ItemsStackPanel_SizeChanged; + } + } + + private void ItemsStackPanel_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (scrollViewer != null) + { + bool isScrollbarVisible = scrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Visible; + + if (isScrollbarVisible) + { + InvertedListView.Padding = new Thickness(-12, 0, 12, 24); + } + else + { + InvertedListView.Padding = new Thickness(-12, 0, -12, 24); + } + } + } + + private T? FindElement(DependencyObject element) + where T : DependencyObject + { + if (element is T targetElement) + { + return targetElement; + } + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) + { + var child = VisualTreeHelper.GetChild(element, i); + var result = FindElement(child); + if (result != null) + { + return result; + } + } + + return null; + } +} \ No newline at end of file diff --git a/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs b/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs new file mode 100644 index 0000000..ed579b9 --- /dev/null +++ b/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using System.IO; + +namespace AIDevGallery.Samples.SharedCode; +internal static class ChatCompletionServiceFactory +{ + public static (IChatCompletionService ChatCompletionService, Kernel SemanticKernel) GetSemanticKernelChatCompletionService(string modelPath) + { +#pragma warning disable SKEXP0070 + IKernelBuilder builder = Kernel.CreateBuilder().AddOnnxRuntimeGenAIChatCompletion(Path.GetFileNameWithoutExtension(modelPath), modelPath); +#pragma warning restore SKEXP0070 + + Kernel kernel = builder.Build(); + + return (kernel.GetRequiredService(), kernel); + } +} \ No newline at end of file diff --git a/AIDevGallery/Samples/scenarios.json b/AIDevGallery/Samples/scenarios.json index 90b283b..4ca1b5e 100644 --- a/AIDevGallery/Samples/scenarios.json +++ b/AIDevGallery/Samples/scenarios.json @@ -18,6 +18,11 @@ "Description": "Chat with a local language model. Send messages to a Chat Completion service that responds while keeping track of chat context and history.", "Id": "chat" }, + "SemanticKernelChat": { + "Name": "Semantic Kernel Chat", + "Description": "Chat with a local language model via the semantic kernel. Send messages to a Chat Completion service, implemented with Semantic Kernel, that responds while keeping track of chat context and history. Note: this sample may take longer than others to load.", + "Id": "semantic-kernel-chat" + }, "TranslateText": { "Name": "Translate Text", "Description": "Translate text with a local language model. Select a target language with the dropdown, enter text in the textbox in a language of your choosing, and then click 'Translate' to translate to the target language. Your source language will be auto-detected by the model.", diff --git a/Directory.Packages.props b/Directory.Packages.props index 17c5346..c436880 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,8 +3,9 @@ - + + From 1e0c21b1687faf06366cdf07f9a85eb9db05e1d3 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Thu, 6 Feb 2025 11:14:54 -0800 Subject: [PATCH 2/5] Fix project export. --- AIDevGallery/AIDevGallery.csproj | 8 +++++++- Directory.Packages.props | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AIDevGallery/AIDevGallery.csproj b/AIDevGallery/AIDevGallery.csproj index 3ecd1cb..4ecfaa1 100644 --- a/AIDevGallery/AIDevGallery.csproj +++ b/AIDevGallery/AIDevGallery.csproj @@ -63,7 +63,9 @@ - + + + @@ -84,6 +86,10 @@ + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index c436880..50319fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,8 @@ + + From f04e1fbecb78b60941a723cdce377be406e5a34d Mon Sep 17 00:00:00 2001 From: Zachary Teutsch Date: Thu, 6 Feb 2025 15:10:52 -0800 Subject: [PATCH 3/5] minor improvements, still a memory leak --- .../Language Models/SemanticKernelChat.xaml.cs | 14 +++++++------- AIDevGallery/Samples/scenarios.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs index 7e1d8c3..b19c64b 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -58,16 +58,13 @@ public SemanticKernelChat() protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { - await InitModel(sampleParams.ModelPath); + await InitModel(sampleParams.ModelPath, sampleParams.CancellationToken); sampleParams.NotifyCompletion(); } - private async Task InitModel(string modelPath) + private async Task InitModel(string modelPath, CancellationToken token) { (_chatCompletionService, _semanticKernel) = ChatCompletionServiceFactory.GetSemanticKernelChatCompletionService(modelPath); - - // Force service to load by sending it a dummy request - await _chatCompletionService.GetChatMessageContentAsync(string.Empty, null, _semanticKernel); InputBox.IsEnabled = true; _modelReady = true; } @@ -82,6 +79,8 @@ private void Page_Loaded() private void CleanUp() { CancelResponse(); + _chatCompletionService = null; + _semanticKernel = null; } private void CancelResponse() @@ -136,9 +135,10 @@ private void AddMessage(string text) Messages.Add(new Message(text.Trim(), DateTime.Now, ChatRole.User)); _chatHistory.AddUserMessage(text); var contentStartedBeingGenerated = false; // - NarratorHelper.Announce(InputBox, "Generating response, please wait.", "ChatWaitAnnouncementActivityId"); // > + NarratorHelper.Announce(InputBox, "Generating response, please wait.", "ChatWaitAnnouncementActivityId"); // - Task.Run(async () => + Task.Run( + async () => { var responseMessage = new Message(string.Empty, DateTime.Now, ChatRole.Assistant); diff --git a/AIDevGallery/Samples/scenarios.json b/AIDevGallery/Samples/scenarios.json index 4ca1b5e..69eb239 100644 --- a/AIDevGallery/Samples/scenarios.json +++ b/AIDevGallery/Samples/scenarios.json @@ -20,7 +20,7 @@ }, "SemanticKernelChat": { "Name": "Semantic Kernel Chat", - "Description": "Chat with a local language model via the semantic kernel. Send messages to a Chat Completion service, implemented with Semantic Kernel, that responds while keeping track of chat context and history. Note: this sample may take longer than others to load.", + "Description": "Chat with a local language model via the semantic kernel. Send messages to a Chat Completion service, implemented with Semantic Kernel, that responds while keeping track of chat context and history. Note: The first response from this sample may take a moment.", "Id": "semantic-kernel-chat" }, "TranslateText": { From 1c5a4ec9e38b7ed519fbae3917db1e6f0b246797 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 7 Feb 2025 12:45:04 -0800 Subject: [PATCH 4/5] Changed to using IChatClient's AsChatCompletionService. --- AIDevGallery/AIDevGallery.csproj | 6 ----- AIDevGallery/Pages/HomePage.xaml | 1 - .../SemanticKernelChat.xaml.cs | 23 ++++++++++++------- .../ChatCompletionServiceFactory.cs | 21 ----------------- Directory.Packages.props | 6 ++--- 5 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs diff --git a/AIDevGallery/AIDevGallery.csproj b/AIDevGallery/AIDevGallery.csproj index 68ebf58..58236c9 100644 --- a/AIDevGallery/AIDevGallery.csproj +++ b/AIDevGallery/AIDevGallery.csproj @@ -63,8 +63,6 @@ - - @@ -85,10 +83,6 @@ - - - - diff --git a/AIDevGallery/Pages/HomePage.xaml b/AIDevGallery/Pages/HomePage.xaml index 34e1286..b3173d9 100644 --- a/AIDevGallery/Pages/HomePage.xaml +++ b/AIDevGallery/Pages/HomePage.xaml @@ -16,7 +16,6 @@ xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:utils="using:AIDevGallery.Utils" - xmlns:wuc="using:WinUICommunity" Loaded="Page_Loaded" mc:Ignorable="d"> diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs index b19c64b..357eda9 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -26,10 +26,9 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Scenario = ScenarioType.TextSemanticKernelChat, NugetPackageReferences = [ "CommunityToolkit.Mvvm", - "Microsoft.SemanticKernel.Connectors.Onnx", + "Microsoft.SemanticKernel.Core" ], SharedCode = [ - SharedCodeEnum.ChatCompletionServiceFactory, SharedCodeEnum.ChatTemplateSelector, SharedCodeEnum.Message, ])] @@ -58,15 +57,23 @@ public SemanticKernelChat() protected override async Task LoadModelAsync(SampleNavigationParameters sampleParams) { - await InitModel(sampleParams.ModelPath, sampleParams.CancellationToken); - sampleParams.NotifyCompletion(); - } + var model = await sampleParams.GetIChatClientAsync(); + if (model == null) + { + return; + } + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + _chatCompletionService = model.AsChatCompletionService(); +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + IKernelBuilder builder = Kernel.CreateBuilder(); + _semanticKernel = builder.Build(); - private async Task InitModel(string modelPath, CancellationToken token) - { - (_chatCompletionService, _semanticKernel) = ChatCompletionServiceFactory.GetSemanticKernelChatCompletionService(modelPath); InputBox.IsEnabled = true; _modelReady = true; + + sampleParams.NotifyCompletion(); } // diff --git a/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs b/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs deleted file mode 100644 index ed579b9..0000000 --- a/AIDevGallery/Samples/SharedCode/ChatCompletionServiceFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using System.IO; - -namespace AIDevGallery.Samples.SharedCode; -internal static class ChatCompletionServiceFactory -{ - public static (IChatCompletionService ChatCompletionService, Kernel SemanticKernel) GetSemanticKernelChatCompletionService(string modelPath) - { -#pragma warning disable SKEXP0070 - IKernelBuilder builder = Kernel.CreateBuilder().AddOnnxRuntimeGenAIChatCompletion(Path.GetFileNameWithoutExtension(modelPath), modelPath); -#pragma warning restore SKEXP0070 - - Kernel kernel = builder.Build(); - - return (kernel.GetRequiredService(), kernel); - } -} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 2652f8b..e241157 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,10 +4,8 @@ - - - - + + From eb0d5f48c728f06961295c1746f3cf44d30eded0 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 7 Feb 2025 12:54:32 -0800 Subject: [PATCH 5/5] Added missing dependencies. --- .../Language Models/SemanticKernelChat.xaml.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs index 357eda9..6dd8f85 100644 --- a/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs +++ b/AIDevGallery/Samples/Open Source Models/Language Models/SemanticKernelChat.xaml.cs @@ -26,9 +26,11 @@ namespace AIDevGallery.Samples.OpenSourceModels.LanguageModels; Scenario = ScenarioType.TextSemanticKernelChat, NugetPackageReferences = [ "CommunityToolkit.Mvvm", + "Microsoft.ML.OnnxRuntimeGenAI.DirectML", "Microsoft.SemanticKernel.Core" ], SharedCode = [ + SharedCodeEnum.GenAIModel, SharedCodeEnum.ChatTemplateSelector, SharedCodeEnum.Message, ])]