Files
ai-agents/dashboard/app.py
T

266 lines
8.3 KiB
Python

from fastapi import FastAPI, Depends, HTTPException, Request, Response, Cookie
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
from sqlalchemy.orm import Session
from pydantic import BaseModel
from datetime import datetime, timezone
from typing import Optional
import hashlib
import hmac
import json
import os
import secrets
from database import get_db, init_db
from models import Agent, Run
app = FastAPI(title="Agent Command Center", version="1.0.0")
# --- Auth ---
AUTH_USER = os.environ.get("AUTH_USER", "eric")
AUTH_PASS = os.environ.get("AUTH_PASS", "Kj8#mPx2vQ!nR4wL")
SESSION_SECRET = os.environ.get("SESSION_SECRET", secrets.token_hex(32))
# In-memory session store
_sessions: dict[str, str] = {}
def create_session(username: str) -> str:
token = secrets.token_urlsafe(32)
_sessions[token] = username
return token
def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[str]:
if session and session in _sessions:
return _sessions[session]
return None
def require_auth(session: Optional[str] = Cookie(None)):
user = get_current_user(session)
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
class LoginRequest(BaseModel):
username: str
password: str
@app.post("/api/login")
def login(creds: LoginRequest, response: Response):
if creds.username == AUTH_USER and creds.password == AUTH_PASS:
token = create_session(creds.username)
response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7)
return {"status": "ok", "user": creds.username}
raise HTTPException(status_code=401, detail="Invalid credentials")
@app.post("/api/logout")
def logout(response: Response, session: Optional[str] = Cookie(None)):
if session and session in _sessions:
del _sessions[session]
response.delete_cookie("session")
return {"status": "ok"}
@app.get("/login")
def login_page():
return FileResponse("static/login.html")
# --- Pydantic schemas ---
class AgentCreate(BaseModel):
id: str
name: str
description: str = ""
schedule: str = "manual"
class AgentUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
schedule: Optional[str] = None
status: Optional[str] = None
class RunCreate(BaseModel):
status: str = "running"
output: str = ""
error: str = ""
metadata: dict = {}
class RunUpdate(BaseModel):
status: Optional[str] = None
output: Optional[str] = None
error: Optional[str] = None
finished_at: Optional[str] = None
metadata: Optional[dict] = None
# --- API routes ---
@app.get("/api/health")
def health():
return {"status": "ok", "service": "agent-command-center"}
@app.get("/api/agents")
def list_agents(user: str = Depends(require_auth), db: Session = Depends(get_db)):
agents = db.query(Agent).all()
result = []
for a in agents:
last_run = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).first()
recent_runs = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).limit(10).all()
success_streak = 0
for r in recent_runs:
if r.status == "success":
success_streak += 1
else:
break
result.append({
"id": a.id,
"name": a.name,
"description": a.description,
"schedule": a.schedule,
"status": a.status,
"created_at": a.created_at.isoformat() if a.created_at else None,
"last_run": {
"status": last_run.status,
"started_at": last_run.started_at.isoformat() if last_run.started_at else None,
"finished_at": last_run.finished_at.isoformat() if last_run.finished_at else None,
} if last_run else None,
"success_streak": success_streak,
"total_runs": db.query(Run).filter(Run.agent_id == a.id).count(),
})
return result
@app.post("/api/agents")
def create_agent(agent: AgentCreate, db: Session = Depends(get_db)):
existing = db.query(Agent).filter(Agent.id == agent.id).first()
if existing:
raise HTTPException(status_code=409, detail="Agent already exists")
new_agent = Agent(
id=agent.id,
name=agent.name,
description=agent.description,
schedule=agent.schedule,
)
db.add(new_agent)
db.commit()
return {"id": new_agent.id, "status": "created"}
@app.get("/api/agents/{agent_id}")
def get_agent(agent_id: str, user: str = Depends(require_auth), db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
runs = db.query(Run).filter(Run.agent_id == agent_id).order_by(Run.started_at.desc()).limit(50).all()
return {
"id": agent.id,
"name": agent.name,
"description": agent.description,
"schedule": agent.schedule,
"status": agent.status,
"created_at": agent.created_at.isoformat() if agent.created_at else None,
"runs": [{
"id": r.id,
"started_at": r.started_at.isoformat() if r.started_at else None,
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
"status": r.status,
"output": r.output,
"error": r.error,
"metadata": r.metadata_,
} for r in runs],
}
@app.put("/api/agents/{agent_id}")
def update_agent(agent_id: str, update: AgentUpdate, db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
for field, value in update.model_dump(exclude_none=True).items():
setattr(agent, field, value)
db.commit()
return {"id": agent.id, "status": "updated"}
@app.post("/api/agents/{agent_id}/runs")
def create_run(agent_id: str, run: RunCreate, db: Session = Depends(get_db)):
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent not found")
new_run = Run(
agent_id=agent_id,
status=run.status,
output=run.output,
error=run.error,
metadata_=run.metadata,
)
if run.status in ("success", "failed"):
new_run.finished_at = datetime.now(timezone.utc)
db.add(new_run)
db.commit()
return {"id": new_run.id, "status": new_run.status}
@app.put("/api/runs/{run_id}")
def update_run(run_id: int, update: RunUpdate, db: Session = Depends(get_db)):
run = db.query(Run).filter(Run.id == run_id).first()
if not run:
raise HTTPException(status_code=404, detail="Run not found")
if update.status is not None:
run.status = update.status
if update.output is not None:
run.output = update.output
if update.error is not None:
run.error = update.error
if update.metadata is not None:
run.metadata_ = update.metadata
if update.finished_at is not None:
run.finished_at = datetime.fromisoformat(update.finished_at)
elif update.status in ("success", "failed"):
run.finished_at = datetime.now(timezone.utc)
db.commit()
return {"id": run.id, "status": run.status}
@app.get("/api/runs")
def list_runs(limit: int = 50, user: str = Depends(require_auth), db: Session = Depends(get_db)):
runs = db.query(Run).order_by(Run.started_at.desc()).limit(limit).all()
return [{
"id": r.id,
"agent_id": r.agent_id,
"started_at": r.started_at.isoformat() if r.started_at else None,
"finished_at": r.finished_at.isoformat() if r.finished_at else None,
"status": r.status,
"output": r.output,
"error": r.error,
"metadata": r.metadata_,
} for r in runs]
# --- Static files (frontend) ---
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
def root(session: Optional[str] = Cookie(None)):
user = get_current_user(session)
if not user:
return RedirectResponse("/login", status_code=302)
return FileResponse("static/index.html")
# --- Startup ---
@app.on_event("startup")
def startup():
init_db()