From e964bc99707a3eb4d40b56d169a47383ce667c19 Mon Sep 17 00:00:00 2001 From: eliranwong Date: Wed, 23 Oct 2024 17:40:10 +0000 Subject: [PATCH] terminal mode work with groqchat --- setup.py | 2 +- uniquebible/latest_changes.txt | 6 + uniquebible/util/LocalCliHandler.py | 558 +----------------------- uniquebible/util/RemoteCliMainWindow.py | 2 +- 4 files changed, 15 insertions(+), 553 deletions(-) diff --git a/setup.py b/setup.py index 0ed41dddb4..7cc91ac9be 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ # https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/ setup( name=package, - version="0.1.14", + version="0.1.15", python_requires=">=3.8, <3.13", description=f"UniqueBible App is a cross-platform & offline bible application, integrated with high-quality resources and unique features. Developers: Eliran Wong and Oliver Tseng", long_description=long_description, diff --git a/uniquebible/latest_changes.txt b/uniquebible/latest_changes.txt index e4e921099f..aa195ae6b0 100755 --- a/uniquebible/latest_changes.txt +++ b/uniquebible/latest_changes.txt @@ -1,5 +1,11 @@ PIP package: +0.1.15 + +* use `groqchat` for bible chat in terminal mode + +* fixed a message when uba is closing on Termux + 0.1.13-0.1.14 * worked with latest ToolMate AI diff --git a/uniquebible/util/LocalCliHandler.py b/uniquebible/util/LocalCliHandler.py index 7fbc4e0acf..9fcc2d3cb8 100644 --- a/uniquebible/util/LocalCliHandler.py +++ b/uniquebible/util/LocalCliHandler.py @@ -283,7 +283,6 @@ def initPromptElements(self): ] bible_chat_history = os.path.join(os.getcwd(), "terminal_history", "bible_chat") - openai_image_history = os.path.join(os.getcwd(), "terminal_history", "openai_image") find_history = os.path.join(os.getcwd(), "terminal_history", "find") module_history_concordance = os.path.join(os.getcwd(), "terminal_history", "concordance") module_history_books = os.path.join(os.getcwd(), "terminal_history", "books") @@ -310,7 +309,6 @@ def initPromptElements(self): #system_command_history = os.path.join(os.getcwd(), "terminal_history", "system_command") self.terminal_bible_chat_session = PromptSession(history=FileHistory(bible_chat_history)) - self.terminal_openai_image_session = PromptSession(history=FileHistory(openai_image_history)) self.terminal_live_filter_session = PromptSession(history=FileHistory(live_filter)) self.terminal_concordance_selection_session = PromptSession(history=FileHistory(module_history_concordance)) self.terminal_books_selection_session = PromptSession(history=FileHistory(module_history_books)) @@ -373,7 +371,9 @@ def addShortcuts(self): self.dotCommands[key] = (f"an alias to '{value}'", partial(self.getContent, value)) def getDotCommands(self): - return {} if config.runMode == "stream" else { + if config.runMode == "stream": + return {} + dotCommands = { config.terminal_cancel_action: ("cancel action in current prompt", self.cancelAction), ".togglecolorbrightness": ("toggle color brightness", self.togglecolorbrightness), ".togglecolourbrightness": ("an alias to '.togglecolorbrightness'", self.togglecolorbrightness), @@ -658,9 +658,10 @@ def getDotCommands(self): ".customise": ("customise ...", self.customise), ".google": ("google ...", self.google), ".watson": ("watson ...", self.watson), - ".chat": ("bible chat", self.bibleChat), - #".image": ("bible chat", self.generateImage), } + if shutil.which("groqchat"): + dotCommands[".chat"] = ("bible chat", self.bibleChat) + return dotCommands def clear_screen(self): clear() @@ -2330,553 +2331,8 @@ def showErrors(self): if config.developer: print(traceback.format_exc()) - def runCompletion(self, thisMessage): - from openai import OpenAI - return OpenAI().chat.completions.create( - model=config.chatGPTApiModel, - messages=thisMessage, - n=1, - temperature=config.chatGPTApiTemperature, - max_tokens=config.chatGPTApiMaxTokens, - stream=True, - ) - - def runCompletion_old(self, thisMessage): - import openai - 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.chatApiFunctionCall, - 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): - import openai - from openai import OpenAI - def changeAPIkey(): - if not config.terminalEnableTermuxAPI or (config.terminalEnableTermuxAPI and self.fingerprint()): - self.print("Enter your OpenAI API Key [required]:") - apikey = self.simplePrompt(default=config.openaiApiKey) - if apikey and not apikey.strip().lower() == config.terminal_cancel_action: - config.openaiApiKey = apikey - self.print("Enter your Organization ID [required for ChatGPT-4]:") - oid = self.simplePrompt(default=config.openaiApiOrganization) - if oid and not oid.strip().lower() == config.terminal_cancel_action: - config.openaiApiOrganization = oid - if not config.openaiApiKey: - changeAPIkey() - - # The following config values can be modified with plugins, to extend functionalities - config.predefinedContexts = { - "[none]": "", - "[custom]": "", - } - config.inputSuggestions = [] - config.chatGPTTransformers = [] - config.chatGPTApiFunctionSignatures = [] - config.chatGPTApiAvailableFunctions = {} - - def runCompletion(thisMessage): - functionJustCalled = False - def runThisCompletion(thisThisMessage): - ''' - if config.chatGPTApiFunctionSignatures and not functionJustCalled: - return openai.ChatCompletion.create( - model=config.chatGPTApiModel, - messages=thisThisMessage, - n=config.chatApiNoOfChoices, - temperature=0.0 if config.chatGPTApiPredefinedContext == "Execute Python Code" else config.chatGPTApiTemperature, - max_tokens=config.chatGPTApiMaxTokens, - functions=config.chatGPTApiFunctionSignatures, - function_call={"name": "run_python"} if config.chatGPTApiPredefinedContext == "Execute Python Code" else config.chatApiFunctionCall, - )''' - return OpenAI().chat.completions.create( - model=config.chatGPTApiModel, - messages=thisThisMessage, - n=config.chatApiNoOfChoices, - temperature=config.chatGPTApiTemperature, - max_tokens=config.chatGPTApiMaxTokens, - ) - - while True: - completion = runThisCompletion(thisMessage) - response_message = completion["choices"][0]["message"] - if response_message.get("function_call"): - # check function name - function_name = response_message["function_call"]["name"] - - if function_name == "python": - config.pythonFunctionResponse = "" - function_args = response_message["function_call"]["arguments"] - new_function_args = self.fineTunePythonCode(function_args) - try: - exec(new_function_args, globals()) - function_response = str(config.pythonFunctionResponse) - except: - function_response = function_args - 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) - - # 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 - if not config.chatAfterFunctionCalled: - self.print(function_response) - break - else: - functionJustCalled = True - else: - break - - return completion - - # reset message when a new chart is started or context is changed - def resetMessages(): - systemMessage = "You’re a kind helpful assistant." - if config.chatApiFunctionCall == "auto" and config.chatGPTApiFunctionSignatures: - systemMessage += " Only use the functions you have been provided with." - messages = [ - {"role": "system", "content" : systemMessage} - ] - return messages - def getCurrentContext(): - if not config.chatGPTApiPredefinedContext in config.predefinedContexts: - config.chatGPTApiPredefinedContext = "[none]" - if config.chatGPTApiPredefinedContext == "[none]": - # no context - context = "" - elif config.chatGPTApiPredefinedContext == "[custom]": - # custom input in the settings dialog - context = config.chatGPTApiContext - else: - # users can modify config.predefinedContexts via plugins - context = config.predefinedContexts[config.chatGPTApiPredefinedContext] - # change configs for particular contexts - if config.chatGPTApiPredefinedContext == "Execute Python Code": - if config.chatApiFunctionCall == "none": - config.chatApiFunctionCall = "auto" - if config.chatApiLoadingInternetSearches == "always": - config.chatApiLoadingInternetSearches = "auto" - return context - def fineTuneUserInput(userInput): - # customise chat context - context = getCurrentContext() - if context and (config.chatGPTApiPredefinedContext == "Execute Python Code" or not self.conversationStarted or (self.conversationStarted and config.chatGPTApiContextInAllInputs)): - userInput = f"{context}\n{userInput}" - return userInput - # required - openai.api_key = os.environ["OPENAI_API_KEY"] = config.openaiApiKey - # optional - if config.openaiApiOrganization: - openai.organization = config.openaiApiOrganization - messages = resetMessages() - if openai.api_key: - pluginFolder = os.path.join(os.getcwd(), "plugins", "chatGPT") - # always run 'integrate google searches' - internetSeraches = "integrate google searches" - script = os.path.join(pluginFolder, "{0}.py".format(internetSeraches)) - self.execPythonFile(script) - for plugin in FileUtil.fileNamesWithoutExtension(pluginFolder, "py"): - if not plugin in config.chatGPTPluginExcludeList: - script = os.path.join(pluginFolder, "{0}.py".format(plugin)) - self.execPythonFile(script) - if internetSeraches in config.chatGPTPluginExcludeList: - del config.chatGPTApiFunctionSignatures[0] - try: - self.conversationStarted = False - def startChat(): - chat = config.thisTranslation["chat"] - self.print(f"{chat}: {config.chatGPTApiPredefinedContext if not config.chatGPTApiPredefinedContext == '[none]' else ''}") - self.print("(blank entry for options)") - self.conversationStarted = False - startChat() - multilineInput = False - completer = FuzzyCompleter(WordCompleter(config.inputSuggestions, ignore_case=True)) if config.inputSuggestions else None - features = ( - ".new", - ".singleLineInput", - ".multiLineInput", - ".changeapikey", - ".chatgptmodel", - ".maxtokens", - ".functioncall", - ".functionresponse", - ".context", - ".contextInFirstInputOnly", - ".contextInAllInputs", - ".latestSearches", - ".autolatestSearches", - ".noLatestSearches", - ".share" if config.terminalEnableTermuxAPI else ".save", - ) - featuresLower = [i.lower() for i in features] + [".save", ".share"] - while True: - userInput = self.simplePrompt(promptSession=self.terminal_bible_chat_session, multiline=multilineInput, completer=completer) - # display options when empty string is entered - if not userInput.strip(): - # userInput = ".context" - #if userInput.lower().strip() == "...": - descriptions = ( - "start a new chat", - "single-line user input", - "multi-line user input", - "change API key", - "change ChatGPT model", - "change maximum tokens", - "change function call", - "change function response", - "change chat context", - "apply context in first input ONLY", - "apply context in ALL inputs", - "integrate latest online search result", - "integrate latest online search result when needed", - "exclude latest online search result", - "share content" if config.terminalEnableTermuxAPI else "save content", - ) - feature = self.dialogs.getValidOptions(options=features, descriptions=descriptions, title="Bible Chat Options", default=".context") - if feature: - if feature == ".chatgptmodel": - models = ("gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "gpt-4-32k") - model = self.dialogs.getValidOptions(options=models, title="ChatGPT model", default=config.chatGPTApiModel) - if model: - config.chatGPTApiModel = model - self.print(f"ChatGPT model selected: {model}") - elif feature == ".functioncall": - calls = ("auto", "none") - call = self.dialogs.getValidOptions(options=calls, title="ChatGPT Function Call", default=config.chatApiFunctionCall) - if call: - config.chatApiFunctionCall = call - self.print(f"ChaptGPT function call: {'enabled' if config.chatApiFunctionCall == 'auto' else 'disabled'}!") - elif feature == ".functionresponse": - calls = ("enable", "disable") - call = self.dialogs.getValidOptions(options=calls, title="Automatic Chat Generation with Function Response", default="enable" if config.chatAfterFunctionCalled else "disable") - if call: - config.chatAfterFunctionCalled = (call == "enable") - self.print(f"Automatic Chat Generation with Function Response: {'enabled' if config.chatAfterFunctionCalled else 'disabled'}!") - elif feature == ".maxtokens": - maxtokens = self.simplePrompt(numberOnly=True, default=str(config.chatGPTApiMaxTokens)) - if maxtokens and not maxtokens.strip().lower() == config.terminal_cancel_action and int(maxtokens) > 0: - config.chatGPTApiMaxTokens = int(maxtokens) - self.print(f"Maximum tokens entered: {maxtokens}") - elif feature == ".changeapikey": - changeAPIkey() - elif feature == ".singleLineInput": - multilineInput = False - self.print("Multi-line user input disabled!") - elif feature == ".multiLineInput": - multilineInput = True - self.print("Multi-line user input enabled!") - elif feature == ".latestSearches": - config.chatApiLoadingInternetSearches = "always" - self.print("Latest online search results always enabled!") - elif feature == ".autolatestSearches": - config.chatApiLoadingInternetSearches = "auto" - config.chatApiFunctionCall = "auto" - if "integrate google searches" in config.chatGPTPluginExcludeList: - config.chatGPTPluginExcludeList.remove("integrate google searches") - self.print("Latest online search results enabled, if necessary!") - elif feature == ".noLatestSearches": - config.chatApiLoadingInternetSearches = "none" - if not "integrate google searches" in config.chatGPTPluginExcludeList: - config.chatGPTPluginExcludeList.append("integrate google searches") - self.print("Latest online search results disabled!") - elif feature == ".contextInFirstInputOnly": - config.chatGPTApiContextInAllInputs = False - self.print("Predefined context is now applied in the first input only!") - elif feature == ".contextInAllInputs": - config.chatGPTApiContextInAllInputs = True - self.print("Predefined context is now applied in all inputs!") - else: - userInput = feature - if userInput.strip().lower() == config.terminal_cancel_action: - return self.cancelAction() - elif userInput.strip().lower() == ".context": - contexts = list(config.predefinedContexts.keys()) - predefinedContext = self.dialogs.getValidOptions(options=contexts, title="Bible Chat Predefined Contexts", default=config.chatGPTApiPredefinedContext) - if predefinedContext: - config.chatGPTApiPredefinedContext = predefinedContext - if config.chatGPTApiPredefinedContext == "[custom]": - customContext = self.simplePrompt(default=config.chatGPTApiContext) - if customContext and not customContext.strip().lower() == config.terminal_cancel_action: - config.chatGPTApiContext = customContext.strip() - print(f"Context selected: {config.chatGPTApiPredefinedContext}") - elif userInput.strip().lower() == ".new" and self.conversationStarted: - messages = resetMessages() - startChat() - elif userInput.strip().lower() in (".share", ".save") and self.conversationStarted: - plainText = "" - for i in messages: - if i["role"] == "user": - content = i["content"] - plainText += f">>> {content}" - elif i["role"] == "assistant": - content = i["content"] - if plainText: - plainText += "\n\n" - plainText += f"{content}\n\n" - plainText = plainText.strip() - if config.terminalEnableTermuxAPI: - pydoc.pipepager(plainText, cmd="termux-share -a send") - else: - try: - filename = re.sub('[\\\/\:\*\?\"\<\>\|]', "", messages[2 if config.chatGPTApiContext.strip() else 1]["content"])[:40].strip() - if filename: - chatFile = os.path.join(config.marvelData, "chats", f"{filename}.txt") - with open(chatFile, "w", encoding="utf-8") as fileObj: - fileObj.write(plainText) - if os.path.isfile(chatFile): - os.system(f'''{config.open} "{chatFile}"''') - except: - self.print("Failed to save a file!") - elif userInput.strip() and not userInput.strip().lower() in featuresLower: - # start spinning - stop_event = threading.Event() - spinner_thread = threading.Thread(target=self.spinning_animation, args=(stop_event,)) - spinner_thread.start() - # get responses - fineTunedUserInput = fineTuneUserInput(userInput) - messages.append({"role": "user", "content": fineTunedUserInput}) - - # force loading internet searches - if config.chatApiLoadingInternetSearches == "always": - try: - completion = openai.ChatCompletion.create( - model=config.chatGPTApiModel, - messages=messages, - max_tokens=config.chatGPTApiMaxTokens, - temperature=config.chatGPTApiTemperature, - n=1, - functions=config.integrate_google_searches_signature, - function_call={"name": "integrate_google_searches"}, - ) - response_message = completion["choices"][0]["message"] - if response_message.get("function_call"): - function_args = json.loads(response_message["function_call"]["arguments"]) - fuction_to_call = config.chatGPTApiAvailableFunctions.get("integrate_google_searches") - function_response = fuction_to_call(function_args) - messages.append(response_message) # extend conversation with assistant's reply - messages.append( - { - "role": "function", - "name": "integrate_google_searches", - "content": function_response, - } - ) - except: - print("Unable to load internet resources.") - - # enable output stream if choice is set to 1 - if config.chatApiNoOfChoices == 1: - completion = self.runCompletion(messages) - # stop spinning - stop_event.set() - spinner_thread.join() - 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: - completion = runCompletion(messages) - # stop spinning - stop_event.set() - spinner_thread.join() - for index, choice in enumerate(completion.choices): - chat_response = choice.message.content - if chat_response: - # transform response with plugins - for t in config.chatGPTTransformers: - chat_response = t(chat_response) - if len(completion.choices) > 1: - self.print(f"### Response {(index+1)}:") - self.print(chat_response) - if index == 0: - messages[-1] = {"role": "user", "content": userInput} - messages.append({"role": "assistant", "content": chat_response}) - self.conversationStarted = True - #stop_event.set() - #spinner_thread.join() - #self.print("##########") - # error codes: https://platform.openai.com/docs/guides/error-codes/python-library-error-types - except openai.error.APIError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle API error here, e.g. retry or log - print(f"OpenAI API returned an API Error: {e}") - except openai.error.APIConnectionError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle connection error here - print(f"Failed to connect to OpenAI API: {e}") - except openai.error.RateLimitError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle rate limit error (we recommend using exponential backoff) - print(f"OpenAI API request exceeded rate limit: {e}") - else: - self.print("OpenAI API key not found!") - return "" - - def generateImage(self): - import openai - # required - openai.api_key = os.environ["OPENAI_API_KEY"] = config.openaiApiKey - # optional - if config.openaiApiOrganization: - openai.organization = config.openaiApiOrganization - if openai.api_key: - try: - while True: - self.print("Discribe your image:") - userInput = self.simplePrompt(promptSession=self.terminal_openai_image_session) - if userInput.lower() == config.terminal_cancel_action: - return self.cancelAction() - # start spinning - stop_event = threading.Event() - spinner_thread = threading.Thread(target=self.spinning_animation, args=(stop_event,)) - spinner_thread.start() - # get responses - #https://platform.openai.com/docs/guides/images/introduction - response = openai.Image.create( - prompt=userInput, - n=1, - size="1024x1024", - response_format="b64_json", - ) - # stop spinning - stop_event.set() - spinner_thread.join() - # open image - #imageUrl = response['data'][0]['url'] - jsonFile = os.path.join("temp", "openai_image.json") - with open(jsonFile, mode="w", encoding="utf-8") as fileObj: - json.dump(response, fileObj) - imageFile = os.path.join("temp", "openai_image.png") - with open(jsonFile, mode="r", encoding="utf-8") as fileObj: - jsonContent = json.load(fileObj) - image_data = b64decode(jsonContent["data"][0]["b64_json"]) - with open(imageFile, mode="wb") as pngObj: - pngObj.write(image_data) - if config.terminalEnableTermuxAPI: - self.getCliOutput(f"termux-share {imageFile}") - else: - os.system(f"{config.open} {imageFile}") - # error codes: https://platform.openai.com/docs/guides/error-codes/python-library-error-types - except openai.error.APIError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle API error here, e.g. retry or log - print(f"OpenAI API returned an API Error: {e}") - except openai.error.APIConnectionError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle connection error here - print(f"Failed to connect to OpenAI API: {e}") - except openai.error.RateLimitError as e: - try: - stop_event.set() - spinner_thread.join() - except: - pass - #Handle rate limit error (we recommend using exponential backoff) - print(f"OpenAI API request exceeded rate limit: {e}") - else: - self.print("OpenAI API key not found!") - return "" + os.system('''groqchat -n "Bible Chat" -s "You are an expert on the bible" Hi!''') def names(self): with open(os.path.join("plugins", "menu", "Bible_Data", "Bible Names.txt"), "r", encoding="utf-8") as input_file: diff --git a/uniquebible/util/RemoteCliMainWindow.py b/uniquebible/util/RemoteCliMainWindow.py index fff34e6654..3bfbb62b60 100644 --- a/uniquebible/util/RemoteCliMainWindow.py +++ b/uniquebible/util/RemoteCliMainWindow.py @@ -340,7 +340,7 @@ def closeMediaPlayer(self): pass # close macOS text-to-speak voice - if WebtopUtil.isPackageInstalled("say"): + if shutil.which("say"): os.system("pkill say") VlcUtil.closeVlcPlayer() # close espeak on Linux