@@ -196,7 +196,7 @@ def delete_automation(self, automation_id):
196196 # ---------------------------------------------------------------
197197 # Execution
198198 # ---------------------------------------------------------------
199- def execute_automation (self , automation_id , device_id ):
199+ def execute_automation (self , automation_id , device_id , self_heal = False ):
200200 automation = self .get_automation (automation_id )
201201 if not automation :
202202 raise ValueError (f"Automation { automation_id } not found" )
@@ -215,6 +215,7 @@ def execute_automation(self, automation_id, device_id):
215215 "current_step_index" : 0 ,
216216 "step_results" : [],
217217 "error" : None ,
218+ "self_heal" : self_heal ,
218219 }
219220 self ._automation_runs .append (run_record )
220221
@@ -231,8 +232,11 @@ def execute_automation(self, automation_id, device_id):
231232 return run_record
232233
233234 def _run_automation_thread (self , run_record , steps , device_id , cancel_event ):
235+ from devicekit .mixins .nl_automation import UI_TARGETING_STEP_TYPES
236+
234237 step_results = []
235238 completed = 0
239+ self_heal_enabled = run_record .get ("self_heal" , False )
236240
237241 for idx , step in enumerate (steps ):
238242 if cancel_event .is_set ():
@@ -267,39 +271,72 @@ def _run_automation_thread(self, run_record, steps, device_id, cancel_event):
267271 result ["duration_ms" ] = elapsed
268272 completed += 1
269273 except Exception as e :
270- elapsed = int ((time .time () - start_ts ) * 1000 )
271- result ["status" ] = "failed"
272- result ["error" ] = str (e )
273- result ["duration_ms" ] = elapsed
274- # Best-effort screenshot capture on failure
275- try :
276- screenshot_data = self .take_screenshot (device_id )
277- if screenshot_data :
278- result ["failure_screenshot" ] = base64 .b64encode (screenshot_data ).decode ('ascii' )
279- except Exception :
280- pass
281- step_results .append (result )
282- # Mark remaining steps as skipped
283- for remaining in steps [idx + 1 :]:
284- step_results .append ({
285- "step_id" : remaining .get ("id" , "" ),
286- "step_type" : remaining .get ("type" , "unknown" ),
287- "label" : remaining .get ("label" , "" ),
288- "status" : "skipped" ,
289- "output" : None ,
290- "error" : None ,
291- "duration_ms" : 0 ,
274+ error_str = str (e )
275+ step_type = step .get ("type" , "" )
276+
277+ # Attempt self-healing for UI-targeting steps
278+ healed = False
279+ if self_heal_enabled and step_type in UI_TARGETING_STEP_TYPES :
280+ try :
281+ heal_result = self .self_heal_step (device_id , step , error_str )
282+ if heal_result .get ("healed" ):
283+ # Re-execute with healed step config
284+ healed_step = heal_result ["new_step" ]
285+ heal_start = time .time ()
286+ output = self ._execute_step (healed_step , device_id )
287+ elapsed = int ((time .time () - start_ts ) * 1000 )
288+ result ["status" ] = "completed"
289+ result ["output" ] = str (output ) if output else None
290+ result ["duration_ms" ] = elapsed
291+ result ["healed" ] = True
292+ result ["original_step" ] = copy .deepcopy (step )
293+ result ["healed_step" ] = healed_step
294+ result ["heal_reasoning" ] = heal_result .get ("reasoning" , "" )
295+ completed += 1
296+ healed = True
297+ else :
298+ # Heal attempted but failed
299+ result ["healed" ] = False
300+ result ["heal_reasoning" ] = heal_result .get ("reasoning" , "Could not find element" )
301+ except Exception as heal_err :
302+ logger .warning (f"Self-heal error for step { idx } : { heal_err } " )
303+ result ["healed" ] = False
304+ result ["heal_reasoning" ] = f"Heal error: { heal_err } "
305+
306+ if not healed :
307+ elapsed = int ((time .time () - start_ts ) * 1000 )
308+ result ["status" ] = "failed"
309+ result ["error" ] = error_str
310+ result ["duration_ms" ] = elapsed
311+ # Best-effort screenshot capture on failure
312+ try :
313+ screenshot_data = self .take_screenshot (device_id )
314+ if screenshot_data :
315+ result ["failure_screenshot" ] = base64 .b64encode (screenshot_data ).decode ('ascii' )
316+ except Exception :
317+ pass
318+ step_results .append (result )
319+ # Mark remaining steps as skipped
320+ for remaining in steps [idx + 1 :]:
321+ step_results .append ({
322+ "step_id" : remaining .get ("id" , "" ),
323+ "step_type" : remaining .get ("type" , "unknown" ),
324+ "label" : remaining .get ("label" , "" ),
325+ "status" : "skipped" ,
326+ "output" : None ,
327+ "error" : None ,
328+ "duration_ms" : 0 ,
329+ })
330+ run_record .update ({
331+ "status" : "failed" ,
332+ "finished_at" : time .time (),
333+ "completed_steps" : completed ,
334+ "current_step_index" : idx ,
335+ "step_results" : step_results ,
336+ "error" : error_str ,
292337 })
293- run_record .update ({
294- "status" : "failed" ,
295- "finished_at" : time .time (),
296- "completed_steps" : completed ,
297- "current_step_index" : idx ,
298- "step_results" : step_results ,
299- "error" : str (e ),
300- })
301- self ._active_runs .pop (run_record ["id" ], None )
302- return
338+ self ._active_runs .pop (run_record ["id" ], None )
339+ return
303340
304341 step_results .append (result )
305342 run_record ["completed_steps" ] = completed
0 commit comments