feat: add a web UI server (#1345)

* update rdagent cmd

* fix log error message

* use multiProcessing.Process instead of subprocess.Popen

* add traces to gitignore

* add user interactor in RDLoop (finance scenarios)

* add interactor (feedback, hypothesis) for quant scens

* fix the test_end in qlib conf

* add features init config, general instruction to qlib scenarios

* set base features for based exp

* fix bug when combine factors

* move traces folder to git_ignore_folder

* fix bug in features init

* fix quant interact bug

* fix logger warning error

* bug fixes

* modify rdagent logger, now it can set file output

* adjust cli functions and fix logger bug

* fix server port transport problem

* update server_ui in cli

* add web code

* fix CI problem

* black fix

* update web ui README

* update README

* update readme
This commit is contained in:
XianBW
2026-03-18 14:04:52 +08:00
committed by GitHub
parent 7cd64a26fd
commit 14395488b9
141 changed files with 12852 additions and 279 deletions
+1
View File
@@ -42,6 +42,7 @@ class LLMHypothesisGen(HypothesisGen):
),
hypothesis_output_format=context_dict["hypothesis_output_format"],
hypothesis_specification=context_dict["hypothesis_specification"],
user_instruction=plan.get("user_instruction", None) if plan is not None else None,
)
user_prompt = T(".prompts:hypothesis_gen.user_prompt").r(
targets=self.targets,
+8 -1
View File
@@ -3,10 +3,17 @@ hypothesis_gen:
The user is working on generating new hypotheses for the {{ targets }} in a data-driven research and development process.
The {{ targets }} are used in the following scenario:
{{ scenario }}
{% if user_instruction %}
**User's overall instruction:**
{{ user_instruction }}
{% endif %}
The user has already proposed several hypotheses and conducted evaluations on them. This information will be provided to you. Your task is to analyze previous experiments, reflect on the decision made in each experiment, and consider why experiments with a decision of true were successful while those with a decision of false failed. Then, think about how to improve further — either by refining the existing approach or by exploring an entirely new direction.
If one exists and you agree with it, feel free to use it. If you disagree, please generate an improved version.
{% if hypothesis_specification %}
To assist you in formulating new hypotheses, the user has provided some additional information: {{ hypothesis_specification }}.
To assist you in formulating new hypotheses, the user has provided some additional information:
{{ hypothesis_specification }}
**Important:** If the hypothesis_specification outlines the next steps you need to follow, ensure you adhere to those instructions.
{% endif %}
Please generate the output using the following format and specifications:
+134 -1
View File
@@ -4,6 +4,9 @@ It is from `rdagent/app/qlib_rd_loop/model.py` and try to replace `rdagent/app/q
"""
import asyncio
import json
from multiprocessing import Queue
from pathlib import Path
from typing import Any
from rdagent.components.workflow.conf import BasePropSetting
@@ -11,6 +14,7 @@ from rdagent.core.conf import RD_AGENT_SETTINGS
from rdagent.core.developer import Developer
from rdagent.core.proposal import (
Experiment2Feedback,
ExperimentPlan,
Hypothesis,
Hypothesis2Experiment,
HypothesisFeedback,
@@ -20,6 +24,7 @@ from rdagent.core.proposal import (
from rdagent.core.scenario import Scenario
from rdagent.core.utils import import_class
from rdagent.log import rdagent_logger as logger
from rdagent.utils.qlib import ALPHA20, validate_qlib_features
from rdagent.utils.workflow import LoopBase, LoopMeta
@@ -36,6 +41,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
else None
)
self.plan: ExperimentPlan = {
"features": ALPHA20,
"feature_codes": {},
} # for user interaction
self.hypothesis2experiment: Hypothesis2Experiment = (
import_class(PROP_SETTING.hypothesis2experiment)()
if hasattr(PROP_SETTING, "hypothesis2experiment") and PROP_SETTING.hypothesis2experiment
@@ -58,8 +68,125 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
super().__init__()
# excluded steps
def _set_interactor(self, user_request_q: Queue, user_response_q: Queue):
self.user_request_q = user_request_q
self.user_response_q = user_response_q
def _init_base_features(self, base_features_path: str | None):
if base_features_path is not None:
try:
base_dir = Path(base_features_path)
base_factors_file = base_dir / "base_factors.json"
feature_codes: dict[str, str] = {}
for py_file in sorted(base_dir.glob("*.py")):
feature_codes[py_file.name] = py_file.read_text()
self.plan["feature_codes"] = feature_codes
if not base_factors_file.exists():
logger.info(f"No base_factors.json found under {base_dir}. Keeping default base features.")
logger.info(f"{len(feature_codes)} feature code files loaded from {base_dir}.")
else:
with base_factors_file.open("r") as f:
features = json.load(f)
if not isinstance(features, dict):
raise ValueError(
"`base_factors.json` must contain a JSON object of feature_name -> expression."
)
if validate_qlib_features(list(features.values())):
self.plan["features"] = features
logger.info(
f"Loaded base features from {base_factors_file}. {len(features)} features loaded and {len(feature_codes)} feature code files loaded."
)
else:
logger.warning(
f"Base feature validation failed for features loaded from {base_factors_file}. Using default features."
)
except Exception as e:
logger.warning(f"Failed to load base features from {base_features_path}: {e}. Using default features.")
else:
logger.info("No base features path provided. Using default features.")
def _interact_init_params(self) -> None:
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
return
logger.info("Waiting for user interaction on initial parameters...")
try:
self.user_request_q.put(
{
"user_instruction": None,
}
)
res_dict = self.user_response_q.get()
logger.info("Received user instruction response.")
self.plan.update(res_dict)
if "feature_codes" not in self.plan:
self.plan[
"user_instruction"
] += f"\n\n{str(list(self.plan['feature_codes'].keys()))} has been configured as the base factor; do not generate duplicate factors."
fea_valid_msg = ""
while True:
logger.info("Requesting base feature configuration from user.")
self.user_request_q.put(
{
"features": self.plan["features"],
"feature_validation_msg": fea_valid_msg,
}
)
self.plan["features"] = self.user_response_q.get()
logger.info("Received base feature configuration response.")
if validate_qlib_features(list(self.plan["features"].values())):
logger.info(f"Base feature validation passed. {len(self.plan['features'])} features selected.")
break
else:
logger.info("Base feature validation failed. Asking user to revise.")
fea_valid_msg = "Some features are invalid, please revise."
except (EOFError, OSError):
logger.info("User interaction failed, using default initial parameters.")
return
logger.info("Received user interaction on initial parameters.")
def _interact_hypo(self, hypo: Hypothesis) -> Hypothesis:
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
return hypo
logger.info("Waiting for user interaction on hypothesis...")
try:
self.user_request_q.put(hypo.__dict__)
res_dict = self.user_response_q.get()
modified_hypo = type(hypo)(**res_dict)
except (EOFError, OSError, TypeError):
logger.info("User interaction failed, using original hypothesis.")
return hypo
logger.info("Received user interaction on hypothesis.")
return modified_hypo
def _interact_feedback(self, feedback: HypothesisFeedback) -> HypothesisFeedback:
if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")):
return feedback
logger.info("Waiting for user interaction on feedback...")
try:
self.user_request_q.put(feedback.__dict__)
res_dict = self.user_response_q.get()
modified_feedback = HypothesisFeedback(**res_dict)
except (EOFError, OSError, TypeError):
logger.info("User interaction failed, using original feedback.")
return feedback
logger.info("Received user interaction on feedback.")
return modified_feedback
def _propose(self):
hypothesis = self.hypothesis_gen.gen(self.trace)
hypothesis = self.hypothesis_gen.gen(self.trace, self.plan)
# user can change the hypothesis here
hypothesis = self._interact_hypo(hypothesis)
logger.log_object(hypothesis, tag="hypothesis generation")
return hypothesis
@@ -74,6 +201,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel():
hypo = self._propose()
exp = self._exp_gen(hypo)
exp.base_features = self.plan["features"]
exp.base_feature_codes = self.plan["feature_codes"]
if exp.based_experiments:
exp.based_experiments[-1].base_features = self.plan["features"]
exp.based_experiments[-1].base_feature_codes = self.plan["feature_codes"]
return {"propose": hypo, "exp_gen": exp}
await asyncio.sleep(1)
@@ -99,6 +231,7 @@ class RDLoop(LoopBase, metaclass=LoopMeta):
)
else:
feedback = self.summarizer.generate_feedback(prev_out["running"], self.trace)
feedback = self._interact_feedback(feedback)
logger.log_object(feedback, tag="feedback")
return feedback