File size: 24,850 Bytes
43203c1
b9fbc5e
 
 
 
 
 
 
 
 
43203c1
 
d1eb779
d1ce30b
f45845d
631a418
f45845d
d1ce30b
b9fbc5e
9b58814
d1ce30b
 
d7a5a15
b9fbc5e
d1ce30b
14d782a
b9fbc5e
 
 
 
 
 
 
636020c
7993e54
 
 
 
8a21a90
5c5c390
11c99e4
5c5c390
 
636020c
1e200b9
7993e54
b9fbc5e
7993e54
 
6673668
1e200b9
636020c
 
 
7993e54
 
da38434
1e200b9
 
72fd6ef
 
7993e54
72fd6ef
14d782a
636020c
b9fbc5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e200b9
be9c158
7993e54
11c99e4
0ee0f76
 
 
d873e9a
0ee0f76
6673668
b9fbc5e
d873e9a
11c99e4
b9fbc5e
d873e9a
f10d43c
 
d873e9a
b9fbc5e
d873e9a
0ee0f76
72fd6ef
 
d873e9a
 
 
 
 
 
 
11c99e4
72fd6ef
f10d43c
0ee0f76
f10d43c
d873e9a
f10d43c
 
b9fbc5e
72fd6ef
f10d43c
938da61
be9c158
f10d43c
 
b9fbc5e
 
f10d43c
b9fbc5e
 
 
 
 
 
 
 
 
 
 
 
 
 
be9c158
 
 
 
 
 
636020c
 
b9fbc5e
 
 
d1ce30b
b9fbc5e
 
 
d1ce30b
8a21a90
1e200b9
 
d1ce30b
1e200b9
 
 
7993e54
cb5c52b
b9fbc5e
7993e54
11c99e4
72fd6ef
 
11c99e4
72fd6ef
 
11c99e4
72fd6ef
1e200b9
b9fbc5e
938da61
7993e54
1e200b9
72fd6ef
7993e54
b9fbc5e
7993e54
2328ff4
b9fbc5e
72fd6ef
7993e54
b9fbc5e
 
 
72fd6ef
be9c158
7993e54
b9fbc5e
 
 
 
 
7993e54
b9fbc5e
7993e54
 
72fd6ef
7993e54
b9fbc5e
d1ce30b
72fd6ef
b9fbc5e
72fd6ef
 
 
 
b9fbc5e
72fd6ef
 
d1ce30b
7993e54
d1ce30b
f4c235c
dc41149
740148a
b9fbc5e
220eb0b
1e200b9
2328ff4
d1ce30b
cfa82b8
7993e54
72fd6ef
f4c235c
 
a668668
1e200b9
740148a
7993e54
72fd6ef
 
cb5c52b
b9fbc5e
d7a5a15
6673668
d7a5a15
72fd6ef
1e200b9
a668668
 
7993e54
 
a668668
7993e54
a668668
636020c
f4c235c
 
8a21a90
636020c
43203c1
d7a5a15
7993e54
b9fbc5e
7993e54
d873e9a
494ef15
 
7993e54
b9fbc5e
 
 
6673668
b9fbc5e
636020c
 
7993e54
 
2328ff4
7993e54
b9fbc5e
 
 
 
 
 
 
 
be9c158
2328ff4
 
 
 
b9fbc5e
2328ff4
 
 
494ef15
7993e54
b9fbc5e
494ef15
636020c
6673668
b9fbc5e
636020c
b9fbc5e
 
 
740148a
 
1e200b9
 
7993e54
 
 
72fd6ef
7993e54
 
 
b9fbc5e
7993e54
 
a668668
7993e54
740148a
43ab0e1
740148a
f83e05d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""
CÓDIGO COMPLETO Y CORREGIDO - VERSIÓN 7.8 (Agente de Instrucciones de Lenguaje Natural)
- MEJORA MAYOR: Se ha creado un nuevo `InstructionParsingAgent` que interpreta las instrucciones
  en lenguaje natural del usuario desde el cuadro de "Especificaciones Adicionales".
- FUNCIONALIDAD AVANZADA: El usuario puede especificar qué métricas usar (R2, RMSE, o ambas) y
  cuántos "Top N" modelos seleccionar, simplemente escribiéndolo.
- MODELSELECTIONAGENT MEJORADO: La lógica del "Plan B" ahora es dinámica y se basa en las
  instrucciones parseadas, calculando un score combinado si se especifican múltiples métricas.
- UI SIMPLIFICADA: Se han eliminado los controles estáticos de ranking, reemplazados por el
  cuadro de texto de instrucciones, haciendo la interfaz más limpia y potente.
"""

import gradio as gr
from gradio_client import Client, handle_file
import pandas as pd
import json
import tempfile
import os
import re
from datetime import datetime
import plotly.graph_objects as go
import logging
import numpy as np
from smolagents import CodeAgent, InferenceClientModel

# --- CONFIGURACIÓN Y CLIENTES ---
logging.basicConfig(level=logging.INFO); logger = logging.getLogger(__name__)

# --- INICIALIZACIÓN DE MODELO PARA AGENTES ---
try:
    hf_engine = InferenceClientModel(model_id="deepseek-ai/DeepSeek-V2-Lite-Instruct")
    logger.info("✅ Modelo de lenguaje (DeepSeek-V2-Lite) inicializado para agentes.")
except Exception: hf_engine = None; logger.error("❌ No se pudo inicializar el modelo de lenguaje para agentes.")

try: biotech_client = Client("C2MV/BiotechU4"); logger.info("✅ Cliente BiotechU4 inicializado.")
except: biotech_client = None
try: analysis_client = Client("C2MV/Project-HF-2025-2"); logger.info("✅ Cliente Project-HF-2025-2 inicializado.")
except: analysis_client = None

# ============================================================================
# 🤖 SISTEMA DE AGENTES (LÓGICA ACTUALIZADA)
# ============================================================================

class LoggingAgent:
    def __init__(self): self.log_entries, self.start_time = [], datetime.now(); logger.info("🕵️ LoggingAgent activado.")
    def register(self, agent_name, action, details=""):
        entry = f"**{datetime.now().strftime('%H:%M:%S')} | {agent_name}:** {action}"; self.log_entries.append(entry + (f"\n> *Detalles: {details}*" if details else ""))
    def get_report(self):
        if not self.log_entries: return "### 🕵️ Informe de Actividad\n\nNo se registraron actividades."
        return "### 🕵️ Informe de Actividad de Agentes\n\n---\n\n" + "\n\n---\n\n".join(self.log_entries) + f"\n\n---\n\n**Tiempo total: {(datetime.now() - self.start_time).total_seconds():.2f} s.**"
    def clear(self): self.log_entries, self.start_time = [], datetime.now()


class StructureValidationAgent:
    def __init__(self, log_agent: LoggingAgent): self.log_agent = log_agent
    def validate(self, file_obj):
        try:
            if file_obj.name.endswith(('.xls', '.xlsx')): df = pd.read_excel(file_obj.name, header=1)
            else: df = pd.read_csv(file_obj.name)
            if df.empty: return False, "Validación fallida: El archivo está vacío."
        except Exception as e: return False, f"Error crítico al leer el archivo: {e}"
        self.log_agent.register("StructureValidationAgent", "Validación de formato de archivo superada.")
        return True, "Formato de archivo básico validado."


class InstructionParsingAgent:
    """Agente que convierte el lenguaje natural del usuario en un plan de acción estructurado."""
    def __init__(self, log_agent: LoggingAgent, llm_engine):
        self.log_agent = log_agent
        self.agent = CodeAgent(tools=[], model=llm_engine) if llm_engine else None

    def parse(self, text: str):
        default_instructions = {'metrics': ['R2'], 'top_n': 3}
        if not self.agent:
            self.log_agent.register("InstructionParsingAgent", "LLM no disponible, usando defaults.")
            return default_instructions

        prompt = f"""
        Analyze the user's instruction for a data analysis task. Extract the metrics they want to use for ranking and the number of top models to select.
        The possible metrics are "R2", "RMSE".
        Your output MUST be ONLY a valid JSON object with two keys: "metrics" (a list of strings) and "top_n" (an integer).

        - If the user mentions "R2" or "R-cuadrado", include "R2" in the metrics list.
        - If the user mentions "RMSE", include "RMSE" in the metrics list.
        - If the user mentions a number like "top 3", "los 2 mejores", or just a digit, set "top_n" to that number.
        - If no metrics are mentioned, default to ["R2"].
        - If no number is mentioned, default to 3.

        Example 1: "Usa el promedio de R2 y RMSE y elige el top 2" -> {{"metrics": ["R2", "RMSE"], "top_n": 2}}
        Example 2: "dame los 3 mejores modelos segun el menor RMSE" -> {{"metrics": ["RMSE"], "top_n": 3}}
        Example 3: "el mejor R2" -> {{"metrics": ["R2"], "top_n": 1}}
        Example 4: "analizar los datos" -> {{"metrics": ["R2"], "top_n": 3}}

        User instruction: "{text}"
        JSON Output:
        """
        try:
            response_str = self.agent.run(prompt)
            json_str = response_str[response_str.find('{'):response_str.rfind('}')+1]
            instructions = json.loads(json_str)
            # Validar que el formato es correcto
            if 'metrics' not in instructions or 'top_n' not in instructions:
                raise ValueError("JSON de salida no contiene las claves esperadas.")
            self.log_agent.register("InstructionParsingAgent", "Instrucciones del usuario parseadas con éxito.")
            return instructions
        except Exception as e:
            self.log_agent.register("InstructionParsingAgent", "Error parseando instrucciones, usando defaults.", f"Error: {e}")
            return default_instructions


class ModelSelectionAgent:
    """Agente 3 (CON FALLBACK CONFIGURABLE): Identifica los mejores modelos, con un plan B personalizable."""
    def __init__(self, log_agent: LoggingAgent): self.log_agent = log_agent

    def _find_column(self, df_columns, possible_names):
        for name in possible_names:
            for col in df_columns:
                if str(col).lower() == name.lower(): return col
        return None

    def identify_best_models(self, results_df, component, r2_threshold, rmse_threshold, instructions: dict):
        self.log_agent.register("ModelSelectionAgent", f"Iniciando identificación para: '{component}'.")
        
        # 1. Normalizar columna del Modelo
        model_col = self._find_column(results_df.columns, ['model', 'modelo'])
        if not model_col: return [], "Error: No se encontró la columna de nombres de modelos ('Model')."
        df_norm = results_df.rename(columns={model_col: 'Model'})

        # 2. Identificar columnas de métricas
        r2_target_col, rmse_target_col = None, None
        if component != 'all':
            r2_target_col = self._find_column(df_norm.columns, [f'r2_{component}'])
            rmse_target_col = self._find_column(df_norm.columns, [f'rmse_{component}'])
        else:
            metric_cols_r2 = [c for c in df_norm.columns if 'r2_' in str(c).lower()]
            metric_cols_rmse = [c for c in df_norm.columns if 'rmse_' in str(c).lower()]
            if metric_cols_r2 and metric_cols_rmse:
                r2_target_col, rmse_target_col = 'R2_Avg', 'RMSE_Avg'
                df_norm[r2_target_col] = df_norm[metric_cols_r2].mean(axis=1, skipna=True)
                df_norm[rmse_target_col] = df_norm[metric_cols_rmse].mean(axis=1, skipna=True)

        if not r2_target_col or not rmse_target_col or r2_target_col not in df_norm.columns or rmse_target_col not in df_norm.columns:
            return [], f"Error: No se encontraron las métricas para el componente '{component}'."

        # 3. Agrupar por modelo y calcular métrica de rendimiento promedio
        model_performance = df_norm.groupby('Model').agg({r2_target_col: 'mean', rmse_target_col: 'mean'}).reset_index()
        
        # 4. Intento 1: Filtrado Estricto
        good_models_df = model_performance[(model_performance[r2_target_col] >= r2_threshold) & (model_performance[rmse_target_col] <= rmse_threshold)]

        if not good_models_df.empty:
            best_models_list = sorted([str(model).lower() for model in good_models_df['Model'].tolist()])
            reasoning = f"Agente identificó **{len(best_models_list)}** modelo(s) que cumplen tus criterios: `{', '.join(best_models_list)}`."
            return best_models_list, reasoning
        else:
            # 5. Intento 2: Plan B - Ranking Estratégico basado en instrucciones
            self.log_agent.register("ModelSelectionAgent", "Filtro primario falló. Activando fallback: 'Ranking por Instrucciones'.", f"Plan: {instructions}")
            
            use_r2 = 'R2' in instructions['metrics']
            use_rmse = 'RMSE' in instructions['metrics']
            top_n = instructions['top_n']

            # Calcular el score de rendimiento
            if use_r2 and use_rmse:
                model_performance['Score'] = model_performance[r2_target_col] / (model_performance[rmse_target_col] + 1e-9)
                sort_col, ascending, metric_name = 'Score', False, "R²/RMSE combinado"
            elif use_rmse:
                sort_col, ascending, metric_name = rmse_target_col, True, "RMSE"
            else: # Por defecto R2
                sort_col, ascending, metric_name = r2_target_col, False, "R²"

            sorted_performance = model_performance.sort_values(by=sort_col, ascending=ascending)
            top_n_df = sorted_performance.head(top_n)
                
            best_models_list = sorted([str(model).lower() for model in top_n_df['Model'].tolist()])
            reasoning = (f"**Advertencia:** Ningún modelo cumplió con los criterios iniciales.\n\n"
                         f"Como plan B, el agente ha seleccionado los **Top {len(best_models_list)}** modelos con el mejor **{metric_name} promedio**: `{', '.join(best_models_list)}`.")
            return best_models_list, reasoning

# --- INICIALIZACIÓN DE AGENTES GLOBALES ---
log_agent = LoggingAgent(); validation_agent = StructureValidationAgent(log_agent)
instruction_parser_agent = InstructionParsingAgent(log_agent, hf_engine)
model_selection_agent = ModelSelectionAgent(log_agent)

# --- FUNCIONES DEL PIPELINE ---
def create_dummy_plot(title="Esperando resultados..."):
    fig = go.Figure(go.Scatter(x=[], y=[])); fig.update_layout(title=title, template="plotly_white", height=500, annotations=[dict(text="Sube un archivo y ejecuta", showarrow=False)])
    return fig

def detect_experiments(file_obj):
    if not file_obj: return gr.update(choices=[], value=[])
    try:
        df_first_row = pd.read_excel(file_obj.name, header=None, nrows=1)
        exp_names = [str(name).strip() for name in df_first_row.iloc[0].dropna().tolist()]
        return gr.update(choices=exp_names, value=exp_names, interactive=True)
    except Exception as e: return gr.update(choices=[], value=[], interactive=False, placeholder=f"Error: {e}")

# ... (ETAPA 1: run_base_analysis - sin cambios) ...
def run_base_analysis(file, models, exp_names_selected, component, use_de, maxfev, progress=gr.Progress()):
    log_agent.clear(); progress(0, desc="🚀 Iniciando Análisis Base...")
    if not file or not models or not exp_names_selected:
        return create_dummy_plot(), None, "❌ Por favor, sube un archivo y selecciona modelos/experimentos.", gr.update(interactive=False), {}, None, None, log_agent.get_report()
    log_agent.register("Pipeline (Etapa 1)", "Iniciando Análisis Base."); progress(0.2, desc="Validando archivo...")
    is_valid, msg = validation_agent.validate(file)
    if not is_valid: return create_dummy_plot(), None, msg, gr.update(interactive=False), {}, None, None, log_agent.get_report()
    progress(0.5, desc="Ejecutando análisis biotecnológico...");
    if not biotech_client: return create_dummy_plot(), None, "❌ Cliente BiotechU4 no disponible.", gr.update(interactive=False), {}, None, None, log_agent.get_report()
    try:
        exp_names_str = ",".join(exp_names_selected); models_lower = [str(m).lower() for m in models]
        plot_info, df_data, status = biotech_client.predict(file=handle_file(file.name), models=models_lower, component=component, use_de=use_de, maxfev=maxfev, exp_names=exp_names_str, api_name="/run_analysis_wrapper")
        if "Error" in status: raise Exception(status)
    except Exception as e:
        return create_dummy_plot(), None, f"❌ Error en Análisis Base: {e}", gr.update(interactive=False), {}, None, None, log_agent.get_report()
    progress(1, desc="🎉 Análisis Base Completado")
    final_status = "✅ Análisis Base completado. \n➡️ Ahora puedes aplicar el filtro de IA y generar el informe final."
    results_df_obj = {'data': df_data['data'], 'headers': df_data['headers']}
    fig = go.Figure(json.loads(plot_info['plot'])) if plot_info and 'plot' in plot_info else create_dummy_plot()
    original_params = {'exp_names': exp_names_selected, 'component': component, 'use_de': use_de, 'maxfev': maxfev}
    return fig, df_data, final_status, gr.update(interactive=True), results_df_obj, file.name, original_params, log_agent.get_report()

# --- ETAPA 2: REFINAMIENTO Y REPORTE IA (ACTUALIZADA) ---
def refine_and_generate_report(baseline_results, file_path, original_params, r2_threshold, rmse_threshold, instructions_text, ia_model, detail_level, language, max_output_tokens, use_personal_key, personal_api_key, progress=gr.Progress()):
    progress(0, desc="🚀 Iniciando Refinamiento con IA..."); log_agent.register("Pipeline (Etapa 2)", "Iniciando Refinamiento.")
    if not baseline_results or not file_path or not original_params:
        return gr.update(), None, None, None, "❌ No hay resultados base para refinar.", None, log_agent.get_report()
    
    progress(0.1, desc="Agente de Parseo interpretando instrucciones...")
    instructions = instruction_parser_agent.parse(instructions_text)
    log_agent.register("InstructionParsingAgent", "Instrucciones interpretadas.", f"Plan: {instructions}")

    progress(0.2, desc="Agente de Selección identificando mejores modelos...")
    results_df = pd.DataFrame(baseline_results['data'], columns=baseline_results['headers'])
    best_models, reasoning = model_selection_agent.identify_best_models(results_df, original_params['component'], r2_threshold, rmse_threshold, instructions)
    
    if not best_models:
        return gr.update(), baseline_results, None, None, f"🤖 Análisis del Agente:\n{reasoning}", None, log_agent.get_report()
    
    progress(0.4, desc="Re-ejecutando análisis con los mejores modelos...");
    try:
        exp_names_str = ",".join(original_params['exp_names'])
        final_plot_info, final_df_data, final_status = biotech_client.predict(file=handle_file(file_path), models=best_models, component=original_params['component'], use_de=original_params['use_de'], maxfev=original_params['maxfev'], exp_names=exp_names_str, api_name="/run_analysis_wrapper")
        if "Error" in final_status: raise Exception(final_status)
    except Exception as e:
        return gr.update(), None, None, None, f"❌ Error en el re-análisis final: {e}", None, log_agent.get_report()

    progress(0.6, desc="Generando informe IA..."); temp_csv_file = None
    try:
        final_results_df = pd.DataFrame(final_df_data['data'], columns=final_df_data['headers'])
        with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv', delete=False, encoding='utf-8') as temp_f:
            final_results_df.to_csv(temp_f.name, index=False); temp_csv_file = temp_f.name
        current_analysis_client = analysis_client
        if use_personal_key and personal_api_key: current_analysis_client = Client("C2MV/Project-HF-2025-2", hf_token=personal_api_key)
        chunk_update_dict = current_analysis_client.predict(files=[handle_file(temp_csv_file)], api_name="/update_chunk_column_selector")
        selected_chunk_column = chunk_update_dict['choices'][0][0]
        result = current_analysis_client.predict(files=[handle_file(temp_csv_file)], chunk_column=selected_chunk_column, qwen_model=ia_model, detail_level=detail_level, language=language, additional_specs="", max_output_tokens=max_output_tokens, api_name="/process_files_and_analyze")
        _, analysis_report, implementation_code, token_usage = result
    except Exception as e:
        return gr.update(), final_df_data, None, None, f"❌ Error generando informe IA: {e}", None, log_agent.get_report()
    finally:
        if temp_csv_file and os.path.exists(temp_csv_file): os.remove(temp_csv_file)
        
    progress(0.9, desc="Finalizando..."); final_report_path = None
    if analysis_report:
        export_dir = "exported_reports"; os.makedirs(export_dir, exist_ok=True)
        final_report_path = os.path.join(export_dir, f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md")
        with open(final_report_path, 'w', encoding='utf-8') as f: f.write(analysis_report)
    
    final_status = f"✅ Refinamiento y reporte completados.\n{reasoning}\nInforme IA generado con {token_usage}."
    final_fig = go.Figure(json.loads(final_plot_info['plot'])) if final_plot_info and 'plot' in final_plot_info else create_dummy_plot()
    return final_fig, final_df_data, analysis_report, implementation_code, final_status, final_report_path, log_agent.get_report()

# ... (create_dummy_excel_file y constantes sin cambios) ...
def create_dummy_excel_file():
    examples_dir = "examples"; os.makedirs(examples_dir, exist_ok=True); file_path = os.path.join(examples_dir, "archivo.xlsx")
    if not os.path.exists(file_path):
        exp_names = ['CN 20_1', 'CN 20_2', 'CN 30_1', 'CN 40_1']; writer = pd.ExcelWriter(file_path, engine='xlsxwriter'); worksheet = writer.book.add_worksheet('Datos'); writer.sheets['Datos'] = worksheet
        for i, name in enumerate(exp_names): worksheet.write(0, i * 4, name)
        start_col = 0
        for _ in exp_names:
            time = np.arange(0, 11, 2); biomass = 0.2 + (np.random.rand() * 20) / (1 + np.exp(4 - 0.5 * time)) + np.random.rand(len(time)) * 0.2
            substrate = 10 * np.exp(-0.2 * time) + np.random.rand(len(time)) * 0.3; product = 1 * (1 - np.exp(-0.3 * time)) + np.random.rand(len(time)) * 0.1
            df = pd.DataFrame({'Tiempo': time, 'Biomasa': biomass, 'Sustrato': substrate, 'Producto': product})
            df.to_excel(writer, sheet_name='Datos', startrow=1, startcol=start_col, index=False); start_col += 4
        writer.close()

BIOTECH_MODELS = ['logistic', 'gompertz', 'moser', 'baranyi', 'monod', 'contois', 'andrews', 'tessier', 'richards', 'stannard', 'huang']
IA_MODELS = ["deepseek-ai/DeepSeek-V3-0324"]
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="indigo", neutral_hue="slate")

if __name__ == "__main__":
    create_dummy_excel_file()
    with gr.Blocks(theme=theme, title="BioTech Analysis & Report Generator") as demo:
        gr.Markdown("# 🧬 BioTech Analysis & Report Generator v7.8")
        gr.Markdown("### Un pipeline inteligente de dos etapas: Análisis Base y Refinamiento con IA.")
        baseline_results_state = gr.State(value=None); file_path_state = gr.State(value=None); original_params_state = gr.State(value=None)
        with gr.Row():
            with gr.Column(scale=1):
                gr.Markdown("### 1. Carga y Configuración del Análisis Base")
                file_input = gr.File(label="📁 Archivo de Datos", file_types=[".xlsx", ".xls"]); gr.Examples(examples=["examples/archivo.xlsx"], inputs=[file_input])
                exp_names_input = gr.CheckboxGroup(label="🔬 Experimentos a Analizar", interactive=False)
                models_input = gr.CheckboxGroup(choices=BIOTECH_MODELS, value=BIOTECH_MODELS, label="📊 Modelos a Evaluar")
                component_input = gr.Dropdown(['all', 'biomass', 'substrate', 'product'], value='all', label="📈 Componente a Analizar/Filtrar")
                with gr.Accordion("Parámetros Avanzados", open=False):
                    use_de_input = gr.Checkbox(label="🧮 Usar Evolución Diferencial", value=False)
                    maxfev_input = gr.Slider(label="🔄 Máx. Iteraciones", minimum=10000, maximum=100000, value=50000, step=1000)
                run_base_analysis_btn = gr.Button("1. Ejecutar Análisis Base", variant="secondary")

                with gr.Group():
                    gr.Markdown("### 2. Refinamiento con IA")
                    gr.Markdown("#### Criterios de Selección Primarios (Filtro Estricto)")
                    r2_threshold_slider = gr.Slider(minimum=0.0, maximum=0.99, value=0.9, step=0.01, label="R² Mínimo")
                    rmse_threshold_input = gr.Number(value=0.5, label="RMSE Máximo")
                    
                    # --- NUEVO INPUT DE INSTRUCCIONES ---
                    additional_specs_input = gr.Textbox(label="📝 Instrucciones para Selección Avanzada (Plan B)",
                                                        placeholder="Ej: Usa R2 y RMSE y dame el top 2",
                                                        info="Si ningún modelo cumple los criterios de arriba, el agente seguirá estas instrucciones.")
                    
                    with gr.Accordion("Parámetros del Informe de IA Final", open=False):
                        ia_model_input = gr.Dropdown(choices=IA_MODELS, value=IA_MODELS[0], label="🤖 Modelo de IA para Informe")
                        detail_level_input = gr.Radio(['detailed', 'summarized'], value='detailed', label="📋 Nivel de Detalle")
                        language_input = gr.Dropdown(['es', 'en'], value='es', label="🌐 Idioma")
                        max_output_tokens_input = gr.Slider(minimum=1000, maximum=32000, value=8000, step=100, label="🔢 Máx. Tokens")
                        use_personal_key_input = gr.Checkbox(label="Usar Token HF Personal", value=False)
                        personal_api_key_input = gr.Textbox(label="Token HF", type="password", visible=False)
                    refine_with_ia_btn = gr.Button("2. 🤖 Aplicar Filtro y Generar Informe IA", variant="primary", interactive=False)
            with gr.Column(scale=2):
                gr.Markdown("### 3. Resultados")
                status_output = gr.Textbox(label="📊 Registro de Estado", lines=5, interactive=False)
                with gr.Tabs():
                    with gr.TabItem("📊 Visualización"): plot_output = gr.Plot()
                    with gr.TabItem("📋 Tabla de Modelado"): table_output = gr.Dataframe()
                    with gr.TabItem("📝 Informe IA"): analysis_output = gr.Markdown("El informe aparecerá aquí.")
                    with gr.TabItem("💻 Código"): code_output = gr.Code(language="python")
                    with gr.TabItem("🕵️ Registro de Agentes"): agent_log_output = gr.Markdown()
                download_link_markdown = gr.Markdown("*El enlace de descarga aparecerá aquí.*")
                report_output = gr.File(label="📥 Descargar Informe", interactive=False)
                report_path_state = gr.State(value=None)
        
        file_input.upload(fn=detect_experiments, inputs=file_input, outputs=exp_names_input)
        use_personal_key_input.change(lambda x: gr.update(visible=x), inputs=use_personal_key_input, outputs=personal_api_key_input)
        run_base_analysis_btn.click(
            fn=run_base_analysis, 
            inputs=[file_input, models_input, exp_names_input, component_input, use_de_input, maxfev_input], 
            outputs=[plot_output, table_output, status_output, refine_with_ia_btn, baseline_results_state, file_path_state, original_params_state, agent_log_output]
        )
        refine_with_ia_btn.click(
            fn=refine_and_generate_report,
            inputs=[baseline_results_state, file_path_state, original_params_state, r2_threshold_slider, rmse_threshold_input, additional_specs_input, ia_model_input, detail_level_input, language_input, max_output_tokens_input, use_personal_key_input, personal_api_key_input],
            outputs=[plot_output, table_output, analysis_output, code_output, status_output, report_path_state, agent_log_output]
        )
        def update_dl_link(path):
            if path and os.path.exists(path): return f"**¡Informe listo!** 👉 [**Descargar '{os.path.basename(path)}'**](/file={path})"
            return "*No se generó ningún archivo para descargar.*"
        report_path_state.change(fn=update_dl_link, inputs=report_path_state, outputs=download_link_markdown)
            
    demo.launch(show_error=True, debug=True)