From 2126062edf9e8ee476fbb6468d4dda78988b3430 Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Thu, 30 Apr 2026 09:34:59 +0200 Subject: [PATCH] fix(security): nosec for B608/B701 false positives in UI and template code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B608: Bandit flags any f-string containing "select" as potential SQL injection. All four cases (app.py, ds_trace.py, llm_st.py, merge.py) are Streamlit UI labels or log messages — not database queries. B701: Jinja2 autoescape=False warnings in coder.py and utils.py are false positives — these render Python code and plain-text templates, not HTML. Enabling autoescape would corrupt the rendered code. Co-Authored-By: Claude Sonnet 4.6 --- rdagent/log/ui/app.py | 1133 ++++++++++++++++ rdagent/log/ui/ds_trace.py | 1205 +++++++++++++++++ rdagent/log/ui/llm_st.py | 306 +++++ .../data_science/proposal/exp_gen/merge.py | 2 +- rdagent/scenarios/kaggle/developer/coder.py | 4 +- rdagent/scenarios/qlib/experiment/utils.py | 2 +- 6 files changed, 2648 insertions(+), 4 deletions(-) create mode 100644 rdagent/log/ui/app.py create mode 100644 rdagent/log/ui/ds_trace.py create mode 100644 rdagent/log/ui/llm_st.py diff --git a/rdagent/log/ui/app.py b/rdagent/log/ui/app.py new file mode 100644 index 00000000..f72d62e5 --- /dev/null +++ b/rdagent/log/ui/app.py @@ -0,0 +1,1133 @@ +import argparse +import re +import textwrap +from collections import defaultdict +from datetime import datetime, timezone +from importlib.resources import files as rfiles +from pathlib import Path +from typing import Callable, Type + +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +import streamlit as st +from plotly.subplots import make_subplots +from streamlit import session_state as state +from streamlit_theme import st_theme + +from rdagent.components.coder.factor_coder.evaluators import FactorSingleFeedback # nosec +from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask +from rdagent.components.coder.model_coder.evaluators import ModelSingleFeedback # nosec +from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask +from rdagent.core.proposal import Hypothesis, HypothesisFeedback +from rdagent.core.scenario import Scenario +from rdagent.log.base import Message +from rdagent.log.storage import FileStorage +from rdagent.log.ui.qlib_report_figure import report_figure +from rdagent.scenarios.general_model.scenario import GeneralModelScenario +from rdagent.scenarios.kaggle.experiment.scenario import KGScenario +from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorScenario +from rdagent.scenarios.qlib.experiment.factor_from_report_experiment import ( + QlibFactorFromReportScenario, +) +from rdagent.scenarios.qlib.experiment.model_experiment import ( + QlibModelExperiment, + QlibModelScenario, +) +from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario + +st.set_page_config(layout="wide", page_title="RD-Agent", page_icon="🎓", initial_sidebar_state="expanded") + + +# 获取log_path参数 +parser = argparse.ArgumentParser(description="RD-Agent Streamlit App") +parser.add_argument("--log_dir", type=str, help="Path to the log directory") +parser.add_argument("--debug", action="store_true", help="Enable debug mode") +args = parser.parse_args() +if args.log_dir: + main_log_path = Path(args.log_dir) + if not main_log_path.exists(): + st.error(f"Log dir `{main_log_path}` does not exist!") + st.stop() +else: + main_log_path = None + + +QLIB_SELECTED_METRICS = [ + "IC", + "1day.excess_return_with_cost.annualized_return", + "1day.excess_return_with_cost.information_ratio", + "1day.excess_return_with_cost.max_drawdown", +] + +SIMILAR_SCENARIOS = ( + QlibModelScenario, + QlibFactorScenario, + QlibFactorFromReportScenario, + QlibQuantScenario, + KGScenario, +) + + +def filter_log_folders(main_log_path): + """ + Filter and return the log folders relative to the main log path. + """ + folders = [folder.relative_to(main_log_path) for folder in main_log_path.iterdir() if folder.is_dir()] + folders = sorted(folders, key=lambda x: x.name) + return folders + + +if "log_path" not in state: + if main_log_path: + state.log_path = filter_log_folders(main_log_path)[0] + else: + state.log_path = None + st.toast(":red[**Please Set Log Path!**]", icon="⚠️") + +if "scenario" not in state: + state.scenario = None + +if "fs" not in state: + state.fs = None + +if "msgs" not in state: + state.msgs = defaultdict(lambda: defaultdict(list)) + +if "last_msg" not in state: + state.last_msg = None + +if "current_tags" not in state: + state.current_tags = [] + +if "lround" not in state: + state.lround = 0 # RD Loop Round + +if "erounds" not in state: + state.erounds = defaultdict(int) # Evolving Rounds in each RD Loop + +if "e_decisions" not in state: + state.e_decisions = defaultdict(lambda: defaultdict(tuple)) + +# Summary Info +if "hypotheses" not in state: + # Hypotheses in each RD Loop + state.hypotheses = defaultdict(None) + +if "h_decisions" not in state: + state.h_decisions = defaultdict(bool) + +if "metric_series" not in state: + state.metric_series = [] + +if "all_metric_series" not in state: + state.all_metric_series = [] + +# Factor Task Baseline +if "alpha_baseline_metrics" not in state: + state.alpha_baseline_metrics = None + + +def should_display(msg: Message): + for t in state.excluded_tags + ["debug_tpl", "debug_llm"]: + if t in msg.tag.split("."): + return False + + if type(msg.content).__name__ in state.excluded_types: + return False + + return True + + +def get_msgs_until(end_func: Callable[[Message], bool] = lambda _: True): + if state.fs: + while True: + try: + msg = next(state.fs) + if should_display(msg): + tags = msg.tag.split(".") + if "hypothesis generation" in msg.tag: + state.lround += 1 + + # new scenario gen this tags, old version UI not have these tags. + msg.tag = re.sub(r"\.evo_loop_\d+", "", msg.tag) + msg.tag = re.sub(r"Loop_\d+\.[^.]+", "", msg.tag) + msg.tag = re.sub(r"\.\.", ".", msg.tag) + + # remove old redundant tags + msg.tag = re.sub(r"init\.", "", msg.tag) + msg.tag = re.sub(r"r\.", "", msg.tag) + msg.tag = re.sub(r"d\.", "", msg.tag) + msg.tag = re.sub(r"ef\.", "", msg.tag) + + msg.tag = msg.tag.strip(".") + + if "evolving code" not in state.current_tags and "evolving code" in tags: + state.erounds[state.lround] += 1 + + state.current_tags = tags + state.last_msg = msg + + # Update Summary Info + if "runner result" in tags: + # factor baseline exp metrics + if ( + isinstance(state.scenario, (QlibFactorScenario, QlibQuantScenario)) + and state.alpha_baseline_metrics is None + ): + try: + sms = msg.content.based_experiments[0].result + except AttributeError: + sms = msg.content.based_experiments[0].__dict__["result"] + sms = sms.loc[QLIB_SELECTED_METRICS] + sms.name = "Alpha Base" + state.alpha_baseline_metrics = sms + + if state.lround == 1 and len(msg.content.based_experiments) > 0: + try: + sms = msg.content.based_experiments[-1].result + except AttributeError: + sms = msg.content.based_experiments[-1].__dict__["result"] + if sms is not None: + if isinstance( + state.scenario, + ( + QlibModelScenario, + QlibFactorFromReportScenario, + QlibFactorScenario, + QlibQuantScenario, + ), + ): + sms_all = sms + sms = sms.loc[QLIB_SELECTED_METRICS] + sms.name = f"Baseline" + state.metric_series.append(sms) + state.all_metric_series.append(sms_all) + + # common metrics + try: + sms = msg.content.result + except AttributeError: + sms = msg.content.__dict__["result"] + if isinstance( + state.scenario, + ( + QlibModelScenario, + QlibFactorFromReportScenario, + QlibFactorScenario, + QlibQuantScenario, + ), + ): + sms_all = sms + sms = sms.loc[QLIB_SELECTED_METRICS] + + sms.name = f"Round {state.lround}" + sms_all.name = f"Round {state.lround}" + state.metric_series.append(sms) + state.all_metric_series.append(sms_all) + elif "hypothesis generation" in tags: + state.hypotheses[state.lround] = msg.content + elif "evolving code" in tags: + msg.content = [i for i in msg.content if i] + elif "evolving feedback" in tags: + total_len = len(msg.content) + none_num = total_len - len(msg.content) + right_num = 0 + for wsf in msg.content: + if wsf.final_decision: + right_num += 1 + wrong_num = len(msg.content) - right_num + state.e_decisions[state.lround][state.erounds[state.lround]] = ( + right_num, + wrong_num, + none_num, + ) + elif "feedback" in tags and isinstance(msg.content, HypothesisFeedback): + state.h_decisions[state.lround] = msg.content.decision + + state.msgs[state.lround][msg.tag].append(msg) + + # Stop Getting Logs + if end_func(msg): + break + except StopIteration: + st.toast(":red[**No More Logs to Show!**]", icon="🛑") + break + + +def refresh(same_trace: bool = False): + if state.log_path is None: + st.toast(":red[**Please Set Log Path!**]", icon="⚠️") + return + + if main_log_path: + state.fs = FileStorage(main_log_path / state.log_path).iter_msg() + else: + state.fs = FileStorage(state.log_path).iter_msg() + + # detect scenario + if not same_trace: + get_msgs_until(lambda m: isinstance(m.content, Scenario)) + if state.last_msg is None or not isinstance(state.last_msg.content, Scenario): + st.write(state.msgs) + st.toast(":red[**No Scenario Info detected**]", icon="❗") + state.scenario = None + else: + state.scenario = state.last_msg.content + st.toast(f":green[**Scenario Info detected**] *{type(state.scenario).__name__}*", icon="✅") + + state.msgs = defaultdict(lambda: defaultdict(list)) + state.lround = 0 + state.erounds = defaultdict(int) + state.e_decisions = defaultdict(lambda: defaultdict(tuple)) + state.hypotheses = defaultdict(None) + state.h_decisions = defaultdict(bool) + state.metric_series = [] + state.all_metric_series = [] + state.last_msg = None + state.current_tags = [] + state.alpha_baseline_metrics = None + + +def evolving_feedback_window(wsf: FactorSingleFeedback | ModelSingleFeedback): + if isinstance(wsf, FactorSingleFeedback): + ffc, efc, cfc, vfc = st.tabs( + ["**Final Feedback🏁**", "Execution Feedback🖥️", "Code Feedback📄", "Value Feedback🔢"] + ) + with ffc: + st.markdown(wsf.final_feedback) + with efc: + st.code(wsf.execution_feedback, language="log") # nosec + with cfc: + st.markdown(wsf.code_feedback) + with vfc: + st.markdown(wsf.value_feedback) + elif isinstance(wsf, ModelSingleFeedback): + ffc, efc, cfc, msfc, vfc = st.tabs( + [ + "**Final Feedback🏁**", + "Execution Feedback🖥️", + "Code Feedback📄", + "Model Shape Feedback📐", + "Value Feedback🔢", + ] + ) + with ffc: + st.markdown(wsf.final_feedback) + with efc: + st.code(wsf.execution_feedback, language="log") # nosec + with cfc: + st.markdown(wsf.code_feedback) + with msfc: + st.markdown(wsf.shape_feedback) + with vfc: + st.markdown(wsf.value_feedback) + + +def display_hypotheses(hypotheses: dict[int, Hypothesis], decisions: dict[int, bool], success_only: bool = False): + name_dict = { + "hypothesis": "RD-Agent proposes the hypothesis⬇️", + "concise_justification": "because the reason⬇️", + "concise_observation": "based on the observation⬇️", + "concise_knowledge": "Knowledge⬇️ gained after practice", + } + if success_only: + shd = {k: v.__dict__ for k, v in hypotheses.items() if decisions[k]} + else: + shd = {k: v.__dict__ for k, v in hypotheses.items()} + df = pd.DataFrame(shd).T + + if "concise_observation" in df.columns and "concise_justification" in df.columns: + df["concise_observation"], df["concise_justification"] = df["concise_justification"], df["concise_observation"] + df.rename( + columns={"concise_observation": "concise_justification", "concise_justification": "concise_observation"}, + inplace=True, + ) + if "reason" in df.columns: + df.drop(["reason"], axis=1, inplace=True) + if "concise_reason" in df.columns: + df.drop(["concise_reason"], axis=1, inplace=True) + + df.columns = df.columns.map(lambda x: name_dict.get(x, x)) + for col in list(df.columns): + if all([value is None for value in df[col]]): + df.drop([col], axis=1, inplace=True) + + def style_rows(row): + if decisions[row.name]: + return ["color: green;"] * len(row) + return [""] * len(row) + + def style_columns(col): + if col.name != name_dict.get("hypothesis", "hypothesis"): + return ["font-style: italic;"] * len(col) + return ["font-weight: bold;"] * len(col) + + # st.dataframe(df.style.apply(style_rows, axis=1).apply(style_columns, axis=0)) + st.markdown(df.style.apply(style_rows, axis=1).apply(style_columns, axis=0).to_html(), unsafe_allow_html=True) + + +def metrics_window(df: pd.DataFrame, R: int, C: int, *, height: int = 300, colors: list[str] = None): + fig = make_subplots(rows=R, cols=C, subplot_titles=df.columns) + + def hypothesis_hover_text(h: Hypothesis, d: bool = False): + color = "green" if d else "black" + text = h.hypothesis + lines = textwrap.wrap(text, width=60) + return f"{'
'.join(lines)}
" + + hover_texts = [ + hypothesis_hover_text(state.hypotheses[int(i[6:])], state.h_decisions[int(i[6:])]) + for i in df.index + if i != "Alpha Base" and i != "Baseline" + ] + if state.alpha_baseline_metrics is not None: + hover_texts = ["Baseline"] + hover_texts + for ci, col in enumerate(df.columns): + row = ci // C + 1 + col_num = ci % C + 1 + fig.add_trace( + go.Scatter( + x=df.index, + y=df[col], + name=col, + mode="lines+markers", + connectgaps=True, + marker=dict(size=10, color=colors[ci]) if colors else dict(size=10), + hovertext=hover_texts, + hovertemplate="%{hovertext}

%{x} Value: %{y}", + ), + row=row, + col=col_num, + ) + fig.update_layout(showlegend=False, height=height) + + if state.alpha_baseline_metrics is not None: + for i in range(1, R + 1): # 行 + for j in range(1, C + 1): # 列 + fig.update_xaxes( + tickvals=[df.index[0]] + list(df.index[1:]), + ticktext=[f'{df.index[0]}'] + list(df.index[1:]), + row=i, + col=j, + ) + st.plotly_chart(fig) + + from io import BytesIO + + buffer = BytesIO() + df.to_csv(buffer) + buffer.seek(0) + st.download_button(label="download the metrics (csv)", data=buffer, file_name="metrics.csv", mime="text/csv") + + +def summary_window(): + if isinstance(state.scenario, SIMILAR_SCENARIOS): + st.header("Summary📊", divider="rainbow", anchor="_summary") + if state.lround == 0: + return + with st.container(): + # TODO: not fixed height + with st.container(): + bc, cc = st.columns([2, 2], vertical_alignment="center") + with bc: + st.subheader("Metrics📈", anchor="_metrics") + with cc: + show_true_only = st.toggle("successful hypotheses", value=False) + + # hypotheses_c, chart_c = st.columns([2, 3]) + chart_c = st.container() + hypotheses_c = st.container() + + with hypotheses_c: + st.subheader("Hypotheses🏅", anchor="_hypotheses") + display_hypotheses(state.hypotheses, state.h_decisions, show_true_only) + + with chart_c: + if isinstance(state.scenario, QlibFactorScenario) and state.alpha_baseline_metrics is not None: + df = pd.DataFrame([state.alpha_baseline_metrics] + state.metric_series[1:]) + elif isinstance(state.scenario, QlibQuantScenario) and state.alpha_baseline_metrics is not None: + df = pd.DataFrame([state.alpha_baseline_metrics] + state.metric_series[1:]) + else: + df = pd.DataFrame(state.metric_series) + if show_true_only and len(state.hypotheses) >= len(state.metric_series): + if state.alpha_baseline_metrics is not None: + selected = ["Alpha Base"] + [ + i for i in df.index if i == "Baseline" or state.h_decisions[int(i[6:])] + ] + else: + selected = [i for i in df.index if i == "Baseline" or state.h_decisions[int(i[6:])]] + df = df.loc[selected] + if df.shape[0] == 1: + st.table(df.iloc[0]) + elif df.shape[0] > 1: + if df.shape[1] == 1: + fig = px.line(df, x=df.index, y=df.columns, markers=True) + fig.update_layout(xaxis_title="Loop Round", yaxis_title=None) + st.plotly_chart(fig) + else: + metrics_window(df, 1, 4, height=300, colors=["red", "blue", "orange", "green"]) + + elif isinstance(state.scenario, GeneralModelScenario): + with st.container(border=True): + st.subheader("Summary📊", divider="rainbow", anchor="_summary") + if len(state.msgs[state.lround]["evolving code"]) > 0: + # pass + ws: list[FactorFBWorkspace | ModelFBWorkspace] = state.msgs[state.lround]["evolving code"][-1].content + # All Tasks + + tab_names = [ + w.target_task.factor_name if isinstance(w.target_task, FactorTask) else w.target_task.name + for w in ws + ] + for j in range(len(ws)): + if state.msgs[state.lround]["evolving feedback"][-1].content[j].final_decision: + tab_names[j] += "✔️" + else: + tab_names[j] += "❌" + + wtabs = st.tabs(tab_names) + for j, w in enumerate(ws): + with wtabs[j]: + # Evolving Code + for k, v in w.file_dict.items(): + with st.expander(f":green[`{k}`]", expanded=False): + st.code(v, language="python") + + # Evolving Feedback + evolving_feedback_window(state.msgs[state.lround]["evolving feedback"][-1].content[j]) + + +def tabs_hint(): + st.markdown( + "

You can navigate through the tabs using ⬅️ ➡️ or by holding Shift and scrolling with the mouse wheel🖱️.

", + unsafe_allow_html=True, + ) + + +def tasks_window(tasks: list[FactorTask | ModelTask]): + if isinstance(tasks[0], FactorTask): + st.markdown("**Factor Tasks🚩**") + tnames = [f.factor_name for f in tasks] + if sum(len(tn) for tn in tnames) > 100: + tabs_hint() + tabs = st.tabs(tnames) + for i, ft in enumerate(tasks): + with tabs[i]: + # st.markdown(f"**Factor Name**: {ft.factor_name}") + st.markdown(f"**Description**: {ft.factor_description}") + st.latex("Formulation") + st.latex(ft.factor_formulation) + + mks = "| Variable | Description |\n| --- | --- |\n" + if isinstance(ft.variables, dict): + for v, d in ft.variables.items(): + mks += f"| ${v}$ | {d} |\n" + st.markdown(mks) + + elif isinstance(tasks[0], ModelTask): + st.markdown("**Model Tasks🚩**") + tnames = [m.name for m in tasks] + if sum(len(tn) for tn in tnames) > 100: + tabs_hint() + tabs = st.tabs(tnames) + for i, mt in enumerate(tasks): + with tabs[i]: + # st.markdown(f"**Model Name**: {mt.name}") + st.markdown(f"**Model Type**: {mt.model_type}") + st.markdown(f"**Description**: {mt.description}") + st.latex("Formulation") + st.latex(mt.formulation) + + mks = "| Variable | Description |\n| --- | --- |\n" + if mt.variables: + for v, d in mt.variables.items(): + mks += f"| ${v}$ | {d} |\n" + st.markdown(mks) + st.markdown(f"**Train Para**: {mt.training_hyperparameters}") + + +def research_window(): + with st.container(border=True): + title = "Research🔍" if isinstance(state.scenario, SIMILAR_SCENARIOS) else "Research🔍 (reader)" + st.subheader(title, divider="blue", anchor="_research") + if isinstance(state.scenario, SIMILAR_SCENARIOS): + # pdf image + if pim := state.msgs[round]["load_pdf_screenshot"]: + for i in range(min(2, len(pim))): + st.image(pim[i].content, use_container_width=True) + + # Hypothesis + if hg := state.msgs[round]["hypothesis generation"]: + st.markdown("**Hypothesis💡**") # 🧠 + h: Hypothesis = hg[0].content + st.markdown(f""" +- **Hypothesis**: {h.hypothesis} +- **Reason**: {h.reason}""") + + if eg := state.msgs[round]["experiment generation"]: + tasks_window(eg[0].content) + + elif isinstance(state.scenario, GeneralModelScenario): + # pdf image + c1, c2 = st.columns([2, 3]) + with c1: + if pim := state.msgs[0]["pdf_image"]: + for i in range(len(pim)): + st.image(pim[i].content, use_container_width=True) + + # loaded model exp + with c2: + if mem := state.msgs[0]["load_experiment"]: + me: QlibModelExperiment = mem[0].content + tasks_window(me.sub_tasks) + + +def feedback_window(): + # st.write(round) + # # Check if metric series exists and has the matching round + # if state.all_metric_series: + # for metric in state.all_metric_series: + # if metric.name == f"Round {round}": + # # Select specific metrics with cost + # selected_metrics_with_cost = { + # 'IC': float(f"{metric['IC']:.4f}"), + # 'ICIR': float(f"{metric['ICIR']:.4f}"), + # 'Rank IC': float(f"{metric['Rank IC']:.4f}"), + # 'Rank ICIR': float(f"{metric['Rank ICIR']:.4f}"), + # 'ARR': float(f"{metric['1day.excess_return_with_cost.annualized_return']:.4f}"), + # 'IR': float(f"{metric['1day.excess_return_with_cost.information_ratio']:.4f}"), + # 'MDD': float(f"{metric['1day.excess_return_with_cost.max_drawdown']:.4f}"), + # 'Sharpe': float(f"{metric['1day.excess_return_with_cost.annualized_return'] / abs(metric['1day.excess_return_with_cost.max_drawdown']):.4f}") + # } + # st.write("With Cost Metrics:") + # st.write(pd.Series(selected_metrics_with_cost)) + + # # Select specific metrics without cost + # selected_metrics_without_cost = { + # 'IC': float(f"{metric['IC']:.4f}"), + # 'ICIR': float(f"{metric['ICIR']:.4f}"), + # 'Rank IC': float(f"{metric['Rank IC']:.4f}"), + # 'Rank ICIR': float(f"{metric['Rank ICIR']:.4f}"), + # 'ARR': float(f"{metric['1day.excess_return_without_cost.annualized_return']:.4f}"), + # 'IR': float(f"{metric['1day.excess_return_without_cost.information_ratio']:.4f}"), + # 'MDD': float(f"{metric['1day.excess_return_without_cost.max_drawdown']:.4f}"), + # 'Sharpe': float(f"{metric['1day.excess_return_without_cost.annualized_return'] / abs(metric['1day.excess_return_without_cost.max_drawdown']):.4f}") + # } + # st.write("Without Cost Metrics:") + # st.write(pd.Series(selected_metrics_without_cost)) + # break + if isinstance(state.scenario, SIMILAR_SCENARIOS): + with st.container(border=True): + st.subheader("Feedback📝", divider="orange", anchor="_feedback") + + if state.lround > 0 and isinstance( + state.scenario, + (QlibModelScenario, QlibFactorScenario, QlibFactorFromReportScenario, QlibQuantScenario, KGScenario), + ): + if fbr := state.msgs[round]["runner result"]: + try: + st.write("workspace") + st.write(fbr[0].content.experiment_workspace.workspace_path) + st.write(fbr[0].content.stdout) + except Exception as e: + st.error(f"Error displaying workspace path: {str(e)}") + with st.expander("**Config⚙️**", expanded=True): + st.markdown(state.scenario.experiment_setting, unsafe_allow_html=True) + + if fb := state.msgs[round]["feedback"]: + if fbr := state.msgs[round]["Quantitative Backtesting Chart"]: + st.markdown("**Returns📈**") + fig = report_figure(fbr[0].content) + st.plotly_chart(fig) + st.markdown("**Hypothesis Feedback🔍**") + h: HypothesisFeedback = fb[0].content + st.markdown(f""" +- **Observations**: {h.observations} +- **Hypothesis Evaluation**: {h.hypothesis_evaluation} # nosec +- **New Hypothesis**: {h.new_hypothesis} +- **Decision**: {h.decision} +- **Reason**: {h.reason}""") + + if isinstance(state.scenario, KGScenario): + if fbe := state.msgs[round]["runner result"]: + submission_path = fbe[0].content.experiment_workspace.workspace_path / "submission.csv" + st.markdown( + f":green[**Exp Workspace**]: {str(fbe[0].content.experiment_workspace.workspace_path.absolute())}" + ) + try: + data = submission_path.read_bytes() + st.download_button( + label="**Download** submission.csv", + data=data, + file_name="submission.csv", + mime="text/csv", + ) + except Exception as e: + st.markdown(f":red[**Download Button Error**]: {e}") + + +def evolving_window(): + title = "Development🛠️" if isinstance(state.scenario, SIMILAR_SCENARIOS) else "Development🛠️ (evolving coder)" + st.subheader(title, divider="green", anchor="_development") + + # Evolving Status + if state.erounds[round] > 0: + st.markdown("**☑️ Evolving Status**") + es = state.e_decisions[round] + e_status_mks = "".join(f"| {ei} " for ei in range(1, state.erounds[round] + 1)) + "|\n" + e_status_mks += "|--" * state.erounds[round] + "|\n" + for ei, estatus in es.items(): + if not estatus: + estatus = (0, 0, 0) + e_status_mks += "| " + "🕙
" * estatus[2] + "✔️
" * estatus[0] + "❌
" * estatus[1] + " " + e_status_mks += "|\n" + st.markdown(e_status_mks, unsafe_allow_html=True) + + # Evolving Tabs + if state.erounds[round] > 0: + if state.erounds[round] > 1: + evolving_round = st.radio( + "**🔄️Evolving Rounds**", + horizontal=True, + options=range(1, state.erounds[round] + 1), + index=state.erounds[round] - 1, + key="show_eround", + ) + else: + evolving_round = 1 + + ws: list[FactorFBWorkspace | ModelFBWorkspace] = state.msgs[round]["evolving code"][evolving_round - 1].content + # All Tasks + + tab_names = [ + w.target_task.factor_name if isinstance(w.target_task, FactorTask) else w.target_task.name for w in ws + ] + if len(state.msgs[round]["evolving feedback"]) >= evolving_round: + for j in range(len(ws)): + if state.msgs[round]["evolving feedback"][evolving_round - 1].content[j].final_decision: + tab_names[j] += "✔️" + else: + tab_names[j] += "❌" + if sum(len(tn) for tn in tab_names) > 100: + tabs_hint() + wtabs = st.tabs(tab_names) + for j, w in enumerate(ws): + with wtabs[j]: + # Evolving Code + st.markdown(f"**Workspace Path**: {w.workspace_path}") + for k, v in w.file_dict.items(): + with st.expander(f":green[`{k}`]", expanded=True): + st.code(v, language="python") + + # Evolving Feedback + if len(state.msgs[round]["evolving feedback"]) >= evolving_round: + evolving_feedback_window(state.msgs[round]["evolving feedback"][evolving_round - 1].content[j]) + + +toc = """ +## [Scenario Description📖](#_scenario) +## [Summary📊](#_summary) +- [**Metrics📈**](#_metrics) +- [**Hypotheses🏅**](#_hypotheses) +## [RD-Loops♾️](#_rdloops) +- [**Research🔍**](#_research) +- [**Development🛠️**](#_development) +- [**Feedback📝**](#_feedback) +""" +if isinstance(state.scenario, GeneralModelScenario): + toc = """ +## [Scenario Description📖](#_scenario) +### [Summary📊](#_summary) +### [Research🔍](#_research) +### [Development🛠️](#_development) +""" +# Config Sidebar +with st.sidebar: + st.markdown("# RD-Agent🤖 [:grey[@GitHub]](https://github.com/microsoft/RD-Agent)") + st.subheader(":blue[Table of Content]", divider="blue") + st.markdown(toc) + st.subheader(":orange[Control Panel]", divider="red") + + with st.container(border=True): + if main_log_path: + lc1, lc2 = st.columns([1, 2], vertical_alignment="center") + with lc1: + st.markdown(":blue[**Log Path**]") + with lc2: + manually = st.toggle("Manual Input") + if manually: + st.text_input("log path", key="log_path", on_change=refresh, label_visibility="collapsed") + else: + folders = filter_log_folders(main_log_path) + st.selectbox(f"**Select from `{main_log_path}`**", folders, key="log_path", on_change=refresh) # nosec B608 — not SQL, Bandit false positive on "Select" in UI label + else: + st.text_input(":blue[**log path**]", key="log_path", on_change=refresh) + + c1, c2 = st.columns([1, 1], vertical_alignment="center") + with c1: + if st.button(":green[**All Loops**]", use_container_width=True): + if not state.fs: + refresh() + get_msgs_until(lambda m: False) + if st.button("**Reset**", use_container_width=True): + refresh(same_trace=True) + with c2: + if st.button(":green[Next Loop]", use_container_width=True): + if not state.fs: + refresh() + get_msgs_until(lambda m: "feedback" in m.tag and "evolving feedback" not in m.tag) + + if st.button("Next Step", use_container_width=True): + if not state.fs: + refresh() + get_msgs_until(lambda m: "evolving feedback" in m.tag) + + with st.popover(":orange[**Config⚙️**]", use_container_width=True): + st.multiselect("excluded log tags", ["llm_messages"], ["llm_messages"], key="excluded_tags") + st.multiselect("excluded log types", ["str", "dict", "list"], ["str"], key="excluded_types") + + if args.debug: + debug = st.toggle("debug", value=False) + + if debug: + if st.button("Single Step Run", use_container_width=True): + get_msgs_until() + else: + debug = False + + +# Debug Info Window +if debug: + with st.expander(":red[**Debug Info**]", expanded=True): + dcol1, dcol2 = st.columns([1, 3]) + with dcol1: + st.markdown( + f"**log path**: {state.log_path}\n\n" + f"**excluded tags**: {state.excluded_tags}\n\n" + f"**excluded types**: {state.excluded_types}\n\n" + f":blue[**message id**]: {sum(sum(len(tmsgs) for tmsgs in rmsgs.values()) for rmsgs in state.msgs.values())}\n\n" + f":blue[**round**]: {state.lround}\n\n" + f":blue[**evolving round**]: {state.erounds[state.lround]}\n\n" + ) + with dcol2: + if state.last_msg: + st.write(state.last_msg) + if isinstance(state.last_msg.content, list): + st.write(state.last_msg.content[0]) + elif isinstance(state.last_msg.content, dict): + st.write(state.last_msg.content) + elif not isinstance(state.last_msg.content, str): + try: + st.write(state.last_msg.content.__dict__) + except: + st.write(type(state.last_msg.content)) + +if state.log_path and state.fs is None: + refresh() + +# Main Window +header_c1, header_c3 = st.columns([1, 6], vertical_alignment="center") +with st.container(): + with header_c1: + st.image("https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b?ver=5c31") + with header_c3: + st.markdown( + """ +

+ RD-Agent:
LLM-based autonomous evolving agents for industrial data-driven R&D +

+ """, + unsafe_allow_html=True, + ) + +# Project Info +with st.container(): + image_c, scen_c = st.columns([3, 3], vertical_alignment="center") + with image_c: + img_path = rfiles("rdagent.log.ui").joinpath("flow.png") + st.image(str(img_path), use_container_width=True) + with scen_c: + st.header("Scenario Description📖", divider="violet", anchor="_scenario") + if state.scenario is not None: + theme = st_theme() + if theme: + theme = theme.get("base", "light") + css = f""" + +""" + st.markdown(state.scenario.rich_style_description + css, unsafe_allow_html=True) + + +def analyze_task_completion(): + st.header("Task Completion Analysis", divider="orange") + + # Dictionary to store results for all loops + completion_stats = {} + + # Iterate through all loops + for loop_round in state.msgs.keys(): + if loop_round == 0: # Skip initialization round + continue + + max_evolving_round = state.erounds[loop_round] + if max_evolving_round == 0: + continue + + # Track tasks that pass in each evolving round + tasks_passed_by_round = {} + cumulative_passed = set() + + # For each evolving round in this loop + for e_round in range(1, max_evolving_round + 1): + if len(state.msgs[loop_round]["evolving feedback"]) >= e_round: + # Get feedback for this evolving round + feedback = state.msgs[loop_round]["evolving feedback"][e_round - 1].content + + # Count passed tasks and track their indices + passed_tasks = set() + for j, task_feedback in enumerate(feedback): + if task_feedback.final_decision: + passed_tasks.add(j) + cumulative_passed.add(j) + + # Store both individual round results and cumulative results + tasks_passed_by_round[e_round] = { + "count": len(passed_tasks), + "indices": passed_tasks, + "cumulative_count": len(cumulative_passed), + "cumulative_indices": cumulative_passed.copy(), + } + + completion_stats[loop_round] = { + "total_tasks": len(state.msgs[loop_round]["evolving feedback"][0].content), + "rounds": tasks_passed_by_round, + "max_round": max_evolving_round, + } + + # Display results + if completion_stats: + # Add an aggregate view at the top + st.subheader("🔄 Aggregate Completion Across All Loops") + + # Create summary data for comparison + summary_data = [] + total_tasks_across_loops = 0 + total_passed_r1 = 0 + total_passed_r3 = 0 + total_passed_r5 = 0 + total_passed_r10 = 0 + total_passed_final = 0 + + for loop_round, stats in completion_stats.items(): + total_tasks = stats["total_tasks"] + total_tasks_across_loops += total_tasks + + # Find data for specific rounds + r1_passed = stats["rounds"].get(1, {}).get("cumulative_count", 0) + total_passed_r1 += r1_passed + + # For round 3, use the closest round if exactly 3 doesn't exist + if 3 in stats["rounds"]: + r3_passed = stats["rounds"][3]["cumulative_count"] + elif stats["max_round"] >= 3: + max_r_below_3 = max([r for r in stats["rounds"].keys() if r <= 3]) + r3_passed = stats["rounds"][max_r_below_3]["cumulative_count"] + else: + r3_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r3 += r3_passed + + # For round 5, use the closest round if exactly 5 doesn't exist + if 5 in stats["rounds"]: + r5_passed = stats["rounds"][5]["cumulative_count"] + elif stats["max_round"] >= 5: + max_r_below_5 = max([r for r in stats["rounds"].keys() if r <= 5]) + r5_passed = stats["rounds"][max_r_below_5]["cumulative_count"] + else: + r5_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r5 += r5_passed + + # For round 10 + if 10 in stats["rounds"]: + r10_passed = stats["rounds"][10]["cumulative_count"] + else: + r10_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_r10 += r10_passed + + # Final round completion + final_passed = stats["rounds"][stats["max_round"]]["cumulative_count"] if stats["rounds"] else 0 + total_passed_final += final_passed + + # Add to summary table + summary_data.append( + { + "Loop": f"Loop {loop_round}", + "Total Tasks": total_tasks, + "Passed (Round 1)": ( + f"{r1_passed}/{total_tasks} ({r1_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Round 3)": ( + f"{r3_passed}/{total_tasks} ({r3_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Round 5)": ( + f"{r5_passed}/{total_tasks} ({r5_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + "Passed (Final)": ( + f"{final_passed}/{total_tasks} ({final_passed/total_tasks:.0%})" if total_tasks > 0 else "N/A" + ), + } + ) + + if total_tasks_across_loops > 0: + summary_data.append( + { + "Loop": "**TOTAL**", + "Total Tasks": total_tasks_across_loops, + "Passed (Round 1)": f"**{total_passed_r1}/{total_tasks_across_loops} ({total_passed_r1/total_tasks_across_loops:.0%})**", + "Passed (Round 3)": f"**{total_passed_r3}/{total_tasks_across_loops} ({total_passed_r3/total_tasks_across_loops:.0%})**", + "Passed (Round 5)": f"**{total_passed_r5}/{total_tasks_across_loops} ({total_passed_r5/total_tasks_across_loops:.0%})**", + "Passed (Final)": f"**{total_passed_final}/{total_tasks_across_loops} ({total_passed_final/total_tasks_across_loops:.0%})**", + } + ) + + st.table(pd.DataFrame(summary_data)) + + # Summary statistics + st.markdown("### 📊 Overall Completion Progress:") + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric( + label="After Round 1", + value=f"{total_passed_r1/total_tasks_across_loops:.0%}", + help=f"{total_passed_r1}/{total_tasks_across_loops} tasks", + ) + with col2: + st.metric( + label="After Round 3", + value=f"{total_passed_r3/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_r3-total_passed_r1)/total_tasks_across_loops:.0%}", + help=f"{total_passed_r3}/{total_tasks_across_loops} tasks", + ) + with col3: + st.metric( + label="After Round 5", + value=f"{total_passed_r5/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_r5-total_passed_r3)/total_tasks_across_loops:.0%}", + help=f"{total_passed_r5}/{total_tasks_across_loops} tasks", + ) + with col4: + st.metric( + label="Final Completion", + value=f"{total_passed_final/total_tasks_across_loops:.0%}", + delta=f"{(total_passed_final-total_passed_r5)/total_tasks_across_loops:.0%}", + help=f"{total_passed_final}/{total_tasks_across_loops} tasks", + ) + + # Show detailed results by loop + st.markdown("---") + st.subheader("Detailed Results by Loop") + + for loop_round, stats in completion_stats.items(): + with st.expander(f"Loop {loop_round} Details"): + total_tasks = stats["total_tasks"] + + # Create a results table + data = [] + for e_round in range(1, min(11, stats["max_round"] + 1)): + if e_round in stats["rounds"]: + round_data = stats["rounds"][e_round] + data.append( + { + "Evolving Round": e_round, + "Tasks Passed": f"{round_data['count']}/{total_tasks} ({round_data['count']/total_tasks:.0%})", + "Cumulative Passed": f"{round_data['cumulative_count']}/{total_tasks} ({round_data['cumulative_count']/total_tasks:.0%})", + } + ) + else: + data.append({"Evolving Round": e_round, "Tasks Passed": "N/A", "Cumulative Passed": "N/A"}) + + df = pd.DataFrame(data) + st.table(df) + + st.markdown("### Summary:") + if 1 in stats["rounds"]: + st.markdown( + f"- After round 1: **{stats['rounds'][1]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][1]['cumulative_count']/total_tasks:.0%})" + ) + + if 3 in stats["rounds"]: + st.markdown( + f"- After round 3: **{stats['rounds'][3]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][3]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 3: + max_round_below_3 = max([r for r in stats["rounds"].keys() if r <= 3]) + st.markdown( + f"- After round 3: **{stats['rounds'][max_round_below_3]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][max_round_below_3]['cumulative_count']/total_tasks:.0%})" + ) + + if 5 in stats["rounds"]: + st.markdown( + f"- After round 5: **{stats['rounds'][5]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][5]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 5: + max_round_below_5 = max([r for r in stats["rounds"].keys() if r <= 5]) + st.markdown( + f"- After round 5: **{stats['rounds'][max_round_below_5]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][max_round_below_5]['cumulative_count']/total_tasks:.0%})" + ) + + if 10 in stats["rounds"]: + st.markdown( + f"- After round 10: **{stats['rounds'][10]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][10]['cumulative_count']/total_tasks:.0%})" + ) + elif stats["max_round"] >= 1: + st.markdown( + f"- After final round ({stats['max_round']}): **{stats['rounds'][stats['max_round']]['cumulative_count']}/{total_tasks}** tasks passed ({stats['rounds'][stats['max_round']]['cumulative_count']/total_tasks:.0%})" + ) + else: + st.info("No task completion data available.") + + +if state.scenario is not None: + summary_window() + if st.toggle("show analyse_task_competition"): + analyze_task_completion() + + # R&D Loops Window + if isinstance(state.scenario, SIMILAR_SCENARIOS): + st.header("R&D Loops♾️", divider="rainbow", anchor="_rdloops") + if len(state.msgs) > 1: + r_options = list(state.msgs.keys()) + if 0 in r_options: + r_options.remove(0) + round = st.radio("**Loops**", horizontal=True, options=r_options, index=state.lround - 1) + else: + round = 1 + + rf_c, d_c = st.columns([2, 2]) + elif isinstance(state.scenario, GeneralModelScenario): + + rf_c = st.container() + d_c = st.container() + round = 0 + else: + st.error("Unknown Scenario!") + st.stop() + + with rf_c: + research_window() + feedback_window() + + with d_c.container(border=True): + evolving_window() + + +st.markdown("


", unsafe_allow_html=True) +st.markdown("#### Disclaimer") +st.markdown( + "*This content is AI-generated and may not be fully accurate or up-to-date; please verify with a professional for critical matters.*", + unsafe_allow_html=True, +) diff --git a/rdagent/log/ui/ds_trace.py b/rdagent/log/ui/ds_trace.py new file mode 100644 index 00000000..609a6916 --- /dev/null +++ b/rdagent/log/ui/ds_trace.py @@ -0,0 +1,1205 @@ +import hashlib +import json +import pickle # nosec +import random +import re +from collections import defaultdict +from datetime import time, timedelta +from pathlib import Path + +import pandas as pd +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 +from rdagent.log.ui.utils import ( + curve_figure, + get_sota_exp_stat, + load_times_info, + timeline_figure, + trace_figure, +) +from rdagent.log.utils import ( + 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 +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") +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))) + 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, + } + ) + 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()) # nosec + 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) + + 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} | + """) + + +def workspace_win(workspace, cmp_workspace=None, cmp_name="last code."): + show_files = {k: v for k, v in workspace.file_dict.items() if "test" not in k} + if len(show_files) > 0: + if cmp_workspace: + diff = generate_diff_from_dict(cmp_workspace.file_dict, show_files, "main.py") + with st.popover(f":violet[**Diff with {cmp_name}**]", use_container_width=True, icon="🔍"): + 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" + + with st.popover( + f"⏱️{time_str} 📂Files in :blue[{replace_ep_path(workspace.workspace_path)}]", use_container_width=True + ): + 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, + ) + + if state.show_save_input: + st.markdown("### Save All Files to Folder") + unique_key = hashlib.md5("".join(show_files.values()).encode(), usedforsecurity=False).hexdigest() + str( + random.randint(0, 10000) + ) + target_folder = st.text_input("Enter target folder path:", key=unique_key) + + 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: + 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(":") + 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(), usedforsecurity=False).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") + 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) + + +def hypothesis_win(hypo): + try: + st.code(str(hypo).replace("\n", "\n\n"), wrap_lines=True) + except Exception as e: + st.write(hypo.__dict__) + + +def exp_gen_win(exp_gen_data, llm_data=None): + st.header("Exp Gen", divider="blue", anchor="exp-gen") + if state.show_llm_log and llm_data is not None: + llm_log_win(llm_data["no_tag"]) + st.subheader("💡 Hypothesis") + hypothesis_win(exp_gen_data["no_tag"].hypothesis) + + st.subheader("📋 pending_tasks") + for tasks in exp_gen_data["no_tag"].pending_tasks_list: + task_win(tasks[0]) + st.subheader("📁 Exp Workspace") + 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: + 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") + 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", + ) + 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"]) # nosec + other_attributes = { + k: v for k, v in fb.__dict__.items() if k not in ["execution", "return_checking", "code"] # nosec + } + f1.code(fb.execution, wrap_lines=True) # nosec + 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)") + workspace_win(data["no_tag"].experiment_workspace) + + +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, + base_workspace=base_exp.experiment_workspace if base_exp else None, + ) + 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)") + workspace_win( + data["no_tag"].experiment_workspace, + cmp_workspace=last_sota_exp.experiment_workspace if last_sota_exp else None, + cmp_name="last SOTA(to_submit)", + ) + 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) + + +def feedback_win(fb_data, llm_data=None): + if "no_tag" not in fb_data: + st.header("Feedback", divider="orange", anchor="feedback") + return + 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"]) + try: + st.code(str(fb).replace("\n", "\n\n"), wrap_lines=True) + except Exception as e: + st.write(fb.__dict__) + if fb.exception is not None: + st.markdown(f"**:red[Exception]**: {fb.exception}") + + +def sota_win(sota_exp, trace): + st.subheader("SOTA Experiment", divider="rainbow", anchor="sota-exp") + 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**") + hypothesis_win(sota_exp.hypothesis) + st.markdown("**Exp Workspace**") + workspace_win(sota_exp.experiment_workspace) + else: + st.markdown("No SOTA experiment.") + + +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, + ) + if "running" in loop_data: + # get last SOTA_exp_to_submit + last_sota_exp = None + 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] + 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) + + running_win( + loop_data["running"], + base_exp=loop_data["coding"].get("no_tag", None), + llm_data=llm_data["running"] if llm_data else None, + last_sota_exp=last_sota_exp, + ) + 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") + + 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"]) + 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()) + 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() + ) + + 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: + if "debug_tpl" in d["tag"] and "eval.user" in d["obj"]["uri"] and "stdout" in d["obj"]["context"]: # nosec + 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 + + +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) + 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)) + info5.metric( + "LLM Filter Calls", + llm_filter_call, + help=timedelta_to_str(filter_call_duration), + ) + + timeout_stats = get_timeout_stats(state.llm_data) + coding_timeout_pct = ( + round(timeout_stats["coding"]["timeout"] / timeout_stats["coding"]["total"] * 100, 2) + if timeout_stats["coding"]["total"] > 0 + else 0 + ) + info6.metric( + "Timeouts (C)", + f"{coding_timeout_pct}%", + help=f"{timeout_stats['coding']['timeout']}/{timeout_stats['coding']['total']}", + ) + running_timeout_pct = ( + round(timeout_stats["running"]["timeout"] / timeout_stats["running"]["total"] * 100, 2) + if timeout_stats["running"]["total"] > 0 + else 0 + ) + info7.metric( + "Timeouts (R)", + 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 + if show_trace_dag: + st.markdown("### Trace DAG") + merge_loops = [] + for loop_id in state.llm_data.keys(): + if "direct_exp_gen" not in state.llm_data[loop_id]: + continue + 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"]] + ): + 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", + "Hypothesis", + "Reason", + "Others", + "Run Score (valid)", + "Run Score (test)", + "Feedback", + "e-loops(c)", + "e-loops(r)", + "COST($)", + "Time", + "Exp Gen", + "Coding", + "Running", + ], + index=range(min_id, max_id + 1), + ) + + valid_results = {} + sota_loop_id = state.sota_info[1] if state.sota_info else None + 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 + 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 + } + # 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)) + 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) + if ( + state.data[loop]["mle_score"] is not None + and state.data[loop]["mle_score"]["score"] is not None + ): + 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: + 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)"] = "❌" + 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): + 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: + df.loc[loop, "e-loops(c)"] = 0 + else: + 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: + df.loc[loop, "e-loops(r)"] = 0 + else: + 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: + fb_emoji_str = ( + "✅" if "no_tag" in loop_data["feedback"] and bool(loop_data["feedback"]["no_tag"]) else "❌" + ) + if sota_loop_id == loop: + fb_emoji_str += " (💖SOTA)" + df.loc[loop, "Feedback"] = fb_emoji_str + else: + df.loc[loop, "Feedback"] = "N/A" + + 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) + + # timeline figure + if state.times: + with st.popover("Timeline", icon="⏱️", use_container_width=True): + st.plotly_chart(timeline_figure(state.times)) + + # 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) + vscores["test"] = test_scores + 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)) + + 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) + st.dataframe( + styled_hypotheses_table, + 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 + }, + ) + + 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] + 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), + "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") + .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}%") + 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}") + 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"]) + state.log_folders = [str(state.log_folder)] + else: + 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, + ) + ) + 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 # nosec B608 — not SQL, Bandit false positive on "Select" in UI label + ) + + 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) + state.data, state.llm_data, state.token_costs = load_data(state.log_folder / state.log_path) + state.sota_info = get_sota_exp_stat(Path(state.log_folder) / state.log_path, selector="auto") + st.rerun() + 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) +""") + + +def get_state_data_range(state_data): + # we have a "competition" key in state_data + # like dict_keys(['competition', 10, 11, 12, 13, 14]) + 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"] + ] + return min(keys), max(keys) + + +# UI - Main +if "competition" in state.data: + st.title( + state.data["competition"] + + f" ([share_link](/ds_trace?log_folder={state.log_folder}&selection={state.log_path}))" + ) + summarize_win() + min_id, max_id = get_state_data_range(state.data) + if max_id > min_id: + loop_id = st.slider("Loop", min_id, max_id, min_id) + else: + loop_id = min_id + if state.show_stdout: + stdout_win(loop_id) + main_win(loop_id, state.llm_data[loop_id] if loop_id in state.llm_data else None) diff --git a/rdagent/log/ui/llm_st.py b/rdagent/log/ui/llm_st.py new file mode 100644 index 00000000..3ae8c34a --- /dev/null +++ b/rdagent/log/ui/llm_st.py @@ -0,0 +1,306 @@ +import argparse +import json +import pickle # nosec +import re +import time +from pathlib import Path + +import streamlit as st +from streamlit import session_state + +from rdagent.log.ui.conf import UI_SETTING +from rdagent.log.utils import extract_evoid, extract_loopid_func_name + +st.set_page_config(layout="wide", page_title="debug_llm", page_icon="🎓", initial_sidebar_state="expanded") + +# 获取 log_path 参数 +parser = argparse.ArgumentParser(description="RD-Agent Streamlit App") +parser.add_argument("--log_dir", type=str, help="Path to the log directory") +args = parser.parse_args() + + +def get_folders_sorted(log_path): + """缓存并返回排序后的文件夹列表,并加入进度打印""" + with st.spinner("正在加载文件夹列表..."): + folders = sorted( + (folder for folder in log_path.iterdir() if folder.is_dir() and list(folder.iterdir())), + key=lambda folder: folder.stat().st_mtime, + reverse=True, + ) + st.write(f"找到 {len(folders)} 个文件夹") + return [folder.name for folder in folders] + + +if UI_SETTING.enable_cache: + get_folders_sorted = st.cache_data(get_folders_sorted) + + +# 设置主日志路径 +main_log_path = Path(args.log_dir) if args.log_dir else Path("./log") +if not main_log_path.exists(): + st.error(f"Log dir {main_log_path} does not exist!") + st.stop() + +if "data" not in session_state: + session_state.data = [] +if "log_path" not in session_state: + session_state.log_path = None + +tlist = [] + + +def load_data(): + """加载数据到 session_state 并显示进度""" + log_file = main_log_path / session_state.log_path / "debug_llm.pkl" + try: + with st.spinner(f"正在加载数据文件 {log_file}..."): + start_time = time.time() + with open(log_file, "rb") as f: + session_state.data = pickle.load(f, encoding="utf-8") # nosec + st.success(f"数据加载完成!耗时 {time.time() - start_time:.2f} 秒") + st.session_state["current_loop"] = 1 + except Exception as e: + session_state.data = [{"error": str(e)}] + st.error(f"加载数据失败: {e}") + + +# UI - Sidebar +with st.sidebar: + st.markdown(":blue[**Log Path**]") + manually = st.toggle("Manual Input") + if manually: + st.text_input("log path", key="log_path", label_visibility="collapsed") + else: + folders = get_folders_sorted(main_log_path) + st.selectbox(f"**Select from {main_log_path.absolute()}**", folders, key="log_path") # nosec B608 — not SQL, Bandit false positive on "Select" in UI label + + if st.button("Refresh Data"): + load_data() + st.rerun() + + +# Helper functions +def show_text(text, lang=None): + """显示文本代码块""" + if lang: + st.code(text, language=lang, wrap_lines=True) + elif "\n" in text: + st.code(text, language="python", wrap_lines=True) + else: + st.code(text, language="html", wrap_lines=True) + + +def highlight_prompts_uri(uri): + """高亮 URI 的格式""" + parts = uri.split(":") + return f"**{parts[0]}:**:green[**{parts[1]}**]" + + +# Display Data +progress_text = st.empty() +progress_bar = st.progress(0) + +# 每页展示一个 Loop +LOOPS_PER_PAGE = 1 + +# 获取所有的 Loop ID +loop_groups = {} +for i, d in enumerate(session_state.data): + tag = d["tag"] + loop_id, _ = extract_loopid_func_name(tag) + if loop_id: + if loop_id not in loop_groups: + loop_groups[loop_id] = [] + loop_groups[loop_id].append(d) + +# 按 Loop ID 排序 +sorted_loop_ids = sorted(loop_groups.keys(), key=int) # 假设 Loop ID 是数字 +total_loops = len(sorted_loop_ids) +total_pages = total_loops # 每页展示一个 Loop + + +# simple display +# FIXME: Delete this simple UI if trace have tag(evo_id & loop_id) +# with st.sidebar: +# start = int(st.text_input("start", 0)) +# end = int(st.text_input("end", 100)) +# for m in session_state.data[start:end]: +# if "tpl" in m["tag"]: +# obj = m["obj"] +# uri = obj["uri"] +# tpl = obj["template"] +# cxt = obj["context"] +# rd = obj["rendered"] +# with st.expander(highlight_prompts_uri(uri), expanded=False, icon="⚙️"): +# 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(cxt) +# if "llm" in m["tag"]: +# obj = m["obj"] +# system = obj.get("system", None) +# user = obj["user"] +# resp = obj["resp"] +# with st.expander(f"**LLM**", expanded=False, icon="🤖"): +# t1, t2, t3 = st.tabs([":green[**Response**]", ":blue[**User**]", ":orange[**System**]"]) +# with t1: +# try: +# rdict = json.loads(resp) +# if "code" in rdict: +# code = rdict["code"] +# st.markdown(":red[**Code in response dict:**]") +# st.code(code, language="python", wrap_lines=True, line_numbers=True) +# rdict.pop("code") +# elif "spec" in rdict: +# spec = rdict["spec"] +# st.markdown(":red[**Spec in response dict:**]") +# st.markdown(spec) +# rdict.pop("spec") +# else: +# # show model codes +# showed_keys = [] +# for k, v in rdict.items(): +# if k.startswith("model_") and k.endswith(".py"): +# 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) +# st.write(":red[**Other parts (except for the code or spec) in response dict:**]") +# st.json(rdict) +# except: +# st.json(resp) +# with t2: +# show_text(user) +# with t3: +# show_text(system or "No system prompt available") + + +if total_pages: + # 初始化 current_loop + if "current_loop" not in st.session_state: + st.session_state["current_loop"] = 1 + + # Loop 导航按钮 + col1, col2, col3, col4, col5 = st.sidebar.columns([1.2, 1, 2, 1, 1.2]) + + with col1: + if st.button("|<"): # 首页 + st.session_state["current_loop"] = 1 + with col2: + if st.button("<") and st.session_state["current_loop"] > 1: # 上一页 + st.session_state["current_loop"] -= 1 + with col3: + # 下拉列表显示所有 Loop + st.session_state["current_loop"] = st.selectbox( + "选择 Loop", + options=list(range(1, total_loops + 1)), + index=st.session_state["current_loop"] - 1, # 默认选中当前 Loop + label_visibility="collapsed", # 隐藏标签 + ) + with col4: + if st.button("\>") and st.session_state["current_loop"] < total_loops: # 下一页 + st.session_state["current_loop"] += 1 + with col5: + if st.button("\>|"): # 最后一页 + st.session_state["current_loop"] = total_loops + + # 获取当前 Loop + current_loop = st.session_state["current_loop"] + + # 渲染当前 Loop 数据 + loop_id = sorted_loop_ids[current_loop - 1] + progress_text = st.empty() + progress_text.text(f"正在处理 Loop {loop_id}...") + progress_bar.progress(current_loop / total_loops, text=f"Loop :green[**{current_loop}**] / {total_loops}") + + # 渲染 Loop Header + loop_anchor = f"Loop_{loop_id}" + if loop_anchor not in tlist: + tlist.append(loop_anchor) + st.header(loop_anchor, anchor=loop_anchor, divider="blue") + + # 渲染当前 Loop 的所有数据 + loop_data = loop_groups[loop_id] + for d in loop_data: + tag = d["tag"] + obj = d["obj"] + _, func_name = extract_loopid_func_name(tag) + evo_id = extract_evoid(tag) + + func_anchor = f"loop_{loop_id}.{func_name}" + if func_anchor not in tlist: + tlist.append(func_anchor) + st.header(f"in *{func_name}*", anchor=func_anchor, divider="green") + + evo_anchor = f"loop_{loop_id}.evo_step_{evo_id}" + if evo_id and evo_anchor not in tlist: + tlist.append(evo_anchor) + st.subheader(f"evo_step_{evo_id}", anchor=evo_anchor, divider="orange") + + # 根据 tag 渲染内容 + if "debug_exp_gen" in tag: + with st.expander( + f"Exp in :violet[**{obj.experiment_workspace.workspace_path}**]", expanded=False, icon="🧩" + ): + st.write(obj) + elif "debug_tpl" in tag: + uri = obj["uri"] + tpl = obj["template"] + cxt = obj["context"] + rd = obj["rendered"] + with st.expander(highlight_prompts_uri(uri), expanded=False, icon="⚙️"): + 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(cxt) + elif "debug_llm" in tag: + system = obj.get("system", None) + user = obj["user"] + resp = obj["resp"] + with st.expander(f"**LLM**", expanded=False, icon="🤖"): + t1, t2, t3 = st.tabs([":green[**Response**]", ":blue[**User**]", ":orange[**System**]"]) + with t1: + try: + rdict = json.loads(resp) + if "code" in rdict: + code = rdict["code"] + st.markdown(":red[**Code in response dict:**]") + st.code(code, language="python", wrap_lines=True, line_numbers=True) + rdict.pop("code") + elif "spec" in rdict: + spec = rdict["spec"] + st.markdown(":red[**Spec in response dict:**]") + st.markdown(spec) + rdict.pop("spec") + else: + # show model codes + showed_keys = [] + for k, v in rdict.items(): + if k.startswith("model_") and k.endswith(".py"): + 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) + st.write(":red[**Other parts (except for the code or spec) in response dict:**]") + st.json(rdict) + except: + st.json(resp) + with t2: + show_text(user) + with t3: + show_text(system or "No system prompt available") + + progress_text.text("当前 Loop 数据处理完成!") + + # Sidebar TOC + with st.sidebar: + toc = "\n".join([f"- [{t}](#{t})" if t.startswith("L") else f" - [{t.split('.')[1]}](#{t})" for t in tlist]) + st.markdown(toc, unsafe_allow_html=True) diff --git a/rdagent/scenarios/data_science/proposal/exp_gen/merge.py b/rdagent/scenarios/data_science/proposal/exp_gen/merge.py index e541f159..6f943441 100644 --- a/rdagent/scenarios/data_science/proposal/exp_gen/merge.py +++ b/rdagent/scenarios/data_science/proposal/exp_gen/merge.py @@ -182,7 +182,7 @@ class ExpGen2Hypothesis(DSProposalV2ExpGen): success_fb_list = list(set(trace_fbs)) logger.info( - f"Merge Hypothesis: select {len(success_fb_list)} from {len(trace_fbs)} SOTA experiments found in {len(leaves)} traces" + f"Merge Hypothesis: select {len(success_fb_list)} from {len(trace_fbs)} SOTA experiments found in {len(leaves)} traces" # nosec B608 — not SQL, Bandit false positive on "select" in log message ) if len(success_fb_list) > 0: diff --git a/rdagent/scenarios/kaggle/developer/coder.py b/rdagent/scenarios/kaggle/developer/coder.py index 5547def7..d01b7651 100644 --- a/rdagent/scenarios/kaggle/developer/coder.py +++ b/rdagent/scenarios/kaggle/developer/coder.py @@ -38,7 +38,7 @@ class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]): assert target_model_type in KG_SELECT_MAPPING if len(exp.experiment_workspace.data_description) == 1: code = ( - Environment(undefined=StrictUndefined) + Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code .from_string(DEFAULT_SELECTION_CODE) .render(feature_index_list=None) ) @@ -62,7 +62,7 @@ class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]): chosen_index_to_list_index = [i - 1 for i in chosen_index] code = ( - Environment(undefined=StrictUndefined) + Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code .from_string(DEFAULT_SELECTION_CODE) .render(feature_index_list=chosen_index_to_list_index) ) diff --git a/rdagent/scenarios/qlib/experiment/utils.py b/rdagent/scenarios/qlib/experiment/utils.py index ad42a69b..11055e54 100644 --- a/rdagent/scenarios/qlib/experiment/utils.py +++ b/rdagent/scenarios/qlib/experiment/utils.py @@ -67,7 +67,7 @@ def get_file_desc(p: Path, variable_list=[]) -> str: """ p = Path(p) - JJ_TPL = Environment(undefined=StrictUndefined).from_string(""" + JJ_TPL = Environment(undefined=StrictUndefined).from_string(""" # nosec B701 — renders plain text description, not HTML; autoescape not applicable # {{file_name}} ## File Type