Spaces:
Sleeping
Sleeping
| 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." | |