Files
Atomizer/atomizer-dashboard/backend/api/routes/devloop.py

417 lines
12 KiB
Python
Raw Normal View History

"""
DevLoop API Endpoints - Closed-loop development orchestration.
Provides REST API and WebSocket for:
- Starting/stopping development cycles
- Monitoring progress
- Executing single phases
- Viewing history and learnings
"""
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Any, Dict, List, Optional
import asyncio
import json
import sys
from pathlib import Path
from datetime import datetime
# Add project root to path
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
router = APIRouter(prefix="/devloop", tags=["devloop"])
# Global orchestrator instance
_orchestrator = None
_active_cycle = None
_websocket_clients: List[WebSocket] = []
def get_orchestrator():
"""Get or create the DevLoop orchestrator."""
global _orchestrator
if _orchestrator is None:
from optimization_engine.devloop import DevLoopOrchestrator
_orchestrator = DevLoopOrchestrator(
{
"dashboard_url": "http://localhost:8000",
"websocket_url": "ws://localhost:8000",
"studies_dir": str(Path(__file__).parent.parent.parent.parent.parent / "studies"),
"learning_enabled": True,
}
)
# Subscribe to state updates
_orchestrator.subscribe(_broadcast_state_update)
return _orchestrator
def _broadcast_state_update(state):
"""Broadcast state updates to all WebSocket clients."""
asyncio.create_task(
_send_to_all_clients(
{
"type": "state_update",
"state": {
"phase": state.phase.value,
"iteration": state.iteration,
"current_task": state.current_task,
"last_update": state.last_update,
},
}
)
)
async def _send_to_all_clients(message: Dict):
"""Send message to all connected WebSocket clients."""
disconnected = []
for client in _websocket_clients:
try:
await client.send_json(message)
except Exception:
disconnected.append(client)
# Clean up disconnected clients
for client in disconnected:
if client in _websocket_clients:
_websocket_clients.remove(client)
# ============================================================================
# Request/Response Models
# ============================================================================
class StartCycleRequest(BaseModel):
"""Request to start a development cycle."""
objective: str = Field(..., description="What to achieve")
context: Optional[Dict[str, Any]] = Field(default=None, description="Additional context")
max_iterations: Optional[int] = Field(default=10, description="Maximum iterations")
class StepRequest(BaseModel):
"""Request to execute a single step."""
phase: str = Field(..., description="Phase to execute: plan, implement, test, analyze")
data: Optional[Dict[str, Any]] = Field(default=None, description="Phase-specific data")
class CycleStatusResponse(BaseModel):
"""Response with cycle status."""
active: bool
phase: str
iteration: int
current_task: Optional[str]
last_update: str
# ============================================================================
# REST Endpoints
# ============================================================================
@router.get("/status")
async def get_status() -> CycleStatusResponse:
"""Get current DevLoop status."""
orchestrator = get_orchestrator()
state = orchestrator.get_state()
return CycleStatusResponse(
active=state["phase"] != "idle",
phase=state["phase"],
iteration=state["iteration"],
current_task=state.get("current_task"),
last_update=state["last_update"],
)
@router.post("/start")
async def start_cycle(request: StartCycleRequest, background_tasks: BackgroundTasks):
"""
Start a new development cycle.
The cycle runs in the background and broadcasts progress via WebSocket.
"""
global _active_cycle
orchestrator = get_orchestrator()
# Check if already running
if orchestrator.state.phase.value != "idle":
raise HTTPException(status_code=409, detail="A development cycle is already running")
# Start cycle in background
async def run_cycle():
global _active_cycle
try:
result = await orchestrator.run_development_cycle(
objective=request.objective,
context=request.context,
max_iterations=request.max_iterations,
)
_active_cycle = result
# Broadcast completion
await _send_to_all_clients(
{
"type": "cycle_complete",
"result": {
"objective": result.objective,
"status": result.status,
"iterations": len(result.iterations),
"duration_seconds": result.total_duration_seconds,
},
}
)
except Exception as e:
await _send_to_all_clients({"type": "cycle_error", "error": str(e)})
background_tasks.add_task(run_cycle)
return {
"message": "Development cycle started",
"objective": request.objective,
}
@router.post("/stop")
async def stop_cycle():
"""Stop the current development cycle."""
orchestrator = get_orchestrator()
if orchestrator.state.phase.value == "idle":
raise HTTPException(status_code=400, detail="No active cycle to stop")
# Set state to idle (will stop at next phase boundary)
orchestrator._update_state(phase=orchestrator.state.phase.__class__.IDLE, task="Stopping...")
return {"message": "Cycle stop requested"}
@router.post("/step")
async def execute_step(request: StepRequest):
"""
Execute a single phase step.
Useful for manual control or debugging.
"""
orchestrator = get_orchestrator()
if request.phase == "plan":
objective = request.data.get("objective", "") if request.data else ""
context = request.data.get("context") if request.data else None
result = await orchestrator.step_plan(objective, context)
elif request.phase == "implement":
plan = request.data if request.data else {}
result = await orchestrator.step_implement(plan)
elif request.phase == "test":
scenarios = request.data.get("scenarios", []) if request.data else []
result = await orchestrator.step_test(scenarios)
elif request.phase == "analyze":
test_results = request.data if request.data else {}
result = await orchestrator.step_analyze(test_results)
else:
raise HTTPException(
status_code=400,
detail=f"Unknown phase: {request.phase}. Valid: plan, implement, test, analyze",
)
return {"phase": request.phase, "result": result}
@router.get("/history")
async def get_history():
"""Get history of past development cycles."""
orchestrator = get_orchestrator()
return orchestrator.export_history()
@router.get("/last-cycle")
async def get_last_cycle():
"""Get details of the most recent cycle."""
global _active_cycle
if _active_cycle is None:
raise HTTPException(status_code=404, detail="No cycle has been run yet")
return {
"objective": _active_cycle.objective,
"status": _active_cycle.status,
"start_time": _active_cycle.start_time,
"end_time": _active_cycle.end_time,
"iterations": [
{
"iteration": it.iteration,
"success": it.success,
"duration_seconds": it.duration_seconds,
"has_plan": it.plan is not None,
"has_tests": it.test_results is not None,
"has_fixes": it.fixes is not None,
}
for it in _active_cycle.iterations
],
"total_duration_seconds": _active_cycle.total_duration_seconds,
}
@router.get("/health")
async def health_check():
"""Check DevLoop system health."""
orchestrator = get_orchestrator()
# Check dashboard connection
from optimization_engine.devloop import DashboardTestRunner
runner = DashboardTestRunner()
dashboard_health = await runner.run_health_check()
return {
"devloop": "healthy",
"orchestrator_state": orchestrator.get_state()["phase"],
"dashboard": dashboard_health,
}
# ============================================================================
# WebSocket Endpoint
# ============================================================================
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""
WebSocket endpoint for real-time DevLoop updates.
Messages sent:
- state_update: Phase/iteration changes
- cycle_complete: Cycle finished
- cycle_error: Cycle failed
- test_progress: Individual test results
"""
await websocket.accept()
_websocket_clients.append(websocket)
orchestrator = get_orchestrator()
try:
# Send initial state
await websocket.send_json(
{
"type": "connection_ack",
"state": orchestrator.get_state(),
}
)
# Handle incoming messages
while True:
try:
data = await asyncio.wait_for(websocket.receive_json(), timeout=30.0)
msg_type = data.get("type")
if msg_type == "ping":
await websocket.send_json({"type": "pong"})
elif msg_type == "get_state":
await websocket.send_json(
{
"type": "state",
"state": orchestrator.get_state(),
}
)
elif msg_type == "start_cycle":
# Allow starting cycle via WebSocket
objective = data.get("objective", "")
context = data.get("context")
asyncio.create_task(orchestrator.run_development_cycle(objective, context))
await websocket.send_json(
{
"type": "cycle_started",
"objective": objective,
}
)
except asyncio.TimeoutError:
# Send heartbeat
await websocket.send_json({"type": "heartbeat"})
except WebSocketDisconnect:
pass
finally:
if websocket in _websocket_clients:
_websocket_clients.remove(websocket)
# ============================================================================
# Convenience Endpoints for Common Tasks
# ============================================================================
@router.post("/create-study")
async def create_study_cycle(
study_name: str,
problem_statement: Optional[str] = None,
background_tasks: BackgroundTasks = None,
):
"""
Convenience endpoint to start a study creation cycle.
This is a common workflow that combines planning, implementation, and testing.
"""
orchestrator = get_orchestrator()
context = {
"study_name": study_name,
"task_type": "create_study",
}
if problem_statement:
context["problem_statement"] = problem_statement
# Start the cycle
async def run_cycle():
result = await orchestrator.run_development_cycle(
objective=f"Create optimization study: {study_name}",
context=context,
)
return result
if background_tasks:
background_tasks.add_task(run_cycle)
return {"message": f"Study creation cycle started for '{study_name}'"}
else:
result = await run_cycle()
return {
"message": f"Study '{study_name}' creation completed",
"status": result.status,
"iterations": len(result.iterations),
}
@router.post("/run-tests")
async def run_tests(scenarios: List[Dict[str, Any]]):
"""
Run a set of test scenarios directly.
Useful for testing specific features without a full cycle.
"""
from optimization_engine.devloop import DashboardTestRunner
runner = DashboardTestRunner()
results = await runner.run_test_suite(scenarios)
return results