from functional import * class ChoiceStrategy(metaclass=ABCMeta): def __init__(self, choice): self.choice = choice self.delta = choice['delta'] @abstractmethod def support(self): pass @abstractmethod def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): pass class RoleChoiceStrategy(ChoiceStrategy): def support(self): return 'role' in self.delta def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): bot_backend.set_assistant_role_name(assistant_role_name=self.delta['role']) return history, whether_exit class ContentChoiceStrategy(ChoiceStrategy): def support(self): return 'content' in self.delta and self.delta['content'] is not None # null value of content often occur in function call: # { # "role": "assistant", # "content": null, # "function_call": { # "name": "python", # "arguments": "" # } # } def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): bot_backend.add_content(content=self.delta.get('content', '')) history[-1][1] = bot_backend.content return history, whether_exit class NameFunctionCallChoiceStrategy(ChoiceStrategy): def support(self): return 'function_call' in self.delta and 'name' in self.delta['function_call'] def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): python_function_dict = bot_backend.jupyter_kernel.available_functions additional_tools = bot_backend.additional_tools bot_backend.set_function_name(function_name=self.delta['function_call']['name']) bot_backend.copy_current_bot_history(bot_history=history) if bot_backend.function_name not in python_function_dict and bot_backend.function_name not in additional_tools: history.append( [ None, f'GPT attempted to call a function that does ' f'not exist: {bot_backend.function_name}\n ' ] ) whether_exit = True return history, whether_exit class ArgumentsFunctionCallChoiceStrategy(ChoiceStrategy): def support(self): return 'function_call' in self.delta and 'arguments' in self.delta['function_call'] def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): bot_backend.add_function_args_str(function_args_str=self.delta['function_call']['arguments']) if bot_backend.function_name == 'python': # handle hallucinatory function calls """ In practice, we have noticed that GPT, especially GPT-3.5, may occasionally produce hallucinatory function calls. These calls involve a non-existent function named `python` with arguments consisting solely of raw code text (not a JSON format). """ temp_code_str = bot_backend.function_args_str bot_backend.update_code_str(code_str=temp_code_str) bot_backend.update_display_code_block( display_code_block="\n🔴Working:\n```python\n{}\n```".format(temp_code_str) ) history = copy.deepcopy(bot_backend.bot_history) history[-1][1] += bot_backend.display_code_block elif bot_backend.function_name == 'execute_code': temp_code_str = parse_json(function_args=bot_backend.function_args_str, finished=False) if temp_code_str is not None: bot_backend.update_code_str(code_str=temp_code_str) bot_backend.update_display_code_block( display_code_block="\n🔴Working:\n```python\n{}\n```".format( temp_code_str ) ) history = copy.deepcopy(bot_backend.bot_history) history[-1][1] += bot_backend.display_code_block else: history = copy.deepcopy(bot_backend.bot_history) history[-1][1] += bot_backend.display_code_block else: pass return history, whether_exit class FinishReasonChoiceStrategy(ChoiceStrategy): def support(self): return self.choice['finish_reason'] is not None def execute(self, bot_backend: BotBackend, history: List, whether_exit: bool): if bot_backend.content: bot_backend.add_gpt_response_content_message() bot_backend.update_finish_reason(finish_reason=self.choice['finish_reason']) if bot_backend.finish_reason == 'function_call': if bot_backend.function_name in bot_backend.jupyter_kernel.available_functions: history, whether_exit = self.handle_execute_code_finish_reason( bot_backend=bot_backend, history=history, whether_exit=whether_exit ) else: history, whether_exit = self.handle_tool_finish_reason( bot_backend=bot_backend, history=history, whether_exit=whether_exit ) bot_backend.reset_gpt_response_log_values(exclude=['finish_reason']) return history, whether_exit def handle_execute_code_finish_reason(self, bot_backend: BotBackend, history: List, whether_exit: bool): function_dict = bot_backend.jupyter_kernel.available_functions try: code_str = self.get_code_str(bot_backend) bot_backend.update_code_str(code_str=code_str) bot_backend.update_display_code_block( display_code_block="\n🟢Finished:\n```python\n{}\n```".format(code_str) ) history = copy.deepcopy(bot_backend.bot_history) history[-1][1] += bot_backend.display_code_block # function response bot_backend.update_code_executing_state(code_executing=True) text_to_gpt, content_to_display = function_dict[ bot_backend.function_name ](code_str) bot_backend.update_code_executing_state(code_executing=False) # add function call to conversion bot_backend.add_function_call_response_message(function_response=text_to_gpt, save_tokens=True) if bot_backend.interrupt_signal_sent: bot_backend.append_system_msg(prompt='Code execution is manually stopped by user, no need to fix.') add_code_execution_result_to_bot_history( content_to_display=content_to_display, history=history, unique_id=bot_backend.unique_id ) return history, whether_exit except json.JSONDecodeError: history.append( [None, f"GPT generate wrong function args: {bot_backend.function_args_str}"] ) whether_exit = True return history, whether_exit except KeyError as key_error: history.append([None, f'Backend key_error: {key_error}']) whether_exit = True return history, whether_exit except Exception as e: history.append([None, f'Backend error: {e}']) whether_exit = True return history, whether_exit @staticmethod def handle_tool_finish_reason(bot_backend: BotBackend, history: List, whether_exit: bool): function_dict = bot_backend.additional_tools function_name = bot_backend.function_name function = function_dict[function_name]['tool'] # parser function args try: kwargs = json.loads(bot_backend.function_args_str) kwargs.update(function_dict[function_name]['additional_parameters']) except json.JSONDecodeError: history.append( [None, f"GPT generate wrong function args: {bot_backend.function_args_str}"] ) whether_exit = True return history, whether_exit else: # function response function_response, hypertext_to_display = function(**kwargs) # add function call to conversion bot_backend.add_function_call_response_message(function_response=function_response, save_tokens=False) # add hypertext response to bot history add_function_response_to_bot_history(hypertext_to_display=hypertext_to_display, history=history) return history, whether_exit @staticmethod def get_code_str(bot_backend): if bot_backend.function_name == 'python': code_str = bot_backend.function_args_str else: code_str = parse_json(function_args=bot_backend.function_args_str, finished=True) if code_str is None: raise json.JSONDecodeError return code_str class ChoiceHandler: strategies = [ RoleChoiceStrategy, ContentChoiceStrategy, NameFunctionCallChoiceStrategy, ArgumentsFunctionCallChoiceStrategy, FinishReasonChoiceStrategy ] def __init__(self, choice): self.choice = choice def handle(self, bot_backend: BotBackend, history: List, whether_exit: bool): for Strategy in self.strategies: strategy_instance = Strategy(choice=self.choice) if not strategy_instance.support(): continue history, whether_exit = strategy_instance.execute( bot_backend=bot_backend, history=history, whether_exit=whether_exit ) return history, whether_exit def parse_response(chunk, history: List, bot_backend: BotBackend): """ :return: history, whether_exit """ whether_exit = False if chunk['choices']: choice = chunk['choices'][0] choice_handler = ChoiceHandler(choice=choice) history, whether_exit = choice_handler.handle( history=history, bot_backend=bot_backend, whether_exit=whether_exit ) return history, whether_exit