🦔

Semantic KernelのChatCompletionをつかってBlazorでストリーミング対応する

2023/05/12に公開

ChatGPTをつかっているとテキスト文字がパラパラと出てくるような表示になっているかと思います。このような表示をSemantic KernelのChatCompletionをつかってBlazorで実装するにはどうしたらよいかを書いてみます。

前提:Semantic Kernel 0.13.442.1-preview
ASP.NET Core 7.0.5

SemanticKernelLogic
    public class SemanticKernelLogic
    {
        public IKernel kernel;
        public IChatCompletion GptChat4 { get; set; }
        private readonly ILogger<SemanticKernelLogic> _logger;
        private readonly IConfiguration _configuration;
        private OpenAIChatHistory chatHistory;

        public string GeneratedHtml { get; set; } = string.Empty;

        /// <summary>
        /// kernelの初期化からOpenAIChatHistoryをインスタンス化するところまでやる
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="configuration"></param>
        public SemanticKernelLogic(ILogger<SemanticKernelLogic> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
            string serviceId = _configuration.GetValue<string>("ServiceId") ?? string.Empty;
            string deploymentName = _configuration.GetValue<string>("DeploymentName") ?? string.Empty;
            string baseUrl = _configuration.GetValue<string>("BaseUrl") ?? string.Empty;
            string key = _configuration.GetValue<string>("Gtp4Key") ?? string.Empty;

            kernel = new KernelBuilder().Configure(c =>
            {
	        // 0.13 からkernelのインスタン化のパラメータが変わった
                //c.AddAzureChatCompletionService(serviceId, deploymentName, baseUrl, key);
                c.AddAzureChatCompletionService(deploymentName, baseUrl, key);

            }).WithLogger(_logger).Build();
            GptChat4 = kernel.GetService<IChatCompletion>();

            chatHistory = (OpenAIChatHistory)GptChat4.CreateNewChat("あなたはほのかという名前のAIアシスタントです。くだけた女性の口調で人に役立つ回答をします。");

        }
	
        public async Task StreamRun(string input)
        {
            _logger.LogInformation("input : {}", input);
            chatHistory.AddUserMessage(input);
            var setting = new ChatRequestSettings
            {
                Temperature = 0.8,
                MaxTokens = 2000,
                FrequencyPenalty = 0.5,

            };
            var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseAutoLinks().UseBootstrap().UseDiagrams().UseGridTables().Build();
            string fullMessage = string.Empty;
            GeneratedHtml = string.Empty;
	    // ここでストリーミング対応
            await foreach (string message in GptChat4.GenerateMessageStreamAsync(chatHistory, setting))
            {
                _logger.LogInformation("message : {}", message);
                if (message != null && message.Length > 0)
                {
                    var messageHtml = Markdown.ToHtml(fullMessage, pipeline);
                    _logger.LogInformation("messageHtml : {}", messageHtml);

                    GeneratedHtml = messageHtml;
                    fullMessage += message;
		    // razor側に変更通知する
                    NotifyStateChanged();
                }

            }
            chatHistory.AddAssistantMessage(fullMessage);

        }

        private void NotifyStateChanged() => OnChange?.Invoke();
        public event Action? OnChange;
    }

以下がrazorのコード主要部分

@code {

    // チャットメッセージ
    private List<ChatMessage> Messages = new();

    private async void OnSearch()
    {
        if (!string.IsNullOrWhiteSpace(Search))
        {
            logic.GeneratedHtml = string.Empty;
            ChatMessage UserMessage = new();
            UserMessage.Name = "User";
            UserMessage.Message = Search;
            Messages.Add(UserMessage);
	    
            Search = string.Empty;
            ChatMessage reply = new();
            reply.Name = "Reply";
            reply.Message = logic.GeneratedHtml;

            Messages.Add(reply);
            StateHasChanged();
	    
            await logic.StreamRun(UserMessage.Message);

            StateHasChanged();
        }
    }
    protected override void OnInitialized()
    {
        logic.OnChange += () => OnCallback();
    }

    public void OnCallback()
    {
        _ = InvokeAsync(() =>
        {
	    // 変更通知があったらチャットメッセージに追加していく
            Messages.FindLast(x => x.Name == "Reply")!.Message = logic.GeneratedHtml;
            StateHasChanged();
        });
    }
}

実際に動かしてみたデモです。
https://www.youtube.com/watch?v=Zwh-lwiN9PM

全体のコードです。
https://github.com/tomokusaba/SemanticKernelBlazorSample

Discussion