Compartilhar via


Adicionar preenchimento de chat do OpenAI ao aplicativo para desktop WinUI 3/SDK do Aplicativo Windows

Neste tutorial, você aprenderá a integrar a API do OpenAI ao seu aplicativo para desktop WinUI 3/SDK do Aplicativo Windows. Criaremos uma interface semelhante a um bate-papo que permite gerar respostas a mensagens usando a API de conclusão de bate-papo da OpenAI:

Um aplicativo de bate-papo menos mínimo.

Pré-requisitos

Criar um projeto

  1. Abra o Visual Studio e crie um projeto por meio de File>New>Project.
  2. Pesquise o WinUI e selecione o modelo de projeto Blank App, Packaged (WinUI 3 in Desktop) C#.
  3. Especifique um nome de projeto, nome da solução e diretório. Neste exemplo, o projeto ChatGPT_WinUI3 pertence a uma solução ChatGPT_WinUI3, que será criada em C:\Projects\.

Após criar o projeto, você verá a seguinte estrutura de arquivo padrão no Gerenciador de Soluções:

A estrutura de diretório padrão.

Definir as variáveis de ambiente

Para usar o SDK do OpenAI, você precisará definir uma variável de ambiente com a chave de API. Neste exemplo, usaremos a variável de ambiente OPENAI_API_KEY. Depois de obter a chave de API no painel do desenvolvedor de OpenAI, é possível definir a variável de ambiente da linha de comando da seguinte maneira:

setx OPENAI_API_KEY <your-api-key>

Observe que esse método funciona bem para desenvolvimento, mas convém usar um método mais seguro para aplicativos de produção (por exemplo: é possível armazenar a chave de API em um cofre de chave de segurança que um serviço remoto pode acessar em nome do aplicativo). Consulte Práticas recomendadas para segurança de chaves OpenAI.

Instalar o SDK do OpenAI

No menu View do Visual Studio, selecione Terminal. Será exibida uma instância de Developer Powershell. Execute o seguinte comando no diretório raiz do projeto para instalar o SDK:

dotnet add package Betalgo.OpenAI

Inicializar o SDK

Em MainWindow.xaml.cs, inicialize o SDK com a chave de 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
            });
        }
    }
}

Criar a interface do usuário do bate-papo

Usaremos um StackPanel para exibir uma lista de mensagens e uma TextBox para permitir que os usuários insiram novas mensagens. Atualize MainWindow.xaml da seguinte maneira:

<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>

Implementar o envio, recebimento e a exibição de mensagens

Adicione um manipulador de eventos SendButton_Click para manipular o envio, o recebimento e a exibição de mensagens:

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

Executar o aplicativo

Execute o aplicativo e tente conversar! Você deverá ver algo como:

Um aplicativo de bate-papo mínimo.

Melhorar a interface de bate-papo

Vamos fazer as seguintes melhorias na interface de bate-papo:

  • Adicione um ScrollViewer ao StackPanel para ativar a rolagem.
  • Adicione um TextBlock para exibir a resposta do GPT de uma maneira visualmente mais distinta da entrada do usuário.
  • Adicione uma ProgressBar para indicar quando o aplicativo está aguardando uma resposta da API do GPT.
  • Centralize o StackPanel na janela, semelhante à interface web do ChatGPT.
  • Certifique-se de que as mensagens sejam quebradas na próxima linha quando atingirem a borda da janela.
  • Aumente a TextBox e torne-a responsivo à tecla Enter.

Começando pelo topo:

Adicione ScrollViewer

Empacote a ListView em um ScrollViewer para habilitar a rolagem vertical em conversas longas:

        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
                <ListView x:Name="ConversationList" />
            </ScrollViewer>
            <!-- ... -->
        </StackPanel>

Use TextBlock.

Modifique o método AddMessageToConversation para estilizar a entrada do usuário e a resposta do GPT de forma diferente:

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

Adicione ProgressBar

Para indicar quando o aplicativo está aguardando uma resposta, adicione uma ProgressBar ao 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>

Em seguida, atualize o manipulador de eventos SendButton_Click para mostrar a ProgressBar enquanto aguarda uma resposta:

    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!
    }

Centralizar o StackPanel

Para centralizar o StackPanel e transferir as mensagens para baixo em direção à TextBox, ajuste as configurações de Grid na MainWindow.xaml:

    <Grid VerticalAlignment="Bottom" HorizontalAlignment="Center">
        <!-- ... -->
    </Grid>

Quebra de linha de mensagens

Para garantir que as mensagens sejam quebradas para a próxima linha quando atingirem a borda da janela, atualize a MainWindow.xaml para usar um ItemsControl.

Substitua:

    <ScrollViewer x:Name="ConversationScrollViewer" VerticalScrollBarVisibility="Auto" MaxHeight="500">
        <ListView x:Name="ConversationList" />
    </ScrollViewer>

Com isso:

    <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>

Em seguida, apresentaremos uma classe de MessageItem para facilitar a associação e a coloração:

    // ...
    public class MessageItem
    {
        public string Text { get; set; }
        public SolidColorBrush Color { get; set; }
    }
    // ...

Finalmente, atualize o método AddMessageToConversation para usar a nova 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);
    }
    // ...

Melhorar o TextBox

Para aumentar a TextBox e torná-la responsiva à tecla Enter, atualize MainWindow.xaml da seguinte maneira:

    <!-- ... -->
    <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>
    <!-- ... -->

Em seguida, adicione o manipulador de eventos InputTextBox_KeyDown para manipular a tecla 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());
        }
    }
    //...

Executar o aplicativo melhorado

A interface de bate-papo nova e aprimorada deve ter a seguinte aparência:

Um aplicativo de bate-papo menos mínimo.

Recapitulação

Veja aqui o que você realizou neste tutorial:

  1. Você adicionou os recursos de API do OpenAI ao aplicativo para desktop WinUI 3/SDK do Aplicativo Windows instalando um SDK da comunidade e inicializando-o com a chave de API.
  2. Criamos uma interface semelhante a um bate-papo que permite gerar respostas a mensagens usando a API de conclusão de bate-papo da OpenAI:
  3. Você melhorou a interface de bate-papo ao:
    1. adicionar um ScrollViewer,
    2. usar um TextBlock para exibir a resposta do GPT,
    3. adicionar uma ProgressBar para indicar quando o aplicativo está aguardando uma resposta da API do GPT.
    4. centralizar o StackPanel na janela,
    5. garantir que as mensagens sejam quebradas para a próxima linha quando atingirem a borda da janela e
    6. tornando o TextBox maior, redimensionável e responsivo à Enter chave.

Arquivos de código completos

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