diff --git a/rdagent/core/utils.py b/rdagent/core/utils.py index dd6b8e751..ab61b68e9 100644 --- a/rdagent/core/utils.py +++ b/rdagent/core/utils.py @@ -56,6 +56,14 @@ def __reduce__(self) -> NoReturn: def parse_json(response: str) -> Any: + import re + + # Some LLM providers (e.g. Volcengine) return Python-style booleans + # (True/False) instead of JSON-standard (true/false). Normalize them + # before parsing to avoid JSONDecodeError. + response = re.sub(r"\bTrue\b", "true", response) + response = re.sub(r"\bFalse\b", "false", response) + response = re.sub(r"\bNone\b", "null", response) try: return json.loads(response) except json.decoder.JSONDecodeError: diff --git a/rdagent/log/ui/app.py b/rdagent/log/ui/app.py index 881b32fb6..fe4c2fa1a 100644 --- a/rdagent/log/ui/app.py +++ b/rdagent/log/ui/app.py @@ -229,6 +229,9 @@ def get_msgs_until(end_func: Callable[[Message], bool] = lambda _: True): state.hypotheses[state.lround] = msg.content elif "evolving code" in tags: msg.content = [i for i in msg.content if i] + # Also store under the short key so downstream consumers + # that read state.msgs[round]["evolving code"] can find it. + state.msgs[state.lround]["evolving code"].append(msg) elif "evolving feedback" in tags: total_len = len(msg.content) none_num = total_len - len(msg.content) diff --git a/rdagent/log/utils/__init__.py b/rdagent/log/utils/__init__.py index 44edb67e6..89518ba85 100644 --- a/rdagent/log/utils/__init__.py +++ b/rdagent/log/utils/__init__.py @@ -99,7 +99,12 @@ def extract_evoid(tag: str) -> str | None: def extract_json(log_content: str) -> dict | None: match = re.search(r"\{.*\}", log_content, re.DOTALL) if match: - return cast(dict, json.loads(match.group(0))) + raw = match.group(0) + # Normalize Python-style booleans/null that some LLM providers return + raw = re.sub(r"\bTrue\b", "true", raw) + raw = re.sub(r"\bFalse\b", "false", raw) + raw = re.sub(r"\bNone\b", "null", raw) + return cast(dict, json.loads(raw)) return None diff --git a/rdagent/scenarios/qlib/developer/utils.py b/rdagent/scenarios/qlib/developer/utils.py index cd4abef3b..a55cea3d4 100644 --- a/rdagent/scenarios/qlib/developer/utils.py +++ b/rdagent/scenarios/qlib/developer/utils.py @@ -160,6 +160,40 @@ def process_factor_data(exp_or_list: List[QlibFactorExperiment] | QlibFactorExpe # Combine all successful factor data if factor_dfs: + # Normalize MultiIndex levels: some factors may produce 3-level + # MultiIndex (e.g. instrument, datetime, instrument) while others + # use the standard 2-level (datetime, instrument). Detect and fix + # mismatched indices before concat to prevent AssertionError. + target_nlevels = None + for df in factor_dfs: + if target_nlevels is None: + target_nlevels = df.index.nlevels + elif df.index.nlevels != target_nlevels: + # Reindex to the most common level count (usually 2) + level_counts = {} + for d in factor_dfs: + n = d.index.nlevels + level_counts[n] = level_counts.get(n, 0) + 1 + target_nlevels = max(level_counts, key=level_counts.get) + break + + if target_nlevels is not None: + normalized = [] + for df in factor_dfs: + if df.index.nlevels != target_nlevels: + # Try to swap/reorder levels or reset extra levels + if df.index.nlevels == 3 and target_nlevels == 2: + # 3-level index with duplicated instrument: drop the extra level + df.index = df.index.droplevel(0) + elif df.index.nlevels == 2 and target_nlevels == 3: + # Re-construct 3-level from 2-level is not possible without context + # Just try swapping as fallback + df = df.swaplevel(0, 1, axis=0) + normalized.append(df) + else: + normalized.append(df) + factor_dfs = normalized + try: return pd.concat(factor_dfs, axis=1) except Exception as concat_error: