27
27
from ...webagent_utils_async .utils .utils import urls_to_images
28
28
29
29
logger = logging .getLogger (__name__ )
30
+ logger .setLevel (logging .INFO )
30
31
openai_client = OpenAI ()
31
32
32
33
class MCTSAgent :
@@ -183,6 +184,23 @@ async def rmcts(self) -> List[Dict[str, Any]]:
183
184
except Exception as e :
184
185
logger .error (f"Error expanding node: { str (e )} " )
185
186
current_node .is_terminal = True
187
+ # Expansion Step: Expand the selected node if possible
188
+ if not current_node .is_terminal and current_node .depth < self .config .max_depth :
189
+ logger .info (f"\n Expansion Step:" )
190
+ logger .info (f"Expanding node: { current_node .action } " )
191
+
192
+ expansion_success = await self .expand (current_node , None )
193
+ if not expansion_success :
194
+ # No children were generated; backtrack if possible.
195
+ if len (path ) > 1 :
196
+ logger .info ("Backtracking due to expansion failure (no children generated)." )
197
+ path .pop () # Remove the current dead-end node.
198
+ current_node = path [- 1 ] # Set current_node to its parent.
199
+ else :
200
+ logger .warning ("Expansion failed at root; no further backtracking possible." )
201
+ break
202
+ else :
203
+ logger .info (f"Successfully expanded node with { len (current_node .children )} children" )
186
204
187
205
# Simulation: Evaluate the current path
188
206
logger .info (f"\n Simulation Step:" )
@@ -217,8 +235,8 @@ async def rmcts(self) -> List[Dict[str, Any]]:
217
235
logger .info (f"New best score: { score :.3f} " )
218
236
219
237
# Reflection-based backpropagation
220
- if score < 0.25 : # If the path is not satisfactory
221
- logger .info (f"\n Reflection Step (Score { score :.3f} < 0.25 ):" )
238
+ if score < 0.75 : # If the path is not satisfactory
239
+ logger .info (f"\n Reflection Step (Score { score :.3f} < 0.75 ):" )
222
240
223
241
# Generate reflection prompt
224
242
reflection_prompt = f"""Analyze the current trajectory and suggest improvements.
@@ -265,10 +283,10 @@ async def rmcts(self) -> List[Dict[str, Any]]:
265
283
except Exception as e :
266
284
logger .error (f"Error in reflection: { str (e )} " )
267
285
268
- # # If we've found a satisfactory solution, return it
269
- # if score >= 0.75:
270
- # logger.info(f"\nFound satisfactory solution with score {score:.3f}")
271
- # return [{"action": node.action} for node in path[1:]]
286
+ # If we've found a satisfactory solution, return it
287
+ if score >= 0.75 :
288
+ logger .info (f"\n Found satisfactory solution with score { score :.3f} " )
289
+ return [{"action" : node .action } for node in path [1 :]]
272
290
273
291
except Exception as e :
274
292
logger .error (f"Error in simulation: { str (e )} " )
@@ -286,13 +304,13 @@ async def rmcts(self) -> List[Dict[str, Any]]:
286
304
287
305
# If we've exhausted all iterations and haven't found a perfect solution,
288
306
# return the best path we found
289
- if best_path :
307
+ if best_path and len ( best_path ) > 1 :
290
308
logger .info (f"\n Search complete. Returning best path found with score { best_score :.3f} " )
291
309
return [{"action" : node .action } for node in best_path [1 :]]
292
-
293
- # If no path was found at all
294
- logger .warning ("\n No valid path found" )
295
- return []
310
+
311
+ # If no valid path was found or path was just the root, return a default action
312
+ logger .warning ("\n No valid path found, returning fallback action " )
313
+ return [{ "action" : "refresh()" , "description" : "Fallback action - no valid path found" } ]
296
314
297
315
except Exception as e :
298
316
error_msg = f"Error in RMCTS search: { str (e )} "
@@ -500,8 +518,8 @@ async def rmcts_with_websocket(self, websocket) -> List[Dict[str, Any]]:
500
518
})
501
519
502
520
# Reflection-based backpropagation
503
- if score < 0.25 : # If the path is not satisfactory
504
- logger .info (f"\n Reflection Step (Score { score :.3f} < 0.25 ):" )
521
+ if score < 0.75 : # If the path is not satisfactory
522
+ logger .info (f"\n Reflection Step (Score { score :.3f} < 0.75 ):" )
505
523
506
524
await websocket .send_json ({
507
525
"type" : "reflection_start" ,
@@ -568,20 +586,20 @@ async def rmcts_with_websocket(self, websocket) -> List[Dict[str, Any]]:
568
586
"timestamp" : datetime .utcnow ().isoformat ()
569
587
})
570
588
571
- # # If we've found a satisfactory solution, return it
572
- # if score >= 0.75:
573
- # logger.info(f"\nFound satisfactory solution with score {score:.3f}")
589
+ # If we've found a satisfactory solution, return it
590
+ if score >= 0.75 :
591
+ logger .info (f"\n Found satisfactory solution with score { score :.3f} " )
574
592
575
- # # Send completion update if websocket is provided
576
- # await websocket.send_json({
577
- # "type": "search_complete",
578
- # "status": "success",
579
- # "score": score,
580
- # "path": [{"id": id(node), "action": node.action} for node in path[1:]],
581
- # "timestamp": datetime.utcnow().isoformat()
582
- # })
593
+ # Send completion update if websocket is provided
594
+ await websocket .send_json ({
595
+ "type" : "search_complete" ,
596
+ "status" : "success" ,
597
+ "score" : score ,
598
+ "path" : [{"id" : id (node ), "action" : node .action } for node in path [1 :]],
599
+ "timestamp" : datetime .utcnow ().isoformat ()
600
+ })
583
601
584
- # return [{"action": node.action} for node in path[1:]]
602
+ return [{"action" : node .action } for node in path [1 :]]
585
603
586
604
except Exception as e :
587
605
logger .error (f"Error in simulation: { str (e )} " )
@@ -611,7 +629,7 @@ async def rmcts_with_websocket(self, websocket) -> List[Dict[str, Any]]:
611
629
612
630
# If we've exhausted all iterations and haven't found a perfect solution,
613
631
# return the best path we found
614
- if best_path :
632
+ if best_path and len ( best_path ) > 1 :
615
633
logger .info (f"\n Search complete. Returning best path found with score { best_score :.3f} " )
616
634
617
635
# Send completion update if websocket is provided
@@ -635,8 +653,10 @@ async def rmcts_with_websocket(self, websocket) -> List[Dict[str, Any]]:
635
653
"message" : "No valid path found" ,
636
654
"timestamp" : datetime .utcnow ().isoformat ()
637
655
})
638
-
639
- return []
656
+
657
+ # If no valid path was found or path was just the root, return a default action
658
+ logger .warning ("\n No valid path found, returning fallback action" )
659
+ return [{"action" : "refresh()" , "description" : "Fallback action - no valid path found" }]
640
660
641
661
except Exception as e :
642
662
error_msg = f"Error in RMCTS search: { str (e )} "
@@ -753,48 +773,29 @@ async def _reset_browser(self, websocket=None) -> Optional[tuple]:
753
773
})
754
774
return None , None
755
775
756
- async def expand (self , node : LATSNode , websocket = None ) -> None :
776
+ async def expand (self , node : LATSNode , websocket = None ) -> bool :
757
777
"""
758
- Expand a node by generating its children.
778
+ Expand a node by generating its children. If no children are generated,
779
+ mark the node as terminal and return False to trigger backtracking.
759
780
760
781
Args:
761
- node: Node to expand
762
- websocket: Optional WebSocket connection to send updates to
782
+ node: Node to expand.
783
+ websocket: Optional WebSocket connection to send updates.
784
+
785
+ Returns:
786
+ bool: True if expansion succeeded (children generated), False otherwise.
763
787
"""
764
788
try :
765
789
children_state = await self .generate_children (node , websocket )
766
- logger .info (f"Generated { len (children_state )} children for node: { node .action } " )
767
790
except Exception as e :
768
791
logger .error (f"Exception during generation of children for node { node .action } : { e } " )
769
792
children_state = []
770
-
771
- # if not children_state:
772
- # logger.warning(f"No valid children found for node: {node.action}")
773
- # # Mark the node as terminal but don't halt the entire search
774
- # node.is_terminal = True
775
- # return
793
+
776
794
if not children_state :
777
- logger .warning ("No valid children returned, creating fallback children" )
778
- children_state = [
779
- {
780
- "natural_language_description" : "Navigate back to try a different approach" ,
781
- "action" : "navigate_backward()" ,
782
- "prob" : 0.15 ,
783
- "element" : None
784
- },
785
- {
786
- "natural_language_description" : "Refresh the page to reinitialize search" ,
787
- "action" : "refresh()" ,
788
- "prob" : 0.1 ,
789
- "element" : None
790
- },
791
- {
792
- "natural_language_description" : "Click a random element for exploration" ,
793
- "action" : "click('random')" ,
794
- "prob" : 0.05 ,
795
- "element" : None
796
- }
797
- ]
795
+ logger .warning ("No children generated. Marking node as terminal and triggering backtracking." )
796
+ node .is_terminal = True
797
+ return False # Indicate that expansion did not generate children.
798
+
798
799
for child_state in children_state :
799
800
try :
800
801
child = LATSNode (
@@ -818,6 +819,7 @@ async def expand(self, node: LATSNode, websocket=None) -> None:
818
819
})
819
820
except Exception as e :
820
821
logger .error (f"Error creating child node from state { child_state } : { e } " )
822
+ return True # Expansion succeeded (children were generated).
821
823
822
824
async def generate_children (self , node : LATSNode , websocket = None ) -> list [dict ]:
823
825
"""
@@ -833,7 +835,7 @@ async def generate_children(self, node: LATSNode, websocket=None) -> list[dict]:
833
835
# Reset browser and get live URL
834
836
live_browser_url , session_id = await self ._reset_browser (websocket )
835
837
path = self .get_path_to_root (node )
836
-
838
+ logger . info ( f"######### Generating children for path with { len ( path ) } nodes" )
837
839
# Execute path
838
840
for n in path [1 :]: # Skip root node
839
841
if websocket :
@@ -843,23 +845,41 @@ async def generate_children(self, node: LATSNode, websocket=None) -> list[dict]:
843
845
"action" : n .action ,
844
846
"timestamp" : datetime .utcnow ().isoformat ()
845
847
})
846
-
847
- success = await playwright_step_execution (
848
- n ,
849
- self .goal ,
850
- self .playwright_manager ,
851
- is_replay = False ,
852
- log_folder = self .config .log_folder
853
- )
854
- if not success :
855
- n .is_terminal = True
856
- if websocket :
857
- await websocket .send_json ({
858
- "type" : "replay_failed" ,
859
- "node_id" : id (n ),
860
- "timestamp" : datetime .utcnow ().isoformat ()
861
- })
862
- return []
848
+ try :
849
+ success = await playwright_step_execution (
850
+ n ,
851
+ self .goal ,
852
+ self .playwright_manager ,
853
+ is_replay = False ,
854
+ log_folder = self .config .log_folder
855
+ )
856
+ logger .info (f"#########Success: { success } " )
857
+
858
+ if not success :
859
+ logger .warning (f"Action execution failed: { n .action } " )
860
+ n .is_terminal = True
861
+ if websocket :
862
+ await websocket .send_json ({
863
+ "type" : "replay_failed" ,
864
+ "node_id" : id (n ),
865
+ "timestamp" : datetime .utcnow ().isoformat ()
866
+ })
867
+ return [{
868
+ "natural_language_description" : "Recover from failed action" ,
869
+ "action" : "refresh()" ,
870
+ "prob" : 0.1 ,
871
+ "element" : None
872
+ }]
873
+ except Exception as e :
874
+ logger .error (f"Error executing action { n .action } : { str (e )} " )
875
+ # Provide fallback actions instead of bubbling up the exception
876
+ return [{
877
+ "natural_language_description" : "Recover from action error" ,
878
+ "action" : "refresh()" ,
879
+ "prob" : 0.1 ,
880
+ "element" : None
881
+ }]
882
+
863
883
864
884
if not n .feedback :
865
885
n .feedback = await generate_feedback (
0 commit comments