Добавление завершения чата OpenAI в классическое приложение WinUI 3 / Windows App SDK

В этом руководстве вы узнаете, как интегрировать API OpenAI в классическое приложение WinUI 3 / Windows App SDK. Мы создадим интерфейс чата, который позволяет создавать ответы на сообщения с помощью API завершения чата OpenAI:

Менее минимальное приложение чата.

Необходимые компоненты

Создание проекта

  1. Откройте Visual Studio и создайте проект с помощью File>>NewProject.
  2. WinUI Найдите и выберите Blank App, Packaged (WinUI 3 in Desktop) шаблон проекта C#.
  3. Укажите имя проекта, имя решения и каталог. В этом примере наш ChatGPT_WinUI3 проект принадлежит ChatGPT_WinUI3 решению, в котором будет создано C:\Projects\.

После создания проекта в Обозреватель решений должна появиться следующая структура файлов по умолчанию:

Структура каталогов по умолчанию.

Настройка переменной среды

Чтобы использовать пакет SDK OpenAI, необходимо задать переменную среды с помощью ключа API. В этом примере мы будем использовать OPENAI_API_KEY переменную среды. После получения ключа API на панели мониторинга разработчика OpenAI можно задать переменную среды из командной строки следующим образом:

setx OPENAI_API_KEY <your-api-key>

Обратите внимание, что этот метод хорошо подходит для разработки, но вы хотите использовать более безопасный метод для рабочих приложений (например, вы можете сохранить ключ API в безопасном хранилище ключей, к которому удаленная служба может обращаться от имени приложения). Ознакомьтесь с рекомендациями по безопасности ключей OpenAI.

Установка пакета SDK OpenAI

В меню Visual Studio View выберите Terminal. Должен появиться экземпляр Developer Powershell . Выполните следующую команду из корневого каталога проекта, чтобы установить пакет SDK:

dotnet add package Betalgo.OpenAI

Инициализация пакета SDK

Инициализировать MainWindow.xaml.csпакет SDK с помощью ключа API:

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

Создание пользовательского интерфейса чата

Мы будем использовать 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 , указывающее, когда приложение ожидает ответа от API GPT.
  • StackPanel Центр в окне, аналогично веб-интерфейсу ChatGPT.
  • Убедитесь, что сообщения упаковываются в следующую строку, когда они достигают края окна.
  • 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

Чтобы указать, когда приложение ожидает ответа, добавьте в 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, настройте Grid параметры в MainWindow.xaml:

    <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. Вы добавили возможности API OpenAI в классическое приложение WinUI 3 / Windows App SDK, установив пакет SDK сообщества и инициализировав его с помощью ключа API.
  2. Вы создали интерфейс чата, который позволяет создавать ответы на сообщения с помощью API завершения чата OpenAI.
  3. Вы улучшили интерфейс чата, выполнив следующие действия.
    1. добавление ,ScrollViewer
    2. TextBlock с помощью ответа GPT,
    3. добавлено значение ProgressBar , указывающее, когда приложение ожидает ответа из API GPT,
    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());
            }
        }
    }
}