Team Ai
Apppublic

Tribh/devops-copilot

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
api.py163 linesDownload Raw Back to devops_copilot
1"""2api.py — FastAPI server exposing DevOps Copilot control plane.3 4Endpoints:5  POST /sessions/{session_id}/approve   — Grant human approval (unblocks engine)6  POST /sessions/{session_id}/reject    — Reject proposed action (marks false positive)7  GET  /sessions/{session_id}           — Inspect current session state8  GET  /sessions                        — List all active sessions9  GET  /health                          — Liveness probe10"""11from fastapi import FastAPI, HTTPException12from fastapi.middleware.cors import CORSMiddleware13from fastapi.responses import JSONResponse14from pydantic import BaseModel15from typing import Optional, List, Dict, Any16from devops_copilot.core.persistence import PersistenceLayer17from devops_copilot.core.observability import FALSE_POSITIVE_TOTAL, ACTIVE_INCIDENTS18from devops_copilot.tools.devops_tools import scrape_metrics, log_store19from devops_copilot.utils.logger import logger20import os21 22app = FastAPI(23    title="AgentNexus DevOps Copilot API",24    description="Control plane for the AI DevOps Copilot — approve/reject agent actions.",25    version="1.0.0",26)27 28@app.get("/")29async def read_root():30    return {"status": "ACTIVE", "version": "v3.0.0", "service": "DevOps Copilot", "message": "SRE Watchtower Ready"}31 32# Enable CORS for the portfolio frontend33app.add_middleware(34    CORSMiddleware,35    allow_origins=["*"], # In production, restrict to your portfolio URL36    allow_credentials=True,37    allow_methods=["*"],38    allow_headers=["*"],39)40 41# Shared persistence layer (same DB as the engine)42_db_path = os.getenv("DATABASE_URL", "agentnexus_state.db").replace("sqlite:///./", "")43persistence = PersistenceLayer(db_path=_db_path)44 45 46@app.on_event("startup")47async def _startup():48    await persistence.setup()49    logger.info("API server started.")50 51 52# ── Request/Response models ────────────────────────────────────────────────────53 54class ApproveRequest(BaseModel):55    reason: Optional[str] = "Approved via API"56 57 58class RejectRequest(BaseModel):59    reason: Optional[str] = "Rejected — false positive"60 61 62# ── Endpoints ──────────────────────────────────────────────────────────────────63 64@app.get("/health")65async def health():66    return {"status": "ok"}67 68 69@app.get("/sessions")70async def list_sessions():71    sessions = await persistence.list_sessions()72    return {"sessions": sessions, "count": len(sessions)}73 74 75@app.get("/sessions/{session_id}")76async def get_session(session_id: str):77    state = await persistence.load_session(session_id)78    if state is None:79        raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found.")80    return state81 82 83@app.post("/sessions/{session_id}/approve")84async def approve_action(session_id: str, body: ApproveRequest = ApproveRequest()):85    """86    Grant human approval for a PENDING_APPROVAL action.87    The engine polls session state — setting human_approved=True unblocks it.88    """89    try:90        await persistence.set_approved(session_id)91    except KeyError as e:92        raise HTTPException(status_code=404, detail=str(e))93    logger.info(f"Approved session {session_id}: {body.reason}")94    return {"session_id": session_id, "status": "approved", "reason": body.reason}95 96 97@app.post("/sessions/{session_id}/reject")98async def reject_action(session_id: str, body: RejectRequest = RejectRequest()):99    """100    Reject a pending action and track it as a false positive in Prometheus.101    """102    state = await persistence.load_session(session_id)103    if state is None:104        raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found.")105 106    # Record false positive metric107    FALSE_POSITIVE_TOTAL.inc()108 109    # Mark rejected so the engine can clean up or try a different plan110    state.setdefault("metadata", {})["human_approved"] = False111    state["metadata"]["rejected"] = True112    await persistence.save_session(session_id, state)113 114    logger.warning(f"Rejected session {session_id}: {body.reason}")115    return {"session_id": session_id, "status": "rejected", "reason": body.reason}116 117 118@app.get("/health-summary")119async def health_summary():120    """121    Returns a unified view of all monitored services.122    Used by the portfolio dashboard.123    """124    services = ["docugenie", "portfolio"]125    summary = []126    127    for svc in services:128        # Get live metrics summary from the tool129        metrics_raw = scrape_metrics(svc)130        131        # Simple health check from log volume/errors132        window = 300133        error_rate = log_store.get_error_rate_sync(svc, window_seconds=window)134        135        summary.append({136            "service": svc,137            "status": "HEALTHY" if error_rate < 0.1 else "DEGRADED",138            "metrics": metrics_raw,139            "error_rate": f"{error_rate*100:.1f}%"140        })141 142    return {143        "overall_status": "OK" if all(s["status"] == "HEALTHY" for s in summary) else "ISSUE_DETECTED",144        "active_incidents": 0, # Should tie into ACTIVE_INCIDENTS gauge145        "services": summary146    }147 148 149class ScenarioRequest(BaseModel):150    scenario: str151 152 153@app.post("/trigger-scenario")154async def trigger_scenario(body: ScenarioRequest):155    """156    Trigger a specific chaos scenario.157    """158    from devops_copilot.tools.devops_tools import trigger_chaos_simulation159    result = trigger_chaos_simulation(body.scenario)160    if "Error" in result or "Unknown" in result:161        raise HTTPException(status_code=400, detail=result)162    return {"status": "success", "message": result}163