Share via


WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI 채팅 완료 추가

이 방법에서는 WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI의 API를 어떻게 통합하는지 알아봅니다. 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 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의 웹 인터페이스와 유사하게 가운데로 배치합니다.
  • 메시지가 창 가장자리에 도달하면 다음 줄로 래핑되는지 확인합니다.
  • 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 방향으로 끌어오려면 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. 커뮤니티 SDK를 설치하고 API 키로 초기화하여 WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI의 API 기능을 추가했습니다.
  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());
            }
        }
    }
}