From 83fbd58db33edbb742d050eb613cf2e2b96ea4f6 Mon Sep 17 00:00:00 2001 From: Hugo Saporetti Junior Date: Fri, 22 Nov 2024 20:00:32 -0300 Subject: [PATCH] Adjustments in TUI. Add new assistive Icon --- src/main/askai/core/askai.py | 2 +- src/main/askai/core/askai_cli.py | 1 + src/main/askai/core/commander/commander.py | 9 ++++++++ .../askai/core/engine/openai/openai_vision.py | 4 ++-- src/main/askai/core/support/text_formatter.py | 17 +++++++++----- src/main/askai/tui/app_header.py | 18 ++++++++++----- src/main/askai/tui/app_icons.py | 5 ++++- src/main/askai/tui/askai_app.py | 22 ++++++++++++++----- 8 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/main/askai/core/askai.py b/src/main/askai/core/askai.py index cf486a8f..e62ff8fd 100644 --- a/src/main/askai/core/askai.py +++ b/src/main/askai/core/askai.py @@ -58,6 +58,7 @@ class AskAi: @staticmethod def _abort(): """Abort the execution and exit.""" + terminal.restore() sys.exit(ExitStatus.FAILED.val) def __init__( @@ -134,7 +135,6 @@ def abort(self, signals: Any | None = None, frame: Any | None = None) -> None: self._abort() events.abort.emit(message="User interrupted [ctrl+c]") threading.Timer(1, lambda: setattr(self, "_abort_count", 0)).start() - terminal.restore() def run(self) -> None: """Run the application.""" diff --git a/src/main/askai/core/askai_cli.py b/src/main/askai/core/askai_cli.py index f444c039..df20c0e3 100644 --- a/src/main/askai/core/askai_cli.py +++ b/src/main/askai/core/askai_cli.py @@ -81,6 +81,7 @@ def run(self) -> None: elif output: cache.save_reply(question, output) cache.save_input_history() + # FIXME This is only writing the final answer to the markdown file. with open(self.console_path, "a+", encoding=Charset.UTF_8.val) as f_console: f_console.write(f"{shared.username_md}{question}\n\n") f_console.write(f"{shared.nickname_md}{output}\n\n") diff --git a/src/main/askai/core/commander/commander.py b/src/main/askai/core/commander/commander.py index 97532543..6ae70986 100644 --- a/src/main/askai/core/commander/commander.py +++ b/src/main/askai/core/commander/commander.py @@ -187,6 +187,15 @@ def help(command: str | None) -> None: display_text(commander_help(command.replace("/", ""))) +@ask_commander.command() +def assistive() -> None: + """Toggle assistive mode ON/OFF.""" + configs.is_assistive = not configs.is_assistive + text_formatter.commander_print( + f"`Assistive responses` is {'%GREEN%ON' if configs.is_assistive else '%RED%OFF'}%NC%" + ) + + @ask_commander.command() def debug() -> None: """Toggle debug mode ON/OFF.""" diff --git a/src/main/askai/core/engine/openai/openai_vision.py b/src/main/askai/core/engine/openai/openai_vision.py index 51dcb75c..2a1498de 100644 --- a/src/main/askai/core/engine/openai/openai_vision.py +++ b/src/main/askai/core/engine/openai/openai_vision.py @@ -57,7 +57,7 @@ def create_image_caption_chain(inputs: dict) -> MessageContent: :param inputs: Dictionary containing the image and prompt information. :return: MessageContent object with the generated caption. """ - model: BaseChatModel = ChatOpenAI(temperature=0.8, model="gpt-4o-mini", max_tokens=1024) + model: BaseChatModel = ChatOpenAI(model="gpt-4o-mini") msg: BaseMessage = model.invoke( [ HumanMessage( @@ -99,7 +99,7 @@ def caption( check_argument(len((final_path := str(find_file(final_path) or ""))) > 0, f"Invalid image path: {final_path}") vision_prompt: str = self._get_vision_prompt(query, image_type) load_image_chain = TransformChain( - input_variables=["image_path", "parser_guides"], output_variables=["image"], transform=self._encode_image + input_variables=["image_path", "parser_guides"], output_variables=["image"], transform_cb=self._encode_image ) out_parser: JsonOutputParser = self._get_out_parser(image_type) vision_chain = load_image_chain | self.create_image_caption_chain | out_parser diff --git a/src/main/askai/core/support/text_formatter.py b/src/main/askai/core/support/text_formatter.py index eb5ed4ed..ec86d9a4 100644 --- a/src/main/askai/core/support/text_formatter.py +++ b/src/main/askai/core/support/text_formatter.py @@ -10,6 +10,13 @@ Copyright (c) 2024, HomeSetup """ +from textwrap import dedent +from typing import Any, AnyStr +import os +import re + +from askai.core.askai_events import events +from askai.core.model.ai_reply import AIReply from hspylib.core.metaclass.singleton import Singleton from hspylib.core.tools.text_tools import ensure_endswith, ensure_startswith, strip_escapes from hspylib.modules.cli.vt100.vt_code import VtCode @@ -17,11 +24,6 @@ from rich.console import Console from rich.markdown import Markdown from rich.text import Text -from textwrap import dedent -from typing import Any, AnyStr - -import os -import re class TextFormatter(metaclass=Singleton): @@ -153,7 +155,10 @@ def commander_print(self, text: AnyStr) -> None: """Display an AskAI-commander formatted text. :param text: The text to be displayed. """ - self.display_markdown(f"%ORANGE% Commander%NC%: {str(text)}") + cmd_message: str = f"%ORANGE% Commander%NC%: {str(text)}" + self.display_markdown(cmd_message) + if os.environ.get("ASKAI_APP") is not None: + events.reply.emit(reply=AIReply.info(VtColor.strip_colors(cmd_message))) assert (text_formatter := TextFormatter().INSTANCE) is not None diff --git a/src/main/askai/tui/app_header.py b/src/main/askai/tui/app_header.py index f1d4b70a..4dc22c02 100644 --- a/src/main/askai/tui/app_header.py +++ b/src/main/askai/tui/app_header.py @@ -128,8 +128,10 @@ class HeaderNotifications(Widget): """Display a notification widget on the right of the header.""" speaking = Reactive(configs.is_speak) + assistive = Reactive(configs.is_assistive) debugging = Reactive(configs.is_debug) caching = Reactive(configs.is_cache) + rag = Reactive(configs.is_rag) listening = Reactive(False) headphones = Reactive(False) idiom = Reactive(f"{configs.language.name} ({configs.language.idiom})") @@ -141,16 +143,18 @@ def __init__(self): def __str__(self): device_info: str = f"{recorder.input_device[1]}" if recorder.input_device else "-" voice: str = shared.engine.configs().tts_voice - return dedent( - f""" + # fmt: off + return dedent(f"""\ + Assistive: {'' if self.assistive else ''} Debugging: {'' if self.debugging else ''} Listening: {'' if self.listening else ''} - Speaking: {'  ' + voice if self.speaking else ''} + Speaking: {'  ' + voice if self.speaking else ''} Caching: {'' if self.caching else ''} + RAG: {'' if self.rag else ''} Audio In: {device_info} - Idiom:  {self.idiom} - """ - ).strip() + Idiom: {AppIcons.GLOBE} {self.idiom} + """).strip() + # fmt: on def _on_mount(self, _: Mount) -> None: self.set_interval(1, callback=self.refresh, name="update clock") @@ -162,6 +166,7 @@ def render(self) -> RenderResult: f"{AppIcons.HEADPHONES if self.headphones else AppIcons.BUILT_IN_SPEAKER} " f"{AppIcons.LISTENING_ON if self.listening else AppIcons.LISTENING_OFF} " f"{AppIcons.SPEAKING_ON if self.speaking else AppIcons.SPEAKING_OFF} " + f"{AppIcons.ASSISTIVE_ON if self.assistive else AppIcons.ASSISTIVE_OFF} " f"{AppIcons.CACHING_ON if self.caching else AppIcons.CACHING_OFF} " f"{AppIcons.DEBUG_ON if self.debugging else AppIcons.DEBUG_OFF} " f"{AppIcons.SEPARATOR_V} {now(f'%a %d %b %X')}", @@ -172,6 +177,7 @@ def render(self) -> RenderResult: def refresh_icons(self) -> None: """Update the application widgets.""" self.headphones = recorder.is_headphones() + self.assistive = configs.is_assistive self.debugging = configs.is_debug self.caching = configs.is_cache self.speaking = configs.is_speak diff --git a/src/main/askai/tui/app_icons.py b/src/main/askai/tui/app_icons.py index 15238a5e..63213902 100644 --- a/src/main/askai/tui/app_icons.py +++ b/src/main/askai/tui/app_icons.py @@ -19,7 +19,7 @@ class AppIcons(Enumeration): """Enumerated icons of the new AskAI UI application.""" - # icons:                  鬒          + # icons:                鬒          DEFAULT = "" STARTED = "" @@ -28,6 +28,7 @@ class AppIcons(Enumeration): EXIT = "" HELP = "" SETTINGS = "" + GLOBE = "" INFO = "" CONSOLE = "" DEBUG_ON = "" @@ -36,6 +37,8 @@ class AppIcons(Enumeration): SPEAKING_OFF = "婢" CACHING_ON = "凌" CACHING_OFF = "稜" + ASSISTIVE_ON = "" + ASSISTIVE_OFF = "" SEPARATOR_V = "" SEPARATOR_H = "" LISTENING_ON = "" diff --git a/src/main/askai/tui/askai_app.py b/src/main/askai/tui/askai_app.py index ae841970..4a863c61 100644 --- a/src/main/askai/tui/askai_app.py +++ b/src/main/askai/tui/askai_app.py @@ -69,6 +69,7 @@ class AskAiApp(App[None]): ("c", "clear", " Clear"), ("d", "debugging", " Debugging"), ("s", "speaking", " Speaking"), + ("a", "assistive", " Assistive"), ("ctrl+l", "ptt", " Push-to-Talk"), ("ctrl+s", "stop", " Stop-GenAi"), ] @@ -209,14 +210,19 @@ def check_action(self, action: str, _) -> Optional[bool]: # All other keys display as normal return True + @work(thread=True) def enable_controls(self, enable: bool = True) -> None: """Enable or disable all UI controls, including the header, input, and footer. :param enable: Whether to enable (True) or disable (False) the UI controls (default is True). """ - self.input_actions.set_class(not enable, "-hidden") - self.header.disabled = not enable - self.line_input.loading = not enable - self.footer.disabled = not enable + + def _invoke_later_(arg: bool = True) -> None: + self.input_actions.set_class(not arg, "-hidden") + self.header.disabled = not arg + self.line_input.loading = not arg + self.footer.disabled = not arg + + self.call_from_thread(_invoke_later_, enable) def activate_markdown(self) -> None: """Activate the markdown console widget.""" @@ -247,6 +253,10 @@ async def action_debugging(self) -> None: """Toggle Debugging ON/OFF.""" self.ask_and_reply("/debug") + async def action_assistive(self) -> None: + """Toggle Assistive ON/OFF.""" + self.ask_and_reply("/assistive") + @work(thread=True) async def action_ptt(self) -> None: """Handle the Push-To-Talk (PTT) action for Speech-To-Text (STT) input. This method allows the user to use @@ -381,9 +391,9 @@ def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]: :param question: The question to ask the AI engine. :return: A tuple containing a boolean indicating success or failure, and the AI's reply as an optional string. """ - self.call_from_thread(self.enable_controls, False) + self.enable_controls(False) status, reply = self.askai.ask_and_reply(question) - self.call_from_thread(self.enable_controls) + self.enable_controls() return status, reply