diff --git a/memoryscope/chat/api_memory_chat.py b/memoryscope/chat/api_memory_chat.py index 7c0ab78a..da1d73de 100644 --- a/memoryscope/chat/api_memory_chat.py +++ b/memoryscope/chat/api_memory_chat.py @@ -74,7 +74,6 @@ class ApiMemoryChat(BaseMemoryChat): self._memory_service: BaseMemoryService = self.context.memory_service_dict[self._memory_service] # init service & update kwargs self._memory_service.init_service(human_name=self.human_name, assistant_name=self.assistant_name) - self._memory_service.start_backend_service() return self._memory_service @property @@ -95,62 +94,68 @@ class ApiMemoryChat(BaseMemoryChat): self._generation_model = self.context.model_dict[self._generation_model] return self._generation_model - def chat_with_memory(self, query: str, role_name: str = "") -> ModelResponse | ModelResponseGen: - """ - Engages in a conversation with the AI model, utilizing conversation memory. - The function sends the user's query, incorporates conversation history and memory, - and optionally remembers the AI's response based on the user's preference. - - Args: - query (str): The user's input or query for the AI. - role_name (str, optional): The user's name, default value is human_name. - - Returns: - - ModelResponse: In non-streaming mode, returns a complete AI response. - - ModelResponseGen: In streaming mode, returns a generator yielding AI response parts. - - Side Effects: - - Updates the conversation memory with the query of user and (optionally) the response of AI. - - Retrieves and includes historical messages and memory content in the context of conversation. - """ + def get_new_message(self, query: str, role_name: str = "") -> Message: if not role_name: role_name = self.human_name - new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query) - self.add_messages(new_message) - - messages: List[Message] = [] + 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 - memories: str = self.memory_service.retrieve_memory() if memories: memory_prompt = self.prompt_handler.memory_prompt system_prompt = "\n".join([x.strip() for x in [system_prompt, memory_prompt, memories]]) - messages.append(Message(role=MessageRoleEnum.SYSTEM, content=system_prompt)) + 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_new_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: - messages.extend(history_messages) + chat_messages.extend(history_messages) # Append the current user's message to the conversation context - messages.append(new_message) - self.logger.info(f"messages={messages}") + chat_messages.append(new_message) + self.logger.info(f"chat_messages={chat_messages}") - result = self.generation_model.call(messages=messages, stream=self.stream, **self.generation_model_kwargs) + resp = self.generation_model.call(messages=chat_messages, stream=self.stream, **self.generation_model_kwargs) if self.stream: - assert isinstance(result, ModelResponseGen) + assert isinstance(resp, ModelResponseGen) model_response: ModelResponse | None = None - for model_response in result: + for model_response in resp: yield model_response - if model_response and model_response.message: - self.add_messages(model_response.message) + if remember_response: + if model_response and model_response.message: + model_response.message.role_name = self.assistant_name + self.memory_service.add_messages([new_message, model_response.message]) + else: + self.logger.info("model_response or model_response.message is empty!") else: - assert isinstance(result, ModelResponse) - model_response: ModelResponse = result - if model_response and model_response.message: - self.add_messages(model_response.message) + assert isinstance(resp, ModelResponse) + model_response: ModelResponse = resp + if remember_response: + if model_response and model_response.message: + model_response.message.role_name = self.assistant_name + self.memory_service.add_messages([new_message, model_response.message]) + else: + self.logger.info("model_response or model_response.message is empty!") return model_response diff --git a/memoryscope/chat/base_memory_chat.py b/memoryscope/chat/base_memory_chat.py index d67c66be..dab56a0a 100644 --- a/memoryscope/chat/base_memory_chat.py +++ b/memoryscope/chat/base_memory_chat.py @@ -18,19 +18,27 @@ class BaseMemoryChat(metaclass=ABCMeta): self.logger = Logger.get_logger() @abstractmethod - def chat_with_memory(self, query: str, role_name: str = ""): + def get_new_message(self, query: str, role_name: str = "") -> Message: + raise NotImplementedError + + @abstractmethod + def get_system_message_with_memory(self, memories: str) -> Message: + raise NotImplementedError + + @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. Args: query (str): The user's query or message to start the chat. role_name (str): The role's name. - - Returns: - This method should return the chat response generated after processing the query - with the associated memory context. The actual return type and content are defined by the implementing - subclass. + remember_response (bool): whether update memory service. """ + raise NotImplementedError @property def memory_service(self) -> BaseMemoryService: @@ -45,6 +53,9 @@ class BaseMemoryChat(metaclass=ABCMeta): def add_messages(self, messages: List[Message] | Message): self.memory_service.add_messages(messages) + def start_backend_service(self): + self.memory_service.start_backend_service() + def do_memory_operation(self, op_name: str, **kwargs): return self.memory_service.do_operation(op_name=op_name, **kwargs) diff --git a/memoryscope/chat/cli_memory_chat.py b/memoryscope/chat/cli_memory_chat.py index a4ec880a..52c01d12 100644 --- a/memoryscope/chat/cli_memory_chat.py +++ b/memoryscope/chat/cli_memory_chat.py @@ -100,7 +100,6 @@ class CliMemoryChat(BaseMemoryChat): self._memory_service: BaseMemoryService = self.context.memory_service_dict[self._memory_service] # init service & update kwargs self._memory_service.init_service(human_name=self.human_name, assistant_name=self.assistant_name) - self._memory_service.start_backend_service() return self._memory_service @property @@ -121,53 +120,71 @@ class CliMemoryChat(BaseMemoryChat): self._generation_model = self.context.model_dict[self._generation_model] return self._generation_model - def chat_with_memory(self, query: str, role_name: str = "") -> ModelResponse | ModelResponseGen: - """ - Engages in a conversation with the AI model, utilizing conversation memory. - The function sends the user's query, incorporates conversation history and memory, - and optionally remembers the AI's response based on the user's preference. - - Args: - query (str): The user's input or query for the AI. - role_name (str, optional): The user's name, default value is human_name. - - Returns: - - ModelResponse: In non-streaming mode, returns a complete AI response. - - ModelResponseGen: In streaming mode, returns a generator yielding AI response parts. - - Side Effects: - - Updates the conversation memory with the query of user and (optionally) the response of AI. - - Retrieves and includes historical messages and memory content in the context of conversation. - """ + def get_new_message(self, query: str, role_name: str = "") -> Message: if not role_name: role_name = self.human_name - new_message: Message = Message(role=MessageRoleEnum.USER.value, role_name=role_name, content=query) - self.add_messages(new_message) - - messages: List[Message] = [] + 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 - memories: str = self.memory_service.retrieve_memory() if memories: memory_prompt = self.prompt_handler.memory_prompt system_prompt = "\n".join([x.strip() for x in [system_prompt, memory_prompt, memories]]) - messages.append(Message(role=MessageRoleEnum.SYSTEM, content=system_prompt)) + 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_new_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: - messages.extend(history_messages) + chat_messages.extend(history_messages) # Append the current user's message to the conversation context - messages.append(new_message) - self.logger.info(f"messages={messages}") + 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 - return self.generation_model.call(messages=messages, + resp = self.generation_model.call(messages=chat_messages, stream=self.stream, **self.generation_model_kwargs) + if self.stream: + assert isinstance(resp, ModelResponseGen) + model_response: ModelResponse | None = None + for model_response in resp: + questionary.print(model_response.delta, end="") + questionary.print("") + + 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]) + + else: + assert isinstance(resp, ModelResponse) + 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]) + @staticmethod def parse_query_command(query: str): """ @@ -296,19 +313,8 @@ class CliMemoryChat(BaseMemoryChat): questionary.print(f"{self.assistant_name}: ", end="", style="bold") # Fetch and display AI's response - self.memory_service.start_backend_service() - if self.stream: - model_response = None - for model_response in self.chat_with_memory(query=query): - questionary.print(model_response.delta, end="") - questionary.print("") - else: - model_response = self.chat_with_memory(query=query) - questionary.print(model_response.message.content) - - # Append AI's response to the conversation memory - model_response.message.role_name = self.assistant_name - self.add_messages(model_response.message) + self.start_backend_service() + self.chat_with_memory(query=query) except KeyboardInterrupt: # Handle user interruption and confirm exit diff --git a/memoryscope/memory/service/base_memory_service.py b/memoryscope/memory/service/base_memory_service.py index d72fc4c1..c8d964c0 100644 --- a/memoryscope/memory/service/base_memory_service.py +++ b/memoryscope/memory/service/base_memory_service.py @@ -31,25 +31,6 @@ class BaseMemoryService(metaclass=ABCMeta): self._op_description_dict: Dict[str, str] = {} self.logger = Logger.get_logger() - @abstractmethod - def add_messages(self, messages: List[Message] | Message): - raise NotImplementedError - - @abstractmethod - def do_operation(self, op_name: str, **kwargs): - """ - Abstract method defining the interface for executing a specific operation by its name. - This method must be implemented by subclasses to provide the actual operation logic. - - Args: - op_name (str): The name identifying the operation to be performed. - **kwargs: Additional keyword arguments required for the operation execution. - - Raises: - NotImplementedError: This exception is raised when the method is not overridden in a subclass. - """ - raise NotImplementedError - @property def op_description_dict(self) -> Dict[str, str]: """ @@ -63,27 +44,9 @@ class BaseMemoryService(metaclass=ABCMeta): self._op_description_dict = {k: v.description for k, v in self._operation_dict.items()} return self._op_description_dict - def retrieve_memory(self): - """ - Executes the operation associated with retrieved memory. - Asserts that the operation for retrieved memory has been initialized. - - Returns: - Any: The result of the retrieved memory operation. - """ - assert self.retrieve_memory_key in self._operation_dict, f"op={self.retrieve_memory_key} is not inited!" - return self.do_operation(self.retrieve_memory_key) - - def read_message(self): - """ - Executes the operation associated with reading messages. - Asserts that the operation for reading messages has been initialized. - - Returns: - Any: The result of the read message operation. - """ - assert self.read_message_key in self._operation_dict, f"op={self.read_message_key} is not inited!" - return self.do_operation(self.read_message_key) + @abstractmethod + def add_messages(self, messages: List[Message] | Message): + raise NotImplementedError @abstractmethod def init_service(self, **kwargs): @@ -94,3 +57,27 @@ class BaseMemoryService(metaclass=ABCMeta): def stop_backend_service(self): pass + + def do_operation(self, op_name: str, **kwargs): + """ + Executes a specific operation by its name with provided keyword arguments. + + Args: + op_name (str): The name of the operation to execute. + **kwargs: Keyword arguments for the operation's execution. + + Returns: + The result of the operation execution, if any. Otherwise, None. + + Raises: + Warning: If the operation name is not initialized in `_operation_dict`. + """ + if op_name not in self._operation_dict: + self.logger.warning(f"op_name={op_name} is not inited!") + return + return self._operation_dict[op_name].run_operation(**kwargs) + + def __getattr__(self, name: str): + return lambda **kwargs: self.do_operation(name, **kwargs) + + diff --git a/memoryscope/memory/worker/frontend/set_query_worker.py b/memoryscope/memory/worker/frontend/set_query_worker.py index 552baf5e..1f079288 100644 --- a/memoryscope/memory/worker/frontend/set_query_worker.py +++ b/memoryscope/memory/worker/frontend/set_query_worker.py @@ -22,22 +22,39 @@ class SetQueryWorker(MemoryBaseWorker): along with its creation timestamp. """ query = "" # Default query value - query_timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default + timestamp = int(datetime.datetime.now().timestamp()) # Current timestamp as default if "query" in self.chat_kwargs: - # Check if a specific 'query' has been provided via chat kwargs + # set query if exists query = self.chat_kwargs["query"] if not query: query = "" query = query.strip() + # set ts if exists + _timestamp = self.chat_kwargs.get("timestamp") + if _timestamp and isinstance(_timestamp, int): + timestamp = _timestamp + + # check role_name + role_name = self.chat_kwargs.get("role_name") + if role_name: + assert role_name == self.target_name, \ + f"role_name={role_name} is not supported in human/assistant memory workflow!" + elif self.chat_messages: # If no explicit query is given, use the content of the latest chat message chat_messages = [msg for msg in self.chat_messages if msg.role == MessageRoleEnum.USER.value] if chat_messages: message = chat_messages[-1] query = message.content - query_timestamp = message.time_created + timestamp = message.time_created + + # check role_name + role_name = message.role_name + if role_name: + assert role_name == self.target_name, \ + f"role_name={role_name} is not supported in human/assistant memory workflow!" # Store the determined query and its timestamp in the context - self.set_context(QUERY_WITH_TS, (query, query_timestamp)) + self.set_context(QUERY_WITH_TS, (query, timestamp)) diff --git a/tests/other/test_attr.py b/tests/other/test_attr.py new file mode 100644 index 00000000..eaf2fbf8 --- /dev/null +++ b/tests/other/test_attr.py @@ -0,0 +1,15 @@ +class MyClass: + def __init__(self): + self.existing_attribute = "I exist" + + def do(self, name: str, **kwargs): + print("do %s %s" % (name, kwargs)) + + def __getattr__(self, name): + return lambda **kwargs: self.do(name, **kwargs) + + +# 创建类的实例 +obj = MyClass() + +obj.haha(a=1, b=2)