SciDER / scider /workflows /full_workflow.py
leonardklin's picture
Upload 355 files
1499363 verified
Raw
History Blame Contribute Delete
18.9 kB
"""
Full SciDER Workflow
Complete workflow that chains DataAgent and ExperimentAgent from scratch.
This workflow takes raw data, analyzes it, generates experiment code,
executes it, and produces final metrics.
For partial workflows (e.g., starting from existing data analysis), see:
- data_workflow.py: Only runs DataAgent
- experiment_workflow.py: Only runs ExperimentAgent
"""
import shutil
from pathlib import Path
from typing import Literal
from loguru import logger
from pydantic import BaseModel, PrivateAttr, model_validator
from scider.core.code_env import WorkspaceInitConfig
from scider.core.constant import override_user_approval
from scider.workflows.data_workflow import DataWorkflow
from scider.workflows.hypo_data_workflow import HypoDataWorkflow
from scider.workflows.experiment_workflow import ExperimentWorkflow
from scider.workflows.paper_bootstrap import build_experimental_log, build_sparse_idea_from_query
from scider.workflows.utils import get_separator
from scider.workflows.writing_workflow import WritingWorkflow
class FullWorkflow(BaseModel):
"""
Full SciDER Workflow - chains DataAgent and ExperimentAgent from scratch.
This workflow executes:
1. DataWorkflow - Analyzes input data, produces data_analysis.md
2. ExperimentWorkflow - Generates code, executes experiments, produces metrics
Internally uses DataWorkflow and ExperimentWorkflow for better modularity.
Usage:
workflow = FullWorkflow(
data_path="data/data.csv",
workspace_path="workspace",
user_query="Train an SVR model",
)
workflow.run()
print(workflow.final_summary)
"""
# ==================== INPUT ====================
data_path: Path | None = None # real data; mutually exclusive with feature_desc
feature_desc: str | None = None # synthetic data description; mutually exclusive with data_path
num_rows: int = 1000 # forwarded to HypoDataWorkflow
workspace_path: Path
user_query: str
repo_source: str | None = None
max_revisions: int = 5
data_agent_recursion_limit: int = 100
experiment_agent_recursion_limit: int = 100
data_desc: str | None = None # Optional additional description of the data
# Paper writing phase (off by default)
run_paper_writing: bool = False
paper_workspace_path: Path | None = None
paper_template_dir_path: Path | None = None
paper_template_tex_path: Path | None = None
paper_conference_guidelines_path: Path | None = None
paper_agent_recursion_limit: int = 150
# None → LocalEnv's default (uv-managed, auto `uv init`). Forwarded to
# the nested DataWorkflow / ExperimentWorkflow / WritingWorkflow.
workspace_init_config: WorkspaceInitConfig | None = None
# ==================== INTERNAL STATE ====================
current_phase: Literal[
"init", "data_analysis", "experiment", "paper_writing", "complete", "failed"
] = "init"
data_summary: str = ""
data_agent_history: list = []
# Paper subagent results (from DataWorkflow)
papers: list[dict] = []
datasets: list[dict] = []
metrics: list[dict] = []
paper_search_summary: str | None = None
# ==================== OUTPUT ====================
final_status: Literal["success", "failed", "max_revisions_reached"] | None = None
final_summary: str = ""
execution_results: list = []
error_message: str | None = None
# Paper writing outputs (populated only if run_paper_writing=True)
paper_final_tex_path: Path | None = None
paper_final_pdf_path: Path | None = None
paper_writing_summary: str = ""
# Internal: sub-workflows
_data_workflow: DataWorkflow | HypoDataWorkflow | None = PrivateAttr(default=None)
_experiment_workflow: ExperimentWorkflow | None = PrivateAttr(default=None)
_writing_workflow: "WritingWorkflow | None" = PrivateAttr(default=None)
@model_validator(mode="after")
def _validate_data_mode(self) -> "FullWorkflow":
has_path = self.data_path is not None
has_desc = bool(self.feature_desc and self.feature_desc.strip())
if has_path and has_desc:
raise ValueError(
"Provide either data_path (real data) or feature_desc (synthetic), not both."
)
if not has_path and not has_desc:
raise ValueError("FullWorkflow requires either data_path or feature_desc.")
return self
def run(self) -> "FullWorkflow":
"""
Run the complete workflow: DataWorkflow -> ExperimentWorkflow.
Returns:
self (for chaining)
"""
logger.info(get_separator())
logger.info("Starting Full SciDER Workflow")
logger.info(get_separator())
# Step 1: Run DataWorkflow
if not self._run_data_phase():
self._finalize()
return self
# Step 2: Run ExperimentWorkflow
self._run_experiment_phase()
# Step 3: (Optional) Paper writing
if self.run_paper_writing and self.current_phase != "failed":
self._run_paper_writing_phase()
# Step 4: Finalize
self._finalize()
return self
def _run_data_phase(self) -> bool:
"""
Run DataWorkflow or HypoDataWorkflow depending on which input was provided.
Returns:
True if successful, False if failed
"""
logger.info("Phase 1: Running data phase")
self.current_phase = "data_analysis"
if self.feature_desc:
self._data_workflow = HypoDataWorkflow(
feature_desc=self.feature_desc,
workspace_path=self.workspace_path,
num_rows=self.num_rows,
user_query=self.user_query,
recursion_limit=self.data_agent_recursion_limit,
extra_data_desc=self.data_desc,
workspace_init_config=self.workspace_init_config,
)
else:
self._data_workflow = DataWorkflow(
data_path=self.data_path,
workspace_path=self.workspace_path,
recursion_limit=self.data_agent_recursion_limit,
data_desc=self.data_desc,
workspace_init_config=self.workspace_init_config,
)
try:
self._data_workflow.run()
if self._data_workflow.final_status == "success":
self.data_summary = self._data_workflow.data_summary
if isinstance(self._data_workflow, DataWorkflow):
self.data_agent_history = self._data_workflow.data_agent_history
self._data_workflow.save_summary()
logger.info("Data phase completed successfully")
return True
else:
self.error_message = self._data_workflow.error_message
self.current_phase = "failed"
return False
except Exception as e:
logger.exception("Data phase failed")
self.error_message = f"Data phase failed: {e}"
self.current_phase = "failed"
return False
def _run_experiment_phase(self) -> bool:
"""
Run ExperimentWorkflow to generate and execute experiments.
Returns:
True if successful, False if failed
"""
logger.info("Phase 2: Running ExperimentWorkflow")
self.current_phase = "experiment"
self._experiment_workflow = ExperimentWorkflow(
workspace_path=self.workspace_path,
user_query=self.user_query,
data_summary=self.data_summary,
repo_source=self.repo_source,
max_revisions=self.max_revisions,
recursion_limit=self.experiment_agent_recursion_limit,
workspace_init_config=self.workspace_init_config,
)
try:
self._experiment_workflow.run()
# Extract results and save summary to workspace
self.final_status = self._experiment_workflow.final_status
self.execution_results = self._experiment_workflow.execution_results
self._experiment_workflow.save_summary()
self.final_summary = self._compose_summary()
self.current_phase = "complete"
logger.info(f"ExperimentWorkflow completed: {self.final_status}")
return True
except Exception as e:
logger.exception("ExperimentWorkflow failed")
self.error_message = f"ExperimentWorkflow failed: {e}"
self.current_phase = "failed"
self.final_status = "failed"
return False
def _run_paper_writing_phase(self) -> bool:
"""Run the WritingWorkflow to turn the SciDER outputs into a paper."""
logger.info("Phase 3: Running WritingWorkflow for paper generation")
self.current_phase = "paper_writing"
idea_md = build_sparse_idea_from_query(self.user_query)
experiment_final_summary = (
self._experiment_workflow.final_summary if self._experiment_workflow else ""
)
experimental_log = build_experimental_log(
data_summary=self.data_summary,
experiment_summary=experiment_final_summary,
user_query=self.user_query,
)
return self._execute_writing_workflow(idea_md, experimental_log, experiment_final_summary)
def _execute_writing_workflow(
self, idea_md: str, experimental_log: str, experiment_summary: str
) -> bool:
"""Shared logic: instantiate, run, and extract results from WritingWorkflow."""
from scider.workflows.utils import run_paper_writing_phase
try:
self._writing_workflow = run_paper_writing_phase(
workspace_path=self.workspace_path,
idea_md=idea_md,
experimental_log=experimental_log,
user_query=self.user_query,
data_summary=self.data_summary,
experiment_summary=experiment_summary,
paper_workspace_path=self.paper_workspace_path,
paper_template_dir_path=self.paper_template_dir_path,
paper_template_tex_path=self.paper_template_tex_path,
paper_conference_guidelines_path=self.paper_conference_guidelines_path,
paper_agent_recursion_limit=self.paper_agent_recursion_limit,
workspace_init_config=self.workspace_init_config,
)
if self._writing_workflow.final_status != "success":
self.error_message = (
self._writing_workflow.error_message or "WritingWorkflow failed"
)
self.current_phase = "failed"
return False
self.paper_final_tex_path = self._writing_workflow.final_tex_path
self.paper_final_pdf_path = self._writing_workflow.final_pdf_path
self.paper_writing_summary = self._writing_workflow.final_summary
logger.info("WritingWorkflow completed successfully")
return True
except Exception as e:
logger.exception("WritingWorkflow failed")
self.error_message = f"WritingWorkflow failed: {e}"
self.current_phase = "failed"
return False
def _compose_summary(self) -> str:
"""Compose the final summary."""
exp_summary = (
self._experiment_workflow.final_summary if self._experiment_workflow else "N/A"
)
current_revision = (
self._experiment_workflow.current_revision if self._experiment_workflow else 0
)
paper_section = ""
if self.run_paper_writing and self._writing_workflow is not None:
paper_section = f"""
---
## Paper
{self.paper_writing_summary or "(no writing report available)"}
**Final LaTeX**: {self.paper_final_tex_path or "N/A"}
**Final PDF**: {self.paper_final_pdf_path or "N/A"}
"""
return f"""# Full SciDER Workflow Summary
## Data Analysis
{self.data_summary}
---
## Experiment Results
{exp_summary}{paper_section}
---
## Workflow Metadata
- **Data Source**: {f"data_path={self.data_path}" if self.data_path else f"feature_desc={self.feature_desc!r} ({self.num_rows} rows)"}
- **Workspace**: {self.workspace_path}
- **Repo Source**: {self.repo_source or 'Not specified'}
- **Final Status**: {self.final_status}
- **Total Revisions**: {current_revision}
"""
def _finalize(self):
"""Finalize the workflow."""
logger.info("Finalizing workflow")
if self.current_phase == "failed":
self.final_summary = f"# Workflow Failed\n\nError: {self.error_message}"
elif not self.final_summary:
self.final_summary = "# Workflow Completed\n\nNo summary available."
logger.info(get_separator())
logger.info(f"Workflow completed: {self.final_status}")
logger.info(get_separator())
def save_summary(self, path: str | Path | None = None) -> Path:
"""Save the final summary to a file."""
if path is None:
path = self.workspace_path / "workflow_summary.md"
path = Path(path)
path.write_text(self.final_summary)
logger.info(f"Summary saved to {path}")
return path
def run_full_workflow(
workspace_path: str | Path,
user_query: str,
data_path: str | Path | None = None,
feature_desc: str | None = None,
num_rows: int = 1000,
repo_source: str | None = None,
max_revisions: int = 3,
data_agent_recursion_limit: int = 100,
experiment_agent_recursion_limit: int = 100,
data_desc: str | None = None,
user_approval_enabled: bool = False,
run_paper_writing: bool = False,
paper_workspace_path: str | Path | None = None,
paper_template_dir_path: str | Path | None = None,
paper_template_tex_path: str | Path | None = None,
paper_conference_guidelines_path: str | Path | None = None,
paper_agent_recursion_limit: int = 150,
workspace_init_config: WorkspaceInitConfig | None = None,
) -> FullWorkflow:
"""
Convenience function to run the full SciDER workflow.
Args:
data_path: Path to the data file or directory to analyze
workspace_path: Workspace directory for the experiment
user_query: User's experiment objective
repo_source: Optional repository source (local path or git URL)
max_revisions: Maximum revision loops for experiment agent
data_agent_recursion_limit: Recursion limit for DataAgent (default=100)
experiment_agent_recursion_limit: Recursion limit for ExperimentAgent (default=100)
data_desc: Optional additional description of the data
Returns:
FullWorkflow: Completed workflow with results
Example:
>>> result = run_full_workflow(
... data_path="data/data.csv",
... workspace_path="workspace",
... user_query="Train an SVR model to predict prices",
... )
>>> print(result.final_summary)
Note:
These directories are then passed to DataWorkflow and ExperimentWorkflow.
"""
workflow = FullWorkflow(
data_path=Path(data_path) if data_path else None,
feature_desc=feature_desc,
num_rows=num_rows,
workspace_path=workspace_path,
user_query=user_query,
repo_source=repo_source,
max_revisions=max_revisions,
data_agent_recursion_limit=data_agent_recursion_limit,
experiment_agent_recursion_limit=experiment_agent_recursion_limit,
data_desc=data_desc,
run_paper_writing=run_paper_writing,
paper_workspace_path=Path(paper_workspace_path) if paper_workspace_path else None,
paper_template_dir_path=Path(paper_template_dir_path) if paper_template_dir_path else None,
paper_template_tex_path=Path(paper_template_tex_path) if paper_template_tex_path else None,
paper_conference_guidelines_path=(
Path(paper_conference_guidelines_path) if paper_conference_guidelines_path else None
),
paper_agent_recursion_limit=paper_agent_recursion_limit,
workspace_init_config=workspace_init_config,
)
with override_user_approval(user_approval_enabled):
return workflow.run()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Full SciDER Workflow - Run complete workflow (DataAgent -> ExperimentAgent)",
prog="python -m scider.workflows.full_workflow",
)
parser.add_argument("workspace_path", help="Workspace directory for the experiment")
parser.add_argument("user_query", help="User's experiment objective")
parser.add_argument(
"--data-path",
default=None,
help="Path to a real data file or directory (mutually exclusive with --feature-desc)",
)
parser.add_argument(
"--feature-desc",
default=None,
help="Description for synthetic data generation (mutually exclusive with --data-path)",
)
parser.add_argument(
"--num-rows",
type=int,
default=1000,
help="Number of rows to generate in hypo mode (default: 1000)",
)
parser.add_argument(
"--repo-source",
default=None,
help="Optional repository source (local path or git URL)",
)
parser.add_argument(
"--max-revisions",
type=int,
default=5,
help="Maximum revision loops for ExperimentAgent (default: 5)",
)
parser.add_argument(
"--data-recursion-limit",
type=int,
default=100,
help="Recursion limit for DataAgent (default: 100)",
)
parser.add_argument(
"--experiment-recursion-limit",
type=int,
default=100,
help="Recursion limit for ExperimentAgent (default: 100)",
)
parser.add_argument(
"--session-name",
default=None,
help="Custom session name (otherwise uses timestamp)",
)
parser.add_argument(
"--data-desc",
default=None,
help="Optional additional description of the data",
)
args = parser.parse_args()
result = run_full_workflow(
workspace_path=args.workspace_path,
user_query=args.user_query,
data_path=args.data_path,
feature_desc=args.feature_desc,
num_rows=args.num_rows,
repo_source=args.repo_source,
max_revisions=args.max_revisions,
data_agent_recursion_limit=args.data_recursion_limit,
experiment_agent_recursion_limit=args.experiment_recursion_limit,
data_desc=args.data_desc,
)
print("\n" + get_separator())
print("FULL WORKFLOW COMPLETE")
print(get_separator())
print(f"\nStatus: {result.final_status}")
print(f"\nFinal Summary:\n{result.final_summary}")