Spaces:
Running
Running
| """ | |
| 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}") |