diff --git a/rdagent/app/data_science/loop.py b/rdagent/app/data_science/loop.py index 8df7853c..d64c2e2f 100644 --- a/rdagent/app/data_science/loop.py +++ b/rdagent/app/data_science/loop.py @@ -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__": diff --git a/rdagent/app/qlib_rd_loop/factor.py b/rdagent/app/qlib_rd_loop/factor.py index 9a9d7174..b4e4ed1a 100755 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -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. diff --git a/rdagent/components/coder/model_coder/task_loader.py b/rdagent/components/coder/model_coder/task_loader.py index 4735e703..66da5c56 100644 --- a/rdagent/components/coder/model_coder/task_loader.py +++ b/rdagent/components/coder/model_coder/task_loader.py @@ -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 diff --git a/rdagent/log/server/README.md b/rdagent/log/server/README.md index dc76e16d..19b55649 100644 --- a/rdagent/log/server/README.md +++ b/rdagent/log/server/README.md @@ -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": "...", "b.py": "...", //... } }, { + "evo_id": "0", "target_task_name": "task_2", - "codes": { + "workspace": { "a.py": "...", //... } @@ -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 } } -``` \ No newline at end of file +``` + +# TODO + +## Session + +- How to continue. +- show & copy trace_id(name)? +- + +## Page + +1. remove Medical, add Finance Whole Pipeline +2. \ No newline at end of file diff --git a/rdagent/log/server/app.py b/rdagent/log/server/app.py index 78ad2367..807cd352 100644 --- a/rdagent/log/server/app.py +++ b/rdagent/log/server/app.py @@ -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("/", methods=["GET"]) diff --git a/rdagent/log/server/debug_app.py b/rdagent/log/server/debug_app.py new file mode 100644 index 00000000..50265744 --- /dev/null +++ b/rdagent/log/server/debug_app.py @@ -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("/", 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) diff --git a/rdagent/log/ui/conf.py b/rdagent/log/ui/conf.py index c948d6e4..ee32412c 100644 --- a/rdagent/log/ui/conf.py +++ b/rdagent/log/ui/conf.py @@ -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() diff --git a/rdagent/log/ui/ds_summary.py b/rdagent/log/ui/ds_summary.py index 5ee9317a..b6456200 100755 --- a/rdagent/log/ui/ds_summary.py +++ b/rdagent/log/ui/ds_summary.py @@ -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( diff --git a/rdagent/log/ui/storage.py b/rdagent/log/ui/storage.py index 97b58317..bff1c48b 100644 --- a/rdagent/log/ui/storage.py +++ b/rdagent/log/ui/storage.py @@ -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