177 lines
5.9 KiB
Python
177 lines
5.9 KiB
Python
"""
|
|
RL Post-training Timeline Viewer
|
|
Hierarchical view: Session > Loop > Stage > Events
|
|
|
|
Run:
|
|
streamlit run rdagent/app/rl/ui/app.py
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import streamlit as st
|
|
from streamlit import session_state as state
|
|
|
|
from rdagent.app.rl.ui.components import render_session, render_summary
|
|
from rdagent.app.rl.ui.config import ALWAYS_VISIBLE_TYPES, OPTIONAL_TYPES
|
|
from rdagent.app.rl.ui.data_loader import get_summary, get_valid_sessions, load_session
|
|
from rdagent.app.rl.ui.rl_summary import render_job_summary
|
|
|
|
DEFAULT_LOG_BASE = "log/"
|
|
|
|
|
|
def _safe_resolve(user_input: str | None, safe_root: Path) -> Path:
|
|
"""Resolve user path relative to safe_root; raise ValueError if it escapes."""
|
|
safe_root = safe_root.expanduser().resolve()
|
|
if not user_input:
|
|
return safe_root
|
|
|
|
if "\x00" in user_input:
|
|
raise ValueError("Invalid path: contains null byte")
|
|
|
|
try:
|
|
normalized = os.path.normpath(user_input)
|
|
if os.path.splitdrive(normalized)[0]:
|
|
raise ValueError("Absolute paths with drive letters are not allowed")
|
|
path_obj = Path(normalized).expanduser()
|
|
if path_obj.is_absolute():
|
|
raise ValueError("Absolute paths are not allowed")
|
|
candidate = (safe_root / path_obj).resolve(strict=False)
|
|
candidate.relative_to(safe_root)
|
|
return candidate
|
|
except (OSError, ValueError) as exc:
|
|
raise ValueError(f"Invalid path outside of allowed root: {user_input}") from exc
|
|
|
|
|
|
def get_job_options(base_path: Path) -> list[str]:
|
|
"""Scan directory and return job options list."""
|
|
options = []
|
|
has_root_tasks = False
|
|
job_dirs = []
|
|
|
|
if not base_path.exists():
|
|
return options
|
|
|
|
for d in base_path.iterdir():
|
|
if not d.is_dir():
|
|
continue
|
|
if (d / "__session__").exists():
|
|
has_root_tasks = True
|
|
else:
|
|
try:
|
|
if any((sub / "__session__").exists() for sub in d.iterdir() if sub.is_dir()):
|
|
job_dirs.append(d.name)
|
|
except PermissionError:
|
|
pass
|
|
|
|
job_dirs.sort(reverse=True)
|
|
options.extend(job_dirs)
|
|
if has_root_tasks:
|
|
options.append(". (Current)")
|
|
|
|
return options
|
|
|
|
|
|
def main():
|
|
st.set_page_config(layout="wide", page_title="RL Timeline", page_icon="🤖")
|
|
|
|
with st.sidebar:
|
|
view_mode = st.radio("View Mode", ["Job Summary", "Single Task"], horizontal=True)
|
|
st.divider()
|
|
|
|
default_log = os.environ.get("RL_LOG_PATH", DEFAULT_LOG_BASE)
|
|
safe_root = Path(default_log).expanduser().resolve()
|
|
job_folder = str(safe_root)
|
|
selected_types = ALWAYS_VISIBLE_TYPES.copy()
|
|
is_root_job = False
|
|
|
|
if view_mode == "Job Summary":
|
|
st.header("Job")
|
|
base_folder = st.text_input("Base Folder", value=default_log, key="base_folder_input")
|
|
try:
|
|
base_path = _safe_resolve(base_folder, safe_root)
|
|
except ValueError as e:
|
|
st.error(str(e))
|
|
return
|
|
|
|
job_options = get_job_options(base_path)
|
|
if job_options:
|
|
selected_job = st.selectbox("Select Job", job_options, key="job_select")
|
|
if selected_job.startswith("."):
|
|
job_folder = str(base_path)
|
|
is_root_job = True
|
|
else:
|
|
job_folder = str(base_path / selected_job)
|
|
state.selected_job_folder = job_folder
|
|
else:
|
|
st.warning("No jobs found in this directory")
|
|
job_folder = str(base_path)
|
|
|
|
if st.button("Refresh", type="primary", key="refresh_job"):
|
|
st.rerun()
|
|
else:
|
|
st.header("Session")
|
|
default_path = getattr(state, "selected_job_folder", default_log)
|
|
log_folder = st.text_input("Log Folder", value=default_path)
|
|
try:
|
|
log_path = _safe_resolve(log_folder, safe_root)
|
|
except ValueError as e:
|
|
st.error(str(e))
|
|
return
|
|
|
|
sessions = get_valid_sessions(log_path)
|
|
if not sessions:
|
|
st.warning("No valid sessions found")
|
|
return
|
|
|
|
selected_session = st.selectbox("Session", sessions)
|
|
|
|
if st.button("Load", type="primary") or "session" not in state:
|
|
with st.spinner("Loading..."):
|
|
state.session = load_session(log_path / selected_session)
|
|
state.session_name = selected_session
|
|
|
|
st.divider()
|
|
|
|
st.subheader("Show More")
|
|
selected_types = ALWAYS_VISIBLE_TYPES.copy()
|
|
for event_type, (label, default) in OPTIONAL_TYPES.items():
|
|
if st.toggle(label, value=default, key=f"toggle_{event_type}"):
|
|
selected_types.append(event_type)
|
|
|
|
st.divider()
|
|
|
|
if "session" in state:
|
|
summary = get_summary(state.session)
|
|
st.subheader("Summary")
|
|
st.metric("Loops", summary.get("loop_count", 0))
|
|
st.metric("LLM Calls", summary.get("llm_call_count", 0))
|
|
success = summary.get("docker_success", 0)
|
|
fail = summary.get("docker_fail", 0)
|
|
st.metric("Docker", f"{success}✓ / {fail}✗")
|
|
|
|
if view_mode == "Job Summary":
|
|
st.title("📊 RL Job Summary")
|
|
job_path = Path(job_folder).resolve()
|
|
if job_path.exists():
|
|
render_job_summary(job_path, is_root=is_root_job)
|
|
else:
|
|
st.warning(f"Job folder not found: {job_folder}")
|
|
return
|
|
|
|
st.title("🤖 RL Timeline Viewer")
|
|
|
|
if "session" not in state:
|
|
st.info("Select a session and click **Load** to view")
|
|
return
|
|
|
|
session = state.session
|
|
summary = get_summary(session)
|
|
render_summary(summary)
|
|
st.divider()
|
|
render_session(session, selected_types)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|