将 OpenAI 聊天完成添加到 WinUI 3/Windows 应用 SDK 桌面应用
在本操作说明中,你将了解如何将 OpenAI 的 API 集成到 WinUI 3/Windows 应用 SDK 桌面应用中。 我们将构建一个类似聊天的界面,可在其中使用 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
项目属于要在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
在 ScrollViewer
中对 ListView
进行换行以启用长对话的垂直滚动:
<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());
}
}
//...
运行改进后的应用
经过改进的全新聊天界面应如下所示:
回顾
下面是您在本操作说明中完成的工作:
- 你已通过安装社区 SDK 并使用 API 密钥进行初始化,将 OpenAI 的 API 功能添加到 WinUI 3/Windows 应用 SDK 桌面应用。
- 你将构建一个类似聊天的界面,可在其中使用 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());
}
}
}
}