v2.0: Multi-user platform with agent catalog, admin panel, LLM providers
This commit is contained in:
+421
-191
@@ -1,63 +1,125 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request, Response, Cookie
|
||||
from fastapi import FastAPI, Depends, HTTPException, Response, Cookie
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
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")
|
||||
from models import User, AgentCatalog, AgentInstance, Run, LLMProvider
|
||||
|
||||
app = FastAPI(title="Agent Command Center", version="2.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] = {}
|
||||
_sessions: dict[str, dict] = {} # token -> {user_id, username, role}
|
||||
|
||||
|
||||
def create_session(username: str) -> str:
|
||||
def hash_password(password: str) -> str:
|
||||
salt = secrets.token_hex(16)
|
||||
h = hashlib.sha256((salt + password).encode()).hexdigest()
|
||||
return f"{salt}:{h}"
|
||||
|
||||
|
||||
def verify_password(password: str, password_hash: str) -> bool:
|
||||
salt, h = password_hash.split(":", 1)
|
||||
return hashlib.sha256((salt + password).encode()).hexdigest() == h
|
||||
|
||||
|
||||
def create_session(user: User) -> str:
|
||||
token = secrets.token_urlsafe(32)
|
||||
_sessions[token] = username
|
||||
_sessions[token] = {"user_id": user.id, "username": user.username, "role": user.role}
|
||||
return token
|
||||
|
||||
|
||||
def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[str]:
|
||||
def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[dict]:
|
||||
if session and session in _sessions:
|
||||
return _sessions[session]
|
||||
return None
|
||||
|
||||
|
||||
def require_auth(session: Optional[str] = Cookie(None)):
|
||||
def require_auth(session: Optional[str] = Cookie(None)) -> dict:
|
||||
user = get_current_user(session)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return user
|
||||
|
||||
|
||||
def require_admin(session: Optional[str] = Cookie(None)) -> dict:
|
||||
user = require_auth(session)
|
||||
if user["role"] != "admin":
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
return user
|
||||
|
||||
|
||||
# --- Schemas ---
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class InstanceCreate(BaseModel):
|
||||
catalog_id: str
|
||||
name: Optional[str] = None
|
||||
config: dict = {}
|
||||
schedule: Optional[str] = None
|
||||
|
||||
class InstanceUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
config: Optional[dict] = None
|
||||
schedule: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
class RunCreate(BaseModel):
|
||||
status: str = "running"
|
||||
output: str = ""
|
||||
error: str = ""
|
||||
metadata: dict = {}
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
display_name: str = ""
|
||||
role: str = "user"
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
display_name: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
class LLMProviderCreate(BaseModel):
|
||||
name: str
|
||||
provider_type: str = "anthropic"
|
||||
api_url: str = ""
|
||||
api_key: str = ""
|
||||
default_model: str = ""
|
||||
is_default: bool = False
|
||||
|
||||
class LLMProviderUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
provider_type: Optional[str] = None
|
||||
api_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
default_model: Optional[str] = None
|
||||
is_default: Optional[bool] = None
|
||||
|
||||
|
||||
# --- Auth Routes ---
|
||||
|
||||
@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")
|
||||
def login(creds: LoginRequest, response: Response, db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.username == creds.username).first()
|
||||
if not user or not verify_password(creds.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
token = create_session(user)
|
||||
response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7)
|
||||
return {"status": "ok", "user": user.username, "role": user.role, "display_name": user.display_name}
|
||||
|
||||
|
||||
@app.post("/api/logout")
|
||||
@@ -68,155 +130,174 @@ def logout(response: Response, session: Optional[str] = Cookie(None)):
|
||||
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"
|
||||
config: dict = {}
|
||||
|
||||
class AgentUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
schedule: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
config: Optional[dict] = 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/{agent_id}/config")
|
||||
def get_agent_config(agent_id: str, db: Session = Depends(get_db)):
|
||||
"""Internal endpoint for agents to fetch their config. No auth required."""
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
return agent.config or {}
|
||||
|
||||
|
||||
@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,
|
||||
"config": a.config or {},
|
||||
"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,
|
||||
config=agent.config,
|
||||
)
|
||||
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()
|
||||
@app.get("/api/me")
|
||||
def me(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||
return {
|
||||
"id": agent.id,
|
||||
"name": agent.name,
|
||||
"description": agent.description,
|
||||
"schedule": agent.schedule,
|
||||
"status": agent.status,
|
||||
"config": agent.config or {},
|
||||
"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],
|
||||
"id": u.id, "username": u.username, "display_name": u.display_name,
|
||||
"role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@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")
|
||||
updates = update.model_dump(exclude_none=True)
|
||||
if "config" in updates:
|
||||
current_config = agent.config or {}
|
||||
current_config.update(updates.pop("config"))
|
||||
agent.config = current_config
|
||||
for field, value in updates.items():
|
||||
setattr(agent, field, value)
|
||||
# --- Health ---
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "agent-command-center", "version": "2.0.0"}
|
||||
|
||||
|
||||
# --- Agent Catalog ---
|
||||
|
||||
@app.get("/api/catalog")
|
||||
def list_catalog(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
entries = db.query(AgentCatalog).all()
|
||||
# Check which ones this user already has instances of
|
||||
user_instance_ids = {
|
||||
i.catalog_id for i in db.query(AgentInstance).filter(AgentInstance.user_id == user["user_id"]).all()
|
||||
}
|
||||
return [{
|
||||
"id": e.id, "name": e.name, "description": e.description,
|
||||
"category": e.category, "config_schema": e.config_schema or {},
|
||||
"default_config": e.default_config or {},
|
||||
"supports_schedule": e.supports_schedule, "is_sub_agent": e.is_sub_agent,
|
||||
"enabled": e.id in user_instance_ids,
|
||||
} for e in entries]
|
||||
|
||||
|
||||
@app.get("/api/catalog/{catalog_id}")
|
||||
def get_catalog_entry(catalog_id: str, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404)
|
||||
return {
|
||||
"id": entry.id, "name": entry.name, "description": entry.description,
|
||||
"category": entry.category, "config_schema": entry.config_schema or {},
|
||||
"default_config": entry.default_config or {},
|
||||
"supports_schedule": entry.supports_schedule, "is_sub_agent": entry.is_sub_agent,
|
||||
}
|
||||
|
||||
|
||||
# --- Agent Instances (user-scoped) ---
|
||||
|
||||
def serialize_instance(inst, db):
|
||||
last_run = db.query(Run).filter(Run.instance_id == inst.id).order_by(Run.started_at.desc()).first()
|
||||
recent = db.query(Run).filter(Run.instance_id == inst.id).order_by(Run.started_at.desc()).limit(10).all()
|
||||
streak = 0
|
||||
for r in recent:
|
||||
if r.status == "success":
|
||||
streak += 1
|
||||
else:
|
||||
break
|
||||
return {
|
||||
"id": inst.id, "catalog_id": inst.catalog_id, "name": inst.name,
|
||||
"config": inst.config or {}, "schedule": inst.schedule, "status": inst.status,
|
||||
"created_at": inst.created_at.isoformat() if inst.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": streak,
|
||||
"total_runs": db.query(Run).filter(Run.instance_id == inst.id).count(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/instances")
|
||||
def list_instances(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
instances = db.query(AgentInstance).filter(AgentInstance.user_id == user["user_id"]).all()
|
||||
return [serialize_instance(i, db) for i in instances]
|
||||
|
||||
|
||||
@app.post("/api/instances")
|
||||
def create_instance(data: InstanceCreate, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
catalog = db.query(AgentCatalog).filter(AgentCatalog.id == data.catalog_id).first()
|
||||
if not catalog:
|
||||
raise HTTPException(status_code=404, detail="Agent type not found in catalog")
|
||||
config = {**(catalog.default_config or {}), **data.config}
|
||||
inst = AgentInstance(
|
||||
user_id=user["user_id"],
|
||||
catalog_id=data.catalog_id,
|
||||
name=data.name or catalog.name,
|
||||
config=config,
|
||||
schedule=data.schedule or ("sub-agent" if catalog.is_sub_agent else "manual"),
|
||||
)
|
||||
db.add(inst)
|
||||
db.commit()
|
||||
return {"id": agent.id, "status": "updated"}
|
||||
return {"id": inst.id, "status": "created"}
|
||||
|
||||
|
||||
@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")
|
||||
@app.get("/api/instances/{instance_id}")
|
||||
def get_instance(instance_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
inst = db.query(AgentInstance).filter(
|
||||
AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"]
|
||||
).first()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=404)
|
||||
runs = db.query(Run).filter(Run.instance_id == instance_id).order_by(Run.started_at.desc()).limit(50).all()
|
||||
catalog = db.query(AgentCatalog).filter(AgentCatalog.id == inst.catalog_id).first()
|
||||
result = serialize_instance(inst, db)
|
||||
result["config_schema"] = catalog.config_schema if catalog else {}
|
||||
result["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]
|
||||
return result
|
||||
|
||||
|
||||
@app.put("/api/instances/{instance_id}")
|
||||
def update_instance(instance_id: int, update: InstanceUpdate, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
inst = db.query(AgentInstance).filter(
|
||||
AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"]
|
||||
).first()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=404)
|
||||
if update.name is not None:
|
||||
inst.name = update.name
|
||||
if update.schedule is not None:
|
||||
inst.schedule = update.schedule
|
||||
if update.status is not None:
|
||||
inst.status = update.status
|
||||
if update.config is not None:
|
||||
current = inst.config or {}
|
||||
current.update(update.config)
|
||||
inst.config = current
|
||||
db.commit()
|
||||
return {"id": inst.id, "status": "updated"}
|
||||
|
||||
|
||||
@app.delete("/api/instances/{instance_id}")
|
||||
def delete_instance(instance_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
inst = db.query(AgentInstance).filter(
|
||||
AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"]
|
||||
).first()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=404)
|
||||
db.query(Run).filter(Run.instance_id == instance_id).delete()
|
||||
db.delete(inst)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
# --- Internal endpoints (no auth, for agent scripts) ---
|
||||
|
||||
@app.get("/api/instances/{instance_id}/config")
|
||||
def get_instance_config(instance_id: int, db: Session = Depends(get_db)):
|
||||
inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=404)
|
||||
return inst.config or {}
|
||||
|
||||
|
||||
@app.post("/api/instances/{instance_id}/runs")
|
||||
def create_run(instance_id: int, run: RunCreate, db: Session = Depends(get_db)):
|
||||
inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=404)
|
||||
new_run = Run(
|
||||
agent_id=agent_id,
|
||||
instance_id=instance_id,
|
||||
user_id=inst.user_id,
|
||||
status=run.status,
|
||||
output=run.output,
|
||||
error=run.error,
|
||||
@@ -229,46 +310,195 @@ def create_run(agent_id: str, run: RunCreate, db: Session = Depends(get_db)):
|
||||
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}
|
||||
|
||||
# --- Runs (user-scoped) ---
|
||||
|
||||
@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()
|
||||
def list_runs(limit: int = 50, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
runs = db.query(Run).filter(Run.user_id == user["user_id"]).order_by(Run.started_at.desc()).limit(limit).all()
|
||||
return [{
|
||||
"id": r.id,
|
||||
"agent_id": r.agent_id,
|
||||
"id": r.id, "instance_id": r.instance_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_,
|
||||
"status": r.status, "output": r.output, "error": r.error, "metadata": r.metadata_,
|
||||
} for r in runs]
|
||||
|
||||
|
||||
# --- Static files (frontend) ---
|
||||
# --- Admin: Users ---
|
||||
|
||||
@app.get("/api/admin/users")
|
||||
def admin_list_users(admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
users = db.query(User).all()
|
||||
return [{
|
||||
"id": u.id, "username": u.username, "display_name": u.display_name,
|
||||
"role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
"instance_count": db.query(AgentInstance).filter(AgentInstance.user_id == u.id).count(),
|
||||
} for u in users]
|
||||
|
||||
|
||||
@app.post("/api/admin/users")
|
||||
def admin_create_user(data: UserCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
if db.query(User).filter(User.username == data.username).first():
|
||||
raise HTTPException(status_code=409, detail="Username exists")
|
||||
user = User(
|
||||
username=data.username,
|
||||
password_hash=hash_password(data.password),
|
||||
display_name=data.display_name or data.username,
|
||||
role=data.role,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return {"id": user.id, "status": "created"}
|
||||
|
||||
|
||||
@app.put("/api/admin/users/{user_id}")
|
||||
def admin_update_user(user_id: int, update: UserUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404)
|
||||
if update.display_name is not None:
|
||||
user.display_name = update.display_name
|
||||
if update.role is not None:
|
||||
user.role = update.role
|
||||
if update.password is not None:
|
||||
user.password_hash = hash_password(update.password)
|
||||
db.commit()
|
||||
return {"id": user.id, "status": "updated"}
|
||||
|
||||
|
||||
@app.delete("/api/admin/users/{user_id}")
|
||||
def admin_delete_user(user_id: int, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404)
|
||||
for inst in db.query(AgentInstance).filter(AgentInstance.user_id == user_id).all():
|
||||
db.query(Run).filter(Run.instance_id == inst.id).delete()
|
||||
db.delete(inst)
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
# --- Admin: LLM Providers ---
|
||||
|
||||
@app.get("/api/admin/llm-providers")
|
||||
def admin_list_providers(admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
providers = db.query(LLMProvider).all()
|
||||
return [{
|
||||
"id": p.id, "name": p.name, "provider_type": p.provider_type,
|
||||
"api_url": p.api_url, "api_key": "***" if p.api_key else "",
|
||||
"default_model": p.default_model, "is_default": p.is_default,
|
||||
} for p in providers]
|
||||
|
||||
|
||||
@app.post("/api/admin/llm-providers")
|
||||
def admin_create_provider(data: LLMProviderCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
if data.is_default:
|
||||
db.query(LLMProvider).update({"is_default": False})
|
||||
provider = LLMProvider(
|
||||
name=data.name, provider_type=data.provider_type, api_url=data.api_url,
|
||||
api_key=data.api_key, default_model=data.default_model, is_default=data.is_default,
|
||||
)
|
||||
db.add(provider)
|
||||
db.commit()
|
||||
return {"id": provider.id, "status": "created"}
|
||||
|
||||
|
||||
@app.put("/api/admin/llm-providers/{provider_id}")
|
||||
def admin_update_provider(provider_id: int, update: LLMProviderUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
provider = db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404)
|
||||
if update.is_default:
|
||||
db.query(LLMProvider).update({"is_default": False})
|
||||
for field, value in update.model_dump(exclude_none=True).items():
|
||||
setattr(provider, field, value)
|
||||
db.commit()
|
||||
return {"id": provider.id, "status": "updated"}
|
||||
|
||||
|
||||
@app.delete("/api/admin/llm-providers/{provider_id}")
|
||||
def admin_delete_provider(provider_id: int, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
provider = db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise HTTPException(status_code=404)
|
||||
db.delete(provider)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
# --- Admin: Catalog Management ---
|
||||
|
||||
class CatalogCreate(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
category: str = "utility"
|
||||
config_schema: dict = {}
|
||||
default_config: dict = {}
|
||||
supports_schedule: bool = True
|
||||
is_sub_agent: bool = False
|
||||
|
||||
class CatalogUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
config_schema: Optional[dict] = None
|
||||
default_config: Optional[dict] = None
|
||||
supports_schedule: Optional[bool] = None
|
||||
is_sub_agent: Optional[bool] = None
|
||||
|
||||
|
||||
@app.post("/api/admin/catalog")
|
||||
def admin_create_catalog(data: CatalogCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
if db.query(AgentCatalog).filter(AgentCatalog.id == data.id).first():
|
||||
raise HTTPException(status_code=409, detail="Catalog entry exists")
|
||||
entry = AgentCatalog(**data.model_dump())
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
return {"id": entry.id, "status": "created"}
|
||||
|
||||
|
||||
@app.put("/api/admin/catalog/{catalog_id}")
|
||||
def admin_update_catalog(catalog_id: str, update: CatalogUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404)
|
||||
for field, value in update.model_dump(exclude_none=True).items():
|
||||
setattr(entry, field, value)
|
||||
db.commit()
|
||||
return {"id": entry.id, "status": "updated"}
|
||||
|
||||
|
||||
@app.delete("/api/admin/catalog/{catalog_id}")
|
||||
def admin_delete_catalog(catalog_id: str, admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||
entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404)
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
# --- Static files ---
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
def login_page():
|
||||
return FileResponse("static/login.html")
|
||||
|
||||
|
||||
@app.get("/admin")
|
||||
def admin_page(session: Optional[str] = Cookie(None)):
|
||||
user = get_current_user(session)
|
||||
if not user:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
if user["role"] != "admin":
|
||||
return RedirectResponse("/", status_code=302)
|
||||
return FileResponse("static/admin.html")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root(session: Optional[str] = Cookie(None)):
|
||||
user = get_current_user(session)
|
||||
|
||||
Reference in New Issue
Block a user