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

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()