diff --git a/src/demo/components/vision_demo.py b/src/demo/components/vision_demo.py index 49a7f83a..b2c6da79 100644 --- a/src/demo/components/vision_demo.py +++ b/src/demo/components/vision_demo.py @@ -1,7 +1,7 @@ -import os - from askai.core.features.router.tools.vision import offline_captioner +import os + if __name__ == "__main__": # init_context("vision-demo") # vision: AIVision = shared.engine.vision() diff --git a/src/main/askai/__classpath__.py b/src/main/askai/__classpath__.py index fc22f085..24538017 100644 --- a/src/main/askai/__classpath__.py +++ b/src/main/askai/__classpath__.py @@ -12,18 +12,17 @@ Copyright (c) 2024, HomeSetup """ +from askai.core.model.api_keys import ApiKeys +from clitt.core.term.commons import is_a_tty +from hspylib.core.metaclass.classpath import Classpath +from hspylib.core.tools.commons import is_debugging, parent_path, root_dir + import logging as log import os +import pydantic import sys import warnings -import pydantic -from clitt.core.term.commons import is_a_tty -from hspylib.core.metaclass.classpath import Classpath -from hspylib.core.tools.commons import parent_path, root_dir, is_debugging - -from askai.core.model.api_keys import ApiKeys - if not is_debugging(): warnings.simplefilter("ignore", category=FutureWarning) warnings.simplefilter("ignore", category=UserWarning) diff --git a/src/main/askai/__main__.py b/src/main/askai/__main__.py index 228084bd..f13c240f 100755 --- a/src/main/askai/__main__.py +++ b/src/main/askai/__main__.py @@ -12,12 +12,12 @@ Copyright (c) 2024, HomeSetup """ -import logging as log -import os -import sys -from textwrap import dedent -from typing import Any, Optional - +from askai.__classpath__ import classpath +from askai.core.askai import AskAi +from askai.core.askai_cli import AskAiCli +from askai.core.askai_configs import configs +from askai.core.support.shared_instances import shared +from askai.tui.askai_app import AskAiApp from clitt.core.tui.tui_application import TUIApplication from hspylib.core.enums.charset import Charset from hspylib.core.tools.commons import to_bool @@ -26,13 +26,12 @@ from hspylib.modules.application.argparse.parser_action import ParserAction from hspylib.modules.application.exit_status import ExitStatus from hspylib.modules.application.version import Version +from textwrap import dedent +from typing import Any, Optional -from askai.__classpath__ import classpath -from askai.core.askai import AskAi -from askai.core.askai_cli import AskAiCli -from askai.core.askai_configs import configs -from askai.core.support.shared_instances import shared -from askai.tui.askai_app import AskAiApp +import logging as log +import os +import sys class Main(TUIApplication): diff --git a/src/main/askai/core/askai.py b/src/main/askai/core/askai.py index 489dfa46..79a56151 100644 --- a/src/main/askai/core/askai.py +++ b/src/main/askai/core/askai.py @@ -22,6 +22,7 @@ from askai.core.engine.ai_engine import AIEngine from askai.core.enums.router_mode import RouterMode from askai.core.features.router.ai_processor import AIProcessor +from askai.core.model.ai_reply import AIReply from askai.core.support.chat_context import ChatContext from askai.core.support.shared_instances import shared from askai.core.support.utilities import read_stdin @@ -37,7 +38,7 @@ from hspylib.modules.eventbus.event import Event from openai import RateLimitError from pathlib import Path -from typing import List, Optional, TypeAlias +from typing import AnyStr, List, Optional, TypeAlias import logging as log import os @@ -155,24 +156,24 @@ def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]: ask_commander(args, standalone_mode=False) elif not (output := cache.read_reply(question)): log.debug('Response not found for "%s" in cache. Querying from %s.', question, self.engine.nickname()) - events.reply.emit(message=msg.wait(), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.wait())) output = processor.process(question, context=read_stdin(), query_prompt=self._query_prompt) - events.reply.emit(message=(output or msg.no_output("processor"))) + events.reply.emit(reply=AIReply.info(output or msg.no_output("processor"))) else: log.debug("Reply found for '%s' in cache.", question) - events.reply.emit(message=output) + events.reply.emit(reply=AIReply.info(output)) shared.context.push("HISTORY", question) shared.context.push("HISTORY", output, "assistant") except (NotImplementedError, ImpossibleQuery) as err: - events.reply_error.emit(message=str(err)) + events.reply.emit(reply=AIReply.error(err)) except (MaxInteractionsReached, InaccurateResponse) as err: - events.reply_error.emit(message=msg.unprocessable(str(err))) + events.reply.emit(reply=AIReply.error(msg.unprocessable(err))) except UsageError as err: - events.reply_error.emit(message=msg.invalid_command(err)) + events.reply.emit(reply=AIReply.error(msg.invalid_command(err))) except IntelligibleAudioError as err: - events.reply_error.emit(message=msg.intelligible(err)) + events.reply.emit(reply=AIReply.error(msg.intelligible(err))) except RateLimitError: - events.reply_error.emit(message=msg.quote_exceeded()) + events.reply.emit(reply=AIReply.error(msg.quote_exceeded())) status = False except TerminatingQuery: status = False @@ -195,13 +196,13 @@ def _create_console_file(self, overwrite: bool = True) -> None: ) f_console.flush() - def _reply(self, message: str) -> None: + def _reply(self, message: AnyStr) -> None: """Reply to the user with the AI-generated response. :param message: The message to send as a reply to the user. """ ... - def _reply_error(self, message: str) -> None: + def _reply_error(self, message: AnyStr) -> None: """Reply to the user with an AI-generated error message or system error. :param message: The error message to be displayed to the user. """ @@ -219,4 +220,4 @@ def _cb_mode_changed_event(self, ev: Event) -> None: f"`{msg.press_esc_enter()}` \n\n" f"> {msg.qna_welcome()}" ) - events.reply.emit(message=sum_msg) + events.reply.emit(reply=AIReply.info(sum_msg)) diff --git a/src/main/askai/core/askai_cli.py b/src/main/askai/core/askai_cli.py index c85d7ab6..fb612077 100644 --- a/src/main/askai/core/askai_cli.py +++ b/src/main/askai/core/askai_cli.py @@ -12,21 +12,6 @@ Copyright (c) 2024, HomeSetup """ -import logging as log -import os -from functools import partial -from pathlib import Path -from threading import Thread -from typing import List, TypeAlias - -import nltk -import pause -from clitt.core.term.cursor import cursor -from clitt.core.term.screen import screen -from clitt.core.tui.line_input.keyboard_input import KeyboardInput -from hspylib.modules.eventbus.event import Event -from rich.progress import Progress - from askai.core.askai import AskAi from askai.core.askai_configs import configs from askai.core.askai_events import * @@ -36,9 +21,23 @@ from askai.core.component.cache_service import cache, CACHE_DIR from askai.core.component.recorder import recorder from askai.core.component.scheduler import scheduler +from askai.core.model.ai_reply import AIReply from askai.core.support.shared_instances import shared from askai.core.support.text_formatter import text_formatter from askai.core.support.utilities import display_text +from clitt.core.term.cursor import cursor +from clitt.core.term.screen import screen +from clitt.core.tui.line_input.keyboard_input import KeyboardInput +from hspylib.modules.eventbus.event import Event +from pathlib import Path +from rich.progress import Progress +from threading import Thread +from typing import AnyStr, List, TypeAlias + +import logging as log +import nltk +import os +import pause QueryString: TypeAlias = str | List[str] | None @@ -88,7 +87,7 @@ def run(self) -> None: self._reply(msg.goodbye()) display_text("", markdown=False) - def _reply(self, message: str) -> None: + def _reply(self, message: AnyStr) -> None: """Reply to the user with the AI-generated response. :param message: The message to send as a reply to the user. """ @@ -101,7 +100,7 @@ def _reply(self, message: str) -> None: else: display_text(text, f"{shared.nickname}") - def _reply_error(self, message: str) -> None: + def _reply_error(self, message: AnyStr) -> None: """Reply to the user with an AI-generated error message or system error. :param message: The error message to be displayed to the user. """ @@ -118,19 +117,19 @@ def _input(self) -> Optional[str]: """ return shared.input_text(f"{shared.username}", f"Message {self.engine.nickname()}") - def _cb_reply_event(self, ev: Event, error: bool = False) -> None: + def _cb_reply_event(self, ev: Event) -> None: """Callback to handle reply events. :param ev: The event object representing the reply event. - :param error: Indicates whether the reply is an error (default is False). """ - if message := ev.args.message: - if error: - self._reply_error(message) + reply: AIReply + if reply := ev.args.reply: + if reply.is_error: + self._reply_error(str(reply)) else: - if ev.args.verbosity.casefold() == "normal" or configs.is_debug: + if ev.args.reply.verbosity <= 1 or configs.is_debug: if ev.args.erase_last: cursor.erase_line() - self._reply(message) + self._reply(str(reply)) def _cb_mic_listening_event(self, ev: Event) -> None: """Callback to handle microphone listening events. @@ -171,7 +170,6 @@ def _startup(self) -> None: # Start and manage the progress bar askai_bus = AskAiEvents.bus(ASKAI_BUS_NAME) askai_bus.subscribe(REPLY_EVENT, self._cb_reply_event) - askai_bus.subscribe(REPLY_ERROR_EVENT, partial(self._cb_reply_event, error=True)) if configs.is_interactive: splash_thread: Thread = Thread(daemon=True, target=self._splash) splash_thread.start() diff --git a/src/main/askai/core/askai_events.py b/src/main/askai/core/askai_events.py index 2f2d4cc7..efb5d7ba 100644 --- a/src/main/askai/core/askai_events.py +++ b/src/main/askai/core/askai_events.py @@ -22,8 +22,6 @@ REPLY_EVENT: str = "askai-reply-event" -REPLY_ERROR_EVENT: str = "askai-reply-error-event" - MIC_LISTENING_EVENT: str = "askai-mic-listening-event" DEVICE_CHANGED_EVENT: str = "askai-input-device-changed-event" @@ -37,8 +35,7 @@ class AskAiEvents(Enumeration): # fmt: off ASKAI_BUS = FluidEventBus( ASKAI_BUS_NAME, - reply=FluidEvent(REPLY_EVENT, verbosity='normal', erase_last=False), - reply_error=FluidEvent(REPLY_ERROR_EVENT), + reply=FluidEvent(REPLY_EVENT, erase_last=False), listening=FluidEvent(MIC_LISTENING_EVENT, listening=True), device_changed=FluidEvent(DEVICE_CHANGED_EVENT, device=None), mode_changed=FluidEvent(MODE_CHANGED_EVENT, mode=None, sum_path=None, glob=None), diff --git a/src/main/askai/core/askai_messages.py b/src/main/askai/core/askai_messages.py index 67464684..b6da9ce7 100644 --- a/src/main/askai/core/askai_messages.py +++ b/src/main/askai/core/askai_messages.py @@ -18,6 +18,7 @@ from askai.language.translators.deepl_translator import DeepLTranslator from functools import cached_property, lru_cache from hspylib.core.metaclass.singleton import Singleton +from typing import AnyStr import re @@ -50,15 +51,15 @@ def translator(self) -> AITranslator: return AskAiMessages.get_translator(Language.EN_US, configs.language) @lru_cache(maxsize=256) - def translate(self, text: str) -> str: + def translate(self, text: AnyStr) -> str: """Translate text using the configured language. :param text: The text to be translated. :return: The translated text. """ # Avoid translating debug messages. - if re.match(r"^~~\[DEBUG]~~.*", text, flags=re.IGNORECASE | re.MULTILINE): + if re.match(r"^~~\[DEBUG]~~.*", str(text), flags=re.IGNORECASE | re.MULTILINE): return text - return self.translator.translate(text) + return self.translator.translate(str(text)) # Informational @@ -179,10 +180,10 @@ def access_grant(self) -> str: def no_query_string(self) -> str: return "No query string was provided in non-interactive mode !" - def invalid_response(self, response_text: str) -> str: + def invalid_response(self, response_text: AnyStr) -> str: return f"Invalid query response/type => '{response_text}' !" - def invalid_command(self, response_text: str) -> str: + def invalid_command(self, response_text: AnyStr) -> str: return f"Invalid **AskAI** command => '{response_text}' !" def cmd_no_exist(self, command: str) -> str: @@ -200,25 +201,25 @@ def missing_package(self, err: ImportError) -> str: def summary_not_possible(self, err: BaseException = None) -> str: return f"Summarization was not possible {'=> ' + str(err) if err else ''}!" - def intelligible(self, reason: str) -> str: + def intelligible(self, reason: AnyStr) -> str: return f"Your speech was not intelligible => '{reason}' !" - def impossible(self, reason: str) -> str: + def impossible(self, reason: AnyStr) -> str: return f"Impossible to fulfill your request => `{reason}` !" - def timeout(self, reason: str) -> str: + def timeout(self, reason: AnyStr) -> str: return f"Time out while {reason} !" - def llm_error(self, error: str) -> str: + def llm_error(self, error: AnyStr) -> str: return f"**LLM** failed to reply: {error} !" - def fail_to_search(self, error: str) -> str: + def fail_to_search(self, error: AnyStr) -> str: return f"'Internet Search' failed: {error} !" - def too_many_actions(self) -> str: + def too_many_actions(self) -> AnyStr: return "Failed to complete the request => 'Max chained actions reached' !" - def unprocessable(self, reason: str) -> str: + def unprocessable(self, reason: AnyStr) -> str: return f"Sorry, {reason}" def quote_exceeded(self) -> str: diff --git a/src/main/askai/core/commander/commander.py b/src/main/askai/core/commander/commander.py index 506d23b4..1b9a9cb5 100644 --- a/src/main/askai/core/commander/commander.py +++ b/src/main/askai/core/commander/commander.py @@ -13,7 +13,7 @@ Copyright (c) 2024, HomeSetup """ from askai.core.askai_configs import configs -from askai.core.askai_events import ASKAI_BUS_NAME, AskAiEvents, REPLY_ERROR_EVENT, REPLY_EVENT +from askai.core.askai_events import ASKAI_BUS_NAME, AskAiEvents, REPLY_EVENT from askai.core.commander.commands.cache_cmd import CacheCmd from askai.core.commander.commands.camera_cmd import CameraCmd from askai.core.commander.commands.general_cmd import GeneralCmd @@ -26,7 +26,6 @@ from askai.language.language import AnyLocale, Language from click import Command, Group from clitt.core.term.cursor import cursor -from functools import partial from hspylib.core.enums.charset import Charset from hspylib.core.tools.commons import sysout, to_bool from hspylib.modules.eventbus.event import Event @@ -154,7 +153,6 @@ def _reply_event(ev: Event, error: bool = False) -> None: shared.create_context(context_size) askai_bus = AskAiEvents.bus(ASKAI_BUS_NAME) askai_bus.subscribe(REPLY_EVENT, _reply_event) - askai_bus.subscribe(REPLY_ERROR_EVENT, partial(_reply_event, error=True)) @click.group() diff --git a/src/main/askai/core/commander/commands/tts_stt_cmd.py b/src/main/askai/core/commander/commands/tts_stt_cmd.py index 9bea6a7f..79c35308 100644 --- a/src/main/askai/core/commander/commands/tts_stt_cmd.py +++ b/src/main/askai/core/commander/commands/tts_stt_cmd.py @@ -12,20 +12,19 @@ Copyright (c) 2024, HomeSetup """ -import os from abc import ABC -from pathlib import Path - -import pause -from clitt.core.tui.mselect.mselect import mselect - from askai.core.askai_configs import configs from askai.core.askai_settings import settings from askai.core.component.audio_player import player -from askai.core.component.recorder import recorder, InputDevice +from askai.core.component.recorder import InputDevice, recorder from askai.core.support.shared_instances import shared from askai.core.support.text_formatter import text_formatter from askai.core.support.utilities import copy_file +from clitt.core.tui.mselect.mselect import mselect +from pathlib import Path + +import os +import pause class TtsSttCmd(ABC): @@ -118,7 +117,8 @@ def _set_device(_device) -> bool: if not name_or_index: device: InputDevice = mselect( - all_devices, f"{'-=' * 40}%EOL%AskAI::Select the Audio Input device%EOL%{'=-' * 40}%EOL%") + all_devices, f"{'-=' * 40}%EOL%AskAI::Select the Audio Input device%EOL%{'=-' * 40}%EOL%" + ) elif name_or_index.isdecimal() and 0 <= int(name_or_index) <= len(all_devices): name_or_index = all_devices[int(name_or_index)][1] device = next((dev for dev in all_devices if dev[1] == name_or_index), None) diff --git a/src/main/askai/core/component/camera.py b/src/main/askai/core/component/camera.py index c941734e..51fa70a3 100644 --- a/src/main/askai/core/component/camera.py +++ b/src/main/askai/core/component/camera.py @@ -20,6 +20,7 @@ from askai.core.component.cache_service import FACE_DIR, IMG_IMPORTS_DIR, PHOTO_DIR from askai.core.component.image_store import ImageData, ImageFile, ImageMetadata, store from askai.core.features.router.tools.vision import image_captioner, parse_caption +from askai.core.model.ai_reply import AIReply from askai.core.model.image_result import ImageResult from askai.core.support.utilities import build_img_path from askai.exception.exceptions import CameraAccessFailure, WebCamInitializationFailure @@ -63,13 +64,13 @@ def _countdown(count: int) -> None: :param count: The number of seconds for the countdown. """ if i := count: - events.reply.emit(message=msg.smile(i)) + events.reply.emit(reply=AIReply.mute(msg.smile(i))) while (i := (i - 1)) >= 0: player.play_sfx("click") pause.seconds(1) - events.reply.emit(message=msg.smile(i), erase_last=True) + events.reply.emit(reply=AIReply.mute(msg.smile(i)), erase_last=True) player.play_sfx("camera-shutter") - events.reply.emit(message=" !!Click!!!", erase_last=True) + events.reply.emit(reply=AIReply.mute(" !!!Click!!!"), erase_last=True) def __init__(self): self._cam = None @@ -109,7 +110,7 @@ def capture( self.initialize() if not self._cam.isOpened(): - events.reply_error.emit(message=msg.camera_not_open()) + events.reply.emit(reply=AIReply.error(msg.camera_not_open())) return None self._countdown(countdown) @@ -130,7 +131,7 @@ def capture( ) if store_image: store.store_image(photo_file) - events.reply.emit(message=msg.photo_captured(photo_file.img_path), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.photo_captured(photo_file.img_path))) return photo_file, photo return None diff --git a/src/main/askai/core/component/internet_service.py b/src/main/askai/core/component/internet_service.py index e47b0845..4c62f697 100644 --- a/src/main/askai/core/component/internet_service.py +++ b/src/main/askai/core/component/internet_service.py @@ -20,6 +20,7 @@ from askai.core.component.geo_location import geo_location from askai.core.component.summarizer import summarizer from askai.core.engine.openai.temperature import Temperature +from askai.core.model.ai_reply import AIReply from askai.core.model.search_result import SearchResult from askai.core.support.langchain_support import lc_llm from askai.core.support.shared_instances import shared @@ -116,12 +117,12 @@ def google_search(self, search: SearchResult) -> str: :param search: The AI search parameters encapsulated in a SearchResult object. :return: A refined string containing the search results. """ - events.reply.emit(message=msg.searching()) + events.reply.emit(reply=AIReply.info(msg.searching())) search.sites = search.sites or ["google.com", "bing.com", "duckduckgo.com", "ask.com"] terms = self._build_google_query(search).strip() try: log.info("Searching Google for '%s'", terms) - events.reply.emit(message=msg.final_query(terms), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.final_query(terms))) ctx = str(self._tool.run(terms)) llm_prompt = ChatPromptTemplate.from_messages([("system", "{query}\n\n{context}")]) context: List[Document] = [Document(ctx)] @@ -139,7 +140,7 @@ def scrap_sites(self, search: SearchResult) -> str: :param search: The AI search parameters encapsulated in a SearchResult object. :return: A string containing the summarized contents of the scraped web page. """ - events.reply.emit(message=msg.scrapping()) + events.reply.emit(reply=AIReply.info(msg.scrapping())) if len(search.sites) > 0: log.info("Scrapping sites: '%s'", str(", ".join(search.sites))) loader = WebBaseLoader( diff --git a/src/main/askai/core/component/recorder.py b/src/main/askai/core/component/recorder.py index 335d0efa..a81e10b8 100644 --- a/src/main/askai/core/component/recorder.py +++ b/src/main/askai/core/component/recorder.py @@ -12,28 +12,28 @@ Copyright (c) 2024, HomeSetup """ -import logging as log -import operator -import sys -from pathlib import Path -from typing import Callable, Optional, TypeAlias - +from askai.core.askai_configs import configs +from askai.core.askai_events import events +from askai.core.askai_messages import msg +from askai.core.component.cache_service import REC_DIR +from askai.core.component.scheduler import Scheduler +from askai.core.model.ai_reply import AIReply +from askai.core.support.utilities import display_text, seconds +from askai.exception.exceptions import InvalidInputDevice, InvalidRecognitionApiError +from askai.language.language import Language from hspylib.core.enums.enumeration import Enumeration from hspylib.core.metaclass.classpath import AnyPath from hspylib.core.metaclass.singleton import Singleton from hspylib.core.preconditions import check_argument, check_state from hspylib.core.zoned_datetime import now_ms from hspylib.modules.application.exit_status import ExitStatus +from pathlib import Path from speech_recognition import AudioData, Microphone, Recognizer, RequestError, UnknownValueError, WaitTimeoutError +from typing import Callable, Optional, TypeAlias -from askai.core.askai_configs import configs -from askai.core.askai_events import events -from askai.core.askai_messages import msg -from askai.core.component.cache_service import REC_DIR -from askai.core.component.scheduler import Scheduler -from askai.core.support.utilities import seconds, display_text -from askai.exception.exceptions import InvalidInputDevice, InvalidRecognitionApiError -from askai.language.language import Language +import logging as log +import operator +import sys InputDevice: TypeAlias = tuple[int, str] @@ -164,12 +164,12 @@ def listen( except WaitTimeoutError as err: err_msg: str = msg.timeout(f"waiting for a speech input => '{err}'") log.warning("Timed out while waiting for a speech input!") - events.reply_error.emit(message=err_msg, erase_last=True) + events.reply.emit(reply=AIReply.error(message=err_msg), erase_last=True) stt_text = None except UnknownValueError as err: err_msg: str = msg.intelligible(err) log.warning("Speech was not intelligible!") - events.reply_error.emit(message=err_msg, erase_last=True) + events.reply.emit(reply=AIReply.error(message=err_msg), erase_last=True) stt_text = None except AttributeError as err: raise InvalidInputDevice(str(err)) from err @@ -195,7 +195,7 @@ def _write_audio_file( f_rec.write(audio.get_wav_data()) log.debug("Voice recorded and saved as %s", audio_path) if api := getattr(self._rec, recognition_api.value): - events.reply.emit(message=msg.transcribing(), verbosity="debug", erase_last=True) + events.reply.emit(reply=AIReply.debug(message=msg.transcribing()), erase_last=True) log.debug("Recognizing voice using %s", recognition_api) assert isinstance(api, Callable) return api(audio, language=language.language) @@ -232,7 +232,7 @@ def _detect_noise(self) -> None: except UnknownValueError as err: err_msg: str = msg.intelligible(f"Unable to detect noise level => '{err}'") log.warning("Timed out while waiting for a speech input!") - events.reply_error.emit(message=err_msg, erase_last=True) + events.reply.emit(reply=AIReply.error(err_msg), erase_last=True) def _select_device(self) -> None: """Select a device for recording.""" diff --git a/src/main/askai/core/component/summarizer.py b/src/main/askai/core/component/summarizer.py index d72ae93d..833b4a23 100644 --- a/src/main/askai/core/component/summarizer.py +++ b/src/main/askai/core/component/summarizer.py @@ -16,6 +16,7 @@ from askai.core.askai_events import events from askai.core.askai_messages import msg from askai.core.component.cache_service import PERSIST_DIR +from askai.core.model.ai_reply import AIReply from askai.core.model.summary_result import SummaryResult from askai.core.support.langchain_support import lc_llm from askai.exception.exceptions import DocumentsNotFound @@ -106,7 +107,7 @@ def generate(self, folder: AnyPath, glob: str) -> bool: """ self._folder: str = str(PathObject.of(folder)) self._glob: str = glob.strip() - events.reply.emit(message=msg.summarizing(self.sum_path)) + events.reply.emit(reply=AIReply.info(msg.summarizing(self.sum_path))) embeddings: Embeddings = lc_llm.create_embeddings() try: @@ -127,7 +128,7 @@ def generate(self, folder: AnyPath, glob: str) -> bool: return True except ImportError as err: log.error("Unable to summarize '%s' => %s", self.sum_path, err) - events.reply_error.emit(message=msg.missing_package(err)) + events.reply.emit(reply=AIReply.error(msg.missing_package(err))) return False diff --git a/src/main/askai/core/engine/ai_engine.py b/src/main/askai/core/engine/ai_engine.py index 38053a7e..0490bf3a 100644 --- a/src/main/askai/core/engine/ai_engine.py +++ b/src/main/askai/core/engine/ai_engine.py @@ -15,13 +15,12 @@ from askai.core.askai_configs import AskAiConfigs from askai.core.engine.ai_model import AIModel from askai.core.engine.ai_vision import AIVision +from askai.core.model.ai_reply import AIReply from langchain_core.embeddings import Embeddings from langchain_core.language_models import BaseChatModel, BaseLLM from pathlib import Path from typing import Optional, Protocol -from askai.core.model.ai_reply import AIReply - class AIEngine(Protocol): """Provide an interface for AI engines.""" diff --git a/src/main/askai/core/engine/openai/openai_engine.py b/src/main/askai/core/engine/openai/openai_engine.py index 765a9447..7ae858e9 100644 --- a/src/main/askai/core/engine/openai/openai_engine.py +++ b/src/main/askai/core/engine/openai/openai_engine.py @@ -21,6 +21,7 @@ from askai.core.engine.openai.openai_configs import OpenAiConfigs from askai.core.engine.openai.openai_model import OpenAIModel from askai.core.engine.openai.openai_vision import OpenAIVision +from askai.core.model.ai_reply import AIReply from hspylib.core.preconditions import check_not_none from langchain_core.embeddings import Embeddings from langchain_core.language_models import BaseChatModel, BaseLLM @@ -35,8 +36,6 @@ import pause import tiktoken -from askai.core.model.ai_reply import AIReply - class OpenAIEngine: """Provide a base class for OpenAI features. This class implements the AIEngine protocol.""" diff --git a/src/main/askai/core/features/router/procs/task_splitter.py b/src/main/askai/core/features/router/procs/task_splitter.py index 11e8870b..5ce56e24 100644 --- a/src/main/askai/core/features/router/procs/task_splitter.py +++ b/src/main/askai/core/features/router/procs/task_splitter.py @@ -12,19 +12,6 @@ Copyright (c) 2024, HomeSetup """ -import logging as log -import os -from pathlib import Path -from textwrap import dedent -from typing import Any, Optional, Type, TypeAlias - -from hspylib.core.exception.exceptions import InvalidArgumentError -from hspylib.core.metaclass.singleton import Singleton -from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, PromptTemplate -from langchain_core.runnables.history import RunnableWithMessageHistory -from pydantic_core import ValidationError -from retry import retry - from askai.core.askai_configs import configs from askai.core.askai_events import events from askai.core.askai_messages import msg @@ -36,11 +23,24 @@ from askai.core.features.router.task_agent import agent from askai.core.features.router.tools.general import final_answer from askai.core.model.action_plan import ActionPlan +from askai.core.model.ai_reply import AIReply from askai.core.model.model_result import ModelResult from askai.core.support.langchain_support import lc_llm from askai.core.support.rag_provider import RAGProvider from askai.core.support.shared_instances import shared from askai.exception.exceptions import InaccurateResponse, InterruptionRequest, TerminatingQuery +from hspylib.core.exception.exceptions import InvalidArgumentError +from hspylib.core.metaclass.singleton import Singleton +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, PromptTemplate +from langchain_core.runnables.history import RunnableWithMessageHistory +from pathlib import Path +from pydantic_core import ValidationError +from retry import retry +from textwrap import dedent +from typing import Any, Optional, Type, TypeAlias + +import logging as log +import os AgentResponse: TypeAlias = dict[str, Any] @@ -74,7 +74,7 @@ def wrap_answer( """ output: str = answer model: RoutingModel = RoutingModel.of_model(model_result.mid) - events.reply.emit(message=msg.model_select(str(model)), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.model_select(model))) args = {"user": shared.username, "idiom": shared.idiom, "context": answer, "question": query} prompt_args: list[str] = [k for k in args.keys()] @@ -90,7 +90,7 @@ def wrap_answer( ctx: str = str(shared.context.flat("HISTORY")) args = {"improvements": rag.reasoning, "context": ctx, "response": answer, "question": query} prompt_args = [k for k in args.keys()] - events.reply.emit(message=msg.refine_answer(answer), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.refine_answer(answer))) output = final_answer("taius-refiner", prompt_args, **args) case _: # Default is to leave the last AI response intact. @@ -155,11 +155,11 @@ def _process_wrapper() -> Optional[str]: log.info("Router::[RESPONSE] Received from AI: \n%s.", str(response.content)) resp_history: list[str] = list() plan: ActionPlan = ActionPlan.create(question, response, model) - events.reply.emit(message=msg.action_plan(str(plan)), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.action_plan(str(plan)))) try: if task_list := plan.tasks: if plan.speak: - events.reply.emit(message=plan.speak) + events.reply.emit(reply=AIReply.info(plan.speak)) for idx, action in enumerate(task_list, start=1): path_str: str | None = ( "Path: " + action.path diff --git a/src/main/askai/core/features/router/task_agent.py b/src/main/askai/core/features/router/task_agent.py index c417023e..a0712516 100644 --- a/src/main/askai/core/features/router/task_agent.py +++ b/src/main/askai/core/features/router/task_agent.py @@ -6,6 +6,7 @@ from askai.core.enums.acc_response import AccResponse from askai.core.features.router.task_toolkit import features from askai.core.features.validation.accuracy import assert_accuracy +from askai.core.model.ai_reply import AIReply from askai.core.support.langchain_support import lc_llm from askai.core.support.shared_instances import shared from functools import lru_cache @@ -54,7 +55,7 @@ def invoke(self, task: str) -> str: :param task: The AI task that outlines the steps to generate the response. :return: The agent's response as a string. """ - events.reply.emit(message=msg.task(task), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.task(task))) if (response := self._exec_task(task)) and (output := response["output"]): log.info("Router::[RESPONSE] Received from AI: \n%s.", output) shared.context.push("HISTORY", task, "assistant") diff --git a/src/main/askai/core/features/router/task_toolkit.py b/src/main/askai/core/features/router/task_toolkit.py index cbdff5e8..84af52cb 100644 --- a/src/main/askai/core/features/router/task_toolkit.py +++ b/src/main/askai/core/features/router/task_toolkit.py @@ -12,8 +12,6 @@ Copyright (c) 2024, HomeSetup """ -from hspylib.core.tools.text_tools import ensure_startswith, ensure_endswith - from askai.core.askai_messages import msg from askai.core.features.router.tools.analysis import query_output from askai.core.features.router.tools.browser import browse @@ -28,6 +26,7 @@ from functools import lru_cache from hspylib.core.metaclass.classpath import AnyPath from hspylib.core.metaclass.singleton import Singleton +from hspylib.core.tools.text_tools import ensure_endswith, ensure_startswith from langchain_core.tools import BaseTool, StructuredTool from textwrap import dedent from typing import Callable, Optional @@ -111,8 +110,10 @@ def image_captioner(self, image_path: str) -> str: :param image_path: The absolute path of the image file to be analyzed. :return: A string containing the generated caption describing the image. """ - return ensure_endswith(ensure_startswith( - parse_caption(image_captioner(image_path)), f"\n>  Description of '{image_path}':\n"), "\n") + return ensure_endswith( + ensure_startswith(parse_caption(image_captioner(image_path)), f"\n>  Description of '{image_path}':\n"), + "\n", + ) def webcam_capturer(self, photo_name: str | None, detect_faces: bool = False) -> str: """Capture a photo using the webcam, and save it locally. This tool is useful for taking photos, detect people's diff --git a/src/main/askai/core/features/router/tools/analysis.py b/src/main/askai/core/features/router/tools/analysis.py index 0b1d99c7..7aefdee4 100644 --- a/src/main/askai/core/features/router/tools/analysis.py +++ b/src/main/askai/core/features/router/tools/analysis.py @@ -16,6 +16,7 @@ from askai.core.askai_messages import msg from askai.core.askai_prompt import prompt from askai.core.engine.openai.temperature import Temperature +from askai.core.model.ai_reply import AIReply from askai.core.support.langchain_support import lc_llm from askai.core.support.shared_instances import shared from askai.core.support.text_formatter import TextFormatter @@ -46,6 +47,6 @@ def query_output(query: str, context: str = None) -> str: log.info("Analysis::[QUERY] '%s' context=%s", query, context) if response := runnable.invoke({"input": query}, config={"configurable": {"session_id": "HISTORY"}}): output = response.content - events.reply.emit(message=msg.analysis(output), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.analysis(output))) return TextFormatter.ensure_ln(output or "Sorry, I don't know.") diff --git a/src/main/askai/core/features/router/tools/terminal.py b/src/main/askai/core/features/router/tools/terminal.py index 0fb690f1..253235b8 100644 --- a/src/main/askai/core/features/router/tools/terminal.py +++ b/src/main/askai/core/features/router/tools/terminal.py @@ -15,6 +15,7 @@ from askai.core.askai_events import events from askai.core.askai_messages import msg from askai.core.features.validation.accuracy import resolve_x_refs +from askai.core.model.ai_reply import AIReply from askai.core.support.shared_instances import shared from askai.core.support.utilities import extract_path, media_type_of from clitt.core.term.terminal import Terminal @@ -114,7 +115,7 @@ def _execute_bash(command_line: str) -> Tuple[bool, str]: if (command := command_line.split(" ")[0].strip()) and which(command): command = expandvars(command_line.replace("~/", f"{os.getenv('HOME')}/").strip()) log.info("Executing command `%s'", command) - events.reply.emit(message=msg.executing(command_line), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.executing(command_line))) output, exit_code = Terminal.INSTANCE.shell_exec(command, shell=True) if exit_code == ExitStatus.SUCCESS: log.info("Command succeeded: \n|-CODE=%s \n|-PATH: %s \n|-CMD: %s ", exit_code, os.getcwd(), command) diff --git a/src/main/askai/core/features/router/tools/vision.py b/src/main/askai/core/features/router/tools/vision.py index db9fed22..e42c1bf6 100644 --- a/src/main/askai/core/features/router/tools/vision.py +++ b/src/main/askai/core/features/router/tools/vision.py @@ -1,20 +1,20 @@ -import os -from textwrap import indent - -import torch -from PIL import Image -from hspylib.core.config.path_object import PathObject -from hspylib.core.enums.enumeration import Enumeration -from hspylib.core.metaclass.classpath import AnyPath -from transformers import BlipForConditionalGeneration, BlipProcessor - from askai.core.askai_events import events from askai.core.askai_messages import msg from askai.core.component.cache_service import PICTURE_DIR from askai.core.engine.ai_vision import AIVision from askai.core.features.validation.accuracy import resolve_x_refs +from askai.core.model.ai_reply import AIReply from askai.core.model.image_result import ImageResult from askai.core.support.shared_instances import shared +from hspylib.core.config.path_object import PathObject +from hspylib.core.enums.enumeration import Enumeration +from hspylib.core.metaclass.classpath import AnyPath +from PIL import Image +from textwrap import indent +from transformers import BlipForConditionalGeneration, BlipProcessor + +import os +import torch class HFModel(Enumeration): @@ -47,7 +47,7 @@ def offline_captioner(path_name: AnyPath) -> str: posix_path: PathObject = x_ref_path if x_ref_path.exists else posix_path if posix_path.exists: - events.reply.emit(message=msg.describe_image(str(posix_path))) + events.reply.emit(reply=AIReply.debug(msg.describe_image(posix_path))) # Use GPU if it's available device = "cuda" if torch.cuda.is_available() else "cpu" image = Image.open(str(posix_path)).convert("RGB") @@ -83,7 +83,7 @@ def image_captioner(path_name: AnyPath, load_dir: AnyPath | None = None) -> str: posix_path: PathObject = x_ref_path if x_ref_path.exists else posix_path if posix_path.exists: - events.reply.emit(message=msg.describe_image(str(posix_path)), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.describe_image(posix_path))) vision: AIVision = shared.engine.vision() image_caption = vision.caption(posix_path.filename, load_dir or posix_path.abs_dir or PICTURE_DIR) diff --git a/src/main/askai/core/features/router/tools/webcam.py b/src/main/askai/core/features/router/tools/webcam.py index 36d644d9..2c2053cb 100644 --- a/src/main/askai/core/features/router/tools/webcam.py +++ b/src/main/askai/core/features/router/tools/webcam.py @@ -1,9 +1,8 @@ -from hspylib.core.tools.text_tools import ensure_startswith, ensure_endswith - from askai.core.askai_configs import configs from askai.core.component.camera import camera from askai.core.features.router.tools.vision import image_captioner, parse_caption from askai.core.support.utilities import display_text +from hspylib.core.tools.text_tools import ensure_endswith, ensure_startswith from os.path import basename from textwrap import indent diff --git a/src/main/askai/core/features/validation/accuracy.py b/src/main/askai/core/features/validation/accuracy.py index 9859ef83..772dc8a3 100644 --- a/src/main/askai/core/features/validation/accuracy.py +++ b/src/main/askai/core/features/validation/accuracy.py @@ -18,6 +18,7 @@ from askai.core.askai_prompt import prompt from askai.core.engine.openai.temperature import Temperature from askai.core.enums.acc_response import AccResponse +from askai.core.model.ai_reply import AIReply from askai.core.support.langchain_support import lc_llm from askai.core.support.rag_provider import RAGProvider from askai.core.support.shared_instances import shared @@ -65,7 +66,7 @@ def assert_accuracy(question: str, ai_response: str, pass_threshold: AccResponse if mat := AccResponse.matches(output): status, details = mat.group(1), mat.group(2) log.info("Accuracy check -> status: '%s' reason: '%s'", status, details) - events.reply.emit(message=msg.assert_acc(status, details), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.assert_acc(status, details))) if (rag_resp := AccResponse.of_status(status, details)).is_interrupt: # AI flags that it can't continue interacting. log.warning(msg.interruption_requested(output)) @@ -101,7 +102,7 @@ def resolve_x_refs(ref_name: str, context: str | None = None) -> str: runnable, shared.context.flat, input_messages_key="pathname", history_messages_key="context" ) log.info("Analysis::[QUERY] '%s' context=%s", ref_name, context) - events.reply.emit(message=msg.x_reference(ref_name), verbosity="debug") + events.reply.emit(reply=AIReply.debug(msg.x_reference(ref_name))) response = runnable.invoke({"pathname": ref_name}, config={"configurable": {"session_id": "HISTORY"}}) if response and (output := response.content) and shared.UNCERTAIN_ID != output: output = response.content diff --git a/src/main/askai/core/model/ai_reply.py b/src/main/askai/core/model/ai_reply.py index 47d39481..3182d4a9 100644 --- a/src/main/askai/core/model/ai_reply.py +++ b/src/main/askai/core/model/ai_reply.py @@ -14,6 +14,9 @@ """ from dataclasses import dataclass +from typing import AnyStr, Literal, TypeAlias + +Verbosity: TypeAlias = Literal[1, 2, 3, 4, 5] @dataclass(frozen=True) @@ -23,14 +26,32 @@ class AIReply: message: str = "" is_success: bool = True is_debug: bool = False - is_verbose: bool = False + verbosity: Verbosity = 1 is_speakable: bool = True def __str__(self) -> str: - return ( - f"Success: {self.is_success}\t" - f"Debug: {self.is_debug}\t" - f"Speakable: {self.is_speakable}\t" - f"Verbose: {self.is_verbose}\t" - f"Message: {self.message}\t" - ) + return self.message + + @staticmethod + def info(message: AnyStr, verbosity: Verbosity = 1, speakable: bool = True) -> "AIReply": + """TODO""" + return AIReply(str(message), True, False, verbosity, speakable) + + @staticmethod + def error(message: AnyStr) -> "AIReply": + """TODO""" + return AIReply(str(message), True, False, 1, False) + + @staticmethod + def debug(message: AnyStr) -> "AIReply": + """TODO""" + return AIReply(str(message), True, True, 1, False) + + @staticmethod + def mute(message: AnyStr, verbosity: Verbosity = 1) -> "AIReply": + """TODO""" + return AIReply(str(message), True, False, verbosity, False) + + @property + def is_error(self) -> bool: + return not self.is_success diff --git a/src/main/askai/tui/askai_app.py b/src/main/askai/tui/askai_app.py index 267c579c..da65a269 100644 --- a/src/main/askai/tui/askai_app.py +++ b/src/main/askai/tui/askai_app.py @@ -25,13 +25,13 @@ from askai.core.component.scheduler import scheduler from askai.core.engine.ai_engine import AIEngine from askai.core.enums.router_mode import RouterMode +from askai.core.model.ai_reply import AIReply from askai.core.support.shared_instances import shared from askai.core.support.text_formatter import text_formatter from askai.tui.app_header import Header from askai.tui.app_icons import AppIcons from askai.tui.app_suggester import InputSuggester from askai.tui.app_widgets import AppHelp, AppInfo, AppSettings, Splash -from functools import partial from hspylib.core.enums.charset import Charset from hspylib.core.tools.commons import file_is_not_empty from hspylib.core.tools.text_tools import ensure_endswith @@ -321,17 +321,17 @@ def display_text(self, markdown_text: str) -> None: """ self._display_buffer.append(msg.translate(markdown_text)) - def _cb_reply_event(self, ev: Event, error: bool = False) -> None: + def _cb_reply_event(self, ev: Event) -> None: """Callback to handle reply events. :param ev: The event object representing the reply event. - :param error: Indicates whether the reply is an error (default is False). """ - if message := ev.args.message: - if error: - self._reply_error(message) + reply: AIReply + if reply := ev.args.reply: + if reply.is_error: + self._reply_error(str(reply)) else: - if ev.args.verbosity.casefold() == "normal" or configs.is_debug: - self._reply(message) + if ev.args.reply.verbosity <= 1 or configs.is_debug: + self._reply(str(reply)) def _cb_mic_listening_event(self, ev: Event) -> None: """Callback to handle microphone listening events. @@ -377,7 +377,6 @@ def _startup(self) -> None: os.chdir(Path.home()) askai_bus = AskAiEvents.bus(ASKAI_BUS_NAME) askai_bus.subscribe(REPLY_EVENT, self._cb_reply_event) - askai_bus.subscribe(REPLY_ERROR_EVENT, partial(self._cb_reply_event, error=True)) nltk.download("averaged_perceptron_tagger", quiet=True, download_dir=CACHE_DIR) recorder.setup() scheduler.start()