From 7ed06a20267675a8cdfd9c9dc086cf19649be866 Mon Sep 17 00:00:00 2001 From: Eliran Wong Date: Thu, 5 Oct 2023 17:56:11 +0100 Subject: [PATCH] v35.02; stream chat output with func call enabled --- UniqueBibleAppVersion.txt | 2 +- gui/Worker.py | 163 ++++++++++++++++++++---- latest_changes.txt | 4 + patches.txt | 12 +- plugins/chatGPT/execute python code.py | 19 +-- util/LocalCliHandler.py | 166 +++++++++++++++++++++---- 6 files changed, 306 insertions(+), 60 deletions(-) diff --git a/UniqueBibleAppVersion.txt b/UniqueBibleAppVersion.txt index 243b4f01d2..7fa7fbe775 100755 --- a/UniqueBibleAppVersion.txt +++ b/UniqueBibleAppVersion.txt @@ -1 +1 @@ -35.01 +35.02 diff --git a/gui/Worker.py b/gui/Worker.py index 663d20e80e..d599fcd543 100644 --- a/gui/Worker.py +++ b/gui/Worker.py @@ -88,6 +88,133 @@ def __init__(self, parent): self.parent = parent self.threadpool = QThreadPool() + def fineTunePythonCode(self, code): + insert_string = "import config\nconfig.pythonFunctionResponse = " + code = re.sub("^!(.*?)$", r"import os\nos.system(\1)", code, flags=re.M) + if "\n" in code: + substrings = code.rsplit("\n", 1) + lastLine = re.sub("print\((.*)\)", r"\1", substrings[-1]) + code = code if lastLine.startswith(" ") else f"{substrings[0]}\n{insert_string}{lastLine}" + else: + code = f"{insert_string}{code}" + return code + + def getFunctionResponse(self, response_message, function_name): + if function_name == "python": + config.pythonFunctionResponse = "" + python_code = textwrap.dedent(response_message["function_call"]["arguments"]) + refinedCode = self.fineTunePythonCode(python_code) + + print("--------------------") + print(f"running python code ...") + if config.developer or config.codeDisplay: + print("```") + print(python_code) + print("```") + print("--------------------") + + try: + exec(refinedCode, globals()) + function_response = str(config.pythonFunctionResponse) + except: + function_response = python_code + info = {"information": function_response} + function_response = json.dumps(info) + else: + fuction_to_call = config.chatGPTApiAvailableFunctions[function_name] + function_args = json.loads(response_message["function_call"]["arguments"]) + function_response = fuction_to_call(function_args) + return function_response + + def getStreamFunctionResponseMessage(self, completion, function_name): + function_arguments = "" + for event in completion: + delta = event["choices"][0]["delta"] + if delta and delta.get("function_call"): + function_arguments += delta["function_call"]["arguments"] + return { + "role": "assistant", + "content": None, + "function_call": { + "name": function_name, + "arguments": function_arguments, + } + } + + def runCompletion(self, thisMessage, progress_callback): + self.functionJustCalled = False + def runThisCompletion(thisThisMessage): + if config.chatGPTApiFunctionSignatures and not self.functionJustCalled: + return openai.ChatCompletion.create( + model=config.chatGPTApiModel, + messages=thisThisMessage, + n=1, + temperature=config.chatGPTApiTemperature, + max_tokens=config.chatGPTApiMaxTokens, + functions=config.chatGPTApiFunctionSignatures, + function_call=config.chatGPTApiFunctionCall, + stream=True, + ) + return openai.ChatCompletion.create( + model=config.chatGPTApiModel, + messages=thisThisMessage, + n=1, + temperature=config.chatGPTApiTemperature, + max_tokens=config.chatGPTApiMaxTokens, + stream=True, + ) + + while True: + completion = runThisCompletion(thisMessage) + function_name = "" + try: + # consume the first delta + for event in completion: + delta = event["choices"][0]["delta"] + # Check if a function is called + if not delta.get("function_call"): + self.functionJustCalled = True + elif "name" in delta["function_call"]: + function_name = delta["function_call"]["name"] + # check the first delta is enough + break + # Continue only when a function is called + if self.functionJustCalled: + break + + # get stream function response message + response_message = self.getStreamFunctionResponseMessage(completion, function_name) + + # get function response + function_response = self.getFunctionResponse(response_message, function_name) + + # process function response + # send the info on the function call and function response to GPT + thisMessage.append(response_message) # extend conversation with assistant's reply + thisMessage.append( + { + "role": "function", + "name": function_name, + "content": function_response, + } + ) # extend conversation with function response + + self.functionJustCalled = True + + if not config.chatAfterFunctionCalled: + progress_callback.emit("\n\n~~~ ") + progress_callback.emit(function_response) + return None + except: + self.showErrors() + break + + return completion + + def showErrors(self): + if config.developer: + print(traceback.format_exc()) + def getResponse(self, messages, progress_callback, functionJustCalled=False): responses = "" if config.chatGPTApiLoadingInternetSearches == "always" and not functionJustCalled: @@ -118,27 +245,21 @@ def getResponse(self, messages, progress_callback, functionJustCalled=False): except: print("Unable to load internet resources.") try: - if config.chatGPTApiNoOfChoices == 1 and (config.chatGPTApiFunctionCall == "none" or not config.chatGPTApiFunctionSignatures or functionJustCalled): - completion = openai.ChatCompletion.create( - model=config.chatGPTApiModel, - messages=messages, - max_tokens=config.chatGPTApiMaxTokens, - temperature=config.chatGPTApiTemperature, - n=config.chatGPTApiNoOfChoices, - stream=True, - ) - progress_callback.emit("\n\n~~~ ") - for event in completion: - # stop generating response - stop_file = ".stop_chatgpt" - if os.path.isfile(stop_file): - os.remove(stop_file) - break - # RETRIEVE THE TEXT FROM THE RESPONSE - event_text = event["choices"][0]["delta"] # EVENT DELTA RESPONSE - progress = event_text.get("content", "") # RETRIEVE CONTENT - # STREAM THE ANSWER - progress_callback.emit(progress) + if config.chatGPTApiNoOfChoices == 1: + completion = self.runCompletion(messages, progress_callback) + if completion is not None: + progress_callback.emit("\n\n~~~ ") + for event in completion: + # stop generating response + stop_file = ".stop_chatgpt" + if os.path.isfile(stop_file): + os.remove(stop_file) + break + # RETRIEVE THE TEXT FROM THE RESPONSE + event_text = event["choices"][0]["delta"] # EVENT DELTA RESPONSE + progress = event_text.get("content", "") # RETRIEVE CONTENT + # STREAM THE ANSWER + progress_callback.emit(progress) else: if config.chatGPTApiFunctionSignatures: completion = openai.ChatCompletion.create( diff --git a/latest_changes.txt b/latest_changes.txt index 601420bc10..0704794e54 100755 --- a/latest_changes.txt +++ b/latest_changes.txt @@ -1,3 +1,7 @@ +Changes in 35.02: +Improved plugin "Bible Chat": +* support output stream with function calling enabled + Changes in 35.01: Improved plugin "Bible Chat": * Added previous entered commands to auto completion diff --git a/patches.txt b/patches.txt index ba8582ee71..532e56eee3 100755 --- a/patches.txt +++ b/patches.txt @@ -1385,11 +1385,11 @@ (34.97, "file", "gui/MainWindow.py") (34.97, "file", "util/ConfigUtil.py") (34.98, "file", "plugins/chatGPT/integrate google searches.py") -(35.00, "file", "gui/Worker.py") -(35.00, "file", "plugins/chatGPT/execute python code.py") -(35.00, "file", "util/LocalCliHandler.py") (35.01, "file", "plugins/chatGPT/000_UBA.py") (35.01, "file", "plugins/menu/Bible Chat.py") -(35.01, "file", "patches.txt") -(35.01, "file", "UniqueBibleAppVersion.txt") -(35.01, "file", "latest_changes.txt") +(35.02, "file", "gui/Worker.py") +(35.02, "file", "plugins/chatGPT/execute python code.py") +(35.02, "file", "util/LocalCliHandler.py") +(35.02, "file", "patches.txt") +(35.02, "file", "UniqueBibleAppVersion.txt") +(35.02, "file", "latest_changes.txt") diff --git a/plugins/chatGPT/execute python code.py b/plugins/chatGPT/execute python code.py index 52f4ee3cb1..83404c3a60 100644 --- a/plugins/chatGPT/execute python code.py +++ b/plugins/chatGPT/execute python code.py @@ -21,16 +21,21 @@ # Open VLC player. def run_python(function_args): + def fineTunePythonCode(code): + insert_string = "import config\nconfig.pythonFunctionResponse = " + code = re.sub("^!(.*?)$", r"import os\nos.system(\1)", code, flags=re.M) + if "\n" in code: + substrings = code.rsplit("\n", 1) + lastLine = re.sub("print\((.*)\)", r"\1", substrings[-1]) + code = code if lastLine.startswith(" ") else f"{substrings[0]}\n{insert_string}{lastLine}" + else: + code = f"{insert_string}{code}" + return code + # retrieve argument values from a dictionary #print(function_args) function_args = function_args.get("code") # required - - insert_string = "import config\nconfig.pythonFunctionResponse = " - if "\n" in function_args: - substrings = function_args.rsplit("\n", 1) - new_function_args = f"{substrings[0]}\n{insert_string}{substrings[-1]}" - else: - new_function_args = f"{insert_string}{function_args}" + new_function_args = fineTunePythonCode(function_args) try: exec(new_function_args, globals()) function_response = str(config.pythonFunctionResponse) diff --git a/util/LocalCliHandler.py b/util/LocalCliHandler.py index 9c6ac574d9..21b4596d96 100644 --- a/util/LocalCliHandler.py +++ b/util/LocalCliHandler.py @@ -1,4 +1,4 @@ -import re, config, pprint, os, requests, platform, pydoc, markdown, sys, subprocess, json, shutil, webbrowser +import re, config, pprint, os, requests, platform, pydoc, markdown, sys, subprocess, json, shutil, webbrowser, traceback import openai, threading, time from duckduckgo_search import ddg from functools import partial @@ -2161,6 +2161,132 @@ def spinning_animation(self, stop_event): print(symbol, end='\r') time.sleep(0.1) + def fineTunePythonCode(self, code): + insert_string = "import config\nconfig.pythonFunctionResponse = " + code = re.sub("^!(.*?)$", r"import os\nos.system(\1)", code, flags=re.M) + if "\n" in code: + substrings = code.rsplit("\n", 1) + lastLine = re.sub("print\((.*)\)", r"\1", substrings[-1]) + code = code if lastLine.startswith(" ") else f"{substrings[0]}\n{insert_string}{lastLine}" + else: + code = f"{insert_string}{code}" + return code + + def getFunctionResponse(self, response_message, function_name): + if function_name == "python": + config.pythonFunctionResponse = "" + python_code = textwrap.dedent(response_message["function_call"]["arguments"]) + refinedCode = self.fineTunePythonCode(python_code) + + print("--------------------") + print(f"running python code ...") + if config.developer or config.codeDisplay: + print("```") + print(python_code) + print("```") + print("--------------------") + + try: + exec(refinedCode, globals()) + function_response = str(config.pythonFunctionResponse) + except: + function_response = python_code + info = {"information": function_response} + function_response = json.dumps(info) + else: + fuction_to_call = config.chatGPTApiAvailableFunctions[function_name] + function_args = json.loads(response_message["function_call"]["arguments"]) + function_response = fuction_to_call(function_args) + return function_response + + def getStreamFunctionResponseMessage(self, completion, function_name): + function_arguments = "" + for event in completion: + delta = event["choices"][0]["delta"] + if delta and delta.get("function_call"): + function_arguments += delta["function_call"]["arguments"] + return { + "role": "assistant", + "content": None, + "function_call": { + "name": function_name, + "arguments": function_arguments, + } + } + + def showErrors(self): + if config.developer: + print(traceback.format_exc()) + + def runCompletion(self, thisMessage): + self.functionJustCalled = False + def runThisCompletion(thisThisMessage): + if config.chatGPTApiFunctionSignatures and not self.functionJustCalled: + return openai.ChatCompletion.create( + model=config.chatGPTApiModel, + messages=thisThisMessage, + n=1, + temperature=config.chatGPTApiTemperature, + max_tokens=config.chatGPTApiMaxTokens, + functions=config.chatGPTApiFunctionSignatures, + function_call=config.chatGPTApiFunctionCall, + stream=True, + ) + return openai.ChatCompletion.create( + model=config.chatGPTApiModel, + messages=thisThisMessage, + n=1, + temperature=config.chatGPTApiTemperature, + max_tokens=config.chatGPTApiMaxTokens, + stream=True, + ) + + while True: + completion = runThisCompletion(thisMessage) + function_name = "" + try: + # consume the first delta + for event in completion: + delta = event["choices"][0]["delta"] + # Check if a function is called + if not delta.get("function_call"): + self.functionJustCalled = True + elif "name" in delta["function_call"]: + function_name = delta["function_call"]["name"] + # check the first delta is enough + break + # Continue only when a function is called + if self.functionJustCalled: + break + + # get stream function response message + response_message = self.getStreamFunctionResponseMessage(completion, function_name) + + # get function response + function_response = self.getFunctionResponse(response_message, function_name) + + # process function response + # send the info on the function call and function response to GPT + thisMessage.append(response_message) # extend conversation with assistant's reply + thisMessage.append( + { + "role": "function", + "name": function_name, + "content": function_response, + } + ) # extend conversation with function response + + self.functionJustCalled = True + + if not config.chatAfterFunctionCalled: + self.print(function_response) + return None + except: + self.showErrors() + break + + return completion + def bibleChat(self): def changeAPIkey(): if not config.terminalEnableTermuxAPI or (config.terminalEnableTermuxAPI and self.fingerprint()): @@ -2216,12 +2342,7 @@ def runThisCompletion(thisThisMessage): if function_name == "python": config.pythonFunctionResponse = "" function_args = response_message["function_call"]["arguments"] - insert_string = "import config\nconfig.pythonFunctionResponse = " - if "\n" in function_args: - substrings = function_args.rsplit("\n", 1) - new_function_args = f"{substrings[0]}\n{insert_string}{substrings[-1]}" - else: - new_function_args = f"{insert_string}{function_args}" + new_function_args = self.fineTunePythonCode(function_args) try: exec(new_function_args, globals()) function_response = str(config.pythonFunctionResponse) @@ -2490,27 +2611,22 @@ def startChat(): except: print("Unable to load internet resources.") - if config.chatGPTApiNoOfChoices == 1 and (config.chatGPTApiFunctionCall == "none" or not config.chatGPTApiFunctionSignatures): - completion = openai.ChatCompletion.create( - model=config.chatGPTApiModel, - messages=messages, - n=config.chatGPTApiNoOfChoices, - temperature=config.chatGPTApiTemperature, - max_tokens=config.chatGPTApiMaxTokens, - stream=True, - ) + # enable output stream if choice is set to 1 + if config.chatGPTApiNoOfChoices == 1: + completion = self.runCompletion(messages) # stop spinning stop_event.set() spinner_thread.join() - chat_response = "" - for event in completion: - # RETRIEVE THE TEXT FROM THE RESPONSE - event_text = event["choices"][0]["delta"] # EVENT DELTA RESPONSE - answer = event_text.get("content", "") # RETRIEVE CONTENT - # STREAM THE ANSWER - chat_response += answer - print(answer, end='', flush=True) # Print the response - print("\n") + if completion is not None: + chat_response = "" + for event in completion: + # RETRIEVE THE TEXT FROM THE RESPONSE + event_text = event["choices"][0]["delta"] # EVENT DELTA RESPONSE + answer = event_text.get("content", "") # RETRIEVE CONTENT + # STREAM THE ANSWER + chat_response += answer + print(answer, end='', flush=True) # Print the response + print("\n") messages[-1] = {"role": "user", "content": userInput} messages.append({"role": "assistant", "content": chat_response}) else: