chore: initial Predix state (RD-Agent fork + EURUSD setup)

This commit is contained in:
TPTBusiness
2026-03-22 21:20:02 +01:00
parent 4ea4d4fdf0
commit d057926d18
52 changed files with 378 additions and 9202 deletions
-6
View File
@@ -1,6 +0,0 @@
[bumpversion]
current_version = 0.0.0
commit = True
tag = True
[bumpversion:file:pyproject.toml]
-21
View File
@@ -1,21 +0,0 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
// Configuration Format: [level, applicability, value]
// level: Error level, usually expressed as a number:
// 0 - disable rule
// 1 - Warning (does not prevent commits)
// 2 - Error (will block the commit)
// applicability: the conditions under which the rule applies, commonly used values:
// “always” - always apply the rule
// “never” - never apply the rule
// value: the specific value of the rule, e.g. a maximum length of 100.
// Refs: https://commitlint.js.org/reference/rules-configuration.html
"header-max-length": [2, "always", 100],
"type-enum": [
2,
"always",
["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "Release-As"]
]
}
};
-10
View File
@@ -1,10 +0,0 @@
# 1. Pull down your Azure Container Registry image
FROM rdagentappregistry.azurecr.io/rd-agent-mle:20250623
# 2. (Optional) install any additional tools you need
# e.g. git, bash-completion, etc.
# RUN apt update && \
# apt install -y git bash-completion && \
# rm -rf /var/lib/apt/lists/*
RUN apt update && \
apt install -y git bash-completion
-39
View File
@@ -1,39 +0,0 @@
# Introduction
!!!!!This dev container is not for public development!!!!!!
!!!!!Please don't use it if you are just a public open-source user.!!!!!!
# Steps to run the dev container (for internal use only)
Prerequisites(this is the reason why this dev container is not for public use):
- Make sure you have the `rdagentappregistry.azurecr.io/rd-agent-mle:20250623` image locally & DevContainer is installed in your IDE
- The kaggle dataset is located at `/home/shared/RD-Agent/kaggle`
1. Open the project and select "Open In DevContainer"
2. Set up your Kaggle Key (do not share this; other internal URLs are hardcoded in the config files)
```bash
export KAGGLE_USERNAME=
export KAGGLE_KEY=
```
3. Run: python rdagent/app/data_science/loop.py --competition nomad2018-predict-transparent-conductors
# Additional Notes
- Please install and use this Dev Container in VS Code.
- You **must open VS Code remotely and enter the `RD-Agent` directory before running the DevContainer configuration (`.devcontainer/devcontainer.json`)**. Otherwise, the workspace and path mappings will not work as expected.
- To open the DevContainer correctly in VS Code:
1. Remotely connect to the machine and open the `RD-Agent` folder in VS Code.
2. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac), type and select **"Dev Containers: Reopen in Container"**.
# How to grade your submission in the DevContainer
1. save your submission file in `./sumission.csv`
2. Run evaluation
DS_COMPETITION=<your competition name>
conda run -n mlebench mlebench grade-sample submission.csv $DS_COMPETITION --data-dir /tmp/kaggle/zip_files/
-29
View File
@@ -1,29 +0,0 @@
{
"name": "rd-agent-mle DevContainer",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"workspaceFolder": "/workspace/RD-Agent",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace/RD-Agent,type=bind,consistency=cached",
"remoteUser": "root",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"mounts": [
"source=/home/shared/RD-Agent/kaggle,target=/tmp/kaggle,type=bind,consistency=cached,readonly"
],
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-toolsai.jupyter"
],
"runArgs": [
"--init",
"--shm-size=1g",
"--env-file", "${localWorkspaceFolder}/.devcontainer/env",
"--network=host",
"--gpus=all"
],
"postCreateCommand": "make dev"
}
-47
View File
@@ -1,47 +0,0 @@
# Global configs:
MAX_RETRY=12000
RETRY_WAIT_SECONDS=5
TIMEOUT_FAIL_LIMIT=100
# litellm
# CHAT_MODEL=gpt-4o
# CHAT_TEMPERATURE=0.7
CHAT_STREAM=False
CHAT_TEMPERATURE=1
CHAT_MODEL=o1-preview
SYSTEM_PROMPT_ROLE=user
BACKEND=rdagent.oai.backend.LiteLLMAPIBackend
OPENAI_API_KEY=sk-1234
OPENAI_API_BASE=http://ep14.213428.xyz:38881
# amc chat model configs:
EMBEDDING_MODEL=text-embedding-ada-002
# Cache Setting (Optional):
DUMP_CHAT_CACHE=True
USE_CHAT_CACHE=False
DUMP_EMBEDDING_CACHE=True
USE_EMBEDDING_CACHE=False
LOG_LLM_CHAT_CONTENT=True
DS_LOCAL_DATA_PATH=/tmp/kaggle
DS_IF_USING_MLE_DATA=True
PICKLE_CACHE_FOLDER_PATH_STR=./log/pickle_cache
CACHE_WITH_PICKLE=False
ENABLE_CACHE=False
PROMPT_CACHE_PATH=./log/prompt_cache.db
DS_CODER_COSTEER_ENV_TYPE=conda
# DS_PROPOSAL_VERSION=v2 deprecated
DS_CODER_ON_WHOLE_PIPELINE=True
COSTEER_V2_QUERY_FORMER_TRACE_LIMIT=3
# export PYTHONPATH=. # this is for running researcher branch;
-61
View File
@@ -1,61 +0,0 @@
"""
This file is a template for the .env file.
Please copy this file to .env and fill in the values.
For more information about configuration options, please refer to the documentation
"""
# ==========================================
# Global configs:
MAX_RETRY=10
RETRY_WAIT_SECONDS=20
# ==========================================
# ==========================================
# Backend Configuration
# ==========================================
# BACKEND=rdagent.oai.backend.LiteLLMAPIBackend
# ==========================================
# ==========================================
# Backend Configuration (choose one)
# ==========================================
# 1. Set universal API key
# CHAT_MODEL="gpt-4o"
# EMBEDDING_MODEL="text-embedding-3-small"
# OPENAI_API_BASE="https://your-endpoint.com/v1"
# OPENAI_API_KEY="sk-your-api-key-here"
# 2. Set separate API KEY
# Chat configuration
OPENAI_API_KEY="sk-chat-key"
OPENAI_API_BASE="https://xxx-litellm.com/v1"
CHAT_MODEL='gpt-4o'
# Embedding configuration (using other service)
# Use siliconflow as example, pay attention to the litellm_proxy prefix
LITELLM_PROXY_API_KEY="sk-embedding-service-key"
LITELLM_PROXY_API_BASE="https://api.siliconflow.cn/v1"
EMBEDDING_MODEL="litellm_proxy/BAAI/bge-large-en-v1.5"
# ==========================================
# ==========================================
# Other Configuration
# ==========================================
# CHAT_AZURE_API_BASE=<for_Azure_user>
# CHAT_AZURE_API_VERSION=<for_Azure_user>
# EMBEDDING_AZURE_API_BASE=<for_Azure_user>
# EMBEDDING_AZURE_API_VERSION=<for_Azure_user>
# Cache Setting (Optional):
# USE_CHAT_CACHE=True
# USE_EMBEDDING_CACHE=True
# FT_DOCKER_ENABLE_CACHE=True
# DS_DOCKER_ENABLE_CACHE=True
# Senario Configs:
# ==========================================
-2
View File
@@ -1,2 +0,0 @@
github:
- MIIC-finance
-51
View File
@@ -1,51 +0,0 @@
---
name: "\U0001F41B Bug Report"
about: Submit a bug report to help us improve RD-Agent
labels: bug
---
## 🐛 Bug Description
<!-- A clear and concise description of what the bug is. -->
## To Reproduce
Steps to reproduce the behavior:
1.
2.
3.
## Expected Behavior
<!-- A clear and concise description of what you expected to happen. -->
## Screenshot
<!-- A screenshot of the error message or anything shouldn't appear-->
## Environment
**Note**: Users can run `rdagent collect_info` to get system information and paste it directly here.
- Name of current operating system:
- Processor architecture:
- System, version, and hardware information:
- Version number of the system:
- Python version:
- Container ID:
- Container Name:
- Container Status:
- Image ID used by the container:
- Image tag used by the container:
- Container port mapping:
- Container Label:
- Startup Commands:
- RD-Agent version:
- Package version:
## Additional Notes
<!-- Add any other information about the problem here. -->
-9
View File
@@ -1,9 +0,0 @@
---
name: "\U0001F4D6 Documentation"
about: Report an issue related to documentation
---
## 📖 Documentation
<!-- Please specify whether it's tutorial part or API reference part, and describe it.-->
-25
View File
@@ -1,25 +0,0 @@
---
name: "\U0001F31FFeature Request"
about: Request for a new RD-Agent feature
labels: enhancement
---
## 🌟 Feature Description
<!-- A clear and concise description of the feature proposal -->
## Motivation
1. Application scenario
2. Related works (Papers, Github repos etc.):
3. Any other relevant and important information:
<!-- Please describe why the feature is important. -->
## Alternatives
<!-- A short description of any alternative solutions or features you've considered. -->
## Additional Notes
<!-- Add any other context or screenshots about the feature request here. -->
-10
View File
@@ -1,10 +0,0 @@
---
name: "❓Questions & Help"
about: Have some questions? We can offer help.
labels: question
---
## ❓ Questions and Help
We sincerely suggest you to carefully read the [documentation](http://rdagent.readthedocs.io/). After that, if you still feel puzzled, please describe the question clearly under this issue.
-34
View File
@@ -1,34 +0,0 @@
<!--- Thank you for submitting a Pull Request! In order to make our work smoother. -->
<!--- please make sure your Pull Request meets the following requirements: -->
<!--- 1. Provide a general summary of your changes in the Title above; -->
<!--- 2. Add appropriate prefixes to titles, such as `build:`, `chore:`, `ci:`, `docs:`, `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, `style:`, `test:`(Ref: https://www.conventionalcommits.org/). -->
<!--- Category: -->
<!--- Patch Updates: `fix:` -->
<!--- Example: fix(auth): correct login validation issue -->
<!--- minor update (introduces new functionality): `feat` -->
<!--- Example: feature(parser): add ability to parse arrays -->
<!--- major update(destructive update): Include BREAKING CHANGE in the commit message footer, or add `! ` in the commit footer to indicate that there is a destructive update. -->
<!--- Example: feat(auth)! : remove support for old authentication method -->
<!--- Other updates: `build:`, `chore:`, `ci:`, `docs:`, `perf:`, `refactor:`, `revert:`, `style:`, `test:`. -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Are there any related issues? If so, please put the link here. -->
<!--- Why is this change required? What problem does it solve? -->
## How Has This Been Tested?
<!--- Put an `x` in all the boxes that apply: --->
- [ ] If you are adding a new feature, test on your own test scripts.
<!--- **ATTENTION**: If you are adding a new feature, please make sure your codes are **correctly tested**. If our test scripts do not cover your cases, please provide your own test scripts under the `tests` folder and test them. More information about test scripts can be found [here](https://docs.python.org/3/library/unittest.html#basic-example), or you could refer to those we provide under the `tests` folder. -->
## Screenshots of Test Results (if appropriate):
1. Your own tests:
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Fix bugs
- [ ] Add new feature
- [ ] Update documentation
-19
View File
@@ -1,19 +0,0 @@
updates:
- commit-message:
prefix: build(actions)
directory: /
package-ecosystem: github-actions
schedule:
interval: weekly
- commit-message:
prefix: build(requirements)
directory: /
groups:
dev:
dependency-type: development
prod:
dependency-type: production
package-ecosystem: pip
schedule:
interval: weekly
version: 2
-69
View File
@@ -1,69 +0,0 @@
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
ci:
if: ${{ !cancelled() && ! failure() }}
needs: dependabot
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
cache: pip
python-version: ${{ matrix.python-version }}
- run: make dev
- name: lint test docs and build
run: make lint docs-gen test-offline # test docs build
strategy:
matrix:
python-version:
- '3.10'
- '3.11'
dependabot:
if: ${{ github.actor == 'dependabot[bot]' && startsWith(github.head_ref, 'dependabot/pip/') }}
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Set up Git
run: |
git config --global user.name github-actions
git config --global user.email github-actions@github.com
- name: Set up Python with multiple versions.
uses: actions/setup-python@v5
with:
cache: pip
python-version: |
3.10
3.11
- name: Install pipenv using pipx
run: pipx install pipenv
- name: Generate constraints for all supported Python versions
run: |
CI= PYTHON_VERSION=3.10 make constraints
CI= PYTHON_VERSION=3.11 make constraints
- name: Push changes if applicable
run: |
if [[ -n `git status --porcelain` ]]; then
git commit -a -m "build: Update constraints for dependabot."
git push
fi
name: CI
on:
pull_request:
types:
- opened
- synchronize
push:
branches:
- main
-35
View File
@@ -1,35 +0,0 @@
name: Lint pull request title
on:
pull_request:
types:
- opened
- synchronize
- reopened
- edited
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
lint-title:
runs-on: ubuntu-latest
steps:
# This step is necessary because the lint title uses the .commitlintrc.js file in the project root directory.
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '16'
- name: Install commitlint
run: npm install --save-dev @commitlint/{config-conventional,cli}
- name: Validate PR Title with commitlint
env:
BODY: ${{ github.event.pull_request.title }}
run: |
echo "$BODY" | npx commitlint --config .commitlintrc.js
-17
View File
@@ -1,17 +0,0 @@
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
documentation-links:
runs-on: ubuntu-latest
steps:
- uses: readthedocs/actions/preview@v1
with:
project-slug: RDAgent
name: Read the Docs Pull Request Preview
on:
pull_request_target:
types:
- opened
permissions:
pull-requests: write
-48
View File
@@ -1,48 +0,0 @@
name: Release
on:
push:
branches:
- main
permissions:
contents: read
jobs:
release_and_publish:
permissions:
contents: write
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Release please
id: release_please
uses: googleapis/release-please-action@v4
with:
# The current PAT (personal access token) was created on 2024-08-05,
# since the maximum validity of PAT is 1 year, you need to change the PAT before 2025-08-05.
token: ${{ secrets.PAT }}
release-type: simple
- uses: actions/checkout@v4
if: ${{ steps.release_please.outputs.release_created }}
with:
fetch-depth: 0
- name: Set up Python
if: ${{ steps.release_please.outputs.release_created }}
uses: actions/setup-python@v5
with:
cache: pip
python-version: '3.10'
- name: Install dependencies
if: ${{ steps.release_please.outputs.release_created }}
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine # better-exceptions(optional for debug)
- run: make dev
if: ${{ steps.release_please.outputs.release_created }}
- run: make build
if: ${{ steps.release_please.outputs.release_created }}
- name: upload
if: ${{ steps.release_please.outputs.release_created }}
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
make upload
+11 -189
View File
@@ -1,190 +1,12 @@
# Custom
*.swp
.DS_Store
Pipfile
public
release-notes.md
typescript*
tmp/
.ai/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
/log*/
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env*
*.env
.venv
^env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# all pkl files
*.pkl
# all h5 files
*.h5
# all vs-code files
.vscode/
# reports
reports/
# git_ignore_folder
.env
.env.backup*
*.backup*
git_ignore_folder/
#cache
*cache*/
*cache.json
# DB files
*.db
# Docker
factor_template/mlruns/
env_tpl
mlruns/
# possible output from coder or runner
*.pth
*qlib_res.csv
# shell script
*.out
/*.sh
.aider*
rdagent/app/benchmark/factor/example.json
# UI Server resources
videos/
static/
# AI assistant
.cursor/
.claude/
AGENTS.md
!rdagent/**/AGENTS.md
scripts/
fin_quant.log
selector.log
pickle_cache/
log/
__pycache__/
*.pyc
*.pyo
prompt_cache.db
-38
View File
@@ -1,38 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.10"
# During the build process, you need to fetch tags, and since the default command to read the docs only pulls shallow code, it will cause an error.
# So we added the `git fetch --tags --unshallow || true` command to fetch the full tag record.
# Adding this command overrides the default command, so we copied it over to make sure the build was successful.
commands:
- python -mvirtualenv $READTHEDOCS_VIRTUALENV_PATH
- python -m pip install --upgrade --no-cache-dir pip setuptools
- python -m pip install --upgrade --no-cache-dir sphinx
- python -m pip install --exists-action=w --no-cache-dir -r requirements/docs.txt
- python -m pip install --upgrade --upgrade-strategy only-if-needed --no-cache-dir .
- git fetch --tags --unshallow || true
- mkdir -p $READTHEDOCS_OUTPUT/html/
- python -m sphinx -T -b html -d _build/doctrees -D language=en ./docs $READTHEDOCS_OUTPUT/html
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build all formats
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- requirements: requirements/docs.txt
- method: pip
path: .
-2
View File
@@ -1,2 +0,0 @@
[client]
showSidebarNavigation = false
+47 -7
View File
@@ -1,15 +1,55 @@
hypothesis_generation:
system: |-
You are an expert in financial analysis. Your task is to generate a well-reasoned hypothesis based on the provided financial factors and report content.
Please ensure your response is in JSON format as shown below:
You are an expert quantitative researcher specialized in FX (foreign exchange) trading,
specifically EURUSD intraday strategies on 15-minute bars.
EURUSD domain knowledge you must apply:
- London session (08:00-12:00 UTC): highest volatility, trending behavior — favor momentum strategies
- NY session (13:00-17:00 UTC): second volatility peak, also trending
- Asian session (00:00-07:00 UTC): low volatility, mean-reverting behavior
- London/NY overlap (13:00-17:00 UTC): strongest directional moves of the day
- Weekend gap risk: avoid holding positions after Friday 20:00 UTC
- Spread cost: ~1.5 bps per trade — strategies must minimize unnecessary entries
- EURUSD is mean-reverting on short windows (<1h), trending on longer (>4h)
- Key macro drivers: ECB/Fed rate decisions, NFP (first Friday of month), CPI releases
Available model types you can propose:
- TimeSeries: LSTM, GRU, TCN (Temporal Convolutional Network), Transformer, PatchTST
- Tabular: XGBoost, LightGBM, RandomForest (on engineered features)
- Hybrid: CNN+LSTM, XGBoost+LSTM ensemble
- Statistical: Regime-switching (HMM), Kalman filter
Available features in the dataset:
- OHLCV: open, high, low, close, volume (15min bars)
- Returns: ret_1, ret_4, ret_8, ret_16, ret_96
- Technical: rsi_14, macd_hist, adx_14, atr_14, bb_pct, stoch_k, cci_14
- Volatility: vol_real_4, vol_real_16, vol_ratio, zscore_ret_96
- Time/Session: hour, is_london, is_ny, is_overlap, hour_sin, hour_cos
- Lags: rsi_14_lag1-8, macd_hist_lag1-8, bb_pct_lag1-8
Your hypothesis must:
1. Specify which session(s) the strategy targets
2. Name which model type to use and why it fits EURUSD
3. Include a session filter (is_london / is_ny)
4. Include a spread filter (only trade when expected |return| > 0.0003)
5. Specify target: classification (fwd_sign_4) or regression (fwd_ret_4)
Please ensure your response is in JSON format:
{
"hypothesis": "A clear and concise hypothesis based on the provided information.",
"reason": "A detailed explanation supporting the generated hypothesis.",
"hypothesis": "A clear and concise trading hypothesis for EURUSD 15min.",
"reason": "Detailed explanation including session, model choice, and expected edge.",
"model_type": "One of: TimeSeries / Tabular / XGBoost",
"target_session": "london / ny / asian / all",
"expected_arr_range": "e.g. 8-12%"
}
user: |-
The following are the financial factors and their descriptions:
Previously tried approaches and their results:
{{ factor_descriptions }}
The report content is as follows:
{{ report_content }}
Additional context:
{{ report_content }}
Generate a NEW hypothesis that is meaningfully different from what has been tried.
Focus on approaches that have NOT been tested yet.
Target: beat current best ARR of 9.62%.
@@ -31,6 +31,15 @@ evolving_strategy_model_coder:
system: |-
User is trying to implement some pytorch models in the following scenario:
{{ scenario }}
EURUSD-specific rules (ALWAYS apply these in generated code):
1. Session filter: use is_london and is_ny columns — weight/filter signals to active sessions
2. Spread filter: only generate signal when abs(predicted_return) > 0.0003
3. ADX regime: if adx_proxy > 1.2 use trend model; if adx_proxy < 0.8 use mean-reversion
4. Weekend filter: zero out signals when dayofweek==4 and hour>=20
5. Max trade frequency: target <15 trades per day (avoid spread cost death)
6. Supported model_type values: "Tabular", "TimeSeries", "XGBoost"
Your code is expected to align the scenario in any form which means The user needs to get the prediction of the model based on the input data.
To help you write the correct code, the user might provide multiple information that helps you write the correct code:
-4
View File
@@ -1,4 +0,0 @@
from rdagent.log.logger import RDAgentLog
from rdagent.log.utils import LogColors
rdagent_logger: RDAgentLog = RDAgentLog()
-103
View File
@@ -1,103 +0,0 @@
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Generator
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Literal, Optional
@dataclass
class Message:
"""The info unit of the storage"""
tag: str # namespace like like a.b.c
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] # The level of the logging
timestamp: datetime # The time when the message is generated
caller: Optional[
str
] # The caller of the logging like `rdagent.oai.llm_utils:_create_chat_completion_inner_function:55`(file:func:line)
pid_trace: Optional[str] # The process id trace; A-B-C represents A create B, B create C
content: object # The content
class Storage:
"""
Basic storage to support saving objects;
# Usage:
The storage has mainly two kind of users:
- The logging end: you can choose any of the following method to use the object
- We can use it directly with the native logging storage
- We can use it with other logging tools; For example, serve as a handler for loggers
- The view end:
- Mainly for the subclass of `logging.base.View`
- It should provide two kind of ways to provide content
- offline content provision.
- online content preovision.
"""
@abstractmethod
def log(
self,
obj: object,
tag: str = "",
timestamp: datetime | None = None,
) -> str | Path:
"""
Parameters
----------
obj : object
The object for logging.
name : str
The name of the object. For example "a.b.c"
We may log a lot of objects to a same name
Returns
-------
str | Path
The storage identifier of the object.
"""
...
@abstractmethod
def iter_msg(self) -> Generator[Message, None, None]:
"""
Iterate the message in the storage.
"""
...
@abstractmethod
def truncate(self, time: datetime) -> None:
"""
Remove all log entries after the specified time.
"""
...
def __str__(self) -> str:
return self.__class__.__name__
class View:
"""
Motivation:
Display the content in the storage
"""
# TODO: pleas fix me
@abstractmethod
def display(self, s: Storage, watch: bool = False) -> None:
"""
Parameters
----------
s : Storage
watch : bool
should we watch the new content and display them
"""
...
-34
View File
@@ -1,34 +0,0 @@
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from pydantic_settings import SettingsConfigDict
from rdagent.core.conf import ExtendedBaseSettings
class LogSettings(ExtendedBaseSettings):
model_config = SettingsConfigDict(env_prefix="LOG_", protected_namespaces=())
trace_path: str = str(Path.cwd() / "log" / datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S-%f"))
format_console: str | None = None
""""If it is None, leave it as the default"""
ui_server_port: int | None = None
storages: dict[str, list[int | str]] = {}
def set_ui_server_port(self, port: int | None) -> None:
self.ui_server_port = port
if port is None:
self.storages.pop("rdagent.log.ui.storage.WebStorage", None)
return
self.storages["rdagent.log.ui.storage.WebStorage"] = [port, self.trace_path]
def model_post_init(self, _context: Any, /) -> None:
self.set_ui_server_port(self.ui_server_port)
LOG_SETTINGS = LogSettings()
-153
View File
@@ -1,153 +0,0 @@
import os
import sys
from contextlib import contextmanager
from contextvars import ContextVar
from datetime import datetime
from pathlib import Path
from typing import Generator
from loguru import logger
from psutil import Process
from rdagent.core.utils import SingletonBaseClass, import_class
from .base import Storage
from .conf import LOG_SETTINGS
from .storage import FileStorage
from .utils import get_caller_info
class RDAgentLog(SingletonBaseClass):
"""
The files are organized based on the tag & PID
Here is an example tag
.. code-block::
a
- b
- c
- 123
- common_logs.log
- 1322
- common_logs.log
- 1233
- <timestamp>.pkl
- d
- 1233-673 ...
- 1233-4563 ...
- 1233-365 ...
"""
# Thread-/coroutine-local tag; In Linux forked subprocess, it will be copied to the subprocess.
_tag_ctx: ContextVar[str] = ContextVar("_tag_ctx", default="")
_raw_log_key = "_rdagent_raw"
@classmethod
def _configure_console_sinks(cls) -> None:
raw_filter = lambda record: bool(record["extra"].get(cls._raw_log_key, False))
normal_filter = lambda record: not raw_filter(record)
if LOG_SETTINGS.format_console is not None:
logger.add(sys.stdout, format=LOG_SETTINGS.format_console, filter=normal_filter)
else:
logger.add(sys.stdout, filter=normal_filter)
logger.add(sys.stdout, format="{message}", filter=raw_filter)
@property
def _tag(self) -> str: # Get current tag
return self._tag_ctx.get()
@_tag.setter # Set current tag
def _tag(self, value: str) -> None:
self._tag_ctx.set(value)
def __init__(self) -> None:
logger.remove()
self._configure_console_sinks()
self.storage = FileStorage(LOG_SETTINGS.trace_path)
self.other_storages: list[Storage] = []
self.refresh_storages_from_settings()
self.main_pid = os.getpid()
def refresh_storages_from_settings(self) -> None:
self.other_storages = []
for storage, args in LOG_SETTINGS.storages.items():
storage_cls = import_class(storage)
self.other_storages.append(storage_cls(*args))
def rebind_console_to_current_streams(self) -> None:
"""Rebind loguru sinks to the current stdio objects.
This is needed in forked/spawned subprocesses after stdout/stderr have been
redirected, because loguru keeps references to the original stream objects.
"""
logger.remove()
self._configure_console_sinks()
@contextmanager
def tag(self, tag: str) -> Generator[None, None, None]:
if tag.strip() == "":
raise ValueError("Tag cannot be empty.")
# Generate a new complete tag
current_tag = self._tag_ctx.get()
new_tag = tag if current_tag == "" else f"{current_tag}.{tag}"
# Set and save token for later restore
token = self._tag_ctx.set(new_tag)
try:
yield
finally:
# Restore previous tag (thread/coroutine safe)
self._tag_ctx.reset(token)
def set_storages_path(self, path: str | Path) -> None:
if isinstance(path, str):
path = Path(path)
for storage in [self.storage] + self.other_storages:
if hasattr(storage, "path"):
storage.path = path
def truncate_storages(self, time: datetime) -> None:
for storage in [self.storage] + self.other_storages:
storage.truncate(time=time)
def get_pids(self) -> str:
"""
Returns a string of pids from the current process to the main process.
Split by '-'.
"""
pid = os.getpid()
process = Process(pid)
pid_chain = f"{pid}"
while process.pid != self.main_pid:
parent_pid = process.ppid()
parent_process = Process(parent_pid)
pid_chain = f"{parent_pid}-{pid_chain}"
process = parent_process
return pid_chain
def log_object(self, obj: object, *, tag: str = "") -> None:
tag = f"{self._tag}.{tag}.{self.get_pids()}".strip(".")
for storage in [self.storage] + self.other_storages:
storage.log(obj, tag=tag)
def _log(self, level: str, msg: str, *, tag: str = "", raw: bool = False) -> None:
caller_info = get_caller_info(level=3)
tag = f"{self._tag}.{tag}.{self.get_pids()}".strip(".")
patched_logger = logger.patch(lambda r: r.update(caller_info)).bind(**{self._raw_log_key: raw}).opt(raw=raw)
log_func = getattr(patched_logger, level)
log_func(msg)
def info(self, msg: str, *, tag: str = "", raw: bool = False) -> None:
self._log("info", msg, tag=tag, raw=raw)
def warning(self, msg: str, *, tag: str = "", raw: bool = False) -> None:
self._log("warning", msg, tag=tag, raw=raw)
def error(self, msg: str, *, tag: str = "", raw: bool = False) -> None:
self._log("error", msg, tag=tag, raw=raw)
-260
View File
@@ -1,260 +0,0 @@
import pickle
import traceback
from collections import defaultdict
from pathlib import Path
import fire
import pandas as pd
from rdagent.core.experiment import FBWorkspace
from rdagent.core.proposal import ExperimentFeedback
from rdagent.log.storage import FileStorage
from rdagent.log.utils import extract_json, extract_loopid_func_name, is_valid_session
from rdagent.log.utils.folder import get_first_session_file_after_duration
from rdagent.scenarios.data_science.experiment.experiment import DSExperiment
from rdagent.scenarios.data_science.test_eval import (
MLETestEval,
NoTestEvalError,
get_test_eval,
)
# from rdagent.scenarios.kaggle.kaggle_crawler import score_rank
from rdagent.utils.workflow import LoopBase
def save_grade_info(log_trace_path: Path):
test_eval = get_test_eval()
trace_storage = FileStorage(log_trace_path)
for msg in trace_storage.iter_msg(tag="competition"):
competition = msg.content
for msg in trace_storage.iter_msg(tag="running"):
if isinstance(msg.content, DSExperiment):
# TODO: mle_score.txt is not a general name now.
# Please use a more general name like test_score.txt
try:
mle_score_str = test_eval.eval(competition, msg.content.experiment_workspace)
trace_storage.log(
mle_score_str, tag=f"{msg.tag}.mle_score.pid", save_type="pkl", timestamp=msg.timestamp
)
except Exception as e:
print(f"Error in {log_trace_path}: {e}", traceback.format_exc())
def save_all_grade_info(log_folder: str | Path) -> None:
for log_trace_path in Path(log_folder).iterdir():
if is_valid_session(log_trace_path):
try:
save_grade_info(log_trace_path)
except NoTestEvalError as e:
print(f"Error in {log_trace_path}: {e}", traceback.format_exc())
def _get_loop_and_fn_after_hours(log_folder: Path, hours: int):
stop_session_fp = get_first_session_file_after_duration(log_folder, f"{hours}h")
with stop_session_fp.open("rb") as f:
session_obj: LoopBase = pickle.load(f)
loop_trace = session_obj.loop_trace
stop_li = max(loop_trace.keys())
last_loop = loop_trace[stop_li]
last_step = last_loop[-1]
stop_fn = session_obj.steps[last_step.step_idx]
print(f"Stop Loop: {stop_li=}, {stop_fn=}")
files = sorted(
(log_folder / "__session__").glob("*/*_*"), key=lambda f: (int(f.parent.name), int(f.name.split("_")[0]))
)
print(f"Max Session: {files[-1:]=}")
return stop_li, stop_fn
def summarize_folder(log_folder: Path, hours: int | None = None) -> None:
test_eval = get_test_eval()
is_mle = isinstance(test_eval, MLETestEval)
"""
Summarize the log folder and save the summary as a pickle file.
Args:
log_folder (Path): The path to the log folder (contains many log traces).
hours (int | None): The number of hours to stat. If None, stat all.
"""
log_folder = Path(log_folder)
stat = defaultdict(dict)
for log_trace_path in log_folder.iterdir(): # One log trace
if not is_valid_session(log_trace_path):
continue
loop_num = 0
made_submission_num = 0
valid_submission_num = 0
above_median_num = 0
get_medal_num = 0
bronze_num = 0
silver_num = 0
gold_num = 0
test_scores = {}
test_ranks = {}
valid_scores = {}
bronze_threshold = 0.0
silver_threshold = 0.0
gold_threshold = 0.0
median_threshold = 0.0
success_loop_num = 0
sota_exp_stat = ""
sota_exp_score = None
sota_exp_rank = None
grade_output = None
if hours:
stop_li, stop_fn = _get_loop_and_fn_after_hours(log_trace_path, hours)
msgs = [(msg, extract_loopid_func_name(msg.tag)) for msg in FileStorage(log_trace_path).iter_msg()]
msgs = [(msg, int(loop_id) if loop_id else loop_id, fn) for msg, (loop_id, fn) in msgs]
msgs.sort(key=lambda m: m[1] if m[1] else -1) # sort by loop id
for msg, loop_id, fn in msgs: # messages in log trace
if loop_id:
loop_num = max(loop_id + 1, loop_num)
if hours and loop_id == stop_li and fn == stop_fn:
break
if msg.tag and "llm" not in msg.tag and "session" not in msg.tag:
if "competition" in msg.tag:
stat[log_trace_path.name]["competition"] = msg.content
# get threshold scores
workflowexp = FBWorkspace()
if is_mle:
stdout = workflowexp.execute(
env=test_eval.env,
entry=f"mlebench grade-sample None {stat[log_trace_path.name]['competition']} --data-dir /mle/data",
)
grade_output = extract_json(stdout)
if grade_output:
bronze_threshold = grade_output["bronze_threshold"]
silver_threshold = grade_output["silver_threshold"]
gold_threshold = grade_output["gold_threshold"]
median_threshold = grade_output["median_threshold"]
if "running" in msg.tag:
if isinstance(msg.content, DSExperiment):
if msg.content.result is not None:
valid_scores[loop_id] = msg.content.result
elif "mle_score" in msg.tag:
grade_output = extract_json(msg.content)
if grade_output:
if grade_output["submission_exists"]:
made_submission_num += 1
if grade_output["score"] is not None:
test_scores[loop_id] = grade_output["score"]
# if is_mle:
# _, test_ranks[loop_id] = score_rank(
# stat[log_trace_path.name]["competition"], grade_output["score"]
# )
if grade_output["valid_submission"]:
valid_submission_num += 1
if grade_output["above_median"]:
above_median_num += 1
if grade_output["any_medal"]:
get_medal_num += 1
if grade_output["bronze_medal"]:
bronze_num += 1
if grade_output["silver_medal"]:
silver_num += 1
if grade_output["gold_medal"]:
gold_num += 1
if "feedback" in msg.tag and "evolving" not in msg.tag:
if isinstance(msg.content, ExperimentFeedback) and bool(msg.content):
success_loop_num += 1
if grade_output: # sota exp's grade output
if grade_output["gold_medal"]:
sota_exp_stat = "gold"
elif grade_output["silver_medal"]:
sota_exp_stat = "silver"
elif grade_output["bronze_medal"]:
sota_exp_stat = "bronze"
elif grade_output["above_median"]:
sota_exp_stat = "above_median"
elif grade_output["valid_submission"]:
sota_exp_stat = "valid_submission"
elif grade_output["submission_exists"]:
sota_exp_stat = "made_submission"
if grade_output["score"] is not None:
sota_exp_score = grade_output["score"]
# if is_mle:
# _, sota_exp_rank = score_rank(
# stat[log_trace_path.name]["competition"], grade_output["score"]
# )
stat[log_trace_path.name].update(
{
"loop_num": loop_num,
"made_submission_num": made_submission_num,
"valid_submission_num": valid_submission_num,
"above_median_num": above_median_num,
"get_medal_num": get_medal_num,
"bronze_num": bronze_num,
"silver_num": silver_num,
"gold_num": gold_num,
"test_scores": test_scores,
# "test_ranks": test_ranks,
"valid_scores": valid_scores,
"success_loop_num": success_loop_num,
"sota_exp_stat": sota_exp_stat,
"sota_exp_score": sota_exp_score,
# "sota_exp_rank": sota_exp_rank,
"bronze_threshold": bronze_threshold,
"silver_threshold": silver_threshold,
"gold_threshold": gold_threshold,
"median_threshold": median_threshold,
}
)
# Save the summary
save_name = f"summary_{hours}h.pkl" if hours else "summary.pkl"
save_p = log_folder / save_name
if save_p.exists():
save_p.unlink()
print(f"Old {save_name} removed.")
pd.to_pickle(stat, save_p)
# {
# "competition_id": "stanford-covid-vaccine",
# "score": null,
# "gold_threshold": 0.34728,
# "silver_threshold": 0.35175,
# "bronze_threshold": 0.3534,
# "median_threshold": 0.363095,
# "any_medal": false,
# "gold_medal": false,
# "silver_medal": false,
# "bronze_medal": false,
# "above_median": false,
# "submission_exists": true,
# "valid_submission": false,
# "is_lower_better": true,
# "created_at": "2025-01-21T11:59:33.788201",
# "submission_path": "submission.csv"
# }
def grade_summary(log_folder: str) -> None:
"""
Generate test scores for log traces in the log folder and save the summary.
"""
log_folder = Path(log_folder)
save_all_grade_info(log_folder)
summarize_folder(log_folder)
if __name__ == "__main__":
fire.Fire(
{
"grade": save_all_grade_info,
"summary": summarize_folder,
"grade_summary": grade_summary,
}
)
-253
View File
@@ -1,253 +0,0 @@
# API
## A. Controls
### 1. /upload [POST]
#### Request
- "scenario": one of six values
1. "Finance Data Building"
2. "Finance Data Building (Reports)"
3. "Finance Model Implementation"
4. "General Model Implementation"
5. "Medical Model Implementation"
6. "Data Science"
- "files": **2** scenarios need this
1. in "Finance Data Building (Reports)" Scenario, one or more pdf files.
2. in "General Model Implementation" Scenario, one pdf file or one pdf link like `https://arxiv.org/pdf/2210.09789`
- "competition": **Data Science** Scenario need this, one of 75 competitions.
- "loops": Number of loops after which RD-Agent will automatically stop (optional; if not set, it will not stop automatically and must be stopped manually).
- "all_duration": Total duration (in hours) for which the RD-Agent should run before stopping automatically. If not set, the agent will continue running until stopped manually or by the "loops" parameter.
#### Response
- "id": a unique identifier string, such as `/home/rdagent_log/data_science/competition_A/trace_1` or `/home/rdagent_log/finance/trace_1`, used to mark the series of logs generated by this RD-Agent run.
### 2. /control [POST]
#### Request
- "id": identifier
- "action": one of three values
1. "pause"
2. "resume"
3. "stop"
#### Response
- "status": "success" / "error: ..."
### 3. /trace [POST]
Returns the sequence of Messages generated for the current id on the backend that **have not yet been returned to the frontend**.
#### Request
- "id": identifier
- "all": True / False. True means all Messages not yet provided to the frontend will be returned; False returns a random 1 to 10 Messages. In most cases, this should be True.
- "reset": True / False. Reset means the pointer for "not yet returned to the frontend" will be set back to the first Message generated for this id, i.e., return from the beginning. In most cases, this should be False.
#### Response
- a list of [Messages](#b-messages)
## B. Messages
### Research
Only **2** Message in one loop
1. hypothesis
```json
{
"tag": "research.hypothesis",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": {
"hypothesis": "...",
"reason": "...",
"component": "...", // only exists in Data Science Scenario
"concise_reason": "...",
"concise_justification": "...",
"concise_observation": "...",
"concise_knowledge": "...",
}
}
```
2. tasks
```json
{
"tag": "research.tasks",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": [ // list of tasks
{
"name": "...",
"description": "...",
"model_type": "...", // only exists in "Finance Model Implementation", "General Model Implementation", "Medical Model Implementation", or some tasks of "Data Science"
"architecture": "...", // same as above
"hyperparameters": "...", // same as above
},
{
}
//... same as above
]
}
```
### evolving
- 1 to 10 pairs of Messages (codes & feedbacks), each identified by an "evo_id" indicating the evolving round.
- In the **Data Science** scenario, each evolving round contains only **one task**, but the "codes" for that task may include **multiple code files**.
- In other scenarios, each evolving round may contain **multiple tasks**, but each task's "codes" will include only **one code file**.
1. codes
```json
{
"tag": "evolving.codes",
"timestamp": "<isoformat>",
"loop_id": "1",
"evo_id": "0",
"content": [ // list of task_name & codes
{
"evo_id": "0",
"target_task_name": "task_1",
"workspace": { // one or more codes
"a.py": "...<python codes>",
"b.py": "...<python codes>",
//...
}
},
{
"evo_id": "0",
"target_task_name": "task_2",
"workspace": {
"a.py": "...<python codes>",
//...
}
}
//... same as above
]
}
{
"tag": "evolving.codes",
"timestamp": "<isoformat>",
"loop_id": "1",
"evo_id": "1",
"content": [
//... same as above
]
}
```
2. feedbacks
```json
{
"tag": "evolving.feedbacks",
"timestamp": "<isoformat>",
"loop_id": "1",
"evo_id": "0",
"content": [ // list of feedbacks
{
"evo_id": "0",
"final_decision": "True", // True or False
"execution": "...",
"code": "...",
"return_checking": "..."
},
//... same as above
]
}
{
"tag": "evolving.codes",
"timestamp": "<isoformat>",
"loop_id": "1",
"evo_id": "1",
"content": [
//... same as above
]
}
```
### feedback
Each tag below appears only once per loop.
1. config (only exists in "Finance Data Building"/"Finance Data Building (Reports)"/"Finance Model Implementation")
```json
{
"tag": "feedback.config",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": {
"config": "a markdown string",
}
}
```
2. return_chart (only exists in "Finance Data Building"/"Finance Data Building (Reports)"/"Finance Model Implementation")
```json
{
"tag": "feedback.return_chart",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": {
"chart_html": "chart html codes string",
}
}
```
3. metric
```json
{
"tag": "feedback.metric",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": {
"result": "{ \"<metric_name>\": <value>, ... }" // A JSON string containing metric names and their corresponding values.
}
}
```
4. hypothesis_feedback
```json
{
"tag": "feedback.hypothesis_feedback",
"timestamp": "<isoformat>",
"loop_id": "1",
"content": {
"decision": "True",
"reason": "...",
"exception": "...",
"observations": "...", // may not exists
"hypothesis_evaluation": "...", // may not existsc
"new_hypothesis": "...", // may not exists
}
}
```
# TODO
## Session
- How to continue.
- show & copy trace_id(name)?
-
## Page
1. remove Medical, add Finance Whole Pipeline
2.
-562
View File
@@ -1,562 +0,0 @@
import logging
import os
import random
import traceback
from collections import defaultdict
from contextlib import redirect_stderr, redirect_stdout
from datetime import datetime, timezone
from multiprocessing import Process, Queue
from pathlib import Path
from queue import Empty
import randomname
import typer
from flask import Flask, jsonify, request, send_file, send_from_directory
from flask_cors import CORS
from werkzeug.utils import secure_filename
from rdagent.log.storage import FileStorage
from rdagent.log.ui.conf import UI_SETTING
from rdagent.log.ui.storage import WebStorage
app = Flask(__name__, static_folder=str(Path(UI_SETTING.static_path).resolve()))
CORS(app)
app.config["UI_SERVER_PORT"] = 19899
_YELLOW = "\033[33m"
_RESET = "\033[0m"
class _YellowWarningFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
if record.levelno == logging.WARNING:
record.levelname = f"{_YELLOW}{record.levelname}{_RESET}"
return super().format(record)
def _configure_app_logger() -> None:
formatter = _YellowWarningFormatter(
fmt="[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
for handler in app.logger.handlers:
handler.setFormatter(formatter)
_configure_app_logger()
_TARGETS_WITHOUT_USER_INTERACTION = {"general_model", "fin_factor_report"}
class RDAgentTask:
def __init__(
self,
target_name: str,
kwargs: dict,
stdout_path: str,
log_trace_path: str,
scenario: str,
trace_name: str,
ui_server_port: int | None = None,
create_process: bool = True,
) -> None:
self.target_name = target_name
self.kwargs = kwargs
self.stdout_path = stdout_path
self.log_trace_path = log_trace_path
self.scenario = scenario
self.trace_name = trace_name
self.ui_server_port = ui_server_port
self.process: Process | None = None
# Two IPC queues for user interaction.
# - `user_request_q`: rdagent subprocess -> server (dicts to render on frontend)
# - `user_response_q`: server -> rdagent subprocess (user input dicts)
# NOTE: Use multiprocessing.Queue because rdagent is started as a separate process.
self.user_request_q: Queue = Queue(maxsize=1024)
self.user_response_q: Queue = Queue(maxsize=1024)
if create_process:
self.process = Process(
target=self._run,
name=f"rdagent:{self.scenario}:{self.trace_name}",
)
self.messages: list[dict] = []
self.pointers: defaultdict[str, int] = defaultdict(int)
def start(self) -> None:
if self.process is not None:
self.process.start()
def is_alive(self) -> bool:
return self.process is not None and self.process.is_alive()
def get_end_code(self) -> int:
if self.process is None or self.process.exitcode is None:
return 0
return self.process.exitcode
def stop(self) -> None:
if self.process is not None and self.process.is_alive():
self.process.terminate()
self.process.join()
# Best-effort cleanup for IPC queues.
for q in (self.user_request_q, self.user_response_q):
try:
q.cancel_join_thread()
except Exception:
pass
try:
q.close()
except Exception:
pass
def _run(self) -> None:
from rdagent.log.conf import LOG_SETTINGS
LOG_SETTINGS.set_ui_server_port(self.ui_server_port)
from rdagent.log import rdagent_logger
rdagent_logger.refresh_storages_from_settings()
rdagent_logger.set_storages_path(self.log_trace_path)
Path(self.stdout_path).parent.mkdir(parents=True, exist_ok=True)
with open(self.stdout_path, "w") as log_file:
with redirect_stdout(log_file), redirect_stderr(log_file):
rdagent_logger.rebind_console_to_current_streams()
try:
# Only interactive targets should receive IPC queues.
if self.target_name not in _TARGETS_WITHOUT_USER_INTERACTION:
self.kwargs.setdefault(
"user_interaction_queues",
(self.user_request_q, self.user_response_q),
)
if self.target_name == "data_science":
from rdagent.app.data_science.loop import main as data_science
data_science(**self.kwargs)
elif self.target_name == "general_model":
from rdagent.app.general_model.general_model import (
extract_models_and_implement as general_model,
)
general_model(**self.kwargs)
elif self.target_name == "fin_factor":
from rdagent.app.qlib_rd_loop.factor import main as fin_factor
fin_factor(**self.kwargs)
elif self.target_name == "fin_factor_report":
from rdagent.app.qlib_rd_loop.factor_from_report import (
main as fin_factor_report,
)
fin_factor_report(**self.kwargs)
elif self.target_name == "fin_model":
from rdagent.app.qlib_rd_loop.model import main as fin_model
fin_model(**self.kwargs)
elif self.target_name == "fin_quant":
from rdagent.app.qlib_rd_loop.quant import main as fin_quant
fin_quant(**self.kwargs)
else:
raise ValueError(f"Unknown target: {self.target_name}")
except Exception:
traceback.print_exc()
rdagent_processes: dict[str, RDAgentTask] = {}
log_folder_path = Path(UI_SETTING.trace_folder).absolute()
def _drain_user_requests_into_messages(task: RDAgentTask) -> None:
"""Move a single pending user-interaction request into `task.messages`.
Assumption: each rdagent process only has one active request at a time.
"""
try:
req = task.user_request_q.get_nowait()
except Empty:
return
except Exception:
return
# Standardize the message shape for the frontend.
# The agent can send either a full message dict, or a raw content dict.
if isinstance(req, dict) and {"tag", "timestamp", "content"}.issubset(req.keys()):
msg = req
else:
msg = {
"tag": "user_interaction.request",
"timestamp": datetime.now(timezone.utc).isoformat(),
"content": req,
}
task.messages.append(msg)
@app.route("/favicon.ico")
def favicon():
return send_from_directory(app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon")
def _normalize_static_request_path(fn: str) -> str:
static_prefix = UI_SETTING.static_path.strip("./")
if static_prefix and fn.startswith(f"{static_prefix}/"):
return fn[len(static_prefix) + 1 :]
return fn
def _get_or_create_task(trace_id: str) -> RDAgentTask:
task = rdagent_processes.get(trace_id)
if task is None:
task = RDAgentTask(
target_name="",
kwargs={},
stdout_path="",
log_trace_path=trace_id,
scenario="",
trace_name="",
ui_server_port=None,
create_process=False,
)
rdagent_processes[trace_id] = task
return task
def _resolve_stdout_path(trace_id: str) -> Path | None:
normalized_trace_id = str(trace_id or "").strip()
if not normalized_trace_id:
return None
task = rdagent_processes.get(str(log_folder_path / normalized_trace_id))
if task is None or not task.stdout_path:
return None
stdout_path = Path(task.stdout_path).resolve()
try:
if os.path.commonpath([str(stdout_path), str(log_folder_path)]) != str(log_folder_path):
return None
except ValueError:
return None
return stdout_path
def read_trace(log_path: Path, id: str = "") -> None:
fs = FileStorage(log_path)
ws = WebStorage(port=1, path=log_path)
task = _get_or_create_task(id)
task.messages = []
last_timestamp = None
for msg in fs.iter_msg():
data = ws._obj_to_json(obj=msg.content, tag=msg.tag, id=id, timestamp=msg.timestamp.isoformat())
if data:
if isinstance(data, list):
for d in data:
task.messages.append(d["msg"])
last_timestamp = msg.timestamp
else:
task.messages.append(data["msg"])
last_timestamp = msg.timestamp
now = datetime.now(timezone.utc)
if last_timestamp and (now - last_timestamp).total_seconds() > 1800:
task.messages.append(
{
"tag": "END",
"timestamp": now.isoformat(),
"content": {"error_msg": "Trace session has ended.", "end_code": 0},
}
)
# load all traces from the log folder
# for p in log_folder_path.glob("*/*/"):
# read_trace(p, id=str(p))
@app.route("/trace", methods=["POST"])
def update_trace():
data = request.get_json()
trace_id = data.get("id")
return_all = data.get("all")
reset = data.get("reset")
msg_num = random.randint(1, 10)
app.logger.info(data)
log_folder_path = Path(UI_SETTING.trace_folder).absolute()
if not trace_id:
return jsonify({"error": "Trace ID is required"}), 400
trace_id = str(log_folder_path / trace_id)
task = _get_or_create_task(trace_id)
# Make sure any pending user-interaction requests are visible to the frontend.
_drain_user_requests_into_messages(task)
if task.process is not None and not task.is_alive():
if not task.messages or task.messages[-1].get("tag") != "END":
task.messages.append(
{
"tag": "END",
"timestamp": datetime.now(timezone.utc).isoformat(),
"content": {
"error_msg": "RD-Agent process has completed.",
"end_code": task.get_end_code(),
},
}
)
app.logger.warning(f"Process for {trace_id} has ended.")
user_ip = request.remote_addr
if reset:
task.pointers[user_ip] = 0
start_pointer = task.pointers[user_ip]
end_pointer = start_pointer + msg_num
if end_pointer > len(task.messages) or return_all:
end_pointer = len(task.messages)
returned_msgs = task.messages[start_pointer:end_pointer]
task.pointers[user_ip] = end_pointer
if returned_msgs:
app.logger.info([msg["tag"] for msg in returned_msgs])
return jsonify(returned_msgs), 200
@app.route("/stdout", methods=["GET"])
def download_stdout_file():
trace_id = request.args.get("id", "")
stdout_path = _resolve_stdout_path(trace_id)
if stdout_path is None:
return jsonify({"error": "Trace ID is required or invalid"}), 400
if not stdout_path.exists() or not stdout_path.is_file():
return jsonify({"error": "Stdout file not found"}), 404
return send_file(
stdout_path,
as_attachment=True,
download_name=stdout_path.name,
mimetype="text/plain",
)
@app.route("/upload", methods=["POST"])
def upload_file():
# 获取请求体中的字段
global rdagent_processes
scenario = request.form.get("scenario")
files = request.files.getlist("files")
competition = request.form.get("competition")
loop_n = request.form.get("loops")
all_duration = request.form.get("all_duration")
# scenario = "Data Science Loop"
if scenario == "Data Science":
competition = competition[10:] # Eg. MLE-Bench:aerial-cactus-competition
trace_name = f"{competition}-{randomname.get_name()}"
else:
trace_name = randomname.get_name()
trace_files_path = log_folder_path / "uploads" / scenario / trace_name
log_trace_path = (log_folder_path / scenario / trace_name).absolute()
stdout_path = log_folder_path / scenario / f"{trace_name}.log"
if not stdout_path.exists():
stdout_path.parent.mkdir(parents=True, exist_ok=True)
# save files
for file in files:
if file:
p = (log_folder_path / "uploads" / scenario / trace_name).resolve()
sanitized_filename = secure_filename(file.filename) # Sanitize filename
target_path = (p / sanitized_filename).resolve() # Normalize target path
# Ensure target_path is within the allowed base directory
if os.path.commonpath([str(target_path), str(p)]) == str(p) and target_path.is_file() == False:
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
file.save(target_path)
else:
return jsonify({"error": "Invalid file path"}), 400
target_name = None
kwargs = {}
loop_n_val = int(loop_n) if loop_n else None
all_duration_val = f"{all_duration}h" if all_duration else None
if scenario == "Finance Data Building":
target_name = "fin_factor"
kwargs = {
"loop_n": loop_n_val,
"all_duration": all_duration_val,
"base_features_path": str(trace_files_path),
}
if scenario == "Finance Model Implementation":
target_name = "fin_model"
kwargs = {
"loop_n": loop_n_val,
"all_duration": all_duration_val,
"base_features_path": str(trace_files_path),
}
if scenario == "Finance Whole Pipeline":
target_name = "fin_quant"
kwargs = {
"loop_n": loop_n_val,
"all_duration": all_duration_val,
"base_features_path": str(trace_files_path),
}
if scenario == "Finance Data Building (Reports)":
target_name = "fin_factor_report"
kwargs = {"report_folder": str(trace_files_path), "all_duration": all_duration_val}
if scenario == "General Model Implementation":
if len(files) == 0: # files is one link
rfp = request.form.get("files")[0]
else: # one file is uploaded
rfp = str(trace_files_path / files[0].filename)
target_name = "general_model"
kwargs = {"report_file_path": rfp}
if scenario == "Data Science":
target_name = "data_science"
kwargs = {"competition": competition, "loop_n": loop_n_val, "timeout": all_duration_val}
if target_name is None:
return jsonify({"error": "Unknown scenario"}), 400
app.logger.info(f"Started process for {log_trace_path} with target: {target_name}, kwargs: {kwargs}")
task = RDAgentTask(
target_name=target_name,
kwargs=kwargs,
stdout_path=str(stdout_path),
log_trace_path=str(log_trace_path),
scenario=scenario,
trace_name=trace_name,
ui_server_port=app.config["UI_SERVER_PORT"],
)
task.start()
app.logger.warning(f"Task {log_trace_path} started.")
rdagent_processes[str(log_trace_path)] = task
return (
jsonify(
{
"id": f"{scenario}/{trace_name}",
}
),
200,
)
@app.route("/receive", methods=["POST"])
def receive_msgs():
try:
data = request.get_json()
if not data:
return jsonify({"error": "No JSON data received"}), 400
except Exception as e:
return jsonify({"error": "Internal Server Error"}), 500
if isinstance(data, list):
for d in data:
task = _get_or_create_task(d["id"])
task.messages.append(d["msg"])
else:
task = _get_or_create_task(data["id"])
task.messages.append(data["msg"])
return jsonify({"status": "success"}), 200
@app.route("/user_interaction/submit", methods=["POST"])
def submit_user_interaction_response():
"""Frontend submits a user response; server forwards it to the rdagent subprocess via IPC queue."""
data = request.get_json(silent=True) or {}
trace_id = data.get("id")
payload = data.get("payload")
if not trace_id:
return jsonify({"error": "Trace ID is required"}), 400
if payload is None:
return jsonify({"error": "Missing 'payload'"}), 400
trace_id = str(log_folder_path / trace_id)
task = _get_or_create_task(trace_id)
try:
task.user_response_q.put(payload, block=False)
except Exception as e:
return jsonify({"error": f"Failed to enqueue user response: {e}"}), 500
return jsonify({"status": "success"}), 200
@app.route("/control", methods=["POST"])
def control_process():
global rdagent_processes
data = request.get_json()
app.logger.info(data)
if not data or "id" not in data or "action" not in data:
return jsonify({"error": "Missing 'id' or 'action' in request"}), 400
id = str(log_folder_path / data["id"])
action = data["action"]
if action != "stop":
return jsonify({"error": "Only 'stop' action is supported"}), 400
if id not in rdagent_processes or rdagent_processes[id] is None:
return jsonify({"error": "No running process for given id"}), 400
task = rdagent_processes[id]
if task.process is None:
return jsonify({"error": "No running process for given id"}), 400
try:
if task.is_alive():
task.stop()
if not task.messages or task.messages[-1].get("tag") != "END":
task.messages.append(
{
"tag": "END",
"timestamp": datetime.now(timezone.utc).isoformat(),
"content": {"error_msg": "RD-Agent process was stopped by user.", "end_code": -1},
}
)
app.logger.warning(f"Process for {id} has been stopped.")
return jsonify({"status": "stopped"}), 200
except Exception as e:
return jsonify({"error": f"Failed to {action} process, {e}"}), 500
@app.route("/test", methods=["GET"])
def test():
# return 'Hello, World!'
msgs = {k: [i["tag"] for i in task.messages] for k, task in rdagent_processes.items()}
pointers = {k: dict(task.pointers) for k, task in rdagent_processes.items()}
return jsonify({"msgs": msgs, "pointers": pointers}), 200
@app.route("/", methods=["GET"])
def index():
# return 'Hello, World!'
# return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
return send_from_directory(app.static_folder, "index.html")
@app.route("/<path:fn>", methods=["GET"])
def server_static_files(fn):
return send_from_directory(app.static_folder, _normalize_static_request_path(fn))
def main(port: int = 19899):
app.config["UI_SERVER_PORT"] = port
app.run(debug=False, host="0.0.0.0", port=port)
if __name__ == "__main__":
typer.run(main)
-174
View File
@@ -1,174 +0,0 @@
import multiprocessing
import os
import random
import signal
import subprocess
import threading
import time
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
import randomname
import typer
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
from rdagent.log.ui.conf import UI_SETTING
app = Flask(__name__, static_folder=UI_SETTING.static_path)
CORS(app)
rdagent_processes = defaultdict()
server_port = 19899
@app.route("/favicon.ico")
def favicon():
return send_from_directory(app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon")
msgs_for_frontend = defaultdict(list)
pointers = defaultdict(int)
@app.route("/trace", methods=["POST"])
def update_trace():
global pointers, msgs_for_frontend
data = request.get_json()
# app.logger.info(data)
trace_id = data.get("id")
return_all = data.get("all")
reset = data.get("reset")
msg_num = random.randint(1, 10)
if reset:
pointers[trace_id] = 0
end_pointer = pointers[trace_id] + msg_num
if end_pointer > len(msgs_for_frontend[trace_id]) or return_all:
end_pointer = len(msgs_for_frontend[trace_id])
returned_msgs = msgs_for_frontend[trace_id][pointers[trace_id] : end_pointer]
pointers[trace_id] = end_pointer
# if len(returned_msgs):
# app.logger.info(data)
# app.logger.info([i["tag"] for i in returned_msgs])
# try:
# import json
# resp = json.dumps(returned_msgs, ensure_ascii=False)
# except Exception as e:
# app.logger.error(f"Error in jsonify: {e}")
# for msg in returned_msgs:
# try:
# rr = json.dumps(msg, ensure_ascii=False)
# except Exception as e:
# app.logger.error(f"Error in jsonify individual message: {e}")
# app.logger.error(msg)
return jsonify(returned_msgs), 200
@app.route("/upload", methods=["POST"])
def upload_file():
# 获取请求体中的字段
global rdagent_processes, server_port, msgs_for_frontend
scenario = request.form.get("scenario")
files = request.files.getlist("files")
competition = request.form.get("competition")
loop_n = request.form.get("loops")
all_duration = request.form.get("all_duration")
log_folder_path = Path("/home/bowen/workspace/new_traces").absolute()
if scenario == "Data Science":
trace_path = log_folder_path / "o1-preview" / f"{competition[10:]}.1"
else:
trace_path = log_folder_path / scenario
id = f"{scenario}/{randomname.get_name()}"
def read_trace(log_path: Path, t: float = 0.2, id: str = "") -> None:
from rdagent.log.storage import FileStorage
from rdagent.log.ui.storage import WebStorage
fs = FileStorage(log_path)
ws = WebStorage(port=1, path=log_path)
msgs_for_frontend[id] = []
for msg in fs.iter_msg():
data = ws._obj_to_json(obj=msg.content, tag=msg.tag, id=id, timestamp=msg.timestamp.isoformat())
if data:
if isinstance(data, list):
for d in data:
time.sleep(t)
msgs_for_frontend[id].append(d["msg"])
else:
time.sleep(t)
msgs_for_frontend[id].append(data["msg"])
msgs_for_frontend[id].append({"tag": "END", "timestamp": datetime.now(timezone.utc).isoformat(), "content": {}})
# 启动后台线程,不阻塞 return
threading.Thread(target=read_trace, args=(trace_path, 0.5, id), daemon=True).start()
return jsonify({"id": id}), 200
@app.route("/receive", methods=["POST"])
def receive_msgs():
try:
data = request.get_json()
app.logger.info(data["msg"]["tag"])
if not data:
return jsonify({"error": "No JSON data received"}), 400
except Exception as e:
return jsonify({"error": "Internal Server Error"}), 500
if isinstance(data, list):
for d in data:
msgs_for_frontend[d["id"]].append(d["msg"])
else:
msgs_for_frontend[data["id"]].append(data["msg"])
return jsonify({"status": "success"}), 200
@app.route("/control", methods=["POST"])
def control_process():
global rdagent_processes
data = request.get_json()
app.logger.info(data)
if not data or "id" not in data or "action" not in data:
return jsonify({"error": "Missing 'id' or 'action' in request"}), 400
id = data["id"]
action = data["action"]
return jsonify({"status": "success", "message": f"Received action '{action}' for process with id '{id}'"})
@app.route("/test", methods=["GET"])
def test():
# return 'Hello, World!'
return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
@app.route("/", methods=["GET"])
def index():
# return 'Hello, World!'
# return {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()}
return send_from_directory(app.static_folder, "index.html")
@app.route("/<path:fn>", methods=["GET"])
def server_static_files(fn):
return send_from_directory(app.static_folder, fn)
def main(port: int = 19899):
global server_port
server_port = port
app.run(debug=True, host="0.0.0.0", port=port)
if __name__ == "__main__":
typer.run(main)
-116
View File
@@ -1,116 +0,0 @@
import json
import pickle
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Generator, Literal
from .base import Message, Storage
from .utils import gen_datetime
LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def _remove_empty_dir(path: Path) -> None:
"""
Recursively remove empty directories.
This function will remove the directory if it is empty after removing its subdirectories.
"""
if path.is_dir():
sub_dirs = [sub for sub in path.iterdir() if sub.is_dir()]
for sub in sub_dirs:
_remove_empty_dir(sub)
if not any(path.iterdir()):
path.rmdir()
class FileStorage(Storage):
"""
The info are logginged to the file systems
TODO: describe the storage format
"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
def log(
self,
obj: object,
tag: str = "",
timestamp: datetime | None = None,
save_type: Literal["json", "text", "pkl"] = "pkl",
**kwargs: Any,
) -> str | Path:
# TODO: We can remove the timestamp after we implement PipeLog
timestamp = gen_datetime(timestamp)
cur_p = self.path / tag.replace(".", "/")
cur_p.mkdir(parents=True, exist_ok=True)
path = cur_p / f"{timestamp.strftime('%Y-%m-%d_%H-%M-%S-%f')}.log"
if save_type == "json":
path = path.with_suffix(".json")
with path.open("w") as f:
try:
json.dump(obj, f)
except TypeError:
json.dump(json.loads(str(obj)), f)
return path
elif save_type == "pkl":
path = path.with_suffix(".pkl")
with path.open("wb") as f:
pickle.dump(obj, f)
return path
elif save_type == "text":
obj = str(obj)
with path.open("w") as f:
f.write(obj)
return path
log_pattern = re.compile(
r"(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \| "
r"(?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL) *\| "
r"(?P<caller>.+:.+:\d+) - "
)
def iter_msg(self, tag: str | None = None, pattern: str | None = None) -> Generator[Message, None, None]:
msg_l = []
if pattern:
pkl_files = pattern
elif tag:
pkl_files = f"**/{tag.replace('.','/')}/**/*.pkl"
else:
pkl_files = "**/*.pkl"
for file in self.path.glob(pkl_files):
if file.name == "debug_llm.pkl":
continue
pkl_log_tag = ".".join(file.relative_to(self.path).as_posix().replace("/", ".").split(".")[:-3])
pid = file.parent.name
with file.open("rb") as f:
content = pickle.load(f)
timestamp = datetime.strptime(file.stem, "%Y-%m-%d_%H-%M-%S-%f").replace(tzinfo=timezone.utc)
m = Message(tag=pkl_log_tag, level="INFO", timestamp=timestamp, caller="", pid_trace=pid, content=content)
msg_l.append(m)
msg_l.sort(key=lambda x: x.timestamp)
for m in msg_l:
yield m
def truncate(self, time: datetime) -> None:
for file in self.path.glob("**/*.pkl"):
timestamp = datetime.strptime(file.stem, "%Y-%m-%d_%H-%M-%S-%f").replace(tzinfo=timezone.utc)
if timestamp > time.replace(tzinfo=timezone.utc):
file.unlink()
_remove_empty_dir(self.path)
def __str__(self) -> str:
return f"FileStorage({self.path})"
-86
View File
@@ -1,86 +0,0 @@
import re
from datetime import datetime, timedelta
from rdagent.core.utils import SingletonBaseClass
from rdagent.log import rdagent_logger as logger
class RDAgentTimer:
def __init__(self) -> None:
self.started: bool = False
self.target_time: datetime | None = None
self.all_duration: timedelta | None = None
self._remain_time_duration: timedelta | None = None
def reset(self, all_duration: str | timedelta) -> None:
if isinstance(all_duration, str):
pattern = re.compile(r"^\s*(\d*\.?\d+)\s*([smhd]?)\s*$")
match = pattern.match(all_duration)
if not match:
return None
value = float(match.group(1))
unit = match.group(2)
if unit == "s":
self.all_duration = timedelta(seconds=value)
elif unit == "m":
self.all_duration = timedelta(minutes=value)
elif unit == "h":
self.all_duration = timedelta(hours=value)
elif unit == "d":
self.all_duration = timedelta(days=value)
else:
self.all_duration = timedelta(seconds=value)
elif isinstance(all_duration, timedelta):
self.all_duration = all_duration
self.target_time = datetime.now() + self.all_duration
logger.info(f"Timer set to {self.all_duration} seconds and counting down.")
self.started = True
return None
def restart_by_remain_time(self) -> None:
if self._remain_time_duration is not None:
self.target_time = datetime.now() + self._remain_time_duration
self.started = True
logger.info(f"Timer restarted with remaining time: {self._remain_time_duration}")
else:
logger.warning("No remaining time to restart the timer.")
return None
def add_duration(self, duration: timedelta) -> None:
if self.started and self.target_time is not None:
logger.info(f"Adding {duration} to the timer. Currently {self.remain_time()} remains.")
self.target_time = self.target_time + duration
self.update_remain_time()
def is_timeout(self) -> bool:
if self.started and self.target_time is not None:
self.update_remain_time()
if datetime.now() > self.target_time:
return True
return False
def update_remain_time(self) -> None:
if self.started and self.target_time is not None:
self._remain_time_duration = self.target_time - datetime.now()
return None
def remain_time(self) -> timedelta | None:
if self.started:
self.update_remain_time()
return self._remain_time_duration
return None
class RDAgentTimerWrapper(SingletonBaseClass):
def __init__(self) -> None:
self.timer: RDAgentTimer = RDAgentTimer()
self.api_fail_count: int = 0
self.latest_api_fail_time: datetime | None = None
def replace_timer(self, timer: RDAgentTimer) -> None:
self.timer = timer
logger.info("Timer replaced successfully.")
RD_Agent_TIMER_wrapper: RDAgentTimerWrapper = RDAgentTimerWrapper()
-7
View File
@@ -1,7 +0,0 @@
"""
UI is a kind of view for user.
We are not sure how generality of the UI, we can't make decision among following options:
- in general folder like rdagent/log/ui
- It is for specific scenario rdagent/scenarios/
"""
-53
View File
@@ -1,53 +0,0 @@
# %%
import json
from pathlib import Path
import streamlit as st
from rdagent.log.ui.conf import UI_SETTING
from rdagent.utils.repo.diff import generate_diff_from_dict
aide_path = UI_SETTING.aide_path
if not Path(aide_path).exists():
st.error(f"Path {aide_path} does not exist, set it by `UI_AIDE_PATH`")
st.stop()
jps = [str(i) for i in Path(aide_path).rglob("**/filtered_journal.json")]
jps = sorted(jps)
# st.write(jps)
left, right = st.columns([1, 4])
with left:
default = 0
ppp = f"{aide_path}/{st.query_params.get('jnp')}/logs/filtered_journal.json"
if ppp in jps:
default = jps.index(ppp)
jnp = st.radio("Select Journal", options=jps, index=default, format_func=lambda x: str(x).split("/")[-3])
jnp = Path(jnp)
with jnp.open("r") as f:
d = json.load(f)
# with jnp_.open("r") as f:
# d1 = json.load(f)
nm = {nd["id"]: nd for nd in d["nodes"]}
# %%
with right:
st.header("AIDE trace", divider="rainbow")
st.subheader(jnp)
for c, p in d["node2parent"].items():
f = nm[p]
t = nm[c]
df_lines = generate_diff_from_dict({"aide.py": f["code"]}, {"aide.py": t["code"]})
with st.expander(f"Node {p} -> {c}"):
st.markdown(f"## Parent ({f['metric']['value']}) Analysis")
st.code(f["analysis"], wrap_lines=True)
st.markdown(f"## Child ({t['metric']['value']}) Plan")
st.code(t["plan"], wrap_lines=True)
st.markdown("## Diff")
st.code("".join(df_lines), language="diff", wrap_lines=True)
# print("".join(df_lines))
# %%
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
from pydantic_settings import SettingsConfigDict
from rdagent.core.conf import ExtendedBaseSettings
class UIBasePropSetting(ExtendedBaseSettings):
model_config = SettingsConfigDict(env_prefix="UI_", protected_namespaces=())
default_log_folders: list[str] = ["./log"]
baseline_result_path: str = "./baseline.csv"
aide_path: str = "./aide"
amlt_path: str = "/data/share_folder_local/amlt"
static_path: str = "./git_ignore_folder/static"
trace_folder: str = "./git_ignore_folder/traces"
enable_cache: bool = True
UI_SETTING = UIBasePropSetting()
-208
View File
@@ -1,208 +0,0 @@
"""
Please refer to rdagent/log/ui/utils.py:get_summary_df for more detailed documents about metrics
"""
import re
from pathlib import Path
import pandas as pd
import plotly.express as px
import streamlit as st
from streamlit import session_state as state
from rdagent.log.ui.utils import (
ALL,
HIGH,
LITE,
MEDIUM,
curve_figure,
get_statistics_df,
get_summary_df,
lite_curve_figure,
percent_df,
)
from rdagent.scenarios.kaggle.kaggle_crawler import get_metric_direction
def curves_win(summary: dict):
# draw curves
cbwin1, cbwin2 = st.columns(2)
if cbwin1.toggle("Show Curves", key="show_curves"):
for k, v in summary.items():
with st.container(border=True):
st.markdown(f"**:blue[{k}] - :violet[{v['competition']}]**")
try:
tscores = {k: v for k, v in v["test_scores"].items()}
tscores = pd.Series(tscores)
vscores = {}
for k, vs in v["valid_scores"].items():
if not vs.index.is_unique:
st.warning(
f"Loop {k}'s valid scores index are not unique, only the last one will be kept to show."
)
st.write(vs)
vscores[k] = vs[~vs.index.duplicated(keep="last")].iloc[:, 0]
if len(vscores) > 0:
metric_name = list(vscores.values())[0].name
else:
metric_name = "None"
vscores = pd.DataFrame(vscores)
if "ensemble" in vscores.index:
ensemble_row = vscores.loc[["ensemble"]]
vscores = pd.concat([ensemble_row, vscores.drop("ensemble")])
vscores = vscores.T
vscores["test"] = tscores
vscores.index = [f"L{i}" for i in vscores.index]
vscores.columns.name = metric_name
st.plotly_chart(curve_figure(vscores))
except Exception as e:
import traceback
st.markdown("- Error: " + str(e))
st.code(traceback.format_exc())
st.markdown("- Valid Scores: ")
# st.write({k: type(v) for k, v in v["valid_scores"].items()})
st.json(v["valid_scores"])
if cbwin2.toggle("Show Curves (Lite)", key="show_curves_lite"):
st.pyplot(lite_curve_figure(summary))
def all_summarize_win():
def shorten_folder_name(folder: str) -> str:
if "amlt" in folder:
return folder[folder.rfind("amlt") + 5 :].split("/")[0]
if "ep" in folder:
return folder[folder.rfind("ep") :]
return folder
selected_folders = st.multiselect(
"Show these folders",
state.log_folders,
state.log_folders,
format_func=shorten_folder_name,
)
for lf in selected_folders:
if not (Path(lf) / "summary.pkl").exists():
st.warning(
f"summary.pkl not found in **{lf}**\n\nRun:`dotenv run -- python rdagent/log/mle_summary.py grade_summary --log_folder={lf} --hours=<>`"
)
summary = {}
dfs = []
for lf in selected_folders:
s, df = get_summary_df(lf)
df.index = [f"{shorten_folder_name(lf)} - {idx}" for idx in df.index]
dfs.append(df)
summary.update({f"{shorten_folder_name(lf)} - {k}": v for k, v in s.items()})
base_df = pd.concat(dfs)
valid_rate = float(base_df.get("Valid Improve", pd.Series()).mean())
test_rate = float(base_df.get("Test Improve", pd.Series()).mean())
submit_merge_rate = float(base_df.get("Submit Merge", pd.Series()).mean())
merge_sota_avg = float(base_df.get("Merge Sota", pd.Series()).mean())
base_df = percent_df(base_df)
base_df.insert(0, "Select", True)
bt1, bt2 = st.columns(2)
select_lite_level = bt2.selectbox(
"Select MLE-Bench Competitions Level",
options=["ALL", "HIGH", "MEDIUM", "LITE"],
index=0,
key="select_lite_level",
)
if select_lite_level != "ALL":
if select_lite_level == "HIGH":
lite_set = set(HIGH)
elif select_lite_level == "MEDIUM":
lite_set = set(MEDIUM)
elif select_lite_level == "LITE":
lite_set = set(LITE)
else:
lite_set = set()
base_df["Select"] = base_df["Competition"].isin(lite_set)
else:
base_df["Select"] = True # select all if ALL is chosen
if bt1.toggle("Select Best", key="select_best"):
def apply_func(cdf: pd.DataFrame):
cp = base_df.loc[cdf.index[0], "Competition"]
md = get_metric_direction(cp)
# If SOTA Exp Score (valid, to_submit) column is empty, return the first index
if cdf["SOTA Exp Score (valid, to_submit)"].dropna().empty:
return cdf.index[0]
if md:
best_idx = cdf["SOTA Exp Score (valid, to_submit)"].idxmax()
else:
best_idx = cdf["SOTA Exp Score (valid, to_submit)"].idxmin()
return best_idx
best_idxs = base_df.groupby("Competition").apply(apply_func, include_groups=False)
base_df["Select"] = base_df.index.isin(best_idxs.values)
base_df = st.data_editor(
base_df,
column_config={
"Select": st.column_config.CheckboxColumn("Select", help="Stat this trace.", disabled=False),
},
disabled=(col for col in base_df.columns if col not in ["Select"]),
)
st.markdown("Ours vs Base: `math.exp(abs(math.log(sota_exp_score / baseline_score)))`")
# 统计选择的比赛
base_df = base_df[base_df["Select"]]
st.markdown(f"**统计的比赛数目: :red[{base_df.shape[0]}]**")
stat_win_left, stat_win_right = st.columns(2)
with stat_win_left:
stat_df = get_statistics_df(base_df)
st.dataframe(stat_df.round(2))
markdown_table = f"""
| xxx | {stat_df.iloc[0,1]:.1f} | {stat_df.iloc[1,1]:.1f} | {stat_df.iloc[2,1]:.1f} | {stat_df.iloc[3,1]:.1f} | {stat_df.iloc[4,1]:.1f} | {stat_df.iloc[5,1]:.1f} | {stat_df.iloc[6,1]:.1f} |
| Valid Improve {valid_rate * 100:.2f}% | Test Improve {test_rate * 100:.2f}% | Submit Merge {submit_merge_rate * 100:.2f}% | Merge Sota {merge_sota_avg * 100:.2f}% |
"""
st.text(markdown_table)
with stat_win_right:
Loop_counts = base_df["Total Loops"]
# Create histogram
fig = px.histogram(
Loop_counts, nbins=15, title="Distribution of Total Loops", color_discrete_sequence=["#3498db"]
)
fig.update_layout(title_font_size=16, title_font_color="#2c3e50")
# Calculate statistics
mean_value = Loop_counts.mean()
median_value = Loop_counts.median()
# Add mean and median lines
fig.add_vline(x=mean_value, line_color="#e74c3c", line_width=3)
fig.add_vline(x=median_value, line_color="#f39c12", line_width=3)
fig.add_annotation(
x=0.02,
y=0.95,
xref="paper",
yref="paper",
text=f"<span style='color:#e74c3c; font-weight:bold'>Mean: {mean_value:.1f}</span><br><span style='color:#f39c12; font-weight:bold'>Median: {median_value:.1f}</span>",
showarrow=False,
bgcolor="rgba(255,255,255,0.9)",
bordercolor="rgba(128,128,128,0.5)",
borderwidth=1,
font=dict(size=12, color="#333333"),
)
st.plotly_chart(fig, use_container_width=True)
# write curve
st.subheader("Curves", divider="rainbow")
curves_win(summary)
with st.container(border=True):
try:
all_summarize_win()
except Exception as e:
import traceback
st.error(f"Error occurred when show summary:\n{e}")
st.code(traceback.format_exc())
File diff suppressed because it is too large Load Diff
-172
View File
@@ -1,172 +0,0 @@
import json
import pickle
import time
from datetime import datetime, timedelta
from pathlib import Path
import streamlit as st
from streamlit import session_state as state
from rdagent.app.data_science.conf import DS_RD_SETTING
st.set_page_config(layout="wide", page_title="RD-Agent_user_interact", page_icon="🎓", initial_sidebar_state="expanded")
# 初始化session state
if "sessions" not in state:
state.sessions = {}
if "selected_session_name" not in state:
state.selected_session_name = None
def render_main_content():
"""渲染主要内容区域"""
if state.selected_session_name is not None and state.selected_session_name in state.sessions:
selected_session_data = state.sessions[state.selected_session_name]
if selected_session_data is not None:
st.title(
f"Session: {state.selected_session_name[:4]} with competition {selected_session_data['competition']}"
)
st.title("Contextual Information:")
st.subheader("Competition scenario:", divider=True)
scenario = st.code(selected_session_data["scenario_description"], language="yaml")
st.subheader("Former attempts summary:", divider=True)
scenario = st.code(selected_session_data["ds_trace_desc"], language="yaml")
if selected_session_data["current_code"] != "":
st.subheader("Current SOTA code", divider=True)
scenario = st.code(
body=selected_session_data["current_code"],
language="python",
)
st.subheader("Hypothesis candidates:", divider=True)
hypothesis_candidates = selected_session_data["hypothesis_candidates"]
tabs = st.tabs(
[
f"{'' if i == selected_session_data['target_hypothesis_index'] or selected_session_data['target_hypothesis_index'] == -1 else ''}Hypothesis {i+1}"
for i in range(len(hypothesis_candidates))
]
)
for index, hypothesis in enumerate(hypothesis_candidates):
with tabs[index]:
st.code(str(hypothesis), language="yaml")
st.text("✅ means picked as target hypothesis")
st.title("Decisions to make:")
with st.form(key="user_form"):
st.caption("Please modify the fields below and submit to provide your feedback.")
target_hypothesis = st.text_area(
"Target hypothesis: (you can copy from candidates)",
value=(original_hypothesis := selected_session_data["target_hypothesis"].hypothesis),
height="content",
)
target_task = st.text_area(
"Target task description:",
value=(original_task_desc := selected_session_data["task"].description),
height="content",
)
original_user_instruction = selected_session_data.get("user_instruction")
user_instruction_list = []
if selected_session_data.get("former_user_instructions") is not None:
st.caption(
"Former user instructions, you can modify or delete the content to remove certain instruction."
)
for user_instruction in selected_session_data.get("former_user_instructions"):
user_instruction_list.append(
st.text_area("Former user instruction", value=user_instruction, height="content")
)
user_instruction_list.append(st.text_area("Add new user instruction", value="", height="content"))
submit = st.form_submit_button("Submit")
approve = st.form_submit_button("Approve without changes")
if submit or approve:
if approve:
submit_dict = {
"action": "confirm",
}
else:
user_instruction_str_list = [ui for ui in user_instruction_list if ui.strip() != ""]
user_instruction_str_list = (
None if len(user_instruction_str_list) == 0 else user_instruction_str_list
)
action = (
"confirm"
if target_hypothesis == original_hypothesis
and target_task == original_task_desc
and user_instruction_str_list == original_user_instruction
else "rewrite"
)
submit_dict = {
"target_hypothesis": target_hypothesis,
"task_description": target_task,
"user_instruction": user_instruction_str_list,
"action": action,
}
json.dump(
submit_dict,
open(
DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}_RET.json", "w"
),
)
Path(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl").unlink(
missing_ok=True
)
st.success("Your feedback has been submitted. Thank you!")
time.sleep(5)
state.selected_session_name = None
if st.button("Extend expiration by 60s"):
session_data = pickle.load(
open(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl", "rb")
)
session_data["expired_datetime"] = session_data["expired_datetime"] + timedelta(seconds=60)
pickle.dump(
session_data,
open(DS_RD_SETTING.user_interaction_mid_folder / f"{state.selected_session_name}.pkl", "wb"),
)
else:
st.warning("Please select a session from the sidebar.")
# 每秒更新一次sessions
@st.fragment(run_every=1)
def update_sessions():
log_folder = Path(DS_RD_SETTING.user_interaction_mid_folder)
state.sessions = {}
for session_file in log_folder.glob("*.pkl"):
try:
session_data = pickle.load(open(session_file, "rb"))
if session_data["expired_datetime"] > datetime.now():
state.sessions[session_file.stem] = session_data
else:
session_file.unlink(missing_ok=True)
ret_file = log_folder / f"{session_file.stem}_RET.json"
ret_file.unlink(missing_ok=True)
except Exception as e:
continue
render_main_content()
@st.fragment(run_every=1)
def render_sidebar():
st.title("R&D-Agent User Interaction Portal")
if state.sessions:
st.header("Active Sessions")
st.caption("Click a session to view:")
session_names = [name for name in state.sessions]
for session_name in session_names:
with st.container(border=True):
remaining = state.sessions[session_name]["expired_datetime"] - datetime.now()
total_sec = int(remaining.total_seconds())
label = f"{total_sec}s to expire" if total_sec > 0 else "Expired"
if st.button(f"session id:{session_name[:4]}", key=f"session_btn_{session_name}"):
state.selected_session_name = session_name
state.data = state.sessions[session_name]
st.markdown(f"{label}")
else:
st.warning("No active sessions available. Please wait.")
update_sessions()
with st.sidebar:
render_sidebar()
-51
View File
@@ -1,51 +0,0 @@
from pathlib import Path
import streamlit as st
from streamlit import session_state as state
from rdagent.app.data_science.loop import DataScienceRDLoop
from rdagent.log.ui.conf import UI_SETTING
def convert_log_folder_str(lf: str) -> str:
if "/" not in lf:
return f"{UI_SETTING.amlt_path}/{lf.strip()}/combined_logs"
return lf.strip()
def extract_amlt_name(x: str) -> str:
if "amlt" not in x:
return x
return x[x.rfind("amlt") + 5 :].split("/")[0]
# 设置主日志路径
if "log_folder" not in state:
state.log_folder = Path("./log")
if "log_folders" not in state:
state.log_folders = [convert_log_folder_str(i) for i in UI_SETTING.default_log_folders]
summary_page = st.Page("ds_summary.py", title="Summary", icon="📊")
trace_page = st.Page("ds_trace.py", title="Trace", icon="📈")
aide_page = st.Page("aide.py", title="Aide", icon="🧑‍🏫")
st.set_page_config(layout="wide", page_title="RD-Agent", page_icon="🎓", initial_sidebar_state="expanded")
st.navigation([summary_page, trace_page, aide_page]).run()
# UI - Sidebar
with st.sidebar:
st.subheader("Pages", divider="rainbow")
st.page_link(summary_page, icon="📊")
st.page_link(trace_page, icon="📈")
st.page_link(aide_page, icon="🧑‍🏫")
st.subheader("Settings", divider="rainbow")
with st.form("log_folder_form", border=False):
log_folder_str = st.text_area(
"**Log Folders**(split by ';')", value=";".join(extract_amlt_name(i) for i in state.log_folders)
)
if st.form_submit_button("Confirm"):
state.log_folders = [
convert_log_folder_str(folder) for folder in log_folder_str.split(";") if folder.strip()
]
st.rerun()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

-306
View File
@@ -1,306 +0,0 @@
import argparse
import json
import pickle
import re
import time
from pathlib import Path
import streamlit as st
from streamlit import session_state
from rdagent.log.ui.conf import UI_SETTING
from rdagent.log.utils import extract_evoid, extract_loopid_func_name
st.set_page_config(layout="wide", page_title="debug_llm", page_icon="🎓", initial_sidebar_state="expanded")
# 获取 log_path 参数
parser = argparse.ArgumentParser(description="RD-Agent Streamlit App")
parser.add_argument("--log_dir", type=str, help="Path to the log directory")
args = parser.parse_args()
def get_folders_sorted(log_path):
"""缓存并返回排序后的文件夹列表,并加入进度打印"""
with st.spinner("正在加载文件夹列表..."):
folders = sorted(
(folder for folder in log_path.iterdir() if folder.is_dir() and list(folder.iterdir())),
key=lambda folder: folder.stat().st_mtime,
reverse=True,
)
st.write(f"找到 {len(folders)} 个文件夹")
return [folder.name for folder in folders]
if UI_SETTING.enable_cache:
get_folders_sorted = st.cache_data(get_folders_sorted)
# 设置主日志路径
main_log_path = Path(args.log_dir) if args.log_dir else Path("./log")
if not main_log_path.exists():
st.error(f"Log dir {main_log_path} does not exist!")
st.stop()
if "data" not in session_state:
session_state.data = []
if "log_path" not in session_state:
session_state.log_path = None
tlist = []
def load_data():
"""加载数据到 session_state 并显示进度"""
log_file = main_log_path / session_state.log_path / "debug_llm.pkl"
try:
with st.spinner(f"正在加载数据文件 {log_file}..."):
start_time = time.time()
with open(log_file, "rb") as f:
session_state.data = pickle.load(f)
st.success(f"数据加载完成!耗时 {time.time() - start_time:.2f}")
st.session_state["current_loop"] = 1
except Exception as e:
session_state.data = [{"error": str(e)}]
st.error(f"加载数据失败: {e}")
# UI - Sidebar
with st.sidebar:
st.markdown(":blue[**Log Path**]")
manually = st.toggle("Manual Input")
if manually:
st.text_input("log path", key="log_path", label_visibility="collapsed")
else:
folders = get_folders_sorted(main_log_path)
st.selectbox(f"**Select from {main_log_path.absolute()}**", folders, key="log_path")
if st.button("Refresh Data"):
load_data()
st.rerun()
# Helper functions
def show_text(text, lang=None):
"""显示文本代码块"""
if lang:
st.code(text, language=lang, wrap_lines=True)
elif "\n" in text:
st.code(text, language="python", wrap_lines=True)
else:
st.code(text, language="html", wrap_lines=True)
def highlight_prompts_uri(uri):
"""高亮 URI 的格式"""
parts = uri.split(":")
return f"**{parts[0]}:**:green[**{parts[1]}**]"
# Display Data
progress_text = st.empty()
progress_bar = st.progress(0)
# 每页展示一个 Loop
LOOPS_PER_PAGE = 1
# 获取所有的 Loop ID
loop_groups = {}
for i, d in enumerate(session_state.data):
tag = d["tag"]
loop_id, _ = extract_loopid_func_name(tag)
if loop_id:
if loop_id not in loop_groups:
loop_groups[loop_id] = []
loop_groups[loop_id].append(d)
# 按 Loop ID 排序
sorted_loop_ids = sorted(loop_groups.keys(), key=int) # 假设 Loop ID 是数字
total_loops = len(sorted_loop_ids)
total_pages = total_loops # 每页展示一个 Loop
# simple display
# FIXME: Delete this simple UI if trace have tag(evo_id & loop_id)
# with st.sidebar:
# start = int(st.text_input("start", 0))
# end = int(st.text_input("end", 100))
# for m in session_state.data[start:end]:
# if "tpl" in m["tag"]:
# obj = m["obj"]
# uri = obj["uri"]
# tpl = obj["template"]
# cxt = obj["context"]
# rd = obj["rendered"]
# with st.expander(highlight_prompts_uri(uri), expanded=False, icon="⚙️"):
# t1, t2, t3 = st.tabs([":green[**Rendered**]", ":blue[**Template**]", ":orange[**Context**]"])
# with t1:
# show_text(rd)
# with t2:
# show_text(tpl, lang="django")
# with t3:
# st.json(cxt)
# if "llm" in m["tag"]:
# obj = m["obj"]
# system = obj.get("system", None)
# user = obj["user"]
# resp = obj["resp"]
# with st.expander(f"**LLM**", expanded=False, icon="🤖"):
# t1, t2, t3 = st.tabs([":green[**Response**]", ":blue[**User**]", ":orange[**System**]"])
# with t1:
# try:
# rdict = json.loads(resp)
# if "code" in rdict:
# code = rdict["code"]
# st.markdown(":red[**Code in response dict:**]")
# st.code(code, language="python", wrap_lines=True, line_numbers=True)
# rdict.pop("code")
# elif "spec" in rdict:
# spec = rdict["spec"]
# st.markdown(":red[**Spec in response dict:**]")
# st.markdown(spec)
# rdict.pop("spec")
# else:
# # show model codes
# showed_keys = []
# for k, v in rdict.items():
# if k.startswith("model_") and k.endswith(".py"):
# st.markdown(f":red[**{k}**]")
# st.code(v, language="python", wrap_lines=True, line_numbers=True)
# showed_keys.append(k)
# for k in showed_keys:
# rdict.pop(k)
# st.write(":red[**Other parts (except for the code or spec) in response dict:**]")
# st.json(rdict)
# except:
# st.json(resp)
# with t2:
# show_text(user)
# with t3:
# show_text(system or "No system prompt available")
if total_pages:
# 初始化 current_loop
if "current_loop" not in st.session_state:
st.session_state["current_loop"] = 1
# Loop 导航按钮
col1, col2, col3, col4, col5 = st.sidebar.columns([1.2, 1, 2, 1, 1.2])
with col1:
if st.button("|<"): # 首页
st.session_state["current_loop"] = 1
with col2:
if st.button("<") and st.session_state["current_loop"] > 1: # 上一页
st.session_state["current_loop"] -= 1
with col3:
# 下拉列表显示所有 Loop
st.session_state["current_loop"] = st.selectbox(
"选择 Loop",
options=list(range(1, total_loops + 1)),
index=st.session_state["current_loop"] - 1, # 默认选中当前 Loop
label_visibility="collapsed", # 隐藏标签
)
with col4:
if st.button("\>") and st.session_state["current_loop"] < total_loops: # 下一页
st.session_state["current_loop"] += 1
with col5:
if st.button("\>|"): # 最后一页
st.session_state["current_loop"] = total_loops
# 获取当前 Loop
current_loop = st.session_state["current_loop"]
# 渲染当前 Loop 数据
loop_id = sorted_loop_ids[current_loop - 1]
progress_text = st.empty()
progress_text.text(f"正在处理 Loop {loop_id}...")
progress_bar.progress(current_loop / total_loops, text=f"Loop :green[**{current_loop}**] / {total_loops}")
# 渲染 Loop Header
loop_anchor = f"Loop_{loop_id}"
if loop_anchor not in tlist:
tlist.append(loop_anchor)
st.header(loop_anchor, anchor=loop_anchor, divider="blue")
# 渲染当前 Loop 的所有数据
loop_data = loop_groups[loop_id]
for d in loop_data:
tag = d["tag"]
obj = d["obj"]
_, func_name = extract_loopid_func_name(tag)
evo_id = extract_evoid(tag)
func_anchor = f"loop_{loop_id}.{func_name}"
if func_anchor not in tlist:
tlist.append(func_anchor)
st.header(f"in *{func_name}*", anchor=func_anchor, divider="green")
evo_anchor = f"loop_{loop_id}.evo_step_{evo_id}"
if evo_id and evo_anchor not in tlist:
tlist.append(evo_anchor)
st.subheader(f"evo_step_{evo_id}", anchor=evo_anchor, divider="orange")
# 根据 tag 渲染内容
if "debug_exp_gen" in tag:
with st.expander(
f"Exp in :violet[**{obj.experiment_workspace.workspace_path}**]", expanded=False, icon="🧩"
):
st.write(obj)
elif "debug_tpl" in tag:
uri = obj["uri"]
tpl = obj["template"]
cxt = obj["context"]
rd = obj["rendered"]
with st.expander(highlight_prompts_uri(uri), expanded=False, icon="⚙️"):
t1, t2, t3 = st.tabs([":green[**Rendered**]", ":blue[**Template**]", ":orange[**Context**]"])
with t1:
show_text(rd)
with t2:
show_text(tpl, lang="django")
with t3:
st.json(cxt)
elif "debug_llm" in tag:
system = obj.get("system", None)
user = obj["user"]
resp = obj["resp"]
with st.expander(f"**LLM**", expanded=False, icon="🤖"):
t1, t2, t3 = st.tabs([":green[**Response**]", ":blue[**User**]", ":orange[**System**]"])
with t1:
try:
rdict = json.loads(resp)
if "code" in rdict:
code = rdict["code"]
st.markdown(":red[**Code in response dict:**]")
st.code(code, language="python", wrap_lines=True, line_numbers=True)
rdict.pop("code")
elif "spec" in rdict:
spec = rdict["spec"]
st.markdown(":red[**Spec in response dict:**]")
st.markdown(spec)
rdict.pop("spec")
else:
# show model codes
showed_keys = []
for k, v in rdict.items():
if k.startswith("model_") and k.endswith(".py"):
st.markdown(f":red[**{k}**]")
st.code(v, language="python", wrap_lines=True, line_numbers=True)
showed_keys.append(k)
for k in showed_keys:
rdict.pop(k)
st.write(":red[**Other parts (except for the code or spec) in response dict:**]")
st.json(rdict)
except:
st.json(resp)
with t2:
show_text(user)
with t3:
show_text(system or "No system prompt available")
progress_text.text("当前 Loop 数据处理完成!")
# Sidebar TOC
with st.sidebar:
toc = "\n".join([f"- [{t}](#{t})" if t.startswith("L") else f" - [{t.split('.')[1]}](#{t})" for t in tlist])
st.markdown(toc, unsafe_allow_html=True)
-445
View File
@@ -1,445 +0,0 @@
import importlib
import math
import pandas as pd
import plotly.graph_objs as go
from plotly.subplots import make_subplots
class BaseGraph:
_name = None
def __init__(
self, df: pd.DataFrame = None, layout: dict = None, graph_kwargs: dict = None, name_dict: dict = None, **kwargs
):
"""
:param df:
:param layout:
:param graph_kwargs:
:param name_dict:
:param kwargs:
layout: dict
go.Layout parameters
graph_kwargs: dict
Graph parameters, eg: go.Bar(**graph_kwargs)
"""
self._df = df
self._layout = dict() if layout is None else layout
self._graph_kwargs = dict() if graph_kwargs is None else graph_kwargs
self._name_dict = name_dict
self.data = None
self._init_parameters(**kwargs)
self._init_data()
def _init_data(self):
"""
:return:
"""
if self._df.empty:
raise ValueError("df is empty.")
self.data = self._get_data()
def _init_parameters(self, **kwargs):
"""
:param kwargs
"""
# Instantiate graphics parameters
self._graph_type = self._name.lower().capitalize()
# Displayed column name
if self._name_dict is None:
self._name_dict = {_item: _item for _item in self._df.columns}
@staticmethod
def get_instance_with_graph_parameters(graph_type: str = None, **kwargs):
"""
:param graph_type:
:param kwargs:
:return:
"""
try:
_graph_module = importlib.import_module("plotly.graph_objs")
_graph_class = getattr(_graph_module, graph_type)
except AttributeError:
_graph_module = importlib.import_module("qlib.contrib.report.graph")
_graph_class = getattr(_graph_module, graph_type)
return _graph_class(**kwargs)
def _get_layout(self) -> go.Layout:
"""
:return:
"""
return go.Layout(**self._layout)
def _get_data(self) -> list:
"""
:return:
"""
_data = [
self.get_instance_with_graph_parameters(
graph_type=self._graph_type, x=self._df.index, y=self._df[_col], name=_name, **self._graph_kwargs
)
for _col, _name in self._name_dict.items()
]
return _data
@property
def figure(self) -> go.Figure:
"""
:return:
"""
_figure = go.Figure(data=self.data, layout=self._get_layout())
# NOTE: Use the default theme from plotly version 3.x, template=None
_figure["layout"].update(template=None)
return _figure
class SubplotsGraph:
"""Create subplots same as df.plot(subplots=True)
Simple package for `plotly.tools.subplots`
"""
def __init__(
self,
df: pd.DataFrame = None,
kind_map: dict = None,
layout: dict = None,
sub_graph_layout: dict = None,
sub_graph_data: list = None,
subplots_kwargs: dict = None,
**kwargs,
):
"""
:param df: pd.DataFrame
:param kind_map: dict, subplots graph kind and kwargs
eg: dict(kind='Scatter', kwargs=dict())
:param layout: `go.Layout` parameters
:param sub_graph_layout: Layout of each graphic, similar to 'layout'
:param sub_graph_data: Instantiation parameters for each sub-graphic
eg: [(column_name, instance_parameters), ]
column_name: str or go.Figure
Instance_parameters:
- row: int, the row where the graph is located
- col: int, the col where the graph is located
- name: str, show name, default column_name in 'df'
- kind: str, graph kind, default `kind` param, eg: bar, scatter, ...
- graph_kwargs: dict, graph kwargs, default {}, used in `go.Bar(**graph_kwargs)`
:param subplots_kwargs: `plotly.tools.make_subplots` original parameters
- shared_xaxes: bool, default False
- shared_yaxes: bool, default False
- vertical_spacing: float, default 0.3 / rows
- subplot_titles: list, default []
If `sub_graph_data` is None, will generate 'subplot_titles' according to `df.columns`,
this field will be discarded
- specs: list, see `make_subplots` docs
- rows: int, Number of rows in the subplot grid, default 1
If `sub_graph_data` is None, will generate 'rows' according to `df`, this field will be discarded
- cols: int, Number of cols in the subplot grid, default 1
If `sub_graph_data` is None, will generate 'cols' according to `df`, this field will be discarded
:param kwargs:
"""
self._df = df
self._layout = layout
self._sub_graph_layout = sub_graph_layout
self._kind_map = kind_map
if self._kind_map is None:
self._kind_map = dict(kind="Scatter", kwargs=dict())
self._subplots_kwargs = subplots_kwargs
if self._subplots_kwargs is None:
self._init_subplots_kwargs()
self.__cols = self._subplots_kwargs.get("cols", 2) # pylint: disable=W0238
self.__rows = self._subplots_kwargs.get( # pylint: disable=W0238
"rows", math.ceil(len(self._df.columns) / self.__cols)
)
self._sub_graph_data = sub_graph_data
if self._sub_graph_data is None:
self._init_sub_graph_data()
self._init_figure()
def _init_sub_graph_data(self):
"""
:return:
"""
self._sub_graph_data = []
self._subplot_titles = []
for i, column_name in enumerate(self._df.columns):
row = math.ceil((i + 1) / self.__cols)
_temp = (i + 1) % self.__cols
col = _temp if _temp else self.__cols
res_name = column_name.replace("_", " ")
_temp_row_data = (
column_name,
dict(
row=row,
col=col,
name=res_name,
kind=self._kind_map["kind"],
graph_kwargs=self._kind_map["kwargs"],
),
)
self._sub_graph_data.append(_temp_row_data)
self._subplot_titles.append(res_name)
def _init_subplots_kwargs(self):
"""
:return:
"""
# Default cols, rows
_cols = 2
_rows = math.ceil(len(self._df.columns) / 2)
self._subplots_kwargs = dict()
self._subplots_kwargs["rows"] = _rows
self._subplots_kwargs["cols"] = _cols
self._subplots_kwargs["shared_xaxes"] = False
self._subplots_kwargs["shared_yaxes"] = False
self._subplots_kwargs["vertical_spacing"] = 0.3 / _rows
self._subplots_kwargs["print_grid"] = False
self._subplots_kwargs["subplot_titles"] = self._df.columns.tolist()
def _init_figure(self):
"""
:return:
"""
self._figure = make_subplots(**self._subplots_kwargs)
for column_name, column_map in self._sub_graph_data:
if isinstance(column_name, go.Figure):
_graph_obj = column_name
elif isinstance(column_name, str):
temp_name = column_map.get("name", column_name.replace("_", " "))
kind = column_map.get("kind", self._kind_map.get("kind", "Scatter"))
_graph_kwargs = column_map.get("graph_kwargs", self._kind_map.get("kwargs", {}))
_graph_obj = BaseGraph.get_instance_with_graph_parameters(
kind,
**dict(
x=self._df.index,
y=self._df[column_name],
name=temp_name,
**_graph_kwargs,
),
)
else:
raise TypeError()
row = column_map["row"]
col = column_map["col"]
self._figure.add_trace(_graph_obj, row=row, col=col)
if self._sub_graph_layout is not None:
for k, v in self._sub_graph_layout.items():
self._figure["layout"][k].update(v)
# NOTE: Use the default theme from plotly version 3.x: template=None
self._figure["layout"].update(template=None)
self._figure["layout"].update(self._layout)
@property
def figure(self):
return self._figure
def _calculate_maximum(df: pd.DataFrame, is_ex: bool = False):
"""
:param df:
:param is_ex:
:return:
"""
if is_ex:
end_date = df["cum_ex_return_wo_cost_mdd"].idxmin()
start_date = df.loc[df.index <= end_date]["cum_ex_return_wo_cost"].idxmax()
else:
end_date = df["return_wo_mdd"].idxmin()
start_date = df.loc[df.index <= end_date]["cum_return_wo_cost"].idxmax()
return start_date, end_date
def _calculate_mdd(series):
"""
Calculate mdd
:param series:
:return:
"""
return series - series.cummax()
def _calculate_report_data(raw_df: pd.DataFrame) -> pd.DataFrame:
"""
:param df:
:return:
"""
df = raw_df.copy(deep=True)
index_names = df.index.names
df.index = df.index.strftime("%Y-%m-%d")
report_df = pd.DataFrame()
report_df["cum_bench"] = df["bench"].cumsum()
report_df["cum_return_wo_cost"] = df["return"].cumsum()
report_df["cum_return_w_cost"] = (df["return"] - df["cost"]).cumsum()
# report_df['cum_return'] - report_df['cum_return'].cummax()
report_df["return_wo_mdd"] = _calculate_mdd(report_df["cum_return_wo_cost"])
report_df["return_w_cost_mdd"] = _calculate_mdd((df["return"] - df["cost"]).cumsum())
report_df["cum_ex_return_wo_cost"] = (df["return"] - df["bench"]).cumsum()
report_df["cum_ex_return_w_cost"] = (df["return"] - df["bench"] - df["cost"]).cumsum()
report_df["cum_ex_return_wo_cost_mdd"] = _calculate_mdd((df["return"] - df["bench"]).cumsum())
report_df["cum_ex_return_w_cost_mdd"] = _calculate_mdd((df["return"] - df["cost"] - df["bench"]).cumsum())
# return_wo_mdd , return_w_cost_mdd, cum_ex_return_wo_cost_mdd, cum_ex_return_w
report_df["turnover"] = df["turnover"]
report_df.sort_index(ascending=True, inplace=True)
report_df.index.names = index_names
return report_df
def report_figure(df: pd.DataFrame) -> list | tuple:
"""
:param df:
:return:
"""
# Get data
report_df = _calculate_report_data(df)
# Maximum Drawdown
max_start_date, max_end_date = _calculate_maximum(report_df)
ex_max_start_date, ex_max_end_date = _calculate_maximum(report_df, True)
index_name = report_df.index.name
_temp_df = report_df.reset_index()
_temp_df.loc[-1] = 0
_temp_df = _temp_df.shift(1)
_temp_df.loc[0, index_name] = "T0"
_temp_df.set_index(index_name, inplace=True)
_temp_df.iloc[0] = 0
report_df = _temp_df
# Create figure
_default_kind_map = dict(kind="Scatter", kwargs={"mode": "lines+markers"})
_temp_fill_args = {"fill": "tozeroy", "mode": "lines+markers"}
_column_row_col_dict = [
("cum_bench", dict(row=1, col=1)),
("cum_return_wo_cost", dict(row=1, col=1)),
("cum_return_w_cost", dict(row=1, col=1)),
("return_wo_mdd", dict(row=2, col=1, graph_kwargs=_temp_fill_args)),
("return_w_cost_mdd", dict(row=3, col=1, graph_kwargs=_temp_fill_args)),
("cum_ex_return_wo_cost", dict(row=4, col=1)),
("cum_ex_return_w_cost", dict(row=4, col=1)),
("turnover", dict(row=5, col=1)),
("cum_ex_return_w_cost_mdd", dict(row=6, col=1, graph_kwargs=_temp_fill_args)),
("cum_ex_return_wo_cost_mdd", dict(row=7, col=1, graph_kwargs=_temp_fill_args)),
]
_subplot_layout = dict()
for i in range(1, 8):
# yaxis
_subplot_layout.update({"yaxis{}".format(i): dict(zeroline=True, showline=True, showticklabels=True)})
_show_line = i == 7
_subplot_layout.update({"xaxis{}".format(i): dict(showline=_show_line, type="category", tickangle=45)})
_layout_style = dict(
height=1200,
title=" ",
shapes=[
{
"type": "rect",
"xref": "x",
"yref": "paper",
"x0": max_start_date,
"y0": 0.55,
"x1": max_end_date,
"y1": 1,
"fillcolor": "#d3d3d3",
"opacity": 0.3,
"line": {
"width": 0,
},
},
{
"type": "rect",
"xref": "x",
"yref": "paper",
"x0": ex_max_start_date,
"y0": 0,
"x1": ex_max_end_date,
"y1": 0.55,
"fillcolor": "#d3d3d3",
"opacity": 0.3,
"line": {
"width": 0,
},
},
],
)
_subplot_kwargs = dict(
shared_xaxes=True,
vertical_spacing=0.01,
rows=7,
cols=1,
row_width=[1, 1, 1, 3, 1, 1, 3],
print_grid=False,
)
figure = SubplotsGraph(
df=report_df,
layout=_layout_style,
sub_graph_data=_column_row_col_dict,
subplots_kwargs=_subplot_kwargs,
kind_map=_default_kind_map,
sub_graph_layout=_subplot_layout,
).figure
return figure
-126
View File
@@ -1,126 +0,0 @@
from typing import Literal
import streamlit as st
from streamlit.components.v1 import html
FIXED_CONTAINER_CSS = """
:root {{
--background-color: #ffffff; /* Default background color */
}}
div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) {{
position: {mode};
width: inherit;
background-color: inherit;
{position}: {margin};
z-index: 999;
}}
div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) div[data-testid="stVerticalBlock"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) > div[data-testid="stVerticalBlockBorderWrapper"] {{
background-color: transparent;
width: 100%;
}}
div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) div[data-testid="stVerticalBlock"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) > div[data-testid="stVerticalBlockBorderWrapper"] div[data-testid="stVerticalBlockBorderWrapper"] {{
background-color: var(--background-color);
}}
div[data-testid="stVerticalBlockBorderWrapper"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) div[data-testid="stVerticalBlock"]:has(div.fixed-container-{id}):not(:has(div.not-fixed-container)) > div[data-testid="element-container"] {{
display: none;
}}
div[data-testid="stVerticalBlockBorderWrapper"]:has(div.not-fixed-container):not(:has(div[class^='fixed-container-'])) {{
display: none;
}}
""".strip()
FIXED_CONTAINER_JS = """
const root = parent.document.querySelector('.stApp');
let lastBackgroundColor = null;
function updateContainerBackground(currentBackground) {
parent.document.documentElement.style.setProperty('--background-color', currentBackground);
;
}
function checkForBackgroundColorChange() {
const style = window.getComputedStyle(root);
const currentBackgroundColor = style.backgroundColor;
if (currentBackgroundColor !== lastBackgroundColor) {
lastBackgroundColor = currentBackgroundColor; // Update the last known value
updateContainerBackground(lastBackgroundColor);
}
}
const observerCallback = (mutationsList, observer) => {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'style')) {
checkForBackgroundColorChange();
}
}
};
const main = () => {
checkForBackgroundColorChange();
const observer = new MutationObserver(observerCallback);
observer.observe(root, { attributes: true, childList: false, subtree: false });
}
// main();
document.addEventListener("DOMContentLoaded", main);
""".strip()
MARGINS = {
"top": "2.875rem",
"bottom": "0",
}
counter = 0
def st_fixed_container(
*,
height: int | None = None,
border: bool | None = None,
mode: Literal["fixed", "sticky"] = "fixed",
position: Literal["top", "bottom"] = "top",
margin: str | None = None,
transparent: bool = False,
):
if margin is None:
margin = MARGINS[position]
global counter
fixed_container = st.container()
non_fixed_container = st.container()
css = FIXED_CONTAINER_CSS.format(
mode=mode,
position=position,
margin=margin,
id=counter,
)
with fixed_container:
html(f"<script>{FIXED_CONTAINER_JS}</script>", scrolling=False, height=0)
st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
st.markdown(
f"<div class='fixed-container-{counter}'></div>",
unsafe_allow_html=True,
)
with non_fixed_container:
st.markdown(
f"<div class='not-fixed-container'></div>",
unsafe_allow_html=True,
)
counter += 1
parent_container = fixed_container if transparent else fixed_container.container()
return parent_container.container(height=height, border=border)
if __name__ == "__main__":
for i in range(30):
st.write(f"Line {i}")
# with st_fixed_container(mode="sticky", position="top", border=True):
# with st_fixed_container(mode="sticky", position="bottom", border=True):
# with st_fixed_container(mode="fixed", position="top", border=True):
with st_fixed_container(mode="fixed", position="bottom", border=True):
st.write("This is a fixed container.")
st.write("This is a fixed container.")
st.write("This is a fixed container.")
st.container(border=True).write("This is a regular container.")
for i in range(30):
st.write(f"Line {i}")
-345
View File
@@ -1,345 +0,0 @@
from datetime import datetime
from pathlib import Path
from typing import Any, Generator
import requests
from rdagent.log.base import Message, Storage
from rdagent.log.utils import extract_evoid, extract_loopid_func_name, gen_datetime
from .conf import UI_SETTING
class WebStorage(Storage):
"""
The storage for web app.
It is used to provide the data for the web app.
"""
def __init__(self, port: int, path: str) -> None:
"""
Initializes the storage object with the specified port and identifier.
Args:
port (int): The port number to use for the storage service.
path (str): The unique identifier for local storage, the log path.
"""
self.url = f"http://localhost:{port}"
self.path = path
self.msgs = []
def __str__(self):
return f"WebStorage({self.url})"
def log(self, obj: object, tag: str, timestamp: datetime | None = None, **kwargs: Any) -> str | Path:
timestamp = gen_datetime(timestamp)
if "pdf_image" in tag or "load_pdf_screenshot" in tag:
Path(f"{UI_SETTING.static_path}/pdf_images").mkdir(parents=True, exist_ok=True)
obj.save(f"{UI_SETTING.static_path}/pdf_images/{timestamp.isoformat()}.jpg")
try:
data = self._obj_to_json(obj=obj, tag=tag, id=str(self.path), timestamp=timestamp.isoformat())
if not data:
return "Normal log, skipped"
if isinstance(data, list):
for d in data:
self.msgs.append(d)
else:
self.msgs.append(data)
headers = {"Content-Type": "application/json"}
resp = requests.post(f"{self.url}/receive", json=data, headers=headers, timeout=1)
return f"{resp.status_code} {resp.text}"
except (requests.ConnectionError, requests.Timeout) as e:
print(f"Failed to connect to the web storage server at {self.url}: {e}")
def truncate(self, time: datetime) -> None:
self.msgs = [m for m in self.msgs if datetime.fromisoformat(m["msg"]["timestamp"]) <= time]
def iter_msg(self, **kwargs: Any) -> Generator[Message, None, None]:
for msg in self.msgs:
yield Message(
tag=msg["msg"]["tag"],
level="INFO",
timestamp=datetime.fromisoformat(msg["msg"]["timestamp"]),
content=msg,
)
def _obj_to_json(
self,
obj: object,
tag: str,
id: str,
timestamp: str,
) -> list[dict] | dict:
li, fn = extract_loopid_func_name(tag)
ei = extract_evoid(tag)
data = {}
if "hypothesis generation" in tag:
from rdagent.core.proposal import Hypothesis
h: Hypothesis = obj
data = {
"id": id,
"msg": {
"tag": "research.hypothesis",
"timestamp": timestamp,
"loop_id": li,
"content": {
"hypothesis": h.hypothesis,
"reason": h.reason,
"concise_reason": h.concise_reason,
"concise_justification": h.concise_justification,
"concise_observation": h.concise_observation,
"concise_knowledge": h.concise_knowledge,
},
},
}
elif "pdf_image" in tag or "load_pdf_screenshot" in tag:
# obj.save(f"{app.static_folder}/{timestamp}.jpg")
data = {
"id": id,
"msg": {
"tag": "research.pdf_image",
"timestamp": timestamp,
"loop_id": li,
"content": {"image": f"pdf_images/{timestamp}.jpg"},
},
}
elif "experiment generation" in tag or "load_experiment" in tag:
from rdagent.components.coder.factor_coder.factor import FactorTask
from rdagent.components.coder.model_coder.model import ModelTask
if "load_experiment" in tag:
tasks: list[FactorTask | ModelTask] = obj.sub_tasks
else:
tasks: list[FactorTask | ModelTask] = obj
if isinstance(tasks[0], FactorTask):
data = {
"id": id,
"msg": {
"tag": "research.tasks",
"timestamp": timestamp,
"loop_id": li,
"content": [
{
"name": t.factor_name,
"description": t.factor_description,
"formulation": t.factor_formulation,
"variables": t.variables,
}
for t in tasks
],
},
}
elif isinstance(tasks[0], ModelTask):
data = {
"id": id,
"msg": {
"tag": "research.tasks",
"timestamp": timestamp,
"loop_id": li,
"content": [
{
"name": t.name,
"description": t.description,
"model_type": t.model_type,
"formulation": t.formulation,
"variables": t.variables,
}
for t in tasks
],
},
}
elif "direct_exp_gen" in tag:
from rdagent.scenarios.data_science.experiment.experiment import (
DSExperiment,
)
if isinstance(obj, DSExperiment):
from rdagent.scenarios.data_science.proposal.exp_gen.base import (
DSHypothesis,
)
h: DSHypothesis = obj.hypothesis
tasks = [t[0] for t in obj.pending_tasks_list]
t = tasks[0]
t.name = type(t).__name__ # TODO: PipelinTask have "COMPONENT" in name, fix this when creating task.
data = [
{
"id": id,
"msg": {
"tag": "research.hypothesis",
"old_tag": tag,
"timestamp": timestamp,
"loop_id": li,
"content": {
"name_map": {
"hypothesis": "RD-Agent proposes the hypothesis⬇️",
"concise_justification": "because the reason⬇️",
"concise_observation": "based on the observation⬇️",
"concise_knowledge": "Knowledge⬇️ gained after practice",
"no_hypothesis": f"No hypothesis available. Trying to construct the first runnable {h.component} component.",
},
"hypothesis": h.hypothesis,
"reason": h.reason,
"component": h.component,
"concise_reason": h.concise_reason,
"concise_justification": h.concise_justification,
"concise_observation": h.concise_observation,
"concise_knowledge": h.concise_knowledge,
},
},
},
{
"id": id,
"msg": {
"tag": "research.tasks",
"old_tag": tag,
"timestamp": timestamp,
"loop_id": li,
"content": [
(
{
"name": t.name,
"description": t.description,
}
if not hasattr(t, "architecture")
else {
"name": t.name,
"description": t.description,
"model_type": t.model_type,
"architecture": t.architecture,
"hyperparameters": t.hyperparameters,
}
)
],
},
},
]
elif f"evo_loop_{ei}.evolving code" in tag and "running" not in tag:
from rdagent.core.experiment import FBWorkspace
ws: list[FBWorkspace] = [i for i in obj]
data = {
"id": id,
"msg": {
"tag": "evolving.codes",
"timestamp": timestamp,
"loop_id": li,
"evo_id": ei,
"content": [
{
"evo_id": ei,
"target_task_name": (
w.target_task.name if w.target_task else "PipelineTask"
), # TODO: save this when proposal
"workspace": w.file_dict,
}
for w in ws
],
},
}
elif f"evo_loop_{ei}.evolving feedback" in tag and "running" not in tag:
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEERSingleFeedback,
)
fl: list[CoSTEERSingleFeedback] = [i for i in obj]
data = {
"id": id,
"msg": {
"tag": "evolving.feedbacks",
"timestamp": timestamp,
"loop_id": li,
"evo_id": ei,
"content": [
{
"evo_id": ei,
"final_decision": f.final_decision,
# "final_feedback": f.final_feedback,
"execution": f.execution,
"code": f.code,
"return_checking": f.return_checking,
}
for f in fl
],
},
}
elif "scenario" in tag:
data = {
"id": id,
"msg": {
"tag": "feedback.config",
"timestamp": timestamp,
"loop_id": li,
"content": {"config": obj.experiment_setting},
},
}
elif "Quantitative Backtesting Chart" in tag:
import plotly
from rdagent.log.ui.qlib_report_figure import report_figure
data = {
"id": id,
"msg": {
"tag": "feedback.return_chart",
"timestamp": timestamp,
"loop_id": li,
"content": {"chart_html": plotly.io.to_html(report_figure(obj))},
},
}
elif "running" in tag:
from rdagent.core.experiment import Experiment
if isinstance(obj, Experiment):
try:
result = obj.result
except AttributeError: # compatibility with old versions
result = obj.__dict__["result"]
if result is not None:
result_str = result.to_json()
data = {
"id": id,
"msg": {
"tag": "feedback.metric",
"old_tag": tag,
"timestamp": timestamp,
"loop_id": li,
"content": {
"result": result_str,
},
},
}
elif "feedback" in tag:
from rdagent.core.proposal import ExperimentFeedback, HypothesisFeedback
if isinstance(obj, ExperimentFeedback):
ef: ExperimentFeedback = obj
content = (
{
"observations": str(ef.observations),
"hypothesis_evaluation": ef.hypothesis_evaluation,
"new_hypothesis": ef.new_hypothesis,
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
}
if isinstance(ef, HypothesisFeedback)
else {
"decision": ef.decision,
"reason": ef.reason,
"exception": ef.exception,
}
)
data = {
"id": id,
"msg": {
"tag": "feedback.hypothesis_feedback",
"timestamp": timestamp,
"loop_id": li,
"content": content,
},
}
return data
File diff suppressed because it is too large Load Diff
-629
View File
@@ -1,629 +0,0 @@
import time
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timezone
from typing import Callable, Type
import pandas as pd
import plotly.express as px
import streamlit as st
from streamlit.delta_generator import DeltaGenerator
from rdagent.components.coder.factor_coder.evaluators import FactorSingleFeedback
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask
from rdagent.components.coder.model_coder.evaluators import ModelSingleFeedback
from rdagent.components.coder.model_coder.model import ModelFBWorkspace, ModelTask
from rdagent.core.proposal import Hypothesis, HypothesisFeedback, Trace
from rdagent.log.base import Message, Storage, View
from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment
from rdagent.scenarios.qlib.experiment.model_experiment import (
QlibModelExperiment,
QlibModelScenario,
)
st.set_page_config(layout="wide")
TIME_DELAY = 0.001
class WebView(View):
def __init__(self, ui: "StWindow"):
self.ui = ui
# Save logs to your desired data structure
# ...
def display(self, s: Storage, watch: bool = False):
for msg in s.iter_msg(): # iterate overtime
# NOTE: iter_msg will correctly separate the information.
# TODO: msg may support streaming mode.
self.ui.consume_msg(msg)
class StWindow:
def __init__(self, container: "DeltaGenerator"):
self.container = container
def consume_msg(self, msg: Message):
msg_str = f"{msg.timestamp.astimezone(timezone.utc).isoformat()} | {msg.level} | {msg.caller} - {msg.content}"
self.container.code(msg_str, language="log")
class LLMWindow(StWindow):
def __init__(self, container: "DeltaGenerator", session_name: str = "common"):
self.session_name = session_name
self.container = container.expander(f"{self.session_name} message")
def consume_msg(self, msg: Message):
self.container.chat_message("user").markdown(f"{msg.content}")
class ProgressTabsWindow(StWindow):
"""
For windows with stream messages, will refresh when a new tab is created.
"""
def __init__(
self,
container: "DeltaGenerator",
inner_class: Type[StWindow] = StWindow,
mapper: Callable[[Message], str] = lambda x: x.pid_trace,
):
self.inner_class = inner_class
self.mapper = mapper
self.container = container.empty()
self.tab_windows: dict[str, StWindow] = defaultdict(None)
self.tab_caches: dict[str, list[Message]] = defaultdict(list)
def consume_msg(self, msg: Message):
name = self.mapper(msg)
if name not in self.tab_windows:
# new tab need to be created, current streamlit container need to be updated.
names = list(self.tab_windows.keys()) + [name]
if len(names) == 1:
tabs = [self.container.container()]
else:
tabs = self.container.tabs(names)
for id, name in enumerate(names):
self.tab_windows[name] = self.inner_class(tabs[id])
# consume the cache
for name in self.tab_caches:
for msg in self.tab_caches[name]:
self.tab_windows[name].consume_msg(msg)
self.tab_caches[name].append(msg)
self.tab_windows[name].consume_msg(msg)
class ObjectsTabsWindow(StWindow):
def __init__(
self,
container: "DeltaGenerator",
inner_class: Type[StWindow] = StWindow,
mapper: Callable[[object], str] = lambda x: str(x),
tab_names: list[str] | None = None,
):
self.inner_class = inner_class
self.mapper = mapper
self.container = container
self.tab_names = tab_names
def consume_msg(self, msg: Message):
if isinstance(msg.content, list):
if self.tab_names:
assert len(self.tab_names) == len(
msg.content
), "List of objects should have the same length as provided tab names."
objs_dict = {self.tab_names[id]: obj for id, obj in enumerate(msg.content)}
else:
objs_dict = {self.mapper(obj): obj for obj in msg.content}
elif not isinstance(msg.content, dict):
raise ValueError("Message content should be a list or a dict of objects.")
# two many tabs may cause display problem
tab_names = list(objs_dict.keys())
tabs = []
for i in range(0, len(tab_names), 10):
tabs.extend(self.container.tabs(tab_names[i : i + 10]))
for id, obj in enumerate(objs_dict.values()):
splited_msg = Message(
tag=msg.tag,
level=msg.level,
timestamp=msg.timestamp,
caller=msg.caller,
pid_trace=msg.pid_trace,
content=obj,
)
self.inner_class(tabs[id]).consume_msg(splited_msg)
class RoundTabsWindow(StWindow):
def __init__(
self,
container: "DeltaGenerator",
new_tab_func: Callable[[Message], bool],
inner_class: Type[StWindow] = StWindow,
title: str = "Round tabs",
):
container.markdown(f"### **{title}**")
self.inner_class = inner_class
self.new_tab_func = new_tab_func
self.round = 0
self.current_win = StWindow(container)
self.tabs_c = container.empty()
def consume_msg(self, msg: Message):
if self.new_tab_func(msg):
self.round += 1
self.current_win = self.inner_class(self.tabs_c.tabs([str(i) for i in range(1, self.round + 1)])[-1])
self.current_win.consume_msg(msg)
class HypothesisWindow(StWindow):
def consume_msg(self, msg: Message | Hypothesis):
h: Hypothesis = msg.content if isinstance(msg, Message) else msg
self.container.markdown("#### **Hypothesis💡**")
self.container.markdown(f"""
- **Hypothesis**: {h.hypothesis}
- **Reason**: {h.reason}""")
class HypothesisFeedbackWindow(StWindow):
def consume_msg(self, msg: Message | HypothesisFeedback):
h: HypothesisFeedback = msg.content if isinstance(msg, Message) else msg
self.container.markdown("#### **Hypothesis Feedback🔍**")
self.container.markdown(f"""
- **Observations**: {h.observations}
- **Hypothesis Evaluation**: {h.hypothesis_evaluation}
- **New Hypothesis**: {h.new_hypothesis}
- **Decision**: {h.decision}
- **Reason**: {h.reason}""")
class FactorTaskWindow(StWindow):
def consume_msg(self, msg: Message | FactorTask):
ft: FactorTask = msg.content if isinstance(msg, Message) else msg
self.container.markdown(f"**Factor Name**: {ft.factor_name}")
self.container.markdown(f"**Description**: {ft.factor_description}")
self.container.latex(f"Formulation: {ft.factor_formulation}")
variables_df = pd.DataFrame(ft.variables, index=["Description"]).T
variables_df.index.name = "Variable"
self.container.table(variables_df)
self.container.text(f"Factor resources: {ft.factor_resources}")
class ModelTaskWindow(StWindow):
def consume_msg(self, msg: Message | ModelTask):
mt: ModelTask = msg.content if isinstance(msg, Message) else msg
self.container.markdown(f"**Model Name**: {mt.name}")
self.container.markdown(f"**Model Type**: {mt.model_type}")
self.container.markdown(f"**Description**: {mt.description}")
self.container.latex(f"Formulation: {mt.formulation}")
variables_df = pd.DataFrame(mt.variables, index=["Value"]).T
variables_df.index.name = "Variable"
self.container.table(variables_df)
class FactorFeedbackWindow(StWindow):
def consume_msg(self, msg: Message | FactorSingleFeedback):
fb: FactorSingleFeedback = msg.content if isinstance(msg, Message) else msg
self.container.markdown(f"""### :blue[Factor Execution Feedback]
{fb.execution_feedback}
### :blue[Factor Code Feedback]
{fb.code_feedback}
### :blue[Factor Value Feedback]
{fb.value_feedback}
### :blue[Factor Final Feedback]
{fb.final_feedback}
### :blue[Factor Final Decision]
This implementation is {'SUCCESS' if fb.final_decision else 'FAIL'}.
""")
class ModelFeedbackWindow(StWindow):
def consume_msg(self, msg: Message | ModelSingleFeedback):
mb: ModelSingleFeedback = msg.content if isinstance(msg, Message) else msg
self.container.markdown(f"""### :blue[Model Execution Feedback]
{mb.execution_feedback}
### :blue[Model Shape Feedback]
{mb.shape_feedback}
### :blue[Model Value Feedback]
{mb.value_feedback}
### :blue[Model Code Feedback]
{mb.code_feedback}
### :blue[Model Final Feedback]
{mb.final_feedback}
### :blue[Model Final Decision]
This implementation is {'SUCCESS' if mb.final_decision else 'FAIL'}.
""")
class WorkspaceWindow(StWindow):
def __init__(self, container: "DeltaGenerator", show_task_info: bool = False):
self.container = container
self.show_task_info = show_task_info
def consume_msg(self, msg: Message | FactorFBWorkspace | ModelFBWorkspace):
ws: FactorFBWorkspace | ModelFBWorkspace = msg.content if isinstance(msg, Message) else msg
# no workspace
if ws is None:
return
# task info
if self.show_task_info:
task_msg = deepcopy(msg)
task_msg.content = ws.target_task
if isinstance(ws, FactorFBWorkspace):
self.container.subheader("Factor Info")
FactorTaskWindow(self.container.container()).consume_msg(task_msg)
else:
self.container.subheader("Model Info")
ModelTaskWindow(self.container.container()).consume_msg(task_msg)
# task codes
for k, v in ws.file_dict.items():
self.container.markdown(f"`{k}`")
self.container.code(v, language="python")
class QlibFactorExpWindow(StWindow):
def __init__(self, container: DeltaGenerator, show_task_info: bool = False):
self.container = container
self.show_task_info = show_task_info
def consume_msg(self, msg: Message | QlibFactorExperiment):
exp: QlibFactorExperiment = msg.content if isinstance(msg, Message) else msg
# factor tasks
if self.show_task_info:
ftm_msg = deepcopy(msg)
ftm_msg.content = [ws for ws in exp.sub_workspace_list if ws]
self.container.markdown("**Factor Tasks**")
ObjectsTabsWindow(
self.container.container(),
inner_class=WorkspaceWindow,
mapper=lambda x: x.target_task.factor_name,
).consume_msg(ftm_msg)
# result
self.container.markdown("**Results**")
results = pd.DataFrame({f"base_exp_{id}": e.result for id, e in enumerate(exp.based_experiments)})
results["now"] = exp.result
self.container.expander("results table").table(results)
try:
bar_chart = px.bar(results, orientation="h", barmode="group")
self.container.expander("results chart").plotly_chart(bar_chart)
except:
self.container.text("Results are incomplete.")
class QlibModelExpWindow(StWindow):
def __init__(self, container: DeltaGenerator, show_task_info: bool = False):
self.container = container
self.show_task_info = show_task_info
def consume_msg(self, msg: Message | QlibModelExperiment):
exp: QlibModelExperiment = msg.content if isinstance(msg, Message) else msg
# model tasks
if self.show_task_info:
_msg = deepcopy(msg)
_msg.content = [ws for ws in exp.sub_workspace_list if ws]
self.container.markdown("**Model Tasks**")
ObjectsTabsWindow(
self.container.container(),
inner_class=WorkspaceWindow,
mapper=lambda x: x.target_task.name,
).consume_msg(_msg)
# result
self.container.subheader("Results", divider=True)
results = pd.DataFrame({f"base_exp_{id}": e.result for id, e in enumerate(exp.based_experiments)})
results["now"] = exp.result
self.container.expander("results table").table(results)
class SimpleTraceWindow(StWindow):
def __init__(
self, container: "DeltaGenerator" = st.container(), show_llm: bool = False, show_common_logs: bool = False
):
super().__init__(container)
self.show_llm = show_llm
self.show_common_logs = show_common_logs
self.pid_trace = ""
self.current_tag = ""
self.current_win = StWindow(self.container)
self.evolving_tasks: list[str] = []
def consume_msg(self, msg: Message):
# divide tag levels
if len(msg.tag) > len(self.current_tag):
# write a header about current task, if it is llm message, not write.
if not msg.tag.endswith("llm_messages"):
self.container.header(msg.tag.replace(".", ""), divider=True)
self.current_tag = msg.tag
# set log writer (window) according to msg
if msg.tag.endswith("llm_messages"):
# llm messages logs
if not self.show_llm:
return
if not isinstance(self.current_win, LLMWindow):
self.current_win = LLMWindow(self.container)
elif isinstance(msg.content, Hypothesis):
# hypothesis
self.current_win = HypothesisWindow(self.container)
elif isinstance(msg.content, HypothesisFeedback):
# hypothesis feedback
self.current_win = HypothesisFeedbackWindow(self.container)
elif isinstance(msg.content, QlibFactorExperiment):
self.current_win = QlibFactorExpWindow(self.container)
elif isinstance(msg.content, QlibModelExperiment):
self.current_win = QlibModelExpWindow(self.container)
elif isinstance(msg.content, list):
msg.content = [m for m in msg.content if m]
if len(msg.content) == 0:
return
if isinstance(msg.content[0], FactorTask):
self.current_win = ObjectsTabsWindow(
self.container.expander("Factor Tasks"), FactorTaskWindow, lambda x: x.factor_name
)
elif isinstance(msg.content[0], ModelTask):
self.current_win = ObjectsTabsWindow(
self.container.expander("Model Tasks"), ModelTaskWindow, lambda x: x.name
)
elif isinstance(msg.content[0], FactorFBWorkspace):
self.current_win = ObjectsTabsWindow(
self.container.expander("Factor Workspaces"),
inner_class=WorkspaceWindow,
mapper=lambda x: x.target_task.factor_name,
)
self.evolving_tasks = [m.target_task.factor_name for m in msg.content]
elif isinstance(msg.content[0], ModelFBWorkspace):
self.current_win = ObjectsTabsWindow(
self.container.expander("Model Workspaces"),
inner_class=WorkspaceWindow,
mapper=lambda x: x.target_task.name,
)
self.evolving_tasks = [m.target_task.name for m in msg.content]
elif isinstance(msg.content[0], FactorSingleFeedback):
self.current_win = ObjectsTabsWindow(
self.container.expander("Factor Feedbacks"),
inner_class=FactorFeedbackWindow,
tab_names=self.evolving_tasks,
)
elif isinstance(msg.content[0], ModelSingleFeedback):
self.current_win = ObjectsTabsWindow(
self.container.expander("Model Feedbacks"),
inner_class=ModelFeedbackWindow,
tab_names=self.evolving_tasks,
)
else:
# common logs
if not self.show_common_logs:
return
self.current_win = StWindow(self.container)
self.current_win.consume_msg(msg)
def mock_msg(obj) -> Message:
return Message(tag="mock", level="INFO", timestamp=datetime.now(), pid_trace="000", caller="mock", content=obj)
class TraceObjWindow(StWindow):
def __init__(self, container: "DeltaGenerator" = st.container()):
self.container = container
def consume_msg(self, msg: Message | Trace):
if isinstance(msg, Message):
trace: Trace = msg.content
else:
trace = msg
for id, (h, e, hf) in enumerate(trace.hist):
self.container.header(f"Trace History {id}", divider=True)
HypothesisWindow(self.container).consume_msg(mock_msg(h))
if isinstance(e, QlibFactorExperiment):
QlibFactorExpWindow(self.container).consume_msg(mock_msg(e))
else:
QlibModelExpWindow(self.container).consume_msg(mock_msg(e))
HypothesisFeedbackWindow(self.container).consume_msg(mock_msg(hf))
class ResearchWindow(StWindow):
def consume_msg(self, msg: Message):
if msg.tag.endswith("hypothesis generation"):
HypothesisWindow(self.container.container()).consume_msg(msg)
elif msg.tag.endswith("experiment generation"):
if isinstance(msg.content, list):
if isinstance(msg.content[0], FactorTask):
self.container.markdown("**Factor Tasks**")
ObjectsTabsWindow(
self.container.container(), FactorTaskWindow, lambda x: x.factor_name
).consume_msg(msg)
elif isinstance(msg.content[0], ModelTask):
self.container.markdown("**Model Tasks**")
ObjectsTabsWindow(self.container.container(), ModelTaskWindow, lambda x: x.name).consume_msg(msg)
elif msg.tag.endswith("load_pdf_screenshot"):
self.container.image(msg.content)
elif msg.tag.endswith("load_factor_tasks"):
self.container.json(msg.content)
class EvolvingWindow(StWindow):
def __init__(self, container: "DeltaGenerator"):
self.container = container
self.evolving_tasks: list[str] = []
def consume_msg(self, msg: Message):
if msg.tag.endswith("evolving code"):
if isinstance(msg.content, list):
msg.content = [m for m in msg.content if m]
if len(msg.content) == 0:
return
if isinstance(msg.content[0], FactorFBWorkspace):
self.container.markdown("**Factor Codes**")
ObjectsTabsWindow(
self.container.container(),
inner_class=WorkspaceWindow,
mapper=lambda x: x.target_task.factor_name,
).consume_msg(msg)
self.evolving_tasks = [m.target_task.factor_name for m in msg.content]
elif isinstance(msg.content[0], ModelFBWorkspace):
self.container.markdown("**Model Codes**")
ObjectsTabsWindow(
self.container.container(), inner_class=WorkspaceWindow, mapper=lambda x: x.target_task.name
).consume_msg(msg)
self.evolving_tasks = [m.target_task.name for m in msg.content]
elif msg.tag.endswith("evolving feedback"):
if isinstance(msg.content, list):
msg.content = [m for m in msg.content if m]
if len(msg.content) == 0:
return
if isinstance(msg.content[0], FactorSingleFeedback):
self.container.markdown("**Factor Feedbacks🔍**")
ObjectsTabsWindow(
self.container.container(), inner_class=FactorFeedbackWindow, tab_names=self.evolving_tasks
).consume_msg(msg)
elif isinstance(msg.content[0], ModelSingleFeedback):
self.container.markdown("**Model Feedbacks🔍**")
ObjectsTabsWindow(
self.container.container(), inner_class=ModelFeedbackWindow, tab_names=self.evolving_tasks
).consume_msg(msg)
class DevelopmentWindow(StWindow):
def __init__(self, container: "DeltaGenerator"):
self.E_win = RoundTabsWindow(
container.container(),
new_tab_func=lambda x: x.tag.endswith("evolving code"),
inner_class=EvolvingWindow,
title="Evolving Loops🔧",
)
def consume_msg(self, msg: Message):
if "evolving" in msg.tag:
self.E_win.consume_msg(msg)
class FeedbackWindow(StWindow):
def __init__(self, container: "DeltaGenerator"):
self.container = container
def consume_msg(self, msg: Message):
if msg.tag.endswith("returns"):
fig = px.line(msg.content)
self.container.markdown("**Returns📈**")
self.container.plotly_chart(fig)
elif isinstance(msg.content, HypothesisFeedback):
HypothesisFeedbackWindow(self.container.container(border=True)).consume_msg(msg)
elif isinstance(msg.content, QlibModelExperiment):
QlibModelExpWindow(self.container.container(border=True)).consume_msg(msg)
elif isinstance(msg.content, QlibFactorExperiment):
QlibFactorExpWindow(self.container.container(border=True)).consume_msg(msg)
class SingleRDLoopWindow(StWindow):
def __init__(self, container: "DeltaGenerator"):
self.container = container
col1, col2 = self.container.columns([2, 3])
self.R_win = ResearchWindow(col1.container(border=True))
self.F_win = FeedbackWindow(col1.container(border=True))
self.D_win = DevelopmentWindow(col2.container(border=True))
def consume_msg(self, msg: Message):
tags = msg.tag.split(".")
if "r" in tags:
self.R_win.consume_msg(msg)
elif "d" in tags:
self.D_win.consume_msg(msg)
elif "ef" in tags:
self.F_win.consume_msg(msg)
class TraceWindow(StWindow):
def __init__(
self, container: "DeltaGenerator" = st.container(), show_llm: bool = False, show_common_logs: bool = False
):
self.show_llm = show_llm
self.show_common_logs = show_common_logs
image_c, scen_c = container.columns([2, 3], vertical_alignment="center")
image_c.image("scen.png")
scen_c.container(border=True).markdown(QlibModelScenario().rich_style_description)
top_container = container.container()
col1, col2 = top_container.columns([2, 3])
chart_c = col2.container(border=True, height=500)
chart_c.markdown("**Metrics📈**")
self.chart_c = chart_c.empty()
hypothesis_status_c = col1.container(border=True, height=500)
hypothesis_status_c.markdown("**Hypotheses🏅**")
self.summary_c = hypothesis_status_c.empty()
self.RDL_win = RoundTabsWindow(
container.container(),
new_tab_func=lambda x: x.tag.endswith("hypothesis generation"),
inner_class=SingleRDLoopWindow,
title="R&D Loops♾️",
)
self.hypothesis_decisions = defaultdict(bool)
self.hypotheses: list[Hypothesis] = []
self.results = []
def consume_msg(self, msg: Message):
if not self.show_llm and "llm_messages" in msg.tag:
return
if not self.show_common_logs and isinstance(msg.content, str):
return
if isinstance(msg.content, dict):
return
if msg.tag.endswith("hypothesis generation"):
self.hypotheses.append(msg.content)
elif msg.tag.endswith("ef.feedback"):
self.hypothesis_decisions[self.hypotheses[-1]] = msg.content.decision
self.summary_c.markdown(
"\n".join(
(
f"{id+1}. :green[{self.hypotheses[id].hypothesis}]\n\t>*{self.hypotheses[id].concise_reason}*"
if d
else f"{id+1}. {self.hypotheses[id].hypothesis}\n\t>*{self.hypotheses[id].concise_reason}*"
)
for id, (h, d) in enumerate(self.hypothesis_decisions.items())
)
)
elif msg.tag.endswith("ef.model runner result") or msg.tag.endswith("ef.factor runner result"):
self.results.append(msg.content.result)
if len(self.results) == 1:
self.chart_c.table(self.results[0])
else:
df = pd.DataFrame(self.results, index=range(1, len(self.results) + 1))
fig = px.line(df, x=df.index, y=df.columns, markers=True)
self.chart_c.plotly_chart(fig)
self.RDL_win.consume_msg(msg)
# time.sleep(TIME_DELAY)
-129
View File
@@ -1,129 +0,0 @@
import inspect
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional, TypedDict, cast
class LogColors:
"""
ANSI color codes for use in console output.
"""
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
WHITE = "\033[97m"
GRAY = "\033[90m"
BLACK = "\033[30m"
BOLD = "\033[1m"
ITALIC = "\033[3m"
END = "\033[0m"
@classmethod
def get_all_colors(cls: type["LogColors"]) -> list:
names = dir(cls)
names = [name for name in names if not name.startswith("__") and not callable(getattr(cls, name))]
return [getattr(cls, name) for name in names]
def render(self, text: str, color: str = "", style: str = "") -> str:
"""
render text by input color and style.
It's not recommend that input text is already rendered.
"""
# This method is called too frequently, which is not good.
colors = self.get_all_colors()
# Perhaps color and font should be distinguished here.
if color and color in colors:
error_message = f"color should be in: {colors} but now is: {color}"
raise ValueError(error_message)
if style and style in colors:
error_message = f"style should be in: {colors} but now is: {style}"
raise ValueError(error_message)
text = f"{color}{text}{self.END}"
return f"{style}{text}{self.END}"
@staticmethod
def remove_ansi_codes(s: str) -> str:
"""
It is for removing ansi ctrl characters in the string(e.g. colored text)
"""
ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
return ansi_escape.sub("", s)
class CallerInfo(TypedDict):
function: str
line: int
name: Optional[str]
def get_caller_info(level: int = 2) -> CallerInfo:
# Get the current stack information
stack = inspect.stack()
# The second element is usually the caller's information
caller_info = stack[level]
frame = caller_info[0]
info: CallerInfo = {
"line": caller_info.lineno,
"name": frame.f_globals["__name__"], # Get the module name from the frame's globals
"function": frame.f_code.co_name, # Get the caller's function name
}
return info
def is_valid_session(log_path: Path) -> bool:
return log_path.is_dir() and log_path.joinpath("__session__").exists()
def extract_loopid_func_name(tag: str) -> tuple[str, str] | tuple[None, None]:
"""extract loop id and function name from the tag in Message"""
match = re.search(r"Loop_(\d+)\.([^.]+)", tag)
return cast(tuple[str, str], match.groups()) if match else (None, None)
def extract_evoid(tag: str) -> str | None:
"""extract evo id from the tag in Message"""
match = re.search(r"evo_loop_(\d+)\.", tag)
return cast(str, match.group(1)) if match else None
def extract_json(log_content: str) -> dict | None:
match = re.search(r"\{.*\}", log_content, re.DOTALL)
if match:
return cast(dict, json.loads(match.group(0)))
return None
def gen_datetime(dt: datetime | None = None) -> datetime:
"""
Generate a datetime object in UTC timezone.
- If `dt` is None, it will return the current time in UTC.
- If `dt` is provided, it will convert it to UTC timezone.
"""
if dt is None:
return datetime.now(timezone.utc)
return dt.astimezone(timezone.utc)
def dict_get_with_warning(d: dict, key: str, default: Any = None) -> Any:
"""
Motivation:
- When handling the repsonse from the LLM, we may use dict get to get the value.
- the function prevent falling into default value **silently**.
- Instead, it will log a warning message.
"""
from rdagent.log import rdagent_logger as logger
if key not in d:
logger.warning(f"Key {key} not found in {d}")
return default
return d[key]
-77
View File
@@ -1,77 +0,0 @@
"""
This module provides some useful functions for working with logger folders.
"""
import pickle
from datetime import timedelta
from pathlib import Path
import pandas as pd
from rdagent.utils.workflow import LoopBase
def get_first_session_file_after_duration(log_folder: str | Path, duration: str | pd.Timedelta) -> Path:
log_folder = Path(log_folder)
duration_dt = pd.Timedelta(duration)
# iterate the dump steps in increasing order
files = sorted(
(log_folder / "__session__").glob("*/*_*"), key=lambda f: (int(f.parent.name), int(f.name.split("_")[0]))
)
fp = None
for fp in files:
with fp.open("rb") as f:
session_obj: LoopBase = pickle.load(f)
timer = session_obj.timer
all_duration = timer.all_duration
remain_time_duration = timer.remain_time()
if all_duration is None or remain_time_duration is None:
msg = "Timer is not configured"
raise ValueError(msg)
time_spent = all_duration - remain_time_duration
if time_spent >= duration_dt:
break
if fp is None:
msg = f"No session file found after duration {duration}"
raise ValueError(msg)
return fp
def first_li_si_after_one_time(log_path: Path, hours: int = 12) -> tuple[int, int, str]:
"""
Based on the hours, find the stop loop id and step id (the first step after <hours> hours).
Args:
log_path (Path): The path to the log folder (contains many log traces).
hours (int): The number of hours to stat.
Returns:
tuple[int, int, str]: The loop id, step id and function name.
"""
session_path = log_path / "__session__"
max_li = max(int(p.name) for p in session_path.iterdir() if p.is_dir() and p.name.isdigit())
max_step = max(int(p.name.split("_")[0]) for p in (session_path / str(max_li)).iterdir() if p.is_file())
rdloop_obj_p = next((session_path / str(max_li)).glob(f"{max_step}_*"))
rdloop_obj = DataScienceRDLoop.load(rdloop_obj_p)
loop_trace = rdloop_obj.loop_trace
si2fn = rdloop_obj.steps
duration = timedelta(seconds=0)
for li, lts in loop_trace.items():
for lt in lts:
si = lt.step_idx
duration += lt.end - lt.start
if duration > timedelta(hours=hours):
return li, si, si2fn[si]
if __name__ == "__main__":
from rdagent.app.data_science.loop import DataScienceRDLoop
f = get_first_session_file_after_duration("<path to log aptos2019-blindness-detection>", pd.Timedelta("12h"))
with f.open("rb") as f:
session_obj: LoopBase = pickle.load(f)
loop_trace = session_obj.loop_trace
last_loop = loop_trace[max(loop_trace.keys())]
last_step = last_loop[-1]
session_obj.steps[last_step.step_idx]
+301
View File
@@ -0,0 +1,301 @@
#!/bin/bash
# =============================================================================
# setup_predix_eurusd.sh
# Richtet Predix für EURUSD 15min Trading ein
# Ausführen: bash setup_predix_eurusd.sh
# =============================================================================
set -e # Abbruch bei Fehler
PREDIX_DIR="$HOME/Predix"
CSV_SOURCE="$HOME/Downloads/eurusd_data.csv"
DATA_DIR="$PREDIX_DIR/git_ignore_folder/eurusd_data"
QLIB_DIR="$HOME/.qlib/qlib_data/eurusd_data"
echo "========================================"
echo " Predix EURUSD Setup"
echo "========================================"
# ─── 1. Prüfen ob alles da ist ───────────────────────────────────────────────
echo ""
echo "[1/7] Prüfe Voraussetzungen..."
if [ ! -d "$PREDIX_DIR" ]; then
echo "FEHLER: $PREDIX_DIR nicht gefunden!"
exit 1
fi
if [ ! -f "$CSV_SOURCE" ]; then
echo "FEHLER: $CSV_SOURCE nicht gefunden!"
echo "Bitte eurusd_data.csv in ~/Downloads/ legen"
exit 1
fi
echo "✓ Predix gefunden: $PREDIX_DIR"
echo "✓ CSV gefunden: $CSV_SOURCE"
# ─── 2. Ordner anlegen ───────────────────────────────────────────────────────
echo ""
echo "[2/7] Erstelle Ordnerstruktur..."
mkdir -p "$DATA_DIR"
mkdir -p "$QLIB_DIR/calendars"
mkdir -p "$QLIB_DIR/instruments"
mkdir -p "$QLIB_DIR/features/eurusd"
mkdir -p "$PREDIX_DIR/git_ignore_folder/log"
cp "$CSV_SOURCE" "$DATA_DIR/eurusd_data.csv"
echo "✓ CSV kopiert nach $DATA_DIR"
# ─── 3. CSV → Qlib Format konvertieren ───────────────────────────────────────
echo ""
echo "[3/7] Konvertiere CSV zu Qlib-Format..."
python3 << 'PYEOF'
import pandas as pd
import numpy as np
from pathlib import Path
import os
QLIB_DIR = Path(os.path.expanduser("~/.qlib/qlib_data/eurusd_data"))
CSV_PATH = Path(os.path.expanduser("~/Downloads/eurusd_data.csv"))
# Laden + sortieren
df = pd.read_csv(CSV_PATH, parse_dates=["datetime"])
df = df.sort_values("datetime").reset_index(drop=True)
df.columns = [c.lower() for c in df.columns]
print(f" Rows: {len(df):,} | Range: {df['datetime'].min().date()} -> {df['datetime'].max().date()}")
# ── Kalender (alle 15min Timestamps) ────────────────────────────────────────
cal = df["datetime"].dt.strftime("%Y-%m-%d %H:%M:%S")
cal_path = QLIB_DIR / "calendars" / "15min.txt"
cal.to_csv(cal_path, index=False, header=False)
print(f" ✓ Kalender: {len(cal)} Einträge -> {cal_path}")
# ── Instruments (nur EURUSD) ─────────────────────────────────────────────────
inst_path = QLIB_DIR / "instruments" / "all.txt"
start = df["datetime"].min().strftime("%Y-%m-%d")
end = df["datetime"].max().strftime("%Y-%m-%d")
with open(inst_path, "w") as f:
f.write(f"EURUSD\t{start}\t{end}\n")
print(f" ✓ Instruments -> {inst_path}")
# ── Features (Qlib binary format via CSV) ───────────────────────────────────
feat_dir = QLIB_DIR / "features" / "eurusd"
feat_dir.mkdir(parents=True, exist_ok=True)
# Qlib erwartet: $open, $high, $low, $close, $volume
for col in ["open", "high", "low", "close", "volume"]:
out = feat_dir / f"{col}.day.bin"
# Qlib binary: float32 array
arr = df[col].astype("float32").values
arr.tofile(str(out).replace(".day.bin", f"_15min.bin"))
# Auch als einfache CSV für direkten Zugriff
df.to_csv(QLIB_DIR / "eurusd_15min.csv", index=False)
print(f" ✓ Features + CSV -> {feat_dir}")
# ── Returns + technische Features vorberechnen ───────────────────────────────
def ema(s, p): return s.ewm(span=p, adjust=False).mean()
def rsi(c, p=14):
d = c.diff()
g = d.clip(lower=0).ewm(span=p, adjust=False).mean()
l = (-d.clip(upper=0)).ewm(span=p, adjust=False).mean()
return 100 - 100/(1 + g/(l+1e-9))
feat = pd.DataFrame()
feat["datetime"] = df["datetime"]
feat["close"] = df["close"]
for n in [1,4,8,16,96]:
feat[f"ret_{n}"] = df["close"].pct_change(n)
feat["rsi_14"] = rsi(df["close"], 14)
feat["macd_hist"] = ema(df["close"],12) - ema(df["close"],26)
feat["hour"] = df["datetime"].dt.hour
feat["is_london"] = feat["hour"].isin([8,9,10,11]).astype(int)
feat["is_ny"] = feat["hour"].isin([13,14,15,16]).astype(int)
feat["adx_proxy"] = df["close"].rolling(14).std() / df["close"].rolling(96).std()
feat.dropna(inplace=True)
feat.to_csv(QLIB_DIR / "eurusd_features.csv", index=False)
print(f" ✓ Features CSV: {len(feat):,} Zeilen, {len(feat.columns)} Spalten")
print(" Fertig!")
PYEOF
echo "✓ Qlib-Daten konvertiert"
# ─── 4. .env updaten ─────────────────────────────────────────────────────────
echo ""
echo "[4/7] Update .env..."
ENV_FILE="$PREDIX_DIR/.env"
# Backup
cp "$ENV_FILE" "$ENV_FILE.backup_$(date +%Y%m%d_%H%M%S)"
# QLIB_DATA_DIR auf EURUSD umbiegen
sed -i "s|QLIB_DATA_DIR=.*|QLIB_DATA_DIR=$QLIB_DIR|" "$ENV_FILE"
# LOG_PATH korrigieren (war /home/nico/RD-Agent-Local/log)
sed -i "s|LOG_PATH=.*|LOG_PATH=$PREDIX_DIR/git_ignore_folder/log|" "$ENV_FILE"
# EURUSD-spezifische Vars hinzufügen (falls noch nicht da)
grep -q "EURUSD_DATA_PATH" "$ENV_FILE" || cat >> "$ENV_FILE" << 'ENVEOF'
# ---------- EURUSD ----------
EURUSD_DATA_PATH=/home/nico/.qlib/qlib_data/eurusd_data/eurusd_15min.csv
QLIB_FREQ=15min
QLIB_MARKET=eurusd
BACKTEST_START_TIME=2024-08-09
BACKTEST_END_TIME=2026-03-20
COST_RATE=0.00015
ENVEOF
echo "✓ .env aktualisiert (Backup erstellt)"
# ─── 5. Prompts für EURUSD anpassen ──────────────────────────────────────────
echo ""
echo "[5/7] Passe Qlib-Prompts für EURUSD an..."
PROMPT_FILE="$PREDIX_DIR/rdagent/app/qlib_rd_loop/prompts.yaml"
cp "$PROMPT_FILE" "${PROMPT_FILE}.backup_$(date +%Y%m%d_%H%M%S)"
cat > "$PROMPT_FILE" << 'YAMLEOF'
hypothesis_generation:
system: |-
You are an expert quantitative researcher specialized in FX (foreign exchange) trading,
specifically EURUSD intraday strategies on 15-minute bars.
EURUSD domain knowledge you must apply:
- London session (08:00-12:00 UTC): highest volatility, trending behavior — favor momentum strategies
- NY session (13:00-17:00 UTC): second volatility peak, also trending
- Asian session (00:00-07:00 UTC): low volatility, mean-reverting behavior
- London/NY overlap (13:00-17:00 UTC): strongest directional moves of the day
- Weekend gap risk: avoid holding positions after Friday 20:00 UTC
- Spread cost: ~1.5 bps per trade — strategies must minimize unnecessary entries
- EURUSD is mean-reverting on short windows (<1h), trending on longer (>4h)
- Key macro drivers: ECB/Fed rate decisions, NFP (first Friday of month), CPI releases
Available model types you can propose:
- TimeSeries: LSTM, GRU, TCN (Temporal Convolutional Network), Transformer, PatchTST
- Tabular: XGBoost, LightGBM, RandomForest (on engineered features)
- Hybrid: CNN+LSTM, XGBoost+LSTM ensemble
- Statistical: Regime-switching (HMM), Kalman filter
Available features in the dataset:
- OHLCV: open, high, low, close, volume (15min bars)
- Returns: ret_1, ret_4, ret_8, ret_16, ret_96
- Technical: rsi_14, macd_hist, adx_14, atr_14, bb_pct, stoch_k, cci_14
- Volatility: vol_real_4, vol_real_16, vol_ratio, zscore_ret_96
- Time/Session: hour, is_london, is_ny, is_overlap, hour_sin, hour_cos
- Lags: rsi_14_lag1-8, macd_hist_lag1-8, bb_pct_lag1-8
Your hypothesis must:
1. Specify which session(s) the strategy targets
2. Name which model type to use and why it fits EURUSD
3. Include a session filter (is_london / is_ny)
4. Include a spread filter (only trade when expected |return| > 0.0003)
5. Specify target: classification (fwd_sign_4) or regression (fwd_ret_4)
Please ensure your response is in JSON format:
{
"hypothesis": "A clear and concise trading hypothesis for EURUSD 15min.",
"reason": "Detailed explanation including session, model choice, and expected edge.",
"model_type": "One of: TimeSeries / Tabular / XGBoost",
"target_session": "london / ny / asian / all",
"expected_arr_range": "e.g. 8-12%"
}
user: |-
Previously tried approaches and their results:
{{ factor_descriptions }}
Additional context:
{{ report_content }}
Generate a NEW hypothesis that is meaningfully different from what has been tried.
Focus on approaches that have NOT been tested yet.
Target: beat current best ARR of 9.62%.
YAMLEOF
echo "✓ prompts.yaml aktualisiert (Backup erstellt)"
# ─── 6. Model Coder Prompt erweitern ─────────────────────────────────────────
echo ""
echo "[6/7] Erweitere Model Coder Prompts..."
MODEL_PROMPT="$PREDIX_DIR/rdagent/components/coder/model_coder/prompts.yaml"
cp "$MODEL_PROMPT" "${MODEL_PROMPT}.backup_$(date +%Y%m%d_%H%M%S)"
# EURUSD session filter als Kommentar in den evolving_strategy Block injizieren
python3 << 'PYEOF'
import re
from pathlib import Path
path = Path("/home/nico/Predix/rdagent/components/coder/model_coder/prompts.yaml")
content = path.read_text()
eurusd_note = """
EURUSD-specific rules (ALWAYS apply these in generated code):
1. Session filter: use is_london and is_ny columns — weight/filter signals to active sessions
2. Spread filter: only generate signal when abs(predicted_return) > 0.0003
3. ADX regime: if adx_proxy > 1.2 use trend model; if adx_proxy < 0.8 use mean-reversion
4. Weekend filter: zero out signals when dayofweek==4 and hour>=20
5. Max trade frequency: target <15 trades per day (avoid spread cost death)
6. Supported model_type values: "Tabular", "TimeSeries", "XGBoost"
"""
# Inject after the scenario line in evolving_strategy_model_coder
content = content.replace(
" Your code is expected to align the scenario in any form",
eurusd_note + "\n Your code is expected to align the scenario in any form"
)
path.write_text(content)
print(" ✓ Model coder prompt erweitert")
PYEOF
echo "✓ Model coder Prompt angepasst"
# ─── 7. Git Commits ──────────────────────────────────────────────────────────
echo ""
echo "[7/7] Git Commits..."
cd "$PREDIX_DIR"
git add rdagent/app/qlib_rd_loop/prompts.yaml
git commit -m "feat: EURUSD 15min prompts - session filter, FX domain knowledge
- Add London/NY/Asian session awareness to hypothesis generation
- Add model type suggestions: LSTM, GRU, TCN, Transformer, XGBoost, LightGBM
- Add spread filter (1.5 bps) and ADX regime detection
- Target: beat current best ARR of 9.62%"
git add rdagent/components/coder/model_coder/prompts.yaml
git commit -m "feat: inject EURUSD trading rules into model coder
- Session filter (is_london, is_ny)
- Spread filter (|return| > 0.0003)
- Weekend position close
- Max trade frequency guidance"
git add .env 2>/dev/null || true
echo " (Note: .env nicht committed - enthält API Keys)"
echo ""
echo "========================================"
echo " Setup abgeschlossen!"
echo "========================================"
echo ""
echo "Nächste Schritte:"
echo ""
echo " 1. Starten:"
echo " cd ~/Predix && rdagent fin_quant"
echo ""
echo " 2. Dashboard (in zweitem Terminal):"
echo " cd ~/Predix && rdagent server_ui --port 19899"
echo " → http://localhost:19899"
echo ""
echo " 3. Logs:"
echo " tail -f $PREDIX_DIR/git_ignore_folder/log/*.log"
echo ""
echo "Backup-Dateien (.backup_*) können nach erfolgreichem Test gelöscht werden."
+10 -651
View File
@@ -818,356 +818,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"peer": true
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"peer": true
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -4882,52 +4532,6 @@
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
}
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -5918,7 +5522,7 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true,
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6545,8 +6149,7 @@
"@element-plus/icons-vue": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
"requires": {}
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg=="
},
"@emnapi/core": {
"version": "1.9.0",
@@ -6929,206 +6532,6 @@
}
}
},
"@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"dev": true,
"optional": true,
"peer": true
},
"@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"dev": true,
"optional": true,
"peer": true
},
"@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -7194,8 +6597,7 @@
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dev": true,
"requires": {}
"dev": true
},
"@volar/language-core": {
"version": "2.4.6",
@@ -7363,8 +6765,7 @@
"vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"requires": {}
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="
}
}
},
@@ -7384,8 +6785,7 @@
"vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"requires": {}
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="
}
}
},
@@ -9139,8 +8539,7 @@
"lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"requires": {}
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ=="
},
"magic-string": {
"version": "0.30.12",
@@ -9593,8 +8992,7 @@
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz",
"integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==",
"dev": true,
"requires": {}
"dev": true
},
"posthtml": {
"version": "0.9.2",
@@ -9808,43 +9206,6 @@
"@rolldown/pluginutils": "1.0.0-rc.9"
}
},
"rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"optional": true,
"peer": true,
"requires": {
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"@types/estree": "1.0.8",
"fsevents": "~2.3.2"
}
},
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -10421,8 +9782,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"requires": {}
"dev": true
},
"picomatch": {
"version": "4.0.3",
@@ -10596,7 +9956,7 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true
"dev": true
},
"uc.micro": {
"version": "2.1.0",
@@ -10894,8 +10254,7 @@
"vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"requires": {}
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A=="
},
"vue-echarts": {
"version": "7.0.3",