PMFlow/backend/app/services/experiment_sync/mixin_sync.py

114 lines
4.2 KiB
Python
Raw 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.

"""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