"""Experiment API(Phase 4):列表 / 详情 / 一键复跑。""" from __future__ import annotations import json from datetime import datetime from fastapi import APIRouter, BackgroundTasks, HTTPException from app.api.deps import DbSession, ExperimentRepoDep, JobRepoDep from app.api.jobs import _bg_factories from app.application.services.job_executor import execute_job, new_id from app.domain.entities.research import ( ExperimentRecord, JobRecord, JobStatus, ) router = APIRouter(prefix="/experiments", tags=["experiments"]) def _experiment_meta(exp: ExperimentRecord) -> dict: spec = json.loads(exp.spec_json) return { "id": exp.id, "kind": exp.kind, "factors": [f["name"] for f in spec.get("factors", [])], "period": spec.get("period"), "rebalance": spec.get("rebalance"), "top_n": spec.get("selection", {}).get("top_n"), "summary_text": exp.summary_text, "code_version": exp.code_version, "created_at": exp.created_at, } def _experiment_full(exp: ExperimentRecord) -> dict: from app.api.jobs import _decode_result return { **_experiment_meta(exp), "spec": json.loads(exp.spec_json), "result": _decode_result(exp.kind, exp.result_json), } @router.get("", summary="Experiment 列表") def list_experiments(experiment_repo: ExperimentRepoDep) -> list[dict]: return [_experiment_meta(e) for e in experiment_repo.list_recent(limit=50)] @router.get("/{experiment_id}", summary="Experiment 详情(含完整结果)") def get_experiment(experiment_id: str, experiment_repo: ExperimentRepoDep) -> dict: exp = experiment_repo.get(experiment_id) if exp is None: raise HTTPException(status_code=404, detail=f"Experiment {experiment_id} 不存在") return _experiment_full(exp) @router.post("/{experiment_id}/rerun", summary="一键复跑(AGENT §21:历史实验可重放)") def rerun_experiment( experiment_id: str, background: BackgroundTasks, session: DbSession, experiment_repo: ExperimentRepoDep, job_repo: JobRepoDep, ) -> dict: exp = experiment_repo.get(experiment_id) if exp is None: raise HTTPException(status_code=404, detail=f"Experiment {experiment_id} 不存在") job = JobRecord( id=new_id("JOB"), kind=exp.kind, spec_json=exp.spec_json, status=JobStatus.QUEUED, created_at=datetime.now(), ) job_repo.create(job) session.commit() background.add_task(execute_job, job.id, **_bg_factories()) return {"job_id": job.id, "status": job.status, "origin_experiment": exp.id}