import os import uuid import tempfile import pandas as pd from openai import OpenAI from utils.utils import limpiar_nombre_archivo, preparar_bloques_texto_redes, markdown_a_word_docx, extraer_texto_de_docx, validar_y_leer_excel_redes, combinar_archivos_docx, clean_text from utils.prompts import obtener_prompts_por_informe from analysis.global_summary import generar_resumen_global from analysis.global_summary_final import generar_resumen_global_final from datetime import datetime import zipfile from docx import Document # Inicializar cliente OpenAI openai = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) def analizar_excel_redes_y_generar_pdf(archivo_excel, informe, nombre_cliente=None, nombre_competidor=None): """ Analiza comentarios de redes sociales a partir de un archivo Excel, CSV o un archivo .pkl con un DataFrame, y genera informes automáticos por red social utilizando modelos de lenguaje (GPT). Según el tipo de informe solicitado, aplica distintos prompts para generar análisis como desempeño, contenido temático, comparaciones o diagnóstico estratégico. Los resultados se consolidan y devuelven en un archivo .zip con documentos Word (.docx) por red y un resumen global. Parámetros: - archivo_excel (str): Ruta al archivo a procesar. Puede ser: • un archivo Excel (.xlsx) o CSV (.csv) subido por el usuario • un archivo .pkl generado desde la base de datos con comentarios ya cargados - informe (str): Tipo de informe solicitado por el usuario. - nombre_cliente (str, opcional): Nombre del cliente (si aplica según el informe). - nombre_competidor (str, opcional): Nombre del competidor (si aplica según el informe). Yields: - Tuple: - [ruta_zip]: Lista con la ruta al archivo ZIP generado (o None si hubo error). - mensaje (str): Estado final o mensaje de error. - progreso (str): Texto informativo con el detalle del avance. """ try: # =============================== # 🔍 Validación y lectura del archivo # =============================== if archivo_excel is None: yield None, "❌ Por favor, sube un archivo Excel o csv válido antes de generar el análisis.", None return df, error_msg = validar_y_leer_excel_redes(archivo_excel) if error_msg: yield None, error_msg, None return df["texto"] = df["texto"].apply(clean_text) redes_presentes = sorted(df["red_social"].dropna().unique().tolist()) lista_redes = ", ".join(redes_presentes) # =============================== # 📑 Generación de prompts según el tipo de informe # =============================== prompts, error_prompt = obtener_prompts_por_informe(informe, nombre_cliente, nombre_competidor) if error_prompt: yield None, error_prompt, None return # =============================== # 🧵 Agrupación de comentarios por red # =============================== # Agrupar comentarios por red social y serie (si aplica) if "serie" in df.columns: df["texto"] = df.apply(lambda row: f"[{row['serie']}] {row['texto']}", axis=1) else: df["texto"] = df["texto"] # No modifica si no existe 'serie' grouped = df.groupby("red_social")["texto"].apply(list) # Carpeta temporal donde se guardarán los resúmenes y archivos session_id = str(uuid.uuid4())[:8] temp_dir = os.path.join(tempfile.gettempdir(), f"resumenes_{session_id}") os.makedirs(temp_dir, exist_ok=True) archivos_finales = [] # =============================== # 🔁 Procesamiento por red social # =============================== for red, textos in grouped.items(): yield None, None, f"📊 Generando análisis para {red}..." # 🚫 Saltear redes sin texto válido if not textos or all(t.strip() == "" for t in textos): yield None, f"⚠️ La red {red} no contiene texto válido para analizar.", None continue # Crear directorio temporal para resúmenes de esta red resumen_dir_red = os.path.join(temp_dir, red.lower()) os.makedirs(resumen_dir_red, exist_ok=True) # Preparar bloques de texto (para evitar límite de tokens) bloques = preparar_bloques_texto_redes(textos) if not bloques: yield None, f"⚠️ No se pudieron generar bloques de texto para {red}. Verifica el contenido.", None continue # =============================== # ✍️ Resumen por bloque y clave # =============================== # Crear directorios para resúmenes por clave for clave in prompts: resumen_dir_clave = os.path.join(resumen_dir_red, f"resumenes_{clave}") os.makedirs(resumen_dir_clave, exist_ok=True) # Iterar por cada bloque de comentarios for i, bloque in enumerate(bloques, start=1): comentarios = [c.strip() for c in bloque.split("\n") if c.strip()] if not comentarios: yield None, f"⚠️ El bloque {i} de {red} no contiene comentarios válidos.", None continue message_history = [] # Generar resumen por cada clave de prompt, manteniendo el contexto for clave in prompts: prompt_info = prompts[clave] user_msg = ( prompt_info["user_prompt"].replace("{{REDES_PRESENTES}}", red) + f"\n\n⚠️ Este bloque contiene solo {len(comentarios)} comentarios.\n" + "Limitá el análisis a lo observable y no infieras más allá.\n\n" + "Comentarios:\n\n" + bloque ) # Si es la primera clave, iniciamos el contexto con system y user_msg de la prinera clave if len(message_history) == 0: message_history = [ {"role": "system", "content": prompt_info["system"]}, {"role": "user", "content": user_msg} ] else: # Agregamos nuevo mensaje del usuario para el siguiente análisis encadenado (siguiente clave) message_history.append({ "role": "user", "content": user_msg }) # Intentar generar el resumen con OpenAI try: response = openai.chat.completions.create( model="gpt-4o-mini", messages=message_history, temperature=0.4 ) resumen = response.choices[0].message.content message_history.append({"role": "assistant", "content": resumen}) # Agrega respuesta al contexto except Exception as e: resumen = f"⚠️ Error al generar el resumen del bloque {i} ({clave}): {str(e)}" # Guardar resumen en directorio por clave resumen_dir_clave = os.path.join(resumen_dir_red, f"resumenes_{clave}") os.makedirs(resumen_dir_clave, exist_ok=True) path_resumen = os.path.join(resumen_dir_clave, f"resumen_bloque_{i}.md") with open(path_resumen, "w", encoding="utf-8") as f: f.write(resumen) yield None, None, f"\n\n🧩 **Resumen del bloque {i}/{len(bloques)} de la red {red} generado ({clave})**\n{'-'*80}" # =============================== # 📘 Generación de resúmenes globales por red # =============================== for clave in prompts: yield None, None, f"\n\n🧩 **Resumen global de {clave}**\n{'-'*80}" resumen_dir_clave = os.path.join(resumen_dir_red, f"resumenes_{clave}") resumen_md = os.path.join(resumen_dir_red, f"{clave}_resumen.md") generar_resumen_global(resumen_dir_clave, resumen_md, tipo = "redes") # ✅ Concatenar todos los .md de esta red en resumen_final_red yield None, None, f"📘 Generando informe final de la red **{red}**..." resumen_final_red = os.path.join(resumen_dir_red, f"resumen_final_{red.lower()}.md") with open(resumen_final_red, "w", encoding="utf-8") as f_final: f_final.write(f"# 🧾 Análisis de {red.capitalize()}\n\n") for clave in prompts.keys(): resumen_md = os.path.join(resumen_dir_red, f"{clave}_resumen.md") if os.path.exists(resumen_md): with open(resumen_md, "r", encoding="utf-8") as f_in: contenido = f_in.read() f_final.write(f"\n\n## 📌 Resumen ({clave.upper()})\n\n") f_final.write(contenido) # ✨ Generar documento Word del resumen final de la red nombre_informe_limpio = limpiar_nombre_archivo(informe.replace(" ", "_")) docx_file = os.path.join(resumen_dir_red, f"{nombre_cliente.upper()}_{nombre_informe_limpio}_{red.upper()}_resumen.docx") markdown_a_word_docx(resumen_final_red, docx_file) archivos_finales.append(docx_file) # =============================== # 📎 Consolidación de resultados # =============================== # ✅ Combinar los Words individuales por red en uno solo fecha = datetime.today().strftime("%Y%m%d") nombre_archivo = f"{nombre_cliente.upper()}_Analisis_x_Red_Social_{nombre_informe_limpio}_{fecha}.docx" archivo_docx_unico = os.path.join(temp_dir, nombre_archivo) combinar_archivos_docx(archivos_finales, archivo_docx_unico) archivos_finales.append(archivo_docx_unico) # =============================== # 🧠 Generación del resumen global # =============================== # 🧾 Leer todos los resúmenes por red texto_completo = "" for archivo in archivos_finales: if archivo.endswith("_resumen.docx"): texto = extraer_texto_de_docx(archivo) red = os.path.basename(archivo).replace("_resumen.docx", "") texto_completo += f"\n\n---\n\n📘 **Resumen de {red.capitalize()}**\n\n{texto}" # ✨ Llamada al modelo para generar resumen global resumen_global = generar_resumen_global_final(texto_completo) # 💾 Guardar en .md y .docx el resumen global resumen_md_final = os.path.join(temp_dir, f"resumen_global_{fecha}.md") with open(resumen_md_final, "w", encoding="utf-8") as f: f.write(resumen_global) resumen_docx_final = os.path.join(temp_dir, f"Resumen_Global_{nombre_cliente.upper()}_{nombre_informe_limpio}_{fecha}.docx") markdown_a_word_docx(resumen_md_final, resumen_docx_final) archivos_finales.append(resumen_docx_final) # =============================== # 🗜️ Exportación final como archivo ZIP # =============================== # Crear archivo ZIP con todos los .docx (por clave + resumen global) zip_path = os.path.join(temp_dir, f"{nombre_cliente.upper()}_Analisis_{nombre_informe_limpio}_{fecha}.zip") with zipfile.ZipFile(zip_path, 'w') as zipf: for file_path in archivos_finales: if os.path.exists(file_path): arcname = os.path.relpath(file_path, temp_dir) # Nombre dentro del ZIP zipf.write(file_path, arcname=arcname) # ✅ Devolver solo el ZIP para descarga if os.path.exists(zip_path): yield [zip_path], "✅ Análisis completado correctamente.", "✅ ZIP con todos los resúmenes generados" else: yield None, "⚠️ No se pudo generar el archivo ZIP.", None except Exception as e: yield None, f"⚠️ Error inesperado: {str(e)}", None def comparar_informes_docx(archivos, cliente_c, session_id): """ Compara documentos .docx y genera una comparativa final en formato Word usando markdown intermedio. """ cliente_safe = cliente_c.replace(" ", "_").replace("/", "-").upper() temp_dir = os.path.join(tempfile.gettempdir(), f"Comparativa_{session_id}") os.makedirs(temp_dir, exist_ok=True) textos_procesados = [] for file in archivos: texto = extraer_texto_de_docx(file.name) if len(texto) > 35000: prompt = f"Reduce este documento a no más de 35000 caracteres, manteniendo el contexto de redes sociales presentes y conservando los títulos para cada red social:\n\n{texto}" resumen = openai.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Sos un asistente que sintetiza informes sin perder el detalle de redes sociales."}, {"role": "user", "content": prompt} ], temperature=0.4 ).choices[0].message.content textos_procesados.append(resumen) else: textos_procesados.append(texto) comparativa_prompt = ( f"Tengo estos documentos y necesito hacer un comparativo entre {cliente_c} y los otros, " f"poniendo como figura principal a {cliente_c}, cómo se posiciona en cada red social frente al resto, " f"Luego del comparativo incluye un apartado adicional para **Fortalezas y Debilidades Comparativas**, " f"otro para **Percepciones Globales por Red**, sigue con **Recomendaciones** y, finalmente, **Conclusión general**. " f"⚠️ Devuelve el análisis completamente en formato markdown usando títulos y subtítulos." ) prompt_final = comparativa_prompt + "\n\n".join([f"Documento {i+1}:\n{texto}" for i, texto in enumerate(textos_procesados)]) # Consulta final a OpenAI resultado = openai.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "Eres un analista digital especializado en redes sociales. Devuelves respuestas en markdown limpio."}, {"role": "user", "content": prompt_final} ], temperature=0.4 ).choices[0].message.content fecha = datetime.today().strftime("%Y%m%d") resultado_md = os.path.join(temp_dir, f"Comparativa_{cliente_safe}_{fecha}.md") with open(resultado_md, "w", encoding="utf-8") as f: f.write(resultado) comparativa_docx_final = os.path.join(temp_dir, f"Comparativa_{cliente_safe.upper()}_{fecha}.docx") markdown_a_word_docx(resultado_md, comparativa_docx_final) yield comparativa_docx_final, f"✅ Comparativa completada para {cliente_c}", "Análisis completado correctamente."