import json import re from typing import List, Dict, Tuple import csv, os, time import gradio as gr import matplotlib.pyplot as plt # ========================== # Config & estilos # ========================== DEFAULT_COLS = [ "Código", "Indicador", "Score (0–4)", "Entailment medio", "Evidencias (hipótesis)", "Descripción" ] CUSTOM_CSS = """ #app {max-width: 1200px; margin: 0 auto;} .badge { display:inline-block; padding:10px 14px; border-radius:12px; font-weight:700; background:linear-gradient(135deg,#1f6feb,#5ac8fa); color:white; box-shadow:0 6px 20px rgba(0,0,0,.2); } .card { background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); border-radius: 14px; padding: 14px; box-shadow: 0 8px 24px rgba(0,0,0,.18); } .small {font-size: 12px; opacity: .9;} """ # ========================== # Metadatos IPMA ICB4 4.4.5.x # ========================== INDICATOR_META = { "4.4.5.1": ("Iniciativa y ayuda proactiva", "Inicia acciones sin que se lo pidan; ofrece ayuda, anticipa y equilibra riesgos."), "4.4.5.2": ("Ownership y compromiso", "Asume responsabilidad; impulsa el proyecto; define/monitorea indicadores y mejora procesos."), "4.4.5.3": ("Dirección, coaching y mentoring", "Da dirección; coach/mentor al equipo; alinea visión, valores y objetivos."), "4.4.5.4": ("Poder e influencia", "Usa influencia adecuada; elige bien el canal; es percibido como líder por stakeholders."), "4.4.5.5": ("Decisiones", "Toma decisiones bajo incertidumbre; explica razones; revisa con nueva evidencia; comunica con claridad.") } # ========================== # Modelos (CPU Basic friendly) # ========================== _llm = None _llm_tok = None _gen = None _nli_cache: Dict[str, object] = {} # cache de pipelines NLI por model_id LLM_ID = "Qwen/Qwen2.5-0.5B-Instruct" # LLM pequeño multilingüe para extraer STAR # Selector de NLI con configuración asociada MODEL_CHOICES = { "Velocidad (MiniLM)": { "id": "MoritzLaurer/multilingual-MiniLMv2-L12-mnli-xnli", "calibrate": True, "thresholds": (0.70, 0.50, 0.30, 0.15) # 4,3,2,1 }, "Precisión (DeBERTa)": { "id": "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7", "calibrate": False, "thresholds": (0.80, 0.60, 0.40, 0.20) } } DEFAULT_MODEL_KEY = "Velocidad (MiniLM)" # por defecto en Spaces gratis STAR_PROMPT = """Eres evaluador ICB4. Toma el texto del candidato y devuélvelo en formato STAR como JSON válido con claves: "situation" (<=3 frases), "task" (<=2 frases), "action" (lista de viñetas, verbos de acción), "result" (lista de viñetas, resultados/indicadores/aprendizajes). Siempre responde SOLO con JSON válido y conciso en español, sin comentarios adicionales. TEXTO: {texto} """ HYP: Dict[str, List[str]] = { "4.4.5.1": [ "Tomó la iniciativa sin que se lo pidieran.", "Ofreció ayuda o asesoría no solicitada.", "Pensó con orientación al futuro.", "Equilibró iniciativa y riesgo." ], "4.4.5.2": [ "Mostró compromiso personal con los objetivos.", "Promovió el proyecto y generó entusiasmo.", "Definió o monitoreó indicadores de desempeño.", "Buscó mejoras en procesos." ], "4.4.5.3": [ "Proporcionó dirección clara al equipo.", "Realizó coaching o mentoring para mejorar capacidades.", "Estableció y comunicó visión y valores.", "Alineó objetivos individuales con los comunes." ], "4.4.5.4": [ "Usó apropiadamente poder e influencia.", "Seleccionó el canal de comunicación adecuado para influir.", "Fue percibido como líder por los stakeholders." ], "4.4.5.5": [ "Tomó decisiones bajo incertidumbre considerando pros y contras.", "Explicó el razonamiento de las decisiones.", "Revisó decisiones con nueva evidencia.", "Comunicó claramente la decisión e influyó su adopción." ] } # ========================== # Carga perezosa de modelos # ========================== def lazy_load_llm(): """Pipeline de generación (Qwen 0.5B) para extraer STAR.""" global _llm, _llm_tok, _gen from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline if _gen is not None: return _gen _llm_tok = AutoTokenizer.from_pretrained(LLM_ID) _llm = AutoModelForCausalLM.from_pretrained(LLM_ID, device_map="auto") _gen = pipeline( "text-generation", model=_llm, tokenizer=_llm_tok, max_new_tokens=512, do_sample=False, repetition_penalty=1.1, ) return _gen def lazy_load_nli(model_id: str): """NLI con salida completa y truncado seguro. Cachea por model_id.""" from transformers import pipeline if model_id in _nli_cache: return _nli_cache[model_id] nli = pipeline( "text-classification", model=model_id, tokenizer=model_id, return_all_scores=True, # {label, score} para todas las clases truncation=True # evita degradación por textos largos ) _nli_cache[model_id] = nli return nli # ========================== # Utilidades extracción STAR # ========================== def extract_json_block(text: str) -> str: start = text.find("{") end = text.rfind("}") if start != -1 and end != -1 and end > start: return text[start:end+1] return '{"situation":"","task":"","action":[],"result":[]}' def quick_parse_star(txt: str): t = (txt or "").strip() if not t: return None keys = ("SITUATION", "TASK", "ACTION", "RESULT", "S:", "T:", "A:", "R:") if not any(k in t for k in keys): return None sections = {"situation": "", "task": "", "action": [], "result": []} blocks = re.split(r'(?im)^(SITUATION|TASK|ACTION|RESULT|S:|T:|A:|R:)\s*:?', t) for i in range(1, len(blocks), 2): key = blocks[i].lower()[0] val = blocks[i+1].strip() if key == "s": sections["situation"] = val elif key == "t": sections["task"] = val elif key == "a": sections["action"] = [x.strip("•- ") for x in val.splitlines() if x.strip()] elif key == "r": sections["result"] = [x.strip("•- ") for x in val.splitlines() if x.strip()] return sections def extract_star(user_text: str) -> Dict: parsed = quick_parse_star(user_text) if parsed: return parsed gen = lazy_load_llm() prompt = STAR_PROMPT.format(texto=(user_text or "").strip()) out = gen(prompt)[0]["generated_text"] raw = extract_json_block(out) try: data = json.loads(raw) except Exception: data = {"situation": "", "task": "", "action": [], "result": []} m = re.search(r'Situation[::]\s*(.*)', user_text or "", flags=re.I) if m: data["situation"] = m.group(1).strip() data["action"] = data.get("action", []) data["result"] = data.get("result", []) if isinstance(data["action"], str): data["action"] = [data["action"]] if isinstance(data["result"], str): data["result"] = [data["result"]] return { "situation": (data.get("situation", "") or "").strip(), "task": (data.get("task", "") or "").strip(), "action": [str(a).strip(" •-") for a in data["action"] if str(a).strip()], "result": [str(r).strip(" •-") for r in data["result"] if str(r).strip()], } # ========================== # NLI + scoring (dinámico por modelo) # ========================== def calibrate_prob(p: float, use_calibration: bool) -> float: """Calibración leve solo para MiniLM (p**0.9).""" p = max(0.0, min(1.0, float(p))) return (p ** 0.9) if use_calibration else p def nli_entails(premise: str, hypothesis: str, model_id: str) -> float: """Probabilidad de ENTAILMENT (0..1) robusta a variantes de salida.""" nli = lazy_load_nli(model_id) def _trim(s: str, limit=900): s = (s or "").strip() return s[:limit] text_a = _trim(premise) text_b = _trim(hypothesis) if not text_a or not text_b: return 0.0 try: res = nli({"text": text_a, "text_pair": text_b}) except Exception: return 0.0 # return_all_scores=True → [{label, score}, ...] ó [[{...}]] if isinstance(res, dict): candidates = [res] elif isinstance(res, list): candidates = res[0] if (res and isinstance(res[0], list)) else res else: return 0.0 for c in (d for d in candidates if isinstance(d, dict)): lab = str(c.get("label", "")).lower() if "entail" in lab: try: return float(c.get("score", 0.0)) except Exception: return 0.0 return 0.0 def map_prob_to_score(p: float, thresholds: Tuple[float, float, float, float]) -> int: t4, t3, t2, t1 = thresholds if p >= t4: return 4 if p >= t3: return 3 if p >= t2: return 2 if p >= t1: return 1 return 0 def score_indicator(premise: str, hyps: List[str], model_id: str, use_calibration: bool, thresholds: Tuple[float, float, float, float]) -> Tuple[int, List[Tuple[str, float]], float]: raw = [(h, nli_entails(premise, h, model_id)) for h in hyps] probs = [(h, calibrate_prob(p, use_calibration)) for h, p in raw] avg = sum(p for _, p in probs) / max(1, len(probs)) score = map_prob_to_score(avg, thresholds) probs_sorted = sorted(probs, key=lambda x: x[1], reverse=True)[:2] return score, probs_sorted, avg # ========================== # Evaluación orquestada # ========================== def evaluate(texto: str, model_key: str): """Devuelve: status_msg, matplotlib_fig, {"columns":[...], "data":[...] }.""" try: if not texto or not texto.strip(): return "Introduce un caso en formato STAR (o texto libre).", None, {"columns": [], "data": []} # Config del modelo seleccionado cfg = MODEL_CHOICES.get(model_key, MODEL_CHOICES[DEFAULT_MODEL_KEY]) model_id = cfg["id"] use_calibration = cfg["calibrate"] thresholds = cfg["thresholds"] star = extract_star(texto) # Limita premisa para dar señal clara al NLI (6 A + 4 R) actions = (star.get("action", []) or [])[:6] results = (star.get("result", []) or [])[:4] premise = " ".join(actions) + " " + " ".join(results) # Scoring por indicador scores, table_rows, per_indicator_values = [], [], [] for ind, hyps in HYP.items(): s, ev, avg = score_indicator(premise, hyps, model_id, use_calibration, thresholds) scores.append(s) per_indicator_values.append((ind, s)) best_evid = " / ".join([h for h, _ in ev]) name, desc = INDICATOR_META[ind] table_rows.append([ind, name, s, f"{avg:.2f}", best_evid, desc]) overall = round(sum(scores) / max(1, len(scores)), 2) # Gráfica labels = [f"{k.split('.')[-1]}" for k, _ in per_indicator_values] values = [v for _, v in per_indicator_values] fig, ax = plt.subplots(figsize=(8.2, 4.0)) ax.bar(labels, values) ax.set_ylim(0, 4) ax.set_xlabel("Indicadores 4.4.5.x") ax.set_ylabel("Score (0–4)") fig.suptitle(f"ICB4 4.4.5 Leadership — Score global: {overall} | Modelo: {model_key}", y=0.97) fig.subplots_adjust(top=0.86) for i, v in enumerate(values): ax.text(i, v + 0.08, f"{v}", ha="center", va="bottom") fig.tight_layout() table = { "columns": DEFAULT_COLS, "data": table_rows, "model_key": model_key, # ← etiqueta elegida en el dropdown (MiniLM / DeBERTa) "model_id": model_id # ← repo real en HF (para trazabilidad) } msg = ( f"Evaluación completada. Score global (0–4): {overall}\n" f"Modelo: {model_key}\n" f"Sugerencia: revisa evidencias y ajusta umbrales según tu rúbrica." ) return msg, fig, table except Exception as e: return f"⚠️ Error en evaluate(): {type(e).__name__}: {e}", None, {"columns": [], "data": []} # ========================== # CSV helper # ========================== def make_csv_from_table(table: dict) -> str: """Genera CSV temporal sin incluir la columna 'Modelo (repo)', pero conserva 'Modelo (etiqueta)'.""" cols = table.get("columns", []) rows = table.get("data", []) ts = int(time.time()) path = f"/tmp/icb4_leadership_{ts}.csv" # Detecta y elimina solo la columna 'Modelo (repo)' if "Modelo (repo)" in cols: idx_repo = cols.index("Modelo (repo)") cols = [c for i, c in enumerate(cols) if i != idx_repo] new_rows = [] for r in rows: if len(r) > idx_repo: # Elimina solo la celda correspondiente al campo 'Modelo (repo)' r = [c for i, c in enumerate(r) if i != idx_repo] new_rows.append(r) rows = new_rows # Escribe el CSV final with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(cols) for r in rows: writer.writerow(r) return path if os.path.exists(path) else "" # ========================== # UI (2 columnas + selector modelo + CSV) # ========================== with gr.Blocks(title="ICB4 4.4.5 Leadership — Evaluación STAR (FRAQX)", css=CUSTOM_CSS, elem_id="app") as demo: gr.Markdown( """

ICB4 • 4.4.5 Leadership — Evaluación STAR + NLI

Extracción STAR, scoring (4.4.5.1–4.4.5.5), gráfica y reporte descargable. Elige el modelo NLI según tu prioridad.
""" ) with gr.Row(equal_height=True): # Entrada with gr.Column(scale=5): gr.Markdown("
Entrada
") model_key = gr.Dropdown( choices=list(MODEL_CHOICES.keys()), value=DEFAULT_MODEL_KEY, label="Modelo NLI", info="Velocidad (MiniLM) = más rápido | Precisión (DeBERTa) = mejor calidad" ) texto = gr.Textbox( label="Caso (STAR o texto libre)", lines=16, placeholder="Pega aquí tu caso en formato STAR (S, T, A, R) o texto libre…" ) with gr.Row(): btn = gr.Button("Evaluar", variant="primary", scale=3) gr.ClearButton([texto], value="Limpiar", scale=1) gr.Markdown( """
Ejemplo rápido (clic para autocompletar)
S: El proyecto CRM estaba retrasado 6 semanas y el equipo estaba desmotivado.
T: Recuperar el plan y mejorar la colaboración en 2 sprints.
A: Organicé una sesión de visión y valores; definí métricas; implementé dailies; mentoring a líderes junior; negocié con stakeholders; prioricé backlog mínimo; comuniqué riesgos y fechas realistas.
R: Recuperamos 4 semanas en 2 sprints; NPS interno +22; retrabajo -18%; se mantuvieron prácticas; dos líderes promovidos.
""", ) # Salida with gr.Column(scale=7): gr.Markdown("
Resultados
") status = gr.Markdown(value="**Estado**: —", elem_id="status_md") score_badge = gr.Markdown(value="Score global: —") plot = gr.Plot(label="Gráfica de evaluación (0–4)") table = gr.Dataframe( headers=DEFAULT_COLS, datatype=["str", "str", "number", "str", "str", "str"], interactive=False, label="Detalle por indicador" ) with gr.Row(): download_btn = gr.Button("Descargar CSV") csv_file = gr.File(label="Archivo CSV", visible=False) # Lógica def run_eval(t: str, mk: str): msg, fig, tbl = evaluate(t, mk) status_md = "**Estado** \n" + (msg or "").replace("\n", " \n") badge_html = "Score global: —" try: m = re.search(r"Score global \(0–4\):\s*([0-4](?:\.[0-9])?)", msg or "") if m: badge_html = f"Score global: {m.group(1)}" except Exception: pass cols = (tbl or {}).get("columns") or DEFAULT_COLS data = (tbl or {}).get("data") or [] safe_data = [] for row in data: r = list(row) if len(r) < len(cols): r += [""] * (len(cols) - len(r)) elif len(r) > len(cols): r = r[:len(cols)] safe_data.append(r) if fig is None: fig, ax = plt.subplots(figsize=(6, 2)) ax.axis("off") ax.text(0.5, 0.5, "Sin datos para graficar", ha="center", va="center") return status_md, badge_html, fig, gr.update(value=safe_data, headers=cols) btn.click(fn=run_eval, inputs=[texto, model_key], outputs=[status, score_badge, plot, table]) def export_csv_handler(t: str, mk: str): _, _, tbl = evaluate(t, mk) path = make_csv_from_table(tbl) return path, gr.update(visible=True) download_btn.click(fn=export_csv_handler, inputs=[texto, model_key], outputs=[csv_file, csv_file]) # Lanzamiento if __name__ == "__main__": demo.queue(max_size=16).launch(ssr_mode=False, show_error=True)