From 2867f9590bd65ec715eaec22da138eb80686f18a Mon Sep 17 00:00:00 2001 From: Silviu Druma Date: Fri, 30 Jan 2026 09:46:33 -0500 Subject: [PATCH] feat: add feasability criteria --- apps/api/src/planproof_api/agent/planner.py | 4 +++ apps/api/src/planproof_api/routes.py | 8 +++++- apps/api/tests/test_feasibility.py | 32 +++++++++++++++++---- eval/feasibility.py | 17 +++++++++-- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/apps/api/src/planproof_api/agent/planner.py b/apps/api/src/planproof_api/agent/planner.py index 71c6ab6..7ad1029 100644 --- a/apps/api/src/planproof_api/agent/planner.py +++ b/apps/api/src/planproof_api/agent/planner.py @@ -23,6 +23,10 @@ "If the user did not specify a duration, ask about it in questions. " "Every task MUST have a duration of at least 5 minutes. You are forbidden " "from creating zero-duration tasks to fit constraints. " + "If a task duration is not specified, assume a minimum of 25 minutes for " + "anything labeled \"Meeting\" and 15 minutes for chores. You are " + "FORBIDDEN from scheduling tasks shorter than 15 minutes unless the user " + "explicitly asks for a \"quick check\" or \"5-min task\". " "Current time is provided in 12h format. Be extremely careful with AM/PM: " "3:15 PM is 15:15. If the current time is 6 AM, a 3 PM meeting is in the " "future and must be scheduled. " diff --git a/apps/api/src/planproof_api/routes.py b/apps/api/src/planproof_api/routes.py index 4f1c5c5..7753482 100644 --- a/apps/api/src/planproof_api/routes.py +++ b/apps/api/src/planproof_api/routes.py @@ -151,10 +151,11 @@ def _validate_plan( plan, metadata.actionable_tasks ) missing_keywords = _missing_keywords(plan, metadata.actionable_tasks) - human_feasibility_flags = check_feasibility(plan) + human_feasibility_flags, feasibility_errors = check_feasibility(plan) zero_duration_flags = 0 errors: list[str] = list(constraint_errors) + errors.extend(feasibility_errors) current_dt = isoparse(current_time) for item in plan: start_dt = isoparse(item.start_time) @@ -254,6 +255,11 @@ def _repair_plan( "RULE 2: Do not delete tasks to fix overlaps. Shorten them instead " "(e.g., change 60m to 15m)." ) + repair_prompt = ( + f"{repair_prompt}\n\n" + "Your previous attempt used unrealistic 5-minute durations. Increase " + "durations to at least 25 minutes and shift other tasks accordingly." + ) if constraint_violation_count > 0: repair_prompt = ( f"{repair_prompt}\n\n" diff --git a/apps/api/tests/test_feasibility.py b/apps/api/tests/test_feasibility.py index 4839d40..5b918b7 100644 --- a/apps/api/tests/test_feasibility.py +++ b/apps/api/tests/test_feasibility.py @@ -21,13 +21,15 @@ def test_check_feasibility_no_long_blocks() -> None: _item("2025-01-18T11:15:00-05:00", "2025-01-18T13:00:00-05:00"), ] - assert check_feasibility(items) == 0 + flags, _ = check_feasibility(items) + assert flags == 0 def test_check_feasibility_single_block_over_limit() -> None: items = [_item("2025-01-18T09:00:00-05:00", "2025-01-18T13:30:00-05:00")] - assert check_feasibility(items) == 1 + flags, _ = check_feasibility(items) + assert flags == 1 def test_check_feasibility_multiple_blocks_over_limit() -> None: @@ -36,7 +38,8 @@ def test_check_feasibility_multiple_blocks_over_limit() -> None: _item("2025-01-18T13:45:00-05:00", "2025-01-18T18:15:00-05:00"), ] - assert check_feasibility(items) == 2 + flags, _ = check_feasibility(items) + assert flags == 2 def test_check_feasibility_break_exactly_15_minutes() -> None: @@ -45,7 +48,8 @@ def test_check_feasibility_break_exactly_15_minutes() -> None: _item("2025-01-18T13:15:00-05:00", "2025-01-18T15:00:00-05:00"), ] - assert check_feasibility(items) == 0 + flags, _ = check_feasibility(items) + assert flags == 0 def test_check_feasibility_overlapping_tasks() -> None: @@ -54,4 +58,22 @@ def test_check_feasibility_overlapping_tasks() -> None: _item("2025-01-18T10:30:00-05:00", "2025-01-18T15:30:00-05:00"), ] - assert check_feasibility(items) == 1 + flags, _ = check_feasibility(items) + assert flags == 1 + + +def test_check_feasibility_short_meeting() -> None: + items = [ + PlanItem( + task="Team Meeting", + start_time="2025-01-18T09:00:00-05:00", + end_time="2025-01-18T09:10:00-05:00", + timebox_minutes=0, + why="Test", + ) + ] + + flags, errors = check_feasibility(items) + + assert flags == 1 + assert "Meeting duration is realistically too short." in errors diff --git a/eval/feasibility.py b/eval/feasibility.py index 63e378f..6e79b20 100644 --- a/eval/feasibility.py +++ b/eval/feasibility.py @@ -8,14 +8,15 @@ from planproof_api.agent.schemas import PlanItem -def check_feasibility(plan_items: List["PlanItem"]) -> int: +def check_feasibility(plan_items: List["PlanItem"]) -> tuple[int, list[str]]: if not plan_items: - return 0 + return 0, [] items = sorted(plan_items, key=lambda item: isoparse(item.start_time)) block_start = isoparse(items[0].start_time) block_end = isoparse(items[0].end_time) flags = 0 + errors: list[str] = [] for item in items[1:]: start_time = isoparse(item.start_time) @@ -32,8 +33,18 @@ def check_feasibility(plan_items: List["PlanItem"]) -> int: if end_time > block_end: block_end = end_time + for item in items: + label = (item.task or "").lower() + if "meeting" in label or "sync" in label: + start_time = isoparse(item.start_time) + end_time = isoparse(item.end_time) + duration_minutes = (end_time - start_time).total_seconds() / 60 + if duration_minutes < 20: + flags += 1 + errors.append("Meeting duration is realistically too short.") + block_minutes = (block_end - block_start).total_seconds() / 60 if block_minutes > 240: flags += 1 - return flags + return flags, errors