Spaces:
Sleeping
Sleeping
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) | |