共用方式為


將 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 專案屬於將在 C:\Projects\ 中建立的 ChatGPT_WinUI3 方案。

建立專案後,應在「方案總管」中看到以下預設檔案結構:

預設目錄結構。

設定您的環境變數

若要使用 OpenAI SDK,您必須使用 API 金鑰來設定環境變數。 在此範例中,我們將使用 OPENAI_API_KEY 環境變數。 從 OpenAI 開發人員儀表板取得 API 金鑰之後,您可以從命令列設定環境變數,如下所示:

setx OPENAI_API_KEY <your-api-key>

請注意,此方法適用於開發作業,但您可以針對生產環境應用程式使用更安全的方法 (例如:您可以將 API 金鑰儲存在安全的金鑰保存庫中,由遠端服務代表您的應用程式存取)。 請參閱 OpenAI 金鑰安全的最佳做法

安裝 OpenAI 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()]);
    }
}

執行應用程式

執行應用程式並嘗試聊天! 您應該會看到類似這樣的畫面︰

一個最小的聊天應用程式。

改善聊天介面

讓我們對聊天介面進行下列改善:

  • ScrollViewer 新增至 StackPanel 以啟用捲動功能。
  • 新增 TextBlock,讓 GPT 回應的顯示比使用者輸入的方式更具視覺上的差異。
  • 新增 ProgressBar,指出應用程式何時正在等候來自 GPT API 的回應。
  • StackPanel 於視窗置中,類似於 ChatGPT 的 Web 介面
  • 確定訊息到達視窗邊緣時,會換至下一行。
  • TextBox 更大且回應 Enter 金鑰。

從頂端開始:

新增 ScrollViewer

ListView 包裝在 ScrollViewer 中,以在長對話中啟用垂直捲動:

        <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

若要指出應用程式正在等候回應,請將 ProgressBar 新增至 StackPanel

        <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.xaml 中的 Grid 設定:

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

訊息換行

若要確保訊息在到達視窗邊緣時換行至下一行,請更新 MainWindow.xaml 為使用 ItemsControl

取代這行:

    <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; }
    }
    // ...

最後,更新 AddMessageToConversation 方法以使用新的 MessageItem 類別:

    // ...
    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>
    <!-- ... -->

然後,新增 InputTextBox_KeyDown 事件處理常式以處理 Enter 金鑰:

    //...
    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. 新增 ProgressBar,指出應用程式何時正在等候來自 GPT API 的回應;
    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());
            }
        }
    }
}