Crea un chatbot básico con Python y OpenAI
Segunda parte de la serie de artículos sobre construir tus propias soluciones con LLMs
En el artículo anterior vimos cómo dar nuestros primeros pasos hacia la creación de soluciones con LLMs, específicamente, cómo consumir la API de OpenAI para interactuar con GPT y así enviarle nuestros propios prompts. Si no lo leíste, aquí te dejo el enlace.
En este artículo iremos un paso más allá y construiremos una aplicación en la que podemos conversar con GPT, aunque con ciertas limitaciones.
Aclaración: En este link podrás encontrar un notebook con el código que aparece en este artículo, listo para que lo puedas probar.
Estableciendo el entorno
Al igual que con nuestro “Hola mundo!” del artículo anterior, debemos prepararnos para poder comunicarnos con la API de OpenAI. Para ello, debemos importar la clase OpenAIde la librería openai que nos ayudará a inicializar nuestro cliente de OpenAI:
from openai import OpenAI
import os
# Cargamos nuestra API de OpenAI (en mi caso, la tengo guardada
# como una variable de entorno para evitar copiarla aquí de forma explícita)
openai_api_key = os.getenv("OPENAI_API_KEY")
# Inicializar el cliente de OpenAI
llm = OpenAI(api_key = openai_api_key)
Como lo explico en el comentario del código, al igual que en el artículo anterior, en mi caso particular guardé la API Key de OpenAI como una variable de entorno, para evitar copiarla dentro del código y así exponiéndola a cualquiera que tenga dicho código. Recordá que también podés hacerlo en un archivo .env y luego cargarlo dentro de nuestro código.
Con esto definido, ya tenemos inicializado el cliente que utilizaremos para comunicarnos con la API de OpenAI.
La estructura de un prompt
Los prompts son las instrucciones que le damos a un modelo de lenguaje, ya sean generadores de texto como de imágenes. Por el momento, obviaremos los modelos multimodales, es decir, que trabajan con imágenes, y nos concentraremos en las respuesta de texto.
En estos modelos, los prompts los podemos escribir como a nosotros más nos guste. Sin embargo, si respetamos cierta estructura, la probabilidad de que el modelo nos entienda y, por lo tanto, nos dé el resultado deseado, serán muchísimo mayores.
Dentro de Python, los prompts son proporcionados a la función chat.completions.create() a través de un parámetro messages que es una lista de mensajes que le pasamos al modelo de lenguaje para que construya nuestra respuesta.
Cada uno de los mensajes de esta lista es un diccionario que contiene dos elementos, el rol (role) y el contenido (content).
Roles
El rol dentro de una lista de mensajes podemos pensarlo como un indicador para el modelo de lenguaje de cómo debe interpretar el contenido que hay a continuación. Los roles son tres: developer, user y assistant.
Developer
Este rol, que anteriormente se llamaba system, se utiliza para indicarle al modelo qué rol debe asumir al momento de leer la pregunta y generar la respuesta. Esto lo hacemos escribiéndolo dentro del campo content. Por ejemplo, si queremos que nos responda preguntas sobre Python, pero que lo haga en versos, podríamos definir su rol como sigue:
{'role': 'developer',
'content': """Eres un asistente amigable y experto en Python.
Responde las preguntas de manera clara y concisa.
Responde como si fueras un poeta."""
}
User
El rol de usuario, o user es el que comunmente se utiliza para pasarle el mensaje como lo haríamos con chatGPT, es decir, es el mensaje que nosotros, como usuarios, escribiríamos. Por ejemplo:
{'role': 'user',
'content': 'Explicame el concepto del bucle for'
}
Assistant
Este último rol no es utilizado tan frecuentemente como los otros dos. Los mensajes que tienen el rol de asistente o assistant suelen ser mensajes que habría creado el modelo en una secuencia previa de mensajes. Suele utilizarse también para dar ejemplos que sirvan al modelo de contexto. Se utiliza en complemento con los otros roles.
Ejemplo:
{'role': 'user',
'content': 'Quiero que deletrees Python como si fueras una porrista. Dame la P'},
{'role': 'assistant',
'content': 'Te doy la P'},
{'role': 'user',
'content': 'Dame la Y (continua)'}
Construyamos nuestro chatbot
Ahora sí, comencemos a construir nuestro chatbot.
Definiendo el rol de experto
Como mencionamos en la sección anterior, debemos definir cómo queremos que se comporte nuestro chatbot con respecto al contenido que le pedimos que genere, es decir, qué rol queremos que adopte.
En este ejemplo, vamos a crear un chatbot para que nos explique conceptos básicos de Python de forma sencilla. Para ello, debemos indicarle, mediante el rol developer, que se comporte como un experto en Python:
developer_prompt = {
'role': 'developer',
'content': """
Eres un asistente amigable y experto en Python.
Responde las preguntas de manera clara y concisa.
"""
}
Aquí definimos una variable que contiene el diccionario con el mensaje que le pasaremos al sistema para que adopte el rol de experto, pero aún no se lo hemos indicado al modelo.
El mensaje del usuario, es decir, las preguntas sobre Python en sí, no la definiremos como developer_prompt porque irá cambiando constantemente de forma dinámica a medida que le preguntemos una y otra cosa.
El corazón del chatbot
Definamos ahora el bloque de código que procesará la pregunta y generará la respuesta basado en el rol que le pedimos que asuma. Para esto, lo más sencillo es definir una función que realice esta tarea, ya que en nuestro chatbot, le haremos más de una pregunta y no queremos repetir código:
def obtener_respuesta(dev_prompt, mensaje_usuario):
user_prompt = {
"role": "user",
"content": mensaje_usuario
}
# Llamada a la API
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
dev_prompt,
user_prompt
],
)
# Retorna el texto generado
return response.choices[0].message.content
El programa principal
Una vez que tenemos definida la función que realiza la interacción con el modelo de lenguaje, podemos crear nuestra función principal. Como queremos que sea capaz de hacerle más de una pregunta, crearemos un bucle que nos permita preguntar una y otra vez hasta que le demos una palabra de salida que será, justamente, salir.
print("¡Hola! Soy tu asistente en Python. Escribe 'salir' para terminar la conversación.")
while True:
mensaje_usuario = input("Tú: ")
if mensaje_usuario.lower() == "salir":
print("Asistente: ¡Hasta luego!")
break
respuesta = obtener_respuesta(developer_prompt, mensaje_usuario)
print(f"Asistente: {respuesta}")
Veamos qué hace este código. La primera línea es bastante sencilla y autoexplicatoria. Solamente es un saludo de bienvenida.
Luego, entra en un bucle infinito (while True:) ya que, a menos que se le dé una condición de salida dentro del bucle, permanecerá ejecutándose de manera indefinida.
Nota: Hay que tener cuidado con el uso de bucles infinitos ya que, como dijimos más arriba, si no le proporcionamos una condición de salida, el flujo puede quedarse atrapado dentro del bucle para siempre.
Dentro del bucle, le pedimos al usuario que ingrese su mensaje y lo capturamos con input("Tú: "). Esto muestra el mensaje Tú: en pantalla y espera nuestra pregunta, la cual es almacenada en la variable mensaje_usuario.
Luego se fija si el mensaje ingresado es la palabra salir. Si es así, nos despide con un mensaje y corta el bucle con la sentencia break.
En caso que el mensaje no sea salir, no entra en el bloque dentro del if y continua con la primera línea que es la llamada a nuestra función obtener_respuesta(). Aquí le proporcionamos el prompt que define el rol del sistema y el mensaje que ingresó el usuario, y captura su salida en la variable respuesta.
Por último, simplemente imprime la respuesta recibida. Al finalizar, como todo esto está dentro de un bucle, vuelve a pedir la siguiente pregunta, y así sucesivamente hasta que le indiquemos que queremos terminar.
Primeros tests
Si ejecutamos el código nos aparecerá en pantalla un texto dándonos la bienvenida y estará esperando que ingresemos nuestra consulta al chatbot. Se vería algo así (la vista puede variar dependiendo del Sistema Operativo, el IDE y el intérprete que utilices):
¡Hola! Soy tu asistente en Python. Escribe 'salir' para terminar la conversación.
Tú: _
Te invito a que pruebes preguntarle algo relacionado con Python, por ejemplo, “Quiero que me expliques cómo funcionan los bucles for”. Esto arrojará una respuesta (que voy a copiar aquí porque puede ser muy extensa) a nuestra pregunta.
Pero, ¿qué pasa si le preguntamos algo diferente a Python como, por ejemplo, que nos diga una receta de lemon pie? Al no tener ninguna restricción al respecto, nos dará la receta sin problemas. Pero esto no es lo que queremos para nuestro chatbot. Nosotros queremos que solamente responda preguntas relacionadas a Python y nada más. Para lograrlo, debemos indicarle explícitamente esta instrucción, modificando la declaración de la variable developer_prompt.
developer_prompt = {
"role": "developer",
"content": """
Eres un asistente amigable y experto en Python.
Responde las preguntas de manera clara y concisa.
Solo responde preguntas relacionadas con Python.
Comprueba si el pedido se ajusta a algún concepto relacionado con Python.
Si recibes una pregunta que no esté relacionada en absoluto con Python di que solo conoces sobre Python, nada más.
"""
}
Con esta simple línea que agregamos al developer_prompt, es suficiente para que no responda cosas ajenas a Python. Si, ahora, le preguntamos por la receta, la respuesta será algo así como: “Yo solo puedo responder cosas relacionadas con Python.”
Esto nos hace ver una de las grandes limitaciones que tiene nuestro chatbot, que las detallaré a continuación.
Limitaciones
Como vimos hasta ahora, el flujo del programa es bastante sencillo. Esto hace que también sea bastante limitado. Entre las limitaciones más importantes podemos encontrar:
Memoria
Una de las cosas que no posee este chatbot es memoria de las preguntas anteriores. Si nosotros le pedimos que nos explique, por ejemplo, cómo funciona el bucle for y, luego de su respuesta le preguntamos cuál fue nuestra consulta anterior, nos dirá que no tiene capacidad de retener interaccioens previas. Esto hace que nuestra solución no sea realmente un chatbot, sino que es más bien un sistema de pregunta/respuesta (o Q&A como se lo conoce en inglés).
Para poder solucionar este problema, debemos guardar las preguntas y respuestas previas y pasárselas como contexto ante una nueva interacción. De esta manera, el modelo tendrá forma de saber qué fue lo que estuvo sucediendo antes en la conversación y, así, tener un verdadero chatbot, con el que se pueda conversar.
Existen varias formas de solucionar este problema, desde almacenarlo en un archivo externo de texto plano hasta la utilización de una base de datos (ya sea local o en la nube) donde guardamos cada par pregunta/respuesta como también metadata asociada (fecha y hora de la interacción, orden en la lista de interacciones, etc.). Esta información se la proporcionamos al prompt a través del rol de assistant que vimos anteriormente.
Guardrails
Utilizo este término en inglés porque es el que se usa ampliamente en la jerga, ya que su traducción al español sería algo así como “barandilla” (no me gusta mezclar términos en castellano e inglés, pero en este caso, que me pareció que el término en español no va).
Los guardrails son mecanismos de seguridad que se emplean en las soluciones potenciadas con modelos de lenguaje para evitar que se vaya de contexto y dé información incorrecta, o que utilice un léxico ofensivo, discriminatorio, o que arroje contenido que pueda llegar a ser nocivo (por ejemplo, si le pedimos que nos de la receta para construir una bomba casera).
Como ves, el uso de guardrails es casi obligatorio si queremos que nuestra solución sea robusta y segura. Sin embargo, su implementación tiene ciertos aspectos no tan positivos. Por un lado, los guardrails debemos ingresárselos manualmente uno por uno en el prompt, por ejemplo, utilizando el rol de assistant. Esto hace que su implementación sea, en gran medida, por prueba y error.
No podemos conocer de antemano todos los guardrails que debemos utilizar. Muchos de ellos son bastante evidentes, pero muchos otros surgirán del uso y testeo de nuestra solución, como el ejemplo de más arriba que limita solo a responder preguntas relacionadas con Python. Eso por esto que toda solución debe someterse a una instancia de prueba, o testing, antes de ser abierto al público.
Interfaz gráfica
Una última limitación muy evidente que presenta este chatbot básico que creamos es la falta de una interfaz gráfica. Vimos que, para obtener la instrucción por parte del usuario, el programa la pide utilizando la instrucción input(), que toma lo que nosotros escribamos en la terminal.
Para poder ser una solución más amigable con el usuario debemos incorporar una interfaz visual que haga su uso más sencillo. Existen muchos lenguajes de programación destinados a ello, como React, VUE, Angular, entre otros.
Para este ejemplo, el próximo artículo que publicaré será sobre cómo crear una interfaz gráfica para este simple chatbot.
Conclusiones
Hasta aquí aprendimos cómo comunicarnos con la API de OpenAI y lo utilizamos para crear un chatbot simple. Si bien aún están las limitaciones que mencionamos más arriba, tenemos el bloque fundacional para construir algo grande. Y, lo mejor de todo, es que esto se puede hacer de forma incremental, es decir, que el próximo paso lo podemos construir sobre lo que ya logramos hoy, agregando una funcionalidad extra por ejemplo, como mencioné más arriba, incorporar una interfaz gráfica con Streamlit.
Te invito a que compartas con nosotros tus avances, que nos muestres el chatbot que estás construyendo, para que entre todos nos ayudemos y avancemos.
Nos vemos la próxima con el siguiente artículo. Si te gustó, hacémelo saber y compartíselo a todas las personas que creas que le puede ser útil. A vos te cuesta poco y a mí me ayuda muchísimo para poder llegar a más personas que quieran saber cómo construir soluciones potenciadas por modelos de lenguaje. ¡Hasta la próxima!

