File size: 9,395 Bytes
e0f016c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cb448
 
 
 
 
 
 
 
 
e0f016c
 
 
 
 
 
 
 
 
 
 
 
 
97cb448
e0f016c
 
 
 
97cb448
 
 
 
 
 
 
 
 
e0f016c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cb448
e0f016c
 
 
 
97cb448
 
 
 
 
 
 
 
 
 
 
 
e0f016c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cb448
 
e0f016c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cb448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0f016c
97cb448
e0f016c
97cb448
 
 
 
 
 
 
 
e0f016c
 
97cb448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0f016c
97cb448
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""
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}")