Files
NexQuant/rdagent/log/ui/ds_trace.py
T

1206 lines
51 KiB
Python
Raw Normal View History

import hashlib
import json
import pickle
2025-06-30 20:10:34 +08:00
import random
import re
from collections import defaultdict
from datetime import time, timedelta
from pathlib import Path
import pandas as pd
2025-07-08 18:20:27 +08:00
import plotly.express as px
import streamlit as st
from litellm import get_valid_models
from streamlit import session_state as state
from rdagent.app.data_science.loop import DataScienceRDLoop
from rdagent.log.storage import FileStorage
from rdagent.log.ui.conf import UI_SETTING
2025-07-24 18:50:29 +08:00
from rdagent.log.ui.utils import (
curve_figure,
get_sota_exp_stat,
load_times_info,
timeline_figure,
2025-07-24 18:50:29 +08:00
trace_figure,
)
from rdagent.log.utils import (
2025-06-04 18:30:29 +08:00
LogColors,
extract_evoid,
extract_json,
extract_loopid_func_name,
is_valid_session,
)
from rdagent.oai.backend.litellm import LITELLM_SETTINGS
from rdagent.oai.llm_utils import APIBackend
# Import necessary classes for the response format
from rdagent.scenarios.data_science.proposal.exp_gen.proposal import (
CodingSketch,
HypothesisList,
ScenarioChallenges,
TraceChallenges,
)
from rdagent.utils.agent.tpl import T
2025-05-06 11:34:21 +08:00
from rdagent.utils.repo.diff import generate_diff_from_dict
if "show_stdout" not in state:
state.show_stdout = False
if "show_llm_log" not in state:
state.show_llm_log = False
if "data" not in state:
state.data = defaultdict(lambda: defaultdict(dict))
if "llm_data" not in state:
state.llm_data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
if "log_path" not in state:
state.log_path = None
if "log_folder" not in state:
state.log_folder = Path("./log")
2025-07-24 18:50:29 +08:00
if "sota_info" not in state:
state.sota_info = None
available_models = get_valid_models()
LITELLM_SETTINGS.dump_chat_cache = False
LITELLM_SETTINGS.dump_embedding_cache = False
LITELLM_SETTINGS.use_chat_cache = False
LITELLM_SETTINGS.use_embedding_cache = False
def convert_defaultdict_to_dict(d):
if isinstance(d, defaultdict):
d = {k: convert_defaultdict_to_dict(v) for k, v in d.items()}
return d
def load_data(log_path: Path):
"""
Load and normalize logged data for the UI.
Meaning of "no_tag":
- We attempt to extract an evolution id (ei) from each message tag.
- If no ei can be extracted (i.e., the entry is not tied to a specific evolving step),
the item is stored under the "no_tag" key.
- Typical "no_tag" entries include:
* direct_exp_gen["no_tag"]: the base experiment/hypothesis for the loop
* coding["no_tag"] / running["no_tag"]: the final workspace/result for that stage
* llm_data[loop_id][function]["no_tag"]: common LLM logs without an ei
"""
data = defaultdict(lambda: defaultdict(dict))
llm_data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
2025-07-08 18:20:27 +08:00
token_costs = defaultdict(list)
for msg in FileStorage(log_path).iter_msg():
if not msg.tag:
continue
li, fn = extract_loopid_func_name(msg.tag)
ei = extract_evoid(msg.tag)
if li is not None:
li = int(li)
if ei is not None:
ei = int(ei)
if "debug_" in msg.tag:
if ei is not None:
llm_data[li][fn][ei].append(
{
"tag": msg.tag,
"obj": msg.content,
}
)
else:
llm_data[li][fn]["no_tag"].append(
{
"tag": msg.tag,
"obj": msg.content,
}
)
2025-07-08 18:20:27 +08:00
elif "token_cost" in msg.tag:
token_costs[li].append(msg)
elif "llm" not in msg.tag and "session" not in msg.tag and "batch embedding" not in msg.tag:
if msg.tag == "competition":
data["competition"] = msg.content
continue
if "SETTINGS" in msg.tag:
data["settings"][msg.tag] = msg.content
continue
msg.tag = re.sub(r"\.evo_loop_\d+", "", msg.tag)
msg.tag = re.sub(r"Loop_\d+\.[^.]+\.?", "", msg.tag)
msg.tag = msg.tag.strip()
if ei is not None:
if ei not in data[li][fn]:
data[li][fn][ei] = {}
data[li][fn][ei][msg.tag] = msg.content
else:
if msg.tag:
data[li][fn][msg.tag] = msg.content
else:
if not isinstance(msg.content, str):
data[li][fn]["no_tag"] = msg.content
# To be compatible with old version log trace, keep this
llm_log_p = log_path / "debug_llm.pkl"
if llm_log_p.exists():
try:
rd = pickle.loads(llm_log_p.read_bytes())
except:
rd = []
for d in rd:
t = d["tag"]
if "debug_exp_gen" in t:
continue
if "debug_tpl" in t and "filter_" in d["obj"]["uri"]:
continue
lid, fn = extract_loopid_func_name(t)
ei = extract_evoid(t)
if lid:
lid = int(lid)
if ei is not None:
ei = int(ei)
if ei is not None:
llm_data[lid][fn][ei].append(d)
else:
llm_data[lid][fn]["no_tag"].append(d)
2025-07-08 18:20:27 +08:00
return (
convert_defaultdict_to_dict(data),
convert_defaultdict_to_dict(llm_data),
convert_defaultdict_to_dict(token_costs),
)
if UI_SETTING.enable_cache:
load_data = st.cache_data(persist=True)(load_data)
def load_stdout(stdout_path: Path):
if stdout_path.exists():
stdout = stdout_path.read_text()
else:
stdout = f"Please Set: {stdout_path}"
return stdout
# UI windows
def task_win(task):
with st.expander(f"**:violet[{task.name}]**", expanded=False):
st.markdown(task.description)
if hasattr(task, "package_info"):
st.markdown(f"**:blue[Package Info:]**")
st.code(task.package_info)
if hasattr(task, "architecture"): # model task
st.markdown(f"""
| Model_type | Architecture | hyperparameters |
|------------|--------------|-----------------|
| {task.model_type} | {task.architecture} | {task.hyperparameters} |
""")
2025-06-30 20:10:34 +08:00
def workspace_win(workspace, cmp_workspace=None, cmp_name="last code."):
2025-05-06 11:34:21 +08:00
show_files = {k: v for k, v in workspace.file_dict.items() if "test" not in k}
if len(show_files) > 0:
2025-05-06 11:34:21 +08:00
if cmp_workspace:
diff = generate_diff_from_dict(cmp_workspace.file_dict, show_files, "main.py")
2025-07-31 18:14:38 +08:00
with st.popover(f":violet[**Diff with {cmp_name}**]", use_container_width=True, icon="🔍"):
2025-05-06 11:34:21 +08:00
st.code("".join(diff), language="diff", wrap_lines=True, line_numbers=True)
rtime = workspace.running_info.running_time
time_str = timedelta_to_str(timedelta(seconds=rtime) if rtime else None) or "00:00:00"
2025-07-31 18:14:38 +08:00
with st.popover(
f"⏱️{time_str} 📂Files in :blue[{replace_ep_path(workspace.workspace_path)}]", use_container_width=True
2025-07-31 18:14:38 +08:00
):
2025-08-29 15:05:15 +08:00
st.write(replace_ep_path(workspace.workspace_path))
code_tabs = st.tabs(show_files.keys())
for ct, codename in zip(code_tabs, show_files.keys()):
with ct:
st.code(
show_files[codename],
language=("python" if codename.endswith(".py") else "markdown"),
wrap_lines=True,
line_numbers=True,
)
2025-06-30 20:10:34 +08:00
if state.show_save_input:
st.markdown("### Save All Files to Folder")
unique_key = hashlib.md5("".join(show_files.values()).encode()).hexdigest() + str(
random.randint(0, 10000)
)
target_folder = st.text_input("Enter target folder path:", key=unique_key)
2025-06-30 20:10:34 +08:00
if st.button("Save Files", key=f"save_files_button_{unique_key}"):
if target_folder.strip() == "":
st.warning("Please enter a valid folder path.")
else:
target_folder_path = Path(target_folder)
target_folder_path.mkdir(parents=True, exist_ok=True)
for filename, content in workspace.file_dict.items():
save_path = target_folder_path / filename
save_path.parent.mkdir(parents=True, exist_ok=True)
save_path.write_text(content, encoding="utf-8")
st.success(f"All files saved to: {target_folder}")
else:
2025-05-06 11:34:21 +08:00
st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]")
# Helper functions
def show_text(text, lang=None):
"""显示文本代码块"""
if lang:
st.code(text, language=lang, wrap_lines=True, line_numbers=True)
elif "\n" in text:
st.code(text, language="python", wrap_lines=True, line_numbers=True)
else:
st.code(text, language="html", wrap_lines=True)
def highlight_prompts_uri(uri):
"""高亮 URI 的格式"""
parts = uri.split(":")
2025-05-06 11:34:21 +08:00
if len(parts) > 1:
return f"**{parts[0]}:**:green[**{parts[1]}**]"
return f"**{uri}**"
def llm_log_win(llm_d: list):
def to_str_recursive(obj):
if isinstance(obj, dict):
return {k: to_str_recursive(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [to_str_recursive(v) for v in obj]
elif isinstance(obj, tuple):
return tuple(to_str_recursive(v) for v in obj)
else:
return str(obj)
for d in llm_d:
if "debug_tpl" in d["tag"]:
uri = d["obj"]["uri"]
if "filter_redundant_text" in uri:
continue
tpl = d["obj"]["template"]
cxt = d["obj"]["context"]
rd = d["obj"]["rendered"]
with st.popover(highlight_prompts_uri(uri), icon="⚙️", use_container_width=True):
t1, t2, t3 = st.tabs([":green[**Rendered**]", ":blue[**Template**]", ":orange[**Context**]"])
with t1:
show_text(rd)
with t2:
show_text(tpl, lang="django")
with t3:
st.json(to_str_recursive(cxt))
elif "debug_llm" in d["tag"]:
system = d["obj"].get("system", None)
user = d["obj"]["user"]
resp = d["obj"]["resp"]
start_time = d["obj"].get("start", "")
end_time = d["obj"].get("end", "")
if start_time and end_time:
start_str = start_time.strftime("%m-%d %H:%M:%S")
end_str = end_time.strftime("%m-%d %H:%M:%S")
duration = end_time - start_time
time_info_str = (
f"🕰️:blue[**{start_str} ~ {end_str}**] ⏳:violet[**{round(duration.total_seconds(), 2)}s**]"
)
else:
time_info_str = ""
with st.expander(f"**LLM** {time_info_str}", icon="🤖", expanded=False):
t1, t2, t3, t4 = st.tabs(
[":green[**Response**]", ":blue[**User**]", ":orange[**System**]", ":violet[**ChatBot**]"]
)
with t1:
try:
rdict = json.loads(resp)
showed_keys = []
for k, v in rdict.items():
if k.endswith(".py") or k.endswith(".md"):
st.markdown(f":red[**{k}**]")
st.code(v, language="python", wrap_lines=True, line_numbers=True)
showed_keys.append(k)
for k in showed_keys:
rdict.pop(k)
if len(showed_keys) > 0:
st.write(":red[**Other parts (except for the code or spec) in response dict:**]")
st.json(rdict)
except:
show_text(resp)
with t2:
show_text(user)
with t3:
show_text(system or "No system prompt available")
with t4:
input_c, resp_c = st.columns(2)
key = hashlib.md5(resp.encode()).hexdigest()
with input_c:
btc1, btc2, btc3 = st.columns(3)
trace_model = (
state.data.get("settings", {})
.get("LITELLM_SETTINGS", {})
.get("chat_model", available_models[0])
)
trace_reasoning_effort = (
state.data.get("settings", {}).get("LITELLM_SETTINGS", {}).get("reasoning_effort", None)
)
LITELLM_SETTINGS.chat_model = btc1.selectbox(
"Chat Model",
options=available_models,
index=available_models.index(trace_model),
key=key + "_chat_model",
)
LITELLM_SETTINGS.reasoning_effort = btc2.selectbox(
"Reasoning Effort",
options=[None, "low", "medium", "high"],
index=[None, "low", "medium", "high"].index(trace_reasoning_effort),
key=key + "_reasoning_effort",
)
rf = btc3.selectbox(
"Response Format",
options=[None, ScenarioChallenges, TraceChallenges, HypothesisList, CodingSketch],
format_func=lambda x: x.__name__ if x else "None",
key=key + "_response_format",
)
json_mode = st.checkbox("JSON Mode", value=False, key=key + "_json_mode")
2025-07-18 16:01:39 +08:00
sys_p = input_c.text_area(label="system", value=system, height="content", key=key + "_system")
user_p = input_c.text_area(label="user", value=user, height="content", key=key + "_user")
with resp_c:
if st.button("Call LLM", key=key + "_call_llm"):
with st.spinner("Calling LLM..."):
try:
resp_new = APIBackend().build_messages_and_create_chat_completion(
user_prompt=user_p,
system_prompt=sys_p,
json_mode=json_mode,
response_format=rf,
)
except Exception as e:
resp_new = f"Error: {e}"
try: # json format string
rdict = json.loads(resp_new)
st.json(rdict)
except:
try: # common string
st.code(resp_new, wrap_lines=True, line_numbers=True)
except: # response format type
st.write(resp_new)
2025-05-06 11:34:21 +08:00
def hypothesis_win(hypo):
try:
st.code(str(hypo).replace("\n", "\n\n"), wrap_lines=True)
except Exception as e:
st.write(hypo.__dict__)
2025-05-06 11:34:21 +08:00
def exp_gen_win(exp_gen_data, llm_data=None):
st.header("Exp Gen", divider="blue", anchor="exp-gen")
2025-04-09 12:21:54 +08:00
if state.show_llm_log and llm_data is not None:
llm_log_win(llm_data["no_tag"])
st.subheader("💡 Hypothesis")
2025-05-06 11:34:21 +08:00
hypothesis_win(exp_gen_data["no_tag"].hypothesis)
st.subheader("📋 pending_tasks")
2025-05-06 11:34:21 +08:00
for tasks in exp_gen_data["no_tag"].pending_tasks_list:
task_win(tasks[0])
st.subheader("📁 Exp Workspace")
2025-05-06 11:34:21 +08:00
workspace_win(exp_gen_data["no_tag"].experiment_workspace)
def evolving_win(data, key, llm_data=None, base_workspace=None):
with st.container(border=True):
if len(data) > 1:
evo_id = st.slider("Evolving", 0, len(data) - 1, 0, key=key)
elif len(data) == 1:
evo_id = 0
else:
st.markdown("No evolving.")
return
if evo_id in data:
2025-04-09 12:21:54 +08:00
if state.show_llm_log and llm_data is not None:
llm_log_win(llm_data[evo_id])
# get evolving workspace
if "evolving code" in data[evo_id] and data[evo_id]["evolving code"][0] is not None:
evolving_code_workspace = data[evo_id]["evolving code"][0]
else:
evolving_code_workspace = None
if evolving_code_workspace is not None:
st.subheader("codes")
2025-06-30 20:10:34 +08:00
workspace_win(
evolving_code_workspace,
cmp_workspace=data[evo_id - 1]["evolving code"][0] if evo_id > 0 else base_workspace,
cmp_name="last evolving code" if evo_id > 0 else "base workspace",
2025-06-30 20:10:34 +08:00
)
fb = data[evo_id]["evolving feedback"][0]
st.subheader("evolving feedback" + ("✅" if bool(fb) else "❌"))
f1, f2, f3, f4 = st.tabs(["execution", "return_checking", "code", "others"])
other_attributes = {
k: v for k, v in fb.__dict__.items() if k not in ["execution", "return_checking", "code"]
}
f1.code(fb.execution, wrap_lines=True)
f2.code(fb.return_checking, wrap_lines=True)
f3.code(fb.code, wrap_lines=True)
f4.json(other_attributes)
else:
st.write("data[evo_id]['evolving code'][0] is None.")
st.write(data[evo_id])
else:
st.markdown("No evolving.")
def coding_win(data, base_exp, llm_data: dict | None = None):
st.header("Coding", divider="blue", anchor="coding")
if llm_data is not None:
common_llm_data = llm_data.pop("no_tag", [])
evolving_data = {k: v for k, v in data.items() if isinstance(k, int)}
task_set = set()
for v in evolving_data.values():
for t in v:
if "Task" in t.split(".")[0]:
task_set.add(t.split(".")[0])
if task_set:
# 新版存Task tag的Trace
for task in task_set:
st.subheader(task)
task_data = {k: {a.split(".")[1]: b for a, b in v.items() if task in a} for k, v in evolving_data.items()}
evolving_win(
task_data,
key=task,
llm_data=llm_data if llm_data else None,
base_workspace=base_exp.experiment_workspace,
)
else:
# 旧版未存Task tag的Trace
evolving_win(
evolving_data,
key="coding",
llm_data=llm_data if llm_data else None,
base_workspace=base_exp.experiment_workspace,
)
if state.show_llm_log:
llm_log_win(common_llm_data)
if "no_tag" in data:
st.subheader("Exp Workspace (coding final)")
2025-06-30 20:10:34 +08:00
workspace_win(data["no_tag"].experiment_workspace)
2025-09-01 15:58:05 +08:00
def running_win(data, base_exp, llm_data=None, last_sota_exp=None):
st.header("Running", divider="blue", anchor="running")
if llm_data is not None:
common_llm_data = llm_data.pop("no_tag", [])
evolving_win(
{k: v for k, v in data.items() if isinstance(k, int)},
key="running",
llm_data=llm_data if llm_data else None,
2025-08-18 10:56:51 +08:00
base_workspace=base_exp.experiment_workspace if base_exp else None,
)
2025-04-09 12:21:54 +08:00
if state.show_llm_log and llm_data is not None:
llm_log_win(common_llm_data)
if "no_tag" in data:
st.subheader("Exp Workspace (running final)")
2025-05-06 11:34:21 +08:00
workspace_win(
data["no_tag"].experiment_workspace,
2025-09-01 15:58:05 +08:00
cmp_workspace=last_sota_exp.experiment_workspace if last_sota_exp else None,
2025-07-29 10:56:41 +08:00
cmp_name="last SOTA(to_submit)",
2025-05-06 11:34:21 +08:00
)
st.subheader("Result")
try:
st.write(data["no_tag"].result)
except AttributeError as e: # Compatible with old versions
st.write(data["no_tag"].__dict__["result"])
mle_score_text = data.get("mle_score", "no submission to score")
mle_score = extract_json(mle_score_text)
st.subheader(
"MLE Submission Score"
+ ("✅" if (isinstance(mle_score, dict) and mle_score["score"] is not None) else "❌")
)
if isinstance(mle_score, dict):
st.json(mle_score)
else:
st.code(mle_score_text, wrap_lines=True)
2025-05-06 11:34:21 +08:00
def feedback_win(fb_data, llm_data=None):
if "no_tag" not in fb_data:
st.header("Feedback", divider="orange", anchor="feedback")
return
2025-07-08 18:20:27 +08:00
fb = fb_data["no_tag"]
st.header("Feedback" + ("✅" if bool(fb) else "❌"), divider="orange", anchor="feedback")
if state.show_llm_log and llm_data is not None:
llm_log_win(llm_data["no_tag"])
2025-06-30 20:10:34 +08:00
try:
2025-07-08 18:20:27 +08:00
st.code(str(fb).replace("\n", "\n\n"), wrap_lines=True)
2025-06-30 20:10:34 +08:00
except Exception as e:
2025-07-08 18:20:27 +08:00
st.write(fb.__dict__)
if fb.exception is not None:
st.markdown(f"**:red[Exception]**: {fb.exception}")
2025-05-06 11:34:21 +08:00
def sota_win(sota_exp, trace):
st.subheader("SOTA Experiment", divider="rainbow", anchor="sota-exp")
2025-05-06 11:34:21 +08:00
if hasattr(trace, "sota_exp_to_submit") and trace.sota_exp_to_submit is not None:
st.markdown(":orange[trace.**sota_exp_to_submit**]")
sota_exp = trace.sota_exp_to_submit
else:
st.markdown(":orange[trace.**sota_experiment()**]")
if sota_exp:
st.markdown(f"**SOTA Exp Hypothesis**")
2025-05-06 11:34:21 +08:00
hypothesis_win(sota_exp.hypothesis)
st.markdown("**Exp Workspace**")
2025-06-30 20:10:34 +08:00
workspace_win(sota_exp.experiment_workspace)
else:
st.markdown("No SOTA experiment.")
2025-05-06 11:34:21 +08:00
def main_win(loop_id, llm_data=None):
loop_data = state.data[loop_id]
exp_gen_win(loop_data["direct_exp_gen"], llm_data["direct_exp_gen"] if llm_data else None)
if "coding" in loop_data:
coding_win(
loop_data["coding"],
base_exp=loop_data["direct_exp_gen"]["no_tag"],
llm_data=llm_data["coding"] if llm_data else None,
)
2025-05-06 11:34:21 +08:00
if "running" in loop_data:
2025-07-29 10:56:41 +08:00
# get last SOTA_exp_to_submit
2025-09-01 15:58:05 +08:00
last_sota_exp = None
2025-08-04 17:28:25 +08:00
if "record" in loop_data:
current_trace = loop_data["record"]["trace"]
current_selection = current_trace.get_current_selection()
if len(current_selection) > 0: # TODO: Why current_selection can be "()"?
current_idx = current_selection[0]
parent_idxs = current_trace.get_parents(current_idx)
if len(parent_idxs) >= 2 and hasattr(current_trace, "idx2loop_id"):
parent_idx = parent_idxs[-2]
parent_loop_id = current_trace.idx2loop_id[parent_idx]
2025-09-01 15:58:05 +08:00
if parent_loop_id in state.data:
# in some cases, the state.data is synthesized, logs does not necessarily exist
last_sota_exp = state.data[parent_loop_id]["record"].get("sota_exp_to_submit", None)
2025-07-29 10:56:41 +08:00
running_win(
2025-05-06 11:34:21 +08:00
loop_data["running"],
2025-08-18 10:56:51 +08:00
base_exp=loop_data["coding"].get("no_tag", None),
llm_data=llm_data["running"] if llm_data else None,
2025-09-01 15:58:05 +08:00
last_sota_exp=last_sota_exp,
)
2025-05-06 11:34:21 +08:00
if "feedback" in loop_data:
# Show final diff between the final workspace and the base workspace
base_workspace = loop_data["direct_exp_gen"]["no_tag"].experiment_workspace
final_workspace = None
if "running" in loop_data and "no_tag" in loop_data["running"]:
final_workspace = loop_data["running"]["no_tag"].experiment_workspace
elif "coding" in loop_data and "no_tag" in loop_data["coding"]:
final_workspace = loop_data["coding"]["no_tag"].experiment_workspace
if final_workspace is not None and base_workspace is not None:
st.subheader("Final Diff")
workspace_win(final_workspace, cmp_workspace=base_workspace, cmp_name="base workspace")
2025-05-06 11:34:21 +08:00
feedback_win(loop_data["feedback"], llm_data.get("feedback", None) if llm_data else None)
if "record" in loop_data and "SOTA experiment" in loop_data["record"]:
st.header("Record", divider="violet", anchor="record")
if state.show_llm_log and llm_data is not None and "record" in llm_data:
llm_log_win(llm_data["record"]["no_tag"])
2025-05-06 11:34:21 +08:00
sota_win(loop_data["record"]["SOTA experiment"], loop_data["record"]["trace"])
def replace_ep_path(p: Path):
# 替换workspace path为对应ep机器mount在ep03的path
# TODO: FIXME: 使用配置项来处理
match = re.search(r"ep\d+", str(state.log_folder))
if match:
ep = match.group(0)
return Path(
str(p).replace("repos/RD-Agent-Exp", f"repos/batch_ctrl/all_projects/{ep}").replace("/Data", "/data")
)
return p
def get_llm_call_stats(llm_data: dict) -> tuple[int, int]:
total_llm_call = 0
total_filter_call = 0
total_call_duration = timedelta()
filter_call_duration = timedelta()
filter_sys_prompt = T("rdagent.utils.prompts:filter_redundant_text.system").r()
for li, loop_d in llm_data.items():
for fn, loop_fn_d in loop_d.items():
for k, v in loop_fn_d.items():
for d in v:
if "debug_llm" in d["tag"]:
total_llm_call += 1
total_call_duration += d["obj"].get("end", timedelta()) - d["obj"].get("start", timedelta())
2025-07-24 18:50:29 +08:00
if "system" in d["obj"] and filter_sys_prompt == d["obj"]["system"]:
total_filter_call += 1
filter_call_duration += d["obj"].get("end", timedelta()) - d["obj"].get(
"start", timedelta()
)
2025-07-31 18:14:38 +08:00
return total_llm_call, total_filter_call, total_call_duration, filter_call_duration
def get_timeout_stats(llm_data: dict):
timeout_stat = {
"coding": {
"total": 0,
"timeout": 0,
},
"running": {
"total": 0,
"timeout": 0,
},
}
for li, loop_d in llm_data.items():
for fn, loop_fn_d in loop_d.items():
for k, v in loop_fn_d.items():
for d in v:
2025-08-18 10:56:51 +08:00
if "debug_tpl" in d["tag"] and "eval.user" in d["obj"]["uri"] and "stdout" in d["obj"]["context"]:
stdout = d["obj"]["context"]["stdout"]
if "The running time exceeds" in stdout: # Timeout case
timeout_stat[fn]["timeout"] += 1
timeout_stat[fn]["total"] += 1
return timeout_stat
def timedelta_to_str(td: timedelta | None) -> str:
if isinstance(td, timedelta):
total_seconds = int(td.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
return td
2025-07-08 18:20:27 +08:00
def summarize_win():
st.header("Summary", divider="rainbow")
with st.container(border=True):
min_id, max_id = get_state_data_range(state.data)
info0, info1, info2, info3, info4, info5, info6, info7 = st.columns(8)
2025-07-08 18:20:27 +08:00
show_trace_dag = info0.toggle("Show trace DAG", key="show_trace_dag")
only_success = info0.toggle("Only Success", key="only_success")
with info1.popover("LITELLM", icon="⚙️"):
st.write(state.data.get("settings", {}).get("LITELLM_SETTINGS", "No settings found."))
with info2.popover("RD_AGENT", icon="⚙️"):
st.write(state.data.get("settings", {}).get("RD_AGENT_SETTINGS", "No settings found."))
with info3.popover("RDLOOP", icon="⚙️"):
st.write(state.data.get("settings", {}).get("RDLOOP_SETTINGS", "No settings found."))
llm_call, llm_filter_call, llm_call_duration, filter_call_duration = get_llm_call_stats(state.llm_data)
info4.metric("LLM Calls", llm_call, help=timedelta_to_str(llm_call_duration))
2025-07-31 18:14:38 +08:00
info5.metric(
"LLM Filter Calls",
llm_filter_call,
help=timedelta_to_str(filter_call_duration),
2025-07-31 18:14:38 +08:00
)
timeout_stats = get_timeout_stats(state.llm_data)
2025-08-01 18:07:19 +08:00
coding_timeout_pct = (
round(timeout_stats["coding"]["timeout"] / timeout_stats["coding"]["total"] * 100, 2)
if timeout_stats["coding"]["total"] > 0
else 0
)
info6.metric(
2025-07-31 18:14:38 +08:00
"Timeouts (C)",
2025-08-01 18:07:19 +08:00
f"{coding_timeout_pct}%",
help=f"{timeout_stats['coding']['timeout']}/{timeout_stats['coding']['total']}",
)
2025-08-01 18:07:19 +08:00
running_timeout_pct = (
round(timeout_stats["running"]["timeout"] / timeout_stats["running"]["total"] * 100, 2)
if timeout_stats["running"]["total"] > 0
else 0
)
info7.metric(
2025-07-31 18:14:38 +08:00
"Timeouts (R)",
2025-08-01 18:07:19 +08:00
f"{running_timeout_pct}%",
help=f"{timeout_stats['running']['timeout']}/{timeout_stats['running']['total']}",
)
final_trace = list(FileStorage(state.log_folder / state.log_path).iter_msg(tag="record.trace"))[-1].content
2025-07-08 18:20:27 +08:00
if show_trace_dag:
st.markdown("### Trace DAG")
2025-07-22 17:02:56 +08:00
merge_loops = []
for loop_id in state.llm_data.keys():
if "direct_exp_gen" not in state.llm_data[loop_id]:
continue
2025-08-13 22:59:34 +08:00
if "scenarios.data_science.proposal.exp_gen.merge" in "".join(
[i["obj"]["uri"] for i in state.llm_data[loop_id]["direct_exp_gen"]["no_tag"] if "uri" in i["obj"]]
):
2025-07-22 17:02:56 +08:00
merge_loops.append(loop_id)
st.pyplot(trace_figure(final_trace, merge_loops))
# Find all root nodes (for grouping loops by trace)
root_nodes = {}
parent_nodes = {}
for node in range(len(final_trace.hist)):
parents = final_trace.get_parents(node)
root_nodes[node] = parents[0]
parent_nodes[node] = parents[-2] if len(parents) > 1 else None
if hasattr(final_trace, "idx2loop_id"):
root_nodes = {final_trace.idx2loop_id[n]: final_trace.idx2loop_id[r] for n, r in root_nodes.items()}
parent_nodes = {
final_trace.idx2loop_id[n]: final_trace.idx2loop_id[r] if r is not None else r
for n, r in parent_nodes.items()
}
# Generate Summary Table
df = pd.DataFrame(
columns=[
"Root N",
"Parent N",
"Component",
2025-04-07 20:16:37 +08:00
"Hypothesis",
"Reason",
"Others",
"Run Score (valid)",
"Run Score (test)",
"Feedback",
2025-07-31 18:14:38 +08:00
"e-loops(c)",
"e-loops(r)",
2025-07-08 18:20:27 +08:00
"COST($)",
"Time",
"Exp Gen",
"Coding",
"Running",
],
index=range(min_id, max_id + 1),
)
2025-07-08 18:20:27 +08:00
valid_results = {}
2025-07-24 18:50:29 +08:00
sota_loop_id = state.sota_info[1] if state.sota_info else None
2025-05-04 15:51:32 +08:00
for loop in range(min_id, max_id + 1):
loop_data = state.data[loop]
df.loc[loop, "Parent N"] = parent_nodes.get(loop, None)
df.loc[loop, "Root N"] = root_nodes.get(loop, None)
df.loc[loop, "Component"] = loop_data["direct_exp_gen"]["no_tag"].hypothesis.component
2025-04-07 20:16:37 +08:00
df.loc[loop, "Hypothesis"] = loop_data["direct_exp_gen"]["no_tag"].hypothesis.hypothesis
df.loc[loop, "Reason"] = loop_data["direct_exp_gen"]["no_tag"].hypothesis.reason
df.at[loop, "Others"] = {
k: v
for k, v in loop_data["direct_exp_gen"]["no_tag"].hypothesis.__dict__.items()
if k not in ["component", "hypothesis", "reason"] and v is not None
2025-04-07 20:16:37 +08:00
}
# In the test before 0.8.0 release, we found that when running `ui` of `data_science` (custom dataset),
# when `loop=0`, it doesn't exist in `state.token_costs.keys`, and we will get `KeyError` when running it,
# so we have fixed the problem with this dirty method for the time being.
if loop in state.token_costs:
df.loc[loop, "COST($)"] = sum(tc.content["cost"] for tc in state.token_costs[loop])
# Time Stats
exp_gen_time = timedelta()
coding_time = timedelta()
running_time = timedelta()
all_steps_time = timedelta()
if loop in state.times:
for step_name, step_time in state.times[loop].items():
step_duration = step_time["end_time"] - step_time["start_time"]
if step_name == "exp_gen":
exp_gen_time += step_duration
all_steps_time += step_duration
elif step_name == "coding":
coding_time += step_duration
all_steps_time += step_duration
elif step_name == "running":
running_time += step_duration
all_steps_time += step_duration
elif step_name in ["feedback", "record"]:
all_steps_time += step_duration
df.loc[loop, "Time"] = timedelta_to_str(all_steps_time)
df.loc[loop, "Exp Gen"] = timedelta_to_str(exp_gen_time)
df.loc[loop, "Coding"] = timedelta_to_str(coding_time)
df.loc[loop, "Running"] = timedelta_to_str(running_time)
if "running" in loop_data and "no_tag" in loop_data["running"]:
try:
try:
running_result = loop_data["running"]["no_tag"].result
except AttributeError as e: # Compatible with old versions
running_result = loop_data["running"]["no_tag"].__dict__["result"]
df.loc[loop, "Run Score (valid)"] = str(round(running_result.loc["ensemble"].iloc[0], 5))
2025-07-08 18:20:27 +08:00
valid_results[loop] = running_result
except:
df.loc[loop, "Run Score (valid)"] = "❌"
if "mle_score" not in state.data[loop]:
if "mle_score" in loop_data["running"]:
mle_score_txt = loop_data["running"]["mle_score"]
state.data[loop]["mle_score"] = extract_json(mle_score_txt)
2025-05-06 16:00:13 +08:00
if (
state.data[loop]["mle_score"] is not None
and state.data[loop]["mle_score"]["score"] is not None
):
2025-07-24 18:50:29 +08:00
medal_emoji = (
"🥇"
if state.data[loop]["mle_score"]["gold_medal"]
else (
"🥈"
if state.data[loop]["mle_score"]["silver_medal"]
else "🥉" if state.data[loop]["mle_score"]["bronze_medal"] else ""
)
)
df.loc[loop, "Run Score (test)"] = f"{medal_emoji} {state.data[loop]['mle_score']['score']}"
else:
state.data[loop]["mle_score"] = mle_score_txt
df.loc[loop, "Run Score (test)"] = "❌"
else:
mle_score_path = (
replace_ep_path(loop_data["running"]["no_tag"].experiment_workspace.workspace_path)
/ "mle_score.txt"
)
try:
mle_score_txt = mle_score_path.read_text()
state.data[loop]["mle_score"] = extract_json(mle_score_txt)
if state.data[loop]["mle_score"]["score"] is not None:
2025-07-24 18:50:29 +08:00
medal_emoji = (
"🥇"
if state.data[loop]["mle_score"]["gold_medal"]
else (
"🥈"
if state.data[loop]["mle_score"]["silver_medal"]
else "🥉" if state.data[loop]["mle_score"]["bronze_medal"] else ""
)
)
df.loc[loop, "Run Score (test)"] = (
2025-07-24 18:50:29 +08:00
f"{medal_emoji} {state.data[loop]['mle_score']['score']}"
)
else:
state.data[loop]["mle_score"] = mle_score_txt
df.loc[loop, "Run Score (test)"] = "❌"
except Exception as e:
state.data[loop]["mle_score"] = str(e)
df.loc[loop, "Run Score (test)"] = "❌"
else:
if isinstance(state.data[loop]["mle_score"], dict):
2025-07-24 18:50:29 +08:00
medal_emoji = (
"🥇"
if state.data[loop]["mle_score"]["gold_medal"]
else (
"🥈"
if state.data[loop]["mle_score"]["silver_medal"]
else "🥉" if state.data[loop]["mle_score"]["bronze_medal"] else ""
)
)
df.loc[loop, "Run Score (test)"] = f"{medal_emoji} {state.data[loop]['mle_score']['score']}"
else:
df.loc[loop, "Run Score (test)"] = "❌"
else:
df.loc[loop, "Run Score (valid)"] = "N/A"
df.loc[loop, "Run Score (test)"] = "N/A"
if "coding" in loop_data:
if len([i for i in loop_data["coding"].keys() if isinstance(i, int)]) == 0:
2025-07-31 18:14:38 +08:00
df.loc[loop, "e-loops(c)"] = 0
else:
2025-07-31 18:14:38 +08:00
df.loc[loop, "e-loops(c)"] = max(i for i in loop_data["coding"].keys() if isinstance(i, int)) + 1
if "running" in loop_data:
if len([i for i in loop_data["running"].keys() if isinstance(i, int)]) == 0:
2025-07-31 18:14:38 +08:00
df.loc[loop, "e-loops(r)"] = 0
else:
2025-07-31 18:14:38 +08:00
df.loc[loop, "e-loops(r)"] = max(i for i in loop_data["running"].keys() if isinstance(i, int)) + 1
if "feedback" in loop_data:
2025-08-18 17:48:49 +08:00
fb_emoji_str = (
"✅" if "no_tag" in loop_data["feedback"] and bool(loop_data["feedback"]["no_tag"]) else "❌"
)
2025-07-24 18:50:29 +08:00
if sota_loop_id == loop:
fb_emoji_str += " (💖SOTA)"
df.loc[loop, "Feedback"] = fb_emoji_str
else:
df.loc[loop, "Feedback"] = "N/A"
2025-07-08 18:20:27 +08:00
2025-07-24 18:50:29 +08:00
if only_success:
df = df[df["Feedback"].str.contains("✅", na=False)]
# Add color styling based on root_nodes
def style_dataframe_by_root(df, root_nodes):
# Create a color map for different root nodes - using colors that work well in both light and dark modes
unique_roots = list(set(root_nodes.values()))
colors = [
"rgba(255, 99, 132, 0.3)",
"rgba(54, 162, 235, 0.3)",
"rgba(75, 192, 75, 0.3)",
"rgba(255, 159, 64, 0.3)",
"rgba(153, 102, 255, 0.2)",
"rgba(255, 205, 86, 0.2)",
"rgba(199, 199, 199, 0.2)",
"rgba(83, 102, 255, 0.2)",
]
root_color_map = {root: colors[i % len(colors)] for i, root in enumerate(unique_roots)}
# Create styling function
def apply_color(row):
loop_id = row.name
if loop_id in root_nodes:
root_id = root_nodes[loop_id]
color = root_color_map.get(root_id, "rgba(128, 128, 128, 0.1)")
return [f"background-color: {color}"] * len(row)
return [""] * len(row)
return df.style.apply(apply_color, axis=1)
styled_df = style_dataframe_by_root(
df[df.columns[~df.columns.isin(["Hypothesis", "Reason", "Others"])]], root_nodes
)
st.dataframe(styled_df)
2025-07-24 18:50:29 +08:00
# timeline figure
if state.times:
with st.popover("Timeline", icon="⏱️", use_container_width=True):
st.plotly_chart(timeline_figure(state.times))
2025-07-08 18:20:27 +08:00
# scores curve
vscores = {}
for k, vs in valid_results.items():
if not vs.index.is_unique:
st.warning(f"Loop {k}'s valid scores index are not unique, only the last one will be kept to show.")
st.write(vs)
vscores[k] = vs[~vs.index.duplicated(keep="last")].iloc[:, 0]
if len(vscores) > 0:
metric_name = list(vscores.values())[0].name
else:
metric_name = "None"
vscores = pd.DataFrame(vscores)
if "ensemble" in vscores.index:
ensemble_row = vscores.loc[["ensemble"]]
vscores = pd.concat([ensemble_row, vscores.drop("ensemble")])
vscores = vscores.T
test_scores = df["Run Score (test)"].str.replace(r"[🥇🥈🥉]\s*", "", regex=True)
2025-07-24 19:15:01 +08:00
vscores["test"] = test_scores
2025-07-08 18:20:27 +08:00
vscores.index = [f"L{i}" for i in vscores.index]
vscores.columns.name = metric_name
with st.popover("Scores Curve", icon="📈", use_container_width=True):
st.plotly_chart(curve_figure(vscores))
2025-04-07 20:16:37 +08:00
st.markdown("### Hypotheses Table")
hypotheses_df = df.iloc[:, :8].copy()
others_expanded = pd.json_normalize(hypotheses_df["Others"].fillna({}))
others_expanded.index = hypotheses_df.index
hypotheses_df = hypotheses_df.drop("Others", axis=1)
hypotheses_df = hypotheses_df.drop("Parent N", axis=1)
hypotheses_df = pd.concat([hypotheses_df.iloc[:, :4], others_expanded, hypotheses_df.iloc[:, 4:]], axis=1)
styled_hypotheses_table = style_dataframe_by_root(hypotheses_df, root_nodes)
2025-04-07 20:16:37 +08:00
st.dataframe(
styled_hypotheses_table,
2025-04-07 20:16:37 +08:00
row_height=100,
column_config={
k: st.column_config.TextColumn(
k,
width=(
"small"
if k
in ["Component", "Root N", "Parent N", "Run Score (valid)", "Run Score (test)", "problem_label"]
else "medium"
),
)
for k in hypotheses_df.columns
2025-04-07 20:16:37 +08:00
},
)
def comp_stat_func(x: pd.DataFrame):
total_num = x.shape[0]
valid_num = x[x["Run Score (test)"] != "N/A"].shape[0]
success_num = x[x["Feedback"] == "✅"].shape[0]
2025-07-31 18:14:38 +08:00
avg_e_loops = x["e-loops(c)"].mean()
return pd.Series(
{
"Loop Num": total_num,
"Valid Loop": valid_num,
"Success Loop": success_num,
"Valid Rate": round(valid_num / total_num * 100, 2),
"Success Rate": round(success_num / total_num * 100, 2),
2025-07-31 18:14:38 +08:00
"Avg e-loops(c)": round(avg_e_loops, 2),
}
)
st1, st2 = st.columns([1, 1])
# component statistics
comp_df = (
df.loc[:, ["Component", "Run Score (test)", "Feedback", "e-loops(c)"]]
.groupby("Component")
2025-07-08 18:20:27 +08:00
.apply(comp_stat_func, include_groups=False)
)
comp_df.loc["Total"] = comp_df.sum()
comp_df.loc["Total", "Valid Rate"] = round(
comp_df.loc["Total", "Valid Loop"] / comp_df.loc["Total", "Loop Num"] * 100, 2
)
comp_df.loc["Total", "Success Rate"] = round(
comp_df.loc["Total", "Success Loop"] / comp_df.loc["Total", "Loop Num"] * 100, 2
)
comp_df["Valid Rate"] = comp_df["Valid Rate"].apply(lambda x: f"{x}%")
comp_df["Success Rate"] = comp_df["Success Rate"].apply(lambda x: f"{x}%")
2025-07-31 18:14:38 +08:00
comp_df.loc["Total", "Avg e-loops(c)"] = round(df["e-loops(c)"].mean(), 2)
with st2.popover("Component Statistics", icon="📊", use_container_width=True):
st.dataframe(comp_df)
# component time statistics
time_df = df.loc[:, ["Component", "Time", "Exp Gen", "Coding", "Running"]]
time_df = time_df.astype(
{
"Time": "timedelta64[ns]",
"Exp Gen": "timedelta64[ns]",
"Coding": "timedelta64[ns]",
"Running": "timedelta64[ns]",
}
)
time_stat_df = time_df.groupby("Component").sum()
time_stat_df.loc["Total"] = time_stat_df.sum()
time_stat_df.loc[:, "Exp Gen(%)"] = (time_stat_df["Exp Gen"] / time_stat_df["Time"] * 100).round(2)
time_stat_df.loc[:, "Coding(%)"] = (time_stat_df["Coding"] / time_stat_df["Time"] * 100).round(2)
time_stat_df.loc[:, "Running(%)"] = (time_stat_df["Running"] / time_stat_df["Time"] * 100).round(2)
for col in ["Time", "Exp Gen", "Coding", "Running"]:
time_stat_df[col] = time_stat_df[col].map(timedelta_to_str)
with st1.popover("Time Statistics", icon="⏱️", use_container_width=True):
st.dataframe(time_stat_df)
# COST curve
costs = df["COST($)"].astype(float)
costs.index = [f"L{i}" for i in costs.index]
cumulative_costs = costs.cumsum()
with st.popover("COST Curve", icon="💰", use_container_width=True):
fig = px.line(
x=costs.index,
y=[costs.values, cumulative_costs.values],
labels={"x": "Loop", "value": "COST($)"},
title="COST($) per Loop & Cumulative COST($)",
markers=True,
)
fig.update_traces(mode="lines+markers")
fig.data[0].name = "COST($) per Loop"
fig.data[1].name = "Cumulative COST($)"
st.plotly_chart(fig)
def stdout_win(loop_id: int):
stdout = load_stdout(state.log_folder / f"{state.log_path}.stdout")
if stdout.startswith("Please Set"):
st.toast(stdout, icon="🟡")
return
start_index = stdout.find(f"Start Loop {loop_id}")
end_index = stdout.find(f"Start Loop {loop_id + 1}")
2025-06-04 18:30:29 +08:00
loop_stdout = LogColors.remove_ansi_codes(stdout[start_index:end_index])
with st.container(border=True):
st.subheader(f"Loop {loop_id} stdout")
pattern = f"Start Loop {loop_id}, " + r"Step \d+: \w+"
matches = re.finditer(pattern, loop_stdout)
step_stdouts = {}
for match in matches:
step = match.group(0)
si = match.start()
ei = loop_stdout.find(f"Start Loop {loop_id}", match.end())
step_stdouts[step] = loop_stdout[si:ei].strip()
for k, v in step_stdouts.items():
with st.expander(k, expanded=False):
st.code(v, language="log", wrap_lines=True)
def get_folders_sorted(log_path, sort_by_time=False):
"""
Cache and return the sorted list of folders, with progress printing.
:param log_path: Log path
:param sort_by_time: Whether to sort by time, default False (sort by name)
"""
if not log_path.exists():
st.toast(f"Path {log_path} does not exist!")
return []
with st.spinner("Loading folder list..."):
folders = [folder for folder in log_path.iterdir() if is_valid_session(folder)]
if sort_by_time:
folders = sorted(folders, key=lambda folder: folder.stat().st_mtime, reverse=True)
else:
folders = sorted(folders, key=lambda folder: folder.name)
return [folder.name for folder in folders]
# UI - Sidebar
with st.sidebar:
# TODO: 只是临时的功能
if any("log.srv" in folder for folder in state.log_folders):
day_map = {"srv": "最近(srv)", "srv2": "上一批(srv2)", "srv3": "上上批(srv3)"}
day_srv = st.radio("选择批次", ["srv", "srv2", "srv3"], format_func=lambda x: day_map[x], horizontal=True)
if day_srv == "srv":
state.log_folders = [re.sub(r"log\.srv\d*", "log.srv", folder) for folder in state.log_folders]
elif day_srv == "srv2":
state.log_folders = [re.sub(r"log\.srv\d*", "log.srv2", folder) for folder in state.log_folders]
elif day_srv == "srv3":
state.log_folders = [re.sub(r"log\.srv\d*", "log.srv3", folder) for folder in state.log_folders]
if "log_folder" in st.query_params:
state.log_folder = Path(st.query_params["log_folder"])
2025-05-06 11:34:21 +08:00
state.log_folders = [str(state.log_folder)]
else:
2025-05-06 11:34:21 +08:00
state.log_folder = Path(
st.radio(
f"Select :blue[**one log folder**]",
state.log_folders,
format_func=lambda x: x[x.rfind("amlt") + 5 :].split("/")[0] if "amlt" in x else x,
2025-05-06 11:34:21 +08:00
)
)
if not state.log_folder.exists():
st.warning(f"Path {state.log_folder} does not exist!")
else:
folders = get_folders_sorted(state.log_folder, sort_by_time=False)
if "selection" in st.query_params:
default_index = (
folders.index(st.query_params["selection"]) if st.query_params["selection"] in folders else 0
)
else:
default_index = 0
state.log_path = st.selectbox(
f"Select from :blue[**{state.log_folder.absolute()}**]", folders, index=default_index
)
if st.button("Refresh Data"):
if state.log_path is None:
st.toast("Please select a log path first!", icon="🟡")
st.stop()
state.times = load_times_info(state.log_folder / state.log_path)
2025-07-08 18:20:27 +08:00
state.data, state.llm_data, state.token_costs = load_data(state.log_folder / state.log_path)
2025-08-28 14:18:58 +08:00
state.sota_info = get_sota_exp_stat(Path(state.log_folder) / state.log_path, selector="auto")
st.rerun()
2025-06-30 20:10:34 +08:00
st.toggle("**Show LLM Log**", key="show_llm_log")
st.toggle("*Show stdout*", key="show_stdout")
st.toggle("*Show save workspace*", key="show_save_input")
st.markdown(f"""
- [Summary](#summary)
- [Exp Gen](#exp-gen)
- [Coding](#coding)
- [Running](#running)
- [Feedback](#feedback)
- [Record](#record)
- [SOTA Experiment](#sota-exp)
""")
2025-05-04 15:51:32 +08:00
def get_state_data_range(state_data):
# we have a "competition" key in state_data
# like dict_keys(['competition', 10, 11, 12, 13, 14])
2025-07-08 18:20:27 +08:00
keys = [
k
for k in state_data.keys()
if isinstance(k, int) and "direct_exp_gen" in state_data[k] and "no_tag" in state_data[k]["direct_exp_gen"]
]
2025-05-04 15:51:32 +08:00
return min(keys), max(keys)
# UI - Main
2025-06-05 10:56:54 +08:00
if "competition" in state.data:
2025-07-18 16:01:39 +08:00
st.title(
state.data["competition"]
+ f" ([share_link](/ds_trace?log_folder={state.log_folder}&selection={state.log_path}))"
)
2025-07-08 18:20:27 +08:00
summarize_win()
min_id, max_id = get_state_data_range(state.data)
if max_id > min_id:
2025-05-04 15:51:32 +08:00
loop_id = st.slider("Loop", min_id, max_id, min_id)
else:
loop_id = min_id
if state.show_stdout:
stdout_win(loop_id)
2025-05-06 11:34:21 +08:00
main_win(loop_id, state.llm_data[loop_id] if loop_id in state.llm_data else None)