44
55from fastapi import APIRouter
66
7+ from dateutil import tz
78from dateutil .parser import isoparse
89
910from eval .constraints import check_constraints
@@ -46,12 +47,12 @@ def _format_plan(plan: list[PlanItem]) -> str:
4647
4748@opik .track (name = "initial_planning_step" )
4849def _initial_planning_step (
49- request : PlanRequest , metadata : ExtractedMetadata
50+ request : PlanRequest , metadata : ExtractedMetadata , current_time : str
5051) -> tuple [list [PlanItem ], list [str ], list [str ]]:
5152 return generate_plan (
5253 request .context ,
5354 metadata ,
54- request . current_time ,
55+ current_time ,
5556 request .timezone ,
5657 )
5758
@@ -72,17 +73,20 @@ def _validate_plan(
7273 Returns:
7374 PlanValidation containing metrics and errors.
7475 """
75- constraint_violation_count = check_constraints (
76+ constraint_violation_count , constraint_errors = check_constraints (
7677 plan , metadata .detected_constraints , current_time
7778 )
7879 overlap_minutes = calculate_overlaps (plan )
80+ hallucination_candidates = (
81+ (metadata .task_keywords or []) + (metadata .detected_constraints or [])
82+ )
7983 hallucination_count = check_hallucinations (
80- plan , metadata .ground_truth_entities , metadata . task_keywords
84+ plan , metadata .ground_truth_entities , hallucination_candidates
8185 )
8286 keyword_recall_score = calculate_recall (plan , metadata .task_keywords )
8387 human_feasibility_flags = check_feasibility (plan )
8488
85- errors : list [str ] = []
89+ errors : list [str ] = list ( constraint_errors )
8690 current_dt = isoparse (current_time )
8791 for item in plan :
8892 start_dt = isoparse (item .start_time )
@@ -98,8 +102,6 @@ def _validate_plan(
98102 f'Task "{ item .task } " timebox_minutes mismatch with duration.'
99103 )
100104
101- if constraint_violation_count > 0 :
102- errors .append ("constraint_violation_count > 0" )
103105 if overlap_minutes > 0 :
104106 errors .append ("overlap_minutes > 0" )
105107 if hallucination_count > 0 :
@@ -121,6 +123,7 @@ def _validate_plan(
121123 opik_context .update_current_span (
122124 metadata = {
123125 "constraint_violation_count" : constraint_violation_count ,
126+ "constraint_errors" : constraint_errors ,
124127 "overlap_minutes" : overlap_minutes ,
125128 "hallucination_count" : hallucination_count ,
126129 "keyword_recall_score" : keyword_recall_score ,
@@ -134,7 +137,11 @@ def _validate_plan(
134137
135138@opik .track (name = "repair_step" )
136139def _repair_plan (
137- request : PlanRequest , metadata : ExtractedMetadata , failed_plan : list [PlanItem ], errors : list [str ]
140+ request : PlanRequest ,
141+ metadata : ExtractedMetadata ,
142+ failed_plan : list [PlanItem ],
143+ errors : list [str ],
144+ current_time : str ,
138145) -> tuple [list [PlanItem ], list [str ], list [str ]]:
139146 repair_prompt = (
140147 "Original context:\n "
@@ -147,12 +154,24 @@ def _repair_plan(
147154 return generate_plan (
148155 request .context ,
149156 metadata ,
150- request . current_time ,
157+ current_time ,
151158 request .timezone ,
152159 repair_prompt = repair_prompt ,
153160 )
154161
155162
163+ def _normalize_current_time (current_time : str , timezone : str ) -> str :
164+ current_dt = isoparse (current_time )
165+ local_tz = tz .gettz (timezone ) if timezone else None
166+ if local_tz is None :
167+ return current_dt .isoformat ()
168+ if current_dt .tzinfo is None :
169+ current_dt = current_dt .replace (tzinfo = tz .UTC )
170+ local_dt = current_dt .astimezone (local_tz )
171+ print (f"DEBUG: Normalized Current Time (Local): { local_dt .isoformat ()} " )
172+ return local_dt .isoformat ()
173+
174+
156175@router .post ("/api/plan" , response_model = PlanResponse )
157176@opik .track (name = "plan_request" )
158177def create_plan (request : PlanRequest ) -> PlanResponse :
@@ -161,12 +180,17 @@ def create_plan(request: PlanRequest) -> PlanResponse:
161180 except Exception :
162181 pass
163182
183+ local_current_time = _normalize_current_time (
184+ request .current_time , request .timezone
185+ )
164186 metadata = extract_metadata (request .context )
165187 print (
166188 f"DEBUG: Extractor produced { len (metadata .task_keywords )} keywords"
167189 )
168190 try :
169- plan , assumptions , questions = _initial_planning_step (request , metadata )
191+ plan , assumptions , questions = _initial_planning_step (
192+ request , metadata , local_current_time
193+ )
170194 except PlanGenerationError as exc :
171195 validation = PlanValidation (
172196 status = "fail" ,
@@ -193,7 +217,7 @@ def create_plan(request: PlanRequest) -> PlanResponse:
193217 ),
194218 )
195219
196- validation = _validate_plan (plan , metadata , request . current_time )
220+ validation = _validate_plan (plan , metadata , local_current_time )
197221 print (
198222 "DEBUG: Validation - Overlaps: "
199223 f"{ validation .metrics .overlap_minutes } , "
@@ -207,9 +231,9 @@ def create_plan(request: PlanRequest) -> PlanResponse:
207231 repair_attempted = True
208232 try :
209233 plan , assumptions , questions = _repair_plan (
210- request , metadata , plan , validation .errors
234+ request , metadata , plan , validation .errors , local_current_time
211235 )
212- validation = _validate_plan (plan , metadata , request . current_time )
236+ validation = _validate_plan (plan , metadata , local_current_time )
213237 repair_success = validation .status == "pass"
214238 print (
215239 "DEBUG: Validation (repair) - Overlaps: "
@@ -236,6 +260,8 @@ def create_plan(request: PlanRequest) -> PlanResponse:
236260 pass
237261 print (f"DEBUG: Opik Trace ID: { trace_id } " )
238262
263+ plan .sort (key = lambda item : item .start_time )
264+
239265 return PlanResponse (
240266 plan = plan ,
241267 extracted_metadata = metadata ,
0 commit comments