"""Auto-sync and project validation flows.""" from __future__ import annotations from .shared import Any, Dict, Experiment, ExperimentTruthNodeSync, Optional, Set, TruthNode, and_, select class ExperimentSyncFlowMixin: """Auto sync experiment results to charter truth nodes.""" async def auto_sync_experiment_result( self, experiment_id: int, truth_node_id: int, tenant_id: int, project_id: Optional[int] = None, sync_type: str = "result_sync", auto_apply_threshold: float = 0.8, ) -> Dict[str, Any]: experiment = await self._get_experiment(experiment_id, tenant_id) truth_node = await self._get_truth_node(truth_node_id, tenant_id) if not experiment or not truth_node: raise ValueError("实验或宪章不存在") if project_id is not None: await self.validate_experiment_truth_same_project( experiment=experiment, truth_node=truth_node, tenant_id=tenant_id, project_id=project_id, ) confidence = self._calc_confidence(experiment.current_score) sync_data = self._build_sync_data(experiment, truth_node, sync_type) auto_apply = confidence >= auto_apply_threshold record = ExperimentTruthNodeSync( tenant_id=tenant_id, experiment_id=experiment.id, experiment_external_id=str(experiment.id), truth_node_id=truth_node.id, sync_type=sync_type, sync_data=sync_data, confidence_score=confidence, auto_applied=auto_apply, apply_status="pending", ) if auto_apply: await self._apply_sync_to_truth_node(record, truth_node) record.apply_status = "applied" self.db.add(record) await self.db.commit() await self.db.refresh(record) return { "sync_id": record.id, "confidence_score": record.confidence_score, "auto_applied": record.auto_applied, "apply_status": record.apply_status, "sync_data": record.sync_data, } async def validate_experiment_truth_same_project( self, experiment: Experiment, truth_node: TruthNode, tenant_id: int, project_id: int, ) -> None: """交叉校验实验与宪章均属于指定项目。""" if truth_node.project_id != project_id: raise ValueError(f"宪章 {truth_node.id} 不属于项目 {project_id},拒绝跨项目同步") if experiment.project_id is not None: if int(experiment.project_id) != int(project_id): raise ValueError(f"实验 {experiment.id} 不属于项目 {project_id},拒绝跨项目同步") return project_truth_nodes = await self._get_project_truth_node_ids(project_id, tenant_id) if not project_truth_nodes: raise ValueError(f"项目 {project_id} 下无可用宪章,无法校验实验归属") linked = experiment.linked_context_nodes or [] linked_ids = self._normalize_ids(linked) if not linked_ids: raise ValueError("实验未关联任何上下文节点(linked_context_nodes 为空),无法通过项目归属校验") if linked_ids.isdisjoint(project_truth_nodes): raise ValueError(f"实验 {experiment.id} 未关联项目 {project_id} 下宪章,拒绝跨项目同步") experiment.project_id = project_id async def _get_project_truth_node_ids(self, project_id: int, tenant_id: int) -> Set[int]: result = await self.db.execute( select(TruthNode.id).where( and_( TruthNode.tenant_id == tenant_id, TruthNode.project_id == project_id, ) ) ) return {int(row[0]) for row in result.all()} def _normalize_ids(self, raw: Any) -> Set[int]: ids: Set[int] = set() if isinstance(raw, list): for item in raw: try: ids.add(int(item)) except (TypeError, ValueError): continue return ids