"""Adaptive Loop router (P3-M5). Endpoints for the complete adaptive simulation closed loop, integrating all P3 components. """ from typing import Dict, Any, Optional, List from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from ..services.adaptive_loop import ( AdaptiveLoop, create_loop, get_loop, list_loops, LoopPhase ) router = APIRouter(prefix="/api/adaptive", tags=["Adaptive Loop"]) class CreateLoopRequest(BaseModel): """Request to create a new adaptive loop.""" user_requirement: str = Field(..., description="Natural language simulation requirement") total_budget: int = Field(default=80, ge=10, le=500) batch_size: int = Field(default=4, ge=1, le=16) class ReportResultsRequest(BaseModel): """Request to report simulation results.""" point_results: List[Dict[str, Any]] = Field(..., description="List of {point_id, metrics, status}") @router.post("/loops") def create_new_loop(request: CreateLoopRequest): """Create a new adaptive simulation loop. Initializes the loop with a user requirement. The loop will proceed through: plan generation -> L0 screening -> search init -> batch selection -> simulation -> analysis -> experience update. """ loop = create_loop( user_requirement=request.user_requirement, total_budget=request.total_budget, batch_size=request.batch_size, ) return { "loop_id": loop.loop_id, "phase": loop.phase.value, "message": "Loop created. Call /loops/{id}/generate-plan to start.", } @router.get("/loops") def get_all_loops(): """List all active adaptive loops.""" return {"loops": list_loops()} @router.get("/loops/{loop_id}") def get_loop_state(loop_id: str): """Get complete state of an adaptive loop.""" loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") return loop.get_state() @router.post("/loops/{loop_id}/generate-plan") def generate_loop_plan(loop_id: str): """Step 1: Generate simulation plan from natural language. Uses Kimi k3 AI model to convert the user requirement into a structured simulation plan with scan variables, search strategy, and acceptance criteria. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.generate_plan() except Exception as e: raise HTTPException(status_code=500, detail=f"Plan generation failed: {str(e)}") @router.post("/loops/{loop_id}/init-search") def init_loop_search(loop_id: str): """Step 2-3: Initialize feasibility-first search. Converts plan scan variables to search parameters, runs L0 pre-screening, and generates initial LHS batch. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.initialize_search() except Exception as e: raise HTTPException(status_code=500, detail=f"Search init failed: {str(e)}") @router.post("/loops/{loop_id}/next-batch") def get_loop_next_batch(loop_id: str): """Step 3 (loop): Select next batch of points to simulate. Uses active learning with trust region refinement to balance exploration and exploitation. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.get_next_batch() except Exception as e: raise HTTPException(status_code=500, detail=f"Batch selection failed: {str(e)}") @router.post("/loops/{loop_id}/submit-batch") def submit_loop_batch(loop_id: str): """Step 3b: Submit the current pending batch to the local executor. Creates an adaptive_batch task in the task system; the local Motor-CAD executor claims it, runs each point, and reports results back through /report-results to continue the loop. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.submit_batch_to_executor() except Exception as e: raise HTTPException(status_code=500, detail=f"Batch submission failed: {str(e)}") @router.post("/loops/{loop_id}/report-results") def report_loop_results(loop_id: str, request: ReportResultsRequest): """Step 4-5: Report simulation results and trigger AI analysis. Reports results for the current batch, updates search state, runs AI result analysis with multi-fidelity calibration and confidence grading. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.report_results(request.point_results) except Exception as e: raise HTTPException(status_code=500, detail=f"Result reporting failed: {str(e)}") @router.post("/loops/{loop_id}/update-experience") def update_loop_experience(loop_id: str): """Step 6: Extract AI insights and update experience library. Uses AI to extract design rules, failure patterns, and parameter sensitivity from accumulated simulation results. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") try: return loop.update_experience() except Exception as e: raise HTTPException(status_code=500, detail=f"Experience update failed: {str(e)}") @router.get("/loops/{loop_id}/export") def export_loop_state(loop_id: str): """Export the full loop state (plan + results + search) for checkpointing.""" loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") return loop.export_state() @router.post("/loops/import") def import_loop_state(payload: Dict[str, Any]): """Resume a loop from an exported state (checkpoint restore). Body is the JSON returned by GET /loops/{id}/export. Rebuilds the loop (search included) and registers it in the loop registry. """ if not isinstance(payload, dict) or not payload.get("loop_id"): raise HTTPException(status_code=400, detail="invalid exported loop state") try: loop = AdaptiveLoop.restore_state(payload) except Exception as exc: raise HTTPException(status_code=400, detail=f"restore failed: {exc}") return {"loop_id": loop.loop_id, "phase": loop.phase.value, "message": "loop restored"} def check_loop_completion(loop_id: str): """Check if the adaptive loop should terminate. Returns completion status based on convergence and budget. """ loop = get_loop(loop_id) if not loop: raise HTTPException(status_code=404, detail=f"Loop {loop_id} not found") return loop.check_completion() @router.get("/phases") def get_loop_phases(): """Get all possible loop phases.""" return { "phases": [ {"name": p.name, "value": p.value, "description": _phase_description(p)} for p in LoopPhase ] } def _phase_description(phase: LoopPhase) -> str: """Get human-readable description of a phase.""" descriptions = { LoopPhase.INIT: "Loop initialized, waiting for plan generation", LoopPhase.PLAN_GENERATED: "AI plan generated from natural language", LoopPhase.L0_SCREENED: "L0 pre-screening completed", LoopPhase.SEARCH_INITIALIZED: "Feasibility-first search initialized with LHS batch", LoopPhase.BATCH_SELECTED: "Next batch of points selected via active learning", LoopPhase.SIMULATION_RUNNING: "Simulation execution in progress", LoopPhase.RESULTS_ANALYZED: "AI result analysis completed with confidence grading", LoopPhase.EXPERIENCE_UPDATED: "Experience library updated with extracted insights", LoopPhase.CONVERGED: "Search converged, loop complete", LoopPhase.BUDGET_EXHAUSTED: "Simulation budget exhausted", LoopPhase.COMPLETED: "Loop completed successfully", } return descriptions.get(phase, phase.value)