Files
zpark-ems/backend/app/api/v1/ai_ops.py
Du Wenbo 026c837b91 Squashed 'core/' content from commit 92ec910
git-subtree-dir: core
git-subtree-split: 92ec910a132e379a3a6e442a75bcb07cac0f0010
2026-04-04 18:17:10 +08:00

591 lines
20 KiB
Python

"""AI运维智能体 API - 设备健康、异常检测、诊断、预测维护、洞察"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from datetime import datetime, timezone, timedelta
from pydantic import BaseModel
from app.core.database import get_db
from app.core.deps import get_current_user
from app.models.user import User
from app.models.device import Device
from app.models.ai_ops import (
DeviceHealthScore, AnomalyDetection, DiagnosticReport,
MaintenancePrediction, OpsInsight,
)
from app.services.ai_ops import (
calculate_device_health, scan_anomalies, run_diagnostics,
generate_maintenance_predictions, generate_insights, get_dashboard_data,
)
router = APIRouter(prefix="/ai-ops", tags=["AI运维智能体"])
# ── Device Health ───────────────────────────────────────────────────
@router.get("/health")
async def get_all_health(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取所有设备最新健康评分"""
subq = (
select(
DeviceHealthScore.device_id,
func.max(DeviceHealthScore.timestamp).label("max_ts"),
).group_by(DeviceHealthScore.device_id).subquery()
)
result = await db.execute(
select(DeviceHealthScore).join(
subq, and_(
DeviceHealthScore.device_id == subq.c.device_id,
DeviceHealthScore.timestamp == subq.c.max_ts,
)
)
)
scores = result.scalars().all()
# Get device info
device_ids = [s.device_id for s in scores]
dev_map = {}
if device_ids:
dev_result = await db.execute(
select(Device.id, Device.name, Device.device_type, Device.code)
.where(Device.id.in_(device_ids))
)
dev_map = {r.id: {"name": r.name, "type": r.device_type, "code": r.code} for r in dev_result.all()}
return [{
"device_id": s.device_id,
"device_name": dev_map.get(s.device_id, {}).get("name", f"#{s.device_id}"),
"device_type": dev_map.get(s.device_id, {}).get("type", "unknown"),
"device_code": dev_map.get(s.device_id, {}).get("code", ""),
"health_score": s.health_score,
"status": s.status,
"trend": s.trend,
"factors": s.factors,
"timestamp": str(s.timestamp),
} for s in scores]
@router.get("/health/{device_id}")
async def get_device_health(
device_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取单设备健康详情"""
result = await db.execute(
select(DeviceHealthScore).where(
DeviceHealthScore.device_id == device_id
).order_by(DeviceHealthScore.timestamp.desc()).limit(1)
)
score = result.scalar_one_or_none()
if not score:
raise HTTPException(status_code=404, detail="暂无该设备健康数据")
dev_result = await db.execute(select(Device).where(Device.id == device_id))
device = dev_result.scalar_one_or_none()
return {
"device_id": score.device_id,
"device_name": device.name if device else f"#{device_id}",
"device_type": device.device_type if device else "unknown",
"health_score": score.health_score,
"status": score.status,
"trend": score.trend,
"factors": score.factors,
"timestamp": str(score.timestamp),
}
@router.get("/health/{device_id}/history")
async def get_health_history(
device_id: int,
days: int = Query(7, ge=1, le=90),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取设备健康评分历史"""
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(
select(DeviceHealthScore).where(and_(
DeviceHealthScore.device_id == device_id,
DeviceHealthScore.timestamp >= cutoff,
)).order_by(DeviceHealthScore.timestamp.asc())
)
scores = result.scalars().all()
return [{
"timestamp": str(s.timestamp),
"health_score": s.health_score,
"status": s.status,
"trend": s.trend,
"factors": s.factors,
} for s in scores]
@router.post("/health/calculate")
async def trigger_health_calculation(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""触发全部设备健康评分计算"""
result = await db.execute(select(Device).where(Device.is_active == True))
devices = result.scalars().all()
scores = []
for device in devices:
try:
score = await calculate_device_health(db, device)
scores.append({
"device_id": score.device_id,
"health_score": score.health_score,
"status": score.status,
})
except Exception as e:
scores.append({"device_id": device.id, "error": str(e)})
return {"calculated": len(scores), "results": scores}
# ── Anomaly Detection ───────────────────────────────────────────────
@router.get("/anomalies")
async def list_anomalies(
device_id: int | None = None,
severity: str | None = None,
status: str | None = None,
days: int = Query(7, ge=1, le=90),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""列出异常检测记录"""
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
query = select(AnomalyDetection).where(AnomalyDetection.detected_at >= cutoff)
if device_id:
query = query.where(AnomalyDetection.device_id == device_id)
if severity:
query = query.where(AnomalyDetection.severity == severity)
if status:
query = query.where(AnomalyDetection.status == status)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(AnomalyDetection.detected_at.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
anomalies = result.scalars().all()
# Get device names
dev_ids = list(set(a.device_id for a in anomalies))
dev_map = {}
if dev_ids:
dev_result = await db.execute(select(Device.id, Device.name).where(Device.id.in_(dev_ids)))
dev_map = {r.id: r.name for r in dev_result.all()}
return {
"total": total,
"items": [{
"id": a.id,
"device_id": a.device_id,
"device_name": dev_map.get(a.device_id, f"#{a.device_id}"),
"detected_at": str(a.detected_at),
"anomaly_type": a.anomaly_type,
"severity": a.severity,
"description": a.description,
"metric_name": a.metric_name,
"expected_value": a.expected_value,
"actual_value": a.actual_value,
"deviation_percent": a.deviation_percent,
"status": a.status,
"resolution_notes": a.resolution_notes,
} for a in anomalies],
}
@router.get("/anomalies/{device_id}")
async def get_device_anomalies(
device_id: int,
days: int = Query(7, ge=1, le=90),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取设备异常记录"""
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
result = await db.execute(
select(AnomalyDetection).where(and_(
AnomalyDetection.device_id == device_id,
AnomalyDetection.detected_at >= cutoff,
)).order_by(AnomalyDetection.detected_at.desc())
)
anomalies = result.scalars().all()
return [{
"id": a.id,
"detected_at": str(a.detected_at),
"anomaly_type": a.anomaly_type,
"severity": a.severity,
"description": a.description,
"metric_name": a.metric_name,
"expected_value": a.expected_value,
"actual_value": a.actual_value,
"deviation_percent": a.deviation_percent,
"status": a.status,
} for a in anomalies]
class AnomalyStatusUpdate(BaseModel):
status: str # investigating, resolved, false_positive
resolution_notes: str | None = None
@router.put("/anomalies/{anomaly_id}/status")
async def update_anomaly_status(
anomaly_id: int,
data: AnomalyStatusUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""更新异常状态"""
result = await db.execute(select(AnomalyDetection).where(AnomalyDetection.id == anomaly_id))
anomaly = result.scalar_one_or_none()
if not anomaly:
raise HTTPException(status_code=404, detail="异常记录不存在")
anomaly.status = data.status
if data.resolution_notes:
anomaly.resolution_notes = data.resolution_notes
return {"message": "已更新", "id": anomaly.id, "status": anomaly.status}
@router.post("/anomalies/scan")
async def trigger_anomaly_scan(
device_id: int | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""触发异常扫描"""
anomalies = await scan_anomalies(db, device_id)
return {
"scanned_at": str(datetime.now(timezone.utc)),
"anomalies_found": len(anomalies),
"anomalies": [{
"device_id": a.device_id,
"anomaly_type": a.anomaly_type,
"severity": a.severity,
"description": a.description,
} for a in anomalies],
}
# ── Diagnostics ─────────────────────────────────────────────────────
@router.get("/diagnostics")
async def list_diagnostics(
device_id: int | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""列出诊断报告"""
query = select(DiagnosticReport)
if device_id:
query = query.where(DiagnosticReport.device_id == device_id)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(DiagnosticReport.generated_at.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
reports = result.scalars().all()
dev_ids = list(set(r.device_id for r in reports))
dev_map = {}
if dev_ids:
dev_result = await db.execute(select(Device.id, Device.name).where(Device.id.in_(dev_ids)))
dev_map = {r.id: r.name for r in dev_result.all()}
return {
"total": total,
"items": [{
"id": r.id,
"device_id": r.device_id,
"device_name": dev_map.get(r.device_id, f"#{r.device_id}"),
"generated_at": str(r.generated_at),
"report_type": r.report_type,
"findings": r.findings,
"recommendations": r.recommendations,
"estimated_impact": r.estimated_impact,
"status": r.status,
} for r in reports],
}
@router.post("/diagnostics/{device_id}/run")
async def trigger_diagnostics(
device_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""对指定设备运行诊断"""
try:
report = await run_diagnostics(db, device_id)
return {
"id": report.id,
"device_id": report.device_id,
"report_type": report.report_type,
"findings": report.findings,
"recommendations": report.recommendations,
"estimated_impact": report.estimated_impact,
"status": report.status,
"generated_at": str(report.generated_at),
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.get("/diagnostics/{report_id}")
async def get_diagnostic_detail(
report_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取诊断报告详情"""
result = await db.execute(select(DiagnosticReport).where(DiagnosticReport.id == report_id))
report = result.scalar_one_or_none()
if not report:
raise HTTPException(status_code=404, detail="诊断报告不存在")
dev_result = await db.execute(select(Device.name).where(Device.id == report.device_id))
device_name = dev_result.scalar() or f"#{report.device_id}"
return {
"id": report.id,
"device_id": report.device_id,
"device_name": device_name,
"generated_at": str(report.generated_at),
"report_type": report.report_type,
"findings": report.findings,
"recommendations": report.recommendations,
"estimated_impact": report.estimated_impact,
"status": report.status,
}
# ── Predictive Maintenance ──────────────────────────────────────────
@router.get("/maintenance/predictions")
async def list_predictions(
status: str | None = None,
urgency: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""列出维护预测"""
query = select(MaintenancePrediction)
if status:
query = query.where(MaintenancePrediction.status == status)
if urgency:
query = query.where(MaintenancePrediction.urgency == urgency)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(MaintenancePrediction.predicted_at.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
predictions = result.scalars().all()
dev_ids = list(set(p.device_id for p in predictions))
dev_map = {}
if dev_ids:
dev_result = await db.execute(select(Device.id, Device.name).where(Device.id.in_(dev_ids)))
dev_map = {r.id: r.name for r in dev_result.all()}
return {
"total": total,
"items": [{
"id": p.id,
"device_id": p.device_id,
"device_name": dev_map.get(p.device_id, f"#{p.device_id}"),
"predicted_at": str(p.predicted_at),
"component": p.component,
"failure_mode": p.failure_mode,
"probability": p.probability,
"predicted_failure_date": str(p.predicted_failure_date) if p.predicted_failure_date else None,
"recommended_action": p.recommended_action,
"urgency": p.urgency,
"estimated_downtime_hours": p.estimated_downtime_hours,
"estimated_repair_cost": p.estimated_repair_cost,
"status": p.status,
} for p in predictions],
}
@router.get("/maintenance/schedule")
async def get_maintenance_schedule(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取推荐维护计划"""
result = await db.execute(
select(MaintenancePrediction).where(
MaintenancePrediction.status.in_(["predicted", "scheduled"])
).order_by(MaintenancePrediction.predicted_failure_date.asc())
)
predictions = result.scalars().all()
dev_ids = list(set(p.device_id for p in predictions))
dev_map = {}
if dev_ids:
dev_result = await db.execute(select(Device.id, Device.name).where(Device.id.in_(dev_ids)))
dev_map = {r.id: r.name for r in dev_result.all()}
return [{
"id": p.id,
"device_id": p.device_id,
"device_name": dev_map.get(p.device_id, f"#{p.device_id}"),
"component": p.component,
"failure_mode": p.failure_mode,
"probability": p.probability,
"predicted_failure_date": str(p.predicted_failure_date) if p.predicted_failure_date else None,
"recommended_action": p.recommended_action,
"urgency": p.urgency,
"estimated_downtime_hours": p.estimated_downtime_hours,
"estimated_repair_cost": p.estimated_repair_cost,
"status": p.status,
} for p in predictions]
class PredictionStatusUpdate(BaseModel):
status: str # scheduled, completed, false_alarm
@router.put("/maintenance/predictions/{prediction_id}")
async def update_prediction(
prediction_id: int,
data: PredictionStatusUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""更新预测状态"""
result = await db.execute(select(MaintenancePrediction).where(MaintenancePrediction.id == prediction_id))
pred = result.scalar_one_or_none()
if not pred:
raise HTTPException(status_code=404, detail="预测记录不存在")
pred.status = data.status
return {"message": "已更新", "id": pred.id, "status": pred.status}
@router.post("/maintenance/predict")
async def trigger_predictions(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""触发维护预测生成"""
predictions = await generate_maintenance_predictions(db)
return {
"generated": len(predictions),
"predictions": [{
"device_id": p.device_id,
"component": p.component,
"urgency": p.urgency,
"probability": p.probability,
} for p in predictions],
}
# ── Insights ────────────────────────────────────────────────────────
@router.get("/insights")
async def list_insights(
insight_type: str | None = None,
impact_level: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""列出运营洞察"""
query = select(OpsInsight)
if insight_type:
query = query.where(OpsInsight.insight_type == insight_type)
if impact_level:
query = query.where(OpsInsight.impact_level == impact_level)
count_q = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_q)).scalar()
query = query.order_by(OpsInsight.generated_at.desc()).offset((page - 1) * page_size).limit(page_size)
result = await db.execute(query)
insights = result.scalars().all()
return {
"total": total,
"items": [{
"id": i.id,
"insight_type": i.insight_type,
"title": i.title,
"description": i.description,
"data": i.data,
"impact_level": i.impact_level,
"actionable": i.actionable,
"recommended_action": i.recommended_action,
"generated_at": str(i.generated_at),
"valid_until": str(i.valid_until) if i.valid_until else None,
} for i in insights],
}
@router.get("/insights/latest")
async def get_latest_insights(
limit: int = Query(5, ge=1, le=20),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""获取最新洞察"""
now = datetime.now(timezone.utc)
result = await db.execute(
select(OpsInsight).where(
OpsInsight.valid_until >= now
).order_by(OpsInsight.generated_at.desc()).limit(limit)
)
insights = result.scalars().all()
return [{
"id": i.id,
"insight_type": i.insight_type,
"title": i.title,
"description": i.description,
"impact_level": i.impact_level,
"actionable": i.actionable,
"recommended_action": i.recommended_action,
"generated_at": str(i.generated_at),
} for i in insights]
@router.post("/insights/generate")
async def trigger_insights(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""触发洞察生成"""
insights = await generate_insights(db)
return {
"generated": len(insights),
"insights": [{
"title": i.title,
"insight_type": i.insight_type,
"impact_level": i.impact_level,
} for i in insights],
}
# ── Dashboard ───────────────────────────────────────────────────────
@router.get("/dashboard")
async def ai_ops_dashboard(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""AI运维总览仪表盘"""
return await get_dashboard_data(db)