Skip to content

Commit

Permalink
Improvements and bugfixes. Context and Memory to file cache - part - 1
Browse files Browse the repository at this point in the history
  • Loading branch information
yorevs committed Nov 1, 2024
1 parent 4ee5f26 commit 0400bcd
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 73 deletions.
8 changes: 8 additions & 0 deletions src/main/askai/core/askai_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def is_cache(self) -> bool:
def is_cache(self, value: bool) -> None:
settings.put("askai.cache.enabled", value)

@property
def is_keep_context(self) -> bool:
return settings.get_bool("askai.context.keep.conversation")

@is_keep_context.setter
def is_keep_context(self, value: bool) -> None:
settings.put("askai.context.keep.conversation", value)

@property
def tempo(self) -> int:
return settings.get_int("askai.text.to.speech.tempo")
Expand Down
3 changes: 2 additions & 1 deletion src/main/askai/core/askai_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class AskAiSettings(metaclass=Singleton):
INSTANCE: "AskAiSettings"

# Current settings version. Updating this value will trigger a database recreation using the defaults.
__ACTUAL_VERSION: str = "0.4.0"
__ACTUAL_VERSION: str = "0.4.1"

__RESOURCE_DIR = str(classpath.resource_path)

Expand Down Expand Up @@ -100,6 +100,7 @@ def defaults(self) -> None:
self._settings.put("askai.speak.enabled", "askai", False)
self._settings.put("askai.cache.enabled", "askai", False)
self._settings.put("askai.cache.ttl.minutes", "askai", 25)
self._settings.put("askai.context.keep.conversation", "askai", True)
self._settings.put("askai.preferred.language", "askai", "")
self._settings.put("askai.router.mode.default", "askai", "splitter")
self._settings.put("askai.router.pass.threshold", "askai", "moderate")
Expand Down
61 changes: 52 additions & 9 deletions src/main/askai/core/component/cache_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
from clitt.core.tui.line_input.keyboard_input import KeyboardInput
from hspylib.core.enums.charset import Charset
from hspylib.core.metaclass.singleton import Singleton
from hspylib.core.tools.commons import file_is_not_empty
from hspylib.core.tools.commons import file_is_not_empty, touch_file
from hspylib.core.tools.text_tools import hash_text, ensure_endswith
from hspylib.modules.cache.ttl_cache import TTLCache
from langchain_core.messages import BaseMessage

from askai.core.askai_configs import configs
from askai.core.askai_settings import ASKAI_DIR, CONVERSATION_STARTERS
Expand Down Expand Up @@ -81,6 +82,14 @@
if not file_is_not_empty(str(ASKAI_INPUT_HISTORY_FILE)):
copyfile(str(CONVERSATION_STARTERS), str(ASKAI_INPUT_HISTORY_FILE))

ASKAI_CONTEXT_FILE: Path = Path(CACHE_DIR / "askai-context-history.txt")
if not file_is_not_empty(str(ASKAI_CONTEXT_FILE)):
touch_file(str(ASKAI_CONTEXT_FILE))

ASKAI_MEMORY_FILE: Path = Path(CACHE_DIR / "askai-memory-history.txt")
if not file_is_not_empty(str(ASKAI_MEMORY_FILE)):
touch_file(str(ASKAI_MEMORY_FILE))

CacheEntry = namedtuple("CacheEntry", ["key", "expires"])


Expand Down Expand Up @@ -165,7 +174,12 @@ def read_input_history(self) -> list[str]:
"""Retrieve line input queries from the history file.
:return: A list of input queries stored in the cache.
"""
return ASKAI_INPUT_HISTORY_FILE.read_text().split(os.linesep)
history: str = ASKAI_INPUT_HISTORY_FILE.read_text()
inputs = list(
filter(str.__len__, map(str.strip, history.split(os.linesep)))
)

return inputs

def save_input_history(self, history: list[str] = None) -> None:
"""Save input queries into the history file.
Expand All @@ -177,7 +191,7 @@ def save_input_history(self, history: list[str] = None) -> None:
filter(lambda h: not h.startswith('/'), history)))

def load_input_history(self, predefined: list[str] = None) -> list[str]:
"""Load input queries from the TTL (Time-To-Live) cache extending it with a predefined input history.
"""Load input queries from the history file, extending it with a predefined input history.
:param predefined: A list of predefined input queries to be appended to the final list.
:return: A list of input queries loaded from the cache.
"""
Expand All @@ -186,17 +200,46 @@ def load_input_history(self, predefined: list[str] = None) -> list[str]:
history.extend(list(filter(lambda c: c not in history, predefined)))
return history

def save_context(self, context: list[str]) -> None:
"""Save the context window entries into the TTL (Time-To-Live) cache.
def save_context(self, context: list[str] = None) -> None:
"""Save the context window entries into the context file.
:param context: A list of context entries to be saved.
"""
self._TTL_CACHE.save(self.ASKAI_CONTEXT_KEY, "%EOL%".join(context))
if context := (context or list()):
with open(str(ASKAI_CONTEXT_FILE), 'w', encoding=Charset.UTF_8.val) as f_hist:
list(map(lambda h: f_hist.write(ensure_endswith(os.linesep, h)), context))

def read_context(self) -> list[str]:
"""Read the context window entries from the TTL (Time-To-Live) cache.
"""Read the context window entries from the context file.
:return: A list of context entries retrieved from the cache."""
ctx_str: str = self._TTL_CACHE.read(self.ASKAI_CONTEXT_KEY)
return re.split(r"%EOL%", ctx_str, flags=re.MULTILINE | re.IGNORECASE) if ctx_str else []
flags: int = re.MULTILINE | re.DOTALL | re.IGNORECASE
context: str = ASKAI_CONTEXT_FILE.read_text()
entries = list(
filter(str.__len__, map(str.strip, re.split(r"(human|assistant|system):", context, flags=flags))))

return []

def save_memory(self, memory: list[BaseMessage] = None) -> None:
"""Save the context window entries into the context file.
:param memory: A list of memory entries to be saved.
"""

def _get_role_(msg: BaseMessage) -> str:
return type(msg).__name__.rstrip('Message').replace('AI', 'Assistant').casefold()

if memory := (memory or list()):
with open(str(ASKAI_MEMORY_FILE), 'w', encoding=Charset.UTF_8.val) as f_hist:
list(map(lambda m: f_hist.write(ensure_endswith(os.linesep, f"{_get_role_(m)}: {m.content}")), memory))

def read_memory(self) -> list[BaseMessage]:
"""TODO"""

flags: int = re.MULTILINE | re.DOTALL | re.IGNORECASE
memory: str = ASKAI_MEMORY_FILE.read_text()
memories = list(
filter(str.__len__, map(str.strip, re.split(r"(human|assistant|system):", memory, flags=flags))))

return []



assert (cache := CacheService().INSTANCE) is not None
5 changes: 3 additions & 2 deletions src/main/askai/core/processors/splitter/splitter_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from askai.core.askai_events import events
from askai.core.askai_messages import msg
from askai.core.askai_prompt import prompt
from askai.core.component.cache_service import cache
from askai.core.component.geo_location import geo_location
from askai.core.component.rag_provider import RAGProvider
from askai.core.engine.openai.temperature import Temperature
Expand Down Expand Up @@ -73,8 +74,8 @@ def wrap_answer(question: str, answer: str, model_result: ModelResult = ModelRes
pass # Default is to leave the last AI response as is

# Save the conversation to use with the task agent executor.
if output:
shared.memory.save_context({"input": question}, {"output": output})
cache.save_memory(shared.memory.buffer_as_messages)
shared.context.save()

return output

Expand Down
32 changes: 22 additions & 10 deletions src/main/askai/core/processors/splitter/splitter_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import AnyStr, Optional

from hspylib.core.preconditions import check_state
from hspylib.core.tools.dict_tools import get_or_default
from hspylib.core.tools.validator import Validator
from langchain_core.prompts import PromptTemplate
from transitions import Machine
Expand All @@ -44,18 +45,23 @@ class SplitterPipeline:

FAKE_SLEEP: float = 0.3

def __init__(self, question: AnyStr):
def __init__(self, query: AnyStr):
self._transitions: list[Transition] = [t for t in TRANSITIONS]
self._machine: Machine = Machine(
name=LOGGER_NAME, model=self,
initial=States.STARTUP, states=States, transitions=self._transitions,
auto_transitions=False
)
self._query: str = query
self._previous: States = States.NOT_STARTED
self._result: SplitterResult = SplitterResult(question)
self._result: SplitterResult = SplitterResult(query)
self._iteractions: int = 0
self._failures: dict[str, int] = defaultdict(int)

@property
def query(self) -> str:
return self._query

@property
def previous(self) -> States:
return self._previous
Expand Down Expand Up @@ -84,29 +90,36 @@ def responses(self) -> list[PipelineResponse]:
def question(self) -> str:
return self.result.question

@property
def last_response(self) -> PipelineResponse:
try:
return get_or_default(self.responses, -1)
except IndexError:
return PipelineResponse(self.query)

@property
def last_query(self) -> Optional[str]:
return self.responses[-1].query if len(self.responses) > 0 else None
return self.last_response.query

@last_query.setter
def last_query(self, value: str) -> None:
self.responses[-1].query = value if len(self.responses) > 0 else None
self.last_response.query = value

@property
def last_answer(self) -> Optional[str]:
return self.responses[-1].answer if len(self.responses) > 0 else None
return self.last_response.answer

@last_answer.setter
def last_answer(self, value: str) -> None:
self.responses[-1].answer = value if len(self.responses) > 0 else None
self.last_response.answer = value

@property
def last_accuracy(self) -> Optional[AccResponse]:
return self.responses[-1].accuracy if len(self.responses) > 0 else None
return self.last_response.accuracy

@last_accuracy.setter
def last_accuracy(self, value: AccResponse) -> None:
self.responses[-1].accuracy = value if len(self.responses) > 0 else None
self.last_response.accuracy = value

@property
def plan(self) -> ActionPlan:
Expand Down Expand Up @@ -225,9 +238,8 @@ def st_accuracy_check(self, pass_threshold: AccColor = configs.pass_threshold) -
log.info(f"AI provided a good answer: {self.last_answer}")
if len(self.plan.tasks) > 0:
self.plan.tasks.pop(0)
shared.memory.save_context({"input": self.last_query}, {"output": self.last_answer})
else:
if len(self.responses) > 0:
if self.responses:
self.responses.pop(0)
acc_template = PromptTemplate(input_variables=["problems"], template=issue_report)
if not shared.context.get("EVALUATION"): # Include the guidelines for the first mistake.
Expand Down
16 changes: 0 additions & 16 deletions src/main/askai/core/support/chat_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,6 @@ class ChatContext:

LANGCHAIN_ROLE_MAP: dict = {"human": HumanMessage, "system": SystemMessage, "assistant": AIMessage}

@staticmethod
def of(context: list[str], token_limit: int, max_context_size: int) -> "ChatContext":
"""Create a chat context from a list of context entries formatted as <ROLE: MSG>.
:param context: The initial list of chat context entries.
:param token_limit: The maximum number of tokens allowed by the active engine's model.
:param max_context_size: The maximum allowable size of the context (window size).
:return: A ChatContext instance created from the provided parameters.
"""
ctx = ChatContext(token_limit, max_context_size)
for e in context:
role, reply = list(
filter(None, re.split(r"(human|assistant|system):", e, flags=re.MULTILINE | re.IGNORECASE))
)
ctx.push("HISTORY", reply, role)
return ctx

def __init__(self, token_limit: int, max_context_size: int):
self._store: dict[AnyStr, deque] = defaultdict(partial(deque, maxlen=max_context_size))
self._token_limit: int = token_limit * 1024 # The limit is given in KB
Expand Down
19 changes: 10 additions & 9 deletions src/main/askai/core/support/shared_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from hspylib.modules.application.version import Version
from hspylib.modules.cli.keyboard import Keyboard
from langchain.memory import ConversationBufferWindowMemory
from langchain.memory.chat_memory import BaseChatMemory

from askai.__classpath__ import classpath
from askai.core.askai_configs import configs
Expand Down Expand Up @@ -108,7 +107,7 @@ def idiom(self) -> str:
return self._idiom

@property
def memory(self) -> BaseChatMemory:
def memory(self) -> ConversationBufferWindowMemory:
return self.create_memory()

@property
Expand Down Expand Up @@ -161,21 +160,23 @@ def create_context(self, token_limit: int) -> ChatContext:
:return: An instance of the ChatContext configured with the specified token limit.
"""
if self._context is None:
if configs.is_cache:
self._context = ChatContext.of(cache.read_context(), token_limit, configs.max_short_memory_size)
else:
self._context = ChatContext(token_limit, configs.max_short_memory_size)
self._context = ChatContext(token_limit, configs.max_short_memory_size)
if configs.is_keep_context:
# TODO Add to the context.
ctx = cache.read_context()
return self._context

def create_memory(self, memory_key: str = "chat_history") -> BaseChatMemory:
def create_memory(self, memory_key: str = "chat_history") -> ConversationBufferWindowMemory:
"""Create or retrieve the conversation window memory.
:param memory_key: The key used to identify the memory (default is "chat_history").
:return: An instance of BaseChatMemory associated with the specified memory key.
"""
if self._memory is None:
self._memory = ConversationBufferWindowMemory(
memory_key=memory_key, k=configs.max_short_memory_size, return_messages=True
)
memory_key=memory_key, k=configs.max_short_memory_size, return_messages=True)
if configs.is_keep_context:
# TODO Add to the memory the questions and answers.
mem = cache.read_memory()
return self._memory

def input_text(self, input_prompt: str, placeholder: str | None = None) -> Optional[str]:
Expand Down
52 changes: 26 additions & 26 deletions src/main/askai/resources/conversation-starters.txt
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@

What is the size of the Moon?
Who is the current Prime Minister of the United Kingdom?
Explain the theory of relativity in simple terms.
What are the latest news headlines today?
Summarize the markdown files in my HomeSetup docs folder.
List all the files in my Downloads folder.
Show me all the image files in my Pictures directory.
Open the first PDF document in my Documents folder.
List my music library and check if there's any AC/DC song. If found, show the file name and play it.
Play the latest movie in my Videos folder.
Show me my favorite photos from last year.
Open the first reminder file in my Downloads and tell me what I need to do first.
List my current reminders and mark the completed ones.
Create a small Python program to calculate speed given time and distance, and save it as 'dist.py'.
Check for updates in my Git repositories.
What are the current weather conditions in San Francisco, CA today?
When is the next Los Angeles Lakers match?
Do I have any events scheduled for this weekend?
Tell me who is currently logged into my computer.
Describe the first image in my Downloads folder.
I have downloaded a QR logo, open it for me.
Tell me who you see using the webcam. Respond as if addressing an audience.
Change my desktop wallpaper to the first image in my Pictures folder.
Adjust my system volume to 50%.
Open my calendar and show today's schedule.
Search my Documents folder for any budget spreadsheets.
what is the size of the Moon?
who is the current Prime Minister of the United Kingdom?
explain the theory of relativity in simple terms.
what are the latest news headlines today?
summarize the markdown files in my HomeSetup docs folder.
list all the files in my Downloads folder.
show me all the image files in my Pictures directory.
open the first PDF document in my Documents folder.
list my music library and check if there's any AC/DC song. If found, show the file name and play it.
play the latest movie in my Videos folder.
show me my favorite photos from last year.
open the first reminder file in my Downloads and tell me what I need to do first.
list my current reminders and mark the completed ones.
create a small Python program to calculate speed given time and distance, and save it as 'dist.py'.
check for updates in my Git repositories.
what are the current weather conditions in San Francisco, CA today?
when is the next Los Angeles Lakers match?
do I have any events scheduled for this weekend?
tell me who is currently logged into my computer.
describe the first image in my Downloads folder.
i have downloaded a QR logo, open it for me.
tell me who you see using the webcam. Respond as if addressing an audience.
change my desktop wallpaper to the first image in my Pictures folder.
adjust my system volume to 50%.
open my calendar and show today's schedule.
search my Documents folder for any budget spreadsheets.

0 comments on commit 0400bcd

Please sign in to comment.