184 lines
5.9 KiB
Python
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)
|