PMFlow/backend/app/api/ai_routing.py

125 lines
4.4 KiB
Python

"""AI routing and provider governance API."""
from __future__ import annotations
from typing import Any, Literal, Optional
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.database import get_db
from ..core.response import error_response, success_response
from ..dependencies.auth import get_current_user
from ..dependencies.tenant import get_tenant_id
from ..models import User
from ..services.ai_routing_governance_service import AIRoutingGovernanceService
from ..services.auth import can_approve_ai_routing_change, can_operate_ai_routing, can_view_ai_policy
from ..services.operation_log_service import log_operation
router = APIRouter(prefix="/ai-routing", tags=["ai_routing"])
class TeamPolicySceneUpdate(BaseModel):
key: str = Field(min_length=1, max_length=80)
policy: str = Field(min_length=1, max_length=300)
mode: Optional[str] = Field(default=None, max_length=80)
human_confirmation: bool = True
protected: bool = True
class TeamPolicyUpdateRequest(BaseModel):
strategy: Literal["quality_first", "balanced", "speed_first"] = "balanced"
scenes: list[TeamPolicySceneUpdate] = Field(default_factory=list)
automation_boundary: Optional[str] = Field(default=None, max_length=300)
budget_guard: dict[str, Any] = Field(default_factory=dict)
quality_targets: list[str] = Field(default_factory=list, max_length=8)
def _forbidden(message: str) -> JSONResponse:
return JSONResponse(status_code=403, content=error_response(403, message))
def _service(db: AsyncSession, tenant_id: int) -> AIRoutingGovernanceService:
return AIRoutingGovernanceService(db, tenant_id=int(tenant_id))
@router.get("/overview")
async def get_ai_routing_overview(
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
):
if not can_view_ai_policy(str(current_user.role or "")):
return _forbidden("仅团队负责人可查看 AI 路由概览")
data = await _service(db, tenant_id).get_overview()
return success_response(data)
@router.get("/team-policy")
async def get_ai_team_policy(
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
):
if not can_view_ai_policy(str(current_user.role or "")):
return _forbidden("仅团队负责人可查看团队 AI 策略")
data = await _service(db, tenant_id).get_team_policy()
return success_response(data)
@router.put("/team-policy")
async def update_ai_team_policy(
body: TeamPolicyUpdateRequest,
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
):
if not can_approve_ai_routing_change(str(current_user.role or "")):
return _forbidden("仅 OWNER 可保存团队 AI 策略")
service = _service(db, tenant_id)
before = await service.get_team_policy()
after = await service.update_team_policy(
payload=body.model_dump(mode="json"),
updated_by=int(current_user.id),
)
await log_operation(
db,
tenant_id=int(tenant_id),
user_id=int(current_user.id),
project_id=None,
entity_type="ai_routing_policy",
entity_id="team-policy",
action="update",
before_data=before,
after_data=after,
remark="更新团队 AI 策略",
)
await db.commit()
return success_response(after)
@router.get("/admin-console")
async def get_ai_routing_admin_console(
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
):
if not can_view_ai_policy(str(current_user.role or "")):
return _forbidden("仅团队负责人可查看 AI 路由控制台")
data = await _service(db, tenant_id).get_admin_console()
return success_response(data)
@router.post("/diagnostics/run")
async def run_ai_routing_diagnostics(
db: AsyncSession = Depends(get_db),
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(get_current_user),
):
if not can_operate_ai_routing(str(current_user.role or "")):
return _forbidden("仅 OWNER / REVIEWER 可执行 AI 路由诊断")
data = await _service(db, tenant_id).run_diagnostics()
return success_response(data)