PMFlow/backend/app/api/model_capability.py

492 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""模型能力矩阵 API"""
from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import JSONResponse
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.response import ErrorCode, error_response, success_response
from ..database import get_db
from ..dependencies import get_current_user, get_tenant_id
from ..models import Experiment, TruthNode, User
from ..schemas.model_capability import (
CostOptimizationAnalyzeRequest,
ExperimentSyncRequest,
ExperimentSyncReviewRequest,
ExperimentSyncRollbackRequest,
GenerateMigrationTasksRequest,
LifecycleImpactRequest,
ValidateMatchRequest,
)
from ..services.ai_service import get_ai_service
from ..services.cost_optimization_service import CostOptimizationService
from ..services.experiment_sync_service import ExperimentSyncService
from ..services.model_capability_service import ModelCapabilityService
from ..services.model_lifecycle_service import ModelLifecycleService
router = APIRouter(prefix="/model-capability", tags=["model-capability"])
def _build_service(db: AsyncSession) -> ModelCapabilityService:
return ModelCapabilityService(db=db, ai_service=get_ai_service())
@router.post("/validate-match")
async def validate_model_requirement_match(
request: ValidateMatchRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""需求-模型匹配校验"""
_ = current_user
service = _build_service(db)
try:
data = await service.validate_model_requirement_match(
truth_node_id=request.truth_node_id,
model_version_id=request.model_version_id,
tenant_id=tenant_id,
project_id=request.project_id,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=404,
content=error_response(ErrorCode.NOT_FOUND, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"匹配校验失败: {exc}"),
)
@router.get("/capability-gaps")
async def list_capability_gaps(
status: Optional[str] = Query(default=None),
severity: Optional[str] = Query(default=None),
limit: int = Query(default=100, ge=1, le=500),
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""列出能力差距预警"""
_ = current_user
service = _build_service(db)
try:
data = await service.list_capability_gaps(
tenant_id=tenant_id,
status=status,
severity=severity,
limit=limit,
)
return success_response(data=data)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"查询能力差距失败: {exc}"),
)
@router.get("/cost-tracking/{truth_node_id}")
async def get_cost_tracking(
truth_node_id: int,
period: str = Query(default="weekly", pattern="^(daily|weekly|monthly|project)$"),
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""查询成本跟踪与 ROI"""
_ = current_user
service = _build_service(db)
try:
data = await service.get_cost_tracking(
truth_node_id=truth_node_id,
period=period,
tenant_id=tenant_id,
)
return success_response(data=data)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"查询成本跟踪失败: {exc}"),
)
@router.get("/cost-optimization/{truth_node_id}")
async def get_cost_optimization(
truth_node_id: int,
period: str = Query(default="weekly", pattern="^(daily|weekly|monthly|project)$"),
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""查询成本优化建议(必要时自动触发一次计算)"""
_ = current_user
service = _build_service(db)
try:
tracking = await service.get_cost_tracking(
truth_node_id=truth_node_id,
period=period,
tenant_id=tenant_id,
)
optimization = tracking.get("optimization_suggestions") or {}
if not optimization:
computed = await service.track_cost_and_roi(
truth_node_id=truth_node_id,
period=period,
tenant_id=tenant_id,
)
optimization = computed.get("optimization_suggestions", {})
tracking = await service.get_cost_tracking(
truth_node_id=truth_node_id,
period=period,
tenant_id=tenant_id,
)
return success_response(
data={
"truth_node_id": truth_node_id,
"period": period,
"optimization_suggestions": optimization,
"roi_score": tracking.get("roi_score"),
"actual_cost_usd": tracking.get("actual_cost_usd", 0.0),
}
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"查询成本优化失败: {exc}"),
)
@router.post("/lifecycle/assess-impact")
async def assess_model_update_impact(
request: LifecycleImpactRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""评估模型升级对现有需求集合的影响。"""
_ = current_user
service = ModelLifecycleService(db)
try:
data = await service.assess_model_update_impact(
old_model_version_id=request.old_model_version_id,
new_model_version_id=request.new_model_version_id,
tenant_id=tenant_id,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=404,
content=error_response(ErrorCode.NOT_FOUND, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"影响评估失败: {exc}"),
)
@router.get("/lifecycle/impacts")
async def list_lifecycle_impacts(
limit: int = Query(default=50, ge=1, le=200),
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""列出模型生命周期影响评估记录。"""
_ = current_user
service = ModelLifecycleService(db)
try:
data = await service.list_impacts(tenant_id=tenant_id, limit=limit)
return success_response(data=data)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"查询影响评估失败: {exc}"),
)
@router.post("/lifecycle/{impact_id}/migration-tasks")
async def create_migration_tasks(
impact_id: int,
request: GenerateMigrationTasksRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""基于影响评估自动生成迁移任务建议。"""
_ = current_user
service = ModelLifecycleService(db)
try:
data = await service.generate_migration_tasks(
impact_id=impact_id,
tenant_id=tenant_id,
max_tasks=request.max_tasks,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=404,
content=error_response(ErrorCode.NOT_FOUND, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"生成迁移任务失败: {exc}"),
)
@router.post("/experiment-sync")
async def create_experiment_sync(
request: ExperimentSyncRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""创建实验结果同步记录,满足阈值时自动同步。"""
_ = current_user
service = ExperimentSyncService(db)
try:
data = await service.auto_sync_experiment_result(
experiment_id=request.experiment_id,
truth_node_id=request.truth_node_id,
tenant_id=tenant_id,
project_id=request.project_id,
sync_type=request.sync_type,
auto_apply_threshold=request.auto_apply_threshold,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=404,
content=error_response(ErrorCode.NOT_FOUND, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"创建同步失败: {exc}"),
)
@router.get("/experiment-sync")
async def list_experiment_sync_records(
truth_node_id: Optional[int] = Query(default=None),
limit: int = Query(default=50, ge=1, le=200),
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""查询实验同步记录。"""
_ = current_user
service = ExperimentSyncService(db)
try:
data = await service.list_sync_records(
tenant_id=tenant_id,
truth_node_id=truth_node_id,
limit=limit,
)
return success_response(data=data)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"查询同步记录失败: {exc}"),
)
@router.post("/experiment-sync/{sync_id}/review")
async def review_experiment_sync(
sync_id: int,
request: ExperimentSyncReviewRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""人工审核实验同步记录。"""
service = ExperimentSyncService(db)
try:
data = await service.review_sync(
sync_id=sync_id,
tenant_id=tenant_id,
reviewer_id=current_user.id,
action=request.action,
review_notes=request.review_notes,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=400,
content=error_response(ErrorCode.BAD_REQUEST, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"审核同步失败: {exc}"),
)
@router.post("/experiment-sync/{sync_id}/rollback")
async def rollback_experiment_sync(
sync_id: int,
request: ExperimentSyncRollbackRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""回滚已应用的实验同步。"""
service = ExperimentSyncService(db)
try:
data = await service.rollback_sync(
sync_id=sync_id,
tenant_id=tenant_id,
reviewer_id=current_user.id,
rollback_notes=request.rollback_notes,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=400,
content=error_response(ErrorCode.BAD_REQUEST, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"回滚同步失败: {exc}"),
)
@router.post("/cost-optimization/analyze")
async def analyze_cost_optimization(
request: CostOptimizationAnalyzeRequest,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""生成并持久化成本优化建议。"""
_ = current_user
service = CostOptimizationService(db)
try:
data = await service.analyze_cost_optimization(
truth_node_id=request.truth_node_id,
tenant_id=tenant_id,
period=request.period,
project_id=request.project_id,
)
return success_response(data=data)
except ValueError as exc:
return JSONResponse(
status_code=400,
content=error_response(ErrorCode.BAD_REQUEST, str(exc)),
)
except Exception as exc:
return JSONResponse(
status_code=500,
content=error_response(ErrorCode.INTERNAL_ERROR, f"成本优化分析失败: {exc}"),
)
@router.get("/context/projects/{project_id}/truth-nodes")
async def list_project_truth_nodes(
project_id: int,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""获取项目上下文中的宪章列表,用于前端选择器。"""
_ = current_user
result = await db.execute(
select(TruthNode).where(
and_(
TruthNode.tenant_id == tenant_id,
TruthNode.project_id == project_id,
)
).order_by(TruthNode.created_at.desc())
)
nodes = result.scalars().all()
return success_response(
data={
"items": [
{
"id": n.id,
"title": n.title,
"status": n.status,
"node_type": n.node_type,
}
for n in nodes
]
}
)
@router.get("/context/projects/{project_id}/experiments")
async def list_project_experiments(
project_id: int,
current_user: User = Depends(get_current_user),
tenant_id: int = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
"""获取项目上下文中的实验列表(基于 linked_context_nodes 关联)。"""
_ = current_user
truth_ids_result = await db.execute(
select(TruthNode.id).where(
and_(
TruthNode.tenant_id == tenant_id,
TruthNode.project_id == project_id,
)
)
)
truth_ids = {int(row[0]) for row in truth_ids_result.all()}
if not truth_ids:
return success_response(data={"items": []})
exp_result = await db.execute(
select(Experiment)
.where(Experiment.tenant_id == tenant_id)
.order_by(Experiment.created_at.desc())
)
experiments = exp_result.scalars().all()
def _linked_ids(raw) -> set[int]:
ids = set()
if isinstance(raw, list):
for x in raw:
try:
ids.add(int(x))
except (TypeError, ValueError):
continue
return ids
items = []
for e in experiments:
if e.project_id is not None:
if int(e.project_id) != int(project_id):
continue
items.append(
{
"id": e.id,
"title": e.title or f"Experiment {e.id}",
"status": e.status,
"target_metric": e.target_metric,
}
)
continue
# 迁移兼容:project_id 为空时,回退到 linked_context_nodes 交叉判断。
if _linked_ids(e.linked_context_nodes).isdisjoint(truth_ids):
continue
items.append(
{
"id": e.id,
"title": e.title or f"Experiment {e.id}",
"status": e.status,
"target_metric": e.target_metric,
}
)
return success_response(data={"items": items})