BookBot / app.py
Migue1804's picture
Update app.py
c0d250c verified
import streamlit as st
import os
import requests
import base64
from typing import List, Dict
# Configuraci贸n de la aplicaci贸n
st.set_page_config(
page_title="Biblioteca de Res煤menes de Libros",
layout="wide",
initial_sidebar_state="expanded"
)
# Display the image above the title
st.image('Bookbot.png', use_container_width=True)
# Funci贸n para cargar imagen y codificarla en base64
def get_image_base64(image_path):
"""Carga una imagen y la convierte a base64 para mostrarla"""
try:
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode()
except Exception:
return None
# CSS personalizado para un dise帽o moderno y minimalista
# Modifica la funci贸n apply_custom_css() para forzar el modo claro
# Funci贸n modificada para forzar el modo claro
def apply_custom_css():
st.markdown("""
<style>
/* Forzar tema claro - esta es la parte m谩s importante */
.st-emotion-cache-ue6h4q {
color-scheme: light !important;
}
body {
color-scheme: light !important;
}
/* Estilos para forzar colores de modo claro */
html, body, [class*="css"] {
color: #31333F !important;
}
[data-testid="stAppViewContainer"],
[data-testid="stHeader"],
[data-testid="stToolbar"] {
background-color: #F8F9FA !important;
}
/* Contenedor principal - ajustes para m贸vil */
[data-testid="stAppViewContainer"] > .main {
padding: 1rem !important;
}
@media (max-width: 768px) {
[data-testid="stAppViewContainer"] > .main {
padding: 0.5rem !important;
}
}
/* T铆tulo principal */
.main h1 {
color: #1E3A8A !important;
font-size: 2rem;
font-weight: 600;
margin-bottom: 1.5rem;
}
@media (max-width: 768px) {
.main h1 {
font-size: 1.5rem !important;
margin-bottom: 1rem !important;
}
}
/* Subt铆tulos */
.main h2, .main h3 {
color: #1E3A8A !important;
font-weight: 500;
}
/* Contenedor de chat */
.chat-container {
background-color: white !important;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
/* Mensajes del usuario */
.user-message {
background-color: #E7F3FF !important;
border-radius: 15px 15px 0 15px;
padding: 10px 15px;
margin: 10px 0;
max-width: 85%;
align-self: flex-end;
margin-left: auto;
color: #31333F !important;
}
/* Mensajes del asistente */
.assistant-message {
background-color: #F0F0F0 !important;
border-radius: 15px 15px 15px 0;
padding: 10px 15px;
margin: 10px 0;
max-width: 85%;
color: #31333F !important;
}
/* Ajustes para m贸vil de los mensajes */
@media (max-width: 768px) {
.user-message, .assistant-message {
max-width: 95% !important;
padding: 8px 12px !important;
}
}
/* Campo de entrada de texto */
.stTextInput input, .stTextArea textarea {
border-radius: 20px;
border: 1px solid #E0E0E0 !important;
padding: 10px 15px;
font-size: 1rem;
background-color: white !important;
color: #31333F !important;
}
/* Selectbox y dropdown */
.stSelectbox div[data-baseweb="select"] div[role="button"],
.stMultiselect div[data-baseweb="select"] div[role="button"] {
background-color: white !important;
color: #31333F !important;
border: 1px solid #E0E0E0 !important;
}
div[role="listbox"] {
background-color: white !important;
color: #31333F !important;
}
div[role="option"] {
background-color: white !important;
color: #31333F !important;
}
/* Separador */
hr {
margin: 1rem 0 !important;
border: 0;
border-top: 1px solid #E0E0E0 !important;
}
/* Bot贸n de env铆o */
.stButton button {
background-color: #1E3A8A !important;
color: white !important;
border-radius: 20px;
padding: 5px 20px;
font-weight: 500;
border: none !important;
transition: all 0.3s;
width: 100%;
}
/* Espec铆ficamente para el texto del bot贸n */
.stButton button p, .stButton button span, .stButton button div {
color: white !important;
}
.stButton button:hover {
background-color: #2563EB !important;
box-shadow: 0 4px 8px rgba(37, 99, 235, 0.2);
}
/* Mejoras para visualizaci贸n m贸vil */
@media (max-width: 768px) {
.stButton button {
padding: 5px 15px !important;
font-size: 0.9rem !important;
}
/* Ajustar columnas en m贸vil */
[data-testid="column"] {
width: 100% !important;
min-width: 100% !important;
}
/* Ajustar tama帽o de texto en m贸vil */
p, div, span, label {
font-size: 0.9rem !important;
}
}
/* Captions y textos peque帽os */
.stMarkdown caption, .stMarkdown small {
color: #4B5563 !important;
}
/* Asegurar que todos los textos sean oscuros */
p, div, span, label, li, a {
color: #31333F !important;
}
/* Espec铆fico para los mensajes de chat */
div.user-message strong, div.assistant-message strong {
color: inherit !important;
}
/* Deshabilitar el interruptor de tema oscuro */
button[kind="headerNoPadding"] {
display: none !important;
}
</style>
""", unsafe_allow_html=True)
# Funci贸n para cargar res煤menes de libros desde archivos de texto
def cargar_resumenes_libros(carpeta_libros: str) -> List[Dict[str, str]]:
"""
Carga res煤menes de libros desde archivos de texto en una carpeta espec铆fica.
Args:
carpeta_libros (str): Ruta de la carpeta que contiene los res煤menes de libros.
Returns:
List[Dict[str, str]]: Lista de diccionarios con informaci贸n de los libros.
"""
resumenes = []
try:
# Verificar si la carpeta existe
if not os.path.exists(carpeta_libros):
st.error(f"La carpeta {carpeta_libros} no existe.")
return resumenes
# Listar archivos de texto en la carpeta
archivos = [f for f in os.listdir(carpeta_libros) if f.endswith('.txt')]
if not archivos:
st.warning("No se encontraron archivos de res煤menes en la carpeta.")
return resumenes
# Cargar cada archivo de resumen
for archivo in archivos:
ruta_archivo = os.path.join(carpeta_libros, archivo)
try:
with open(ruta_archivo, 'r', encoding='utf-8') as f:
# Extraer t铆tulo del nombre del archivo (sin extensi贸n)
titulo = os.path.splitext(archivo)[0]
# Leer contenido del archivo
contenido = f.read().strip()
# Extraer un breve resumen (primeros 638 caracteres)
breve_resumen = contenido[:638] + "..." if len(contenido) > 638 else contenido
# Agregar resumen a la lista
resumenes.append({
"titulo": titulo,
"resumen": contenido,
"breve_resumen": breve_resumen
})
except Exception as e:
st.error(f"Error al leer el archivo {archivo}: {str(e)}")
except Exception as e:
st.error(f"Error al cargar los res煤menes: {str(e)}")
return resumenes
# Configuraci贸n de la API de Hugging Face
API_URL = "https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
# Inicializar la sesi贸n state para el historial de chat si no existe
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
# Funci贸n para obtener la API key de Hugging Face desde secrets
@st.cache_resource
def get_huggingface_api_key():
"""Obtener la API key de Hugging Face desde secrets"""
try:
# Intenta acceder al token usando la clave HF_TOKEN
return st.secrets["HF_TOKEN"]
except KeyError:
# Si no est谩 disponible con esa clave, intenta el formato anterior
try:
return st.secrets["huggingface"]["api_key"]
except:
return None
# Funci贸n para enviar solicitudes a la API de Hugging Face
def query_huggingface(payload):
"""Env铆a una solicitud a la API de Hugging Face y retorna la respuesta"""
api_key = get_huggingface_api_key()
if not api_key:
st.error("No se proporcion贸 una API key de Hugging Face.")
return None
headers = {"Authorization": f"Bearer {api_key}"}
try:
# A帽adido verify=False para ignorar la verificaci贸n SSL
response = requests.post(API_URL, headers=headers, json=payload, timeout=60, verify=False)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
st.error("La solicitud a la API de Hugging Face ha excedido el tiempo de espera.")
return None
except requests.exceptions.HTTPError as e:
st.error(f"Error HTTP: {e.response.status_code} - {e.response.text}")
return None
except Exception as e:
st.error(f"Error al comunicarse con la API de Hugging Face: {str(e)}")
return None
# Interfaz principal de la aplicaci贸n
def main():
# Aplicar CSS personalizado
apply_custom_css()
# Cargar res煤menes de libros
carpeta_libros = "libros" # Aseg煤rate de que esta carpeta exista en tu proyecto
resumenes = cargar_resumenes_libros(carpeta_libros)
# Verificar si se cargaron res煤menes
if not resumenes:
st.warning("No se pudieron cargar los res煤menes de libros.")
return
# T铆tulo principal
st.header("馃摎 BookBot te gu铆a en tu camino profesional y personal")
# Secci贸n de selecci贸n - EN LA PARTE CENTRAL
#st.subheader("Selecciona un Libro")
# Obtener t铆tulos de libros
titulos_libros = [libro["titulo"] for libro in resumenes]
# Usar columnas para organizar la selecci贸n y mostrar informaci贸n sobre el modelo
col_select, col_info = st.columns([1, 2])
with col_select:
libro_seleccionado = st.selectbox("Elige un libro para explorar", titulos_libros)
with col_info:
st.caption("Asistente potenciado por DeepSeek-R1-Distill-Qwen-32B")
# Encontrar la informaci贸n del libro seleccionado
libro_actual = next(
(libro for libro in resumenes if libro["titulo"] == libro_seleccionado),
None
)
# Si se encontr贸 un libro, mostrar su informaci贸n
if libro_actual:
# Usar columnas para la imagen y resumen breve
col1, col2 = st.columns([1, 2])
def imagen_a_base64(imagen_path):
"""Convierte una imagen local a formato Base64 para ser usada en HTML."""
with open(imagen_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode()
with col1:
imagen_path = f"imagenes/{libro_seleccionado}.png"
if os.path.exists(imagen_path):
imagen_base64 = imagen_a_base64(imagen_path) # Convertir imagen a Base64
st.markdown(
f"""
<style>
.fixed-size-img {{
width: 300px;
height: 400px;
object-fit: contain;
display: block;
margin: auto;
}}
</style>
<img src="data:image/png;base64,{imagen_base64}" class="fixed-size-img">
""",
unsafe_allow_html=True
)
else:
st.error("Imagen no encontrada")
with col2:
# Mostrar breve resumen
st.subheader("Resumen")
st.markdown(f"{libro_actual['breve_resumen'].replace(chr(10), '<br>')}", unsafe_allow_html=True)
# Separador
st.markdown("---")
# Secci贸n del chatbot
st.subheader("馃挰 Consulta a Bookbot sobre este libro")
# Mostrar el historial de chat
for message in st.session_state.chat_history:
if message["role"] == "user":
st.markdown(f"<div class='user-message'><strong>馃榾 T煤:</strong> {message['content']}</div>", unsafe_allow_html=True)
else:
st.markdown(f"<div class='assistant-message'><strong>馃 Asistente:</strong> {message['content']}</div>", unsafe_allow_html=True)
# Crear el widget de entrada de usuario
user_input = st.text_input("", key="user_query", placeholder="Escribe tu pregunta sobre el libro...")
# Bot贸n en el centro
col_button_l, col_button_c, col_button_r = st.columns([1, 1, 1])
with col_button_c:
send_button = st.button("Enviar")
# Procesar la entrada del usuario
if user_input and send_button:
# Preparar el contexto
context = f"""
CONTEXTO DEL LIBRO:
T铆tulo: {libro_seleccionado}
Resumen: {libro_actual['resumen']}
PREGUNTA DEL USUARIO:
{user_input}
Responde de manera concisa y precisa, bas谩ndote en el resumen y contexto del libro.
"""
# Mostrar un mensaje de espera personalizado con icono
with st.spinner("馃 Pensando..."):
# Llamar a la API de Hugging Face
payload = {
"inputs": context,
"parameters": {
"max_new_tokens": 1000,
"temperature": 0.7,
"top_p": 0.9,
"do_sample": True
}
}
response = query_huggingface(payload)
if response:
# Extraer la respuesta del modelo
if isinstance(response, list) and len(response) > 0:
bot_response = response[0].get("generated_text", "")
# Intentar extraer solo la respuesta del asistente
try:
bot_response = bot_response.split("Responde de manera concisa y precisa")[-1]
if "PREGUNTA DEL USUARIO:" in bot_response:
bot_response = bot_response.split("PREGUNTA DEL USUARIO:")[-1]
bot_response = bot_response.strip()
except:
# Si falla la extracci贸n, usar la respuesta completa
pass
else:
bot_response = str(response)
# Agregar al historial de chat
st.session_state.chat_history.append({"role": "user", "content": user_input})
st.session_state.chat_history.append({"role": "assistant", "content": bot_response})
# Recargar la p谩gina para mostrar la nueva respuesta
st.rerun()
# Ejecutar la aplicaci贸n principal
if __name__ == "__main__":
main()