OpenAI チャット入力候補を WinUI 3/Windows アプリ SDK デスクトップ アプリに追加する

このハウツーでは、OpenAI の API を WinUI 3/Windows アプリ SDK デスクトップ アプリに統合する方法について説明します。 OpenAI のチャット入力候補 API を使用してメッセージへの応答を生成できる、チャットのようなインターフェイスを構築します。

最小限のチャット アプリ。

前提条件

プロジェクトの作成

  1. Visual Studio を開き、File>New>Project で新しいプロジェクトを作成します。
  2. WinUIを検索して、Blank App, Packaged (WinUI 3 in Desktop) C# プロジェクト テンプレートを選択します。
  3. プロジェクト名、ソリューション名、ディレクトリを指定します。 この例では、 ChatGPT_WinUI3 プロジェクトはChatGPT_WinUI3ソリューションに属しており、C:\Projects\で 作成されます。

次の既定のファイル構造がソリューション エクスプローラーに表示されます。

デフォルトのディレクトリ構造。

環境変数を設定する

OpenAI SDK を使用するには、API キーを使用して環境変数を設定する必要があります。 この例では、OPENAI_API_KEY 環境変数を使用します。 OpenAI 開発者ダッシュボードから API キーを取得したら、コマンド ラインから次のように環境変数を設定できます。

setx OPENAI_API_KEY <your-api-key>

この方法は開発には適していますが、運用アプリにはより安全な方法を使用する必要があることに注意してください (たとえば、アプリの代わりにリモート サービスがアクセスできるセキュリティで保護されたキー コンテナーに API キーを格納できます)。 「OpenAI キーの安全性に関するベスト プラクティス」を参照してください。

OpenAI .NET SDK をインストールする

Visual Studio の View メニューから 、 Terminalを選択します。 表示のインスタンスが Developer Powershell 表示されます。 SDKをインストールするには、プロジェクトのルート ディレクトリから次のコマンドを実行します。

dotnet add package Betalgo.OpenAI

SDK を初期化する

MainWindow.xaml.csで 、API キーを使用して SDK を初期化します。

//...
using OpenAI;
using OpenAI.Managers;
using OpenAI.ObjectModels.RequestModels;
using OpenAI.ObjectModels;

namespace ChatGPT_WinUI3
{
    public sealed partial class MainWindow : Window
    {
        private OpenAIService openAiService;

        public MainWindow()
        {
            this.InitializeComponent();
           
            var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");

            openAiService = new OpenAIService(new OpenAiOptions(){
                ApiKey = openAiKey
            });
        }
    }
}

チャット UI を構築する

StackPanel を使用してメッセージの一覧を表示し、TextBox を使用してユーザーが新しいメッセージを入力できるようにします。 次のように MainWindow.xaml を更新します。

<Window
    x:Class="ChatGPT_WinUI3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ChatGPT_WinUI3"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid>
        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <ListView x:Name="ConversationList" />
            <StackPanel Orientation="Horizontal">
                <TextBox x:Name="InputTextBox" HorizontalAlignment="Stretch"/>
                <Button x:Name="SendButton" Content="Send" Click="SendButton_Click"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

メッセージの送信、受信、および表示を実装する

メッセージの 送信、受信、および表示を処理するSendButton_Clickイベント ハンドラーを追加します。

public sealed partial class MainWindow : Window
{
    // ...

    private async void SendButton_Click(object sender, RoutedEventArgs e)
    {
        string userInput = InputTextBox.Text;
        if (!string.IsNullOrEmpty(userInput))
        {
            AddMessageToConversation($"User: {userInput}");
            InputTextBox.Text = string.Empty;
            var completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest()
            {
                Messages = new List<ChatMessage>
                {
                    ChatMessage.FromSystem("You are a helpful assistant."),
                    ChatMessage.FromUser(userInput)
                },
                Model = Models.Gpt_4_1106_preview,
                MaxTokens = 300
            });

            if (completionResult != null && completionResult.Successful) {
                AddMessageToConversation("GPT: " + completionResult.Choices.First().Message.Content);
            } else {
                AddMessageToConversation("GPT: Sorry, something bad happened: " + completionResult.Error?.Message);
            }
        }
    }

    private void AddMessageToConversation(string message)
    {
        ConversationList.Items.Add(message);
        ConversationList.ScrollIntoView(ConversationList.Items[ConversationList.Items.Last()]);
    }
}

アプリを実行する

アプリを実行し、チャットを試してみてください! 次のような結果が表示されます。

最小限のチャット アプリ。

チャット インターフェイスを改善する

チャット インターフェイスを次のように改善しましょう。

  • スクロールを有効にするには、ScrollViewerStackPanelに追加します。
  • ユーザーの入力とより視覚的に異なる方法で GPT 応答を表示する TextBlockを追加します。
  • アプリが GPT API からの応答を待機しているタイミングを示すProgressBarを追加します。
  • ChatGPT の Web インターフェイスと同様に、ウィンドウ内にStackPanelを中央に配置します。
  • メッセージがウィンドウの端に達したときに、メッセージが次の行に折り返されるようにします。
  • TextBoxをより大きくして、Enterキーへの応答性を高めます。

先頭から次の手順を実行します。

ScrollViewer を追加します

長い会話で垂直スクロールを有効にするには、ListViewScrollViewer に折り返します。

        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
                <ListView x:Name="ConversationList" />
            </ScrollViewer>
            <!-- ... -->
        </StackPanel>

TextBlock を使用します

AddMessageToConversationメソッドを変更して、ユーザーの入力と GPT 応答のスタイルを異なる方法で設定します。

    // ...
    private void AddMessageToConversation(string message)
    {
        var messageBlock = new TextBlock();
        messageBlock.Text = message;
        messageBlock.Margin = new Thickness(5);
        if (message.StartsWith("User:"))
        {
            messageBlock.Foreground = new SolidColorBrush(Colors.LightBlue);
        }
        else
        {
            messageBlock.Foreground = new SolidColorBrush(Colors.LightGreen);
        }
        ConversationList.Items.Add(messageBlock);
        ConversationList.ScrollIntoView(ConversationList.Items.Last()); 
    }

ProgressBar を追加します

アプリが応答を待機しているタイミングを示すには、ProgressBarStackPanelに追加します 。

        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
                <ListView x:Name="ConversationList" />
            </ScrollViewer>
            <ProgressBar x:Name="ResponseProgressBar" Height="5" IsIndeterminate="True" Visibility="Collapsed"/> <!-- new! -->
        </StackPanel>

次に、SendButton_Clickイベント ハンドラーを更新して、ProgressBar応答の待機中を示します。

    private async void SendButton_Click(object sender, RoutedEventArgs e)
    {
        ResponseProgressBar.Visibility = Visibility.Visible; // new!

        string userInput = InputTextBox.Text;
        if (!string.IsNullOrEmpty(userInput))
        {
            AddMessageToConversation("User: " + userInput);
            InputTextBox.Text = string.Empty;
            var completionResult = await openAiService.Completions.CreateCompletion(new CompletionCreateRequest()
            {
                Prompt = userInput,
                Model = Models.TextDavinciV3
            });

            if (completionResult != null && completionResult.Successful) {
                AddMessageToConversation("GPT: " + completionResult.Choices.First().Text);
            } else {
                AddMessageToConversation("GPT: Sorry, something bad happened: " + completionResult.Error?.Message);
            }
        }
        ResponseProgressBar.Visibility = Visibility.Collapsed; // new!
    }

StackPanelを中央に揃え

メッセージStackPanelを中央に配置し、メッセージを下方向にTextBox引き下げるには、MainWindow.xamlGrid設定を調整します。

    <Grid VerticalAlignment="Bottom" HorizontalAlignment="Center">
        <!-- ... -->
    </Grid>

メッセージをラップする

メッセージがウィンドウの端に達したときに次の行に折り返されるようにするには、ItemsControlを使用するようにMainWindow.xamlを更新します 。

編集前:

    <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
        <ListView x:Name="ConversationList" />
    </ScrollViewer>

編集後:

    <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
        <ItemsControl x:Name="ConversationList" Width="300">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}" TextWrapping="Wrap" Margin="5" Foreground="{Binding Color}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>

その後、バインドと色分けを容易にするMessageItem クラスを紹介します。

    // ...
    public class MessageItem
    {
        public string Text { get; set; }
        public SolidColorBrush Color { get; set; }
    }
    // ...

最後に、新しいMessageItemクラスを使用するようにAddMessageToConversationメソッドを更新します。

    // ...
    private void AddMessageToConversation(string message)
    {
        var messageItem = new MessageItem();
        messageItem.Text = message;
        messageItem.Color = message.StartsWith("User:") ? new SolidColorBrush(Colors.LightBlue) : new SolidColorBrush(Colors.LightGreen);
        ConversationList.Items.Add(messageItem);

        // handle scrolling
        ConversationScrollViewer.UpdateLayout();
        ConversationScrollViewer.ChangeView(null, ConversationScrollViewer.ScrollableHeight, null);
    }
    // ...

TextBoxを改善する。

TextBoxをより大きくして、Enterキーへの応答性を高めるために、次のようにMainWindow.xamlを更新します。

    <!-- ... -->
    <StackPanel Orientation="Vertical" Width="300">
        <TextBox x:Name="InputTextBox" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" KeyDown="InputTextBox_KeyDown" TextWrapping="Wrap" MinHeight="100" MaxWidth="300"/>
        <Button x:Name="SendButton" Content="Send" Click="SendButton_Click" HorizontalAlignment="Right"/>
    </StackPanel>
    <!-- ... -->

次に、Enterキーを 処理するにはInputTextBox_KeyDownイベント ハンドラーを追加します。

    //...
    private void InputTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
    {
        if (e.Key == Windows.System.VirtualKey.Enter && !string.IsNullOrWhiteSpace(InputTextBox.Text))
        {
            SendButton_Click(this, new RoutedEventArgs());
        }
    }
    //...

改善されたアプリを実行する

新しく改善されたチャット インターフェイスは、次のようになります。

最小限のチャット アプリ。

まとめ

このチュートリアルで実行した内容は次のとおりです。

  1. コミュニティ SDK をインストールし、API キーを使用して初期化することで、OpenAI の API 機能を WinUI 3/Windows アプリ SDK デスクトップ アプリに追加しました。
  2. OpenAI のチャット入力候補 API を使用してメッセージへの応答を生成できる、チャットのようなインターフェイスを構築しました。
  3. 次の方法でチャット インターフェイスを改善しました。
    1. ScrollViewerを追加する
    2. TextBlock を使用して GPT 応答を表示する
    3. アプリが GPT API からの応答を待機しているタイミングを示すProgressBarを追加する
    4. ウィンドウ内でStackPanelを中央に配置し、
    5. メッセージがウィンドウの端に到達したときに次の行に折り返されるようにし、
    6. TextBoxを大きくし、サイズを変更でき、Enter キーに応答できるようにします。

完全なコード例:

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="ChatGPT_WinUI3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ChatGPT_WinUI3"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid VerticalAlignment="Bottom" HorizontalAlignment="Center">
        <StackPanel Orientation="Vertical" HorizontalAlignment="Center">
            <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
                <ItemsControl x:Name="ConversationList" Width="300">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Text}" TextWrapping="Wrap" Margin="5" Foreground="{Binding Color}"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </ScrollViewer>
            <ProgressBar x:Name="ResponseProgressBar" Height="5" IsIndeterminate="True" Visibility="Collapsed"/>
            <StackPanel Orientation="Vertical" Width="300">
                <TextBox x:Name="InputTextBox" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" KeyDown="InputTextBox_KeyDown" TextWrapping="Wrap" MinHeight="100" MaxWidth="300"/>
                <Button x:Name="SendButton" Content="Send" Click="SendButton_Click" HorizontalAlignment="Right"/>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Linq;

using OpenAI;
using OpenAI.Managers;
using OpenAI.ObjectModels.RequestModels;
using OpenAI.ObjectModels;

namespace ChatGPT_WinUI3
{
    public class MessageItem
    {
        public string Text { get; set; }
        public SolidColorBrush Color { get; set; }
    }

    public sealed partial class MainWindow : Window
    {
        private OpenAIService openAiService;

        public MainWindow()
        {
            this.InitializeComponent();

            var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");

            openAiService = new OpenAIService(new OpenAiOptions(){
                ApiKey = openAiKey
            });
        }

        private async void SendButton_Click(object sender, RoutedEventArgs e)
        {
            ResponseProgressBar.Visibility = Visibility.Visible;

            string userInput = InputTextBox.Text;
            if (!string.IsNullOrEmpty(userInput))
            {
                AddMessageToConversation("User: " + userInput);
                InputTextBox.Text = string.Empty;
                var completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest()
                {
                    Messages = new List<ChatMessage>
                    {
                        ChatMessage.FromSystem("You are a helpful assistant."),
                        ChatMessage.FromUser(userInput)
                    },
                    Model = Models.Gpt_4_1106_preview,
                    MaxTokens = 300
                });

                Console.WriteLine(completionResult.ToString());

                if (completionResult != null && completionResult.Successful)
                {
                    AddMessageToConversation("GPT: " + completionResult.Choices.First().Message.Content);
                }
                else
                {
                    AddMessageToConversation("GPT: Sorry, something bad happened: " + completionResult.Error?.Message);
                }
            }
            ResponseProgressBar.Visibility = Visibility.Collapsed;
        }

        private void AddMessageToConversation(string message)
        {
            var messageItem = new MessageItem();
            messageItem.Text = message;
            messageItem.Color = message.StartsWith("User:") ? new SolidColorBrush(Colors.LightBlue) : new SolidColorBrush(Colors.LightGreen);
            ConversationList.Items.Add(messageItem);

            // handle scrolling
            ConversationScrollViewer.UpdateLayout();
            ConversationScrollViewer.ChangeView(null, ConversationScrollViewer.ScrollableHeight, null);
        }

        private void InputTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
        {
            if (e.Key == Windows.System.VirtualKey.Enter && !string.IsNullOrWhiteSpace(InputTextBox.Text))
            {
                SendButton_Click(this, new RoutedEventArgs());
            }
        }
    }
}