diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/api/__init__.py b/examples/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/api/chat_example.py b/examples/api/chat_example.py new file mode 100644 index 00000000..b7781ef5 --- /dev/null +++ b/examples/api/chat_example.py @@ -0,0 +1,66 @@ +import sys + +sys.path.append(".") + +from memoryscope import MemoryScope, Arguments + +arguments = Arguments( + language="cn", + logger_to_screen=False, + memory_chat_class="api_memory_chat", + generation_backend="dashscope_generation", + generation_model="qwen-max", + embedding_backend="dashscope_embedding", + embedding_model="text-embedding-v2", + use_dummy_ranker=False, + rank_backend="dashscope_rank", + rank_model="gte-rerank", +) + + +def chat_example1(): + with MemoryScope(config_path="memoryscope/core/config/demo_config.yaml") as ms: + memory_chat = ms.default_memory_chat + response = memory_chat.chat_with_memory(query="我的爱好是弹琴。") + print("回答:\n" + response.message.content) + print("记忆:\n" + response.meta_data["memories"]) + + +def chat_example2(): + with MemoryScope(arguments=arguments) as ms: + memory_chat = ms.default_memory_chat + + response = memory_chat.chat_with_memory(query="我的爱好是弹琴。") + print("回答:\n" + response.message.content) + print("记忆:\n" + response.meta_data["memories"]) + + +def chat_example3(): + with MemoryScope(arguments=arguments) as ms: + memory_chat = ms.default_memory_chat + + response = memory_chat.chat_with_memory( + query="你知道我消灭了几个星系吗?", + extra_memories="用户是一只哥斯拉,这是真是的,不是虚构的。\n前天用户消灭了星系0x4be?\n大前天用户消灭了星系0x5be?") + print("回答:\n" + response.message.content) + + +def chat_example4(): + with MemoryScope(arguments=arguments) as ms: + memory_chat = ms.default_memory_chat + + response = memory_chat.chat_with_memory(query="我的爱好是弹琴。") + print("回答1:\n" + response.message.content) + memory_chat.memory_service.consolidate_memory() + + response = memory_chat.chat_with_memory(query="你知道我的乐器爱好是什么?", + add_messages=False) + print("回答2:\n" + response.message.content) + print("记忆2:\n" + response.meta_data["memories"]) + + +if __name__ == "__main__": + chat_example1() + # chat_example2() + # chat_example3() + # chat_example4() diff --git a/examples/api/test_interface.py b/examples/api/test_interface.py deleted file mode 100644 index 05bdaef5..00000000 --- a/examples/api/test_interface.py +++ /dev/null @@ -1,18 +0,0 @@ -from memoryscope.cli import MemoryScope -from memoryscope.scheme.message import Message - -ms = MemoryScope().read_config("config/demo_config_no_stream.yaml") -memory_service = ms.default_service -memory_chat = ms.default_chat_handle - -# new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name="我", content="我的爱好是弹琴并且喜欢看电影。") -# memory_service.add_messages(new_message) - -res: Message = memory_chat.chat_with_memory(query="我的爱好是弹琴。", remember_response=True) -print(res.message.content) - -res: Message = memory_chat.chat_with_memory(query="昨天弹出一个光粒,消灭了星系0x4be。", remember_response=True) -print(res.message.content) - -res: Message = memory_chat.chat_with_memory(query="今天弹出一个二向箔,消灭了星系0xa2e。", remember_response=True) -print(res.message.content) diff --git a/examples/cli/dash_cli_cn1.sh b/examples/cli/dash_cli_cn1.sh new file mode 100644 index 00000000..2998bd29 --- /dev/null +++ b/examples/cli/dash_cli_cn1.sh @@ -0,0 +1 @@ +python memoryscope/cli.py -config_path=memoryscope/core/config/demo_config.yaml \ No newline at end of file diff --git a/examples/cli/dash_cli_cn2.sh b/examples/cli/dash_cli_cn2.sh new file mode 100644 index 00000000..14c92cee --- /dev/null +++ b/examples/cli/dash_cli_cn2.sh @@ -0,0 +1,10 @@ +python memoryscope/cli.py \ + -language="cn" \ + -memory_chat_class="cli_memory_chat" \ + -generation_backend="dashscope_generation" \ + -generation_model="qwen-max" \ + -embedding_backend="dashscope_embedding" \ + -embedding_model="text-embedding-v2" \ + -use_dummy_ranker=False \ + -rank_backend="dashscope_rank" \ + -rank_model="gte-rerank" \ No newline at end of file diff --git a/examples/config/demo_config_no_stream.yaml b/examples/config/demo_config_no_stream.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/docker/__init__.py b/examples/docker/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/memoryscope/__init__.py b/memoryscope/__init__.py index 2be4eb6a..0dc03c13 100644 --- a/memoryscope/__init__.py +++ b/memoryscope/__init__.py @@ -1,2 +1,5 @@ +from memoryscope.core.config.arguments import Arguments +from memoryscope.core.memoryscope import MemoryScope + """ Version of MemoryScope.""" __version__ = "0.1.0" diff --git a/memoryscope/cli.py b/memoryscope/cli.py index 85d5bec5..a429fda0 100644 --- a/memoryscope/cli.py +++ b/memoryscope/cli.py @@ -8,7 +8,9 @@ from memoryscope.core.memoryscope import MemoryScope def cli_job(**kwargs): - MemoryScope(**kwargs).default_memory_chat.run() + with MemoryScope(**kwargs) as ms: + memory_chat = ms.default_memory_chat + memory_chat.run() if __name__ == "__main__": diff --git a/memoryscope/core/chat/api_memory_chat.py b/memoryscope/core/chat/api_memory_chat.py index 6aee7c75..d1e8d491 100644 --- a/memoryscope/core/chat/api_memory_chat.py +++ b/memoryscope/core/chat/api_memory_chat.py @@ -9,7 +9,7 @@ from memoryscope.core.service.base_memory_service import BaseMemoryService from memoryscope.core.utils.prompt_handler import PromptHandler from memoryscope.enumeration.message_role_enum import MessageRoleEnum from memoryscope.scheme.message import Message -from memoryscope.scheme.model_response import ModelResponse +from memoryscope.scheme.model_response import ModelResponse, ModelResponseGen class ApiMemoryChat(BaseMemoryChat): @@ -18,6 +18,7 @@ class ApiMemoryChat(BaseMemoryChat): memory_service: str, generation_model: str, context: MemoryscopeContext, + stream: bool = False, human_name: str = None, assistant_name: str = None, **kwargs): @@ -27,6 +28,7 @@ class ApiMemoryChat(BaseMemoryChat): self._memory_service: BaseMemoryService | str = memory_service self._generation_model: BaseModel | str = generation_model self.context: MemoryscopeContext = context + self.stream: bool = stream self.generation_model_kwargs: dict = kwargs.pop("generation_model_kwargs", {}) self.human_name: str = human_name @@ -100,13 +102,31 @@ class ApiMemoryChat(BaseMemoryChat): self._generation_model = self.context.model_dict[self._generation_model] return self._generation_model + def iter_response(self, + remember_response: bool, + resp: ModelResponseGen, + memories: str, + query_message: Message) -> ModelResponseGen: + + model_response: ModelResponse | None = None + for model_response in resp: + yield model_response + + if remember_response: + if model_response and model_response.message: + model_response.message.role_name = self.assistant_name + model_response.meta_data[MEMORIES] = memories + self.memory_service.add_messages([query_message, model_response.message]) + else: + self.logger.warning("model_response or model_response.message is empty!") + def chat_with_memory(self, query: str, role_name: Optional[str] = None, system_prompt: Optional[str] = None, memory_prompt: Optional[str] = None, extra_memories: Optional[str] = None, - add_not_memorized_messages: bool = True, + add_messages: bool = True, remember_response: bool = True, **kwargs): """ @@ -118,7 +138,7 @@ class ApiMemoryChat(BaseMemoryChat): system_prompt (str, optional): System prompt. Defaults to the system_prompt in "memory_chat_prompt.yaml". memory_prompt (str, optional): Memory prompt. Defaults to the memory_prompt in "memory_chat_prompt.yaml". extra_memories (str, optional): Manually added user memory in this function. - add_not_memorized_messages (bool, optional): whether add not memorized messages to LLM. + add_messages (bool, optional): whether add not memorized messages to LLM. remember_response (bool, optional): Flag indicating whether to save the AI's response to memory. Defaults to False. Returns: @@ -161,7 +181,7 @@ class ApiMemoryChat(BaseMemoryChat): chat_messages.append(system_message) # Include past conversation history in the message list - if add_not_memorized_messages: + if add_messages: history_messages = self.memory_service.read_message() if history_messages: chat_messages.extend(history_messages) @@ -171,19 +191,8 @@ class ApiMemoryChat(BaseMemoryChat): self.logger.info(f"chat_messages={chat_messages}") resp = self.generation_model.call(messages=chat_messages, stream=self.stream, **self.generation_model_kwargs) - if self.stream: - model_response: ModelResponse | None = None - for model_response in resp: - yield model_response - - if remember_response: - if model_response and model_response.message: - model_response.message.role_name = self.assistant_name - model_response.meta_data[MEMORIES] = memories - self.memory_service.add_messages([query_message, model_response.message]) - else: - self.logger.warning("model_response or model_response.message is empty!") + return self.iter_response(remember_response, resp, memories, query_message) else: model_response: ModelResponse = resp diff --git a/memoryscope/core/chat/base_memory_chat.py b/memoryscope/core/chat/base_memory_chat.py index 995d1d46..455c93e4 100644 --- a/memoryscope/core/chat/base_memory_chat.py +++ b/memoryscope/core/chat/base_memory_chat.py @@ -1,4 +1,5 @@ from abc import ABCMeta, abstractmethod +from typing import Optional from memoryscope.core.service.base_memory_service import BaseMemoryService from memoryscope.core.utils.logger import Logger @@ -10,8 +11,7 @@ class BaseMemoryChat(metaclass=ABCMeta): It outlines the method to initiate a chat session leveraging memory data, which concrete subclasses must implement. """ - def __init__(self, stream: bool = True, **kwargs): - self.stream: bool = stream + def __init__(self, **kwargs): self.kwargs: dict = kwargs self.logger = Logger.get_logger() @@ -28,16 +28,14 @@ class BaseMemoryChat(metaclass=ABCMeta): @abstractmethod def chat_with_memory(self, query: str, - role_name: str = "", - remember_response: bool = True): - """ - Initiates a chat interaction using the memory service, with the provided query as input. + role_name: Optional[str] = None, + system_prompt: Optional[str] = None, + memory_prompt: Optional[str] = None, + extra_memories: Optional[str] = None, + add_messages: bool = True, + remember_response: bool = True, + **kwargs): - Args: - query (str): The user's query or message to start the chat. - role_name (str): The role's name. - remember_response (bool): whether update memory service. - """ raise NotImplementedError def run(self): diff --git a/memoryscope/core/chat/cli_memory_chat.py b/memoryscope/core/chat/cli_memory_chat.py index 653b92ec..3d610468 100644 --- a/memoryscope/core/chat/cli_memory_chat.py +++ b/memoryscope/core/chat/cli_memory_chat.py @@ -1,22 +1,14 @@ import os import time -from typing import List +from typing import Optional import questionary -from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME -from memoryscope.core.chat.base_memory_chat import BaseMemoryChat -from memoryscope.core.memoryscope_context import MemoryscopeContext -from memoryscope.core.models.base_model import BaseModel -from memoryscope.core.service.base_memory_service import BaseMemoryService -from memoryscope.core.utils.prompt_handler import PromptHandler +from memoryscope.core.chat.api_memory_chat import ApiMemoryChat from memoryscope.core.utils.tool_functions import char_logo -from memoryscope.enumeration.message_role_enum import MessageRoleEnum -from memoryscope.scheme.message import Message -from memoryscope.scheme.model_response import ModelResponse -class CliMemoryChat(BaseMemoryChat): +class CliMemoryChat(ApiMemoryChat): """ Command-line interface for chatting with an AI that integrates memory functionality. Allows users to interact, manage chat history, adjust streaming settings, and view commands' help. @@ -28,51 +20,9 @@ class CliMemoryChat(BaseMemoryChat): "stream": "Toggle between getting streamed responses from the model." } - def __init__(self, - memory_service: str, - generation_model: str, - context: MemoryscopeContext, - human_name: str = None, - assistant_name: str = None, - **kwargs): - + def __init__(self, **kwargs): super().__init__(**kwargs) - - self._memory_service: BaseMemoryService | str = memory_service - self._generation_model: BaseModel | str = generation_model - self.context: MemoryscopeContext = context - self.generation_model_kwargs: dict = kwargs.pop("generation_model_kwargs", {}) - - self.human_name: str = human_name - if not self.human_name: - self.human_name = DEFAULT_HUMAN_NAME[self.context.language] - self.context.meta_data["human_name"] = self.human_name - - self.assistant_name: str = assistant_name - if not self.assistant_name: - self.assistant_name = "AI" - self.context.meta_data["assistant_name"] = self.assistant_name - self._logo = char_logo("MemoryScope") - self._prompt_handler: PromptHandler | None = None - - @property - def prompt_handler(self) -> PromptHandler: - """ - Lazy initialization property for the prompt handler. - - This property ensures that the `_prompt_handler` attribute is only instantiated when it is first accessed. - It uses the current file's path and additional keyword arguments for configuration. - - Returns: - PromptHandler: An instance of the PromptHandler configured for this CLI session. - """ - if self._prompt_handler is None: - self._prompt_handler = PromptHandler(__file__, - language=self.context.language, - prompt_file="memory_chat_prompt", - **self.kwargs) - return self._prompt_handler def print_logo(self): """ @@ -84,104 +34,30 @@ class CliMemoryChat(BaseMemoryChat): for line in self._logo: print(line) - @property - def memory_service(self) -> BaseMemoryService: - """ - Property to access the memory service. If the service is initially set as a string, - it will be looked up in the memory service dictionary of context, initialized, - and then returned as an instance of `BaseMemoryService`. Ensures the memory service - is properly started before use. - - Returns: - BaseMemoryService: An active memory service instance. - - Raises: - ValueError: If the declaration of memory service is not found in the memory service dictionary of context. - """ - if isinstance(self._memory_service, str): - if self._memory_service not in self.context.memory_service_dict: - raise ValueError(f"Missing declaration of memory_service in context: {self._memory_service}") - - self._memory_service: BaseMemoryService = self.context.memory_service_dict[self._memory_service] - # init service & update kwargs - self._memory_service.init_service() - return self._memory_service - - @property - def generation_model(self) -> BaseModel: - """ - Property to get the generation model. If the model is set as a string, it will be resolved from the global - context's model dictionary. - - Raises: - ValueError: If the declaration of generation model is not found in the model dictionary of context . - - Returns: - BaseModel: An actual generation model instance. - """ - if isinstance(self._generation_model, str): - if self._generation_model not in self.context.model_dict: - raise ValueError(f"Missing declaration of generation model in yaml config: {self._generation_model}") - self._generation_model = self.context.model_dict[self._generation_model] - return self._generation_model - - def get_user_message(self, query: str, role_name: str = "") -> Message: - if not role_name: - role_name = self.human_name - return Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query) - - def get_system_message_with_memory(self, memories: str) -> Message: - # Incorporate memory into the system prompt if available - system_prompt = self.prompt_handler.system_prompt - if memories: - memory_prompt = self.prompt_handler.memory_prompt - system_prompt = "\n".join([x.strip() for x in [system_prompt, memory_prompt, memories]]) - return Message(role=MessageRoleEnum.SYSTEM, content=system_prompt) - def chat_with_memory(self, query: str, - role_name: str = "", - remember_response: bool = True): - - chat_messages: List[Message] = [] - - new_message: Message = self.get_user_message(query=query, role_name=role_name) - - # To retrieve memory, prepare the query timestamp and role name by adding new_message. - memories: str = self.memory_service.retrieve_memory(query=new_message.content, - role_name=new_message.role_name, - timestamp=new_message.time_created) - - # format system_message with memories - system_message: Message = self.get_system_message_with_memory(memories=memories) - chat_messages.append(system_message) - - # Include past conversation history in the message list - history_messages = self.memory_service.read_message() - if history_messages: - chat_messages.extend(history_messages) - - # Append the current user's message to the conversation context - chat_messages.append(new_message) - self.logger.info(f"chat_messages={chat_messages}") - - # Invoke the Language Model with the constructed message context, respecting streaming setting - resp = self.generation_model.call(messages=chat_messages, - stream=self.stream, - **self.generation_model_kwargs) + role_name: Optional[str] = None, + system_prompt: Optional[str] = None, + memory_prompt: Optional[str] = None, + extra_memories: Optional[str] = None, + add_messages: bool = True, + remember_response: bool = True, + **kwargs): + resp = super().chat_with_memory(query=query, + role_name=role_name, + system_prompt=system_prompt, + memory_prompt=memory_prompt, + extra_memories=extra_memories, + add_messages=add_messages, + remember_response=remember_response, + **kwargs) if self.stream: - model_response: ModelResponse | None = None - for model_response in resp: - questionary.print(model_response.delta, end="") + for _resp in resp: + questionary.print(_resp.delta, end="") questionary.print("") else: - model_response: ModelResponse = resp - questionary.print(model_response.message.content) - - if remember_response and model_response and model_response.message: - model_response.message.role_name = self.assistant_name - self.memory_service.add_messages([new_message, model_response.message]) + questionary.print(resp.message.content) @staticmethod def parse_query_command(query: str): diff --git a/memoryscope/core/config/arguments.py b/memoryscope/core/config/arguments.py index f6542d3e..44ed574f 100644 --- a/memoryscope/core/config/arguments.py +++ b/memoryscope/core/config/arguments.py @@ -12,6 +12,8 @@ class Arguments(object): logger_name_time_suffix: str = field(default="%Y%m%d_%H%M%S") + logger_to_screen: bool = field(default=False, metadata={"help": "If false, it does not print to the screen."}) + memory_chat_class: str = field(default="cli_memory_chat", metadata={ "help": "cli_memory_chat(Command-line interaction), api_memory_chat(API interface interaction), etc."}) diff --git a/memoryscope/core/config/config_manager.py b/memoryscope/core/config/config_manager.py index b23eed5c..e30d51e3 100644 --- a/memoryscope/core/config/config_manager.py +++ b/memoryscope/core/config/config_manager.py @@ -1,5 +1,6 @@ import json from dataclasses import fields +from datetime import datetime from pathlib import Path from typing import Optional, Literal @@ -7,6 +8,7 @@ import yaml from memoryscope.constants.language_constants import DEFAULT_HUMAN_NAME from memoryscope.core.config.arguments import Arguments +from memoryscope.core.utils.logger import Logger from memoryscope.enumeration.language_enum import LanguageEnum @@ -23,20 +25,40 @@ class ConfigManager(object): if config: self.config = config + self.logger = self._init_logger() + self.logger.info("init by config mode:") elif config_path: self.read_config(config_path) + self.logger = self._init_logger() + self.logger.info("init by config_path mode:") else: self.read_demo_config(demo_config_name) + if arguments: + self.update_config_by_arguments(arguments) + self.logger = self._init_logger() + self.logger.info(f"init by arguments mode: {arguments.__dict__}") - if arguments: - self.update_config_by_arguments(arguments) + elif kwargs: + kwargs = {k: v for k, v in kwargs.items() if k in [x.name for x in fields(Arguments)]} + arguments = Arguments(**kwargs) + self.update_config_by_arguments(arguments) + self.logger = self._init_logger() + self.logger.info(f"init by kwargs mode: {kwargs}") - elif kwargs: - key_list = [x.name for x in fields(Arguments)] - arguments = Arguments(**{k: v for k, v in kwargs.items() if k in key_list}) - self.update_config_by_arguments(arguments) + else: + raise RuntimeError("can not init config manager without kwargs!") + self.logger.info(self.dump_config()) + + def _init_logger(self) -> Logger: + global_config = self.config["global"] + logger_name = global_config["logger_name"] + logger_name_time_suffix = global_config["logger_name_time_suffix"] + if logger_name_time_suffix: + suffix = datetime.now().strftime(logger_name_time_suffix) + logger_name = f"{logger_name}_{suffix}" + return Logger.get_logger(logger_name, to_stream=global_config["logger_to_screen"]) def read_config(self, config_path: str): if config_path.endswith(".yaml"): @@ -60,16 +82,19 @@ class ConfigManager(object): "thread_pool_max_workers": arguments.thread_pool_max_workers, "logger_name": arguments.logger_name, "logger_name_time_suffix": arguments.logger_name_time_suffix, + "logger_to_screen": arguments.logger_to_screen, "use_dummy_ranker": arguments.use_dummy_ranker, }) @staticmethod def update_memory_chat_by_arguments(config: dict, arguments: Arguments): memory_chat_class_split = config["class"].split(".") + stream = arguments.memory_chat_class in ["cli_memory_chat", ] config.update({ "class": ".".join(memory_chat_class_split[:-1] + [arguments.memory_chat_class]), "human_name": DEFAULT_HUMAN_NAME[LanguageEnum(arguments.language)], "assistant_name": "AI", + "stream": stream, }) @staticmethod diff --git a/memoryscope/core/config/demo_config.yaml b/memoryscope/core/config/demo_config.yaml index 88680e7a..60db4b34 100644 --- a/memoryscope/core/config/demo_config.yaml +++ b/memoryscope/core/config/demo_config.yaml @@ -3,6 +3,7 @@ global: thread_pool_max_workers: 5 logger_name: memoryscope logger_name_time_suffix: "%Y%m%d_%H%M%S" + logger_to_screen: false use_dummy_ranker: false memory_chat: @@ -86,7 +87,7 @@ worker: obs_customized: 1.2 insight: 2.0 fuse_time_ratio: 2.0 - fuse_rerank_top_k: 10 + fuse_rerank_top_k: 20 retrieve_top_memory: class: core.worker.frontend.retrieve_memory_worker retrieve_obs_top_k: 100 diff --git a/memoryscope/core/memoryscope.py b/memoryscope/core/memoryscope.py index 54b429b9..1f1ece7b 100644 --- a/memoryscope/core/memoryscope.py +++ b/memoryscope/core/memoryscope.py @@ -1,11 +1,9 @@ -import datetime from concurrent.futures import ThreadPoolExecutor from memoryscope.core.chat.base_memory_chat import BaseMemoryChat from memoryscope.core.config.config_manager import ConfigManager from memoryscope.core.memoryscope_context import MemoryscopeContext from memoryscope.core.service.base_memory_service import BaseMemoryService -from memoryscope.core.utils.logger import Logger from memoryscope.core.utils.tool_functions import init_instance_by_config from memoryscope.enumeration.language_enum import LanguageEnum from memoryscope.enumeration.model_enum import ModelEnum @@ -15,21 +13,9 @@ class MemoryScope(ConfigManager): def __init__(self, **kwargs): super().__init__(**kwargs) - - self.logger = self._init_logger() - self.context: MemoryscopeContext = MemoryscopeContext() self.init_context_by_config() - def _init_logger(self) -> Logger: - global_config = self.config["global"] - logger_name = global_config["logger_name"] - logger_name_time_suffix = global_config["logger_name_time_suffix"] - if logger_name_time_suffix: - suffix = datetime.datetime.now().strftime(logger_name_time_suffix) - logger_name = f"{logger_name}_{suffix}" - return Logger.get_logger(logger_name, to_stream=False) - def init_context_by_config(self): # set global config global_conf = self.config["global"] @@ -86,6 +72,7 @@ class MemoryScope(ConfigManager): def __enter__(self): self.init_context_by_config() + return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() diff --git a/memoryscope/core/utils/logger.py b/memoryscope/core/utils/logger.py index 9f400c6d..52d286f8 100644 --- a/memoryscope/core/utils/logger.py +++ b/memoryscope/core/utils/logger.py @@ -105,7 +105,8 @@ class Logger(logging.Logger): by the handlers are freed properly. """ for handler in self.handlers: - handler.close() # ⭐ Close each handler to release resources + # Close each handler to release resources + handler.close() def clear(self): """