492 lines
16 KiB
Python
492 lines
16 KiB
Python
"""模型能力矩阵 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})
|
||
|