Aggiungere completamenti della chat OpenAI all'app desktop WinUI 3/SDK per app di Windows
In questa procedura si apprenderà come integrare l'API openAI nell'app desktop WinUI 3/SDK per app di Windows. Verrà creata un'interfaccia simile a una chat che consente di generare risposte ai messaggi usando l'API di completamento della chat di OpenAI:
Prerequisiti
- Configurare il computer di sviluppo (vedere Introduzione a WinUI).
- Familiarità con i concetti di base in Come creare un'app Hello World utilizzando C# e WinUI 3 /SDK per app di Windows. Ci baseremo su questa procedura.
- Una chiave API OpenAI dal dashboard degli sviluppatori OpenAI.
- Un SDK OpenAI installato nel progetto. Per un elenco delle raccolte della community, vedere la documentazione di OpenAI. In questa procedura si userà betalgo/openai.
Creare un progetto
- Aprire Visual Studio e creare un nuovo progetto tramite
File
>New
>Project
. - Cercare
WinUI
e selezionare il modello di progettoBlank App, Packaged (WinUI 3 in Desktop)
C#. - Specificare un nome per il progetto, un nome per la soluzione e una directory. In questo esempio il progetto
ChatGPT_WinUI3
appartiene a una soluzioneChatGPT_WinUI3
, che verrà creata inC:\Projects\
.
Dopo aver creato il progetto, in Esplora soluzioni deve essere visualizzata la seguente struttura di file predefinita:
Imposta la variabile di ambiente
Per usare SDK OpenAI, è necessario impostare una variabile di ambiente con la chiave API. In questo esempio, si userà la variabile di ambiente OPENAI_API_KEY
. Dopo aver ottenuto la chiave API dal dashboard degli sviluppatori OpenAI, è possibile impostare la variabile di ambiente dalla riga di comando come indicato di seguito:
setx OPENAI_API_KEY <your-api-key>
Si noti che questo metodo funziona bene per lo sviluppo, ma si vuole usare un metodo più sicuro per le app di produzione (ad esempio: è possibile archiviare la chiave API in un insieme di credenziali delle chiavi sicuro a cui un servizio remoto può accedere per conto dell'app). Consultare la sezione Procedure consigliate per la sicurezza delle chiavi OpenAI.
Installare SDK OpenAI
Dal menu View
di Visual Studio, selezionare Terminal
. Verrà visualizzata un'istanza di Developer Powershell
. Eseguire il comando seguente dalla directory radice del progetto per installare l'SDK:
dotnet add package Betalgo.OpenAI
Inizializzare l'SDK
In MainWindow.xaml.cs
, inizializzare l'SDK con la chiave 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
});
}
}
}
Creare l'interfaccia utente della chat
Verrà usato StackPanel
per visualizzare un elenco di messaggi e un oggetto TextBox
per consentire agli utenti di immettere nuovi messaggi. Aggiornare MainWindow.xaml
come indicato di seguito:
<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>
Implementare l'invio, la ricezione e la visualizzazione di messaggi
Aggiungere un gestore eventi SendButton_Click
per gestire l'invio, la ricezione e la visualizzazione dei messaggi:
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()]);
}
}
Eseguire l'app
Eseguire l'app e provare a chattare. L'output dovrebbe essere simile al seguente:
Migliorare l'interfaccia chat
Verranno ora apportati i miglioramenti seguenti all'interfaccia della chat:
- Aggiungere un oggetto
ScrollViewer
aStackPanel
per abilitare lo scorrimento. - Aggiungere un oggetto
TextBlock
per visualizzare la risposta GPT in modo che sia più netta dal punto di vista visivo dall'input dell'utente. - Aggiungere un oggetto
ProgressBar
per indicare quando l'app è in attesa di una risposta dall'API GPT. - Centrare l'oggetto
StackPanel
nella finestra, simile all'interfaccia Web di ChatGPT. - Assicurarsi che i messaggi vengano visualizzati alla riga successiva quando raggiungono il bordo della finestra.
- Rendere
TextBox
più ampie e reattive alla chiaveEnter
.
A partire dalla parte superiore:
Aggiungere ScrollViewer
Eseguire il wrapping ListView
in un oggetto ScrollViewer
per abilitare lo scorrimento verticale nelle conversazioni lunghe:
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
<ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
<ListView x:Name="ConversationList" />
</ScrollViewer>
<!-- ... -->
</StackPanel>
Utilizzare TextBlock
.
Modificare il metodo AddMessageToConversation
per modellare l'input dell'utente e la risposta GPT in modo diverso:
// ...
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());
}
Aggiungere ProgressBar
Per indicare quando l'app è in attesa di una risposta, aggiungere un oggetto ProgressBar
a 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>
Aggiornare quindi il gestore eventi SendButton_Click
per visualizzare ProgressBar
mentre si è in attesa di una risposta:
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!
}
Allineare al centro l'oggetto StackPanel
Per allineare al centro StackPanel
e trascinare i messaggi verso il basso verso l'oggetto TextBox
, modificare le impostazioni Grid
in MainWindow.xaml
:
<Grid VerticalAlignment="Bottom" HorizontalAlignment="Center">
<!-- ... -->
</Grid>
Invio a capo dei messaggi
Per assicurarsi che i messaggi vengano inviati alla riga successiva quando raggiungono il bordo della finestra, aggiornare MainWindow.xaml
per usare un oggetto ItemsControl
.
Sostituire:
<ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
<ListView x:Name="ConversationList" />
</ScrollViewer>
Con il seguente:
<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>
Verrà quindi introdotta una classe MessageItem
per facilitare l'associazione e la colorazione:
// ...
public class MessageItem
{
public string Text { get; set; }
public SolidColorBrush Color { get; set; }
}
// ...
Aggiornare infine il metodo AddMessageToConversation
per usare la nuova classe 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);
}
// ...
Migliorare l'oggetto TextBox
Per rendere TextBox
più grande e reattivo/a alla chiave Enter
, aggiornare MainWindow.xaml
come segue:
<!-- ... -->
<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>
<!-- ... -->
Aggiungere quindi il gestore eventi InputTextBox_KeyDown
per gestire la chiave 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());
}
}
//...
Eseguire l'app migliorata
L'interfaccia di chat nuova e migliorata dovrebbe essere simile alla seguente:
Riepilogo
Ecco le operazioni eseguite in questa procedura:
- Sono state aggiunte le funzionalità API di OpenAI all'app desktop WinUI 3/SDK per app di Windows installando un SDK della community e inizializzandolo con la chiave API.
- Verrà creata un'interfaccia simile a una chat che consente di generare risposte ai messaggi usando l'API di completamento della chat di OpenAI:
- L'interfaccia di chat è stata migliorata nel modo che segue:
- aggiungendo un oggetto
ScrollViewer
, - utilizzando un oggetto
TextBlock
per visualizzare la risposta GPT, - aggiungendo un oggetto
ProgressBar
per indicare quando l'app è in attesa di una risposta dall'API GPT, - centrando l'oggetto
StackPanel
nella finestra, - assicurandosi che i messaggi vengano visualizzati alla riga successiva quando raggiungono il bordo della finestra, e
- rendendo più
TextBox
grande, ridimensionabile e reattivo allaEnter
chiave.
- aggiungendo un oggetto
File di codice completi
<?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());
}
}
}
}