[docs]defnode_to_function_feedback(node_feedback:TraceGraph):"""Convert a TraceGraph to a FunctionFeedback. roots, others, outputs are dict of variable name and its data and constraints."""depth=0iflen(node_feedback.graph)==0elsenode_feedback.graph[-1][0]graph=[]others={}roots={}output={}documentation={}visited=set()forlevel,nodeinnode_feedback.graph:# the graph is already sortedvisited.add(node)ifnode.is_root:# Need an or condition hereroots.update({node.py_name:(node.data,node._constraint)})else:# Some might be root (i.e. blanket nodes) and some might be intermediate nodes# Blanket nodes belong to rootsifall([pinvisitedforpinnode.parents]):# this is an intermediate nodeassertisinstance(node,MessageNode)documentation.update({get_fun_name(node):node.description})graph.append((level,repr_function_call(node)))iflevel==depth:output.update({node.py_name:(node.data,node._constraint)})else:others.update({node.py_name:(node.data,node._constraint)})else:# this is a blanket node (classified into roots)roots.update({node.py_name:(node.data,node._constraint)})returnFunctionFeedback(graph=graph,others=others,roots=roots,output=output,user_feedback=node_feedback.user_feedback,documentation=documentation,)
[docs]@dataclassclassFunctionFeedback:"""Feedback container used by FunctionPropagator."""graph:List[Tuple[int,str]]# Each item is is a representation of function call. The items are topologically sorted.documentation:Dict[str,str]# Function name and its documentationstringothers:Dict[str,Any]# Intermediate variable names and their dataroots:Dict[str,Any]# Root variable name and its dataoutput:Dict[str,Any]# Leaf variable name and its datauser_feedback:str# User feedback at the leaf of the graph
[docs]classOptoPrime(Optimizer):# This is generic representation prompt, which just explains how to read the problem.representation_prompt=dedent(""" You're tasked to solve a coding/algorithm problem. You will see the instruction, the code, the documentation of each function used in the code, and the feedback about the execution result. Specifically, a problem will be composed of the following parts: - #Instruction: the instruction which describes the things you need to do or the question you should answer. - #Code: the code defined in the problem. - #Documentation: the documentation of each function used in #Code. The explanation might be incomplete and just contain high-level description. You can use the values in #Others to help infer how those functions work. - #Variables: the input variables that you can change. - #Constraints: the constraints or descriptions of the variables in #Variables. - #Inputs: the values of other inputs to the code, which are not changeable. - #Others: the intermediate values created through the code execution. - #Outputs: the result of the code output. - #Feedback: the feedback about the code's execution result. In #Variables, #Inputs, #Outputs, and #Others, the format is: <data_type> <variable_name> = <value> If <type> is (code), it means <value> is the source code of a python code, which may include docstring and definitions. """)# Optimizationdefault_objective="You need to change the <value> of the variables in #Variables to improve the output in accordance to #Feedback."output_format_prompt=dedent(""" Output_format: Your output should be in the following json format, satisfying the json syntax: {{ "reasoning": <Your reasoning>, "answer": <Your answer>, "suggestion": {{ <variable_1>: <suggested_value_1>, <variable_2>: <suggested_value_2>, }} }} In "reasoning", explain the problem: 1. what the #Instruction means 2. what the #Feedback on #Output means to #Variables considering how #Variables are used in #Code and other values in #Documentation, #Inputs, #Others. 3. Reasoning about the suggested changes in #Variables (if needed) and the expected result. If #Instruction asks for an answer, write it down in "answer". If you need to suggest a change in the values of #Variables, write down the suggested values in "suggestion". Remember you can change only the values in #Variables, not others. When <type> of a variable is (code), you should write the new definition in the format of python code without syntax errors, and you should not change the function name or the function signature. If no changes or answer are needed, just output TERMINATE. """)example_problem_template=dedent(""" Here is an example of problem instance and response: ================================ {example_problem} ================================ Your response: {example_response} """)user_prompt_template=dedent(""" Now you see problem instance: ================================ {problem_instance} ================================ """)example_prompt=dedent(""" Here are some feasible but not optimal solutions for the current problem instance. Consider this as a hint to help you understand the problem better. ================================ {examples} ================================ """)final_prompt=dedent(""" Your response: """)default_prompt_symbols={"variables":"#Variables","constraints":"#Constraints","inputs":"#Inputs","outputs":"#Outputs","others":"#Others","feedback":"#Feedback","instruction":"#Instruction","code":"#Code","documentation":"#Documentation",}
[docs]def__init__(self,parameters:List[ParameterNode],llm:AutoGenLLM=None,*args,propagator:Propagator=None,objective:Union[None,str]=None,ignore_extraction_error:bool=True,# ignore the type conversion error when extracting updated values from LLM's suggestioninclude_example=False,# TODO # include example problem and response in the promptmemory_size=0,# Memory size to store the past feedbackmax_tokens=4096,log=True,prompt_symbols=None,filter_dict:Dict=None,# autogen filter_dict**kwargs,):super().__init__(parameters,*args,propagator=propagator,**kwargs)self.ignore_extraction_error=ignore_extraction_errorself.llm=llmorAutoGenLLM()self.objective=objectiveorself.default_objectiveself.example_problem=ProblemInstance.problem_template.format(instruction=self.default_objective,code="y = add(x=a,y=b)\nz = subtract(x=y, y=c)",documentation="add: add x and y \nsubtract: subtract y from x",variables="(int) a = 5",constraints="a: a > 0",outputs="(int) z = 1",others="(int) y = 6",inputs="(int) b = 1\n(int) c = 5",feedback="The result of the code is not as expected. The result should be 10, but the code returns 1",stepsize=1,)self.example_response=dedent(""" {"reasoning": 'In this case, the desired response would be to change the value of input a to 14, as that would make the code return 10.', "answer", {}, "suggestion": {"a": 10} } """)self.include_example=include_exampleself.max_tokens=max_tokensself.log=[]iflogelseNoneself.summary_log=[]iflogelseNoneself.memory=FIFOBuffer(memory_size)self.prompt_symbols=copy.deepcopy(self.default_prompt_symbols)ifprompt_symbolsisnotNone:self.prompt_symbols.update(prompt_symbols)
[docs]defdefault_propagator(self):"""Return the default Propagator object of the optimizer."""returnGraphPropagator()
[docs]defsummarize(self):# Aggregate feedback from all the parametersfeedbacks=[self.propagator.aggregate(node.feedback)fornodeinself.parametersifnode.trainable]summary=sum(feedbacks)# TraceGraph# Construct variables and update others# Some trainable nodes might not receive feedback, because they might not be connected to the outputsummary=node_to_function_feedback(summary)# Classify the root nodes into variables and others# summary.variables = {p.py_name: p.data for p in self.parameters if p.trainable and p.py_name in summary.roots}trainable_param_dict={p.py_name:pforpinself.parametersifp.trainable}summary.variables={py_name:dataforpy_name,datainsummary.roots.items()ifpy_nameintrainable_param_dict}summary.inputs={py_name:dataforpy_name,datainsummary.roots.items()ifpy_namenotintrainable_param_dict}# non-variable rootsreturnsummary
[docs]defconstruct_prompt(self,summary,mask=None,*args,**kwargs):"""Construct the system and user prompt."""system_prompt=(self.representation_prompt+self.output_format_prompt)# generic representation + output ruleuser_prompt=self.user_prompt_template.format(problem_instance=str(self.problem_instance(summary,mask=mask)))# problem instanceifself.include_example:user_prompt=(self.example_problem_template.format(example_problem=self.example_problem,example_response=self.example_response,)+user_prompt)user_prompt+=self.final_prompt# Add examplesiflen(self.memory)>0:prefix=user_prompt.split(self.final_prompt)[0]examples=[]forvariables,feedbackinself.memory:examples.append(json.dumps({"variables":{k:v[0]fork,vinvariables.items()},"feedback":feedback,},indent=4,))examples="\n".join(examples)user_prompt=(prefix+f"\nBelow are some variables and their feedbacks you received in the past.\n\n{examples}\n\n"+self.final_prompt)self.memory.add((summary.variables,summary.user_feedback))returnsystem_prompt,user_prompt
[docs]defconstruct_update_dict(self,suggestion:Dict[str,Any])->Dict[ParameterNode,Any]:"""Convert the suggestion in text into the right data type."""# TODO: might need some automatic type conversionupdate_dict={}fornodeinself.parameters:ifnode.trainableandnode.py_nameinsuggestion:try:update_dict[node]=type(node.data)(suggestion[node.py_name])except(ValueError,KeyError)ase:# catch error due to suggestion missing the key or wrong data typeifself.ignore_extraction_error:warnings.warn(f"Cannot convert the suggestion '{suggestion[node.py_name]}' for {node.py_name} to the right data type")else:raiseereturnupdate_dict
[docs]defextract_llm_suggestion(self,response:str):"""Extract the suggestion from the response."""suggestion={}attempt_n=0whileattempt_n<2:try:suggestion=json.loads(response)["suggestion"]breakexceptjson.JSONDecodeError:# Remove things outside the bracketsresponse=re.findall(r"{.*}",response,re.DOTALL)iflen(response)>0:response=response[0]attempt_n+=1exceptException:attempt_n+=1ifnotisinstance(suggestion,dict):suggestion={}iflen(suggestion)==0:# we try to extract key/value separately and return it as a dictionarypattern=r'"suggestion"\s*:\s*\{(.*?)\}'suggestion_match=re.search(pattern,str(response),re.DOTALL)ifsuggestion_match:suggestion={}# Extract the entire content of the suggestion dictionarysuggestion_content=suggestion_match.group(1)# Regex to extract each key-value pair;# This scheme assumes double quotes but is robust to missing cammas at the end of the linepair_pattern=r'"([a-zA-Z0-9_]+)"\s*:\s*"(.*)"'# Find all matches of key-value pairspairs=re.findall(pair_pattern,suggestion_content,re.DOTALL)forkey,valueinpairs:suggestion[key]=valueiflen(suggestion)==0:ifnotself.ignore_extraction_error:print("Cannot extract suggestion from LLM's response:")print(response)# if the suggested value is a code, and the entire code body is empty (i.e., not even function signature is present)# then we remove such suggestionforkey,valueinsuggestion.items():if"__code"inkeyandvalue=="":delsuggestion[key]returnsuggestion
[docs]defcall_llm(self,system_prompt:str,user_prompt:str,verbose:Union[bool,str]=False,max_tokens:int=4096,):"""Call the LLM with a prompt and return the response."""ifverbosenotin(False,"output"):print("Prompt\n",system_prompt+user_prompt)messages=[{"role":"system","content":system_prompt},{"role":"user","content":user_prompt},]try:# Try tp force it to be a json objectresponse=self.llm(messages=messages,response_format={"type":"json_object"},max_tokens=max_tokens,)exceptException:response=self.llm(messages=messages,max_tokens=max_tokens)response=response.choices[0].message.contentifverbose:print("LLM response:\n",response)returnresponse