Chatbot-RAG-v4 / data /build_menu_json.py
NoeMartinezSanchez
modificacion del menu.json
97cb448
"""
Script para construir el archivo JSON del menú desde un Excel.
Lee todas las hojas del archivo Excel y las convierte en una estructura
jerárquica JSON con: Categoría → Subcategoría → Pregunta → Respuesta.
"""
import os
import json
import logging
from typing import Dict, List, Any, Optional
import pandas as pd
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def normalize_column_name(col: str) -> str:
"""
Normaliza nombres de columnas para coincidir con los esperados.
Args:
col: Nombre original de la columna
Returns:
Nombre normalizado en minúsculas sin acentos
"""
col_lower = col.lower().strip()
mapping = {
'categoría': 'categoria',
'subcategoría': 'subcategoria',
'asunto': 'asunto',
'solución': 'solucion',
'pregunta': 'solucion',
'respuesta': 'respuesta',
'url': 'url',
'formato': 'formato',
'etiquetas': 'etiquetas',
}
return mapping.get(col_lower, col_lower)
def get_column_value(row: pd.Series, possible_names: List[str]) -> Optional[str]:
"""
Obtiene el valor de una columna probando múltiples nombres posibles.
Args:
row: Fila del DataFrame
possible_names: Lista de nombres de columna posibles
Returns:
Valor de la columna o None si no se encuentra
"""
for col_name in possible_names:
# Buscar coincidencia exacta o parcial
for actual_col in row.index:
if actual_col.lower().strip() == col_name.lower().strip():
if pd.notna(row[actual_col]):
return str(row[actual_col]).strip()
# También buscar si contiene el nombre
if col_name.lower() in actual_col.lower():
if pd.notna(row[actual_col]):
return str(row[actual_col]).strip()
return None
def extract_additional_columns(row: pd.Series) -> Dict[str, Any]:
"""
Extrae columnas adicionales como metadatos.
Args:
row: Fila del DataFrame
Returns:
Diccionario con columnas adicionales
"""
main_columns = {'categoria', 'subcategoria', 'subcategoría', 'asunto', 'solucion', 'solución', 'respuesta', 'categoría', 'solución /acción'}
additional = {}
for col in row.index:
col_normalized = normalize_column_name(col)
# Ignorar columnas principales
is_main = False
for main in main_columns:
if main in col_normalized or col_normalized in main:
is_main = True
break
if not is_main and pd.notna(row[col]):
clean_col = col.strip()
additional[clean_col] = str(row[col]).strip()
return additional
def build_menu_json(excel_path: str, output_json_path: str) -> bool:
"""
Lee el archivo Excel y genera el JSON del menú.
Args:
excel_path: Ruta al archivo Excel
output_json_path: Ruta donde se guardará el JSON
Returns:
True si se generó correctamente, False si hubo errores
"""
if not os.path.exists(excel_path):
logger.warning(f"⚠️ Archivo Excel no encontrado: {excel_path}")
return False
try:
logger.info(f"📊 Leyendo Excel: {excel_path}")
excel_file = pd.ExcelFile(excel_path)
sheet_names = excel_file.sheet_names
logger.info(f"📋 Hojas encontradas: {sheet_names}")
menu_structure = {}
for sheet_name in sheet_names:
try:
logger.info(f" → Procesando hoja: {sheet_name}")
df = pd.read_excel(excel_file, sheet_name=sheet_name)
if df.empty:
logger.warning(f" ⚠️ Hoja vacía: {sheet_name}")
continue
# La categoría puede ser el nombre de la hoja O una columna
category_name = sheet_name.strip()
subcategories: Dict[str, List[Dict]] = {}
for idx, row in df.iterrows():
# La categoría puede estar en columna "Categoría" o usar el nombre de la hoja
category_col = get_column_value(row, ['categoria', 'categoría', 'categoría'])
if category_col:
category_name = category_col
# Subcategoría puede ser "Asunto"
subcategory = get_column_value(row, ['subcategoria', 'subcategoría', 'asunto', 'asunto'])
# Pregunta puede estar en "Solución" o "Solución /Acción"
question = get_column_value(row, ['solucion', 'solución', 'pregunta', 'solución /acción'])
# Respuesta
answer = get_column_value(row, ['respuesta'])
if not question or not answer:
continue
subcat_key = subcategory if subcategory else "General"
if subcat_key not in subcategories:
subcategories[subcat_key] = []
question_data = {
"question": question,
"answer": answer,
}
additional_cols = extract_additional_columns(row)
if additional_cols:
question_data["metadata"] = additional_cols
subcategories[subcat_key].append(question_data)
if subcategories:
menu_structure[category_name] = subcategories
total_q = sum(len(v) for v in subcategories.values())
logger.info(f" ✓ Categoría '{category_name}': {len(subcategories)} subcategorías, {total_q} preguntas")
else:
logger.warning(f" ⚠️ Sin datos válidos: {sheet_name}")
except Exception as e:
logger.error(f" ❌ Error procesando hoja '{sheet_name}': {e}")
continue
if not menu_structure:
logger.warning("⚠️ No se generó ninguna categoría")
return False
os.makedirs(os.path.dirname(output_json_path), exist_ok=True)
with open(output_json_path, 'w', encoding='utf-8') as f:
json.dump(menu_structure, f, ensure_ascii=False, indent=2)
logger.info(f"✅ JSON guardado en: {output_json_path}")
logger.info(f" Categorías: {len(menu_structure)}")
return True
except Exception as e:
logger.error(f"❌ Error general al generar JSON: {e}")
return False
def load_menu_json(json_path: str) -> Dict[str, Any]:
"""
Carga el menú desde el archivo JSON.
Args:
json_path: Ruta al archivo JSON
Returns:
Diccionario con la estructura del menú
"""
if not os.path.exists(json_path):
return {}
try:
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"❌ Error cargando JSON: {e}")
return {}
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Genera menu.json desde Excel para el chatbot RAG"
)
parser.add_argument(
"--excel",
default="data/Navegación Jerárquica_FER.xlsx",
help="Ruta al archivo Excel de navegación jerárquica"
)
parser.add_argument(
"--output",
default="data/menu.json",
help="Ruta de salida para el archivo JSON"
)
args = parser.parse_args()
logger.info("=" * 50)
logger.info("🚀 GENERADOR DE MENÚ JSON")
logger.info("=" * 50)
logger.info(f"📄 Excel: {args.excel}")
logger.info(f"📁 Salida: {args.output}")
logger.info("=" * 50)
success = build_menu_json(args.excel, args.output)
if success:
logger.info("=" * 50)
logger.info("✅ PROCESO COMPLETADO")
logger.info("=" * 50)
# Mostrar resumen
menu = load_menu_json(args.output)
total_cats = len(menu)
total_subcats = sum(len(v) for v in menu.values())
total_questions = sum(len(w) for v in menu.values() for w in v.values())
logger.info(f"📊 Resumen:")
logger.info(f" - Categorías: {total_cats}")
logger.info(f" - Subcategorías: {total_subcats}")
logger.info(f" - Preguntas: {total_questions}")
logger.info("")
logger.info(f"📁 Archivo generado: {os.path.abspath(args.output)}")
logger.info("")
logger.info("💡 Este archivo (menu.json) debe subirse al repositorio.")
logger.info(" NO subir el archivo Excel (*.xlsx)")
else:
logger.error("=" * 50)
logger.error("❌ PROCESO FALLIDO")
logger.error("=" * 50)
logger.error(f"Verifica que el archivo Excel exista en: {args.excel}")