"""模型能力矩阵 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})