Compartir a través de


Incorporación de finalizaciones de chat de OpenAI a la aplicación de escritorio winUI 3 o al SDK de Aplicaciones para Windows

En este procedimiento, aprenderá a integrar la API de OpenAI en la aplicación de escritorio WinUI 3 o el SDK de Aplicaciones para Windows. Crearemos una interfaz similar al chat que le permita generar respuestas a mensajes mediante la API de finalizaciones de chat de OpenAI:

Una aplicación de chat menos mínima.

Requisitos previos

Creación de un proyecto

  1. Abra Visual Studio y cree un proyecto nuevo mediante File>New>Project.
  2. Busque WinUI y seleccione la plantilla de proyecto de C# Blank App, Packaged (WinUI 3 in Desktop).
  3. Especifique el nombre del proyecto, el nombre de la solución y el directorio. En este ejemplo, el proyecto ChatGPT_WinUI3 pertenece a una solución ChatGPT_WinUI3, que se creará en C:\Projects\.

Después de crear el proyecto, debería ver la siguiente estructura predeterminada de archivos en el Explorador de soluciones:

Estructura de directorio predeterminada.

Establecimiento de la variable de entorno

Para usar el SDK de OpenAI, deberá establecer una variable de entorno con la clave de API. En este ejemplo, usaremos la variable de entorno OPENAI_API_KEY. Una vez que tenga la clave de API del panel para desarrolladores de OpenAI, puede establecer la variable de entorno desde la línea de comandos de la manera siguiente:

setx OPENAI_API_KEY <your-api-key>

Tenga en cuenta que este método funciona bien en la fase de desarrollo, pero posiblemente quiera usar un método más seguro para las aplicaciones de producción (por ejemplo, puede almacenar la clave de API en un almacén de claves seguro al que un servicio remoto pueda acceder en nombre de la aplicación). Consulte Procedimientos recomendados para la seguridad de claves de OpenAI.

Instalación del SDK de OpenAI

En el menú View de Visual Studio, seleccione Terminal. Verá que aparece una instancia de Developer Powershell. Ejecute el siguiente comando en el directorio raíz del proyecto para instalar el SDK:

dotnet add package Betalgo.OpenAI

Inicializar el SDK

En MainWindow.xaml.cs, inicie el SDK con la clave 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
            });
        }
    }
}

Generación de la interfaz de usuario del chat

Usará StackPanel para mostrar una lista de mensajes y TextBox para permitir que los usuarios escriban nuevos mensajes. Actualice MainWindow.xaml de la siguiente manera:

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

Implementación del envío, recepción y visualización de mensajes

Agregue un controlador de eventos SendButton_Click para controlar el envío, la recepción y la visualización de mensajes:

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

Ejecución de la aplicación

Ejecute la aplicación y pruebe a chatear. Deberíamos ver algo parecido a lo siguiente:

Aplicación de chat mínima.

Mejora de la interfaz del chat

Vamos a realizar las siguientes mejoras en la interfaz del chat:

  • Agregue ScrollViewer a StackPanel para habilitar el desplazamiento.
  • Agregue TextBlock para mostrar la respuesta GPT de una manera que se diferencie aún más de la entrada del usuario.
  • Agregue ProgressBar para indicar en momento en que la aplicación está esperando una respuesta de la API de GPT.
  • Centre StackPanel en la ventana, para que quede similar a la interfaz web de ChatGPT.
  • Asegúrese de que los mensajes se ajusten a la línea siguiente cuando lleguen al borde de la ventana.
  • Haga que TextBox sea mayor y responda a la clave Enter.

A partir de la parte superior:

Agregue ScrollViewer.

Ajuste ListView en ScrollViewer para habilitar el desplazamiento vertical en conversaciones largas:

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

Use TextBlock

Modifique el método AddMessageToConversation para dar diferentes estilos a la entrada de usuario y a la respuesta de 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()); 
    }

Agregue ProgressBar.

Para indicar si la aplicación está esperando una respuesta, agregue 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>

A continuación, actualice el controlador de eventos SendButton_Click para mostrar ProgressBar mientras espera una respuesta:

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

Centrado de StackPanel

Para centrar StackPanel y desplegar los mensajes hacia TextBox, ajuste la configuración Grid en MainWindow.xaml:

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

Ajuste de mensajes

Para asegurarse de que los mensajes se ajusten a la línea siguiente cuando lleguen al borde de la ventana, actualice MainWindow.xaml para usar ItemsControl.

Reemplace esto:

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

Con esto:

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

A continuación, presentaremos una clase MessageItem para facilitar el enlace y el coloreado:

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

Por último, actualice el método AddMessageToConversation para usar la nueva clase 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);
    }
    // ...

Mejora de TextBox

Para que TextBox sea más grande y responda a la clave Enter, actualice MainWindow.xaml de la siguiente manera:

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

A continuación, agregue el controlador de eventos InputTextBox_KeyDown para controlar la clave 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());
        }
    }
    //...

Ejecución de la aplicación mejorada

La interfaz de chat nueva y mejorada debe tener un aspecto similar al siguiente:

Una aplicación de chat menos mínima.

Resumen

Esto es lo que ha logado en estas instrucciones:

  1. Ha agregado las funcionalidades de API de OpenAI a la aplicación de escritorio winUI 3 o el SDK de Aplicaciones para Windows mediante la instalación de un SDK de la comunidad y la inicialización con la clave de API.
  2. Ha compilado una interfaz similar al chat que le permite generar respuestas a mensajes mediante la API de finalizaciones de chat de OpenAI.
  3. Ha mejorado la interfaz de chat:
    1. al agregar ScrollViewer,
    2. al usar TextBlock para mostrar la respuesta GPT,
    3. al agregar ProgressBar para indicar el momento en que la aplicación está esperando una respuesta de la API de GPT,
    4. al centrar StackPanel en la ventana,
    5. al asegurarse de que los mensajes se ajustan a la siguiente línea cuando llegan al borde de la ventana y
    6. hacer que sea TextBox mayor, redimensionable y responda a la Enter clave.

Archivos de código completo

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