""" 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:** " "C=Coding, " "R=Running, " "OK=Success, " "X=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)