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:

Un'app di chat meno minimale.

Prerequisiti

Creare un progetto

  1. Aprire Visual Studio e creare un nuovo progetto tramite File>New>Project.
  2. Cercare WinUI e selezionare il modello di progetto Blank App, Packaged (WinUI 3 in Desktop) C#.
  3. Specificare un nome per il progetto, un nome per la soluzione e una directory. In questo esempio il progetto ChatGPT_WinUI3 appartiene a una soluzione ChatGPT_WinUI3, che verrà creata in C:\Projects\.

Dopo aver creato il progetto, in Esplora soluzioni deve essere visualizzata la seguente struttura di file predefinita:

La struttura predefinita della directory.

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:

Un'app di chat minimale.

Migliorare l'interfaccia chat

Verranno ora apportati i miglioramenti seguenti all'interfaccia della chat:

  • Aggiungere un oggetto ScrollViewer a StackPanel 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 chiave Enter.

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:

Un'app di chat meno minimale.

Riepilogo

Ecco le operazioni eseguite in questa procedura:

  1. 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.
  2. Verrà creata un'interfaccia simile a una chat che consente di generare risposte ai messaggi usando l'API di completamento della chat di OpenAI:
  3. L'interfaccia di chat è stata migliorata nel modo che segue:
    1. aggiungendo un oggetto ScrollViewer,
    2. utilizzando un oggetto TextBlock per visualizzare la risposta GPT,
    3. aggiungendo un oggetto ProgressBar per indicare quando l'app è in attesa di una risposta dall'API GPT,
    4. centrando l'oggetto StackPanel nella finestra,
    5. assicurandosi che i messaggi vengano visualizzati alla riga successiva quando raggiungono il bordo della finestra, e
    6. rendendo più TextBox grande, ridimensionabile e reattivo alla Enter chiave.

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());
            }
        }
    }
}