Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from transformers import pipeline, AutoModelForSeq2SeqLM, AutoTokenizer
|
| 3 |
+
import torch
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
import logging
|
| 7 |
+
import sys
|
| 8 |
+
from collections import defaultdict
|
| 9 |
+
import time # Para pausas simuladas y feedback visual
|
| 10 |
+
|
| 11 |
+
# --- Configuración Básica y Logging ---
|
| 12 |
+
logging.basicConfig(level=logging.INFO,
|
| 13 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 14 |
+
handlers=[logging.StreamHandler(sys.stdout)])
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
logger.info("SentientFlow: The Dynamic Cognition Engine - Iniciando app.py")
|
| 18 |
+
|
| 19 |
+
# --- Carga de Modelos AI (Los "Órganos Cognitivos") ---
|
| 20 |
+
# Definimos los modelos y sus propósitos. Intentamos usar modelos relativamente pequeños
|
| 21 |
+
# que *tienen* que caber en un Space gratuito CPU/GPU básica.
|
| 22 |
+
# Si hay OOM (Out Of Memory) errores, estos modelos deben ser reemplazados por versiones más pequeñas.
|
| 23 |
+
|
| 24 |
+
MODEL_CONFIG = {
|
| 25 |
+
# Modelo para la "Cognición Dinámica" / Planificación Paso a Paso / Razonamiento
|
| 26 |
+
# Debe ser bueno entendiendo instrucciones, el estado actual y generando JSON/texto estructurado.
|
| 27 |
+
# Flan-T5 Base es un buen candidato, pero Small podría ser necesario por memoria. Probemos Base.
|
| 28 |
+
"dynamic_cognition_model": {"model_name": "google/flan-t5-base", "tokenizer_name": "google/flan-t5-base"},
|
| 29 |
+
|
| 30 |
+
# Modelos para "Procesos Cognitivos" (Las tareas a ejecutar) - Usamos Pipelines
|
| 31 |
+
"summarization_pipeline": {"pipeline_key": "summarization", "model_name": "sshleifer/distilbart-cnn-1-2"}, # Un BART pequeño para resumen
|
| 32 |
+
"translation_en_es_pipeline": {"pipeline_key": "translation_en_to_es", "model_name": "Helsinki-NLP/opus-mt-en-es"},
|
| 33 |
+
"sentiment_analysis_pipeline": {"pipeline_key": "sentiment-analysis", "model_name": "cardiffnlp/twitter-roberta-base-sentiment"}, # Modelo de sentimiento rápido
|
| 34 |
+
"ner_extraction_pipeline": {"pipeline_key": "ner", "model_name": "dslim/bert-base-NER"}, # BERT base para NER
|
| 35 |
+
# Modelo/Pipeline para tareas Text2Text más generales: Extracción de Info, Q&A, Síntesis Final
|
| 36 |
+
"text2text_pipeline": {"pipeline_key": "text2text-generation", "model_name": "google/flan-t5-small"}, # T5 Small para tareas flexibles
|
| 37 |
+
# Puedes añadir más modelos/procesos aquí (ej: zero-shot-classification con un modelo pequeño)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Diccionarios para almacenar los modelos y pipelines cargados exitosamente
|
| 41 |
+
loaded_models = {}
|
| 42 |
+
loaded_pipelines = {}
|
| 43 |
+
available_cognitive_processes = {} # Mapeo de nombre de proceso a función de ejecución
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_ai_models():
|
| 47 |
+
"""Carga todos los modelos y pipelines definidos en MODEL_CONFIG."""
|
| 48 |
+
global loaded_models, loaded_pipelines, available_cognitive_processes
|
| 49 |
+
logger.info("🚀 Iniciando carga de modelos para SentientFlow...")
|
| 50 |
+
|
| 51 |
+
# Determinar el dispositivo
|
| 52 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 53 |
+
logger.info(f"Dispositivo de carga: {device}")
|
| 54 |
+
device_idx = 0 if device.type == 'cuda' else -1 # Para pipelines
|
| 55 |
+
|
| 56 |
+
for model_alias, config in MODEL_CONFIG.items():
|
| 57 |
+
model_name = config["model_name"]
|
| 58 |
+
tokenizer_name = config.get("tokenizer_name", model_name) # Tokenizer puede ser el mismo
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
logger.info(f"Cargando modelo/pipeline: {model_alias} ({model_name})...")
|
| 62 |
+
if "pipeline_key" in config:
|
| 63 |
+
# Cargar como pipeline
|
| 64 |
+
pipeline_key = config["pipeline_key"]
|
| 65 |
+
# La función pipeline maneja la carga de modelo/tokenizer y mover a device
|
| 66 |
+
pipe = pipeline(pipeline_key, model=model_name, tokenizer=tokenizer_name, device=device_idx)
|
| 67 |
+
loaded_pipelines[model_alias] = pipe
|
| 68 |
+
logger.info(f"Pipeline '{model_alias}' cargado.")
|
| 69 |
+
else:
|
| 70 |
+
# Cargar solo tokenizer y modelo (para modelos usados directamente, como el planner)
|
| 71 |
+
tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
|
| 72 |
+
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
|
| 73 |
+
model.to(device) # Mover el modelo al dispositivo
|
| 74 |
+
loaded_models[model_alias] = {"model": model, "tokenizer": tokenizer, "device": device}
|
| 75 |
+
logger.info(f"Modelo '{model_alias}' cargado.")
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
logger.error(f"❌ Error cargando {model_alias} ({model_name}): {e}")
|
| 79 |
+
loaded_models[model_alias] = None
|
| 80 |
+
loaded_pipelines[model_alias] = None
|
| 81 |
+
logger.warning(f"{model_alias} no estará disponible.")
|
| 82 |
+
|
| 83 |
+
logger.info("✅ Carga de modelos de IA finalizada.")
|
| 84 |
+
# Limpiar caché de GPU tras carga si es necesario
|
| 85 |
+
if device.type == 'cuda':
|
| 86 |
+
torch.cuda.empty_cache()
|
| 87 |
+
logger.info("Memoria caché de GPU liberada después de la carga.")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# --- Definición de Procesos Cognitivos Disponibles ---
|
| 91 |
+
# Cada función representa un "acto cognitivo". Deben ser lo suficientemente flexibles
|
| 92 |
+
# para tomar como input el texto principal Y los resultados acumulados.
|
| 93 |
+
# Devuelven (resumen_legible_humano, resultado_estructurado_dict).
|
| 94 |
+
|
| 95 |
+
class CognitiveProcesses:
|
| 96 |
+
"""Implementaciones de los actos cognitivos."""
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
def summarize(text_context, cumulative_results, params, objective):
|
| 100 |
+
"""Resume el texto principal o una parte/resultado si params lo indica."""
|
| 101 |
+
pipe = loaded_pipelines.get("summarization_pipeline")
|
| 102 |
+
if not pipe: return "❌ Resumen: Modelo no disponible.", {"summary": None, "status": "error"}
|
| 103 |
+
try:
|
| 104 |
+
# Implementar lógica para resumir based_on params o cumulative_results si se desea en el futuro
|
| 105 |
+
# Por ahora, resume el texto principal (quizás truncado)
|
| 106 |
+
max_input_chars = 3000 # Ajustado para distilbart-cnn-1-2
|
| 107 |
+
text_to_summarize = text_context # default
|
| 108 |
+
if len(text_to_summarize) > max_input_chars:
|
| 109 |
+
logger.warning(f"Resumen: Texto largo ({len(text_to_summarize)}), truncando.")
|
| 110 |
+
text_to_summarize = text_to_summarize[:max_input_chars] + "..."
|
| 111 |
+
|
| 112 |
+
summary = pipe(text_to_summarize, max_length=150, min_length=40, do_sample=False)[0]['summary_text']
|
| 113 |
+
return f"✅ Resumen: {summary}", {"summary": summary, "from_text_length": len(text_context), "status": "success"}
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Error en resumen: {e}")
|
| 116 |
+
return f"❌ Error durante resumen: {e}", {"summary": None, "error": str(e), "status": "error"}
|
| 117 |
+
|
| 118 |
+
@staticmethod
|
| 119 |
+
def translate(text_context, cumulative_results, params, objective):
|
| 120 |
+
"""Traduce el texto principal a un idioma (español o inglés)."""
|
| 121 |
+
target_lang = params.get('language', 'español').lower()
|
| 122 |
+
if target_lang == 'español':
|
| 123 |
+
pipe = loaded_pipelines.get("translation_en_es_pipeline")
|
| 124 |
+
pipe_key = "translation_en_es_pipeline"
|
| 125 |
+
elif target_lang == 'ingles':
|
| 126 |
+
pipe = loaded_pipelines.get("translation_es_en_pipeline") # Asumimos es_en si pide inglés
|
| 127 |
+
pipe_key = "translation_es_en_pipeline"
|
| 128 |
+
else:
|
| 129 |
+
return f"❌ Traducción: Idioma '{target_lang}' no soportado.", {"translation": None, "status": "error"}
|
| 130 |
+
|
| 131 |
+
if not pipe: return f"❌ Traducción a {target_lang}: Modelo no disponible.", {"translation": None, "status": "error"}
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
max_input_chars = 1500 # Ajustado para Opus-MT
|
| 135 |
+
text_to_translate = text_context
|
| 136 |
+
if len(text_to_translate) > max_input_chars:
|
| 137 |
+
logger.warning(f"Traducción a {target_lang}: Texto largo ({len(text_to_translate)}), truncando.")
|
| 138 |
+
text_to_translate = text_to_translate[:max_input_chars] + "..."
|
| 139 |
+
|
| 140 |
+
translation = pipe(text_to_translate, max_length=512)[0]['translation_text']
|
| 141 |
+
return f"✅ Traducción a {target_lang}: {translation}", {"translation": translation, "language": target_lang, "status": "success"}
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logger.error(f"Error en traducción a {target_lang}: {e}")
|
| 144 |
+
return f"❌ Error durante traducción a {target_lang}: {e}", {"translation": None, "error": str(e), "status": "error"}
|
| 145 |
+
|
| 146 |
+
@staticmethod
|
| 147 |
+
def analyze_sentiment(text_context, cumulative_results, params, objective):
|
| 148 |
+
"""Analiza el sentimiento del texto principal."""
|
| 149 |
+
pipe = loaded_pipelines.get("sentiment_analysis_pipeline")
|
| 150 |
+
if not pipe: return "❌ Sentimiento: Modelo no disponible.", {"sentiment": None, "status": "error"}
|
| 151 |
+
try:
|
| 152 |
+
max_input_chars = 500 # Ajustado para modelos tipo BERT
|
| 153 |
+
text_to_analyze = text_context
|
| 154 |
+
if len(text_to_analyze) > max_input_chars:
|
| 155 |
+
logger.warning(f"Sentimiento: Texto largo ({len(text_to_analyze)}), truncando.")
|
| 156 |
+
text_to_analyze = text_to_analyze[:max_input_chars] + "..."
|
| 157 |
+
|
| 158 |
+
sentiment_result = pipe(text_to_analyze)[0]
|
| 159 |
+
# sentiment_result es un dict como {'label': '4 stars', 'score': 0.8}
|
| 160 |
+
return f"✅ Sentimiento: '{sentiment_result['label']}' ({sentiment_result['score']:.2f})", {"sentiment": sentiment_result, "status": "success"}
|
| 161 |
+
except Exception as e:
|
| 162 |
+
logger.error(f"Error en análisis de sentimiento: {e}")
|
| 163 |
+
return f"❌ Error durante análisis de sentimiento: {e}", {"sentiment": None, "error": str(e), "status": "error"}
|
| 164 |
+
|
| 165 |
+
@staticmethod
|
| 166 |
+
def extract_entities(text_context, cumulative_results, params, objective):
|
| 167 |
+
"""Extrae entidades nombradas del texto principal."""
|
| 168 |
+
pipe = loaded_pipelines.get("ner_extraction_pipeline")
|
| 169 |
+
if not pipe: return "❌ Entidades: Modelo no disponible.", {"entities": [], "status": "error"}
|
| 170 |
+
try:
|
| 171 |
+
max_input_chars = 1000 # Ajustado para BERT base
|
| 172 |
+
text_to_extract = text_context
|
| 173 |
+
if len(text_to_extract) > max_input_chars:
|
| 174 |
+
logger.warning(f"Entidades: Texto largo ({len(text_to_extract)}), truncando.")
|
| 175 |
+
text_to_extract = text_to_extract[:max_input_chars] + "..."
|
| 176 |
+
|
| 177 |
+
entities = pipe(text_to_extract)
|
| 178 |
+
# Post-procesamiento para unir sub-palabras
|
| 179 |
+
processed_entities = []
|
| 180 |
+
current_entity = None
|
| 181 |
+
for entity in entities:
|
| 182 |
+
word = entity['word']
|
| 183 |
+
# Reemplazar espacios en sub-tokens si el modelo los introduce (ej: " _ ")
|
| 184 |
+
word = word.replace(" _ ", " ").strip()
|
| 185 |
+
|
| 186 |
+
# Heurística para unir entidades: si es continuation (##) O es el mismo tipo y la siguiente palabra
|
| 187 |
+
# Nota: Modelos BERT pueden tokenizar "New York" como ["New", "York"] o ["New", " York"]. ## indica sub-token,
|
| 188 |
+
# pero a veces modelos distintos usan otras heurísticas o no las usan.
|
| 189 |
+
# Una heurística más robusta podría implicar distancia entre tokens o tipos de entidades.
|
| 190 |
+
# Simplificamos: si empieza con ## o es un token adyacente del mismo tipo.
|
| 191 |
+
is_continuation = word.startswith("##") or (current_entity and entity['index'] == current_entity['end_index'] + 1 and entity['entity'].split('-')[-1] == current_entity['type'])
|
| 192 |
+
|
| 193 |
+
if current_entity is None or not is_continuation:
|
| 194 |
+
if current_entity: # Guardar la entidad anterior si existe
|
| 195 |
+
processed_entities.append({
|
| 196 |
+
"word": "".join(current_entity["words"]).replace(" ##", "").replace(" ##", " ").strip(), # Unir y limpiar sub-tokens, manejar espacios
|
| 197 |
+
"type": current_entity["type"],
|
| 198 |
+
"score": current_entity["score"]
|
| 199 |
+
})
|
| 200 |
+
# Iniciar nueva entidad
|
| 201 |
+
current_entity = {
|
| 202 |
+
"words": [word],
|
| 203 |
+
"type": entity['entity'].split('-')[-1],
|
| 204 |
+
"score": entity['score'],
|
| 205 |
+
"start_index": entity.get('start', -1), # Añadir índices si el pipeline los da
|
| 206 |
+
"end_index": entity.get('end', -1)
|
| 207 |
+
}
|
| 208 |
+
else: # Continuación de la entidad actual
|
| 209 |
+
current_entity["words"].append(word)
|
| 210 |
+
current_entity["score"] = max(current_entity["score"], entity['score']) # O promediar
|
| 211 |
+
current_entity["end_index"] = entity.get('end', current_entity["end_index"])
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# Añadir la última entidad si existe
|
| 215 |
+
if current_entity:
|
| 216 |
+
processed_entities.append({
|
| 217 |
+
"word": "".join(current_entity["words"]).replace(" ##", "").replace(" ##", " ").strip(),
|
| 218 |
+
"type": current_entity["type"],
|
| 219 |
+
"score": current_entity["score"]
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
# Filtrar entidades con score bajo o tipos no deseados si es necesario
|
| 223 |
+
# processed_entities = [e for e in processed_entities if e['score'] > 0.9] # Ejemplo
|
| 224 |
+
|
| 225 |
+
summary_list = [f"{e['word']} ({e['type']})" for e in processed_entities[:10]] # Mostrar las primeras 10 en resumen
|
| 226 |
+
summary_str = "; ".join(summary_list) if summary_list else "ninguna detectada."
|
| 227 |
+
|
| 228 |
+
return f"✅ Entidades Extraídas: {summary_str}", {"entities": processed_entities, "status": "success"}
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logger.error(f"Error en extracción de entidades: {e}")
|
| 231 |
+
return f"❌ Error durante extracción de entidades: {e}", {"entities": [], "error": str(e), "status": "error"}
|
| 232 |
+
|
| 233 |
+
@staticmethod
|
| 234 |
+
def extract_information(text_context, cumulative_results, params, objective):
|
| 235 |
+
"""Extrae información específica usando un modelo text2text basado en una query."""
|
| 236 |
+
pipe = loaded_pipelines.get("text2text_pipeline")
|
| 237 |
+
if not pipe: return "❌ Extracción Info: Modelo no disponible.", {"extracted_info": None, "status": "error"}
|
| 238 |
+
|
| 239 |
+
# La query puede venir de params, del objetivo o ser una query genérica
|
| 240 |
+
query = params.get('query', objective if len(objective) < 100 else "información clave")
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
max_input_chars = 1500 # Ajustado para Flan-T5 small
|
| 244 |
+
# Formato de prompt para T5: "question: ... context: ..."
|
| 245 |
+
prompt = f"question: {query}\ncontext: {text_context}"
|
| 246 |
+
if len(prompt) > max_input_chars:
|
| 247 |
+
logger.warning(f"Extracción Info: Prompt largo ({len(prompt)}), truncando contexto.")
|
| 248 |
+
# Truncamos el contexto si el prompt es muy largo
|
| 249 |
+
prompt = f"question: {query}\ncontext: {text_context[:max_input_chars - len(f'question: {query}\ncontext: ')]}..."
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
extracted_info = pipe(prompt, max_length=150, do_sample=False)[0]['generated_text']
|
| 253 |
+
|
| 254 |
+
return f"✅ Información Extraída ('{query[:50]}...'): {extracted_info}", {"extracted_info": extracted_info, "query": query, "status": "success"}
|
| 255 |
+
except Exception as e:
|
| 256 |
+
logger.error(f"Error en extracción de info: {e}")
|
| 257 |
+
return f"❌ Error durante extracción de info: {e}", {"extracted_info": None, "error": str(e), "status": "error"}
|
| 258 |
+
|
| 259 |
+
@staticmethod
|
| 260 |
+
def synthesize_report(text_context, cumulative_results, params, objective):
|
| 261 |
+
"""Genera un reporte final combinando texto original y resultados acumulados."""
|
| 262 |
+
pipe = loaded_pipelines.get("text2text_pipeline")
|
| 263 |
+
if not pipe: return "❌ Síntesis Final: Modelo no disponible.", {"final_report": "Modelo de síntesis no disponible.", "status": "error"}
|
| 264 |
+
|
| 265 |
+
try:
|
| 266 |
+
# Crear un prompt que incluya el objetivo, el texto original (parcial)
|
| 267 |
+
# y un resumen formateado de los resultados acumulados.
|
| 268 |
+
results_summary_str = "Resultados del análisis:\n"
|
| 269 |
+
if cumulative_results:
|
| 270 |
+
for key, result_data in cumulative_results.items():
|
| 271 |
+
# Intentar formatear resultados comunes de forma legible para el modelo
|
| 272 |
+
if result_data and isinstance(result_data, dict) and result_data.get("status") == "success":
|
| 273 |
+
if "summary" in result_data: results_summary_str += f"- Resumen: {result_data['summary'][:200]}...\n"
|
| 274 |
+
elif "translation" in result_data: results_summary_str += f"- Traducción ({result_data.get('language')}): {result_data['translation'][:200]}...\n"
|
| 275 |
+
elif "sentiment" in result_data: results_summary_str += f"- Sentimiento: {result_data['sentiment'].get('label')} ({result_data['sentiment'].get('score'):.2f})\n"
|
| 276 |
+
elif "entities" in result_data:
|
| 277 |
+
entities_list = [f"{e['word']} ({e['type']})" for e in result_data['entities'][:5]]
|
| 278 |
+
results_summary_str += f"- Entidades Clave: {'; '.join(entities_list)}...\n"
|
| 279 |
+
elif "extracted_info" in result_data: results_summary_str += f"- Información Extraída: {result_data['extracted_info'][:200]}...\n"
|
| 280 |
+
# Añadir manejo para otros tipos de resultados si se crean
|
| 281 |
+
if results_summary_str == "Resultados del análisis:\n":
|
| 282 |
+
results_summary_str = "Resultados del análisis: No hay resultados exitosos para sintetizar."
|
| 283 |
+
else:
|
| 284 |
+
results_summary_str = "Resultados del análisis: No hay resultados parciales."
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# Limitar el texto original para el prompt
|
| 288 |
+
original_text_preview = text_context[:2000] + "..." if len(text_context) > 2000 else text_context # Permitir un poco más de texto original
|
| 289 |
+
|
| 290 |
+
# Prompt para la síntesis final
|
| 291 |
+
prompt = f"""
|
| 292 |
+
Objective: {objective}
|
| 293 |
+
Original Text Summary: {original_text_preview[:1000]}...
|
| 294 |
+
---
|
| 295 |
+
{results_summary_str[:1000]}... # Limitar también el resumen de resultados
|
| 296 |
+
---
|
| 297 |
+
Based on the objective, the original text, and the analysis results provided, synthesize a comprehensive report. Make it insightful and directly address the objective.
|
| 298 |
+
Report:
|
| 299 |
+
"""
|
| 300 |
+
max_input_chars = 2000 # Ajustado para el prompt combinado + texto
|
| 301 |
+
|
| 302 |
+
# Ensure the prompt length is within limits after combining parts
|
| 303 |
+
if len(prompt) > max_input_chars:
|
| 304 |
+
logger.warning(f"Síntesis Final: Prompt muy largo ({len(prompt)}), truncando...")
|
| 305 |
+
# Re-build prompt more aggressively truncated if needed
|
| 306 |
+
prompt = f"""
|
| 307 |
+
Objective: {objective[:200]}...
|
| 308 |
+
Original Text Summary: {original_text_preview[:800]}...
|
| 309 |
+
---
|
| 310 |
+
{results_summary_str[:800]}...
|
| 311 |
+
---
|
| 312 |
+
Based on the objective, text, and analysis results, synthesize a comprehensive report.
|
| 313 |
+
Report:
|
| 314 |
+
"""
|
| 315 |
+
prompt = prompt[:max_input_chars] # Final truncation just in case
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# Generar el reporte final
|
| 319 |
+
final_report_text = pipe(prompt, max_length=600, num_return_sequences=1, do_sample=True, temperature=0.8)[0]['generated_text']
|
| 320 |
+
|
| 321 |
+
return f"✅ Síntesis Final Generada.", {"final_report_text": final_report_text, "status": "success"}
|
| 322 |
+
|
| 323 |
+
except Exception as e:
|
| 324 |
+
logger.error(f"Error en síntesis final: {e}")
|
| 325 |
+
return f"❌ Error durante síntesis final: {e}", {"final_report_text": None, "error": str(e), "status": "error"}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# Mapeo de nombres de procesos (usados por el modelo planificador) a las funciones de CognitiveProcesses
|
| 329 |
+
# Se llena después de cargar los modelos para verificar disponibilidad
|
| 330 |
+
PROCESS_FUNCTION_MAP = {
|
| 331 |
+
"summarize": CognitiveProcesses.summarize,
|
| 332 |
+
"translate": CognitiveProcesses.translate,
|
| 333 |
+
"analyze_sentiment": CognitiveProcesses.analyze_sentiment,
|
| 334 |
+
"extract_entities": CognitiveProcesses.extract_entities,
|
| 335 |
+
"extract_information": CognitiveProcesses.extract_information,
|
| 336 |
+
"synthesize_report": CognitiveProcesses.synthesize_report, # Este debería ser a menudo el último paso
|
| 337 |
+
# Añadir nuevos procesos aquí
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
# Lista de nombres de procesos disponibles para el prompt del modelo de cognición dinámica
|
| 341 |
+
AVAILABLE_PROCESSES = ", ".join([f"`{name}`" for name in PROCESS_FUNCTION_MAP.keys()])
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# --- Motor de Cognición Dinámica (El "Sentient Core") ---
|
| 345 |
+
|
| 346 |
+
def run_dynamic_cognition(objective, initial_text, max_cycles=7, progress=gr.Progress()):
|
| 347 |
+
"""
|
| 348 |
+
Ejecuta ciclos cognitivos dinámicos para lograr el objetivo.
|
| 349 |
+
En cada ciclo, la IA decide el siguiente paso.
|
| 350 |
+
"""
|
| 351 |
+
logger.info(f"🧠 SentientFlow: Iniciando proceso para objetivo: '{objective}'")
|
| 352 |
+
progress(0, desc="Iniciando SentientFlow...")
|
| 353 |
+
|
| 354 |
+
# Obtener modelo de cognición dinámica
|
| 355 |
+
cognition_model_info = loaded_models.get("dynamic_cognition_model")
|
| 356 |
+
if not cognition_model_info or not cognition_model_info["model"] or not cognition_model_info["tokenizer"]:
|
| 357 |
+
logger.error("❌ Modelo de cognición dinámica no cargado.")
|
| 358 |
+
return initial_text, {"status": "error", "message": "Modelo de cognición dinámica no disponible."}, "❌ Error: Modelo de cognición dinámica no cargado. La automatización dinámica es imposible."
|
| 359 |
+
|
| 360 |
+
cognition_model = cognition_model_info["model"]
|
| 361 |
+
cognition_tokenizer = cognition_model_info["tokenizer"]
|
| 362 |
+
cognition_device = cognition_model_info["device"]
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
current_text = initial_text # El texto principal sobre el que operan algunos procesos
|
| 366 |
+
cumulative_results = {} # Memoria de trabajo: acumula resultados estructurados
|
| 367 |
+
execution_log = ["--- Log de SentientFlow ---"]
|
| 368 |
+
history = [] # Para dar contexto al modelo de cognición (pasos anteriores)
|
| 369 |
+
|
| 370 |
+
execution_log.append(f"🎯 Objetivo: {objective}")
|
| 371 |
+
execution_log.append(f"📜 Texto Inicial (parcial): {initial_text[:500]}...")
|
| 372 |
+
execution_log.append("---")
|
| 373 |
+
|
| 374 |
+
# Asegurarse de que los procesos estén mapeados y disponibles
|
| 375 |
+
available_processes_in_map = {name: func for name, func in PROCESS_FUNCTION_MAP.items() if name in AVAILABLE_PROCESSES}
|
| 376 |
+
if not available_processes_in_map:
|
| 377 |
+
msg = "⚠️ Ningún proceso cognitivo disponible (modelos no cargados). La IA no puede hacer nada."
|
| 378 |
+
execution_log.append(msg)
|
| 379 |
+
logger.warning(msg)
|
| 380 |
+
# Intentar ejecutar solo síntesis final con texto original si está disponible
|
| 381 |
+
final_synthesis_result = CognitiveProcesses.synthesize_report(initial_text, {}, {"query": objective}, objective)
|
| 382 |
+
cumulative_results["final_synthesis"] = final_synthesis_result[1]
|
| 383 |
+
execution_log.append(f"\n--- Intentando Síntesis Final Directa ---")
|
| 384 |
+
execution_log.append(final_synthesis_result[0])
|
| 385 |
+
execution_log.append("--- Fin de Ejecución ---")
|
| 386 |
+
return final_synthesis_result[1].get("final_report_text", "Síntesis no generada."), cumulative_results, "\n".join(execution_log)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
# --- Bucle de Ciclos Cognitivos ---
|
| 390 |
+
for cycle in range(max_cycles):
|
| 391 |
+
progress_percentage = (cycle + 1) / (max_cycles + 1) # +1 para incluir la síntesis final
|
| 392 |
+
progress(progress_percentage, desc=f"💭 Ciclo Cognitivo {cycle+1}/{max_cycles}")
|
| 393 |
+
execution_log.append(f"\n--- Ciclo Cognitivo {cycle+1} ---")
|
| 394 |
+
logger.info(f"--- Ciclo Cognitivo {cycle+1} ---")
|
| 395 |
+
|
| 396 |
+
# 1. La IA decide el siguiente paso
|
| 397 |
+
# Crear un prompt para el modelo de cognición dinámica
|
| 398 |
+
# Incluir el objetivo, el texto original (parcial), los resultados acumulados (resumido) y el historial
|
| 399 |
+
results_summary_for_prompt = "Current Results Summary:\n"
|
| 400 |
+
if cumulative_results:
|
| 401 |
+
for key, result_data in cumulative_results.items():
|
| 402 |
+
if result_data and isinstance(result_data, dict) and result_data.get("status") == "success":
|
| 403 |
+
if "summary" in result_data: results_summary_for_prompt += f"- {key}: Summary - {result_data['summary'][:100]}...\n"
|
| 404 |
+
elif "translation" in result_data: results_summary_for_prompt += f"- {key}: Translation ({result_data.get('language')})\n"
|
| 405 |
+
elif "sentiment" in result_data: results_summary_for_prompt += f"- {key}: Sentiment ({result_data['sentiment'].get('label')})\n"
|
| 406 |
+
elif "entities" in result_data: results_summary_for_prompt += f"- {key}: Extracted {len(result_data['entities'])} entities\n"
|
| 407 |
+
elif "extracted_info" in result_data: results_summary_for_prompt += f"- {key}: Info ('{result_data.get('query')[:50]}...')\n"
|
| 408 |
+
elif "final_report_text" in result_data: results_summary_for_prompt += f"- {key}: Final Report (already generated)\n"
|
| 409 |
+
else: results_summary_for_prompt += f"- {key}: Processed data\n"
|
| 410 |
+
else:
|
| 411 |
+
results_summary_for_prompt += "No results yet."
|
| 412 |
+
|
| 413 |
+
history_summary_for_prompt = "Previous steps:\n" + "\n".join([f"- {h['action']} ({h['status']})" for h in history[-3:]]) # Últimos 3 pasos
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
decision_prompt = f"""
|
| 417 |
+
You are the SentientFlow Dynamic Cognition Engine. Your goal is to analyze the user's OBJECTIVE and the provided TEXT, performing a series of cognitive processes using the AVAILABLE PROCESSES to achieve the objective.
|
| 418 |
+
|
| 419 |
+
You operate in cycles. In each cycle, you must decide the NEXT BEST PROCESS to run or if you are READY TO FINISH. You should consider the TEXT, the accumulated RESULTS, and the HISTORY of steps already taken.
|
| 420 |
+
|
| 421 |
+
Respond ONLY with a JSON object like this:
|
| 422 |
+
{{
|
| 423 |
+
"decision": "process" or "finish",
|
| 424 |
+
"process_name": "name_of_available_process" or null,
|
| 425 |
+
"parameters": {{...}}, // Parameters for the process, or empty dict. E.g., {{"language": "spanish"}} or {{"query": "key people"}}
|
| 426 |
+
"reasoning": "Your internal thought process explaining this decision."
|
| 427 |
+
}}
|
| 428 |
+
|
| 429 |
+
AVAILABLE PROCESSES: {", ".join(available_processes_in_map.keys())}
|
| 430 |
+
|
| 431 |
+
OBJECTIVE: {objective}
|
| 432 |
+
TEXT (first 500 chars): {initial_text[:500]}...
|
| 433 |
+
---
|
| 434 |
+
{results_summary_for_prompt[:500]}...
|
| 435 |
+
---
|
| 436 |
+
{history_summary_for_prompt}
|
| 437 |
+
---
|
| 438 |
+
Based on the OBJECTIVE, TEXT, RESULTS, and HISTORY, decide the NEXT BEST PROCESS or if you are READY TO FINISH.
|
| 439 |
+
"""
|
| 440 |
+
max_planner_input_chars = 1500 # Limit input length for the planner model
|
| 441 |
+
if len(decision_prompt) > max_planner_input_chars:
|
| 442 |
+
decision_prompt = decision_prompt[:max_planner_input_chars] + "..."
|
| 443 |
+
logger.warning(f"Planner prompt largo ({len(decision_prompt)}), truncando.")
|
| 444 |
+
|
| 445 |
+
logger.info(f"Prompt para modelo de cognición dinámica:\n---\n{decision_prompt}\n---")
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
# Mover modelo de cognición a device para inferencia
|
| 449 |
+
cognition_model.to(cognition_device)
|
| 450 |
+
input_ids = cognition_tokenizer(decision_prompt, return_tensors="pt").input_ids.to(cognition_device)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
try:
|
| 454 |
+
# Generar la decisión (JSON)
|
| 455 |
+
decision_outputs = cognition_model.generate(input_ids, max_new_tokens=200, num_beams=3, early_stopping=True, no_repeat_ngram_size=2)
|
| 456 |
+
decision_text = cognition_tokenizer.decode(decision_outputs[0], skip_special_tokens=True)
|
| 457 |
+
|
| 458 |
+
# Mover modelo de vuelta a CPU si es necesario
|
| 459 |
+
if cognition_device.type == 'cuda':
|
| 460 |
+
cognition_model.to("cpu")
|
| 461 |
+
torch.cuda.empty_cache()
|
| 462 |
+
logger.info("Modelo de cognición movido a CPU.")
|
| 463 |
+
|
| 464 |
+
logger.info(f"Respuesta cruda del modelo de cognición:\n---\n{decision_text}\n---")
|
| 465 |
+
|
| 466 |
+
# Intentar parsear la respuesta como JSON
|
| 467 |
+
decision_match = re.search(r"```json\n(.*?)\n```", decision_text, re.DOTALL)
|
| 468 |
+
if decision_match:
|
| 469 |
+
decision_json_str = decision_match.group(1).strip()
|
| 470 |
+
else:
|
| 471 |
+
decision_json_str = decision_text.strip() # Intentar parsear output completo si no hay ```json```
|
| 472 |
+
|
| 473 |
+
decision_data = json.loads(decision_json_str)
|
| 474 |
+
|
| 475 |
+
# Validar la estructura de la decisión
|
| 476 |
+
if not isinstance(decision_data, dict) or "decision" not in decision_data or "reasoning" not in decision_data:
|
| 477 |
+
raise ValueError("Estructura de decisión JSON inválida.")
|
| 478 |
+
|
| 479 |
+
execution_log.append(f"💭 Pensamiento Interno: {decision_data.get('reasoning', 'No proporcionado.')}")
|
| 480 |
+
logger.info(f"Decisión del ciclo {cycle+1}: {decision_data.get('decision')}, Proceso: {decision_data.get('process_name')}")
|
| 481 |
+
|
| 482 |
+
if decision_data.get("decision") == "finish":
|
| 483 |
+
execution_log.append("✅ La IA decide que ha terminado el análisis.")
|
| 484 |
+
logger.info("AI decided to finish.")
|
| 485 |
+
break # Salir del bucle de ciclos cognitivos
|
| 486 |
+
|
| 487 |
+
# 2. Ejecutar el proceso decidido
|
| 488 |
+
process_name = decision_data.get("process_name")
|
| 489 |
+
process_params = decision_data.get("parameters", {})
|
| 490 |
+
|
| 491 |
+
if process_name and process_name in available_processes_in_map:
|
| 492 |
+
process_func = available_processes_in_map[process_name]
|
| 493 |
+
execution_log.append(f"⚙️ Ejecutando Proceso '{process_name}' con params: {process_params}...")
|
| 494 |
+
logger.info(f"Executing process: {process_name}")
|
| 495 |
+
|
| 496 |
+
# Ejecutar la función del proceso cognitivo
|
| 497 |
+
human_readable_result, structured_output = process_func(current_text, cumulative_results, process_params, objective)
|
| 498 |
+
|
| 499 |
+
# Añadir resultado a la memoria de trabajo (cumulative_results)
|
| 500 |
+
result_key = f"{process_name}_{len(history)+1}" # Clave única para el resultado
|
| 501 |
+
cumulative_results[result_key] = structured_output
|
| 502 |
+
|
| 503 |
+
execution_log.append(f"Resultado: {human_readable_result}")
|
| 504 |
+
# Mostrar una parte del resultado estructurado si no es solo error/None
|
| 505 |
+
if structured_output and structured_output.get("status") == "success":
|
| 506 |
+
structured_output_preview = json.dumps(structured_output, indent=2)
|
| 507 |
+
execution_log.append(f"Resultado Estructurado (parcial): {structured_output_preview[:500]}...")
|
| 508 |
+
elif structured_output and structured_output.get("error"):
|
| 509 |
+
execution_log.append(f"Resultado Estructurado (error): {json.dumps(structured_output)[:200]}...")
|
| 510 |
+
|
| 511 |
+
# Actualizar history
|
| 512 |
+
history.append({"action": process_name, "status": structured_output.get("status", "unknown"), "key": result_key})
|
| 513 |
+
|
| 514 |
+
# Opcional: Actualizar current_text si el proceso es transformador (ej: resumen, traducción)
|
| 515 |
+
# Esto permite a los siguientes pasos operar sobre el texto transformado
|
| 516 |
+
# Heurística simple: Si el resultado contiene 'summary' o 'translation', actualizamos current_text
|
| 517 |
+
if structured_output and structured_output.get("status") == "success":
|
| 518 |
+
if "summary" in structured_output and structured_output["summary"]:
|
| 519 |
+
current_text = structured_output["summary"]
|
| 520 |
+
logger.info("current_text actualizado a resumen.")
|
| 521 |
+
elif "translation" in structured_output and structured_output["translation"]:
|
| 522 |
+
current_text = structured_output["translation"]
|
| 523 |
+
logger.info("current_text actualizado a traducción.")
|
| 524 |
+
# Podrías añadir más reglas aquí
|
| 525 |
+
|
| 526 |
+
else:
|
| 527 |
+
msg = f"⚠️ La IA decidió un proceso no válido o no disponible: '{process_name}'. Deteniendo ciclos dinámicos."
|
| 528 |
+
execution_log.append(msg)
|
| 529 |
+
logger.warning(msg)
|
| 530 |
+
# Añadir error a resultados acumulados
|
| 531 |
+
cumulative_results[f"decision_error_{len(history)+1}"] = {"error": msg, "decision_data": decision_data, "status": "error"}
|
| 532 |
+
history.append({"action": process_name, "status": "invalid_decision", "key": f"decision_error_{len(history)+1}"})
|
| 533 |
+
break # Detener si la IA decide algo inválido
|
| 534 |
+
|
| 535 |
+
except json.JSONDecodeError as e:
|
| 536 |
+
msg = f"❌ Error al parsear JSON de decisión de la IA: {e}. Output crudo: {decision_text[:300]}..."
|
| 537 |
+
execution_log.append(msg)
|
| 538 |
+
logger.error(msg)
|
| 539 |
+
cumulative_results[f"json_error_{len(history)+1}"] = {"error": msg, "raw_output": decision_text, "status": "error"}
|
| 540 |
+
history.append({"action": "parse_error", "status": "error", "key": f"json_error_{len(history)+1}"})
|
| 541 |
+
break # Detener si falla el parsing de la decisión
|
| 542 |
+
|
| 543 |
+
except Exception as e:
|
| 544 |
+
msg = f"❌ Error inesperado durante el ciclo cognitivo: {e}"
|
| 545 |
+
execution_log.append(msg)
|
| 546 |
+
logger.error(msg)
|
| 547 |
+
cumulative_results[f"cycle_error_{len(history)+1}"] = {"error": msg, "status": "error"}
|
| 548 |
+
history.append({"action": "cycle_error", "status": "error", "key": f"cycle_error_{len(history)+1}"})
|
| 549 |
+
break # Detener en caso de excepción inesperada
|
| 550 |
+
|
| 551 |
+
# Pausa visual y para evitar saturar recursos rápidamente
|
| 552 |
+
time.sleep(0.5)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
# --- Paso Final: Síntesis del Reporte ---
|
| 556 |
+
progress_percentage = (max_cycles + 1) / (max_cycles + 1) # 100%
|
| 557 |
+
progress(progress_percentage, desc="✨ Sintetizando Reporte Final...")
|
| 558 |
+
execution_log.append("\n--- Sintetizando Reporte Final ---")
|
| 559 |
+
logger.info("Running final synthesis.")
|
| 560 |
+
|
| 561 |
+
# El paso de síntesis toma el initial_text y todos los cumulative_results
|
| 562 |
+
final_synthesis_human_readable, final_synthesis_structured_output = CognitiveProcesses.synthesize_report(
|
| 563 |
+
initial_text, # Usar texto original para la síntesis final
|
| 564 |
+
cumulative_results,
|
| 565 |
+
{"query": objective}, # Pasar el objetivo como query/contexto para la síntesis
|
| 566 |
+
objective # Pasar el objetivo
|
| 567 |
+
)
|
| 568 |
+
cumulative_results["final_synthesis"] = final_synthesis_structured_output
|
| 569 |
+
execution_log.append(final_synthesis_human_readable)
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
execution_log.append("\n--- SentientFlow Completado ---")
|
| 573 |
+
logger.info("SentientFlow process completed.")
|
| 574 |
+
|
| 575 |
+
# Devolver el reporte final (texto), todos los resultados estructurados y el log completo
|
| 576 |
+
final_report_text = final_synthesis_structured_output.get("final_report_text", "Reporte final no generado.")
|
| 577 |
+
all_structured_results_str = json.dumps(cumulative_results, indent=2)
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
return final_report_text, all_structured_results_str, "\n".join(execution_log)
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# --- Cargar modelos y definir procesos disponibles al inicio de la app ---
|
| 584 |
+
try:
|
| 585 |
+
load_ai_models()
|
| 586 |
+
# Mapear nombres de procesos a funciones SOLO SI EL MODELO NECESARIO FUE CARGADO
|
| 587 |
+
for name, func in PROCESS_FUNCTION_MAP.items():
|
| 588 |
+
# Esto requiere saber qué modelo/pipeline necesita cada función.
|
| 589 |
+
# HARDCODED MAPPING - THIS IS BRITTLE! Needs a better system in a real app.
|
| 590 |
+
required_model_alias = None
|
| 591 |
+
if func == CognitiveProcesses.summarize: required_model_alias = "summarization_pipeline"
|
| 592 |
+
elif func == CognitiveProcesses.translate: required_model_alias = "translation_en_es_pipeline" # Asumimos en_es como base
|
| 593 |
+
elif func == CognitiveProcesses.analyze_sentiment: required_model_alias = "sentiment_analysis_pipeline"
|
| 594 |
+
elif func == CognitiveProcesses.extract_entities: required_model_alias = "ner_extraction_pipeline"
|
| 595 |
+
elif func == CognitiveProcesses.extract_information or func == CognitiveProcesses.synthesize_report: required_model_alias = "text2text_pipeline"
|
| 596 |
+
# Añadir mapeos para nuevos procesos
|
| 597 |
+
|
| 598 |
+
if required_model_alias and loaded_pipelines.get(required_model_alias) is not None:
|
| 599 |
+
available_cognitive_processes[name] = func
|
| 600 |
+
logger.info(f"Proceso '{name}' DISPONIBLE.")
|
| 601 |
+
elif required_model_alias:
|
| 602 |
+
logger.warning(f"Proceso '{name}' NO DISPONIBLE: Modelo '{required_model_alias}' no cargado.")
|
| 603 |
+
else:
|
| 604 |
+
logger.warning(f"Proceso '{name}' NO MAPEADO a un modelo requerido. No disponible.")
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
# Actualizar la lista de procesos disponibles para el prompt del planner
|
| 608 |
+
AVAILABLE_PROCESSES = ", ".join([f"`{name}`" for name in available_cognitive_processes.keys()])
|
| 609 |
+
if "synthesize_report" in available_cognitive_processes:
|
| 610 |
+
# La síntesis final es un paso especial, la IA planificadora NO debe elegirla en ciclos intermedios.
|
| 611 |
+
# La removemos de la lista de procesos *elegibles* pero la mantenemos en el MAP.
|
| 612 |
+
# Esto es otra HEURÍSTICA. Un planner más inteligente sabría cuándo usarla.
|
| 613 |
+
eligible_processes_for_cycles = list(available_cognitive_processes.keys())
|
| 614 |
+
if "synthesize_report" in eligible_processes_for_cycles:
|
| 615 |
+
eligible_processes_for_cycles.remove("synthesize_report")
|
| 616 |
+
AVAILABLE_PROCESSES_FOR_PLANNER = ", ".join([f"`{name}`" for name in eligible_processes_for_cycles])
|
| 617 |
+
else:
|
| 618 |
+
AVAILABLE_PROCESSES_FOR_PLANNER = AVAILABLE_PROCESSES # Si no hay sintetizador, dejar todos los demás
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
except Exception as e:
|
| 622 |
+
logger.error(f"Fallo crítico en setup de procesos/modelos: {e}")
|
| 623 |
+
# Si esto falla, la app no funcionará
|
| 624 |
+
available_cognitive_processes = {} # Asegurar que está vacío
|
| 625 |
+
AVAILABLE_PROCESSES_FOR_PLANNER = ""
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
# --- Interfaz de Gradio ---
|
| 629 |
+
|
| 630 |
+
# Mensaje sobre modelos disponibles para la UI
|
| 631 |
+
available_tasks_list_str = ", ".join([f"**`{name}`**" for name in available_cognitive_processes.keys()])
|
| 632 |
+
if "synthesize_report" in available_cognitive_processes:
|
| 633 |
+
available_tasks_list_str += ", **`synthesize_report`** (paso final automático)" # Añadir síntesis como paso final
|
| 634 |
+
|
| 635 |
+
title = "✨ SentientFlow: The Dynamic Cognition Engine ✨"
|
| 636 |
+
description = f"""
|
| 637 |
+
**Automatización Cognitiva de Próximo Nivel: Describe un Objetivo, No Pasos.**
|
| 638 |
+
|
| 639 |
+
SentientFlow no sigue un plan fijo. Le das un **objetivo general** sobre tu texto, y la IA usa su **cognición dinámica** para decidir paso a paso (en ciclos) qué análisis o transformación realizar, basándose en lo que ya ha descubierto.
|
| 640 |
+
|
| 641 |
+
**Objetivo:** Describe **qué quieres lograr** (ej: "Entender las opiniones clave", "Extraer datos importantes y resumirlos").
|
| 642 |
+
|
| 643 |
+
**Procesos Cognitivos Disponibles (Modelos Cargados):**
|
| 644 |
+
{available_tasks_list_str if available_cognitive_processes else "**❌ Ningún proceso cognitivo disponible.** Revisa los logs de carga del Space. Los modelos de IA no pudieron cargarse."}
|
| 645 |
+
|
| 646 |
+
*(La disponibilidad de los procesos depende de si los modelos de IA pudieron cargarse correctamente en este Hugging Face Space gratuito. Si un proceso no aparece aquí, su modelo no está disponible.)*
|
| 647 |
+
|
| 648 |
+
**Ejemplos de Objetivos:**
|
| 649 |
+
- `Analizar este artículo y extraer la información más importante, luego resumirlo`
|
| 650 |
+
- `Entender el sentimiento general de estas opiniones y quiénes son los principales actores mencionados`
|
| 651 |
+
- `Traducir este párrafo al español y luego generar preguntas sobre su contenido`
|
| 652 |
+
- `Dame una síntesis completa de este documento, incluyendo sus puntos clave y el sentimiento`
|
| 653 |
+
|
| 654 |
+
*SentientFlow realizará varios ciclos de análisis y síntesis para intentar cumplir tu objetivo. El proceso se detendrá después de un número fijo de ciclos o si la IA decide que ha terminado. Funciona 100% dentro del Space, sin acceso externo.*
|
| 655 |
+
"""
|
| 656 |
+
# Mensaje de advertencia si no hay modelos de IA cargados
|
| 657 |
+
if not available_cognitive_processes:
|
| 658 |
+
description += "\n\n**🚨 ADVERTENCIA: Ningún modelo de IA se cargó correctamente. SentientFlow no podrá realizar ninguna tarea cognitiva.** Revisa la pestaña 'Logs' de tu Space para ver los errores de carga (posiblemente por falta de memoria)."
|
| 659 |
+
# Deshabilitar el botón si no hay modelos
|
| 660 |
+
interface_enabled = False
|
| 661 |
+
else:
|
| 662 |
+
interface_enabled = True
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
gr.Interface(
|
| 666 |
+
fn=run_dynamic_cognition,
|
| 667 |
+
inputs=[
|
| 668 |
+
gr.Textbox(label="Describe tu Objetivo Cognitivo:", placeholder="Ej: 'Analizar opiniones clave y sentimiento'", lines=3),
|
| 669 |
+
gr.Textbox(label="Texto de entrada:", placeholder="Pega el texto aquí...", lines=10)
|
| 670 |
+
],
|
| 671 |
+
outputs=[
|
| 672 |
+
gr.Textbox(label="✨ Reporte Final Sintetizado por la IA:", interactive=False, lines=7),
|
| 673 |
+
gr.Textbox(label="🧠 Resultados Estructurados Acumulados:", interactive=False, lines=10, visible=True, render=True, show_copy_button=True), # Mostrar JSON, permitir copiar
|
| 674 |
+
gr.Textbox(label="📈 Log Detallado (Ciclos Cognitivos y Pensamientos IA):", interactive=False, lines=15)
|
| 675 |
+
],
|
| 676 |
+
title=title,
|
| 677 |
+
description=description,
|
| 678 |
+
allow_flagging="never", # Disable flagging on Hugging Face Spaces
|
| 679 |
+
examples=[
|
| 680 |
+
["Analizar este artículo y extraer la información más importante, luego resumirlo", "Artificial intelligence (AI) is a rapidly evolving field with applications in various industries, including healthcare, finance, and transportation. Leading research institutions like Stanford and MIT are at the forefront of AI development. Experts like Andrew Ng predict significant advancements in the coming decade. However, concerns about ethical implications and job displacement are also growing."],
|
| 681 |
+
["Entender el sentimiento general de estas opiniones y quiénes son los principales actores mencionados", "Customer A: The new phone is great! Fast and good camera. Customer B: Software update broke everything, very frustrated. Customer C: The company's support team was useless."],
|
| 682 |
+
["Traducir este párrafo al español y luego generar preguntas sobre su contenido", "Climate change is a global emergency that goes beyond national borders. It is an issue that requires coordinated solutions at all levels and international cooperation to help countries transition to a low-carbon economy."]
|
| 683 |
+
],
|
| 684 |
+
# Deshabilitar el botón si no hay modelos cargados
|
| 685 |
+
submit_button="🚀 Iniciar Flujo Cognitivo" if interface_enabled else "Modelos IA No Disponibles",
|
| 686 |
+
# state=[available_cognitive_processes] # Podríamos pasar el estado de modelos disponibles si fuera necesario en fn
|
| 687 |
+
).queue().launch()
|
| 688 |
+
|
| 689 |
+
logger.info("Interfaz Gradio lanzada.")
|