From 4dabc614aede60c92224f2133809616032ea1385 Mon Sep 17 00:00:00 2001 From: Hugo Saporetti Junior Date: Tue, 8 Oct 2024 21:01:13 -0300 Subject: [PATCH] Fix the history issue. --- src/main/askai/core/askai.py | 9 ++-- .../askai/core/features/processors/chat.py | 2 - .../core/features/processors/task_splitter.py | 24 +++++----- .../askai/core/features/router/task_agent.py | 3 +- src/main/askai/core/model/acc_response.py | 9 ++-- src/main/askai/core/model/action_plan.py | 22 ++++++--- src/main/askai/core/support/chat_context.py | 16 ++++--- .../resources/prompts/refine-response.txt | 18 +------ src/main/requirements.txt | 47 ------------------- 9 files changed, 49 insertions(+), 101 deletions(-) delete mode 100644 src/main/requirements.txt diff --git a/src/main/askai/core/askai.py b/src/main/askai/core/askai.py index ef8f0993..28c2e7a3 100644 --- a/src/main/askai/core/askai.py +++ b/src/main/askai/core/askai.py @@ -145,17 +145,15 @@ def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]: filter(lambda a: a and a != "None", re.split(r"\s", f"{command.group(1)} {command.group(2)}")) ) ask_commander(args, standalone_mode=False) - elif not (output := cache.read_reply(question)): + return True, None + shared.context.push("HISTORY", question) + if 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(reply=AIReply.detailed(msg.wait())) if output := processor.process(question, context=read_stdin(), query_prompt=self._query_prompt): events.reply.emit(reply=AIReply.info(output)) - shared.context.push("HISTORY", question) - shared.context.push("HISTORY", output, "assistant") else: log.debug("Reply found for '%s' in cache.", question) - shared.context.push("HISTORY", question) - shared.context.push("HISTORY", output, "assistant") events.reply.emit(reply=AIReply.info(output)) except (NotImplementedError, ImpossibleQuery) as err: events.reply.emit(reply=AIReply.error(err)) @@ -173,6 +171,7 @@ def ask_and_reply(self, question: str) -> tuple[bool, Optional[str]]: status = False finally: if output: + shared.context.push("HISTORY", output, "assistant") shared.context.set("LAST_REPLY", output, "assistant") return status, output diff --git a/src/main/askai/core/features/processors/chat.py b/src/main/askai/core/features/processors/chat.py index 3f73723e..51656a78 100644 --- a/src/main/askai/core/features/processors/chat.py +++ b/src/main/askai/core/features/processors/chat.py @@ -78,8 +78,6 @@ def process(self, question: str, **kwargs) -> Optional[str]: if output := runnable.invoke({"input": question}, config={"configurable": {"session_id": history_ctx or ""}}): response = output.content - shared.context.push(history_ctx, question) - shared.context.push(history_ctx, response, "assistant") return response diff --git a/src/main/askai/core/features/processors/task_splitter.py b/src/main/askai/core/features/processors/task_splitter.py index 5a7cf1d8..fae1af1a 100644 --- a/src/main/askai/core/features/processors/task_splitter.py +++ b/src/main/askai/core/features/processors/task_splitter.py @@ -176,9 +176,21 @@ def _splitter_wrapper_(retry_count: int) -> Optional[str]: log.info("Router::[RESPONSE] Received from AI: \n%s.", answer) plan = ActionPlan.create(question, answer, model) if not plan.is_direct and (task_list := plan.tasks): + acc_response: str | None = None events.reply.emit(reply=AIReply.debug(msg.action_plan(str(plan)))) if plan.speak: events.reply.emit(reply=AIReply.info(plan.speak)) + try: + agent_output: str | None = self._process_tasks(task_list, retries) + if len(task_list) > 1: + acc_response: AccResponse = assert_accuracy(question, agent_output, AccColor.MODERATE) + except InterruptionRequest as err: + return str(err) + except self.RETRIABLE_ERRORS: + if retry_count <= 1: + events.reply.emit(reply=AIReply.error(msg.sorry_retry())) + raise + return self.wrap_answer(question, agent_output, plan.model, acc_response) else: # Most of the times, indicates the LLM responded directly. acc_response: AccResponse = assert_accuracy(question, response.content, AccColor.GOOD) @@ -188,18 +200,6 @@ def _splitter_wrapper_(retry_count: int) -> Optional[str]: else: return msg.no_output("Task-Splitter") # Most of the times, indicates a failure. - try: - agent_output: str | None = self._process_tasks(task_list, retries) - acc_response: AccResponse = assert_accuracy(question, agent_output, AccColor.MODERATE) - except InterruptionRequest as err: - return str(err) - except self.RETRIABLE_ERRORS: - if retry_count <= 1: - events.reply.emit(reply=AIReply.error(msg.sorry_retry())) - raise - - return self.wrap_answer(question, agent_output, plan.model, acc_response) - return _splitter_wrapper_(retries) @retry(exceptions=RETRIABLE_ERRORS, tries=configs.max_router_retries, backoff=1, jitter=1) diff --git a/src/main/askai/core/features/router/task_agent.py b/src/main/askai/core/features/router/task_agent.py index b52ce94d..7bedc1a7 100644 --- a/src/main/askai/core/features/router/task_agent.py +++ b/src/main/askai/core/features/router/task_agent.py @@ -60,11 +60,11 @@ def invoke(self, task: str) -> str: shared.context.push("HISTORY", task, "assistant") if (response := self._exec_task(task)) and (output := response["output"]): log.info("Router::[RESPONSE] Received from AI: \n%s.", output) - shared.context.push("HISTORY", output, "assistant") assert_accuracy(task, output, AccColor.MODERATE) shared.memory.save_context({"input": task}, {"output": output}) else: output = msg.no_output("AI") + shared.context.push("HISTORY", output, "assistant") return output @@ -96,7 +96,6 @@ def _exec_task(self, task: AnyStr) -> Optional[Output]: :return: An instance of Output containing the result of the task, or None if the task fails or produces no output. """ - output: str | None = None try: lc_agent: Runnable = self._create_lc_agent() output = lc_agent.invoke({"input": task}) diff --git a/src/main/askai/core/model/acc_response.py b/src/main/askai/core/model/acc_response.py index bc0cf7b7..cc773218 100644 --- a/src/main/askai/core/model/acc_response.py +++ b/src/main/askai/core/model/acc_response.py @@ -12,13 +12,13 @@ Copyright (c) 2024, HomeSetup """ -from askai.core.enums.acc_color import AccColor, AccuracyColors -from askai.core.support.llm_parser import parse_field from dataclasses import dataclass +from pathlib import Path + from hspylib.core.tools.text_tools import ensure_endswith -from os.path import expandvars -import os +from askai.core.enums.acc_color import AccColor, AccuracyColors +from askai.core.support.llm_parser import parse_field @dataclass(frozen=True) @@ -38,6 +38,7 @@ def parse_response(cls, response: str) -> "AccResponse": :param response: The LLM response. :return: An instance of AccResponse created from the parsed response. """ + Path(Path.home() / 'acc-resp.txt').write_text(response) acc_color: AccColor = AccColor.of_color(parse_field("@color", response)) accuracy: float = float(parse_field("@accuracy", response).strip("%")) diff --git a/src/main/askai/core/model/action_plan.py b/src/main/askai/core/model/action_plan.py index 1b439b8b..3c771a9c 100644 --- a/src/main/askai/core/model/action_plan.py +++ b/src/main/askai/core/model/action_plan.py @@ -13,8 +13,8 @@ Copyright (c) 2024, HomeSetup """ from dataclasses import dataclass, field +from pathlib import Path from types import SimpleNamespace -from typing import Any from hspylib.core.preconditions import check_state @@ -60,6 +60,7 @@ def _parse_response(question: str, response: str) -> "ActionPlan": :param response: The router's response. :return: An instance of ActionPlan created from the parsed response. """ + Path(Path.home() / 'splitter-resp.txt').write_text(response) speak: str = parse_field("@speak", response) primary_goal: str = parse_field("@primary_goal", response) @@ -68,12 +69,14 @@ def _parse_response(question: str, response: str) -> "ActionPlan": direct: str = parse_word("direct", response) # fmt: off - if (direct and len(direct) > 1) or len(tasks) == 0: + if direct and len(direct) > 1: plan = ActionPlan._direct_answer(question, direct, primary_goal, ModelResult.default()) elif (direct and len(direct) > 1) or len(tasks) == 1: - plan = ActionPlan._direct_task(question, speak, primary_goal, tasks, ModelResult.default()) + plan = ActionPlan._direct_task(question, speak, primary_goal, tasks[0], ModelResult.default()) elif tasks: plan = ActionPlan(question, speak, primary_goal, False, sub_goals, tasks) + elif not speak and not primary_goal: + plan = ActionPlan._direct_task(question, "", "", response, ModelResult.default()) else: raise InaccurateResponse("AI provided an invalid action plan!") # fmt: on @@ -88,22 +91,27 @@ def _direct_answer(question: str, answer: str, goal: str, model: ModelResult) -> :param model: The result model. :return: An instance of ActionPlan created from the direct response. """ - speak: str = answer.split(',')[0].strip("'\"") + if answer: + speak: str = answer.split(',')[0].strip("'\"") + else: + speak: str = answer return ActionPlan(question, speak, goal, True, [], [], model) @staticmethod - def _direct_task(question: str, speak: str, goal: str, tasks: list[Any], model: ModelResult) -> "ActionPlan": + def _direct_task( + question: str, speak: str, goal: str, task: str | SimpleNamespace, model: ModelResult) -> "ActionPlan": """Create a simple ActionPlan from an AI's direct response in plain text. :param question: The original question that was sent to the AI. :param speak: The spoken response generated by the AI. :param goal: The goal or desired outcome from the task. - :param tasks: A list of tasks related to achieving the goal. + :param task: The task related to achieving the goal. :param model: The result model. :return: An instance of ActionPlan created from the direct response. """ + task_list: list[SimpleNamespace] = [SimpleNamespace(id=1, task=str(task)) if isinstance(task, str) else task] - return ActionPlan(question, speak, goal, False, [], tasks, model) + return ActionPlan(question, speak, goal, False, [], task_list, model) def __str__(self): sub_goals: str = " ".join(f"{i + 1}. {g}" for i, g in enumerate(self.sub_goals)) if self.sub_goals else "N/A" diff --git a/src/main/askai/core/support/chat_context.py b/src/main/askai/core/support/chat_context.py index 8b951134..1ca9d166 100644 --- a/src/main/askai/core/support/chat_context.py +++ b/src/main/askai/core/support/chat_context.py @@ -12,16 +12,19 @@ Copyright (c) 2024, HomeSetup """ -from askai.core.component.cache_service import cache -from askai.exception.exceptions import TokenLengthExceeded + +import os +import re from collections import defaultdict, deque, namedtuple from functools import partial, reduce +from typing import Any, AnyStr, Literal, Optional, TypeAlias, get_args + +from hspylib.core.preconditions import check_argument from langchain_community.chat_message_histories.in_memory import ChatMessageHistory from langchain_core.messages import AIMessage, HumanMessage, SystemMessage -from typing import Any, AnyStr, Literal, Optional, TypeAlias -import os -import re +from askai.core.component.cache_service import cache +from askai.exception.exceptions import TokenLengthExceeded ChatRoles: TypeAlias = Literal["system", "human", "assistant"] @@ -90,9 +93,10 @@ def push(self, key: str, content: Any, role: ChatRoles = "human") -> ContextRaw: :param role: The role associated with the message (default is "human"). :return: The updated chat context. """ + check_argument(role in get_args(ChatRoles), f"Invalid ChatRole: '{role}'") if (token_length := (self.length(key)) + len(content)) > self._token_limit: raise TokenLengthExceeded(f"Required token length={token_length} limit={self._token_limit}") - if (entry := ContextEntry(role, content)) not in (ctx := self._store[key]): + if (entry := ContextEntry(role, content.strip())) not in (ctx := self._store[key]): ctx.append(entry) return self.get(key) diff --git a/src/main/askai/resources/prompts/refine-response.txt b/src/main/askai/resources/prompts/refine-response.txt index ef83d9cd..27e424a7 100644 --- a/src/main/askai/resources/prompts/refine-response.txt +++ b/src/main/askai/resources/prompts/refine-response.txt @@ -19,23 +19,11 @@ Act as a text editor and formatter. Refine the AI response to ensure they are cl - Highlight these details using appropriate Markdown formatting (e.g., `code` for file paths and names). - The user's name is "{user}". Address him by his name in responses. -4. **Use Provided Tips:** - - If available, integrate the provided tips to enhance the final user response: - ---- -{improvements} ---- - -5. **Leave it Untouched**: +4. **Leave it Untouched**: - If no improvements are possible, return the response as is without any extraneous explanation or comments. -6. **Watermark**: - - Add your watermark at the end of the response (do not include the triple quotes): -""" ---- -> Improved by the Refiner Model -""" +{improvements} Human Question: "{question}" @@ -43,9 +31,7 @@ Human Question: "{question}" AI Response: -""" {context} -""" Begin refining the response! diff --git a/src/main/requirements.txt b/src/main/requirements.txt deleted file mode 100644 index d6966e93..00000000 --- a/src/main/requirements.txt +++ /dev/null @@ -1,47 +0,0 @@ -###### AUTO-GENERATED Requirements file for: AskAI ###### - -hspylib>=1.12.50 -hspylib-clitt>=0.9.138 -hspylib-setman>=0.10.41 -retry2>=0.9.5 -pause>=0.3 -tqdm>=4.66.5 -pyperclip>=1.9.0 -python-magic>=0.4.27 -pytz>=2024.1 -langchain>=0.3.0 -langchain-openai>=0.2.0 -langchain-community>=0.3.1 -langchain-google-community>=2.0.0 -openai-whisper>=20231117 -openai>=1.48.0 -google-api-python-client>=2.147.0 -fake_useragent>=1.5.1 -requests>=2.32.3 -urllib3>=1.26.20 -protobuf>=4.25.4 -aiohttp>=3.10.5 -html2text>=2024.2.26 -rich>=13.8.1 -textual>=0.80.1 -soundfile>=0.12.1 -PyAudio>=0.2.14 -SpeechRecognition>=3.10.4 -opencv-python>=4.10.0.84 -pyautogui>=0.9.54 -torch>=2.2.0 -torchvision>=0.17.2 -open-clip-torch -opentelemetry-api>=1.27.0 -opentelemetry-sdk>=1.27.0 -opentelemetry-proto>=1.27.0 -transformers>=4.44.2 -unstructured>=0.15.8 -unstructured[md]>=0.15.8 -tiktoken>=0.7.0 -stanza==1.1.1 -nltk>=3.9.1 -faiss-cpu~=1.8.0 -chromadb>=0.5.5 -deepl>=1.18.0 -argostranslate==1.9.1