cesparzaf's picture
Update app.py
22a5b8d verified
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(
"""
<div style="display:flex;align-items:center;gap:12px;margin:8px 0 2px 0;">
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" height="28">
<h1 style="margin:0;">ICB4 • 4.4.5 Leadership — Evaluación STAR + NLI</h1>
</div>
<div class="small">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.</div>
"""
)
with gr.Row(equal_height=True):
# Entrada
with gr.Column(scale=5):
gr.Markdown("<div class='card'><b>Entrada</b></div>")
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(
"""
<details>
<summary>Ejemplo rápido (clic para autocompletar)</summary>
<div class="small">
S: El proyecto CRM estaba retrasado 6 semanas y el equipo estaba desmotivado.<br/>
T: Recuperar el plan y mejorar la colaboración en 2 sprints.<br/>
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.<br/>
R: Recuperamos 4 semanas en 2 sprints; NPS interno +22; retrabajo -18%; se mantuvieron prácticas; dos líderes promovidos.
</div>
</details>
""",
)
# Salida
with gr.Column(scale=7):
gr.Markdown("<div class='card'><b>Resultados</b></div>")
status = gr.Markdown(value="**Estado**: —", elem_id="status_md")
score_badge = gr.Markdown(value="<span class='badge'>Score global: —</span>")
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 = "<span class='badge'>Score global: —</span>"
try:
m = re.search(r"Score global \(0–4\):\s*([0-4](?:\.[0-9])?)", msg or "")
if m:
badge_html = f"<span class='badge'>Score global: {m.group(1)}</span>"
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)