mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-06 11:37:44 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13cbd42ecf | |||
| 64ed6b0cce | |||
| bf36f54159 | |||
| 79f1d34083 | |||
| 150a818e07 | |||
| b6d1caecc9 | |||
| 73e600bf25 | |||
| 9960633d01 |
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "1.3.7"
|
".": "1.3.9"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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 >=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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ load_dotenv(Path(__file__).parent / ".env")
|
|||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
from rdagent.utils.env import logger
|
||||||
|
|
||||||
app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
|
app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
@@ -510,6 +512,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 +662,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 +960,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 +1342,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 +1558,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
|
||||||
|
|||||||
+4
-1
@@ -27,6 +27,8 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from rdagent.utils.env import logger
|
||||||
|
|
||||||
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 +884,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 +1254,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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("."):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -322,6 +322,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:
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)}]")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+6
-5
@@ -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__":
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
+18
-15
@@ -614,7 +614,7 @@ 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():
|
||||||
@@ -678,7 +678,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 +761,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 +889,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 +924,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 +972,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 +1443,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(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user