SciDER / scider /workflows /hypo_data_workflow.py
leonardklin's picture
Upload 355 files
1499363 verified
Raw
History Blame Contribute Delete
7.37 kB
"""Hypothetical Data Workflow.
Generates synthetic data from user-described features, gets user approval on
the data specification, then runs the standard DataWorkflow on the generated data.
"""
from __future__ import annotations
from pathlib import Path
from typing import Literal
from loguru import logger
from pydantic import BaseModel
from scider.core import constant
from scider.core.approval import ApprovalResult, _get_handler
from scider.core.code_env import WorkspaceInitConfig
from scider.core.hypo_data_gen import (
DataGenSpec,
format_spec_for_review,
generate_csv_from_spec,
generate_data_spec,
)
from scider.workflows.data_workflow import DataWorkflow
class HypoDataWorkflow(BaseModel):
"""Workflow that generates hypothetical data then analyzes it.
Usage::
w = HypoDataWorkflow(
feature_desc="A dataset about iris flowers with sepal/petal measurements",
workspace_path=Path("workspace"),
)
w.run()
print(w.data_summary)
"""
# ── Input ──
feature_desc: str
workspace_path: Path
num_rows: int = 1000
user_query: str = ""
recursion_limit: int = 100
# None β†’ LocalEnv's default (uv-managed, auto `uv init`). Forwarded to the
# wrapped DataWorkflow.
workspace_init_config: WorkspaceInitConfig | None = None
extra_data_desc: str | None = None # extra context injected by FullWorkflow callers
# Memory directories (optional)
# ── Internal state ──
current_phase: Literal[
"init", "spec_gen", "data_gen", "data_analysis", "complete", "failed"
] = "init"
data_spec: DataGenSpec | None = None
generated_data_path: Path | None = None
data_summary: str | None = None
final_status: str = "init"
error_message: str | None = None
model_config = {"arbitrary_types_allowed": True}
# ── Max retry attempts for spec generation ──
MAX_SPEC_RETRIES: int = 3
def run(self) -> "HypoDataWorkflow":
"""Run the full workflow: generate spec β†’ approve β†’ generate CSV β†’ analyze."""
try:
# Phase 1: Generate and approve data spec
self.current_phase = "spec_gen"
spec = self._generate_and_approve_spec()
if spec is None:
self.final_status = "failed"
self.error_message = "Data spec generation was rejected by user."
return self
self.data_spec = spec
# Phase 2: Generate CSV from spec
self.current_phase = "data_gen"
self.generated_data_path = generate_csv_from_spec(
spec, self.workspace_path, self.num_rows
)
logger.info(f"Generated data at: {self.generated_data_path}")
# Phase 3: Run data analysis
self.current_phase = "data_analysis"
data_desc = (
f"This is hypothetical/synthetic data generated from the following description:\n"
f"{self.feature_desc}\n\n"
f"The data was generated with known distributions "
f"({self.num_rows} rows, {len(spec.features)} features). "
f"Focus on verifying the data structure and providing analysis insights."
)
if self.extra_data_desc:
data_desc += f"\n\n{self.extra_data_desc}"
query = self.user_query or f"Analyze this synthetic dataset: {self.feature_desc}"
data_workflow = DataWorkflow(
data_path=self.generated_data_path,
workspace_path=self.workspace_path,
recursion_limit=self.recursion_limit,
data_desc=data_desc,
workspace_init_config=self.workspace_init_config,
)
data_workflow.run()
self.data_summary = data_workflow.data_summary
self.final_status = data_workflow.final_status
self.error_message = data_workflow.error_message
self.current_phase = "complete" if self.final_status == "success" else "failed"
except Exception as e:
logger.exception("HypoDataWorkflow failed")
self.final_status = "failed"
self.error_message = str(e)
self.current_phase = "failed"
return self
def _generate_and_approve_spec(self) -> DataGenSpec | None:
"""Generate data spec via LLM, then request user approval.
Retries up to MAX_SPEC_RETRIES times if user provides feedback.
Returns None if user ultimately rejects.
"""
handler = _get_handler()
feedback = None
for attempt in range(self.MAX_SPEC_RETRIES):
logger.info(f"Generating data spec (attempt {attempt + 1}/{self.MAX_SPEC_RETRIES})")
try:
spec = generate_data_spec(self.feature_desc, self.num_rows, feedback)
except Exception as e:
logger.error(f"Failed to generate data spec: {e}")
if attempt < self.MAX_SPEC_RETRIES - 1:
feedback = f"Previous attempt failed with error: {e}. Please try again."
continue
raise
# Request approval
if not constant.USER_APPROVAL_ENABLED:
logger.info("Auto-approved data spec (USER_APPROVAL_ENABLED=false)")
return spec
summary = format_spec_for_review(spec)
response = handler.request_approval(
node_name="hypo_data_spec",
summary=summary,
title="Review the generated data specification before generating data.",
)
if response.result == ApprovalResult.APPROVED:
logger.info("Data spec approved by user")
return spec
elif response.result == ApprovalResult.FEEDBACK:
feedback = response.feedback
logger.info(f"User provided feedback: {feedback}")
continue
else:
logger.info("Data spec rejected by user")
return None
logger.warning("Max spec retries reached")
return None
def save_summary(self, path: str | Path | None = None) -> Path:
"""Save the data summary to a file."""
if path is None:
path = self.workspace_path / "data_analysis.md"
path = Path(path)
path.write_text(self.data_summary or "")
logger.info(f"Data summary saved to {path}")
return path
def run_hypo_data_workflow(
feature_desc: str,
workspace_path: str | Path,
num_rows: int = 1000,
user_query: str = "",
recursion_limit: int = 100,
user_approval_enabled: bool = True,
workspace_init_config: WorkspaceInitConfig | None = None,
) -> HypoDataWorkflow:
"""Convenience function to run the hypothetical data workflow."""
from scider.core.constant import override_user_approval
with override_user_approval(user_approval_enabled):
w = HypoDataWorkflow(
feature_desc=feature_desc,
workspace_path=Path(workspace_path),
num_rows=num_rows,
user_query=user_query,
recursion_limit=recursion_limit,
workspace_init_config=workspace_init_config,
)
w.run()
return w