Proyecto: Construcción de un Chatbot Simple con Memoria
Pasos del Proyecto
1. Configuración del Proyecto
- Crea un nuevo directorio para tu proyecto:
mkdir chatbot_project
cd chatbot_project - Crea un entorno virtual:
python -m venv venv
- Activa el entorno virtual:
- En Windows:
venv\Scripts\activate
- En macOS y Linux:
source venv/bin/activate
- Instala las bibliotecas requeridas:
pip install flask openai flask-session python-dotenv streamlit psycopg2
- Crea un archivo
.env
en el directorio de tu proyecto y añade tu clave API de OpenAI y la URL de la base de datos:OPENAI_API_KEY=YOUR_OPENAI_API_KEY
DATABASE_URL=YOUR_DATABASE_URL # ej., "postgresql://user:password@host:port/database_name" - Para la base de datos postgres, asegúrate de tener una base de datos postgres en ejecución y que la URL esté configurada correctamente.
2. Implementación en Flask
- Crea un archivo llamado
app.py
con el siguiente código:
from flask import Flask, request, render_template, session, redirect, url_for
import openai
import os
from dotenv import load_dotenv
from flask_session import Session
import psycopg2 #for postgres
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
app = Flask(__name__)
app.secret_key = os.urandom(24)
app.config["SESSION_TYPE"] = "filesystem" # Or "postgresql", "redis", etc. For production, use a proper database.
#app.config["SESSION_TYPE"] = "postgresql"
#app.config["SESSION_PERMANENT"] = True # Optional: Make sessions permanent
#app.config["SESSION_SQLALCHEMY"] = {'url': DATABASE_URL} #needs Flask-SQLAlchemy
Session(app)
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
print(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
print(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
print(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
print(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Chat route ---
@app.route("/", methods=["GET", "POST"])
def chat():
"""Handles the main chat functionality."""
if "session_id" not in session:
session["session_id"] = os.urandom(16).hex() # Generate unique session ID
session_id = session["session_id"]
history = get_history_from_db(session_id) # Get history from the database
if not history:
history = [{"role": "system", "content": "You are a helpful assistant."}]
session["history"] = history # Initialize session
if request.method == "POST":
user_input = request.form["user_input"]
store_message(session_id, "user", user_input) # Store to database
history.append({"role": "user", "content": user_input})
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=history,
temperature=0.6,
)
assistant_reply = response.choices[0].message.content
store_message(session_id, "assistant", assistant_reply) # Store to database
history.append({"role": "assistant", "content": assistant_reply})
session["history"] = history
session.modified = True # IMPORTANT
return render_template("chat.html", history=history[1:]) # Pass history to template
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
return render_template("chat.html", history=history[1:])
@app.route("/clear", methods=["POST"])
def clear_chat():
session.pop("session_id", None)
session.pop("history", None)
return redirect(url_for("chat"))
if __name__ == "__main__":
app.run(debug=True)
Desglose del código:
from flask import ...
: Importa los módulos necesarios de Flask.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.from flask_session import Session
: Importa la claseSession
deflask_session
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con PostgreSQL.load_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL desde la variable de entorno.app = Flask(__name__)
: Crea una instancia de la aplicación Flask.app.secret_key = os.urandom(24)
: Establece una clave secreta para la aplicación Flask. Esto es esencial para usar sesiones de Flask.app.config["SESSION_TYPE"] = "filesystem|postgresql|redis|mongodb"
: Configura el tipo de almacenamiento de sesión. Para este proyecto, estamos usando una base de datos.Session(app)
: Inicializa la extensión Flask-Session, vinculándola a la aplicación Flask.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL desde la variable de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión con un bloque try...except.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
session_id
.
- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
get_history_from_db(session_id)
:- Recupera el historial de conversación para un
session_id
específico desde la base de datos.
- Recupera el historial de conversación para un
@app.route("/", methods=["GET", "POST"])
: Define la ruta para la página principal de la aplicación ("/"). La funciónchat()
maneja tanto las peticiones GET como POST a esta URL.def chat():
if "session_id" not in session:
: Verifica si existe unsession_id
en la sesión del usuario. Si no, genera un ID único usandoos.urandom(16).hex()
y lo almacena en la sesión.session_id = session["session_id"]
: Obtiene elsession_id
de la sesión.history = get_history_from_db(session_id)
: Recupera el historial de conversación desde la base de datos usando el ID de sesión.if not history
: verifica si el historial está vacío y lo inicializa.if request.method == "POST":
: Maneja las peticiones POST (cuando el usuario envía un mensaje).user_input = request.form["user_input"]
: Obtiene la entrada del usuario desde el formulario.store_message(session_id, "user", user_input)
: Almacena el mensaje del usuario en la base de datos, asociado con el ID de sesión.- El código luego llama a la API de OpenAI para obtener una respuesta, almacena la respuesta en la base de datos y actualiza la sesión.
- Renderiza la plantilla "chat.html", pasando el historial de conversación.
return render_template("chat.html", history=history[1:])
: Maneja las peticiones GET (cuando el usuario carga la página). Renderiza la plantilla "chat.html", pasando el historial de conversación.
@app.route("/clear", methods=["POST"])
: Ruta para limpiar la sesión del chat.def clear_chat():
: limpia elsession_id
y elhistory
de la sesión.if __name__ == "__main__":
: Inicia el servidor de desarrollo Flask si el script se ejecuta directamente.- Crea una carpeta llamada
templates
en el mismo directorio queapp.py
. - Crea un archivo llamado
chat.html
dentro de la carpetatemplates
con el siguiente código:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPT-4o Chat Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* --- General Styles --- */
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
color: #1f2937;
}
.container {
max-width: 800px;
width: 100%;
background-color: #fff;
padding: 30px;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h2 {
font-size: 1.875rem;
font-weight: 600;
margin-bottom: 2rem;
color: #1e293b;
text-align: center;
}
/* --- Message Styles --- */
.message {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
}
.message p {
padding: 1rem;
border-radius: 1rem;
max-width: 80%;
}
.user p {
background-color: #e0f2fe;
color: #1d4ed8;
margin-left: auto;
}
.assistant p {
background-color: #f0fdf4;
color: #15803d;
}
.message strong {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.user strong {
color: #1e40af;
}
.assistant strong {
color: #16a34a;
}
/* --- Form Styles --- */
form {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
resize: none;
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 0.25rem;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
input[type="submit"] {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
input[type="submit"]:hover {
background-color: #2563eb;
}
input[type="submit"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
.container {
padding: 20px;
}
textarea {
height: 100px;
}
}
.clear-history-button{
margin-top: 2rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h2>🧠 GPT-4o Chat Assistant</h2>
{% for msg in history %}
<div class="message {{ msg.role }}">
<p><strong>{{ msg.role.capitalize() }}:</strong> {{ msg.content }}</p>
</div>
{% endfor %}
<form method="post">
<textarea name="user_input" placeholder="Type your message..."></textarea><br>
<input type="submit" value="Send Message">
</form>
<form method="post" action="/clear">
<input type="submit" value="Clear Chat" class = "clear-history-button">
</form>
</div>
</body>
</html>
- Esta plantilla HTML proporciona la estructura y el estilo para la interfaz del chat. Incluye un formulario para la entrada del usuario y muestra el historial de la conversación.
3. Implementación de Streamlit
Crea un archivo llamado streamlit_app.py
con el siguiente código:
import streamlit as st
import openai
import os
from dotenv import load_dotenv
import psycopg2 # Import psycopg2
import datetime
from typing import List, Dict
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
st.set_page_config(page_title="GPT-4o Chat with Memory", page_icon="🧠")
st.title("🧠 GPT-4o Chatbot with Persistent Memory")
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
st.error(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
st.error(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
st.error(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
st.error(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Session Management ---
if "session_id" not in st.session_state:
st.session_state.session_id = os.urandom(16).hex() # Generate unique session ID
session_id = st.session_state.session_id
# --- Initialize chat history ---
if "messages" not in st.session_state:
st.session_state.messages = get_history_from_db(session_id)
if not st.session_state.messages:
st.session_state.messages = [{"role": "system", "content": "You are a helpful assistant."}]
# --- Display chat history ---
for msg in st.session_state.messages[1:]: # Skip system message
speaker = "🧑💻 You" if msg["role"] == "user" else "🤖 Assistant"
with st.chat_message(msg["role"]):
st.markdown(f"**{speaker}:** {msg['content']}")
# --- User input ---
user_input = st.chat_input("Ask something...", key="user_input") # Use key for chat_input
if user_input:
# Store user message
store_message(session_id, "user", user_input)
st.session_state.messages.append({"role": "user", "content": user_input})
# Get assistant reply
with st.chat_message("assistant"):
placeholder = st.empty() # Reserve a placeholder for the response
with st.spinner("Thinking..."):
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=st.session_state.messages,
temperature=0.6,
)
reply = response["choices"][0]["message"]["content"]
store_message(session_id, "assistant", reply) # Store assistant reply
st.session_state.messages.append({"role": "assistant", "content": reply})
placeholder.markdown(f"**🤖 Assistant:** {reply}") # Replace placeholder with full response
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
# Clear the input field after sending the message
st.session_state.user_input = ""
st.rerun()
Desglose del código:
import streamlit as st
: Importa la biblioteca Streamlit.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con la base de datos PostgreSQL.import datetime
: Importa el módulo datetime.from typing import List, Dict
: Importa los tipos de datosload_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL.st.set_page_config(...)
: Configura el título y el icono de la página.st.title(...)
: Establece el título de la aplicación Streamlit.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL de las variables de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión usando un bloque
try...except
, mostrando un mensaje de error usandost.error()
.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación. - Maneja posibles errores durante la creación de la tabla usando un bloque
try...except
.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
session_id
. - Maneja posibles errores durante el almacenamiento del mensaje.
- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
- get_history_from_db(session_id):
- Recupera el historial de conversación para un session_id específico de la base de datos.
- La parte principal del código:
if "session_id" not in st.session_state:
: Verifica si existe un ID de sesión en el estado de Streamlit. Si no existe, genera un ID único usandoos.urandom(16).hex()
.session_id = st.session_state.session_id
: Recupera el ID de sesión.if "messages" not in st.session_state:
:- Recupera el historial de conversación de la base de datos.
- Si no existe historial en la base de datos para la sesión actual, inicializa el historial del chat con el mensaje del sistema.
- El código luego muestra el historial del chat.
user_input = st.chat_input(...)
: Crea un campo de entrada de texto para que el usuario escriba su mensaje.- Cuando el usuario envía un mensaje:
- El mensaje se almacena en la base de datos usando la función
store_message()
. - El mensaje se añade al historial del chat en
st.session_state
. - La aplicación llama a la API de OpenAI para obtener una respuesta.
- La respuesta del asistente se almacena en la base de datos y se añade al historial del chat.
- La respuesta del asistente se muestra en la interfaz del chat.
- El mensaje se almacena en la base de datos usando la función
4. Ejecutar las Aplicaciones
- Flask:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Flask:
python app.py
- Abre un navegador web y ve a
http://localhost:5000
para interactuar con el chatbot.
- Streamlit:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Streamlit:
streamlit run streamlit_app.py
- Se abrirá una nueva pestaña del navegador con el chatbot de Streamlit.
5. Mejoras (Opcional)
- Implementar autenticación de usuarios:
- Agregar sistema seguro de inicio de sesión/registro usando tokens JWT o de sesión
- Crear perfiles de usuario para almacenar preferencias de conversación
- Habilitar conversaciones privadas vinculadas a cuentas de usuario específicas
- Agregar función de botón "Limpiar Chat":
- Implementar limpieza tanto completa como selectiva del historial de chat
- Agregar diálogos de confirmación para prevenir borrados accidentales
- Incluir opción para exportar historial de chat antes de limpiarlo
- Mejorar la interfaz frontend:
- Crear diseños responsivos usando frameworks CSS modernos
- Agregar actualizaciones de mensajes en tiempo real usando WebSocket
- Implementar soporte para markdown y resaltado de código
- Desplegar a producción:
- Configurar pipelines de CI/CD para despliegue automatizado
- Configurar certificados SSL para comunicación segura
- Implementar sistemas de monitoreo y registro
- Agregar capacidades de base de datos vectorial:
- Implementar generación de embeddings usando modelos como BERT o USE
- Configurar búsqueda de similitud vectorial usando Pinecone o Milvus
- Crear funcionalidad de búsqueda semántica para el historial de chat
Este proyecto sirve como una base extensa para desarrollar un chatbot con capacidades de memoria. Esto es lo que obtendrás al completarlo:
Primero, obtendrás experiencia práctica con frameworks web modernos - tanto Flask para desarrollo web tradicional como Streamlit para prototipado rápido de aplicaciones de datos. Aprenderás cómo estos frameworks difieren y cuándo usar cada uno.
Segundo, dominarás el trabajo con la API de OpenAI, entendiendo cómo estructurar prompts, manejar respuestas y gestionar interacciones con la API de manera efectiva. Este conocimiento es crucial para construir cualquier aplicación potenciada por IA.
Tercero, aprenderás principios de integración de bases de datos, incluyendo cómo diseñar esquemas, gestionar conexiones y manejar la persistencia de datos. Esto asegura que tu chatbot pueda mantener conversaciones entre sesiones y escalar efectivamente.
Finalmente, este proyecto te preparará para construir aplicaciones de chatbot más sofisticadas, como aquellas que incorporan características avanzadas como análisis de sentimientos, múltiples modelos de IA o procesamiento en tiempo real. Las habilidades que desarrolles aquí forman los bloques de construcción para aplicaciones de IA más complejas.
Pasos del Proyecto
1. Configuración del Proyecto
- Crea un nuevo directorio para tu proyecto:
mkdir chatbot_project
cd chatbot_project - Crea un entorno virtual:
python -m venv venv
- Activa el entorno virtual:
- En Windows:
venv\Scripts\activate
- En macOS y Linux:
source venv/bin/activate
- Instala las bibliotecas requeridas:
pip install flask openai flask-session python-dotenv streamlit psycopg2
- Crea un archivo
.env
en el directorio de tu proyecto y añade tu clave API de OpenAI y la URL de la base de datos:OPENAI_API_KEY=YOUR_OPENAI_API_KEY
DATABASE_URL=YOUR_DATABASE_URL # ej., "postgresql://user:password@host:port/database_name" - Para la base de datos postgres, asegúrate de tener una base de datos postgres en ejecución y que la URL esté configurada correctamente.
2. Implementación en Flask
- Crea un archivo llamado
app.py
con el siguiente código:
from flask import Flask, request, render_template, session, redirect, url_for
import openai
import os
from dotenv import load_dotenv
from flask_session import Session
import psycopg2 #for postgres
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
app = Flask(__name__)
app.secret_key = os.urandom(24)
app.config["SESSION_TYPE"] = "filesystem" # Or "postgresql", "redis", etc. For production, use a proper database.
#app.config["SESSION_TYPE"] = "postgresql"
#app.config["SESSION_PERMANENT"] = True # Optional: Make sessions permanent
#app.config["SESSION_SQLALCHEMY"] = {'url': DATABASE_URL} #needs Flask-SQLAlchemy
Session(app)
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
print(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
print(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
print(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
print(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Chat route ---
@app.route("/", methods=["GET", "POST"])
def chat():
"""Handles the main chat functionality."""
if "session_id" not in session:
session["session_id"] = os.urandom(16).hex() # Generate unique session ID
session_id = session["session_id"]
history = get_history_from_db(session_id) # Get history from the database
if not history:
history = [{"role": "system", "content": "You are a helpful assistant."}]
session["history"] = history # Initialize session
if request.method == "POST":
user_input = request.form["user_input"]
store_message(session_id, "user", user_input) # Store to database
history.append({"role": "user", "content": user_input})
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=history,
temperature=0.6,
)
assistant_reply = response.choices[0].message.content
store_message(session_id, "assistant", assistant_reply) # Store to database
history.append({"role": "assistant", "content": assistant_reply})
session["history"] = history
session.modified = True # IMPORTANT
return render_template("chat.html", history=history[1:]) # Pass history to template
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
return render_template("chat.html", history=history[1:])
@app.route("/clear", methods=["POST"])
def clear_chat():
session.pop("session_id", None)
session.pop("history", None)
return redirect(url_for("chat"))
if __name__ == "__main__":
app.run(debug=True)
Desglose del código:
from flask import ...
: Importa los módulos necesarios de Flask.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.from flask_session import Session
: Importa la claseSession
deflask_session
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con PostgreSQL.load_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL desde la variable de entorno.app = Flask(__name__)
: Crea una instancia de la aplicación Flask.app.secret_key = os.urandom(24)
: Establece una clave secreta para la aplicación Flask. Esto es esencial para usar sesiones de Flask.app.config["SESSION_TYPE"] = "filesystem|postgresql|redis|mongodb"
: Configura el tipo de almacenamiento de sesión. Para este proyecto, estamos usando una base de datos.Session(app)
: Inicializa la extensión Flask-Session, vinculándola a la aplicación Flask.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL desde la variable de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión con un bloque try...except.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
session_id
.
- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
get_history_from_db(session_id)
:- Recupera el historial de conversación para un
session_id
específico desde la base de datos.
- Recupera el historial de conversación para un
@app.route("/", methods=["GET", "POST"])
: Define la ruta para la página principal de la aplicación ("/"). La funciónchat()
maneja tanto las peticiones GET como POST a esta URL.def chat():
if "session_id" not in session:
: Verifica si existe unsession_id
en la sesión del usuario. Si no, genera un ID único usandoos.urandom(16).hex()
y lo almacena en la sesión.session_id = session["session_id"]
: Obtiene elsession_id
de la sesión.history = get_history_from_db(session_id)
: Recupera el historial de conversación desde la base de datos usando el ID de sesión.if not history
: verifica si el historial está vacío y lo inicializa.if request.method == "POST":
: Maneja las peticiones POST (cuando el usuario envía un mensaje).user_input = request.form["user_input"]
: Obtiene la entrada del usuario desde el formulario.store_message(session_id, "user", user_input)
: Almacena el mensaje del usuario en la base de datos, asociado con el ID de sesión.- El código luego llama a la API de OpenAI para obtener una respuesta, almacena la respuesta en la base de datos y actualiza la sesión.
- Renderiza la plantilla "chat.html", pasando el historial de conversación.
return render_template("chat.html", history=history[1:])
: Maneja las peticiones GET (cuando el usuario carga la página). Renderiza la plantilla "chat.html", pasando el historial de conversación.
@app.route("/clear", methods=["POST"])
: Ruta para limpiar la sesión del chat.def clear_chat():
: limpia elsession_id
y elhistory
de la sesión.if __name__ == "__main__":
: Inicia el servidor de desarrollo Flask si el script se ejecuta directamente.- Crea una carpeta llamada
templates
en el mismo directorio queapp.py
. - Crea un archivo llamado
chat.html
dentro de la carpetatemplates
con el siguiente código:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPT-4o Chat Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* --- General Styles --- */
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
color: #1f2937;
}
.container {
max-width: 800px;
width: 100%;
background-color: #fff;
padding: 30px;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h2 {
font-size: 1.875rem;
font-weight: 600;
margin-bottom: 2rem;
color: #1e293b;
text-align: center;
}
/* --- Message Styles --- */
.message {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
}
.message p {
padding: 1rem;
border-radius: 1rem;
max-width: 80%;
}
.user p {
background-color: #e0f2fe;
color: #1d4ed8;
margin-left: auto;
}
.assistant p {
background-color: #f0fdf4;
color: #15803d;
}
.message strong {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.user strong {
color: #1e40af;
}
.assistant strong {
color: #16a34a;
}
/* --- Form Styles --- */
form {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
resize: none;
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 0.25rem;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
input[type="submit"] {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
input[type="submit"]:hover {
background-color: #2563eb;
}
input[type="submit"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
.container {
padding: 20px;
}
textarea {
height: 100px;
}
}
.clear-history-button{
margin-top: 2rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h2>🧠 GPT-4o Chat Assistant</h2>
{% for msg in history %}
<div class="message {{ msg.role }}">
<p><strong>{{ msg.role.capitalize() }}:</strong> {{ msg.content }}</p>
</div>
{% endfor %}
<form method="post">
<textarea name="user_input" placeholder="Type your message..."></textarea><br>
<input type="submit" value="Send Message">
</form>
<form method="post" action="/clear">
<input type="submit" value="Clear Chat" class = "clear-history-button">
</form>
</div>
</body>
</html>
- Esta plantilla HTML proporciona la estructura y el estilo para la interfaz del chat. Incluye un formulario para la entrada del usuario y muestra el historial de la conversación.
3. Implementación de Streamlit
Crea un archivo llamado streamlit_app.py
con el siguiente código:
import streamlit as st
import openai
import os
from dotenv import load_dotenv
import psycopg2 # Import psycopg2
import datetime
from typing import List, Dict
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
st.set_page_config(page_title="GPT-4o Chat with Memory", page_icon="🧠")
st.title("🧠 GPT-4o Chatbot with Persistent Memory")
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
st.error(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
st.error(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
st.error(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
st.error(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Session Management ---
if "session_id" not in st.session_state:
st.session_state.session_id = os.urandom(16).hex() # Generate unique session ID
session_id = st.session_state.session_id
# --- Initialize chat history ---
if "messages" not in st.session_state:
st.session_state.messages = get_history_from_db(session_id)
if not st.session_state.messages:
st.session_state.messages = [{"role": "system", "content": "You are a helpful assistant."}]
# --- Display chat history ---
for msg in st.session_state.messages[1:]: # Skip system message
speaker = "🧑💻 You" if msg["role"] == "user" else "🤖 Assistant"
with st.chat_message(msg["role"]):
st.markdown(f"**{speaker}:** {msg['content']}")
# --- User input ---
user_input = st.chat_input("Ask something...", key="user_input") # Use key for chat_input
if user_input:
# Store user message
store_message(session_id, "user", user_input)
st.session_state.messages.append({"role": "user", "content": user_input})
# Get assistant reply
with st.chat_message("assistant"):
placeholder = st.empty() # Reserve a placeholder for the response
with st.spinner("Thinking..."):
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=st.session_state.messages,
temperature=0.6,
)
reply = response["choices"][0]["message"]["content"]
store_message(session_id, "assistant", reply) # Store assistant reply
st.session_state.messages.append({"role": "assistant", "content": reply})
placeholder.markdown(f"**🤖 Assistant:** {reply}") # Replace placeholder with full response
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
# Clear the input field after sending the message
st.session_state.user_input = ""
st.rerun()
Desglose del código:
import streamlit as st
: Importa la biblioteca Streamlit.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con la base de datos PostgreSQL.import datetime
: Importa el módulo datetime.from typing import List, Dict
: Importa los tipos de datosload_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL.st.set_page_config(...)
: Configura el título y el icono de la página.st.title(...)
: Establece el título de la aplicación Streamlit.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL de las variables de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión usando un bloque
try...except
, mostrando un mensaje de error usandost.error()
.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación. - Maneja posibles errores durante la creación de la tabla usando un bloque
try...except
.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
session_id
. - Maneja posibles errores durante el almacenamiento del mensaje.
- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
- get_history_from_db(session_id):
- Recupera el historial de conversación para un session_id específico de la base de datos.
- La parte principal del código:
if "session_id" not in st.session_state:
: Verifica si existe un ID de sesión en el estado de Streamlit. Si no existe, genera un ID único usandoos.urandom(16).hex()
.session_id = st.session_state.session_id
: Recupera el ID de sesión.if "messages" not in st.session_state:
:- Recupera el historial de conversación de la base de datos.
- Si no existe historial en la base de datos para la sesión actual, inicializa el historial del chat con el mensaje del sistema.
- El código luego muestra el historial del chat.
user_input = st.chat_input(...)
: Crea un campo de entrada de texto para que el usuario escriba su mensaje.- Cuando el usuario envía un mensaje:
- El mensaje se almacena en la base de datos usando la función
store_message()
. - El mensaje se añade al historial del chat en
st.session_state
. - La aplicación llama a la API de OpenAI para obtener una respuesta.
- La respuesta del asistente se almacena en la base de datos y se añade al historial del chat.
- La respuesta del asistente se muestra en la interfaz del chat.
- El mensaje se almacena en la base de datos usando la función
4. Ejecutar las Aplicaciones
- Flask:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Flask:
python app.py
- Abre un navegador web y ve a
http://localhost:5000
para interactuar con el chatbot.
- Streamlit:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Streamlit:
streamlit run streamlit_app.py
- Se abrirá una nueva pestaña del navegador con el chatbot de Streamlit.
5. Mejoras (Opcional)
- Implementar autenticación de usuarios:
- Agregar sistema seguro de inicio de sesión/registro usando tokens JWT o de sesión
- Crear perfiles de usuario para almacenar preferencias de conversación
- Habilitar conversaciones privadas vinculadas a cuentas de usuario específicas
- Agregar función de botón "Limpiar Chat":
- Implementar limpieza tanto completa como selectiva del historial de chat
- Agregar diálogos de confirmación para prevenir borrados accidentales
- Incluir opción para exportar historial de chat antes de limpiarlo
- Mejorar la interfaz frontend:
- Crear diseños responsivos usando frameworks CSS modernos
- Agregar actualizaciones de mensajes en tiempo real usando WebSocket
- Implementar soporte para markdown y resaltado de código
- Desplegar a producción:
- Configurar pipelines de CI/CD para despliegue automatizado
- Configurar certificados SSL para comunicación segura
- Implementar sistemas de monitoreo y registro
- Agregar capacidades de base de datos vectorial:
- Implementar generación de embeddings usando modelos como BERT o USE
- Configurar búsqueda de similitud vectorial usando Pinecone o Milvus
- Crear funcionalidad de búsqueda semántica para el historial de chat
Este proyecto sirve como una base extensa para desarrollar un chatbot con capacidades de memoria. Esto es lo que obtendrás al completarlo:
Primero, obtendrás experiencia práctica con frameworks web modernos - tanto Flask para desarrollo web tradicional como Streamlit para prototipado rápido de aplicaciones de datos. Aprenderás cómo estos frameworks difieren y cuándo usar cada uno.
Segundo, dominarás el trabajo con la API de OpenAI, entendiendo cómo estructurar prompts, manejar respuestas y gestionar interacciones con la API de manera efectiva. Este conocimiento es crucial para construir cualquier aplicación potenciada por IA.
Tercero, aprenderás principios de integración de bases de datos, incluyendo cómo diseñar esquemas, gestionar conexiones y manejar la persistencia de datos. Esto asegura que tu chatbot pueda mantener conversaciones entre sesiones y escalar efectivamente.
Finalmente, este proyecto te preparará para construir aplicaciones de chatbot más sofisticadas, como aquellas que incorporan características avanzadas como análisis de sentimientos, múltiples modelos de IA o procesamiento en tiempo real. Las habilidades que desarrolles aquí forman los bloques de construcción para aplicaciones de IA más complejas.
Pasos del Proyecto
1. Configuración del Proyecto
- Crea un nuevo directorio para tu proyecto:
mkdir chatbot_project
cd chatbot_project - Crea un entorno virtual:
python -m venv venv
- Activa el entorno virtual:
- En Windows:
venv\Scripts\activate
- En macOS y Linux:
source venv/bin/activate
- Instala las bibliotecas requeridas:
pip install flask openai flask-session python-dotenv streamlit psycopg2
- Crea un archivo
.env
en el directorio de tu proyecto y añade tu clave API de OpenAI y la URL de la base de datos:OPENAI_API_KEY=YOUR_OPENAI_API_KEY
DATABASE_URL=YOUR_DATABASE_URL # ej., "postgresql://user:password@host:port/database_name" - Para la base de datos postgres, asegúrate de tener una base de datos postgres en ejecución y que la URL esté configurada correctamente.
2. Implementación en Flask
- Crea un archivo llamado
app.py
con el siguiente código:
from flask import Flask, request, render_template, session, redirect, url_for
import openai
import os
from dotenv import load_dotenv
from flask_session import Session
import psycopg2 #for postgres
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
app = Flask(__name__)
app.secret_key = os.urandom(24)
app.config["SESSION_TYPE"] = "filesystem" # Or "postgresql", "redis", etc. For production, use a proper database.
#app.config["SESSION_TYPE"] = "postgresql"
#app.config["SESSION_PERMANENT"] = True # Optional: Make sessions permanent
#app.config["SESSION_SQLALCHEMY"] = {'url': DATABASE_URL} #needs Flask-SQLAlchemy
Session(app)
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
print(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
print(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
print(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
print(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Chat route ---
@app.route("/", methods=["GET", "POST"])
def chat():
"""Handles the main chat functionality."""
if "session_id" not in session:
session["session_id"] = os.urandom(16).hex() # Generate unique session ID
session_id = session["session_id"]
history = get_history_from_db(session_id) # Get history from the database
if not history:
history = [{"role": "system", "content": "You are a helpful assistant."}]
session["history"] = history # Initialize session
if request.method == "POST":
user_input = request.form["user_input"]
store_message(session_id, "user", user_input) # Store to database
history.append({"role": "user", "content": user_input})
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=history,
temperature=0.6,
)
assistant_reply = response.choices[0].message.content
store_message(session_id, "assistant", assistant_reply) # Store to database
history.append({"role": "assistant", "content": assistant_reply})
session["history"] = history
session.modified = True # IMPORTANT
return render_template("chat.html", history=history[1:]) # Pass history to template
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
return render_template("chat.html", history=history[1:])
@app.route("/clear", methods=["POST"])
def clear_chat():
session.pop("session_id", None)
session.pop("history", None)
return redirect(url_for("chat"))
if __name__ == "__main__":
app.run(debug=True)
Desglose del código:
from flask import ...
: Importa los módulos necesarios de Flask.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.from flask_session import Session
: Importa la claseSession
deflask_session
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con PostgreSQL.load_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL desde la variable de entorno.app = Flask(__name__)
: Crea una instancia de la aplicación Flask.app.secret_key = os.urandom(24)
: Establece una clave secreta para la aplicación Flask. Esto es esencial para usar sesiones de Flask.app.config["SESSION_TYPE"] = "filesystem|postgresql|redis|mongodb"
: Configura el tipo de almacenamiento de sesión. Para este proyecto, estamos usando una base de datos.Session(app)
: Inicializa la extensión Flask-Session, vinculándola a la aplicación Flask.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL desde la variable de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión con un bloque try...except.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
session_id
.
- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
get_history_from_db(session_id)
:- Recupera el historial de conversación para un
session_id
específico desde la base de datos.
- Recupera el historial de conversación para un
@app.route("/", methods=["GET", "POST"])
: Define la ruta para la página principal de la aplicación ("/"). La funciónchat()
maneja tanto las peticiones GET como POST a esta URL.def chat():
if "session_id" not in session:
: Verifica si existe unsession_id
en la sesión del usuario. Si no, genera un ID único usandoos.urandom(16).hex()
y lo almacena en la sesión.session_id = session["session_id"]
: Obtiene elsession_id
de la sesión.history = get_history_from_db(session_id)
: Recupera el historial de conversación desde la base de datos usando el ID de sesión.if not history
: verifica si el historial está vacío y lo inicializa.if request.method == "POST":
: Maneja las peticiones POST (cuando el usuario envía un mensaje).user_input = request.form["user_input"]
: Obtiene la entrada del usuario desde el formulario.store_message(session_id, "user", user_input)
: Almacena el mensaje del usuario en la base de datos, asociado con el ID de sesión.- El código luego llama a la API de OpenAI para obtener una respuesta, almacena la respuesta en la base de datos y actualiza la sesión.
- Renderiza la plantilla "chat.html", pasando el historial de conversación.
return render_template("chat.html", history=history[1:])
: Maneja las peticiones GET (cuando el usuario carga la página). Renderiza la plantilla "chat.html", pasando el historial de conversación.
@app.route("/clear", methods=["POST"])
: Ruta para limpiar la sesión del chat.def clear_chat():
: limpia elsession_id
y elhistory
de la sesión.if __name__ == "__main__":
: Inicia el servidor de desarrollo Flask si el script se ejecuta directamente.- Crea una carpeta llamada
templates
en el mismo directorio queapp.py
. - Crea un archivo llamado
chat.html
dentro de la carpetatemplates
con el siguiente código:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPT-4o Chat Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* --- General Styles --- */
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
color: #1f2937;
}
.container {
max-width: 800px;
width: 100%;
background-color: #fff;
padding: 30px;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h2 {
font-size: 1.875rem;
font-weight: 600;
margin-bottom: 2rem;
color: #1e293b;
text-align: center;
}
/* --- Message Styles --- */
.message {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
}
.message p {
padding: 1rem;
border-radius: 1rem;
max-width: 80%;
}
.user p {
background-color: #e0f2fe;
color: #1d4ed8;
margin-left: auto;
}
.assistant p {
background-color: #f0fdf4;
color: #15803d;
}
.message strong {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.user strong {
color: #1e40af;
}
.assistant strong {
color: #16a34a;
}
/* --- Form Styles --- */
form {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
resize: none;
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 0.25rem;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
input[type="submit"] {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
input[type="submit"]:hover {
background-color: #2563eb;
}
input[type="submit"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
.container {
padding: 20px;
}
textarea {
height: 100px;
}
}
.clear-history-button{
margin-top: 2rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h2>🧠 GPT-4o Chat Assistant</h2>
{% for msg in history %}
<div class="message {{ msg.role }}">
<p><strong>{{ msg.role.capitalize() }}:</strong> {{ msg.content }}</p>
</div>
{% endfor %}
<form method="post">
<textarea name="user_input" placeholder="Type your message..."></textarea><br>
<input type="submit" value="Send Message">
</form>
<form method="post" action="/clear">
<input type="submit" value="Clear Chat" class = "clear-history-button">
</form>
</div>
</body>
</html>
- Esta plantilla HTML proporciona la estructura y el estilo para la interfaz del chat. Incluye un formulario para la entrada del usuario y muestra el historial de la conversación.
3. Implementación de Streamlit
Crea un archivo llamado streamlit_app.py
con el siguiente código:
import streamlit as st
import openai
import os
from dotenv import load_dotenv
import psycopg2 # Import psycopg2
import datetime
from typing import List, Dict
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
st.set_page_config(page_title="GPT-4o Chat with Memory", page_icon="🧠")
st.title("🧠 GPT-4o Chatbot with Persistent Memory")
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
st.error(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
st.error(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
st.error(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
st.error(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Session Management ---
if "session_id" not in st.session_state:
st.session_state.session_id = os.urandom(16).hex() # Generate unique session ID
session_id = st.session_state.session_id
# --- Initialize chat history ---
if "messages" not in st.session_state:
st.session_state.messages = get_history_from_db(session_id)
if not st.session_state.messages:
st.session_state.messages = [{"role": "system", "content": "You are a helpful assistant."}]
# --- Display chat history ---
for msg in st.session_state.messages[1:]: # Skip system message
speaker = "🧑💻 You" if msg["role"] == "user" else "🤖 Assistant"
with st.chat_message(msg["role"]):
st.markdown(f"**{speaker}:** {msg['content']}")
# --- User input ---
user_input = st.chat_input("Ask something...", key="user_input") # Use key for chat_input
if user_input:
# Store user message
store_message(session_id, "user", user_input)
st.session_state.messages.append({"role": "user", "content": user_input})
# Get assistant reply
with st.chat_message("assistant"):
placeholder = st.empty() # Reserve a placeholder for the response
with st.spinner("Thinking..."):
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=st.session_state.messages,
temperature=0.6,
)
reply = response["choices"][0]["message"]["content"]
store_message(session_id, "assistant", reply) # Store assistant reply
st.session_state.messages.append({"role": "assistant", "content": reply})
placeholder.markdown(f"**🤖 Assistant:** {reply}") # Replace placeholder with full response
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
# Clear the input field after sending the message
st.session_state.user_input = ""
st.rerun()
Desglose del código:
import streamlit as st
: Importa la biblioteca Streamlit.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con la base de datos PostgreSQL.import datetime
: Importa el módulo datetime.from typing import List, Dict
: Importa los tipos de datosload_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL.st.set_page_config(...)
: Configura el título y el icono de la página.st.title(...)
: Establece el título de la aplicación Streamlit.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL de las variables de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión usando un bloque
try...except
, mostrando un mensaje de error usandost.error()
.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación. - Maneja posibles errores durante la creación de la tabla usando un bloque
try...except
.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
session_id
. - Maneja posibles errores durante el almacenamiento del mensaje.
- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
- get_history_from_db(session_id):
- Recupera el historial de conversación para un session_id específico de la base de datos.
- La parte principal del código:
if "session_id" not in st.session_state:
: Verifica si existe un ID de sesión en el estado de Streamlit. Si no existe, genera un ID único usandoos.urandom(16).hex()
.session_id = st.session_state.session_id
: Recupera el ID de sesión.if "messages" not in st.session_state:
:- Recupera el historial de conversación de la base de datos.
- Si no existe historial en la base de datos para la sesión actual, inicializa el historial del chat con el mensaje del sistema.
- El código luego muestra el historial del chat.
user_input = st.chat_input(...)
: Crea un campo de entrada de texto para que el usuario escriba su mensaje.- Cuando el usuario envía un mensaje:
- El mensaje se almacena en la base de datos usando la función
store_message()
. - El mensaje se añade al historial del chat en
st.session_state
. - La aplicación llama a la API de OpenAI para obtener una respuesta.
- La respuesta del asistente se almacena en la base de datos y se añade al historial del chat.
- La respuesta del asistente se muestra en la interfaz del chat.
- El mensaje se almacena en la base de datos usando la función
4. Ejecutar las Aplicaciones
- Flask:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Flask:
python app.py
- Abre un navegador web y ve a
http://localhost:5000
para interactuar con el chatbot.
- Streamlit:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Streamlit:
streamlit run streamlit_app.py
- Se abrirá una nueva pestaña del navegador con el chatbot de Streamlit.
5. Mejoras (Opcional)
- Implementar autenticación de usuarios:
- Agregar sistema seguro de inicio de sesión/registro usando tokens JWT o de sesión
- Crear perfiles de usuario para almacenar preferencias de conversación
- Habilitar conversaciones privadas vinculadas a cuentas de usuario específicas
- Agregar función de botón "Limpiar Chat":
- Implementar limpieza tanto completa como selectiva del historial de chat
- Agregar diálogos de confirmación para prevenir borrados accidentales
- Incluir opción para exportar historial de chat antes de limpiarlo
- Mejorar la interfaz frontend:
- Crear diseños responsivos usando frameworks CSS modernos
- Agregar actualizaciones de mensajes en tiempo real usando WebSocket
- Implementar soporte para markdown y resaltado de código
- Desplegar a producción:
- Configurar pipelines de CI/CD para despliegue automatizado
- Configurar certificados SSL para comunicación segura
- Implementar sistemas de monitoreo y registro
- Agregar capacidades de base de datos vectorial:
- Implementar generación de embeddings usando modelos como BERT o USE
- Configurar búsqueda de similitud vectorial usando Pinecone o Milvus
- Crear funcionalidad de búsqueda semántica para el historial de chat
Este proyecto sirve como una base extensa para desarrollar un chatbot con capacidades de memoria. Esto es lo que obtendrás al completarlo:
Primero, obtendrás experiencia práctica con frameworks web modernos - tanto Flask para desarrollo web tradicional como Streamlit para prototipado rápido de aplicaciones de datos. Aprenderás cómo estos frameworks difieren y cuándo usar cada uno.
Segundo, dominarás el trabajo con la API de OpenAI, entendiendo cómo estructurar prompts, manejar respuestas y gestionar interacciones con la API de manera efectiva. Este conocimiento es crucial para construir cualquier aplicación potenciada por IA.
Tercero, aprenderás principios de integración de bases de datos, incluyendo cómo diseñar esquemas, gestionar conexiones y manejar la persistencia de datos. Esto asegura que tu chatbot pueda mantener conversaciones entre sesiones y escalar efectivamente.
Finalmente, este proyecto te preparará para construir aplicaciones de chatbot más sofisticadas, como aquellas que incorporan características avanzadas como análisis de sentimientos, múltiples modelos de IA o procesamiento en tiempo real. Las habilidades que desarrolles aquí forman los bloques de construcción para aplicaciones de IA más complejas.
Pasos del Proyecto
1. Configuración del Proyecto
- Crea un nuevo directorio para tu proyecto:
mkdir chatbot_project
cd chatbot_project - Crea un entorno virtual:
python -m venv venv
- Activa el entorno virtual:
- En Windows:
venv\Scripts\activate
- En macOS y Linux:
source venv/bin/activate
- Instala las bibliotecas requeridas:
pip install flask openai flask-session python-dotenv streamlit psycopg2
- Crea un archivo
.env
en el directorio de tu proyecto y añade tu clave API de OpenAI y la URL de la base de datos:OPENAI_API_KEY=YOUR_OPENAI_API_KEY
DATABASE_URL=YOUR_DATABASE_URL # ej., "postgresql://user:password@host:port/database_name" - Para la base de datos postgres, asegúrate de tener una base de datos postgres en ejecución y que la URL esté configurada correctamente.
2. Implementación en Flask
- Crea un archivo llamado
app.py
con el siguiente código:
from flask import Flask, request, render_template, session, redirect, url_for
import openai
import os
from dotenv import load_dotenv
from flask_session import Session
import psycopg2 #for postgres
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
app = Flask(__name__)
app.secret_key = os.urandom(24)
app.config["SESSION_TYPE"] = "filesystem" # Or "postgresql", "redis", etc. For production, use a proper database.
#app.config["SESSION_TYPE"] = "postgresql"
#app.config["SESSION_PERMANENT"] = True # Optional: Make sessions permanent
#app.config["SESSION_SQLALCHEMY"] = {'url': DATABASE_URL} #needs Flask-SQLAlchemy
Session(app)
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
print(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
print(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
print(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
print(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Chat route ---
@app.route("/", methods=["GET", "POST"])
def chat():
"""Handles the main chat functionality."""
if "session_id" not in session:
session["session_id"] = os.urandom(16).hex() # Generate unique session ID
session_id = session["session_id"]
history = get_history_from_db(session_id) # Get history from the database
if not history:
history = [{"role": "system", "content": "You are a helpful assistant."}]
session["history"] = history # Initialize session
if request.method == "POST":
user_input = request.form["user_input"]
store_message(session_id, "user", user_input) # Store to database
history.append({"role": "user", "content": user_input})
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=history,
temperature=0.6,
)
assistant_reply = response.choices[0].message.content
store_message(session_id, "assistant", assistant_reply) # Store to database
history.append({"role": "assistant", "content": assistant_reply})
session["history"] = history
session.modified = True # IMPORTANT
return render_template("chat.html", history=history[1:]) # Pass history to template
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
print(error_message)
store_message(session_id, "assistant", error_message)
history.append({"role": "assistant", "content": error_message})
session["history"] = history
session.modified = True
return render_template("chat.html", history=history[1:])
return render_template("chat.html", history=history[1:])
@app.route("/clear", methods=["POST"])
def clear_chat():
session.pop("session_id", None)
session.pop("history", None)
return redirect(url_for("chat"))
if __name__ == "__main__":
app.run(debug=True)
Desglose del código:
from flask import ...
: Importa los módulos necesarios de Flask.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.from flask_session import Session
: Importa la claseSession
deflask_session
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con PostgreSQL.load_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL desde la variable de entorno.app = Flask(__name__)
: Crea una instancia de la aplicación Flask.app.secret_key = os.urandom(24)
: Establece una clave secreta para la aplicación Flask. Esto es esencial para usar sesiones de Flask.app.config["SESSION_TYPE"] = "filesystem|postgresql|redis|mongodb"
: Configura el tipo de almacenamiento de sesión. Para este proyecto, estamos usando una base de datos.Session(app)
: Inicializa la extensión Flask-Session, vinculándola a la aplicación Flask.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL desde la variable de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión con un bloque try...except.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
session_id
.
- Almacena un mensaje (usuario o asistente) en la tabla "messages", incluyendo el
get_history_from_db(session_id)
:- Recupera el historial de conversación para un
session_id
específico desde la base de datos.
- Recupera el historial de conversación para un
@app.route("/", methods=["GET", "POST"])
: Define la ruta para la página principal de la aplicación ("/"). La funciónchat()
maneja tanto las peticiones GET como POST a esta URL.def chat():
if "session_id" not in session:
: Verifica si existe unsession_id
en la sesión del usuario. Si no, genera un ID único usandoos.urandom(16).hex()
y lo almacena en la sesión.session_id = session["session_id"]
: Obtiene elsession_id
de la sesión.history = get_history_from_db(session_id)
: Recupera el historial de conversación desde la base de datos usando el ID de sesión.if not history
: verifica si el historial está vacío y lo inicializa.if request.method == "POST":
: Maneja las peticiones POST (cuando el usuario envía un mensaje).user_input = request.form["user_input"]
: Obtiene la entrada del usuario desde el formulario.store_message(session_id, "user", user_input)
: Almacena el mensaje del usuario en la base de datos, asociado con el ID de sesión.- El código luego llama a la API de OpenAI para obtener una respuesta, almacena la respuesta en la base de datos y actualiza la sesión.
- Renderiza la plantilla "chat.html", pasando el historial de conversación.
return render_template("chat.html", history=history[1:])
: Maneja las peticiones GET (cuando el usuario carga la página). Renderiza la plantilla "chat.html", pasando el historial de conversación.
@app.route("/clear", methods=["POST"])
: Ruta para limpiar la sesión del chat.def clear_chat():
: limpia elsession_id
y elhistory
de la sesión.if __name__ == "__main__":
: Inicia el servidor de desarrollo Flask si el script se ejecuta directamente.- Crea una carpeta llamada
templates
en el mismo directorio queapp.py
. - Crea un archivo llamado
chat.html
dentro de la carpetatemplates
con el siguiente código:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPT-4o Chat Assistant</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* --- General Styles --- */
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
color: #1f2937;
}
.container {
max-width: 800px;
width: 100%;
background-color: #fff;
padding: 30px;
border-radius: 0.75rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h2 {
font-size: 1.875rem;
font-weight: 600;
margin-bottom: 2rem;
color: #1e293b;
text-align: center;
}
/* --- Message Styles --- */
.message {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
}
.message p {
padding: 1rem;
border-radius: 1rem;
max-width: 80%;
}
.user p {
background-color: #e0f2fe;
color: #1d4ed8;
margin-left: auto;
}
.assistant p {
background-color: #f0fdf4;
color: #15803d;
}
.message strong {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.user strong {
color: #1e40af;
}
.assistant strong {
color: #16a34a;
}
/* --- Form Styles --- */
form {
margin-top: 2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
textarea {
width: 100%;
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
resize: none;
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 0.25rem;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.06);
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
input[type="submit"] {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
input[type="submit"]:hover {
background-color: #2563eb;
}
input[type="submit"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
/* --- Responsive Adjustments --- */
@media (max-width: 768px) {
.container {
padding: 20px;
}
textarea {
height: 100px;
}
}
.clear-history-button{
margin-top: 2rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #3b82f6;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h2>🧠 GPT-4o Chat Assistant</h2>
{% for msg in history %}
<div class="message {{ msg.role }}">
<p><strong>{{ msg.role.capitalize() }}:</strong> {{ msg.content }}</p>
</div>
{% endfor %}
<form method="post">
<textarea name="user_input" placeholder="Type your message..."></textarea><br>
<input type="submit" value="Send Message">
</form>
<form method="post" action="/clear">
<input type="submit" value="Clear Chat" class = "clear-history-button">
</form>
</div>
</body>
</html>
- Esta plantilla HTML proporciona la estructura y el estilo para la interfaz del chat. Incluye un formulario para la entrada del usuario y muestra el historial de la conversación.
3. Implementación de Streamlit
Crea un archivo llamado streamlit_app.py
con el siguiente código:
import streamlit as st
import openai
import os
from dotenv import load_dotenv
import psycopg2 # Import psycopg2
import datetime
from typing import List, Dict
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")
DATABASE_URL = os.getenv("DATABASE_URL")
st.set_page_config(page_title="GPT-4o Chat with Memory", page_icon="🧠")
st.title("🧠 GPT-4o Chatbot with Persistent Memory")
# --- Database Connection (PostgreSQL) ---
def get_db_connection():
"""Gets a PostgreSQL connection."""
try:
conn = psycopg2.connect(DATABASE_URL)
return conn
except psycopg2.Error as e:
st.error(f"Database connection error: {e}")
return None
def create_table():
"""Creates the messages table if it doesn't exist."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except psycopg2.Error as e:
st.error(f"Error creating table: {e}")
finally:
conn.close()
def store_message(session_id: str, role: str, content: str):
"""Stores a message in the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO messages (session_id, role, content) VALUES (%s, %s, %s)",
(session_id, role, content),
)
conn.commit()
except psycopg2.Error as e:
st.error(f"Error storing message: {e}")
finally:
conn.close()
def get_history_from_db(session_id: str) -> List[Dict[str, str]]:
"""Retrieves the conversation history for a given session ID from the database."""
conn = get_db_connection()
if conn is not None:
try:
cursor = conn.cursor()
cursor.execute(
"SELECT role, content FROM messages WHERE session_id = %s ORDER BY created_at",
(session_id,),
)
history = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
return history
except psycopg2.Error as e:
st.error(f"Error retrieving history from database: {e}")
return []
finally:
conn.close()
return []
create_table() # Ensure the table exists
# --- Session Management ---
if "session_id" not in st.session_state:
st.session_state.session_id = os.urandom(16).hex() # Generate unique session ID
session_id = st.session_state.session_id
# --- Initialize chat history ---
if "messages" not in st.session_state:
st.session_state.messages = get_history_from_db(session_id)
if not st.session_state.messages:
st.session_state.messages = [{"role": "system", "content": "You are a helpful assistant."}]
# --- Display chat history ---
for msg in st.session_state.messages[1:]: # Skip system message
speaker = "🧑💻 You" if msg["role"] == "user" else "🤖 Assistant"
with st.chat_message(msg["role"]):
st.markdown(f"**{speaker}:** {msg['content']}")
# --- User input ---
user_input = st.chat_input("Ask something...", key="user_input") # Use key for chat_input
if user_input:
# Store user message
store_message(session_id, "user", user_input)
st.session_state.messages.append({"role": "user", "content": user_input})
# Get assistant reply
with st.chat_message("assistant"):
placeholder = st.empty() # Reserve a placeholder for the response
with st.spinner("Thinking..."):
try:
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=st.session_state.messages,
temperature=0.6,
)
reply = response["choices"][0]["message"]["content"]
store_message(session_id, "assistant", reply) # Store assistant reply
st.session_state.messages.append({"role": "assistant", "content": reply})
placeholder.markdown(f"**🤖 Assistant:** {reply}") # Replace placeholder with full response
except openai.error.OpenAIError as e:
error_message = f"OpenAI API Error: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
except Exception as e:
error_message = f"An unexpected error occurred: {e}"
store_message(session_id, "assistant", error_message)
st.session_state.messages.append({"role": "assistant", "content": error_message})
st.error(error_message)
placeholder.markdown(f"**🤖 Assistant:** {error_message}")
# Clear the input field after sending the message
st.session_state.user_input = ""
st.rerun()
Desglose del código:
import streamlit as st
: Importa la biblioteca Streamlit.import openai
: Importa la biblioteca OpenAI.import os
: Importa el móduloos
para interactuar con el sistema operativo, como acceder a variables de entorno.from dotenv import load_dotenv
: Importa la funciónload_dotenv
para cargar variables de entorno desde un archivo.env
.import psycopg2
: Importa la bibliotecapsycopg2
para interactuar con la base de datos PostgreSQL.import datetime
: Importa el módulo datetime.from typing import List, Dict
: Importa los tipos de datosload_dotenv()
: Carga las variables de entorno desde un archivo.env
.openai.api_key = os.getenv("OPENAI_API_KEY")
: Obtiene la clave API de OpenAI desde la variable de entorno y la configura para la biblioteca OpenAI.DATABASE_URL = os.getenv("DATABASE_URL")
: Obtiene la URL de la base de datos PostgreSQL.st.set_page_config(...)
: Configura el título y el icono de la página.st.title(...)
: Establece el título de la aplicación Streamlit.get_db_connection()
:- Establece una conexión a la base de datos PostgreSQL usando la URL de las variables de entorno.
- Devuelve el objeto de conexión.
- Maneja posibles errores de conexión usando un bloque
try...except
, mostrando un mensaje de error usandost.error()
.
create_table()
:- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
session_id
único para cada conversación. - Maneja posibles errores durante la creación de la tabla usando un bloque
try...except
.
- Crea la tabla "messages" en la base de datos PostgreSQL si no existe. La tabla almacena el historial de conversación, incluyendo un
store_message(session_id, role, content)
:- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
session_id
. - Maneja posibles errores durante el almacenamiento del mensaje.
- Almacena un mensaje (del usuario o asistente) en la tabla "messages", junto con el
- get_history_from_db(session_id):
- Recupera el historial de conversación para un session_id específico de la base de datos.
- La parte principal del código:
if "session_id" not in st.session_state:
: Verifica si existe un ID de sesión en el estado de Streamlit. Si no existe, genera un ID único usandoos.urandom(16).hex()
.session_id = st.session_state.session_id
: Recupera el ID de sesión.if "messages" not in st.session_state:
:- Recupera el historial de conversación de la base de datos.
- Si no existe historial en la base de datos para la sesión actual, inicializa el historial del chat con el mensaje del sistema.
- El código luego muestra el historial del chat.
user_input = st.chat_input(...)
: Crea un campo de entrada de texto para que el usuario escriba su mensaje.- Cuando el usuario envía un mensaje:
- El mensaje se almacena en la base de datos usando la función
store_message()
. - El mensaje se añade al historial del chat en
st.session_state
. - La aplicación llama a la API de OpenAI para obtener una respuesta.
- La respuesta del asistente se almacena en la base de datos y se añade al historial del chat.
- La respuesta del asistente se muestra en la interfaz del chat.
- El mensaje se almacena en la base de datos usando la función
4. Ejecutar las Aplicaciones
- Flask:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Flask:
python app.py
- Abre un navegador web y ve a
http://localhost:5000
para interactuar con el chatbot.
- Streamlit:
- Abre una terminal, navega al directorio del proyecto y asegúrate de que tu entorno virtual esté activado.
- Ejecuta la aplicación Streamlit:
streamlit run streamlit_app.py
- Se abrirá una nueva pestaña del navegador con el chatbot de Streamlit.
5. Mejoras (Opcional)
- Implementar autenticación de usuarios:
- Agregar sistema seguro de inicio de sesión/registro usando tokens JWT o de sesión
- Crear perfiles de usuario para almacenar preferencias de conversación
- Habilitar conversaciones privadas vinculadas a cuentas de usuario específicas
- Agregar función de botón "Limpiar Chat":
- Implementar limpieza tanto completa como selectiva del historial de chat
- Agregar diálogos de confirmación para prevenir borrados accidentales
- Incluir opción para exportar historial de chat antes de limpiarlo
- Mejorar la interfaz frontend:
- Crear diseños responsivos usando frameworks CSS modernos
- Agregar actualizaciones de mensajes en tiempo real usando WebSocket
- Implementar soporte para markdown y resaltado de código
- Desplegar a producción:
- Configurar pipelines de CI/CD para despliegue automatizado
- Configurar certificados SSL para comunicación segura
- Implementar sistemas de monitoreo y registro
- Agregar capacidades de base de datos vectorial:
- Implementar generación de embeddings usando modelos como BERT o USE
- Configurar búsqueda de similitud vectorial usando Pinecone o Milvus
- Crear funcionalidad de búsqueda semántica para el historial de chat
Este proyecto sirve como una base extensa para desarrollar un chatbot con capacidades de memoria. Esto es lo que obtendrás al completarlo:
Primero, obtendrás experiencia práctica con frameworks web modernos - tanto Flask para desarrollo web tradicional como Streamlit para prototipado rápido de aplicaciones de datos. Aprenderás cómo estos frameworks difieren y cuándo usar cada uno.
Segundo, dominarás el trabajo con la API de OpenAI, entendiendo cómo estructurar prompts, manejar respuestas y gestionar interacciones con la API de manera efectiva. Este conocimiento es crucial para construir cualquier aplicación potenciada por IA.
Tercero, aprenderás principios de integración de bases de datos, incluyendo cómo diseñar esquemas, gestionar conexiones y manejar la persistencia de datos. Esto asegura que tu chatbot pueda mantener conversaciones entre sesiones y escalar efectivamente.
Finalmente, este proyecto te preparará para construir aplicaciones de chatbot más sofisticadas, como aquellas que incorporan características avanzadas como análisis de sentimientos, múltiples modelos de IA o procesamiento en tiempo real. Las habilidades que desarrolles aquí forman los bloques de construcción para aplicaciones de IA más complejas.