"""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}/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}/check-completion") 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)