WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI 채팅 완료 추가
이 방법에서는 WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI의 API를 어떻게 통합하는지 알아봅니다. OpenAI의 채팅 완료 API로 메시지에 대한 응답을 생성할 수 있는 채팅과 유사한 인터페이스를 빌드합니다.
필수 조건
- 개발 컴퓨터 설정(WinUI 시작 참조).
- C# 및 WinUI 3/Windows 앱 SDK로 Hello World 앱을 빌드하는 방법의 핵심 개념에 익숙하다면, 여기에서 그 방법을 기반으로 앱을 빌드해보겠습니다.
- OpenAI 개발자 대시보드의 OpenAI API 키입니다.
- 프로젝트에 설치된 OpenAI SDK입니다. OpenAI 설명서에서 커뮤니티 라이브러리 목록을 참조하세요. 이 방법에서는 betalgo/openai을(를) 사용합니다.
프로젝트 만들기
- Visual Studio를 열고
File
>New
>Project
를 통해 새 프로젝트를 만듭니다. WinUI
를 검색하여Blank App, Packaged (WinUI 3 in Desktop)
C# 프로젝트 템플릿을 선택합니다.- 프로젝트 이름, 솔루션 이름 및 디렉터리를 지정합니다. 이 예시에서
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());
}
}
//...
개선된 앱 실행
새롭게 개선한 채팅 인터페이스는 다음과 같은 모습일 것입니다.
요약
이 방법에서 달성한 내용은 다음과 같습니다.
- 커뮤니티 SDK를 설치하고 API 키로 초기화하여 WinUI 3/Windows 앱 SDK 데스크톱 앱에 OpenAI의 API 기능을 추가했습니다.
- OpenAI의 채팅 완료 API로 메시지에 대한 응답을 생성할 수 있는 채팅과 유사한 인터페이스를 빌드했습니다.
- 채팅 인터페이스를 다음과 같이 개선했습니다.
ScrollViewer
을(를) 추가하고,TextBlock
(으)로 GPT 응답을 표시하고,ProgressBar
을(를) 추가하여 앱이 GPT API의 응답을 기다리는 시기를 표시하고,- 창에서 가운데로
StackPanel
을(를) 배치하고, - 창 가장자리에 메시지가 도달하면 다음 줄로 래핑되게 하여
- 키를
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());
}
}
}
}
관련
Windows developer