Cómo Crear un Chatbot con C#, Unum USearch y OpenAI
Este tutorial está basado en una transcripción de un video generada con el modelo whisper y ChatGPT o1-preview Video Completo Codigo Completo
Una de las solicitudes más comunes de los clientes interesados en trabajar con inteligencia artificial es desarrollar un chatbot que pueda responder en lenguaje natural. En este artículo, exploraremos cómo implementar un chatbot utilizando las librerías proporcionadas por OpenAI y una pequeña base de datos vectorial llama USearch. Este enfoque no es el único, pero es una forma efectiva de abordar el problema.
Introducción
El objetivo es crear un chatbot que pueda interactuar con los usuarios de manera natural, respondiendo a sus consultas basándose en una base de conocimientos predefinida. Utilizaremos .NET 8 y crearemos una aplicación de consola sin interfaz gráfica.
Requisitos Previos
Para seguir este tutorial, necesitarás:
- Una cuenta de OpenAI para acceder a sus modelos y APIs.
- Visual Studio con soporte para .NET 8.
- Conocimientos básicos de C# y .NET.
Paso 1: Configuración Inicial
Crear la Aplicación de Consola
Comienza abriendo Visual Studio y crea una nueva aplicación de consola en .NET 8:
- Selecciona Archivo > Nuevo > Proyecto.
- Elige Aplicación de Consola (.NET).
- Nombra el proyecto, por ejemplo,
ChatBotExample.
Configurar las Variables de Entorno
Necesitas configurar tu clave de API de OpenAI como una variable de entorno:
- Ve a las variables de entorno de tu sistema.
- Crea una nueva variable llamada
OPENAI_API_KEY. - Establece su valor a tu clave de API de OpenAI.
Paso 2: Instalación de Librerías Necesarias
Para interactuar con OpenAI y manejar la base de datos vectorial, necesitamos instalar algunas librerías:
Librería de OpenAI
Instala la librería oficial de OpenAI para .NET (Prerelease):
dotnet add package OpenAI --prerelease
Librería para la Base de Datos Vectorial
Usaremos Unum Usearch para manejar nuestra base de datos vectorial:
dotnet add package Cloud.Unum.USearch
Otras Dependencias
También necesitaremos:
- SQLite para manejar nuestra base de datos local.
- Dapper para facilitar las operaciones con la base de datos.
Instálalos usando:
dotnet add package Microsoft.Data.Sqlite
dotnet add package Dapper
Paso 3: Preparación de la Base de Conocimientos
Necesitamos un dataset que contiene consultas y respuestas relacionadas con una universidad. Para este ejemplo usaremos el dataset de huggingface convertido a SQLite
Cargar el Dataset
- Coloca el archivo del dataset en una carpeta dentro de tu proyecto.
- Usa Dapper para conectarte a la base de datos y extraer las consultas y respuestas.
//POCO para el mapeo
public class Questions
{
public required int IdConsulta { get; set; }
public required string Consulta { get; set; }
public required int Articulo { get; set; }
public required string Texto { get; set; }
}
await using var db = new SqliteConnection($"Data Source=dataset.db");
var questions = db.Query<Questions>("SELECT * FROM converted");
Paso 4: Generación de Embeddings
Los embeddings convierten texto en representaciones numéricas que permiten comparar similitudes semánticas entre diferentes textos.
Obtener Embeddings con OpenAI
Utiliza el modelo text-embedding-3-small de OpenAI para generar embeddings de nuestras consultas y respuestas.
EmbeddingClient embeddingClient = new EmbeddingClient("text-embedding-3-small", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
foreach (var question in questions)
{
string qAndA = $"{question.Consulta}\n{question.Texto}";
var response = await embeddingClient.GenerateEmbeddingAsync(qAndA);
if (response is not null)
{
var floatMemory = response.Value.ToFloats();
index.Add((ulong)question.IdConsulta, floatMemory.ToArray());
}
}
Paso 5: Creación de la Base de Datos Vectorial
Con los embeddings generados, crearemos una base de datos vectorial que nos permitirá buscar respuestas relevantes basadas en la similitud semántica.
Configurar Unum Usearch
- Inicializa un índice vectorial con la dimensión correcta (por ejemplo, 1536 para el modelo
text-embedding-3-small). - Agrega los embeddings al índice junto con sus identificadores.
var index = new USearchIndex(
metricKind: MetricKind.Cos,
quantization: ScalarKind.Float32,
dimensions: 1536, // Dimension de vector de embeddings de OpenAI
connectivity: 16,
expansionAdd: 128,
expansionSearch: 64
);
foreach (var question in questions)
{
string qAndA = $"{question.Consulta}\n{question.Texto}";
var response = await embeddingClient.GenerateEmbeddingAsync(qAndA);
if (response is not null)
{
var floatMemory = response.Value.ToFloats();
index.Add((ulong)question.IdConsulta, floatMemory.ToArray());
}
}
index.Save("dataset.vdb");
Paso 6: Implementación del Chatbot
Ahora que tenemos nuestra base de datos vectorial, podemos comenzar a construir el chatbot.
Flujo del Chatbot
- Entrada del Usuario: El usuario hace una pregunta.
- Generación del Embedding de la Pregunta: Convertimos la pregunta del usuario en un embedding.
- Búsqueda en la Base de Datos Vectorial: Encontramos las respuestas más relevantes basadas en la similitud de embeddings.
- Construcción del Prompt: Creamos un prompt que incluye las respuestas relevantes y la pregunta del usuario.
- Generación de la Respuesta: Usamos el modelo de lenguaje de OpenAI para generar la respuesta final.
string? line = Console.ReadLine(); //Entrada del Usuario
if (line is not null)
{
//Generación del Embedding de la Pregunta
var response = await embeddingClient.GenerateEmbeddingAsync(line);
if (response is not null)
{
var floats = response.Value.ToFloats().ToArray();
int maxResponses = 10;
//Búsqueda en la Base de Datos Vectorial
index.Search(floats, maxResponses, out var keys, out var distances);
var sbPrompt = new StringBuilder();
sbPrompt.AppendLine($"Con las siguientes preguntas y respuestas:");
for (int i = 0; i < maxResponses; i++)
{
int idConsulta = (int)keys[i];
var question = questions.FirstOrDefault(q => q.IdConsulta == idConsulta);
//Construcción del Prompt
if (question is not null)
{
sbPrompt.AppendLine($"Pregunta: {question.Consulta}");
sbPrompt.AppendLine($"Respuesta:\n {question.Texto}");
sbPrompt.AppendLine();
}
}
//Agrega la pregunta inicial al prompt
sbPrompt.AppendLine($"Response la siguiente pregunta: {line}");
string prompt = sbPrompt.ToString();
//Generación de la Respuesta
ChatMessage[] chatMessages = [new UserChatMessage(prompt )];
await foreach (var aiResponse in client.CompleteChatStreamingAsync(chatMessages))
{
if (aiResponse.ContentUpdate.Count > 0)
{
Console.Write(aiResponse.ContentUpdate[0].Text);
}
}
}
}
System Prompt
Para mejorar la interacción, podemos implementar un system prompt que modifique la manera en la que el Bot interactúa con el usuario, por ejemplo:
//system prompt used to define behavior for introduction and answers
string system = "Eres una bot de soporte tecnico que responde solo con la informacion que se mande en tu contexto y para cualquier duda que tu tengas darle la informacion de contacto al cliente: contact@cosme.com.mx numero telefonico: 445577";
//we stream the initial introduction
await foreach (var aiResponse in client.CompleteChatStreamingAsync(
[new UserChatMessage("Presentate ante el cliente de soporte tecnico, trabajas en universidad de Cosme Zamudio."), new SystemChatMessage(system)]))
{
if (aiResponse.ContentUpdate.Count > 0)
{
Console.Write(aiResponse.ContentUpdate[0].Text);
}
}
Conclusión
Hemos creado un chatbot funcional utilizando las APIs más recientes de OpenAI y una base de datos vectorial para manejar el conocimiento. Aunque este es un enfoque básico, puede ser expandido y mejorado para crear soluciones más robustas y escalables.
Recursos
Preguntas Frecuentes
¿Puedo usar otros modelos de OpenAI?
Sí, puedes experimentar con diferentes modelos como gpt-3.5-turbo o GPT-4o para obtener diferentes resultados en las respuestas.
¿Es necesario usar una base de datos vectorial?
Para mejorar la relevancia y precisión de las respuestas, es recomendable. Sin embargo, para datasets muy pequeños, podrías manejar las comparaciones directamente en memoria.
¿Cómo puedo actualizar la base de conocimientos?
Deberás agregar las nuevas consultas y respuestas al dataset, generar los embeddings correspondientes y actualizar el índice vectorial.