import streamlit as st import pandas as pd import plotly.express as px import random import time import joblib import os import statsmodels from dotenv import load_dotenv import os from groq import Groq import html from pydub import AudioSegment import tempfile from io import BytesIO import tempfile #from langchain.agents.agent_toolkits import create_csv_agent #from langchain_groq import ChatGroq # =========================== # Función para generar datos ficticios # =========================== def generar_datos(): meses = [ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" ] paises = ["México", "Colombia", "Argentina", "Chile", "Perú"] data = [ {"mes": mes, "pais": pais, "Total": random.randint(100, 1000)} for mes in meses for pais in paises ] return pd.DataFrame(data), meses, paises # =========================== # Función para el dashboard principal # =========================== def mostrar_dashboard(): # Cargar variables desde el archivo .env load_dotenv() # Acceder a la clave groq_key = os.getenv("GROQ_API_KEY") client = Groq(api_key=groq_key) dfDatos, meses, paises = generar_datos() # Opciones del selectbox lista_opciones = ['5 años', '3 años', '1 año', '5 meses'] # Mostrar barra lateral mostrar_sidebar(client) # Título principal st.header(':bar_chart: Dashboard Sales') # Mostrar métricas #mostrar_metricas() # Mostrar gráficos mostrar_graficos(lista_opciones) # =========================== # Configuración inicial de la página # =========================== #def configurar_pagina(): #st.set_page_config( # page_title="Dashboard Sales", # page_icon=":smile:", # layout="wide", # initial_sidebar_state="expanded" #) # =========================== # Función para la barra lateral # =========================== def mostrar_sidebar(client): sidebar_logo = r"paginas\images\Logo general.png" main_body_logo = r"paginas\images\Logo.png" sidebar_logo_dashboard = r"paginas\images\Logo dashboard.png" st.logo(sidebar_logo, size="large", icon_image=main_body_logo) st.sidebar.image(sidebar_logo_dashboard) st.sidebar.title('🧠 GenAI Forecast') loadCSV() archivo_csv = "df_articles.csv" chatBotProtech(client) downloadCSV(archivo_csv) # Mostrar la tabla solo si se ha subido un archivo válido ''' if 'archivo_subido' in st.session_state and st.session_state.archivo_subido: # Verificamos si el archivo ha sido subido y es válido st.sidebar.markdown("Vista previa del archivo CSV:") # Usar st.dataframe() para que ocupe todo el ancho disponible st.sidebar.dataframe(st.session_state.df_subido, use_container_width=True) # Mostrar la tabla con el archivo subido ''' if st.sidebar.button("Cerrar Sesión"): cerrar_sesion() # =========================== # Función para métricas principales # =========================== ''' def mostrar_metricas(): c1, c2, c3, c4, c5 = st.columns(5) valores = [89, 78, 67, 56, 45] for i, col in enumerate([c1, c2, c3, c4, c5]): valor1 = valores[i] valor2 = valor1 - 10 # Simulación de variación variacion = valor1 - valor2 unidad = "unidades" if i < 4 else "%" col.metric(f"Productos vendidos", f'{valor1:,.0f} {unidad}', f'{variacion:,.0f}') ''' # Función para obtener los meses relevantes def obtener_meses_relevantes(df): # Extraemos los años y meses de la columna 'Date' df['Year'] = pd.to_datetime(df['orddt']).dt.year df['Month'] = pd.to_datetime(df['orddt']).dt.month # Encontramos el primer y último año en el dataset primer_ano = df['Year'].min() ultimo_ano = df['Year'].max() meses_relevantes = [] nombres_meses_relevantes = [] # Recorrer todos los años dentro del rango for ano in range(primer_ano, ultimo_ano + 1): for mes in [1, 4, 7, 10]: # Meses relevantes: enero (1), abril (4), julio (7), octubre (10) if mes in df[df['Year'] == ano]['Month'].values: # Obtener el nombre del mes nombre_mes = pd.to_datetime(f"{ano}-{mes}-01").strftime('%B') # Mes en formato textual (Enero, Abril, etc.) meses_relevantes.append(f"{nombre_mes}-{ano}") nombres_meses_relevantes.append(f"{nombre_mes}-{ano}") return meses_relevantes, nombres_meses_relevantes # =========================== # Función para gráficos # =========================== def mostrar_graficos(lista_opciones): """ c1, c2 = st.columns([20, 80]) with c1: filtroAnios = st.selectbox('Año', options=lista_opciones) with c2: st.markdown("### :pushpin: Ventas actuales") # Si hay un archivo válido subido if "archivo_subido" in st.session_state and st.session_state.archivo_subido: # Cargar datos del archivo subido df = st.session_state.df_subido.copy() df['Date'] = pd.to_datetime(df['Date']) df['Mes-Año'] = df['Date'].dt.strftime('%B-%Y') # Formato deseado df = df.sort_values('Date') # Ordenar por fecha # Obtener los meses relevantes del dataset meses_relevantes, nombres_meses_relevantes = obtener_meses_relevantes(df) # Crear la gráfica fig = px.line( df, x='Mes-Año', y='Sale', title='Ventas mensuales (Archivo Subido)', labels={'Mes-Año': 'Mes-Año', 'Sale': 'Ventas'}, ) else: # Datos por defecto df = pd.DataFrame({ "Mes-Año": ["Enero-2024", "Febrero-2024", "Marzo-2024", "Abril-2024", "Mayo-2024", "Junio-2024", "Julio-2024", "Agosto-2024", "Septiembre-2024", "Octubre-2024", "Noviembre-2024", "Diciembre-2024"], "Sale": [100, 150, 120, 200, 250, 220, 280, 300, 350, 400, 450, 500], }) # Obtener los meses relevantes meses_relevantes = ["Enero-2024", "Abril-2024", "Julio-2024", "Octubre-2024"] nombres_meses_relevantes = ["Enero-2024", "Abril-2024", "Julio-2024", "Octubre-2024"] # Crear la gráfica fig = px.line( df, x='Mes-Año', y='Sale', title='Ventas mensuales (Datos por defecto)', labels={'Mes-Año': 'Mes-Año', 'Sale': 'Ventas'}, line_shape='linear' # Línea continua ) fig.update_xaxes(tickangle=-45) # Ajustar ángulo de etiquetas en X # Mejorar el diseño de la gráfica fig = mejorar_diseno_grafica(fig, meses_relevantes, nombres_meses_relevantes) st.plotly_chart(fig, use_container_width=True) # Evita que ocupe todo el ancho # Gráfica 2: Ventas actuales y proyectadas st.markdown("### :chart_with_upwards_trend: Pronóstico") mostrar_ventas_proyectadas(filtroAnios) """ if "archivo_subido" not in st.session_state or not st.session_state.archivo_subido: st.warning("Por favor, sube un archivo CSV válido para visualizar los gráficos.") return df = st.session_state.df_subido.copy() # Fila 1: 3 gráficas col1, col2, col3 = st.columns(3) with col1: fig1 = px.histogram(df, x='sales', title='Distribución de Ventas') st.plotly_chart(fig1, use_container_width=True) with col2: fig2 = px.box(df, x='segmt', y='sales', title='Ventas por Segmento') st.plotly_chart(fig2, use_container_width=True) with col3: print("") # Fila 2: 2 gráficas col4, col5 = st.columns(2) with col4: fig4 = px.pie(df, names='categ', values='sales', title='Ventas por Categoría') st.plotly_chart(fig4, use_container_width=True) with col5: # Agrupar por nombre de producto y sumar las ventas top_productos = ( df.groupby('prdna')['sales'] .sum() .sort_values(ascending=False) .head(10) .reset_index() ) # Crear gráfica de barras horizontales fig5 = px.bar( top_productos, x='sales', y='prdna', orientation='h', title='Top 10 productos más vendidos', labels={'sales': 'Ventas', 'prdna': 'Producto'}, color='sales', color_continuous_scale='Blues' ) fig5.update_layout(yaxis={'categoryorder': 'total ascending'}) st.plotly_chart(fig5, use_container_width=True) col6, col7 = st.columns(2) with col6: # Fuera del sistema de columnas tabla = df.pivot_table(index='state', columns='subct', values='sales', aggfunc='sum').fillna(0) if not tabla.empty: tabla = tabla.astype(float) fig6 = px.imshow( tabla.values, labels=dict(x="Categoría", y="Estado", color="Ventas"), x=tabla.columns, y=tabla.index, text_auto=True, title="Mapa de Calor: Ventas por Estado y Categoría" ) # Ajuste del tamaño de la figura # fig6.update_layout(height=600, width=1000) # Puedes ajustar según tu pantalla st.plotly_chart(fig6, use_container_width=True) else: st.warning("No hay datos suficientes para mostrar el mapa de calor.") with col7: fig7 = px.bar(df.groupby('state')['sales'].sum().reset_index(), x='state', y='sales', title='Ventas por Estado') st.plotly_chart(fig7, use_container_width=True) # ------------------------------- # CARGA DE CSV Y GUARDADO EN SESIÓN # ------------------------------- def loadCSV(): columnas_requeridas = [ 'rowid','ordid','orddt', 'shpdt','segmt','state', 'cono','prodid','categ', 'subct','prdna','sales' ] with st.sidebar.expander("📁 Subir archivo"): uploaded_file = st.file_uploader("Sube un archivo CSV:", type=["csv"], key="upload_csv") if uploaded_file is not None: # Reseteamos el estado de 'descargado' cuando se sube un archivo st.session_state.descargado = False st.session_state.archivo_subido = False # Reinicia el estado try: # Leer el archivo subido df = pd.read_csv(uploaded_file) # Verificar que las columnas estén presentes y en el orden correcto if list(df.columns) == columnas_requeridas: st.session_state.df_subido = df st.session_state.archivo_subido = True aviso = st.sidebar.success("✅ Archivo subido correctamente.") time.sleep(3) aviso.empty() else: st.session_state.archivo_subido = False aviso = st.sidebar.error(f"El archivo no tiene las columnas requeridas: {columnas_requeridas}.") time.sleep(3) aviso.empty() except Exception as e: aviso = st.sidebar.error(f"Error al procesar el archivo: {str(e)}") time.sleep(3) aviso.empty() # =========================== # Función para descargar archivo CSV # =========================== def downloadCSV(archivo_csv): # Verificamos si el archivo ya ha sido descargado if 'descargado' not in st.session_state: st.session_state.descargado = False if not st.session_state.descargado: # Usamos st.spinner para mostrar un estado de descarga inicial #with st.spinner("Preparando archivo para descarga..."): # time.sleep(2) # Simulación de preparación del archivo # Botón de descarga descarga = st.sidebar.download_button( label="Descargar archivo CSV", data=open(archivo_csv, "rb"), file_name="ventas.csv", mime="text/csv" ) if descarga: # Marcamos el archivo como descargado st.session_state.descargado = True aviso = st.sidebar.success("¡Descarga completada!") # Hacer que el mensaje desaparezca después de 2 segundos time.sleep(3) aviso.empty() else: aviso = st.sidebar.success("¡Ya has descargado el archivo!") time.sleep(3) aviso.empty() # ------------------------------- # CREACIÓN DE AGENTE CSV # ------------------------------- ''' def createCSVAgent(client, df): temp_csv = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") df.to_csv(temp_csv.name, index=False) agent = create_csv_agent( client, temp_csv.name, verbose=False, handle_parsing_errors=True ) return agent ''' ''' def callCSVAgent(client, prompt): if "df_csv" not in st.session_state: return "No hay CSV cargado aún." df = st.session_state.df_csv agente = createCSVAgent(client, df) try: respuesta = agente.run(prompt) except Exception as e: respuesta = f"Error al procesar la pregunta: {e}" return respuesta ''' # ------------------------------- # FUNCIÓN PARA DETECTAR REFERENCIA AL CSV # ------------------------------- def detectedReferenceToCSV(prompt: str) -> bool: palabras_clave = ["csv", "archivo", "contenido cargado", "file", "dataset"] prompt_lower = prompt.lower() return any(palabra in prompt_lower for palabra in palabras_clave) # =========================== # Función para interactuar con el bot # =========================== def chatBotProtech(client): with st.sidebar.expander("📁 Chatbot"): # Inicializar estados if "chat_history" not in st.session_state: st.session_state.chat_history = [] if "audio_data" not in st.session_state: st.session_state.audio_data = None if "transcripcion" not in st.session_state: st.session_state.transcripcion = "" if "mostrar_grabador" not in st.session_state: st.session_state.mostrar_grabador = True # Contenedor para mensajes messages = st.container(height=400) # CSS: estilo tipo Messenger st.markdown(""" """, unsafe_allow_html=True) # Mostrar historial de mensajes with messages: st.header("🤖 ChatBot Protech") for message in st.session_state.chat_history: role = message["role"] content = html.escape(message["content"]) # Escapar contenido HTML bubble_class = "user" if role == "user" else "assistant" icon = "👤" if role == "user" else "🤖" # Mostrar el mensaje en una sola burbuja con ícono en el mismo bloque st.markdown(f"""
{icon}
{content}
""", unsafe_allow_html=True) # --- Manejar transcripción como mensaje automático --- if st.session_state.transcripcion: prompt = st.session_state.transcripcion st.session_state.transcripcion = "" st.session_state.chat_history.append({"role": "user", "content": prompt}) with messages: st.markdown(f"""
{html.escape(prompt)}
👤
""", unsafe_allow_html=True) with messages: with st.spinner("Pensando..."): completion = callDeepseek(client, prompt) response = "" response_placeholder = st.empty() for chunk in completion: content = chunk.choices[0].delta.content or "" response += content response_placeholder.markdown(f"""
🤖
{response}
""", unsafe_allow_html=True) st.session_state.chat_history.append({"role": "assistant", "content": response}) # Captura del input tipo chat if prompt := st.chat_input("Escribe algo..."): st.session_state.chat_history.append({"role": "user", "content": prompt}) # Mostrar mensaje del usuario escapado with messages: st.markdown(f"""
{prompt}
👤
""", unsafe_allow_html=True) # Mostrar respuesta del asistente with messages: with st.spinner("Pensando..."): completion = callDeepseek(client, prompt) response = "" response_placeholder = st.empty() for chunk in completion: content = chunk.choices[0].delta.content or "" response += content response_placeholder.markdown(f"""
🤖
{response}
""", unsafe_allow_html=True) st.session_state.chat_history.append({"role": "assistant", "content": response}) # Grabación de audio (solo si está habilitada) if st.session_state.mostrar_grabador and st.session_state.audio_data is None: audio_data = st.audio_input("Graba tu voz aquí 🎤") if audio_data: st.session_state.audio_data = audio_data st.session_state.mostrar_grabador = False # Ocultar input después de grabar st.rerun() # Forzar recarga para ocultar input y evitar que reaparezca el audio cargado # Mostrar controles solo si hay audio cargado if st.session_state.audio_data: st.audio(st.session_state.audio_data, format="audio/wav") col1, col2 = st.columns(2) with col1: if st.button("✅ Aceptar grabación"): with st.spinner("Convirtiendo y transcribiendo..."): m4a_path = converter_bytes_m4a(st.session_state.audio_data) with open(m4a_path, "rb") as f: texto = callWhisper(client, m4a_path, f) os.remove(m4a_path) st.session_state.transcripcion = texto st.session_state.audio_data = None st.session_state.mostrar_grabador = True st.rerun() with col2: if st.button("❌ Descartar grabación"): st.session_state.audio_data = None st.session_state.transcripcion = "" st.session_state.mostrar_grabador = True st.rerun() # Mostrar transcripción como texto previo al input si existe ''' if st.session_state.transcripcion: st.info(f"📝 Transcripción: {st.session_state.transcripcion}") # Prellenar el input simuladamente prompt = st.session_state.transcripcion st.session_state.transcripcion = "" # Limpiar st.rerun() # Simular que se envió el mensaje ''' #def speechRecognition(): #audio_value = st.audio_input("Record a voice message") def callDeepseek(client, prompt): completion = client.chat.completions.create( #model="meta-llama/llama-4-scout-17b-16e-instruct", model = "deepseek-r1-distill-llama-70b", messages=[{"role": "user", "content": prompt}], temperature=0.6, max_tokens=1024, top_p=1, stream=True, ) return completion def callWhisper(client, filename_audio,file): transcription = client.audio.transcriptions.create( file=(filename_audio, file.read()), model="whisper-large-v3", response_format="verbose_json", ) return transcription.text def converter_bytes_m4a(audio_bytes: BytesIO) -> str: """ Convierte un audio en bytes (WAV, etc.) a un archivo M4A temporal. Retorna la ruta del archivo .m4a temporal. """ # Asegurarse de que el cursor del stream esté al inicio audio_bytes.seek(0) # Leer el audio desde BytesIO usando pydub audio = AudioSegment.from_file(audio_bytes) # Crear archivo temporal para guardar como .m4a temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".m4a") m4a_path = temp_file.name temp_file.close() # Cerramos para que pydub pueda escribirlo # Exportar a M4A usando formato compatible con ffmpeg audio.export(m4a_path, format="ipod") # 'ipod' genera .m4a return m4a_path # =========================== # Función para cargar el modelo SARIMA # =========================== """def cargar_modelo_sarima(ruta_modelo): # Cargar el modelo utilizando joblib modelo = joblib.load(ruta_modelo) return modelo""" # =========================== # Función para obtener el número de periodos basado en el filtro # =========================== def obtener_periodos(filtro): opciones_periodos = { '5 años': 60, '3 años': 36, '1 año': 12, '5 meses': 5 } return opciones_periodos.get(filtro, 12) # =========================== # Función para mostrar ventas actuales y proyectadas # =========================== """ def mostrar_ventas_proyectadas(filtro): ruta_modelo = os.path.join("arima_sales_model.pkl") modelo_sarima = cargar_modelo_sarima(ruta_modelo) if "archivo_subido" in st.session_state and st.session_state.archivo_subido: # Cargar datos del archivo subido df = st.session_state.df_subido.copy() df['Date'] = pd.to_datetime(df['Date']) df = df.sort_values('Date') # Generar predicciones periodos = obtener_periodos(filtro) predicciones = generar_predicciones(modelo_sarima, df, periodos) # Redondear y formatear las ventas df['Sale'] = df['Sale'].round(2).apply(lambda x: f"{x:,.2f}") # Formato con 2 decimales y comas predicciones = [round(val, 2) for val in predicciones] # Redondear predicciones # Preparar datos para graficar df['Tipo'] = 'Ventas Actuales' df_pred = pd.DataFrame({ 'Date': pd.date_range(df['Date'].max(), periods=periodos + 1, freq='ME')[1:], 'Sale': predicciones, 'Tipo': 'Ventas Pronosticadas' }) df_grafico = pd.concat([df[['Date', 'Sale', 'Tipo']], df_pred]) else: st.warning("Por favor, sube un archivo CSV válido para generasr predicciones.") return # Crear gráfica fig = px.line( df_grafico, x='Date', y='Sale', color='Tipo', title='Ventas pronosticadas (Ventas vs Mes)', labels={'Date': 'Fecha', 'Sale': 'Ventas', 'Tipo': 'Serie'} ) # Centramos el título del gráfico fig.update_layout( title={ 'text': "Ventas Actuales y Pronosticadas", 'x': 0.5, # Centrado horizontal 'xanchor': 'center', # Asegura el anclaje central 'yanchor': 'top' # Anclaje superior (opcional) }, title_font=dict(size=18, family="Arial, sans-serif", color='black'), ) fig.update_xaxes(tickangle=-45) # Mejorar el diseño de la leyenda fig.update_layout( legend=dict( title="Leyenda", # Título de la leyenda title_font=dict(size=12, color="black"), font=dict(size=10, color="black"), bgcolor="rgba(240,240,240,0.8)", # Fondo semitransparente bordercolor="gray", borderwidth=1, orientation="h", # Leyenda horizontal yanchor="top", y=-0.3, # Ajustar la posición vertical xanchor="right", x=0.5 # Centrar horizontalmente ) ) st.plotly_chart(fig, use_container_width=True) """ # =========================== # Función para generar predicciones # =========================== def generar_predicciones(modelo, df, periodos): ventas = df['Sale'] predicciones = modelo.forecast(steps=periodos) return predicciones # Función para mejorar el diseño de las gráficas def mejorar_diseno_grafica(fig, meses_relevantes, nombres_meses_relevantes): fig.update_layout( title={ 'text': "Ventas vs Mes", 'x': 0.5, # Centrado horizontal 'xanchor': 'center', # Asegura el anclaje central 'yanchor': 'top' # Anclaje superior (opcional) }, title_font=dict(size=18, family="Arial, sans-serif", color='black'), xaxis=dict( title='Mes-Año', title_font=dict(size=14, family="Arial, sans-serif", color='black'), tickangle=-45, # Rotar las etiquetas showgrid=True, gridwidth=0.5, gridcolor='lightgrey', showline=True, linecolor='black', linewidth=2, tickmode='array', # Controla qué etiquetas mostrar tickvals=meses_relevantes, # Selecciona solo los meses relevantes ticktext=nombres_meses_relevantes, # Meses seleccionados tickfont=dict(size=10), # Reducir el tamaño de la fuente de las etiquetas ), yaxis=dict( title='Ventas', title_font=dict(size=14, family="Arial, sans-serif", color='black'), showgrid=True, gridwidth=0.5, gridcolor='lightgrey', showline=True, linecolor='black', linewidth=2 ), plot_bgcolor='white', # Fondo blanco paper_bgcolor='white', # Fondo del lienzo de la gráfica font=dict(family="Arial, sans-serif", size=12, color="black"), showlegend=False, # Desactivar la leyenda si no es necesaria margin=dict(l=50, r=50, t=50, b=50) # Márgenes ajustados ) return fig # =========================== # Función para cerrar sesión # =========================== def cerrar_sesion(): st.session_state.logged_in = False st.session_state.usuario = None st.session_state.pagina_actual = "login" st.session_state.archivo_subido = False # Limpiar el archivo subido al cerrar sesión st.session_state.df_subido = None # Limpiar datos del archivo # Eliminar parámetros de la URL usando st.query_params st.query_params.clear() # Método correcto para limpiar parámetros de consulta # Redirigir a la página de login st.rerun()