chore: ui server updates (#951)

* ui server update

* update

* fix bugs

* updates

* use randomname

* fix

* fix bugs

* change time interval in debug server

* some change

* fix CI

* time interval change

* updates

* some changes

* fix

* fix curves

* one IP, one pointers to show msgs return progress

* enable /control

* fix bugs

* fix CI

* fix isort
This commit is contained in:
XianBW
2025-07-04 12:32:14 +08:00
committed by GitHub
parent ef6b5866c7
commit c335ebc25c
9 changed files with 392 additions and 107 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ def main(
checkout: bool | str | Path = True,
step_n: int | None = None,
loop_n: int | None = None,
all_duration: str | None = None,
competition="bms-molecular-translation",
timeout=None,
replace_timer=True,
exp_gen_cls: str | None = None,
):
@@ -67,7 +67,7 @@ def main(
if exp_gen_cls is not None:
kaggle_loop.exp_gen = import_class(exp_gen_cls)(kaggle_loop.exp_gen.scen)
asyncio.run(kaggle_loop.run(step_n=step_n, loop_n=loop_n, all_duration=timeout))
asyncio.run(kaggle_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration))
if __name__ == "__main__":
+8 -1
View File
@@ -3,6 +3,7 @@ Factor workflow with session control
"""
import asyncio
from pathlib import Path
from typing import Any
import fire
@@ -25,7 +26,13 @@ class FactorRDLoop(RDLoop):
return exp
def main(path=None, step_n=None, loop_n=None, all_duration=None, checkout=True):
def main(
path: str | None = None,
step_n: int | None = None,
loop_n: int | None = None,
all_duration: str | None = None,
checkout: bool | str | Path = True,
):
"""
Auto R&D Evolving loop for fintech factors.
@@ -97,7 +97,7 @@ def extract_model_from_docs(docs_dict):
class ModelExperimentLoaderFromDict(ModelTaskLoader):
def load(self, model_dict: dict) -> list:
def load(self, model_dict: dict) -> QlibModelExperiment:
"""Load data from a dict."""
task_l = []
for model_name, model_data in model_dict.items():
@@ -117,7 +117,7 @@ class ModelExperimentLoaderFromDict(ModelTaskLoader):
class ModelExperimentLoaderFromPDFfiles(ModelTaskLoader):
@wait_retry(retry_n=5)
def load(self, file_or_folder_path: str) -> dict:
def load(self, file_or_folder_path: str) -> QlibModelExperiment:
docs_dict = load_and_process_pdfs_by_langchain(file_or_folder_path) # dict{file_path:content}
model_dict = extract_model_from_docs(
docs_dict
+20 -4
View File
@@ -116,16 +116,18 @@ Only **2** Message in one loop
"evo_id": "0",
"content": [ // list of task_name & codes
{
"evo_id": "0",
"target_task_name": "task_1",
"codes": { // one or more codes
"workspace": { // one or more codes
"a.py": "...<python codes>",
"b.py": "...<python codes>",
//...
}
},
{
"evo_id": "0",
"target_task_name": "task_2",
"codes": {
"workspace": {
"a.py": "...<python codes>",
//...
}
@@ -155,6 +157,7 @@ Only **2** Message in one loop
"evo_id": "0",
"content": [ // list of feedbacks
{
"evo_id": "0",
"final_decision": "True", // True or False
"execution": "...",
"code": "...",
@@ -230,8 +233,21 @@ Each tag below appears only once per loop.
"reason": "...",
"exception": "...",
"observations": "...", // may not exists
"hypothesis_evaluation": "...", // may not exists
"hypothesis_evaluation": "...", // may not existsc
"new_hypothesis": "...", // may not exists
}
}
```
```
# TODO
## Session
- How to continue.
- show & copy trace_id(name)?
-
## Page
1. remove Medical, add Finance Whole Pipeline
2.
+99 -38
View File
@@ -2,7 +2,6 @@ import os
import random
import signal
import subprocess
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
@@ -12,46 +11,88 @@ import typer
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
msgs_for_frontend = defaultdict(list)
from rdagent.log.storage import FileStorage
from rdagent.log.ui.conf import UI_SETTING
from rdagent.log.ui.storage import WebStorage
from rdagent.log.utils import is_valid_session
app = Flask(__name__, static_folder="./docs/_static")
app = Flask(__name__, static_folder=UI_SETTING.static_path)
CORS(app)
rdagent_processes = defaultdict()
server_port = 19899
log_folder_path = Path(UI_SETTING.trace_folder).absolute()
@app.route("/favicon.ico")
def favicon():
return send_from_directory("./docs/_static", "favicon.ico", mimetype="image/vnd.microsoft.icon")
return send_from_directory(app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon")
pointers = {id: 0 for id in msgs_for_frontend.keys()}
msgs_for_frontend = defaultdict(list)
pointers = defaultdict(lambda: defaultdict(int)) # pointers[trace_id][user_ip]
def read_trace(log_path: Path, id: str = "") -> None:
fs = FileStorage(log_path)
ws = WebStorage(port=1, path=log_path)
msgs_for_frontend[id] = []
last_timestamp = None
for msg in fs.iter_msg():
data = ws._obj_to_json(obj=msg.content, tag=msg.tag, id=id, timestamp=msg.timestamp.isoformat())
if data:
if isinstance(data, list):
for d in data:
msgs_for_frontend[id].append(d["msg"])
last_timestamp = msg.timestamp
else:
msgs_for_frontend[id].append(data["msg"])
last_timestamp = msg.timestamp
now = datetime.now(timezone.utc)
if last_timestamp and (now - last_timestamp).total_seconds() > 1800:
msgs_for_frontend[id].append({"tag": "END", "timestamp": now.isoformat(), "content": {}})
# load all traces from the log folder
for p in log_folder_path.glob("*/*/"):
if is_valid_session(p):
read_trace(p, id=str(p))
@app.route("/trace", methods=["POST"])
def update_trace():
global pointers, msgs_for_frontend
data = request.get_json()
trace_id = data.get("id")
return_all = data.get("all")
reset = data.get("reset")
msg_num = random.randint(1, 10)
app.logger.info(data)
log_folder_path = Path(UI_SETTING.trace_folder).absolute()
if not trace_id:
return jsonify({"error": "Trace ID is required"}), 400
trace_id = str(log_folder_path / trace_id)
user_ip = request.remote_addr
if reset:
pointers[trace_id] = 0
pointers[trace_id][user_ip] = 0
end_pointer = pointers[trace_id] + msg_num
start_pointer = pointers[trace_id][user_ip]
end_pointer = start_pointer + msg_num
if end_pointer > len(msgs_for_frontend[trace_id]) or return_all:
end_pointer = len(msgs_for_frontend[trace_id])
print(f"trace_id: {trace_id}, start_pointer: {pointers[trace_id]}, end_pointer: {end_pointer}")
returned_msgs = msgs_for_frontend[trace_id][pointers[trace_id] : end_pointer]
returned_msgs = msgs_for_frontend[trace_id][start_pointer:end_pointer]
pointers[trace_id] = end_pointer
pointers[trace_id][user_ip] = end_pointer
if returned_msgs:
app.logger.info([msg["tag"] for msg in returned_msgs])
return jsonify(returned_msgs), 200
@app.route("/upload", methods=["GET"])
@app.route("/upload", methods=["POST"])
def upload_file():
# 获取请求体中的字段
global rdagent_processes, server_port
@@ -62,11 +103,18 @@ def upload_file():
all_duration = request.form.get("all_duration")
# scenario = "Data Science Loop"
trace_name = randomname.get_name()
log_folder_path = Path("./RD-Agent_server_trace").absolute()
log_trace_path = (log_folder_path / scenario / trace_name).absolute()
if scenario == "Data Science":
competition = competition[10:] # Eg. MLE-Bench:aerial-cactus-competition
trace_name = f"{competition}-{randomname.get_name()}"
else:
trace_name = randomname.get_name()
trace_files_path = log_folder_path / scenario / "uploads" / trace_name
log_trace_path = (log_folder_path / scenario / trace_name).absolute()
stdout_path = log_folder_path / scenario / f"{trace_name}.stdout"
if not stdout_path.exists():
stdout_path.parent.mkdir(parents=True, exist_ok=True)
# save files
for file in files:
if file:
@@ -87,33 +135,34 @@ def upload_file():
else: # one file is uploaded
rfp = str(trace_files_path / files[0].filename)
cmds = ["rdagent", "general_model", "--report_file_path", rfp]
if scenario == "Medical Model Implementation":
cmds = ["rdagent", "med_model"]
if scenario == "Data Science Loop":
cmds = ["rdagent", "kaggle", "--competition", competition]
if scenario == "Finance Whole Pipeline":
cmds = ["rdagent", "fin_quant"]
if scenario == "Data Science":
cmds = ["rdagent", "data_science", "--competition", competition]
# time control parameters
if loop_n:
cmds += ["--loop_n", loop_n]
if scenario != "Finance Data Building (Reports)":
if loop_n:
cmds += ["--loop_n", loop_n]
if all_duration:
cmds += ["--all_duration", all_duration]
rdagent_processes[str(log_trace_path)] = subprocess.Popen(
cmds,
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
stdout=sys.stdout,
stderr=sys.stderr,
env={
"LOG_TRACE_PATH": str(log_trace_path),
"UI_SERVER_PORT": server_port,
},
)
cmds += ["--all_duration", f"{all_duration}h"]
app.logger.info(f"Started process for {log_trace_path} with parameters: {cmds}")
with stdout_path.open("w") as log_file:
rdagent_processes[str(log_trace_path)] = subprocess.Popen(
cmds,
stdout=log_file,
stderr=log_file,
env={
**os.environ,
"LOG_TRACE_PATH": str(log_trace_path),
"LOG_UI_SERVER_PORT": str(server_port),
},
)
return (
jsonify(
{
"id": str(log_trace_path),
"id": f"{scenario}/{trace_name}",
}
),
200,
@@ -124,6 +173,7 @@ def upload_file():
def receive_msgs():
try:
data = request.get_json()
# app.logger.info(data["msg"]["tag"])
if not data:
return jsonify({"error": "No JSON data received"}), 400
except Exception as e:
@@ -140,12 +190,13 @@ def receive_msgs():
@app.route("/control", methods=["POST"])
def control_process():
global rdagent_processes
global rdagent_processes, msgs_for_frontend
data = request.get_json()
app.logger.info(data)
if not data or "id" not in data or "action" not in data:
return jsonify({"error": "Missing 'id' or 'action' in request"}), 400
id = data["id"]
id = str(log_folder_path / data["id"])
action = data["action"]
if id not in rdagent_processes or rdagent_processes[id] is None:
@@ -175,13 +226,23 @@ def control_process():
else:
return jsonify({"error": "Unknown action"}), 400
except Exception as e:
return jsonify({"error": f"Failed to {action} process"}), 500
return jsonify({"error": f"Failed to {action} process, {e}"}), 500
@app.route("/test", methods=["GET"])
def test():
# return 'Hello, World!'
global msgs_for_frontend, pointers
msgs = {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
pointers = pointers
return jsonify({"msgs": msgs, "pointers": pointers}), 200
@app.route("/", methods=["GET"])
def index():
# return 'Hello, World!'
return msgs_for_frontend
# return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
return send_from_directory(app.static_folder, "index.html")
@app.route("/<path:fn>", methods=["GET"])
+174
View File
@@ -0,0 +1,174 @@
import multiprocessing
import os
import random
import signal
import subprocess
import threading
import time
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
import randomname
import typer
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from rdagent.log.ui.conf import UI_SETTING
app = Flask(__name__, static_folder=UI_SETTING.static_path)
CORS(app)
rdagent_processes = defaultdict()
server_port = 19899
@app.route("/favicon.ico")
def favicon():
return send_from_directory(app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon")
msgs_for_frontend = defaultdict(list)
pointers = defaultdict(int)
@app.route("/trace", methods=["POST"])
def update_trace():
global pointers, msgs_for_frontend
data = request.get_json()
# app.logger.info(data)
trace_id = data.get("id")
return_all = data.get("all")
reset = data.get("reset")
msg_num = random.randint(1, 10)
if reset:
pointers[trace_id] = 0
end_pointer = pointers[trace_id] + msg_num
if end_pointer > len(msgs_for_frontend[trace_id]) or return_all:
end_pointer = len(msgs_for_frontend[trace_id])
returned_msgs = msgs_for_frontend[trace_id][pointers[trace_id] : end_pointer]
pointers[trace_id] = end_pointer
# if len(returned_msgs):
# app.logger.info(data)
# app.logger.info([i["tag"] for i in returned_msgs])
# try:
# import json
# resp = json.dumps(returned_msgs, ensure_ascii=False)
# except Exception as e:
# app.logger.error(f"Error in jsonify: {e}")
# for msg in returned_msgs:
# try:
# rr = json.dumps(msg, ensure_ascii=False)
# except Exception as e:
# app.logger.error(f"Error in jsonify individual message: {e}")
# app.logger.error(msg)
return jsonify(returned_msgs), 200
@app.route("/upload", methods=["POST"])
def upload_file():
# 获取请求体中的字段
global rdagent_processes, server_port, msgs_for_frontend
scenario = request.form.get("scenario")
files = request.files.getlist("files")
competition = request.form.get("competition")
loop_n = request.form.get("loops")
all_duration = request.form.get("all_duration")
log_folder_path = Path("/home/bowen/workspace/new_traces").absolute()
if scenario == "Data Science":
trace_path = log_folder_path / "o1-preview" / f"{competition[10:]}.1"
else:
trace_path = log_folder_path / scenario
id = f"{scenario}/{randomname.get_name()}"
def read_trace(log_path: Path, t: float = 0.2, id: str = "") -> None:
from rdagent.log.storage import FileStorage
from rdagent.log.ui.storage import WebStorage
fs = FileStorage(log_path)
ws = WebStorage(port=1, path=log_path)
msgs_for_frontend[id] = []
for msg in fs.iter_msg():
data = ws._obj_to_json(obj=msg.content, tag=msg.tag, id=id, timestamp=msg.timestamp.isoformat())
if data:
if isinstance(data, list):
for d in data:
time.sleep(t)
msgs_for_frontend[id].append(d["msg"])
else:
time.sleep(t)
msgs_for_frontend[id].append(data["msg"])
msgs_for_frontend[id].append({"tag": "END", "timestamp": datetime.now(timezone.utc).isoformat(), "content": {}})
# 启动后台线程,不阻塞 return
threading.Thread(target=read_trace, args=(trace_path, 0.5, id), daemon=True).start()
return jsonify({"id": id}), 200
@app.route("/receive", methods=["POST"])
def receive_msgs():
try:
data = request.get_json()
app.logger.info(data["msg"]["tag"])
if not data:
return jsonify({"error": "No JSON data received"}), 400
except Exception as e:
return jsonify({"error": "Internal Server Error"}), 500
if isinstance(data, list):
for d in data:
msgs_for_frontend[d["id"]].append(d["msg"])
else:
msgs_for_frontend[data["id"]].append(data["msg"])
return jsonify({"status": "success"}), 200
@app.route("/control", methods=["POST"])
def control_process():
global rdagent_processes
data = request.get_json()
app.logger.info(data)
if not data or "id" not in data or "action" not in data:
return jsonify({"error": "Missing 'id' or 'action' in request"}), 400
id = data["id"]
action = data["action"]
return jsonify({"status": "success", "message": f"Received action '{action}' for process with id '{id}'"})
@app.route("/test", methods=["GET"])
def test():
# return 'Hello, World!'
return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
@app.route("/", methods=["GET"])
def index():
# return 'Hello, World!'
# return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
return send_from_directory(app.static_folder, "index.html")
@app.route("/<path:fn>", methods=["GET"])
def server_static_files(fn):
return send_from_directory(app.static_folder, fn)
def main(port: int = 19899):
global server_port
server_port = port
app.run(debug=True, host="0.0.0.0", port=port)
if __name__ == "__main__":
typer.run(main)
+4
View File
@@ -14,5 +14,9 @@ class UIBasePropSetting(ExtendedBaseSettings):
amlt_path: str = "/data/share_folder_local/amlt"
static_path: str = "./git_ignore_folder/static"
trace_folder: str = "./traces"
UI_SETTING = UIBasePropSetting()
+8 -1
View File
@@ -51,7 +51,7 @@ def curves_win(summary: dict):
with st.container(border=True):
st.markdown(f"**:blue[{k}] - :violet[{v['competition']}]**")
try:
tscores = {f"loop {k-1}": v for k, v in v["test_scores"].items()}
tscores = {f"loop {k}": v for k, v in v["test_scores"].items()}
vscores = {}
for k, vs in v["valid_scores"].items():
if not vs.index.is_unique:
@@ -72,6 +72,13 @@ def curves_win(summary: dict):
ensemble_row = vdf.loc[["ensemble"]]
vdf = pd.concat([ensemble_row, vdf.drop("ensemble")])
vdf.columns = [f"loop {i}" for i in vdf.columns]
# Ensure tdf has all loops present in vdf, fill missing with NaN
for loop_name in vdf.columns:
if loop_name not in tdf.index:
tdf.loc[loop_name] = pd.NA
tdf = tdf.reindex(vdf.columns)
fig = go.Figure()
# Add test scores trace from tdf
fig.add_trace(
+75 -59
View File
@@ -7,6 +7,8 @@ import requests
from rdagent.log.base import Message, Storage
from rdagent.log.utils import extract_evoid, extract_loopid_func_name, gen_datetime
from .conf import UI_SETTING
class WebStorage(Storage):
"""
@@ -30,16 +32,21 @@ class WebStorage(Storage):
def log(self, obj: object, tag: str, timestamp: datetime | None = None, **kwargs: Any) -> str | Path:
timestamp = gen_datetime(timestamp)
if "pdf_image" in tag or "load_pdf_screenshot" in tag:
obj.save(f"{UI_SETTING.static_path}/{timestamp.isoformat()}.jpg")
try:
data = self._obj_to_json(obj=obj, tag=tag, id=self.path, timestamp=timestamp.isoformat())
if not data:
return "Normal log, skipped"
if isinstance(data, list):
for d in data:
self.msgs.append(d)
else:
self.msgs.append(data)
headers = {"Content-Type": "application/json"}
requests.post(f"{self.url}/receive", json=data, headers=headers, timeout=1)
resp = requests.post(f"{self.url}/receive", json=data, headers=headers, timeout=1)
return f"{resp.status_code} {resp.text}"
except (requests.ConnectionError, requests.Timeout) as e:
pass
@@ -56,6 +63,7 @@ class WebStorage(Storage):
)
def _obj_to_json(
self,
obj: object,
tag: str,
id: str,
@@ -84,13 +92,25 @@ class WebStorage(Storage):
},
},
}
elif "experiment generation" in tag:
elif "pdf_image" in tag or "load_pdf_screenshot" in tag:
# obj.save(f"{app.static_folder}/{timestamp}.jpg")
data = {
"id": id,
"msg": {
"tag": "research.pdf_image",
"timestamp": timestamp,
"loop_id": li,
"content": {"image": f"{timestamp}.jpg"},
},
}
elif "experiment generation" in tag or "load_experiment" in tag:
from rdagent.components.coder.factor_coder.factor import FactorTask
from rdagent.components.coder.model_coder.model import ModelTask
from rdagent.core.experiment import Experiment
tasks: list[FactorTask | ModelTask] = obj
if "load_experiment" in tag:
tasks: list[FactorTask | ModelTask] = obj.sub_tasks
else:
tasks: list[FactorTask | ModelTask] = obj
if isinstance(tasks[0], FactorTask):
data = {
"id": id,
@@ -141,6 +161,7 @@ class WebStorage(Storage):
h: DSHypothesis = obj.hypothesis
tasks = [t[0] for t in obj.pending_tasks_list]
t = tasks[0]
t.name = type(t).__name__ # TODO: PipelinTask have "COMPONENT" in name, fix this when creating task.
data = [
{
"id": id,
@@ -193,36 +214,30 @@ class WebStorage(Storage):
},
},
]
elif f"evo_loop_{ei}.evolving code" in tag and "coding" in tag:
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace
from rdagent.components.coder.model_coder.model import (
ModelFBWorkspace,
ModelTask,
)
elif f"evo_loop_{ei}.evolving code" in tag and "running" not in tag:
from rdagent.core.experiment import FBWorkspace
ws: list[FactorFBWorkspace | ModelFBWorkspace] = [i for i in obj]
if all(isinstance(item, FactorFBWorkspace) for item in ws) or all(
isinstance(item, ModelFBWorkspace) for item in ws
):
data = {
"id": id,
"msg": {
"tag": "evolving.codes",
"timestamp": timestamp,
"loop_id": li,
"evo_id": ei,
"content": [
{
"target_task_name": w.target_task.name,
"codes": w.file_dict,
}
for w in ws
if w
],
},
}
elif f"evo_loop_{ei}.evolving feedback" in tag and "coding" in tag:
ws: list[FBWorkspace] = [i for i in obj]
data = {
"id": id,
"msg": {
"tag": "evolving.codes",
"timestamp": timestamp,
"loop_id": li,
"evo_id": ei,
"content": [
{
"evo_id": ei,
"target_task_name": (
w.target_task.name if w.target_task else "PipelineTask"
), # TODO: save this when proposal
"workspace": w.file_dict,
}
for w in ws
],
},
}
elif f"evo_loop_{ei}.evolving feedback" in tag and "running" not in tag:
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEERSingleFeedback,
)
@@ -237,6 +252,7 @@ class WebStorage(Storage):
"evo_id": ei,
"content": [
{
"evo_id": ei,
"final_decision": f.final_decision,
# "final_feedback": f.final_feedback,
"execution": f.execution,
@@ -244,7 +260,6 @@ class WebStorage(Storage):
"return_checking": f.return_checking,
}
for f in fl
if f
],
},
}
@@ -294,31 +309,32 @@ class WebStorage(Storage):
elif "feedback" in tag:
from rdagent.core.proposal import ExperimentFeedback, HypothesisFeedback
ef: ExperimentFeedback = obj
content = (
{
"observations": ef.observations,
"hypothesis_evaluation": ef.hypothesis_evaluation,
"new_hypothesis": ef.new_hypothesis,
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
if isinstance(obj, ExperimentFeedback):
ef: ExperimentFeedback = obj
content = (
{
"observations": str(ef.observations),
"hypothesis_evaluation": ef.hypothesis_evaluation,
"new_hypothesis": ef.new_hypothesis,
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
}
if isinstance(ef, HypothesisFeedback)
else {
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
}
)
data = {
"id": id,
"msg": {
"tag": "feedback.hypothesis_feedback",
"timestamp": timestamp,
"loop_id": li,
"content": content,
},
}
if isinstance(ef, HypothesisFeedback)
else {
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
}
)
data = {
"id": id,
"msg": {
"tag": "feedback.hypothesis_feedback",
"timestamp": timestamp,
"loop_id": li,
"content": content,
},
}
return data