fix(intelligent-eval): parse minute-level time slots (1h windows)
All checks were successful
CI / test (push) Successful in 3m54s

planner 对短窗口(1h)产出分钟级时段(如 0-20min/20-40min/40-60min),
但 parse_time_slot 只支持小时级(8-10h),分钟格式解析失败返回 None →
is_slot_due=False → 审批后评估永不入队、不触发 worker。
- parse_time_slot 支持 h/min 后缀,统一换算成小时(float)返回
- is_slot_due/get_current_slot/count_sessions_in_slot 用 timedelta(hours=float)
  兼容两种格式;decision._parse_time_slot 类型标注同步 float
测试:+1(分钟格式时段解析与到期判断),898 passed
This commit is contained in:
sinohqb 2026-08-18 13:15:46 +08:00
parent 9d87ecf736
commit 60c54a67e4
3 changed files with 38 additions and 8 deletions

View File

@ -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)

View File

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

View File

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