emmixam/ai_workflow_discovery_agent
0
1from __future__ import annotations2 3from schemas import TimeMetric, TimeMetricsReport4 5 6class TimeValidationError(ValueError):7 """Erreur de validation métier sur les métriques temporelles."""8 9 10def _assert_allowed_level(metric: TimeMetric) -> None:11 allowed = {"provided", "observed", "estimated", "missing"}12 if metric.confidence_level not in allowed:13 raise TimeValidationError(14 f"{metric.name}: confidence_level invalide ({metric.confidence_level})."15 )16 17 18def _assert_non_negative_numeric(metric: TimeMetric) -> None:19 if metric.confidence_level == "missing":20 return21 22 if not isinstance(metric.value, (int, float)):23 raise TimeValidationError(24 f"{metric.name}: value doit être numérique pour une métrique temporelle exploitable."25 )26 27 if metric.value < 0:28 raise TimeValidationError(29 f"{metric.name}: value négative interdite ({metric.value})."30 )31 32 33def _assert_missing_consistency(metric: TimeMetric) -> None:34 if metric.confidence_level == "missing":35 if metric.value is not None:36 raise TimeValidationError(37 f"{metric.name}: une métrique 'missing' ne doit pas avoir de valeur."38 )39 else:40 if metric.value is None:41 raise TimeValidationError(42 f"{metric.name}: une métrique non 'missing' doit avoir une valeur."43 )44 45 46def _assert_estimated_has_hypothesis(metric: TimeMetric) -> None:47 if metric.confidence_level == "estimated":48 if metric.hypothesis is None or not metric.hypothesis.strip():49 raise TimeValidationError(50 f"{metric.name}: une métrique 'estimated' doit documenter une hypothèse."51 )52 53 54def validate_time_metric_or_raise(metric: TimeMetric) -> None:55 if not isinstance(metric, TimeMetric):56 raise TimeValidationError(f"TimeMetric attendu, reçu: {type(metric)!r}")57 58 if not metric.name.strip():59 raise TimeValidationError("TimeMetric.name vide.")60 if not metric.unit.strip():61 raise TimeValidationError(f"{metric.name}: unit vide.")62 if not metric.source.strip():63 raise TimeValidationError(f"{metric.name}: source vide.")64 65 _assert_allowed_level(metric)66 _assert_missing_consistency(metric)67 _assert_estimated_has_hypothesis(metric)68 _assert_non_negative_numeric(metric)69 70 71def _assert_metric_units(report: TimeMetricsReport) -> None:72 expected_units = {73 "monthly_volume": {"cases/month", "items/month", "events/month"},74 "activity_duration_minutes": {"minutes", "minute"},75 "case_cycle_time_minutes": {"minutes", "minute"},76 "manual_time_minutes": {"minutes", "minute"},77 "waiting_time_minutes": {"minutes", "minute"},78 }79 80 metrics = {81 "monthly_volume": report.monthly_volume,82 "activity_duration_minutes": report.activity_duration_minutes,83 "case_cycle_time_minutes": report.case_cycle_time_minutes,84 "manual_time_minutes": report.manual_time_minutes,85 "waiting_time_minutes": report.waiting_time_minutes,86 }87 88 for key, metric in metrics.items():89 if metric.unit not in expected_units[key]:90 raise TimeValidationError(91 f"{metric.name}: unité inattendue pour {key} ({metric.unit})."92 )93 94 95def _assert_time_logic(report: TimeMetricsReport) -> None:96 activity = report.activity_duration_minutes97 manual = report.manual_time_minutes98 waiting = report.waiting_time_minutes99 cycle = report.case_cycle_time_minutes100 101 comparable = [activity, manual, waiting, cycle]102 if any(m.confidence_level == "missing" for m in comparable):103 return104 105 activity_value = float(activity.value)106 manual_value = float(manual.value)107 waiting_value = float(waiting.value)108 cycle_value = float(cycle.value)109 110 if manual_value > cycle_value:111 raise TimeValidationError(112 "manual_time_minutes ne peut pas dépasser case_cycle_time_minutes."113 )114 115 if activity_value > cycle_value:116 raise TimeValidationError(117 "activity_duration_minutes ne peut pas dépasser case_cycle_time_minutes."118 )119 120 if waiting_value > cycle_value:121 raise TimeValidationError(122 "waiting_time_minutes ne peut pas dépasser case_cycle_time_minutes."123 )124 125 126def _assert_roi_flags(report: TimeMetricsReport) -> None:127 critical = [128 report.monthly_volume,129 report.activity_duration_minutes,130 report.manual_time_minutes,131 ]132 levels = [m.confidence_level for m in critical]133 134 full_ok = all(level in {"provided", "observed"} for level in levels)135 partial_ok = all(level in {"provided", "observed", "estimated"} for level in levels) and not any(136 level == "missing" for level in levels137 )138 139 if report.can_compute_full_roi and not full_ok:140 raise TimeValidationError(141 "can_compute_full_roi=True incohérent avec les niveaux de confiance critiques."142 )143 144 if report.can_compute_partial_roi and not partial_ok:145 raise TimeValidationError(146 "can_compute_partial_roi=True incohérent avec les niveaux de confiance critiques."147 )148 149 if report.can_compute_full_roi and not report.can_compute_partial_roi:150 raise TimeValidationError(151 "Un ROI complet implique nécessairement qu'un ROI partiel soit possible."152 )153 154 155def validate_time_metrics_report_or_raise(report: TimeMetricsReport) -> None:156 if not isinstance(report, TimeMetricsReport):157 raise TimeValidationError(158 f"TimeMetricsReport attendu, reçu: {type(report)!r}"159 )160 161 metrics = [162 report.monthly_volume,163 report.activity_duration_minutes,164 report.case_cycle_time_minutes,165 report.manual_time_minutes,166 report.waiting_time_minutes,167 ]168 169 for metric in metrics:170 validate_time_metric_or_raise(metric)171 172 _assert_metric_units(report)173 _assert_time_logic(report)174 _assert_roi_flags(report)175 176 177def audit_time_metrics_report(report: TimeMetricsReport) -> list[str]:178 warnings: list[str] = []179 180 metrics = [181 report.monthly_volume,182 report.activity_duration_minutes,183 report.case_cycle_time_minutes,184 report.manual_time_minutes,185 report.waiting_time_minutes,186 ]187 188 estimated_metrics = [m.name for m in metrics if m.confidence_level == "estimated"]189 missing_metrics = [m.name for m in metrics if m.confidence_level == "missing"]190 191 if estimated_metrics:192 warnings.append(193 f"Métriques estimées présentes: {estimated_metrics}. Vérifier les hypothèses."194 )195 196 if missing_metrics:197 warnings.append(198 f"Métriques manquantes: {missing_metrics}. Le ROI peut être partiel ou bloqué."199 )200 201 if report.can_compute_partial_roi and not report.can_compute_full_roi:202 warnings.append("ROI partiel seulement: certaines métriques critiques sont estimées.")203 204 if not report.can_compute_partial_roi:205 warnings.append("ROI désactivé: données temporelles insuffisantes.")206 207 if (208 report.activity_duration_minutes.confidence_level != "missing"209 and report.waiting_time_minutes.confidence_level != "missing"210 and float(report.waiting_time_minutes.value) > float(report.activity_duration_minutes.value) * 3211 ):212 warnings.append(213 "Le temps d'attente dépasse largement le temps d'activité; possible goulot de validation/transfert."214 )215 216 return warnings217 