Files
wehub-resource-sync e64161ec32
CI / ci (3.11) (push) Has been cancelled
CI / ci (3.10) (push) Has been cancelled
CI / dependabot (push) Has been cancelled
Release / release_and_publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:15 +08:00

184 lines
5.9 KiB
Python

"""
RL Job Summary View
Display summary table for all tasks in a job directory
"""
import pickle
from pathlib import Path
import pandas as pd
import streamlit as st
def is_valid_task(task_path: Path) -> bool:
"""Check if directory is a valid RL task (has __session__ subdirectory)"""
return task_path.is_dir() and (task_path / "__session__").exists()
def get_loop_dirs(task_path: Path) -> list[Path]:
"""Get sorted list of Loop directories"""
loops = [d for d in task_path.iterdir() if d.is_dir() and d.name.startswith("Loop_")]
return sorted(loops, key=lambda d: int(d.name.split("_")[1]))
def get_loop_status(task_path: Path, loop_id: int) -> tuple[str, bool | None]:
"""
Get loop status and feedback decision.
Returns: (status_str, feedback_decision)
Status: 'C'=Coding, 'R'=Running, 'X'=Failed, 'OK'=Success
"""
loop_path = task_path / f"Loop_{loop_id}"
if not loop_path.exists():
return "-", None
# Check for feedback
feedback_decision = None
feedback_files = list(loop_path.rglob("**/feedback/**/*.pkl"))
for f in feedback_files:
try:
with open(f, "rb") as fp:
content = pickle.load(fp)
decision = getattr(content, "decision", None)
if decision is not None:
feedback_decision = decision
break
except Exception:
pass
if feedback_decision is not None:
return ("OK" if feedback_decision else "X"), feedback_decision
# Check running stage
running_files = list(loop_path.rglob("**/running/**/*.pkl"))
if running_files:
return "R", None
# Check coding stage
coding_files = list(loop_path.rglob("**/coding/**/*.pkl"))
if coding_files:
return "C", None
return "?", None
def get_max_loops(job_path: Path) -> int:
"""Get maximum number of loops across all tasks"""
max_loops = 0
for task_dir in job_path.iterdir():
if is_valid_task(task_dir):
loops = get_loop_dirs(task_dir)
max_loops = max(max_loops, len(loops))
return max_loops
def get_job_summary_df(job_path: Path) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Generate summary DataFrame for all tasks in job"""
if not job_path.exists():
return pd.DataFrame(), pd.DataFrame()
tasks = [d for d in sorted(job_path.iterdir(), reverse=True) if is_valid_task(d)]
if not tasks:
return pd.DataFrame(), pd.DataFrame()
max_loops = get_max_loops(job_path)
if max_loops == 0:
max_loops = 10
data = []
decisions_data = []
for task_path in tasks:
row = {"Task": task_path.name}
decision_row = {"Task": task_path.name}
success_count = 0
fail_count = 0
for i in range(max_loops):
status, feedback_decision = get_loop_status(task_path, i)
row[f"L{i}"] = status
decision_row[f"L{i}"] = feedback_decision
if feedback_decision is True:
success_count += 1
elif feedback_decision is False:
fail_count += 1
row["Summary"] = f"{success_count}✓/{fail_count}✗"
decision_row["Summary"] = None
data.append(row)
decisions_data.append(decision_row)
df = pd.DataFrame(data)
decisions_df = pd.DataFrame(decisions_data)
if not df.empty:
loop_cols = [c for c in df.columns if c.startswith("L")]
cols = ["Task"] + sorted(loop_cols, key=lambda x: int(x[1:])) + ["Summary"]
df = df[cols]
decisions_df = decisions_df[cols]
return df, decisions_df
def style_status_cell(val: str, decision: bool | None = None) -> str:
"""Style cell based on status value"""
if val == "-":
return "color: #888"
if val == "C":
return "color: #f0ad4e; font-weight: bold"
if val == "R":
return "color: #5bc0de; font-weight: bold"
if val == "X":
return "color: #d9534f; font-weight: bold"
if val == "OK":
return "color: #5cb85c; font-weight: bold"
if val == "?":
return "color: #888"
return ""
def style_df_with_decisions(df: pd.DataFrame, decisions_df: pd.DataFrame):
"""Apply styling to dataframe"""
def apply_styles(row_idx: int, col: str) -> str:
val = df.iloc[row_idx][col]
decision = decisions_df.iloc[row_idx][col] if col in decisions_df.columns else None
return style_status_cell(str(val), decision)
styles = pd.DataFrame("", index=df.index, columns=df.columns)
for row_idx in range(len(df)):
for col in df.columns:
styles.iloc[row_idx][col] = apply_styles(row_idx, col)
return df.style.apply(lambda _: styles, axis=None)
def render_job_summary(job_path: Path, is_root: bool = False) -> None:
"""Render job summary UI"""
title = "Standalone Tasks" if is_root else f"Job: {job_path.name}"
st.subheader(title)
df, decisions_df = get_job_summary_df(job_path)
if df.empty:
st.warning("No valid tasks found in this job directory")
return
st.markdown(
"**Legend:** "
"<span style='color:#f0ad4e'>C</span>=Coding, "
"<span style='color:#5bc0de'>R</span>=Running, "
"<span style='color:#5cb85c'>OK</span>=Success, "
"<span style='color:#d9534f'>X</span>=Failed",
unsafe_allow_html=True,
)
styled_df = style_df_with_decisions(df, decisions_df)
st.dataframe(styled_df, use_container_width=True, hide_index=True)
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Tasks", len(df))
with col2:
loop_cols = [c for c in decisions_df.columns if c.startswith("L")]
tasks_success = decisions_df[loop_cols].apply(lambda row: any(v is True for v in row), axis=1).sum()
st.metric("With Success", tasks_success)
with col3:
total_loops = sum(1 for _, row in decisions_df.iterrows() for c in loop_cols if row[c] is not None)
st.metric("Total Loops", total_loops)