Lukeetah commited on
Commit
0c5042d
·
verified ·
1 Parent(s): 923ffd9

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +689 -0
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.")