network-analysis-ai / analysis /network_analysis.py
odelgi0810
comparativa
4cfb5ea
Raw
History Blame Contribute Delete
15.4 kB
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."