2024-11-25 16:27:34 +08:00
from __future__ import annotations
from abc import abstractmethod
2026-03-02 19:04:10 +08:00
from typing import Callable , Generator
2024-11-25 16:27:34 +08:00
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
2025-02-16 01:40:44 +08:00
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEERMultiFeedback ,
CoSTEERSingleFeedback ,
)
2024-11-25 16:27:34 +08:00
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
from rdagent.components.coder.CoSTEER.knowledge_management import (
CoSTEERQueriedKnowledge ,
)
from rdagent.core.conf import RD_AGENT_SETTINGS
2025-02-16 01:40:44 +08:00
from rdagent.core.evolving_framework import EvolvingStrategy , EvoStep , QueriedKnowledge
2025-02-11 20:50:19 +08:00
from rdagent.core.experiment import FBWorkspace , Task
from rdagent.core.scenario import Scenario
2024-11-25 16:27:34 +08:00
from rdagent.core.utils import multiprocessing_wrapper
class MultiProcessEvolvingStrategy ( EvolvingStrategy ):
2025-08-04 17:38:27 +08:00
KEY_CHANGE_SUMMARY = "__change_summary__" # Optional key for the summary of the change of evolving subjects
2025-10-22 17:35:18 +08:00
def __init__ ( self , scen : Scenario , settings : CoSTEERSettings , improve_mode : bool = False ):
2024-11-25 16:27:34 +08:00
super () . __init__ ( scen )
self . settings = settings
2025-10-22 17:35:18 +08:00
self . improve_mode = improve_mode # improve mode means we only implement the task which has failed before. The main diff is the first loop will not implement all tasks.
2024-11-25 16:27:34 +08:00
def implement_one_task (
self ,
target_task : Task ,
2025-02-16 01:40:44 +08:00
queried_knowledge : QueriedKnowledge | None = None ,
2025-01-17 22:53:05 +08:00
workspace : FBWorkspace | None = None ,
2025-02-16 01:40:44 +08:00
prev_task_feedback : CoSTEERSingleFeedback | None = None ,
2025-01-17 22:53:05 +08:00
) -> dict [ str , str ]: # FIXME: fix interface of previous implement
"""
This method will input the task & current workspace,
and output the modification to applied to the workspace.
(i.e. replace the content <filename> with <content>)
2025-02-16 01:40:44 +08:00
Parameters
----------
target_task : Task
queried_knowledge : QueriedKnowledge | None
workspace : FBWorkspace | None
prev_task_feedback : CoSTEERSingleFeedback | None
task feedback for previous evolving step
None indicate it is the first loop.
2025-01-17 22:53:05 +08:00
Return
------
The new files {<filename>: <content>} to update the workspace.
2025-08-04 17:38:27 +08:00
- Special Keys: self.KEY_CHANGE_SUMMARY;
2025-01-17 22:53:05 +08:00
"""
2024-11-25 16:27:34 +08:00
raise NotImplementedError
2026-03-02 19:04:10 +08:00
def implement_func_list ( self ) -> list [ Callable ]:
"""
One evolve solution will be divided into multiple implement functions.
The functions will be called sequentially.
`implement_one_task` is the default implementation. Please refer to its signature for more details.
"""
return [ self . implement_one_task ]
2024-11-25 16:27:34 +08:00
@abstractmethod
2025-02-16 01:40:44 +08:00
def assign_code_list_to_evo ( self , code_list : list [ dict ], evo : EvolvingItem ) -> None :
2024-11-25 16:27:34 +08:00
"""
Assign the code list to the evolving item.
2025-02-16 01:40:44 +08:00
Due to the implement_one_task take `workspace` as input and output the `modification`.
2025-03-18 17:15:08 +09:00
We should apply implementation to evo
2025-02-16 01:40:44 +08:00
2026-03-02 19:04:10 +08:00
Assumptions:
- The modidication on evo should happen in-place!!
2024-11-25 16:27:34 +08:00
The code list is aligned with the evolving item's sub-tasks.
If a task is not implemented, put a None in the list.
"""
raise NotImplementedError
2026-03-02 19:04:10 +08:00
def assign_code_list_to_evo ( self , code_list : list [ dict | None ], evo ) -> None :
"""Assign code modifications to evolving item.
For runner, coder already generated full training config, so typically no modifications.
But this method is required by the abstract base class.
"""
for index in range ( len ( evo . sub_tasks )):
if code_list [ index ] is None :
continue
if evo . sub_workspace_list [ index ] is None :
evo . sub_workspace_list [ index ] = evo . experiment_workspace
# If there are any modifications (usually empty for runner)
if code_list [ index ]:
# Handle change summary if present
if self . KEY_CHANGE_SUMMARY in code_list [ index ]:
evo . sub_workspace_list [ index ] . change_summary = code_list [ index ] . pop ( self . KEY_CHANGE_SUMMARY )
# Inject any modified files
evo . sub_workspace_list [ index ] . inject_files ( ** code_list [ index ])
return evo
def evolve_iter (
2024-11-25 16:27:34 +08:00
self ,
* ,
evo : EvolvingItem ,
queried_knowledge : CoSTEERQueriedKnowledge | None = None ,
2025-02-16 01:40:44 +08:00
evolving_trace : list [ EvoStep ] = [],
2024-11-25 16:27:34 +08:00
** kwargs ,
2026-03-02 19:04:10 +08:00
) -> Generator [ EvolvingItem , EvolvingItem , None ]:
if queried_knowledge is None :
raise ValueError (
"MultiProcessEvolvingStrategy requires queried_knowledge for efficient implementation. Please set with_knowledge=True in CoSTEER constructor."
)
2025-07-31 13:06:28 +08:00
code_list = [ None for _ in range ( len ( evo . sub_tasks ))]
2025-10-23 16:00:23 +08:00
last_feedback = None
if len ( evolving_trace ) > 0 :
last_feedback = evolving_trace [ - 1 ] . feedback
2026-05-01 13:49:05 +02:00
if not isinstance ( last_feedback , CoSTEERMultiFeedback ):
raise TypeError ( "last_feedback must be of type CoSTEERMultiFeedback" )
2025-10-23 16:00:23 +08:00
2024-11-25 16:27:34 +08:00
# 1.找出需要evolve的task
2025-03-26 22:48:01 +08:00
to_be_finished_task_index : list [ int ] = []
2024-11-25 16:27:34 +08:00
for index , target_task in enumerate ( evo . sub_tasks ):
target_task_desc = target_task . get_task_information ()
if target_task_desc in queried_knowledge . success_task_to_knowledge_dict :
2025-03-26 22:48:01 +08:00
# NOTE: very weird logic:
# it depends on the knowledge to set the already finished task
2025-07-31 13:06:28 +08:00
code_list [ index ] = queried_knowledge . success_task_to_knowledge_dict [
2024-11-25 16:27:34 +08:00
target_task_desc
2025-07-31 13:06:28 +08:00
] . implementation . file_dict
2025-10-23 17:09:18 +08:00
else :
# Schedule the task only if:
# - it is not marked failed
# - and (in improve mode) we actually have prior failure feedback to act on
skip_for_improve_mode = self . improve_mode and (
last_feedback is None
or ( isinstance ( last_feedback , CoSTEERMultiFeedback ) and last_feedback [ index ] is None )
2025-10-23 16:00:23 +08:00
)
2025-10-23 17:09:18 +08:00
if target_task_desc not in queried_knowledge . failed_task_info_set and not skip_for_improve_mode :
to_be_finished_task_index . append ( index )
if skip_for_improve_mode :
code_list [ index ] = (
{}
) # empty implementation for skipped task, but assign_code_list_to_evo will still assign it
2024-11-25 16:27:34 +08:00
2026-03-02 19:04:10 +08:00
for implement_func in self . implement_func_list ():
result = multiprocessing_wrapper (
[
2025-02-16 01:40:44 +08:00
(
2026-03-02 19:04:10 +08:00
implement_func ,
(
evo . sub_tasks [ target_index ],
queried_knowledge ,
evo . experiment_workspace ,
None if last_feedback is None else last_feedback [ target_index ],
),
)
for target_index in to_be_finished_task_index
],
n = RD_AGENT_SETTINGS . multi_proc_n ,
)
for index , target_index in enumerate ( to_be_finished_task_index ):
code_list [ target_index ] = result [ index ]
self . assign_code_list_to_evo ( code_list , evo )
yield evo