PMFlow/backend/app/api/ai_logs.py

204 lines
7.4 KiB
Python

"""AI 处理可视化日志 API。"""
from __future__ import annotations
from datetime import date, datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import and_, case, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.response import success_response
from ..database import get_db
from ..dependencies.auth import get_current_user
from ..models import AIProcessingLog, User
from .ai_logs_utils import apply_project_filter, serialize_log_detail, serialize_log_row
router = APIRouter(prefix="/ai-logs", tags=["AI Logs"])
@router.get("")
async def list_ai_logs(
project_id: Optional[int] = Query(default=None, ge=1),
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant_id = int(current_user.tenant_id)
total_stmt = select(func.count(AIProcessingLog.id)).where(AIProcessingLog.tenant_id == tenant_id)
data_stmt = (
select(AIProcessingLog)
.where(AIProcessingLog.tenant_id == tenant_id)
.order_by(AIProcessingLog.created_at.desc(), AIProcessingLog.id.desc())
.offset(offset)
.limit(limit)
)
total_stmt = apply_project_filter(total_stmt, tenant_id=tenant_id, project_id=project_id)
data_stmt = apply_project_filter(data_stmt, tenant_id=tenant_id, project_id=project_id)
total = int((await db.execute(total_stmt)).scalar() or 0)
rows = (await db.execute(data_stmt)).scalars().all()
items = [serialize_log_row(row) for row in rows]
return success_response(
{
"items": items,
"total": total,
"limit": limit,
"offset": offset,
"has_more": (offset + len(items)) < total,
}
)
@router.get("/stats")
async def get_ai_log_stats(
project_id: Optional[int] = Query(default=None, ge=1),
date_value: str = Query(default="today", alias="date"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant_id = int(current_user.tenant_id)
if date_value == "today":
target_date = datetime.now().date()
else:
try:
target_date = date.fromisoformat(date_value)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"invalid_date:{date_value}") from exc
stats_stmt = (
select(
func.count(AIProcessingLog.id).label("received_count"),
func.sum(case((AIProcessingLog.processing_status == "success", 1), else_=0)).label("success_count"),
func.sum(case((AIProcessingLog.processing_status == "failed", 1), else_=0)).label("failed_count"),
func.sum(
case((AIProcessingLog.routing_decision == "duplicate_filtered", 1), else_=0)
).label("duplicate_filtered_count"),
func.sum(
case((AIProcessingLog.routing_decision == "execute_reply_guard_triggered", 1), else_=0)
).label("execute_reply_guard_count"),
func.sum(
case(
(
AIProcessingLog.routing_decision.in_(["auto_create_project", "auto_enter_signal"]),
1,
),
else_=0,
)
).label("auto_entered_count"),
func.sum(
case(
(
AIProcessingLog.routing_decision.in_(
[
"pending_low_confidence",
"pending_cold_start",
"pending_uncertain_project",
]
),
1,
),
else_=0,
)
).label("pending_count"),
func.sum(case((AIProcessingLog.processing_status == "processing", 1), else_=0)).label(
"processing_count"
),
func.avg(AIProcessingLog.signal_confidence).label("avg_signal_confidence"),
func.avg(AIProcessingLog.project_score).label("avg_project_score"),
)
.where(
and_(
AIProcessingLog.tenant_id == tenant_id,
func.date(AIProcessingLog.created_at) == target_date.isoformat(),
)
)
)
stats_stmt = apply_project_filter(stats_stmt, tenant_id=tenant_id, project_id=project_id)
row = (await db.execute(stats_stmt)).one()
return success_response(
{
"date": target_date.isoformat(),
"received_count": int(row.received_count or 0),
"success_count": int(row.success_count or 0),
"failed_count": int(row.failed_count or 0),
"duplicate_filtered_count": int(row.duplicate_filtered_count or 0),
"execute_reply_guard_count": int(row.execute_reply_guard_count or 0),
"auto_entered_count": int(row.auto_entered_count or 0),
"pending_count": int(row.pending_count or 0),
"processing_count": int(row.processing_count or 0),
"avg_signal_confidence": round(float(row.avg_signal_confidence or 0.0), 4),
"avg_project_score": round(float(row.avg_project_score or 0.0), 4),
}
)
@router.get("/alerts/execute-reply-guard")
async def list_execute_reply_guard_alerts(
limit: int = Query(default=20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant_id = int(current_user.tenant_id)
rows = (
await db.execute(
select(AIProcessingLog)
.where(
and_(
AIProcessingLog.tenant_id == tenant_id,
AIProcessingLog.routing_decision == "execute_reply_guard_triggered",
)
)
.order_by(AIProcessingLog.created_at.desc(), AIProcessingLog.id.desc())
.limit(limit)
)
).scalars().all()
return success_response(
{
"count": len(rows),
"items": [serialize_log_row(row) for row in rows],
}
)
@router.get("/latest")
async def get_latest_ai_log(
project_id: Optional[int] = Query(default=None, ge=1),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant_id = int(current_user.tenant_id)
stmt = (
select(AIProcessingLog)
.where(AIProcessingLog.tenant_id == tenant_id)
.order_by(AIProcessingLog.created_at.desc(), AIProcessingLog.id.desc())
.limit(1)
)
stmt = apply_project_filter(stmt, tenant_id=tenant_id, project_id=project_id)
row = (await db.execute(stmt)).scalar_one_or_none()
return success_response(serialize_log_detail(row) if row else None)
@router.get("/{log_id}")
async def get_ai_log_detail(
log_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant_id = int(current_user.tenant_id)
row = (
await db.execute(
select(AIProcessingLog).where(
and_(
AIProcessingLog.tenant_id == tenant_id,
AIProcessingLog.id == int(log_id),
)
)
)
).scalar_one_or_none()
if row is None:
raise HTTPException(status_code=404, detail="ai_log_not_found")
return success_response(serialize_log_detail(row))