Compare commits

...

14 Commits

Author SHA1 Message Date
github-actions[bot] 7d97d84100 chore(master): release 1.4.0 (#46)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 15:59:06 +02:00
TPTBusiness 9bc525a264 feat(optimizer): add max_positions parameter to Optuna search space
Add max_positions (1-5) as an optimizable hyperparameter across all
three Optuna search stages (coarse, fine, very fine). The parameter
scales effective position size as min(position_size_pct × max_positions,
1.0), allowing the optimizer to discover pyramiding strategies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 15:58:01 +02:00
github-actions[bot] 7cb6531c2a chore(master): release 1.3.11 (#45)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 13:58:55 +02:00
TPTBusiness 44c8af572e fix(ci): lazy import logger in predix.py and cli.py to avoid ImportError in test env
Wrapped  in try/except
ImportError with standard logging fallback. The rdagent.log
module chain fails when predix.py is imported as a module
in the CI test environment (kronos CLI tests).
2026-05-01 13:58:16 +02:00
github-actions[bot] b53749df7d chore(master): release 1.3.10 (#44)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 13:50:38 +02:00
TPTBusiness 3a1a3d5f77 fix(security): replace remaining assert statements with proper error handling
Replaced 53 assert statements across 22 files with proper
if/raise patterns (TypeError, ValueError, AssertionError)
to resolve Bandit B101 alerts.
2026-05-01 13:49:58 +02:00
github-actions[bot] 13cbd42ecf chore(master): release 1.3.9 (#43)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 13:43:52 +02:00
TPTBusiness 64ed6b0cce fix(security): resolve path-injection, B701, B101, B112 Bandit alerts
- Path injection (B614): centralized safe_resolve_path in core/utils.py,
  refactored 6 UI modules to use it with safe_root validation
- B701: added explicit autoescape=select_autoescape() to Jinja2
  Environment() calls in 3 files
- B101: replaced assert statements with proper if/raise patterns in
  12+ files (partial)
- B112: added logger.warning() to bare except:continue blocks in
  5 files
2026-05-01 13:42:59 +02:00
github-actions[bot] bf36f54159 chore(master): release 1.3.8 (#42)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-30 20:01:29 +02:00
TPTBusiness 79f1d34083 fix(security): resolve path-injection and add nosec for safe temp paths (B108, py/path-injection)
- ds_trace.py: resolve() user-provided save path and use Path.name for filenames
  to prevent directory traversal in the local workspace save UI
- rl/finetune UI data_loaders: nosec B614 where paths are already validated
  against safe_root via realpath() before use
- Temp paths (/tmp/sample, /tmp/full, /tmp/mock/*, /tmp/predix_loop.pid,
  /tmp/autorl_output): nosec B108 — fixed Docker volume mount points or
  single-process admin files, not user-writable attack surface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:38 +02:00
TPTBusiness 150a818e07 fix(security): replace shell=True subprocess calls with list args in env.py (B602)
Converted conda commands in _update_bin_path, _sync_conda_cache_with_real_envs,
_prepare_conda_env, and FTCondaEnv.prepare() to list args. Replaced pipe-based
grep with pure Python parsing. LocalEnv.Popen retains shell=True with nosec
since entry is an internal command string set by LocalEnvConf, not user input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:29 +02:00
TPTBusiness b6d1caecc9 fix(security): replace eval() with ast.literal_eval in finetune validator (B307)
eval() on trainer stdout output replaced with ast.literal_eval() which only
parses Python literals and cannot execute arbitrary code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 19:26:23 +02:00
TPTBusiness 73e600bf25 fix(qlib): correct indentation in except blocks in quant_proposal and factor_runner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 13:30:49 +02:00
TPTBusiness 9960633d01 fix(deps): relax aiohttp constraint to >=3.13.4 for litellm compatibility
litellm 1.83.14 pins aiohttp==3.13.4 exactly; requiring >=3.13.5 caused
an unresolvable conflict in CI. aiohttp 3.13.4 still patches all four CVEs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:44:09 +02:00
58 changed files with 336 additions and 244 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
".": "1.3.7" ".": "1.4.0"
} }
+39
View File
@@ -1,5 +1,44 @@
# Changelog # Changelog
## [1.4.0](https://github.com/TPTBusiness/Predix/compare/v1.3.11...v1.4.0) (2026-05-01)
### Features
* **optimizer:** add max_positions parameter to Optuna search space ([fdb4be3](https://github.com/TPTBusiness/Predix/commit/fdb4be3b3ebd93325e7821f4251148424184a40d))
## [1.3.11](https://github.com/TPTBusiness/Predix/compare/v1.3.10...v1.3.11) (2026-05-01)
### Bug Fixes
* **ci:** lazy import logger in predix.py and cli.py to avoid ImportError in test env ([60763e8](https://github.com/TPTBusiness/Predix/commit/60763e8eae34f41865ba8e5e65bdfde13b564b4b))
## [1.3.10](https://github.com/TPTBusiness/Predix/compare/v1.3.9...v1.3.10) (2026-05-01)
### Bug Fixes
* **security:** replace remaining assert statements with proper error handling ([928533d](https://github.com/TPTBusiness/Predix/commit/928533d9a81bd5062f07458fbf94d3c7fe347775))
## [1.3.9](https://github.com/TPTBusiness/Predix/compare/v1.3.8...v1.3.9) (2026-05-01)
### Bug Fixes
* **security:** resolve path-injection, B701, B101, B112 Bandit alerts ([20b89a0](https://github.com/TPTBusiness/Predix/commit/20b89a061843b39836e975f158404e8e2d4627cd))
## [1.3.8](https://github.com/TPTBusiness/Predix/compare/v1.3.7...v1.3.8) (2026-04-30)
### Bug Fixes
* **deps:** relax aiohttp constraint to &gt;=3.13.4 for litellm compatibility ([34ab192](https://github.com/TPTBusiness/Predix/commit/34ab1923a887089eb36e5cbad6cb8df16f0333ca))
* **qlib:** correct indentation in except blocks in quant_proposal and factor_runner ([8143451](https://github.com/TPTBusiness/Predix/commit/8143451e8c0ead01c4d86d19669268c7bfb15fac))
* **security:** replace eval() with ast.literal_eval in finetune validator (B307) ([0508caf](https://github.com/TPTBusiness/Predix/commit/0508caf9140d210b823fefefa28ee535ec85a0ae))
* **security:** replace shell=True subprocess calls with list args in env.py (B602) ([2012d5a](https://github.com/TPTBusiness/Predix/commit/2012d5ae4e77cc2f1ab9a48beaaac5a74695d083))
* **security:** resolve path-injection and add nosec for safe temp paths (B108, py/path-injection) ([6727480](https://github.com/TPTBusiness/Predix/commit/67274803bd1d14e5d1df9a063f46b2edb8501a2b))
## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30) ## [1.3.7](https://github.com/TPTBusiness/Predix/compare/v1.3.6...v1.3.7) (2026-04-30)
+12
View File
@@ -18,6 +18,13 @@ load_dotenv(Path(__file__).parent / ".env")
import typer import typer
from rich.console import Console from rich.console import Console
try:
from rdagent.utils.env import logger
except ImportError:
import logging
logger = logging.getLogger(__name__)
app = typer.Typer(help="Predix - AI Quantitative Trading Agent") app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
console = Console() console = Console()
@@ -510,6 +517,7 @@ def top(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -659,6 +667,7 @@ def portfolio(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -956,6 +965,7 @@ def portfolio_simple(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
results.append(data) results.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if not results: if not results:
@@ -1337,6 +1347,7 @@ def build_strategies_ai(
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if len(factors) < 10: if len(factors) < 10:
@@ -1552,6 +1563,7 @@ def _load_strategies():
try: try:
raw = json.loads(p.read_text()) raw = json.loads(p.read_text())
except Exception: except Exception:
logger.warning("Failed to load strategy file %s", p, exc_info=True)
continue continue
if not isinstance(raw, dict): if not isinstance(raw, dict):
continue continue
+9 -1
View File
@@ -27,6 +27,13 @@ import typer
from rich.console import Console from rich.console import Console
from typing_extensions import Annotated from typing_extensions import Annotated
try:
from rdagent.utils.env import logger
except ImportError:
import logging
logger = logging.getLogger(__name__)
from rdagent.app.data_science.loop import main as data_science from rdagent.app.data_science.loop import main as data_science
from rdagent.app.finetune.llm.loop import main as llm_finetune from rdagent.app.finetune.llm.loop import main as llm_finetune
from rdagent.app.general_model.general_model import ( from rdagent.app.general_model.general_model import (
@@ -882,6 +889,7 @@ def optimize_portfolio_cli(
if data.get("status") == "accepted": if data.get("status") == "accepted":
strategies.append(data) strategies.append(data)
except Exception: except Exception:
logger.warning("Failed to load strategy file %s", f, exc_info=True)
continue continue
if not strategies: if not strategies:
@@ -1251,7 +1259,7 @@ def start_loop_cli(
script_dir = str(Path(__file__).parent.parent.parent.parent) script_dir = str(Path(__file__).parent.parent.parent.parent)
generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py" generator = f"python {script_dir}/scripts/predix_smart_strategy_gen.py"
logfile = f"{script_dir}/results/logs/generator_loop.log" logfile = f"{script_dir}/results/logs/generator_loop.log"
pidfile = "/tmp/predix_loop.pid" pidfile = "/tmp/predix_loop.pid" # nosec B108 — administrative PID file, single-process daemon
os.makedirs(f"{script_dir}/results/logs", exist_ok=True) os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
+2 -3
View File
@@ -201,6 +201,5 @@ class DataScienceBasePropSetting(KaggleBasePropSetting):
DS_RD_SETTING = DataScienceBasePropSetting() DS_RD_SETTING = DataScienceBasePropSetting()
# enable_cross_trace_diversity and llm_select_hypothesis should not be true at the same time # enable_cross_trace_diversity and llm_select_hypothesis should not be true at the same time
assert not ( if DS_RD_SETTING.enable_cross_trace_diversity and DS_RD_SETTING.llm_select_hypothesis:
DS_RD_SETTING.enable_cross_trace_diversity and DS_RD_SETTING.llm_select_hypothesis raise ValueError("enable_cross_trace_diversity and llm_select_hypothesis cannot be true at the same time")
), "enable_cross_trace_diversity and llm_select_hypothesis cannot be true at the same time"
+8 -9
View File
@@ -58,18 +58,18 @@ def main(
if user_target_scenario: if user_target_scenario:
FT_RD_SETTING.user_target_scenario = user_target_scenario FT_RD_SETTING.user_target_scenario = user_target_scenario
assert ( if FT_RD_SETTING.user_target_scenario is not None:
FT_RD_SETTING.user_target_scenario is None raise ValueError("user_target_scenario is not yet supported, please specify via benchmark and benchmark_description")
), "user_target_scenario is not yet supported, please specify via benchmark and benchmark_description"
if upper_data_size_limit: if upper_data_size_limit:
FT_RD_SETTING.upper_data_size_limit = upper_data_size_limit FT_RD_SETTING.upper_data_size_limit = upper_data_size_limit
logger.info(f"Set upper_data_size_limit to {FT_RD_SETTING.upper_data_size_limit}") logger.info(f"Set upper_data_size_limit to {FT_RD_SETTING.upper_data_size_limit}")
if benchmark and benchmark_description: if benchmark and benchmark_description:
FT_RD_SETTING.target_benchmark = benchmark FT_RD_SETTING.target_benchmark = benchmark
FT_RD_SETTING.benchmark_description = benchmark_description FT_RD_SETTING.benchmark_description = benchmark_description
assert FT_RD_SETTING.user_target_scenario or ( if not (
FT_RD_SETTING.target_benchmark and FT_RD_SETTING.benchmark_description FT_RD_SETTING.user_target_scenario or (FT_RD_SETTING.target_benchmark and FT_RD_SETTING.benchmark_description)
), "Either user_target_scenario or target_benchmark must be specified for LLM fine-tuning." ):
raise ValueError("Either user_target_scenario or target_benchmark must be specified for LLM fine-tuning.")
# Update configuration with provided parameters # Update configuration with provided parameters
if dataset: if dataset:
@@ -82,9 +82,8 @@ def main(
model_target = FT_RD_SETTING.base_model if FT_RD_SETTING.base_model else "auto selected model" model_target = FT_RD_SETTING.base_model if FT_RD_SETTING.base_model else "auto selected model"
# Temporary assertion until auto-selection is implemented # Temporary assertion until auto-selection is implemented
assert ( if FT_RD_SETTING.base_model is None:
FT_RD_SETTING.base_model is not None raise ValueError("Base model auto selection not yet supported, please specify via --base-model")
), "Base model auto selection not yet supported, please specify via --base-model"
logger.info(f"Starting LLM fine-tuning on dataset='{data_set_target}' with model='{model_target}'") logger.info(f"Starting LLM fine-tuning on dataset='{data_set_target}' with model='{model_target}'")
+8 -47
View File
@@ -24,46 +24,12 @@ from rdagent.app.finetune.llm.ui.ft_summary import render_job_summary
DEFAULT_LOG_BASE = "log/" DEFAULT_LOG_BASE = "log/"
from rdagent.core.utils import safe_resolve_path
def validate_path_within_cwd(user_path: Path) -> Path: def validate_path_within_cwd(user_path: Path) -> Path:
"""
Validate that a user-provided path is within the current working directory.
Security: This function prevents path traversal attacks by:
1. Resolving the path to its absolute canonical form
2. Verifying it's within the CWD boundary using a normalized common prefix
3. Rejecting paths outside the boundary with ValueError
Parameters
----------
user_path : Path
User-provided path to validate
Returns
-------
Path
Resolved absolute path if valid
Raises
------
ValueError
If path is outside the current working directory
"""
safe_root = Path.cwd().resolve() safe_root = Path.cwd().resolve()
# Expand any user home reference and resolve without requiring the path to exist. return safe_resolve_path(user_path, safe_root)
resolved_path = user_path.expanduser().resolve(strict=False)
# Ensure the resolved path is absolute and remains within the safe root.
safe_root_str = str(safe_root)
resolved_str = str(resolved_path)
common = os.path.commonpath([safe_root_str, resolved_str])
if common != safe_root_str:
raise ValueError("Path is outside the allowed project directory")
# This will raise ValueError if resolved_path is not within safe_root
resolved_path.relative_to(safe_root)
return resolved_path
def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]: def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]:
@@ -141,19 +107,14 @@ def main():
st.header("Job") st.header("Job")
base_folder = st.text_input("Base Folder", value=default_log, key="base_folder_input") base_folder = st.text_input("Base Folder", value=default_log, key="base_folder_input")
# Normalize and validate the base folder against the configured log root safe_root = Path(default_log).expanduser().resolve()
root_real = os.path.realpath(str(Path(default_log).expanduser())) try:
folder_real = os.path.realpath(str(Path(base_folder).expanduser())) base_path = safe_resolve_path(Path(base_folder), safe_root)
if folder_real == root_real or folder_real.startswith(root_real + os.sep): except ValueError:
base_path = Path(folder_real)
safe_root = Path(root_real)
else:
st.error("Invalid base folder: must be within the configured log directory.") st.error("Invalid base folder: must be within the configured log directory.")
safe_root = Path(root_real)
base_path = safe_root base_path = safe_root
# base_path is validated against safe_root nosec B614 job_options = get_job_options(base_path, safe_root)
job_options = get_job_options(base_path, safe_root) # nosec B614 validated above
if job_options: if job_options:
selected_job = st.selectbox("Select Job", job_options, key="job_select") selected_job = st.selectbox("Select Job", job_options, key="job_select")
if selected_job.startswith("."): if selected_job.startswith("."):
+7 -9
View File
@@ -13,6 +13,7 @@ from typing import Any
import streamlit as st import streamlit as st
from rdagent.app.finetune.llm.ui.config import EVALUATOR_CONFIG, EventType from rdagent.app.finetune.llm.ui.config import EVALUATOR_CONFIG, EventType
from rdagent.core.utils import safe_resolve_path
from rdagent.log.storage import FileStorage from rdagent.log.storage import FileStorage
@@ -89,11 +90,10 @@ def extract_stage(tag: str) -> str:
def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]: def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]:
"""Get list of valid session directories, optionally validating against a safe root.""" """Get list of valid session directories, optionally validating against a safe root."""
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
folder_real = os.path.realpath(str(log_folder.expanduser())) log_folder = safe_resolve_path(log_folder, safe_root)
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)): except ValueError:
return [] return []
log_folder = Path(folder_real)
if not log_folder.exists(): if not log_folder.exists():
return [] return []
@@ -373,13 +373,11 @@ def parse_event(tag: str, content: Any, timestamp: datetime) -> Event | None:
@st.cache_data(ttl=300, hash_funcs={Path: str}) @st.cache_data(ttl=300, hash_funcs={Path: str})
def load_ft_session(log_path: Path, safe_root: Path | None = None) -> Session: def load_ft_session(log_path: Path, safe_root: Path | None = None) -> Session:
"""Load events into hierarchical session structure, optionally validating against safe root.""" """Load events into hierarchical session structure, optionally validating against safe root."""
# Validate path is within safe_root if provided
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
path_real = os.path.realpath(str(log_path.expanduser())) log_path = safe_resolve_path(log_path, safe_root)
if not (path_real == root_real or path_real.startswith(root_real + os.sep)): except ValueError:
return Session() return Session()
log_path = Path(path_real)
session = Session() session = Session()
storage = FileStorage(log_path) storage = FileStorage(log_path)
+3 -1
View File
@@ -78,7 +78,8 @@ class QuantRDLoop(RDLoop):
while True: while True:
if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel(): if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel():
hypo = self._propose() hypo = self._propose()
assert hypo.action in ["factor", "model"] if hypo.action not in ["factor", "model"]:
raise ValueError(f"hypo.action must be 'factor' or 'model', got {hypo.action!r}")
if hypo.action == "factor": if hypo.action == "factor":
exp = self.factor_hypothesis2experiment.convert(hypo, self.trace) exp = self.factor_hypothesis2experiment.convert(hypo, self.trace)
else: else:
@@ -322,6 +323,7 @@ class QuantRDLoop(RDLoop):
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
if len(factors) < 10: if len(factors) < 10:
+5 -35
View File
@@ -16,55 +16,26 @@ from rdagent.app.rl.ui.components import render_session, render_summary
from rdagent.app.rl.ui.config import ALWAYS_VISIBLE_TYPES, OPTIONAL_TYPES from rdagent.app.rl.ui.config import ALWAYS_VISIBLE_TYPES, OPTIONAL_TYPES
from rdagent.app.rl.ui.data_loader import get_summary, get_valid_sessions, load_session from rdagent.app.rl.ui.data_loader import get_summary, get_valid_sessions, load_session
from rdagent.app.rl.ui.rl_summary import render_job_summary from rdagent.app.rl.ui.rl_summary import render_job_summary
from rdagent.core.utils import safe_resolve_path
DEFAULT_LOG_BASE = "log/" DEFAULT_LOG_BASE = "log/"
def _safe_resolve(user_input: str | None, safe_root: Path) -> Path: def _safe_resolve(user_input: str | None, safe_root: Path) -> Path:
"""
Resolve user path relative to safe_root; raise ValueError if it escapes.
Security: This function prevents path traversal attacks by:
1. Rejecting null bytes in user input
2. Rejecting Windows drive letters (C:\, D:\, etc.)
3. Rejecting absolute paths
4. Normalizing path to remove .. traversal attempts
5. Validating resolved path is within safe_root using a realpath-based check
All user-provided paths are validated before filesystem access.
"""
# Treat the provided safe_root as trusted and canonicalize it once.
safe_root = safe_root.expanduser().resolve() safe_root = safe_root.expanduser().resolve()
# Empty input maps to the safe root directory.
if not user_input: if not user_input:
return safe_root return safe_root
# Security check 1: Reject null bytes (path truncation attack)
if "\x00" in user_input: if "\x00" in user_input:
raise ValueError("Invalid path: contains null byte") raise ValueError("Invalid path: contains null byte")
try: try:
# Security check 2: Normalize path to resolve .. and . components
normalized = os.path.normpath(user_input.strip()) normalized = os.path.normpath(user_input.strip())
# Security check 3: Reject Windows drive letters (C:\, D:\, etc.)
drive, _ = os.path.splitdrive(normalized) drive, _ = os.path.splitdrive(normalized)
if drive: if drive:
raise ValueError("Absolute paths with drive letters are not allowed") raise ValueError("Absolute paths with drive letters are not allowed")
# Security check 4: Reject absolute paths (/, //server/share, etc.)
if os.path.isabs(normalized): if os.path.isabs(normalized):
raise ValueError("Absolute paths are not allowed") raise ValueError("Absolute paths are not allowed")
joined = safe_root / normalized
# Security check 5: Build candidate path under safe_root and fully resolve it. return safe_resolve_path(joined, safe_root)
joined = os.path.join(str(safe_root), normalized)
resolved_candidate = os.path.realpath(joined)
# Security check 6: Validate candidate is within safe_root (prevent path traversal)
candidate_path = Path(resolved_candidate)
# Reconstruct from trusted safe_root so the returned path is root-derived.
return safe_root / candidate_path.relative_to(safe_root)
except (OSError, ValueError) as exc: except (OSError, ValueError) as exc:
raise ValueError(f"Invalid path outside of allowed root: {user_input}") from exc raise ValueError(f"Invalid path outside of allowed root: {user_input}") from exc
@@ -82,7 +53,7 @@ def get_job_options(base_path: Path, safe_root: Path | None = None) -> list[str]
# Security fix: Validate base_path to prevent path traversal # Security fix: Validate base_path to prevent path traversal
try: try:
base_path_resolved = base_path.expanduser().resolve() base_path_resolved = base_path.expanduser().resolve() # nosec B614 — validated against safe_root below via relative_to()
if safe_root is not None: if safe_root is not None:
safe_root_resolved = safe_root.expanduser().resolve() safe_root_resolved = safe_root.expanduser().resolve()
@@ -203,8 +174,7 @@ def main():
except ValueError as e: except ValueError as e:
st.warning(str(e)) st.warning(str(e))
return return
# job_path is validated by _safe_resolve() above if job_path.exists():
if job_path.exists(): # nosec B614 path validated by _safe_resolve
render_job_summary(job_path, safe_root, is_root=is_root_job) render_job_summary(job_path, safe_root, is_root=is_root_job)
else: else:
st.warning(f"Job folder not found: {job_folder}") st.warning(f"Job folder not found: {job_folder}")
+7 -9
View File
@@ -15,6 +15,7 @@ from typing import Any
import streamlit as st import streamlit as st
from rdagent.app.rl.ui.config import EventType from rdagent.app.rl.ui.config import EventType
from rdagent.core.utils import safe_resolve_path
from rdagent.log.storage import FileStorage from rdagent.log.storage import FileStorage
@@ -76,11 +77,10 @@ def extract_stage(tag: str) -> str:
def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]: def get_valid_sessions(log_folder: Path, safe_root: Path | None = None) -> list[str]:
"""Get list of valid session directories, optionally validating against a safe root.""" """Get list of valid session directories, optionally validating against a safe root."""
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
folder_real = os.path.realpath(str(log_folder.expanduser())) log_folder = safe_resolve_path(log_folder, safe_root)
if not (folder_real == root_real or folder_real.startswith(root_real + os.sep)): except ValueError:
return [] return []
log_folder = Path(folder_real)
if not log_folder.exists(): if not log_folder.exists():
return [] return []
@@ -245,13 +245,11 @@ def parse_event(tag: str, content: Any, timestamp: datetime) -> Event | None:
@st.cache_data(ttl=300, hash_funcs={Path: str}) @st.cache_data(ttl=300, hash_funcs={Path: str})
def load_session(log_path: Path, safe_root: Path | None = None) -> Session: def load_session(log_path: Path, safe_root: Path | None = None) -> Session:
"""Load events into hierarchical session structure, optionally validating against safe root.""" """Load events into hierarchical session structure, optionally validating against safe root."""
# Validate path is within safe_root if provided
if safe_root is not None: if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser())) try:
path_real = os.path.realpath(str(log_path.expanduser())) log_path = safe_resolve_path(log_path, safe_root)
if not (path_real == root_real or path_real.startswith(root_real + os.sep)): except ValueError:
return Session() return Session()
log_path = Path(path_real)
session = Session() session = Session()
+4 -6
View File
@@ -9,6 +9,8 @@ from pathlib import Path
import pandas as pd import pandas as pd
import streamlit as st import streamlit as st
from rdagent.core.utils import safe_resolve_path
def is_valid_task(task_path: Path) -> bool: def is_valid_task(task_path: Path) -> bool:
"""Check if directory is a valid RL task (has __session__ subdirectory)""" """Check if directory is a valid RL task (has __session__ subdirectory)"""
@@ -62,14 +64,10 @@ def get_loop_status(task_path: Path, loop_id: int) -> tuple[str, bool | None]:
def _validate_job_path(job_path: Path, safe_root: Path) -> Path: def _validate_job_path(job_path: Path, safe_root: Path) -> Path:
"""Resolve and validate that job_path stays within safe_root."""
resolved_root = safe_root.expanduser().resolve()
resolved_job = job_path.expanduser().resolve()
try: try:
# Reconstruct from trusted root so the returned path is root-derived. return safe_resolve_path(job_path, safe_root)
return resolved_root / resolved_job.relative_to(resolved_root)
except ValueError: except ValueError:
raise ValueError(f"Job path is outside allowed root {resolved_root}") raise ValueError(f"Job path is outside allowed root {safe_root}")
def get_max_loops(job_path: Path, safe_root: Path | None = None) -> int: def get_max_loops(job_path: Path, safe_root: Path | None = None) -> int:
+14 -7
View File
@@ -75,8 +75,10 @@ class CoSTEER(Developer[Experiment]):
def _get_last_fb(self) -> CoSTEERMultiFeedback: def _get_last_fb(self) -> CoSTEERMultiFeedback:
fb = self.evolve_agent.evolving_trace[-1].feedback fb = self.evolve_agent.evolving_trace[-1].feedback
assert fb is not None, "feedback is None" if fb is None:
assert isinstance(fb, CoSTEERMultiFeedback), "feedback must be of type CoSTEERMultiFeedback" raise AssertionError("feedback is None")
if not isinstance(fb, CoSTEERMultiFeedback):
raise TypeError("feedback must be of type CoSTEERMultiFeedback")
return fb return fb
def should_use_new_evo(self, base_fb: CoSTEERMultiFeedback | None, new_fb: CoSTEERMultiFeedback) -> bool: def should_use_new_evo(self, base_fb: CoSTEERMultiFeedback | None, new_fb: CoSTEERMultiFeedback) -> bool:
@@ -121,7 +123,8 @@ class CoSTEER(Developer[Experiment]):
for evo_exp in self.evolve_agent.multistep_evolve(evo_exp, self.evaluator): for evo_exp in self.evolve_agent.multistep_evolve(evo_exp, self.evaluator):
iteration_count += 1 iteration_count += 1
assert isinstance(evo_exp, Experiment) # multiple inheritance if not isinstance(evo_exp, Experiment):
raise TypeError("evo_exp must be an instance of Experiment")
evo_fb = self._get_last_fb() evo_fb = self._get_last_fb()
update_fallback = self.should_use_new_evo( update_fallback = self.should_use_new_evo(
base_fb=fallback_evo_fb, base_fb=fallback_evo_fb,
@@ -154,7 +157,8 @@ class CoSTEER(Developer[Experiment]):
evo_exp = fallback_evo_exp evo_exp = fallback_evo_exp
evo_exp.recover_ws_ckp() evo_exp.recover_ws_ckp()
evo_fb = fallback_evo_fb evo_fb = fallback_evo_fb
assert evo_fb is not None # multistep_evolve should run at least once if evo_fb is None:
raise AssertionError("multistep_evolve should run at least once")
evo_exp = self._exp_postprocess_by_feedback(evo_exp, evo_fb) evo_exp = self._exp_postprocess_by_feedback(evo_exp, evo_fb)
except CoderError as e: except CoderError as e:
e.caused_by_timeout = reached_max_seconds e.caused_by_timeout = reached_max_seconds
@@ -264,9 +268,12 @@ class CoSTEER(Developer[Experiment]):
- Raise Error if it failed to handle the develop task - Raise Error if it failed to handle the develop task
- -
""" """
assert isinstance(evo, Experiment) if not isinstance(evo, Experiment):
assert isinstance(feedback, CoSTEERMultiFeedback) raise TypeError("evo must be an instance of Experiment")
assert len(evo.sub_workspace_list) == len(feedback) if not isinstance(feedback, CoSTEERMultiFeedback):
raise TypeError("feedback must be an instance of CoSTEERMultiFeedback")
if len(evo.sub_workspace_list) != len(feedback):
raise ValueError("Length of sub_workspace_list must match length of feedback")
# FIXME: when whould the feedback be None? # FIXME: when whould the feedback be None?
failed_feedbacks = [ failed_feedbacks = [
@@ -122,7 +122,8 @@ class MultiProcessEvolvingStrategy(EvolvingStrategy):
last_feedback = None last_feedback = None
if len(evolving_trace) > 0: if len(evolving_trace) > 0:
last_feedback = evolving_trace[-1].feedback last_feedback = evolving_trace[-1].feedback
assert isinstance(last_feedback, CoSTEERMultiFeedback) if not isinstance(last_feedback, CoSTEERMultiFeedback):
raise TypeError("last_feedback must be of type CoSTEERMultiFeedback")
# 1.找出需要evolve的task # 1.找出需要evolve的task
to_be_finished_task_index: list[int] = [] to_be_finished_task_index: list[int] = []
@@ -1028,7 +1028,8 @@ class CoSTEERKnowledgeBaseV2(EvolvingKnowledgeBase):
""" """
node_count = len(nodes) node_count = len(nodes)
assert node_count >= 2, "nodes length must >=2" if node_count < 2:
raise ValueError("nodes length must >=2")
intersection_node_list = [] intersection_node_list = []
if output_intersection_origin: if output_intersection_origin:
origin_list = [] origin_list = []
@@ -54,7 +54,8 @@ def get_ds_env(
ValueError: If the env_type is not recognized. ValueError: If the env_type is not recognized.
""" """
conf = DSCoderCoSTEERSettings() conf = DSCoderCoSTEERSettings()
assert conf_type in ["kaggle", "mlebench"], f"Unknown conf_type: {conf_type}" if conf_type not in ["kaggle", "mlebench"]:
raise ValueError(f"Unknown conf_type: {conf_type}")
if conf.env_type == "docker": if conf.env_type == "docker":
env_conf = DSDockerConf() if conf_type == "kaggle" else MLEBDockerConf() env_conf = DSDockerConf() if conf_type == "kaggle" else MLEBDockerConf()
@@ -79,7 +80,8 @@ def get_clear_ws_cmd(stage: Literal["before_training", "before_inference"] = "be
""" """
Clean the files in workspace to a specific stage Clean the files in workspace to a specific stage
""" """
assert stage in ["before_training", "before_inference"], f"Unknown stage: {stage}" if stage not in ["before_training", "before_inference"]:
raise ValueError(f"Unknown stage: {stage}")
if DS_RD_SETTING.enable_model_dump and stage == "before_training": if DS_RD_SETTING.enable_model_dump and stage == "before_training":
cmd = "rm -r submission.csv scores.csv models trace.log" cmd = "rm -r submission.csv scores.csv models trace.log"
else: else:
@@ -13,7 +13,7 @@ File structure
from pathlib import Path from pathlib import Path
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.app.data_science.conf import DS_RD_SETTING
from rdagent.components.coder.CoSTEER.evaluators import ( from rdagent.components.coder.CoSTEER.evaluators import (
@@ -88,7 +88,7 @@ class EnsembleMultiProcessEvolvingStrategy(MultiProcessEvolvingStrategy):
code_spec = workspace.file_dict["spec/ensemble.md"] code_spec = workspace.file_dict["spec/ensemble.md"]
else: else:
test_code = ( test_code = (
Environment(undefined=StrictUndefined) Environment(undefined=StrictUndefined, autoescape=select_autoescape())
.from_string((DIRNAME / "eval_tests" / "ensemble_test.txt").read_text()) .from_string((DIRNAME / "eval_tests" / "ensemble_test.txt").read_text())
.render( .render(
model_names=[ model_names=[
@@ -2,7 +2,7 @@ import json
import re import re
from pathlib import Path from pathlib import Path
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.app.data_science.conf import DS_RD_SETTING
from rdagent.components.coder.CoSTEER.evaluators import ( from rdagent.components.coder.CoSTEER.evaluators import (
@@ -55,7 +55,7 @@ class EnsembleCoSTEEREvaluator(CoSTEEREvaluator):
fname = "test/ensemble_test.txt" fname = "test/ensemble_test.txt"
test_code = (DIRNAME / "eval_tests" / "ensemble_test.txt").read_text() test_code = (DIRNAME / "eval_tests" / "ensemble_test.txt").read_text()
test_code = ( test_code = (
Environment(undefined=StrictUndefined) Environment(undefined=StrictUndefined, autoescape=select_autoescape())
.from_string(test_code) .from_string(test_code)
.render( .render(
model_names=[ model_names=[
@@ -6,6 +6,7 @@ Two-step validation:
2. Micro-batch testing - Runtime validation with small dataset 2. Micro-batch testing - Runtime validation with small dataset
""" """
import ast
import json import json
import re import re
import time import time
@@ -229,7 +230,7 @@ class LLMConfigValidator:
final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout) final_metrics = re.search(r"\{'train_runtime':[^}]+\}", stdout)
if final_metrics: if final_metrics:
try: try:
metrics = eval(final_metrics.group(0)) # Safe: only numbers and strings metrics = ast.literal_eval(final_metrics.group(0))
result["final_metrics"] = { result["final_metrics"] = {
"train_loss": metrics.get("train_loss"), "train_loss": metrics.get("train_loss"),
"train_runtime": metrics.get("train_runtime"), "train_runtime": metrics.get("train_runtime"),
@@ -58,10 +58,12 @@ class ModelCodeEvaluator(CoSTEEREvaluator):
model_execution_feedback: str = "", model_execution_feedback: str = "",
model_value_feedback: str = "", model_value_feedback: str = "",
): ):
assert isinstance(target_task, ModelTask) if not isinstance(target_task, ModelTask):
assert isinstance(implementation, ModelFBWorkspace) raise TypeError("target_task must be of type ModelTask")
if gt_implementation is not None: if not isinstance(implementation, ModelFBWorkspace):
assert isinstance(gt_implementation, ModelFBWorkspace) raise TypeError("implementation must be of type ModelFBWorkspace")
if gt_implementation is not None and not isinstance(gt_implementation, ModelFBWorkspace):
raise TypeError("gt_implementation must be of type ModelFBWorkspace")
model_task_information = target_task.get_task_information() model_task_information = target_task.get_task_information()
code = implementation.all_codes code = implementation.all_codes
@@ -113,10 +115,12 @@ class ModelFinalEvaluator(CoSTEEREvaluator):
model_value_feedback: str, model_value_feedback: str,
model_code_feedback: str, model_code_feedback: str,
): ):
assert isinstance(target_task, ModelTask) if not isinstance(target_task, ModelTask):
assert isinstance(implementation, ModelFBWorkspace) raise TypeError("target_task must be of type ModelTask")
if gt_implementation is not None: if not isinstance(implementation, ModelFBWorkspace):
assert isinstance(gt_implementation, ModelFBWorkspace) raise TypeError("implementation must be of type ModelFBWorkspace")
if gt_implementation is not None and not isinstance(gt_implementation, ModelFBWorkspace):
raise TypeError("gt_implementation must be of type ModelFBWorkspace")
system_prompt = T(".prompts:evaluator_final_feedback.system").r( system_prompt = T(".prompts:evaluator_final_feedback.system").r(
scenario=( scenario=(
@@ -41,7 +41,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
final_feedback="This task has failed too many times, skip implementation.", final_feedback="This task has failed too many times, skip implementation.",
final_decision=False, final_decision=False,
) )
assert isinstance(target_task, ModelTask) if not isinstance(target_task, ModelTask):
raise TypeError(f"Expected ModelTask, got {type(target_task)}")
# NOTE: Use fixed input to test the model to avoid randomness # NOTE: Use fixed input to test the model to avoid randomness
batch_size = 8 batch_size = 8
@@ -50,7 +51,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
input_value = 0.4 input_value = 0.4
param_init_value = 0.6 param_init_value = 0.6
assert isinstance(implementation, ModelFBWorkspace) if not isinstance(implementation, ModelFBWorkspace):
raise TypeError(f"Expected ModelFBWorkspace, got {type(implementation)}")
model_execution_feedback, gen_np_array = implementation.execute( model_execution_feedback, gen_np_array = implementation.execute(
batch_size=batch_size, batch_size=batch_size,
num_features=num_features, num_features=num_features,
@@ -59,7 +61,8 @@ class ModelCoSTEEREvaluator(CoSTEEREvaluator):
param_init_value=param_init_value, param_init_value=param_init_value,
) )
if gt_implementation is not None: if gt_implementation is not None:
assert isinstance(gt_implementation, ModelFBWorkspace) if not isinstance(gt_implementation, ModelFBWorkspace):
raise TypeError(f"Expected ModelFBWorkspace, got {type(gt_implementation)}")
_, gt_np_array = gt_implementation.execute( _, gt_np_array = gt_implementation.execute(
batch_size=batch_size, batch_size=batch_size,
num_features=num_features, num_features=num_features,
@@ -292,6 +292,7 @@ class OptunaOptimizer:
"volatility_lookback": trial.suggest_int("volatility_lookback", 5, 500, step=5), "volatility_lookback": trial.suggest_int("volatility_lookback", 5, 500, step=5),
"signal_bias": trial.suggest_float("signal_bias", -1.0, 1.0, step=0.05), "signal_bias": trial.suggest_float("signal_bias", -1.0, 1.0, step=0.05),
"max_hold_bars": trial.suggest_int("max_hold_bars", 5, 1000, step=5), "max_hold_bars": trial.suggest_int("max_hold_bars", 5, 1000, step=5),
"max_positions": trial.suggest_int("max_positions", 1, 5, step=1),
} }
# Parameters that are allowed to be negative (not clamped to 0). # Parameters that are allowed to be negative (not clamped to 0).
@@ -308,6 +309,7 @@ class OptunaOptimizer:
"volatility_lookback": 1.0, "volatility_lookback": 1.0,
"signal_bias": -1.0, "signal_bias": -1.0,
"max_hold_bars": 1.0, "max_hold_bars": 1.0,
"max_positions": 1.0,
} }
def _suggest_bounded( def _suggest_bounded(
@@ -357,6 +359,7 @@ class OptunaOptimizer:
"volatility_lookback": (center.get("volatility_lookback", 100), 30), "volatility_lookback": (center.get("volatility_lookback", 100), 30),
"signal_bias": (center.get("signal_bias", 0.0), 0.2), "signal_bias": (center.get("signal_bias", 0.0), 0.2),
"max_hold_bars": (center.get("max_hold_bars", 100), 50), "max_hold_bars": (center.get("max_hold_bars", 100), 50),
"max_positions": (center.get("max_positions", 1), 2),
} }
return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()} return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()}
@@ -388,6 +391,7 @@ class OptunaOptimizer:
"volatility_lookback": (center.get("volatility_lookback", 100), 10), "volatility_lookback": (center.get("volatility_lookback", 100), 10),
"signal_bias": (center.get("signal_bias", 0.0), 0.07), "signal_bias": (center.get("signal_bias", 0.0), 0.07),
"max_hold_bars": (center.get("max_hold_bars", 100), 17), "max_hold_bars": (center.get("max_hold_bars", 100), 17),
"max_positions": (center.get("max_positions", 1), 1),
} }
return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()} return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()}
@@ -467,6 +471,9 @@ class OptunaOptimizer:
# Max holding periods (in bars) # Max holding periods (in bars)
"max_hold_bars": trial.suggest_int("max_hold_bars", 10, 500, step=10), "max_hold_bars": trial.suggest_int("max_hold_bars", 10, 500, step=10),
# Max concurrent positions (1 = no pyramiding, 2-5 = scale-in)
"max_positions": trial.suggest_int("max_positions", 1, 5, step=1),
} }
return params return params
@@ -597,6 +604,13 @@ class OptunaOptimizer:
if signal_bias != 0.0: if signal_bias != 0.0:
signal = (signal.astype(float) + signal_bias).round().astype(int).clip(-1, 1) signal = (signal.astype(float) + signal_bias).round().astype(int).clip(-1, 1)
# Apply max_positions: scale signal by position_size_pct and cap exposure
max_positions = int(params.get("max_positions", 1))
position_size_pct = float(params.get("position_size_pct", 1.0))
# Each "position" is position_size_pct of equity; total exposure capped at max_positions × size
effective_size = min(position_size_pct * max_positions, 1.0)
signal = (signal.astype(float) * effective_size).clip(-1.0, 1.0)
# Build a synthetic close from the factor-mean so we can route # Build a synthetic close from the factor-mean so we can route
# through the same unified engine as every other backtest path. # through the same unified engine as every other backtest path.
# Backtest formulas must match the orchestrator's real-OHLCV path. # Backtest formulas must match the orchestrator's real-OHLCV path.
@@ -85,13 +85,16 @@ def load_and_process_one_pdf_by_azure_document_intelligence(
def load_and_process_pdfs_by_azure_document_intelligence(path: Path) -> dict[str, str]: def load_and_process_pdfs_by_azure_document_intelligence(path: Path) -> dict[str, str]:
assert RD_AGENT_SETTINGS.azure_document_intelligence_key is not None if RD_AGENT_SETTINGS.azure_document_intelligence_key is None:
assert RD_AGENT_SETTINGS.azure_document_intelligence_endpoint is not None raise AssertionError("azure_document_intelligence_key must be set")
if RD_AGENT_SETTINGS.azure_document_intelligence_endpoint is None:
raise AssertionError("azure_document_intelligence_endpoint must be set")
content_dict = {} content_dict = {}
ab_path = path.resolve() ab_path = path.resolve()
if ab_path.is_file(): if ab_path.is_file():
assert ".pdf" in ab_path.suffixes, "The file must be a PDF file." if ".pdf" not in ab_path.suffixes:
raise ValueError("The file must be a PDF file.")
proc = load_and_process_one_pdf_by_azure_document_intelligence proc = load_and_process_one_pdf_by_azure_document_intelligence
content_dict[str(ab_path)] = proc( content_dict[str(ab_path)] = proc(
ab_path, ab_path,
@@ -24,7 +24,8 @@ class UndirectedNode(Node):
super().__init__(content, label, embedding) super().__init__(content, label, embedding)
self.neighbors: set[UndirectedNode] = set() self.neighbors: set[UndirectedNode] = set()
self.appendix = appendix # appendix stores any additional information self.appendix = appendix # appendix stores any additional information
assert isinstance(content, str), "content must be a string" if not isinstance(content, str):
raise TypeError("content must be a string")
def add_neighbor(self, node: UndirectedNode) -> None: def add_neighbor(self, node: UndirectedNode) -> None:
self.neighbors.add(node) self.neighbors.add(node)
@@ -96,7 +97,8 @@ class Graph(KnowledgeBase):
APIBackend().create_embedding(input_content=contents[i : i + size]), APIBackend().create_embedding(input_content=contents[i : i + size]),
) )
assert len(nodes) == len(embeddings), "nodes' length must equals embeddings' length" if len(nodes) != len(embeddings):
raise ValueError("nodes' length must equal embeddings' length")
for node, embedding in zip(nodes, embeddings): for node, embedding in zip(nodes, embeddings):
node.embedding = embedding node.embedding = embedding
return nodes return nodes
@@ -252,7 +254,8 @@ class UndirectedGraph(Graph):
""" """
min_nodes_count = 2 min_nodes_count = 2
assert len(nodes) >= min_nodes_count, "nodes length must >=2" if len(nodes) < min_nodes_count:
raise ValueError("nodes length must >=2")
intersection = None intersection = None
for node in nodes: for node in nodes:
+2 -1
View File
@@ -87,7 +87,8 @@ class ModelWsLoader(WsLoader[ModelTask, ModelFBWorkspace]):
self.path = Path(path) self.path = Path(path)
def load(self, task: ModelTask) -> ModelFBWorkspace: def load(self, task: ModelTask) -> ModelFBWorkspace:
assert task.name is not None if task.name is None:
raise AssertionError("task.name should not be None")
mti = ModelFBWorkspace(task) mti = ModelFBWorkspace(task)
mti.prepare() mti.prepare()
with open(self.path / f"{task.name}.py", "r") as f: with open(self.path / f"{task.name}.py", "r") as f:
+11
View File
@@ -4,6 +4,7 @@ import functools
import importlib import importlib
import json import json
import multiprocessing as mp import multiprocessing as mp
import os
import pickle import pickle
import random import random
from collections.abc import Callable from collections.abc import Callable
@@ -208,3 +209,13 @@ def cache_with_pickle(hash_func: Callable, post_process_func: Callable | None =
return cache_wrapper return cache_wrapper
return cache_decorator return cache_decorator
def safe_resolve_path(user_path: Path, safe_root: Path | None = None) -> Path:
if safe_root is not None:
root_real = os.path.realpath(str(safe_root.expanduser()))
path_real = os.path.realpath(str(user_path.expanduser())) # nosec B614 — validated against safe_root below
if not (path_real == root_real or path_real.startswith(root_real + os.sep)):
raise ValueError(f"Path {user_path} resolves to {path_real}, outside allowed root {safe_root}")
return Path(path_real)
return user_path.expanduser().resolve()
+10 -3
View File
@@ -30,6 +30,7 @@ from rdagent.log.utils import (
extract_loopid_func_name, extract_loopid_func_name,
is_valid_session, is_valid_session,
) )
from rdagent.core.utils import safe_resolve_path
from rdagent.oai.backend.litellm import LITELLM_SETTINGS from rdagent.oai.backend.litellm import LITELLM_SETTINGS
from rdagent.oai.llm_utils import APIBackend from rdagent.oai.llm_utils import APIBackend
@@ -232,13 +233,19 @@ def workspace_win(workspace, cmp_workspace=None, cmp_name="last code."):
if target_folder.strip() == "": if target_folder.strip() == "":
st.warning("Please enter a valid folder path.") st.warning("Please enter a valid folder path.")
else: else:
target_folder_path = Path(target_folder) safe_root = Path(UI_SETTING.trace_folder).expanduser().resolve()
safe_root.mkdir(parents=True, exist_ok=True)
try:
target_folder_path = safe_resolve_path(Path(target_folder), safe_root)
except ValueError:
st.warning(f"Path must be within {safe_root}. Saving to default location.")
target_folder_path = safe_root
target_folder_path.mkdir(parents=True, exist_ok=True) target_folder_path.mkdir(parents=True, exist_ok=True)
for filename, content in workspace.file_dict.items(): for filename, content in workspace.file_dict.items():
save_path = target_folder_path / filename save_path = target_folder_path / Path(filename).name
save_path.parent.mkdir(parents=True, exist_ok=True) save_path.parent.mkdir(parents=True, exist_ok=True)
save_path.write_text(content, encoding="utf-8") save_path.write_text(content, encoding="utf-8")
st.success(f"All files saved to: {target_folder}") st.success(f"All files saved to: {target_folder_path}")
else: else:
st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]") st.markdown(f"No files in :blue[{replace_ep_path(workspace.workspace_path)}]")
+2 -1
View File
@@ -541,7 +541,8 @@ class APIBackend(ABC):
**kwargs, **kwargs,
) -> str | list[list[float]]: ) -> str | list[list[float]]:
"""This function to share operation between embedding and chat completion""" """This function to share operation between embedding and chat completion"""
assert not (chat_completion and embedding), "chat_completion and embedding cannot be True at the same time" if chat_completion and embedding:
raise ValueError("chat_completion and embedding cannot be True at the same time")
max_retry = LLM_SETTINGS.max_retry if LLM_SETTINGS.max_retry is not None else max_retry max_retry = LLM_SETTINGS.max_retry if LLM_SETTINGS.max_retry is not None else max_retry
timeout_count = 0 timeout_count = 0
violation_count = 0 violation_count = 0
+6 -4
View File
@@ -36,16 +36,18 @@ def get_agent_model() -> OpenAIChatModel:
""" """
backend = APIBackend() backend = APIBackend()
assert isinstance(backend, LiteLLMAPIBackend), "Only LiteLLMAPIBackend is supported" if not isinstance(backend, LiteLLMAPIBackend):
raise TypeError("Only LiteLLMAPIBackend is supported")
compl_kwargs = backend.get_complete_kwargs() compl_kwargs = backend.get_complete_kwargs()
selected_model = compl_kwargs["model"] selected_model = compl_kwargs["model"]
_, custom_llm_provider, _, _ = get_llm_provider(selected_model) _, custom_llm_provider, _, _ = get_llm_provider(selected_model)
assert ( if custom_llm_provider not in PROVIDER_TO_ENV_MAP:
custom_llm_provider in PROVIDER_TO_ENV_MAP raise ValueError(
), f"Provider {custom_llm_provider} not supported. Please add it into `PROVIDER_TO_ENV_MAP`" f"Provider {custom_llm_provider} not supported. Please add it into `PROVIDER_TO_ENV_MAP`"
)
prefix = PROVIDER_TO_ENV_MAP[custom_llm_provider] prefix = PROVIDER_TO_ENV_MAP[custom_llm_provider]
api_key = os.getenv(f"{prefix}_API_KEY", None) api_key = os.getenv(f"{prefix}_API_KEY", None)
api_base = os.getenv(f"{prefix}_API_BASE", None) api_base = os.getenv(f"{prefix}_API_BASE", None)
+2 -1
View File
@@ -268,7 +268,8 @@ class JsonReducer(DataReducer):
parent[key] = sampled # type: ignore # parent 是 listkey 是 index, list.__setitem__(key, sampled) parent[key] = sampled # type: ignore # parent 是 listkey 是 index, list.__setitem__(key, sampled)
self.sampled_files.extend([self.extract_filename(i) for i in sampled]) self.sampled_files.extend([self.extract_filename(i) for i in sampled])
break break
assert len(self.sampled_files) > 0 if len(self.sampled_files) <= 0:
raise AssertionError("sampled_files must contain at least one file")
return data return data
def _find_all_lists( def _find_all_lists(
@@ -7,8 +7,10 @@ from sklearn.metrics import roc_auc_score
def prepare_for_auroc_metric(submission: pd.DataFrame, answers: pd.DataFrame, id_col: str, target_col: str) -> dict: def prepare_for_auroc_metric(submission: pd.DataFrame, answers: pd.DataFrame, id_col: str, target_col: str) -> dict:
# Answers checks # Answers checks
assert id_col in answers.columns, f"answers dataframe should have an {id_col} column" if id_col not in answers.columns:
assert target_col in answers.columns, f"answers dataframe should have a {target_col} column" raise InvalidSubmissionError(f"answers dataframe should have an {id_col} column")
if target_col not in answers.columns:
raise InvalidSubmissionError(f"answers dataframe should have a {target_col} column")
# Submission checks # Submission checks
if id_col not in submission.columns: if id_col not in submission.columns:
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
# Check if our submission file exists # Check if our submission file exists
assert Path("submission.csv").exists(), "Error: submission.csv not found" if not Path("submission.csv").exists():
raise FileNotFoundError("Error: submission.csv not found")
submission_lines = Path("submission.csv").read_text().splitlines() submission_lines = Path("submission.csv").read_text().splitlines()
test_lines = Path("submission_test.csv").read_text().splitlines() test_lines = Path("submission_test.csv").read_text().splitlines()
@@ -22,7 +22,8 @@ def prepare_for_metric(submission: pd.DataFrame, answers: pd.DataFrame) -> dict:
if "price" not in submission.columns: if "price" not in submission.columns:
raise InvalidSubmissionError("Submission DataFrame must contain 'price' columns.") raise InvalidSubmissionError("Submission DataFrame must contain 'price' columns.")
assert "price" in answers.columns, "Answers DataFrame must contain 'price' columns." if "price" not in answers.columns:
raise InvalidSubmissionError("Answers DataFrame must contain 'price' columns.")
if len(submission) != len(answers): if len(submission) != len(answers):
raise InvalidSubmissionError("Submission must be the same length as the answers.") raise InvalidSubmissionError("Submission must be the same length as the answers.")
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
# Check if our submission file exists # Check if our submission file exists
assert Path("submission.csv").exists(), "Error: submission.csv not found" if not Path("submission.csv").exists():
raise FileNotFoundError("Error: submission.csv not found")
submission_lines = Path("submission.csv").read_text().splitlines() # 自动生成的 submission_lines = Path("submission.csv").read_text().splitlines() # 自动生成的
test_lines = Path("submission_test.csv").read_text().splitlines() # test.csv test_lines = Path("submission_test.csv").read_text().splitlines() # test.csv
@@ -56,14 +56,17 @@ sparse.save_npz(public / "test" / "X.npz", X_test)
sparse.save_npz(public / "train" / "X.npz", X_train) sparse.save_npz(public / "train" / "X.npz", X_train)
df_train.to_csv(public / "train" / "ARF_12h.csv", index=False) df_train.to_csv(public / "train" / "ARF_12h.csv", index=False)
assert ( if X_train.shape[0] != df_train.shape[0]:
X_train.shape[0] == df_train.shape[0] raise ValueError(
), f"Mismatch: X_train rows ({X_train.shape[0]}) != df_train rows ({df_train.shape[0]})" f"Mismatch: X_train rows ({X_train.shape[0]}) != df_train rows ({df_train.shape[0]})"
assert ( )
X_test.shape[0] == df_test.shape[0] if X_test.shape[0] != df_test.shape[0]:
), f"Mismatch: X_test rows ({X_test.shape[0]}) != df_test rows ({df_test.shape[0]})" raise ValueError(
assert df_test.shape[1] == 2, "Public test set should have 2 columns" f"Mismatch: X_test rows ({X_test.shape[0]}) != df_test rows ({df_test.shape[0]})"
assert df_train.shape[1] == 3, "Public train set should have 3 columns" )
assert len(df_train) + len(df_test) == len( if df_test.shape[1] != 2:
df_label raise ValueError("Public test set should have 2 columns")
), "Length of new_train and new_test should equal length of old_train" if df_train.shape[1] != 3:
raise ValueError("Public train set should have 3 columns")
if len(df_train) + len(df_test) != len(df_label):
raise ValueError("Length of new_train and new_test should equal length of old_train")
@@ -25,11 +25,12 @@ def prepare(raw: Path, public: Path, private: Path):
new_test.to_csv(public / "test.csv", index=False) new_test.to_csv(public / "test.csv", index=False)
# Checks # Checks
assert new_test.shape[1] == 12, "Public test set should have 12 columns" if new_test.shape[1] != 12:
assert new_train.shape[1] == 13, "Public train set should have 13 columns" raise AssertionError("Public test set should have 12 columns")
assert len(new_train) + len(new_test) == len( if new_train.shape[1] != 13:
old_train raise AssertionError("Public train set should have 13 columns")
), "Length of new_train and new_test should equal length of old_train" if len(new_train) + len(new_test) != len(old_train):
raise AssertionError("Length of new_train and new_test should equal length of old_train")
if __name__ == "__main__": if __name__ == "__main__":
+2 -1
View File
@@ -320,7 +320,8 @@ class DataScienceRDLoop(RDLoop):
# only clean current workspace without affecting other loops. # only clean current workspace without affecting other loops.
for k in "direct_exp_gen", "coding", "running": for k in "direct_exp_gen", "coding", "running":
if k in prev_out and prev_out[k] is not None: if k in prev_out and prev_out[k] is not None:
assert isinstance(prev_out[k], DSExperiment) if not isinstance(prev_out[k], DSExperiment):
raise TypeError(f"prev_out[{k!r}] must be an instance of DSExperiment")
clean_workspace(prev_out[k].experiment_workspace.workspace_path) clean_workspace(prev_out[k].experiment_workspace.workspace_path)
# Backup the workspace (only necessary files are included) # Backup the workspace (only necessary files are included)
@@ -213,7 +213,8 @@ class DSTrace(Trace[DataScienceScen, KnowledgeBase]):
self, component: COMPONENT, search_list: list[tuple[DSExperiment, ExperimentFeedback]] = [] self, component: COMPONENT, search_list: list[tuple[DSExperiment, ExperimentFeedback]] = []
) -> bool: ) -> bool:
for exp, fb in search_list: for exp, fb in search_list:
assert isinstance(exp.hypothesis, DSHypothesis), "Hypothesis should be DSHypothesis (and not None)" if not isinstance(exp.hypothesis, DSHypothesis):
raise TypeError("Hypothesis should be DSHypothesis (and not None)")
if exp.hypothesis.component == component and fb: if exp.hypothesis.component == component and fb:
return True return True
return False return False
@@ -377,7 +377,8 @@ class ExpGen2TraceAndMergeV2(ExpGen):
if DS_RD_SETTING.enable_multi_version_exp_gen: if DS_RD_SETTING.enable_multi_version_exp_gen:
exp_gen_version_list = DS_RD_SETTING.exp_gen_version_list.split(",") exp_gen_version_list = DS_RD_SETTING.exp_gen_version_list.split(",")
for version in exp_gen_version_list: for version in exp_gen_version_list:
assert version in ["v3", "v2", "v1"] if version not in ["v3", "v2", "v1"]:
raise ValueError(f"version must be 'v1', 'v2', or 'v3', got {version!r}")
if len(trace.hist) == 0: if len(trace.hist) == 0:
# set the proposal version for the first sub-trace # set the proposal version for the first sub-trace
@@ -339,7 +339,8 @@ class DSProposalV1ExpGen(ExpGen):
eda_output = sota_exp.experiment_workspace.file_dict.get("EDA.md", None) eda_output = sota_exp.experiment_workspace.file_dict.get("EDA.md", None)
scenario_desc = trace.scen.get_scenario_all_desc(eda_output=eda_output) scenario_desc = trace.scen.get_scenario_all_desc(eda_output=eda_output)
assert sota_exp is not None, "SOTA experiment is not provided." if sota_exp is None:
raise ValueError("SOTA experiment is not provided.")
last_exp = trace.last_exp() last_exp = trace.last_exp()
# exp_and_feedback = trace.hist[-1] # exp_and_feedback = trace.hist[-1]
# last_exp = exp_and_feedback[0] # last_exp = exp_and_feedback[0]
@@ -445,8 +446,10 @@ class DSProposalV1ExpGen(ExpGen):
json_target_type=dict[str, dict[str, str | dict] | str], json_target_type=dict[str, dict[str, str | dict] | str],
) )
) )
assert "hypothesis_proposal" in resp_dict, "Hypothesis proposal not provided." if "hypothesis_proposal" not in resp_dict:
assert "task_design" in resp_dict, "Task design not provided." raise ValueError("Hypothesis proposal not provided.")
if "task_design" not in resp_dict:
raise ValueError("Task design not provided.")
task_class = component_info["task_class"] task_class = component_info["task_class"]
hypothesis_proposal = resp_dict.get("hypothesis_proposal", {}) hypothesis_proposal = resp_dict.get("hypothesis_proposal", {})
hypothesis = DSHypothesis( hypothesis = DSHypothesis(
@@ -1149,8 +1152,10 @@ You help users retrieve relevant knowledge from community discussions and public
) )
response_dict = json.loads(response) response_dict = json.loads(response)
assert response_dict.get("component") in HypothesisComponent.__members__, f"Invalid component" if response_dict.get("component") not in HypothesisComponent.__members__:
assert response_dict.get("hypothesis") is not None, f"Invalid hypothesis" raise ValueError(f"Invalid component: {response_dict.get('component')}")
if response_dict.get("hypothesis") is None:
raise ValueError("Invalid hypothesis")
return response_dict return response_dict
# END: for support llm-based hypothesis selection ----- # END: for support llm-based hypothesis selection -----
@@ -1253,7 +1258,8 @@ You help users retrieve relevant knowledge from community discussions and public
description=task_desc, description=task_desc,
) )
assert isinstance(task, PipelineTask), f"Task {task_name} is not a PipelineTask, got {type(task)}" if not isinstance(task, PipelineTask):
raise TypeError(f"Task {task_name} is not a PipelineTask, got {type(task)}")
# only for llm with response schema.(TODO: support for non-schema llm?) # only for llm with response schema.(TODO: support for non-schema llm?)
# If the LLM provides a "packages" field (list[str]), compute runtime environment now and cache it for subsequent prompts in later loops. # If the LLM provides a "packages" field (list[str]), compute runtime environment now and cache it for subsequent prompts in later loops.
if isinstance(task_dict, dict) and "packages" in task_dict and isinstance(task_dict["packages"], list): if isinstance(task_dict, dict) and "packages" in task_dict and isinstance(task_dict["packages"], list):
@@ -293,7 +293,7 @@ class ValidationSelector(SOTAexpSelector):
Sorts all valid experiments by score and returns the top N. Sorts all valid experiments by score and returns the top N.
""" """
mock_folder = f"/tmp/mock/{self.competition}" mock_folder = f"/tmp/mock/{self.competition}" # nosec B108 — Docker volume mount point derived from internal competition name
try: try:
data_py_code, grade_py_code = self._prepare_validation_scripts( data_py_code, grade_py_code = self._prepare_validation_scripts(
@@ -540,7 +540,7 @@ def process_experiment(
# Run main script # Run main script
env = get_ds_env( env = get_ds_env(
extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder}, extra_volumes={f"/tmp/mock/{competition}/{input_folder}": input_folder}, # nosec B108 — Docker volume mount point derived from internal competition name
running_timeout_period=DS_RD_SETTING.full_timeout, running_timeout_period=DS_RD_SETTING.full_timeout,
) )
result = ws.run(env=env, entry="python main.py") result = ws.run(env=env, entry="python main.py")
+2 -1
View File
@@ -35,7 +35,8 @@ def select(X: pd.DataFrame) -> pd.DataFrame:
class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]): class KGModelFeatureSelectionCoder(Developer[KGModelExperiment]):
def develop(self, exp: KGModelExperiment) -> KGModelExperiment: def develop(self, exp: KGModelExperiment) -> KGModelExperiment:
target_model_type = exp.sub_tasks[0].model_type target_model_type = exp.sub_tasks[0].model_type
assert target_model_type in KG_SELECT_MAPPING if target_model_type not in KG_SELECT_MAPPING:
raise ValueError(f"target_model_type {target_model_type} not in KG_SELECT_MAPPING")
if len(exp.experiment_workspace.data_description) == 1: if len(exp.experiment_workspace.data_description) == 1:
code = ( code = (
Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code Environment(undefined=StrictUndefined) # nosec B701 — renders Python code templates, not HTML; autoescape would corrupt code
@@ -165,7 +165,8 @@ class KGScenario(Scenario):
return data_info return data_info
def output_format(self, tag=None) -> str: def output_format(self, tag=None) -> str:
assert tag in [None, "feature", "model"] if tag not in [None, "feature", "model"]:
raise ValueError(f"tag must be None, 'feature', or 'model', got {tag!r}")
feature_output_format = f"""The feature code should output following the format: feature_output_format = f"""The feature code should output following the format:
{T(".prompts:kg_feature_output_format").r()}""" {T(".prompts:kg_feature_output_format").r()}"""
model_output_format = f"""The model code should output following the format:\n""" + T( model_output_format = f"""The model code should output following the format:\n""" + T(
@@ -180,7 +181,8 @@ class KGScenario(Scenario):
return model_output_format return model_output_format
def interface(self, tag=None) -> str: def interface(self, tag=None) -> str:
assert tag in [None, "feature", "XGBoost", "RandomForest", "LightGBM", "NN"] if tag not in [None, "feature", "XGBoost", "RandomForest", "LightGBM", "NN"]:
raise ValueError(f"tag must be None, 'feature', 'XGBoost', 'RandomForest', 'LightGBM', or 'NN', got {tag!r}")
feature_interface = f"""The feature code should follow the interface: feature_interface = f"""The feature code should follow the interface:
{T(".prompts:kg_feature_interface").r()}""" {T(".prompts:kg_feature_interface").r()}"""
if tag == "feature": if tag == "feature":
@@ -195,7 +197,8 @@ class KGScenario(Scenario):
return model_interface return model_interface
def simulator(self, tag=None) -> str: def simulator(self, tag=None) -> str:
assert tag in [None, "feature", "model"] if tag not in [None, "feature", "model"]:
raise ValueError(f"tag must be None, 'feature', or 'model', got {tag!r}")
kg_feature_simulator = ( kg_feature_simulator = (
"The feature code will be sent to the simulator:\n" + T(".prompts:kg_feature_simulator").r() "The feature code will be sent to the simulator:\n" + T(".prompts:kg_feature_simulator").r()
+6 -1
View File
@@ -87,7 +87,12 @@ def crawl_descriptions(
content = e.get_attribute("innerHTML") content = e.get_attribute("innerHTML")
contents.append(content) contents.append(content)
assert len(subtitles) == len(contents) + 1 and subtitles[-1] == "Citation" if not (len(subtitles) == len(contents) + 1 and subtitles[-1] == "Citation"):
raise AssertionError(
f"Expected len(contents)+1 == len(subtitles) and last subtitle == 'Citation', "
f"got len(subtitles)={len(subtitles)}, len(contents)={len(contents)}, "
f"last subtitle={subtitles[-1]!r}"
)
for i in range(len(subtitles) - 1): for i in range(len(subtitles) - 1):
descriptions[subtitles[i]] = contents[i] descriptions[subtitles[i]] = contents[i]
@@ -307,7 +307,8 @@ class KGHypothesisGen(FactorAndModelHypothesisGen):
class KGHypothesis2Experiment(FactorAndModelHypothesis2Experiment): class KGHypothesis2Experiment(FactorAndModelHypothesis2Experiment):
def prepare_context(self, hypothesis: Hypothesis, trace: Trace) -> Tuple[dict, bool]: def prepare_context(self, hypothesis: Hypothesis, trace: Trace) -> Tuple[dict, bool]:
scenario = trace.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment") scenario = trace.scen.get_scenario_all_desc(filtered_tag="hypothesis_and_experiment")
assert isinstance(hypothesis, KGHypothesis) if not isinstance(hypothesis, KGHypothesis):
raise TypeError("hypothesis must be an instance of KGHypothesis")
experiment_output_format = ( experiment_output_format = (
T("scenarios.kaggle.prompts:feature_experiment_output_format").r() T("scenarios.kaggle.prompts:feature_experiment_output_format").r()
if hypothesis.action in [KG_ACTION_FEATURE_ENGINEERING, KG_ACTION_FEATURE_PROCESSING] if hypothesis.action in [KG_ACTION_FEATURE_ENGINEERING, KG_ACTION_FEATURE_PROCESSING]
@@ -1002,7 +1002,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
series.to_frame().to_parquet(str(parquet_path)) series.to_frame().to_parquet(str(parquet_path))
except Exception: except Exception:
logging.debug("Error in save_factor_values_to_parquet", exc_info=True) logging.debug("Error in save_factor_values_to_parquet", exc_info=True)
def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None: def _log_result_warnings(self, factor_name: str, result, metrics: dict) -> None:
""" """
@@ -241,6 +241,7 @@ class StrategyBuilder:
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
factors.append(data) factors.append(data)
except Exception: except Exception:
logger.warning("Failed to load factor file %s", f, exc_info=True)
continue continue
# Sort by absolute IC # Sort by absolute IC
+2 -1
View File
@@ -30,7 +30,8 @@ def _build_execute_calls(exp: QlibFactorExperiment, base_feature_workspaces: lis
execute_calls = [] execute_calls = []
if exp.sub_tasks: if exp.sub_tasks:
assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) if not isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback):
raise TypeError("exp.prop_dev_feedback must be of type CoSTEERMultiFeedback")
execute_calls.extend( execute_calls.extend(
(implementation.execute, ("All",)) (implementation.execute, ("All",))
for implementation, feedback in zip(exp.sub_workspace_list, exp.prop_dev_feedback) for implementation, feedback in zip(exp.sub_workspace_list, exp.prop_dev_feedback)
@@ -56,7 +56,8 @@ class QlibQuantScenario(Scenario):
) )
def background(self, tag=None) -> str: def background(self, tag=None) -> str:
assert tag in [None, "factor", "model"] if tag not in [None, "factor", "model"]:
raise ValueError(f"tag must be None, 'factor', or 'model', got {tag!r}")
quant_background = "The background of the scenario is as follows:\n" + T(".prompts:qlib_quant_background").r( quant_background = "The background of the scenario is as follows:\n" + T(".prompts:qlib_quant_background").r(
runtime_environment=self.get_runtime_environment(), runtime_environment=self.get_runtime_environment(),
) )
@@ -83,7 +84,8 @@ class QlibQuantScenario(Scenario):
return self._source_data return self._source_data
def output_format(self, tag=None) -> str: def output_format(self, tag=None) -> str:
assert tag in [None, "factor", "model"] if tag not in [None, "factor", "model"]:
raise ValueError(f"tag must be None, 'factor', or 'model', got {tag!r}")
factor_output_format = ( factor_output_format = (
"The factor code should output the following format:\n" + T(".prompts:qlib_factor_output_format").r() "The factor code should output the following format:\n" + T(".prompts:qlib_factor_output_format").r()
) )
@@ -99,7 +101,8 @@ class QlibQuantScenario(Scenario):
return model_output_format return model_output_format
def interface(self, tag=None) -> str: def interface(self, tag=None) -> str:
assert tag in [None, "factor", "model"] if tag not in [None, "factor", "model"]:
raise ValueError(f"tag must be None, 'factor', or 'model', got {tag!r}")
factor_interface = ( factor_interface = (
"The factor code should be written in the following interface:\n" + T(".prompts:qlib_factor_interface").r() "The factor code should be written in the following interface:\n" + T(".prompts:qlib_factor_interface").r()
) )
@@ -115,7 +118,8 @@ class QlibQuantScenario(Scenario):
return model_interface return model_interface
def simulator(self, tag=None) -> str: def simulator(self, tag=None) -> str:
assert tag in [None, "factor", "model"] if tag not in [None, "factor", "model"]:
raise ValueError(f"tag must be None, 'factor', or 'model', got {tag!r}")
factor_simulator = "The factor code will be sent to the simulator:\n" + T(".prompts:qlib_factor_simulator").r() factor_simulator = "The factor code will be sent to the simulator:\n" + T(".prompts:qlib_factor_simulator").r()
model_simulator = "The model code will be sent to the simulator:\n" + T(".prompts:qlib_model_simulator").r() model_simulator = "The model code will be sent to the simulator:\n" + T(".prompts:qlib_model_simulator").r()
@@ -185,7 +189,8 @@ class QlibQuantScenario(Scenario):
return common_description(action) + interface(action) + output(action) + simulator(action) return common_description(action) + interface(action) + output(action) + simulator(action)
def get_runtime_environment(self, tag: str = None) -> str: def get_runtime_environment(self, tag: str = None) -> str:
assert tag in [None, "factor", "model"] if tag not in [None, "factor", "model"]:
raise ValueError(f"tag must be None, 'factor', or 'model', got {tag!r}")
if tag is None or tag == "factor": if tag is None or tag == "factor":
# Use factor env to get the runtime environment # Use factor env to get the runtime environment
+12 -10
View File
@@ -4,7 +4,7 @@ import shutil
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined, select_autoescape
from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS
from rdagent.utils.env import QTDockerEnv from rdagent.utils.env import QTDockerEnv
@@ -21,14 +21,16 @@ def generate_data_folder_from_qlib():
entry=f"python generate.py", entry=f"python generate.py",
) )
assert (Path(__file__).parent / "factor_data_template" / "intraday_pv_all.h5").exists(), ( if not (Path(__file__).parent / "factor_data_template" / "intraday_pv_all.h5").exists():
"intraday_pv_all.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n" raise FileNotFoundError(
+ execute_log "intraday_pv_all.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n"
) + execute_log
assert (Path(__file__).parent / "factor_data_template" / "intraday_pv_debug.h5").exists(), ( )
"intraday_pv_debug.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n" if not (Path(__file__).parent / "factor_data_template" / "intraday_pv_debug.h5").exists():
+ execute_log raise FileNotFoundError(
) "intraday_pv_debug.h5 is not generated. It means rdagent/scenarios/qlib/experiment/factor_data_template/generate.py is not executed correctly. Please check the log: \n"
+ execute_log
)
Path(FACTOR_COSTEER_SETTINGS.data_folder).mkdir(parents=True, exist_ok=True) Path(FACTOR_COSTEER_SETTINGS.data_folder).mkdir(parents=True, exist_ok=True)
shutil.copy( shutil.copy(
@@ -67,7 +69,7 @@ def get_file_desc(p: Path, variable_list=[]) -> str:
""" """
p = Path(p) p = Path(p)
JJ_TPL = Environment(undefined=StrictUndefined).from_string(""" # nosec B701 — renders plain text description, not HTML; autoescape not applicable JJ_TPL = Environment(undefined=StrictUndefined, autoescape=select_autoescape()).from_string("""
# {{file_name}} # {{file_name}}
## File Type ## File Type
@@ -176,7 +176,7 @@ class QlibQuantHypothesisGen(FactorAndModelHypothesisGen):
ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else "" ic_val = exp.result.loc["IC"] if "IC" in exp.result.index else ""
ic_str = f" IC={ic_val:.4f}" if ic_val != "" else "" ic_str = f" IC={ic_val:.4f}" if ic_val != "" else ""
except Exception: except Exception:
logging.debug("Error getting IC", exc_info=True) logging.debug("Error getting IC", exc_info=True)
decision_str = "PASS" if fb.decision else "FAIL" decision_str = "PASS" if fb.decision else "FAIL"
obs_short = (fb.observations or "")[:120].replace("\n", " ") obs_short = (fb.observations or "")[:120].replace("\n", " ")
summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}") summary_lines.append(f"- [{decision_str}]{ic_str} {names_str}: {obs_short}")
@@ -77,6 +77,7 @@ def count_valid_factors() -> int:
if data.get("status") == "success" and data.get("ic") is not None: if data.get("status") == "success" and data.get("ic") is not None:
count += 1 count += 1
except Exception: except Exception:
logger.warning("Failed to load factor file %s", json_file, exc_info=True)
continue continue
return count return count
@@ -71,7 +71,7 @@ def submit_for_grading(grading_url: str, model_path: str) -> dict | None:
def main(): def main():
MODEL_PATH = os.environ.get("MODEL_PATH") MODEL_PATH = os.environ.get("MODEL_PATH")
DATA_PATH = os.environ.get("DATA_PATH") DATA_PATH = os.environ.get("DATA_PATH")
OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output") OUTPUT_DIR = os.environ.get("OUTPUT_DIR", "/tmp/autorl_output") # nosec B108 — Docker container output dir, configurable via env var
GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "") GRADING_SERVER_URL = os.environ.get("GRADING_SERVER_URL", "")
TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05")) TRAIN_RATIO = float(os.environ.get("TRAIN_RATIO", "0.05"))
NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3")) NUM_EPOCHS = int(os.environ.get("NUM_EPOCHS", "3"))
+4 -3
View File
@@ -9,7 +9,7 @@ from pathlib import Path
from typing import Any from typing import Any
import yaml import yaml
from jinja2 import Environment, FunctionLoader, StrictUndefined from jinja2 import Environment, FunctionLoader, StrictUndefined, select_autoescape
from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.conf import RD_AGENT_SETTINGS
from rdagent.log import rdagent_logger as logger from rdagent.log import rdagent_logger as logger
@@ -38,7 +38,8 @@ def load_content(uri: str, caller_dir: Path | None = None, ftype: str = "yaml")
caller_dir = get_caller_dir(upshift=1) caller_dir = get_caller_dir(upshift=1)
# Parse the URI # Parse the URI
path_part, *yaml_trace = uri.split(":") path_part, *yaml_trace = uri.split(":")
assert len(yaml_trace) <= 1, f"Invalid uri {uri}, only one yaml trace is allowed." if len(yaml_trace) > 1:
raise ValueError(f"Invalid uri {uri}, only one yaml trace is allowed.")
yaml_trace = [key for yt in yaml_trace for key in yt.split(".")] yaml_trace = [key for yt in yaml_trace for key in yt.split(".")]
# load file_path with priorities. # load file_path with priorities.
@@ -126,7 +127,7 @@ class RDAT:
# loader=FunctionLoader(load_conent) is for supporting grammar like below. # loader=FunctionLoader(load_conent) is for supporting grammar like below.
# `{% include "scenarios.data_science.share:component_spec.DataLoadSpec" %}` # `{% include "scenarios.data_science.share:component_spec.DataLoadSpec" %}`
rendered = ( rendered = (
Environment(undefined=StrictUndefined, loader=FunctionLoader(load_content)) Environment(undefined=StrictUndefined, loader=FunctionLoader(load_content), autoescape=select_autoescape())
.from_string(self.template) .from_string(self.template)
.render(**context) .render(**context)
.strip("\n") .strip("\n")
+22 -17
View File
@@ -614,13 +614,14 @@ class LocalEnv(Env[ASpecificLocalConf]):
if self.conf.extra_volumes is not None: if self.conf.extra_volumes is not None:
for lp, rp in self.conf.extra_volumes.items(): for lp, rp in self.conf.extra_volumes.items():
volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp volumes[lp] = rp["bind"] if isinstance(rp, dict) else rp
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
Path(cache_path).mkdir(parents=True, exist_ok=True) Path(cache_path).mkdir(parents=True, exist_ok=True)
volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r() volumes[cache_path] = T("scenarios.data_science.share:scen.cache_path").r()
for lp, rp in running_extra_volume.items(): for lp, rp in running_extra_volume.items():
volumes[lp] = rp volumes[lp] = rp
assert local_path is not None, "local_path should not be None" if local_path is None:
raise ValueError("local_path should not be None")
volumes = normalize_volumes(volumes, local_path) volumes = normalize_volumes(volumes, local_path)
@contextlib.contextmanager @contextlib.contextmanager
@@ -678,7 +679,7 @@ class LocalEnv(Env[ASpecificLocalConf]):
cwd = Path(local_path).resolve() if local_path else None cwd = Path(local_path).resolve() if local_path else None
env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()} env = {k: str(v) if isinstance(v, int) else v for k, v in env.items()}
process = subprocess.Popen( process = subprocess.Popen( # nosec B602 — entry is an internal command string set by LocalEnvConf, not user input
entry, entry,
cwd=cwd, cwd=cwd,
env={**os.environ, **env}, env={**os.environ, **env},
@@ -761,12 +762,15 @@ class CondaConf(LocalConf):
to ensure bin_path is set correctly even if the conda env was just created. to ensure bin_path is set correctly even if the conda env was just created.
""" """
conda_path_result = subprocess.run( conda_path_result = subprocess.run(
f"conda run -n {self.conda_env_name} --no-capture-output env | grep '^PATH='", ["conda", "run", "-n", self.conda_env_name, "--no-capture-output", "env"],
capture_output=True, capture_output=True,
text=True, text=True,
shell=True,
) )
self.bin_path = conda_path_result.stdout.strip().split("=")[1] if conda_path_result.returncode == 0 else "" if conda_path_result.returncode == 0:
path_lines = [l for l in conda_path_result.stdout.splitlines() if l.startswith("PATH=")]
self.bin_path = path_lines[0].split("=", 1)[1] if path_lines else ""
else:
self.bin_path = ""
class MLECondaConf(CondaConf): class MLECondaConf(CondaConf):
@@ -886,10 +890,9 @@ def _sync_conda_cache_with_real_envs() -> None:
"""Ensure the prepared cache includes environments that already exist on disk.""" """Ensure the prepared cache includes environments that already exist on disk."""
try: try:
result = subprocess.run( result = subprocess.run(
"conda env list", ["conda", "env", "list"],
capture_output=True, capture_output=True,
text=True, text=True,
shell=True,
check=False, check=False,
) )
except Exception as exc: # pragma: no cover - best-effort helper except Exception as exc: # pragma: no cover - best-effort helper
@@ -922,14 +925,15 @@ def _prepare_conda_env(env_name: str, requirements_file: Path, python_version: s
python_version: Python version for the environment python_version: Python version for the environment
""" """
# 1. Create conda environment if not exists # 1. Create conda environment if not exists
result = subprocess.run(f"conda env list | grep -q '^{env_name} '", shell=True) env_list = subprocess.run(["conda", "env", "list"], capture_output=True, text=True, check=False)
if result.returncode != 0: env_exists = any(line.split()[0] == env_name for line in env_list.stdout.splitlines() if line and not line.startswith("#"))
if not env_exists:
print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]") print(f"[yellow]Creating conda env '{env_name}' (Python {python_version})...[/yellow]")
subprocess.check_call(f"conda create -y -n {env_name} python={python_version}", shell=True) subprocess.check_call(["conda", "create", "-y", "-n", env_name, f"python={python_version}"])
subprocess.check_call(f"conda run -n {env_name} pip install --upgrade pip", shell=True) subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "--upgrade", "pip"])
print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]") print(f"[yellow]Installing dependencies from {requirements_file.name}...[/yellow]")
subprocess.check_call(f"conda run -n {env_name} pip install -r {requirements_file}", shell=True) subprocess.check_call(["conda", "run", "-n", env_name, "pip", "install", "-r", str(requirements_file)])
print(f"[green]Conda env '{env_name}' ready[/green]") print(f"[green]Conda env '{env_name}' ready[/green]")
_CONDA_ENV_PREPARED.add(env_name) _CONDA_ENV_PREPARED.add(env_name)
@@ -969,8 +973,8 @@ class FTCondaEnv(LocalEnv[FTCondaConf]):
# Note: flash-attn>=2.8 is required for B200 (sm_100) support # Note: flash-attn>=2.8 is required for B200 (sm_100) support
print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]") print("[yellow]Installing flash-attn (compiling, may take a few minutes)...[/yellow]")
subprocess.check_call( subprocess.check_call(
f"conda run -n {self.conf.conda_env_name} pip install 'flash-attn>=2.8' --no-build-isolation --no-cache-dir", ["conda", "run", "-n", self.conf.conda_env_name, "pip", "install",
shell=True, "flash-attn>=2.8", "--no-build-isolation", "--no-cache-dir"],
) )
# Re-update bin_path after prepare() in case the conda env was just created # Re-update bin_path after prepare() in case the conda env was just created
@@ -1440,7 +1444,7 @@ class DockerEnv(Env[DockerConf]):
if self.conf.extra_volumes is not None: if self.conf.extra_volumes is not None:
for lp, rp in self.conf.extra_volumes.items(): for lp, rp in self.conf.extra_volumes.items():
volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode} volumes[lp] = rp if isinstance(rp, dict) else {"bind": rp, "mode": self.conf.extra_volume_mode}
cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" cache_path = "/tmp/sample" if "/sample/" in "".join(self.conf.extra_volumes.keys()) else "/tmp/full" # nosec B108 — fixed Docker volume mount point, not a user-writable temp file
Path(cache_path).mkdir(parents=True, exist_ok=True) Path(cache_path).mkdir(parents=True, exist_ok=True)
volumes[cache_path] = { volumes[cache_path] = {
"bind": T("scenarios.data_science.share:scen.cache_path").r(), "bind": T("scenarios.data_science.share:scen.cache_path").r(),
@@ -1469,7 +1473,8 @@ class DockerEnv(Env[DockerConf]):
cpu_count=self.conf.cpu_count, # Set CPU limit cpu_count=self.conf.cpu_count, # Set CPU limit
**self._gpu_kwargs(client), **self._gpu_kwargs(client),
) )
assert container is not None # Ensure container was created successfully if container is None:
raise AssertionError("Docker container was not created successfully")
logs = container.logs(stream=True) logs = container.logs(stream=True)
print(Rule("[bold green]Docker Logs Begin[/bold green]", style="dark_orange")) print(Rule("[bold green]Docker Logs Begin[/bold green]", style="dark_orange"))
table = Table(title="Run Info", show_header=False) table = Table(title="Run Info", show_header=False)
+2 -1
View File
@@ -30,7 +30,8 @@ def wait_retry(
>>> counter >>> counter
2 2
""" """
assert retry_n > 0, "retry_n should be greater than 0" if retry_n <= 0:
raise ValueError("retry_n should be greater than 0")
def decorator(f: Callable[..., ASpecificRet]) -> Callable[..., ASpecificRet]: def decorator(f: Callable[..., ASpecificRet]) -> Callable[..., ASpecificRet]:
def wrapper(*args: Any, **kwargs: Any) -> ASpecificRet: def wrapper(*args: Any, **kwargs: Any) -> ASpecificRet:
+2 -1
View File
@@ -84,7 +84,8 @@ class WorkflowTracker:
# Log timer status if timer is started # Log timer status if timer is started
if self.loop_base.timer.started: if self.loop_base.timer.started:
remain_time = self.loop_base.timer.remain_time() remain_time = self.loop_base.timer.remain_time()
assert remain_time is not None if remain_time is None:
raise AssertionError("remain_time should not be None")
mlflow.log_metric("remain_time", remain_time.total_seconds()) mlflow.log_metric("remain_time", remain_time.total_seconds())
mlflow.log_metric( mlflow.log_metric(
"remain_percent", "remain_percent",
+1 -1
View File
@@ -10,7 +10,7 @@ fire
fuzzywuzzy fuzzywuzzy
openai openai
litellm>=1.83.14 # to support `from litellm import get_valid_models` litellm>=1.83.14 # to support `from litellm import get_valid_models`
aiohttp>=3.13.5 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525 aiohttp>=3.13.4 # CVE-2026-22815, CVE-2026-34515, CVE-2026-34516, CVE-2026-34525; >=3.13.4 due to litellm==1.83.14 exact pin
azure.identity azure.identity
pyarrow pyarrow
rich rich