diff --git a/backend/agenteval/intelligent_eval/decision.py b/backend/agenteval/intelligent_eval/decision.py index ff0c7ad..4611b34 100644 --- a/backend/agenteval/intelligent_eval/decision.py +++ b/backend/agenteval/intelligent_eval/decision.py @@ -45,7 +45,7 @@ class Decision: } -def _parse_time_slot(time_slot: str) -> Optional[tuple[int, int]]: +def _parse_time_slot(time_slot: str) -> Optional[tuple[float, float]]: """Thin wrapper delegating to :func:`agenteval.intelligent_eval.domain.parse_time_slot`.""" from agenteval.intelligent_eval.domain import parse_time_slot as _impl return _impl(time_slot) diff --git a/backend/agenteval/intelligent_eval/domain.py b/backend/agenteval/intelligent_eval/domain.py index 4a05e5a..fa4af4c 100644 --- a/backend/agenteval/intelligent_eval/domain.py +++ b/backend/agenteval/intelligent_eval/domain.py @@ -21,13 +21,26 @@ from agenteval.storage.db import ( # --------------------------------------------------------------------------- -def parse_time_slot(time_slot: str) -> Optional[tuple[int, int]]: - """Parse "8-10h" -> (8, 10). Returns None on bad format.""" +def parse_time_slot(time_slot: str) -> Optional[tuple[float, float]]: + """Parse a slot into (start_hours, end_hours), both in hours. + + Supports hour-level ("8-10h" -> (8, 10)) and minute-level ("0-20min" -> + (0, 1/3)) slots. planner 对短窗口(如 1h)会用分钟级时段,长窗口用 + 小时级;统一换算成小时(float)供上层判断。Returns None on bad format. + """ try: - parts = time_slot.replace("h", "").split("-") + raw = time_slot.strip().lower() + factor = 1.0 + if raw.endswith("min"): + raw = raw[:-3] + factor = 1.0 / 60 + elif raw.endswith("h"): + raw = raw[:-1] + factor = 1.0 + parts = raw.split("-") if len(parts) != 2: return None - return (int(parts[0]), int(parts[1])) + return (int(parts[0]) * factor, int(parts[1]) * factor) except (ValueError, AttributeError): return None diff --git a/tests/unit/test_intelligent_eval_task_queue.py b/tests/unit/test_intelligent_eval_task_queue.py index 1e3f578..363ea44 100644 --- a/tests/unit/test_intelligent_eval_task_queue.py +++ b/tests/unit/test_intelligent_eval_task_queue.py @@ -2,9 +2,6 @@ from datetime import timedelta -import pytest -from sqlmodel import Session, select - from agenteval.intelligent_eval import task_queue from agenteval.intelligent_eval.models import IntelligentEvalStatus from agenteval.storage.db import ( @@ -13,6 +10,7 @@ from agenteval.storage.db import ( IntelligentEvalTaskQueueDB, utc_now, ) +from sqlmodel import Session, select def test_is_slot_due(): @@ -28,6 +26,25 @@ def test_is_slot_due(): assert task_queue._is_slot_due({}, timedelta(hours=1)) is False +def test_is_slot_due_minute_format(): + """Minute-level slots (1h window: "0-20min") must be parsed and due on time.""" + from agenteval.intelligent_eval import domain + + # 0-20min: due at 0min, not before + assert task_queue._is_slot_due({"time_slot": "0-20min"}, timedelta(minutes=-1)) is False + assert task_queue._is_slot_due({"time_slot": "0-20min"}, timedelta(minutes=0)) is True + assert task_queue._is_slot_due({"time_slot": "0-20min"}, timedelta(minutes=5)) is True + # 20-40min: not due before 20min, due at 20min + assert task_queue._is_slot_due({"time_slot": "20-40min"}, timedelta(minutes=19)) is False + assert task_queue._is_slot_due({"time_slot": "20-40min"}, timedelta(minutes=20)) is True + + # parse both formats consistently (hours) + assert domain.parse_time_slot("8-10h") == (8.0, 10.0) + assert domain.parse_time_slot("0-20min") == (0.0, 1.0 / 3) + assert domain.parse_time_slot("20-40min") == (1.0 / 3, 2.0 / 3) + assert domain.parse_time_slot("bad") is None + + def test_calculate_session_deficit(db_session: Session): """Test session deficit calculation.""" # Create eval with plan