Spaces:
Sleeping
Sleeping
Refine override validation and cleanup
Browse files- scheduler/core/algorithm.py +39 -13
scheduler/core/algorithm.py
CHANGED
|
@@ -37,6 +37,7 @@ class SchedulingResult:
|
|
| 37 |
scheduled_cases: Mapping of courtroom_id to list of scheduled cases
|
| 38 |
explanations: Decision explanations for each case (scheduled + sample unscheduled)
|
| 39 |
applied_overrides: List of overrides that were successfully applied
|
|
|
|
| 40 |
unscheduled_cases: Cases not scheduled with reasons (e.g., unripe, capacity full)
|
| 41 |
ripeness_filtered: Count of cases filtered due to unripe status
|
| 42 |
capacity_limited: Count of cases that didn't fit due to courtroom capacity
|
|
@@ -47,11 +48,12 @@ class SchedulingResult:
|
|
| 47 |
|
| 48 |
# Core output
|
| 49 |
scheduled_cases: Dict[int, List[Case]]
|
| 50 |
-
|
| 51 |
# Transparency
|
| 52 |
explanations: Dict[str, SchedulingExplanation]
|
| 53 |
applied_overrides: List[Override]
|
| 54 |
-
|
|
|
|
| 55 |
# Diagnostics
|
| 56 |
unscheduled_cases: List[Tuple[Case, str]]
|
| 57 |
ripeness_filtered: int
|
|
@@ -132,18 +134,31 @@ class SchedulingAlgorithm:
|
|
| 132 |
unscheduled: List[Tuple[Case, str]] = []
|
| 133 |
applied_overrides: List[Override] = []
|
| 134 |
explanations: Dict[str, SchedulingExplanation] = {}
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
# Validate overrides if provided
|
| 137 |
if overrides:
|
| 138 |
validator = OverrideValidator()
|
| 139 |
for override in overrides:
|
| 140 |
-
if
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
unscheduled.append(
|
| 143 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
)
|
| 145 |
-
|
| 146 |
-
|
| 147 |
# Filter disposed cases
|
| 148 |
active_cases = [c for c in cases if c.status != CaseStatus.DISPOSED]
|
| 149 |
|
|
@@ -154,7 +169,7 @@ class SchedulingAlgorithm:
|
|
| 154 |
|
| 155 |
# CHECKPOINT 1: Ripeness filtering with override support
|
| 156 |
ripe_cases, ripeness_filtered = self._filter_by_ripeness(
|
| 157 |
-
active_cases, current_date,
|
| 158 |
)
|
| 159 |
|
| 160 |
# CHECKPOINT 2: Eligibility check (min gap requirement)
|
|
@@ -168,9 +183,9 @@ class SchedulingAlgorithm:
|
|
| 168 |
prioritized = self.policy.prioritize(eligible_cases, current_date)
|
| 169 |
|
| 170 |
# CHECKPOINT 5: Apply manual overrides (add/remove/reorder/priority)
|
| 171 |
-
if
|
| 172 |
prioritized = self._apply_manual_overrides(
|
| 173 |
-
prioritized,
|
| 174 |
)
|
| 175 |
|
| 176 |
# CHECKPOINT 6: Allocate to courtrooms
|
|
@@ -208,11 +223,14 @@ class SchedulingAlgorithm:
|
|
| 208 |
below_threshold=False
|
| 209 |
)
|
| 210 |
explanations[case.case_id] = explanation
|
| 211 |
-
|
|
|
|
|
|
|
| 212 |
return SchedulingResult(
|
| 213 |
scheduled_cases=scheduled_allocation,
|
| 214 |
explanations=explanations,
|
| 215 |
applied_overrides=applied_overrides,
|
|
|
|
| 216 |
unscheduled_cases=unscheduled,
|
| 217 |
ripeness_filtered=ripeness_filtered,
|
| 218 |
capacity_limited=capacity_limited,
|
|
@@ -400,5 +418,13 @@ class SchedulingAlgorithm:
|
|
| 400 |
if case.case_id in case_to_courtroom:
|
| 401 |
courtroom_id = case_to_courtroom[case.case_id]
|
| 402 |
allocation[courtroom_id].append(case)
|
| 403 |
-
|
| 404 |
return allocation, capacity_limited
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
scheduled_cases: Mapping of courtroom_id to list of scheduled cases
|
| 38 |
explanations: Decision explanations for each case (scheduled + sample unscheduled)
|
| 39 |
applied_overrides: List of overrides that were successfully applied
|
| 40 |
+
override_rejections: Structured records for rejected overrides
|
| 41 |
unscheduled_cases: Cases not scheduled with reasons (e.g., unripe, capacity full)
|
| 42 |
ripeness_filtered: Count of cases filtered due to unripe status
|
| 43 |
capacity_limited: Count of cases that didn't fit due to courtroom capacity
|
|
|
|
| 48 |
|
| 49 |
# Core output
|
| 50 |
scheduled_cases: Dict[int, List[Case]]
|
| 51 |
+
|
| 52 |
# Transparency
|
| 53 |
explanations: Dict[str, SchedulingExplanation]
|
| 54 |
applied_overrides: List[Override]
|
| 55 |
+
override_rejections: List[Dict[str, str]]
|
| 56 |
+
|
| 57 |
# Diagnostics
|
| 58 |
unscheduled_cases: List[Tuple[Case, str]]
|
| 59 |
ripeness_filtered: int
|
|
|
|
| 134 |
unscheduled: List[Tuple[Case, str]] = []
|
| 135 |
applied_overrides: List[Override] = []
|
| 136 |
explanations: Dict[str, SchedulingExplanation] = {}
|
| 137 |
+
override_rejections: List[Dict[str, str]] = []
|
| 138 |
+
validated_overrides: List[Override] = []
|
| 139 |
+
|
| 140 |
# Validate overrides if provided
|
| 141 |
if overrides:
|
| 142 |
validator = OverrideValidator()
|
| 143 |
for override in overrides:
|
| 144 |
+
if validator.validate(override):
|
| 145 |
+
validated_overrides.append(override)
|
| 146 |
+
else:
|
| 147 |
+
errors = validator.get_errors()
|
| 148 |
+
rejection_reason = "; ".join(errors) if errors else "Validation failed"
|
| 149 |
+
override_rejections.append({
|
| 150 |
+
"judge": override.judge_id,
|
| 151 |
+
"context": override.override_type.value,
|
| 152 |
+
"reason": rejection_reason
|
| 153 |
+
})
|
| 154 |
unscheduled.append(
|
| 155 |
+
(
|
| 156 |
+
None,
|
| 157 |
+
f"Invalid override rejected (judge {override.judge_id}): "
|
| 158 |
+
f"{override.override_type.value} - {rejection_reason}"
|
| 159 |
+
)
|
| 160 |
)
|
| 161 |
+
|
|
|
|
| 162 |
# Filter disposed cases
|
| 163 |
active_cases = [c for c in cases if c.status != CaseStatus.DISPOSED]
|
| 164 |
|
|
|
|
| 169 |
|
| 170 |
# CHECKPOINT 1: Ripeness filtering with override support
|
| 171 |
ripe_cases, ripeness_filtered = self._filter_by_ripeness(
|
| 172 |
+
active_cases, current_date, validated_overrides, applied_overrides
|
| 173 |
)
|
| 174 |
|
| 175 |
# CHECKPOINT 2: Eligibility check (min gap requirement)
|
|
|
|
| 183 |
prioritized = self.policy.prioritize(eligible_cases, current_date)
|
| 184 |
|
| 185 |
# CHECKPOINT 5: Apply manual overrides (add/remove/reorder/priority)
|
| 186 |
+
if validated_overrides:
|
| 187 |
prioritized = self._apply_manual_overrides(
|
| 188 |
+
prioritized, validated_overrides, applied_overrides, unscheduled, active_cases
|
| 189 |
)
|
| 190 |
|
| 191 |
# CHECKPOINT 6: Allocate to courtrooms
|
|
|
|
| 223 |
below_threshold=False
|
| 224 |
)
|
| 225 |
explanations[case.case_id] = explanation
|
| 226 |
+
|
| 227 |
+
self._clear_temporary_case_flags(active_cases)
|
| 228 |
+
|
| 229 |
return SchedulingResult(
|
| 230 |
scheduled_cases=scheduled_allocation,
|
| 231 |
explanations=explanations,
|
| 232 |
applied_overrides=applied_overrides,
|
| 233 |
+
override_rejections=override_rejections,
|
| 234 |
unscheduled_cases=unscheduled,
|
| 235 |
ripeness_filtered=ripeness_filtered,
|
| 236 |
capacity_limited=capacity_limited,
|
|
|
|
| 418 |
if case.case_id in case_to_courtroom:
|
| 419 |
courtroom_id = case_to_courtroom[case.case_id]
|
| 420 |
allocation[courtroom_id].append(case)
|
| 421 |
+
|
| 422 |
return allocation, capacity_limited
|
| 423 |
+
|
| 424 |
+
@staticmethod
|
| 425 |
+
def _clear_temporary_case_flags(cases: List[Case]) -> None:
|
| 426 |
+
"""Remove temporary scheduling flags to keep case objects clean between runs."""
|
| 427 |
+
|
| 428 |
+
for case in cases:
|
| 429 |
+
if hasattr(case, "_priority_override"):
|
| 430 |
+
delattr(case, "_priority_override")
|