From 7b09f0bd2b0e35036b2570ca7e6fd04bc0ce9a7d Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Mon, 22 Jul 2024 10:46:26 +0800 Subject: [PATCH] fix empty content insight bug && modify print memory --- config/demo_config_cn.yaml | 9 ++- memory_scope/chat/cli_memory_chat.yaml | 4 +- .../memory/operation/write_memory_op.py | 24 +++---- .../memory/service/chat_memory_service.py | 46 ++++++++++---- .../worker/frontend/print_memory_worker.py | 47 ++++++-------- .../worker/frontend/print_memory_worker.yaml | 22 +++++++ .../worker/frontend/read_message_worker.py | 9 +-- .../worker/frontend/semantic_rank_worker.py | 2 +- .../summary/get_reflection_subject_worker.py | 5 +- .../summary/long_contra_repeat_worker.py | 4 ++ .../worker/summary/update_insight_worker.py | 15 +++-- .../worker/write/contra_repeat_worker.py | 6 +- .../memory/worker/write/info_filter_worker.py | 1 - .../worker/write/info_filter_worker.yaml | 12 ++++ memory_scope/models/base_model.py | 63 +++++++------------ .../models/llama_index_embedding_model.py | 12 ++-- .../models/llama_index_generation_model.py | 51 ++++++++------- memory_scope/models/llama_index_rank_model.py | 33 ++++------ memory_scope/scheme/model_response.py | 2 + memory_scope/utils/memory_handler.py | 20 +++++- 20 files changed, 217 insertions(+), 170 deletions(-) create mode 100644 memory_scope/memory/worker/frontend/print_memory_worker.yaml diff --git a/config/demo_config_cn.yaml b/config/demo_config_cn.yaml index 331f3494..3bbd9e89 100644 --- a/config/demo_config_cn.yaml +++ b/config/demo_config_cn.yaml @@ -5,13 +5,12 @@ global_config: memory_chat: cli_memory_chat: class: chat.cli_memory_chat - memory_service: memory_chat_service + memory_service: chat_memory_service generation_model: dashscope_generation memory_service: - memory_chat_service: + chat_memory_service: class: memory.service.chat_memory_service - contextual_msg_count: 6 memory_operations: read_message: class: memory.operation.frontend_operation @@ -47,13 +46,13 @@ memory_service: class: memory.operation.write_memory_op workflow: info_filter,[get_observation|get_observation_with_time|load_today_memory],contra_repeat,store_memory description: "write observation memory of the user" - interval_time: 5 + interval_time: 1 summary_memory: class: memory.operation.backend_operation workflow: load_obs_and_insight,get_reflection_subject,update_insight,long_contra_repeat,store_memory description: "summary observation memory of the user" - interval_time: 30 + interval_time: 15 worker: dummy: diff --git a/memory_scope/chat/cli_memory_chat.yaml b/memory_scope/chat/cli_memory_chat.yaml index 09e3d5ef..4880fe0d 100644 --- a/memory_scope/chat/cli_memory_chat.yaml +++ b/memory_scope/chat/cli_memory_chat.yaml @@ -1,8 +1,8 @@ system_prompt: cn: | - 你是一个叫MemoryScope的AI小助手,善于倾听用户的问题和心声,并给出用户建议,回答时使用中文,不要太冗长。 + 你是一个叫MemoryScope的AI小助手,善于倾听用户的问题和心声,回答时使用中文,不要太冗长。 en: | - You are an AI assistant named MemoryScope, good at listening to users' questions and feelings, and providing them with advice. Respond in English without being too lengthy. + You are an AI assistant named MemoryScope, good at listening to users' questions and feelings. Respond in English without being too lengthy. memory_prompt: cn: | diff --git a/memory_scope/memory/operation/write_memory_op.py b/memory_scope/memory/operation/write_memory_op.py index 8d481841..08d8ce6e 100644 --- a/memory_scope/memory/operation/write_memory_op.py +++ b/memory_scope/memory/operation/write_memory_op.py @@ -1,18 +1,15 @@ -from memory_scope.constants.common_constants import CHAT_MESSAGES, RESULT, CHAT_KWARGS +from memory_scope.constants.common_constants import CHAT_KWARGS, CHAT_MESSAGES, RESULT from memory_scope.enumeration.message_role_enum import MessageRoleEnum from memory_scope.memory.operation.backend_operation import BackendOperation class WriteMemoryOp(BackendOperation): - def __init__(self, - message_lock=None, - contextual_msg_count: int = 6, - **kwargs): + def __init__(self, **kwargs): super(WriteMemoryOp, self).__init__(**kwargs) - self.message_lock = message_lock - self.contextual_msg_count: int = contextual_msg_count + self.message_lock = kwargs.get("message_lock", None) + self.contextual_msg_min_count: int = kwargs.get("contextual_msg_min_count", 0) def _run_operation(self, **kwargs): """ @@ -41,15 +38,15 @@ class WriteMemoryOp(BackendOperation): chat_messages = chat_messages[:-1] not_memorized_size = sum([not x.memorized for x in chat_messages]) - if not_memorized_size < self.contextual_msg_count: + if not_memorized_size < self.contextual_msg_min_count: self.logger.info(f"not_memorized_size({not_memorized_size}) < " - f"contextual_msg_count({self.contextual_msg_count}), skip.") + f"contextual_msg_min_count({self.contextual_msg_min_count}), skip.") return self.context.clear() # Add additional arguments to the context - kwargs.update({"contextual_msg_count": self.contextual_msg_count, **self.kwargs}) + kwargs.update(**self.kwargs) self.context[CHAT_KWARGS] = kwargs # Include the most recent messages in the operation context @@ -62,9 +59,8 @@ class WriteMemoryOp(BackendOperation): result = self.context.get(RESULT) # set message memorized - if self.message_lock: - with self.message_lock: - for message in chat_messages: - message.memorized = True + with self.message_lock: + for message in chat_messages: + message.memorized = True return result diff --git a/memory_scope/memory/service/chat_memory_service.py b/memory_scope/memory/service/chat_memory_service.py index 1da26091..afd95d02 100644 --- a/memory_scope/memory/service/chat_memory_service.py +++ b/memory_scope/memory/service/chat_memory_service.py @@ -7,11 +7,26 @@ from memory_scope.utils.tool_functions import init_instance_by_config class ChatMemoryService(BaseMemoryService): - def __init__(self, history_msg_count: int = 100, contextual_msg_count: int = 6, **kwargs): + def __init__(self, + history_msg_count: int = 100, + contextual_msg_max_count: int = 20, + contextual_msg_min_count: int = 0, + **kwargs): + """ + init function. + Args: + history_msg_count (int): The conversation history in memory, control the quantity, and reduce memory usage. + contextual_msg_max_count (int): The maximum context length in a conversation. If it exceeds this length, + it will not be included in the context to prevent token overflow. + contextual_msg_min_count (int): The minimum context length in a conversation. If it is shorter than this + length, no conversation summary will be made and no long-term memory will be generated. + kwargs (dict): other kwargs + """ super().__init__(**kwargs) self.history_msg_count: int = history_msg_count - self.contextual_msg_count: int = contextual_msg_count - assert self.history_msg_count >= self.contextual_msg_count + self.contextual_msg_max_count: int = contextual_msg_max_count + self.contextual_msg_min_count: int = contextual_msg_min_count + assert history_msg_count >= contextual_msg_max_count >= contextual_msg_min_count def add_messages(self, messages: List[Message] | Message): """ @@ -26,17 +41,18 @@ class ChatMemoryService(BaseMemoryService): if isinstance(messages, Message): messages = [messages] - # Sort the messages by their creation time to maintain chronological order - messages = sorted(messages, key=lambda x: x.time_created) + with self.message_lock: + # Append the sorted messages to the chat history + self.chat_messages.extend(messages) - # Append the sorted messages to the chat history - self.chat_messages.extend(messages) + # Sort the messages by their creation time to maintain chronological order + self.chat_messages.sort(key=lambda x: x.time_created) - # If the chat history exceeds the allowed message count, remove the oldest messages - if len(self.chat_messages) > self.history_msg_count: - gap_size = len(self.chat_messages) - self.history_msg_count - for _ in range(gap_size): - self.chat_messages.pop(0) + # If the chat history exceeds the allowed message count, remove the oldest messages + if len(self.chat_messages) > self.history_msg_count: + gap_size = len(self.chat_messages) - self.history_msg_count + for _ in range(gap_size): + self.chat_messages.pop(0) def do_operation(self, op_name: str, **kwargs): """ @@ -69,13 +85,17 @@ class ChatMemoryService(BaseMemoryService): name=name, chat_messages=self.chat_messages, message_lock=self.message_lock, - contextual_msg_count=self.contextual_msg_count) + contextual_msg_max_count=self.contextual_msg_max_count, + contextual_msg_min_count=self.contextual_msg_min_count) operation.init_workflow(**kwargs) # Initialize workflow for each operation self._operation_dict[name] = operation self.logger.info(f"service={self.__class__.__name__} init operation={name}") def start_backend_service(self): + """ + Start all backend operations. + """ for _, operation in self._operation_dict.items(): if operation.operation_type == "backend": # Run backend operations diff --git a/memory_scope/memory/worker/frontend/print_memory_worker.py b/memory_scope/memory/worker/frontend/print_memory_worker.py index 943e323a..ed1902fd 100644 --- a/memory_scope/memory/worker/frontend/print_memory_worker.py +++ b/memory_scope/memory/worker/frontend/print_memory_worker.py @@ -7,60 +7,51 @@ from memory_scope.memory.worker.memory_base_worker import MemoryBaseWorker from memory_scope.scheme.memory_node import MemoryNode from memory_scope.utils.datetime_handler import DatetimeHandler -PRINT_TEMPLATE = """ -The memories of {user_name} about {target_name}. - -{obs_content} - - -{insight_content} - - -{expired_content} -""" - class PrintMemoryWorker(MemoryBaseWorker): + FILE_PATH: str = __file__ def _run(self): + # get long-term memory memory_node_list: List[MemoryNode] = self.memory_handler.get_memories(RETRIEVE_MEMORY_NODES) memory_node_list = sorted(memory_node_list, key=lambda x: x.timestamp, reverse=True) - expired_content_list: List[str] = ["----- expired -----"] - obs_content_list: List[str] = ["----- observation -----"] - insight_content_list: List[str] = ["----- insight -----"] + observation_memory_list: List[str] = [] + insight_memory_list: List[str] = [] + expired_memory_list: List[str] = [] + i = 0 j = 0 k = 0 + # remove duplicate content expired_content_set = set() for node in memory_node_list: + if not node.content: + continue + dt_handler = DatetimeHandler(node.timestamp) - dt = dt_handler.datetime_format("%Y%m%d-%H:%M:%S") - line = f"{dt} {node.content}" + dt = dt_handler.datetime_format("%Y%m%d %H:%M:%S") if StoreStatusEnum(node.store_status) is StoreStatusEnum.EXPIRED: if node.content in expired_content_set: continue else: expired_content_set.add(node.content) i += 1 - expired_content_list.append(f" {i} {line}") + expired_memory_list.append(f"{dt}] {i}. {node.content}") elif MemoryTypeEnum(node.memory_type) in [MemoryTypeEnum.OBSERVATION, MemoryTypeEnum.OBS_CUSTOMIZED]: j += 1 - obs_content_list.append(f" {j} {line} status={node.obs_reflected}") + observation_memory_list.append(f"{dt}] {j}. {node.content} " + f"status({node.obs_reflected},{node.obs_updated})") elif MemoryTypeEnum(node.memory_type) is MemoryTypeEnum.INSIGHT: k += 1 - insight_content_list.append(f" {k} {line}") + insight_memory_list.append(f"{dt}] {k}. {node.content}") - obs_content = "\n".join(obs_content_list) - insight_content = "\n".join(insight_content_list) - expired_content = "\n".join(expired_content_list) - result: str = PRINT_TEMPLATE.format( + result: str = self.prompt_handler.print_template.format( user_name=self.user_name, target_name=self.target_name, - obs_content=obs_content, - insight_content=insight_content, - expired_content=expired_content, - ).strip() + observation_memory="\n".join(observation_memory_list), + insight_memory="\n".join(insight_memory_list), + expired_memory="\n".join(expired_memory_list)).strip() self.set_context(RESULT, result) diff --git a/memory_scope/memory/worker/frontend/print_memory_worker.yaml b/memory_scope/memory/worker/frontend/print_memory_worker.yaml new file mode 100644 index 00000000..7fd03a01 --- /dev/null +++ b/memory_scope/memory/worker/frontend/print_memory_worker.yaml @@ -0,0 +1,22 @@ +print_template: + cn: | + ========== {user_name}关于{target_name}的长期记忆 ========== + ----- 观察记忆 ----- + {observation_memory} + + ----- 洞察记忆 ----- + {insight_memory} + + ----- 过期记忆 ----- + {expired_memory} + + en: | + ========== The {user_name}'s long-term memory about {target_name} ========== + ----- observation memory ----- + {observation_memory} + + ----- insight memory ----- + {insight_memory} + + ----- expired memory ----- + {expired_memory} \ No newline at end of file diff --git a/memory_scope/memory/worker/frontend/read_message_worker.py b/memory_scope/memory/worker/frontend/read_message_worker.py index 053018fe..4d08b2f4 100644 --- a/memory_scope/memory/worker/frontend/read_message_worker.py +++ b/memory_scope/memory/worker/frontend/read_message_worker.py @@ -6,10 +6,11 @@ from memory_scope.memory.worker.memory_base_worker import MemoryBaseWorker class ReadMessageWorker(MemoryBaseWorker): def _run(self): - contextual_msg_count: int = self.chat_kwargs["contextual_msg_count"] - - chat_messages = self.chat_messages.copy() + chat_messages = [x for x in self.chat_messages if not x.memorized] if len(chat_messages) > 0 and chat_messages[-1].role == MessageRoleEnum.USER.value: chat_messages = chat_messages[:-1] - self.set_context(RESULT, chat_messages[-contextual_msg_count:]) + contextual_msg_max_count: int = self.chat_kwargs["contextual_msg_max_count"] + chat_messages = chat_messages[-contextual_msg_max_count:] + + self.set_context(RESULT, chat_messages) diff --git a/memory_scope/memory/worker/frontend/semantic_rank_worker.py b/memory_scope/memory/worker/frontend/semantic_rank_worker.py index 32f5d8b9..dd0dc53d 100644 --- a/memory_scope/memory/worker/frontend/semantic_rank_worker.py +++ b/memory_scope/memory/worker/frontend/semantic_rank_worker.py @@ -35,7 +35,7 @@ class SemanticRankWorker(MemoryBaseWorker): return # drop repeated - memory_node_dict: Dict[str, MemoryNode] = {n.content: n for n in memory_node_list} + memory_node_dict: Dict[str, MemoryNode] = {n.content.strip(): n for n in memory_node_list if n.content.strip()} memory_node_list = list(memory_node_dict.values()) response = self.rank_model.call(query=query, documents=[n.content for n in memory_node_list]) diff --git a/memory_scope/memory/worker/summary/get_reflection_subject_worker.py b/memory_scope/memory/worker/summary/get_reflection_subject_worker.py index 8a558025..ee0da56a 100644 --- a/memory_scope/memory/worker/summary/get_reflection_subject_worker.py +++ b/memory_scope/memory/worker/summary/get_reflection_subject_worker.py @@ -64,8 +64,9 @@ class GetReflectionSubjectWorker(MemoryBaseWorker): # Count unaudited nodes not_reflected_count = len(not_reflected_nodes) if not_reflected_count < self.reflect_obs_cnt_threshold: - self.logger.info(f"not_reflected_count({not_reflected_count}) < is not enough, skip.") - self.continue_run = False + self.logger.info(f"not_reflected_count({not_reflected_count}) < threshold({self.reflect_obs_cnt_threshold})" + f" is not enough, skip.") + # self.continue_run = False return # Compile existing insight keys diff --git a/memory_scope/memory/worker/summary/long_contra_repeat_worker.py b/memory_scope/memory/worker/summary/long_contra_repeat_worker.py index ba7a2918..9fc2d843 100644 --- a/memory_scope/memory/worker/summary/long_contra_repeat_worker.py +++ b/memory_scope/memory/worker/summary/long_contra_repeat_worker.py @@ -85,6 +85,10 @@ class LongContraRepeatWorker(MemoryBaseWorker): self.logger.warning("all_obs_nodes is empty, stop.") return + if len(all_obs_nodes) == 1: + self.logger.info("all_obs_nodes.size=1, stop.") + return + # gene prompt user_query_list = [] for i, n in enumerate(all_obs_nodes): diff --git a/memory_scope/memory/worker/summary/update_insight_worker.py b/memory_scope/memory/worker/summary/update_insight_worker.py index 0c4a5cde..71878b5e 100644 --- a/memory_scope/memory/worker/summary/update_insight_worker.py +++ b/memory_scope/memory/worker/summary/update_insight_worker.py @@ -23,7 +23,7 @@ class UpdateInsightWorker(MemoryBaseWorker): def _parse_params(self, **kwargs): self.update_insight_threshold: float = kwargs.get("update_insight_threshold", 0.1) self.generation_model_kwargs: dict = kwargs.get("generation_model_kwargs", {}) - self.update_insight_max_count: int = kwargs.get("update_insight_max_count", 10) + self.update_insight_max_count: int = kwargs.get("update_insight_max_count", 5) def filter_obs_nodes(self, insight_node: MemoryNode, @@ -165,7 +165,8 @@ class UpdateInsightWorker(MemoryBaseWorker): """ insight_nodes: List[MemoryNode] = self.memory_handler.get_memories(INSIGHT_NODES) not_updated_nodes: List[MemoryNode] = self.memory_handler.get_memories(NOT_UPDATED_NODES) - not_reflected_nodes: List[MemoryNode] = self.memory_handler.get_memories(NOT_REFLECTED_NODES) + not_reflected_nodes: List[MemoryNode] = self.memory_handler.get_memories(keys=[NOT_REFLECTED_NODES, + NOT_UPDATED_NODES]) if not insight_nodes: self.logger.warning("insight_nodes is empty, stopping processing.") @@ -201,10 +202,14 @@ class UpdateInsightWorker(MemoryBaseWorker): for _ in self.gather_thread_result(): pass + # delete empty nodes + empty_nodes = [n for n in insight_nodes if not n.content.strip()] + self.memory_handler.delete_memories(empty_nodes) + for node in not_updated_nodes: node.obs_updated = 1 node.action_status = ActionStatusEnum.MODIFIED - for node in not_reflected_nodes: - node.obs_updated = 1 - node.action_status = ActionStatusEnum.MODIFIED + # for node in not_reflected_nodes: + # node.obs_updated = 1 + # node.action_status = ActionStatusEnum.MODIFIED diff --git a/memory_scope/memory/worker/write/contra_repeat_worker.py b/memory_scope/memory/worker/write/contra_repeat_worker.py index c312ea34..e9ca2094 100644 --- a/memory_scope/memory/worker/write/contra_repeat_worker.py +++ b/memory_scope/memory/worker/write/contra_repeat_worker.py @@ -46,7 +46,7 @@ class ContraRepeatWorker(MemoryBaseWorker): all_obs_nodes: List[MemoryNode] = self.memory_handler.get_memories([NEW_OBS_NODES, NEW_OBS_WITH_TIME_NODES]) if not all_obs_nodes: self.logger.info("all_obs_nodes is empty!") - self.continue_run = False + # self.continue_run = False return today_obs_nodes: List[MemoryNode] = self.memory_handler.get_memories(TODAY_NODES) @@ -55,6 +55,10 @@ class ContraRepeatWorker(MemoryBaseWorker): all_obs_nodes.extend(today_obs_nodes) all_obs_nodes = sorted(all_obs_nodes, key=lambda x: x.timestamp, reverse=True)[:self.contra_repeat_max_count] + if len(all_obs_nodes) == 1: + self.logger.info("all_obs_nodes.size=1, stop.") + return + # build prompt user_query_list = [] for i, n in enumerate(all_obs_nodes): diff --git a/memory_scope/memory/worker/write/info_filter_worker.py b/memory_scope/memory/worker/write/info_filter_worker.py index 034e5518..7146ea55 100644 --- a/memory_scope/memory/worker/write/info_filter_worker.py +++ b/memory_scope/memory/worker/write/info_filter_worker.py @@ -59,7 +59,6 @@ class InfoFilterWorker(MemoryBaseWorker): user_query_list = [] for i, msg in enumerate(info_messages): user_query_list.append(f"{i + 1} {self.target_name}{self.get_language_value(COLON_WORD)} {msg.content}") - self.logger.warning(self.prompt_handler.prompt_dict) system_prompt = self.prompt_handler.info_filter_system.format(batch_size=len(info_messages), user_name=self.target_name) few_shot = self.prompt_handler.info_filter_few_shot.format(user_name=self.target_name) diff --git a/memory_scope/memory/worker/write/info_filter_worker.yaml b/memory_scope/memory/worker/write/info_filter_worker.yaml index 2ecb9dc8..465b80ec 100644 --- a/memory_scope/memory/worker/write/info_filter_worker.yaml +++ b/memory_scope/memory/worker/write/info_filter_worker.yaml @@ -32,6 +32,7 @@ info_filter_few_shot: 4 {user_name}:我今天心情不好,可以安慰我一下吗? 5 {user_name}:能给我整理一张如何使用大模型的技巧列表吗,要求内容尽量精简。 6 {user_name}:记一下,明天下午3点提醒我去拿一下文件。 + 思考:从第1句可以确定推断出张三是{user_name}同事这一重要信息。 结果:<1> <3> 思考:第2句不包含{user_name}信息。 @@ -55,6 +56,7 @@ info_filter_few_shot: 5 {user_name}:假如我要和一个女人准备要孩子,我作为男人,怎么保护女人和孩子以及怎么备孕确保精子质量高对后代好 6 {user_name}:我和你一起出去玩,你会感觉开心吗? 7 {user_name}:林浅,一位对未来充满好奇的年轻女孩,偶然间发现了这家能寄信给未来的邮局。出于对逝去祖父的怀念,她决定写下一封信,寄给五年后的自己,希望能收到祖父生前未说完的故事。五年期限将至,当她几乎忘记这段往事时,一封泛黄的回信悄然降临,不仅带来了祖父未完的冒险故事,还藏着一段关于勇气、爱与自我发现的深刻启示。续写成3000字小说。 + 思考:从第1句可以确定得出{user_name}工作单位是阿里巴巴这一重要信息。 结果:<1> <3> 思考:从第2句可以猜测{user_name}近期露天睡觉,是不确定的信息。 @@ -78,6 +80,8 @@ info_filter_few_shot: 3 {user_name}:我很喜欢打篮球,所以我身体很好 4 {user_name}:篮球明星有哪些? 5 {user_name}:李增杰:这个是星座蛙设,但是我是处女座的,我妈感觉因为我的不正常,我妈不让我看了\n雌猴摸了摸李增杰的头,这样啊\n雌猴打开了哔哩哔哩看了看\n雌猴:要不换个设吧,我听你未来的你说,有一个叫难忘的朱古力232这个人,他弄的设是Windows设\n这是剧本1,剧本2未完待续 + 6 {user_name}:我想知道昨天我们聊了什么? + 思考:第1句是{user_name}假设的信息。 结果:<1> <1> 思考:第2句信息不明,可能是{user_name}假设的信息。 @@ -88,6 +92,8 @@ info_filter_few_shot: 结果:<4> <0> 思考:第5句是{user_name}虚构的内容。 结果:<5> <1> + 思考:第6句是{user_name}的疑问句,没有包含信息。 + 结果:<6> <0> en: | Example 1 @@ -98,6 +104,7 @@ info_filter_few_shot: 4 {user_name}: I'm feeling down today. Can you comfort me a bit? 5 {user_name}: Can you compile a list of tips on how to use large models for me, and try to keep the content concise? 6 {user_name}: Note this down: remind me tomorrow at 3 PM to pick up the documents. + Thought: From the first sentence, it can be inferred that Zhang San is a colleague of {user_name}, which is important information. Result: <1> <3> Thought: The second sentence does not contain information about {user_name}. @@ -120,6 +127,7 @@ info_filter_few_shot: 5 {user_name}: If I am planning to have a child with a woman, as a man, how can I protect the woman and the baby and how can I prepare to ensure high sperm quality for the benefit of the offspring? 6 {user_name}: If we go out to play together, would you feel happy? 7 {user_name}: Rose, a young girl full of curiosity about the future, accidentally discovered this post office that can send letters to the future. Out of nostalgia for her late grandfather, she decided to write a letter to herself five years in the future, hoping to receive the unfinished stories of her grandfather. As the five-year deadline approached, when she had almost forgotten about this event, a yellowed reply quietly arrived, bringing not only her grandfather's unfinished adventure story but also a profound revelation about courage, love, and self-discovery. Continue writing this into a 3000-word novel. + Thought: From the first sentence, it can be determined that {user_name} works at Alibaba, which is important information. Result: <1> <3> Thought: The second sentence suggests that {user_name} might has been sleeping outdoors recently, which is uncertain information. @@ -142,6 +150,8 @@ info_filter_few_shot: 3 {user_name}: I really enjoy playing basketball, so I am in good health. 4 {user_name}: Who are some famous basketball stars? 5 {user_name}: Zack: This is a constellation frog setting, but I am a Virgo. My mom feels I'm abnormal and doesn't let me watch it. \n The female monkey patted Zack's head. "Is that so?" \n The female monkey opened Bilibili and took a look. \n Female monkey: "Why don't you switch the setting? I heard from your future self that there is someone called 'Unforgettable Chocolate 232' who created a Windows setting." \n This is script 1; script 2 is to be continued. + 6 {user_name}: I want to know what we talked about yesterday. + Thought: The first sentence contains only hypothetical information from {user_name}. Result: <1> <1> Thought: The second sentence is unclear and may contain hypothetical information from {user_name}. @@ -152,6 +162,8 @@ info_filter_few_shot: Result: <4> <0> Thought: The fifth sentence contains only fictitious content from {user_name}. Result: <5> <1> + Thought: Sentence 6 is a question from {user_name} that doesn't include any specific information. + Result: <6> <0> info_filter_user_query: cn: | diff --git a/memory_scope/models/base_model.py b/memory_scope/models/base_model.py index 41a9d9e7..8e0197d1 100644 --- a/memory_scope/models/base_model.py +++ b/memory_scope/models/base_model.py @@ -34,9 +34,7 @@ class BaseModel(metaclass=ABCMeta): self.raise_exception: bool = raise_exception self.kwargs: dict = kwargs - self.data = {} self._model: Any = None - self.logger = Logger.get_logger() @property @@ -56,44 +54,31 @@ class BaseModel(metaclass=ABCMeta): return self._model @abstractmethod - def before_call(self, **kwargs) -> None: - """prepare data before call - :param kwargs: - :return: - """ + def before_call(self, model_response: ModelResponse, **kwargs): + pass @abstractmethod - def after_call(self, model_response: ModelResponse | ModelResponseGen, - **kwargs) -> ModelResponse | ModelResponseGen: - """ - :param model_response: - :param kwargs: - :return: - """ + def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse | ModelResponseGen: + pass @abstractmethod - def _call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen: - """ - :param kwargs: - :return: - """ + def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs): + pass def call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen: - """ - :param stream: only llm needs stream - :param kwargs: - :return: - """ with Timer(self.__class__.__name__, time_log_type="none") as t: - self.before_call(stream=stream, **kwargs) + model_response = ModelResponse(m_type=self.m_type) + + self.before_call(stream=stream, model_response=model_response, **kwargs) for i in range(self.max_retries): if self.raise_exception: - model_response = self._call(stream=stream, **kwargs) + self._call(stream=stream, model_response=model_response, **kwargs) else: try: - model_response = self._call(stream=stream, **kwargs) + self._call(stream=stream, model_response=model_response, **kwargs) except Exception as e: - model_response = ModelResponse(m_type=self.m_type, status=False, details=e.args) + model_response.status = False + model_response.details = e.args if isinstance(model_response, ModelResponse) and not model_response.status: self.logger.warning(f"call model={self.model_name} failed! {t.cost_str} retry_cnt={i} " @@ -103,27 +88,23 @@ class BaseModel(metaclass=ABCMeta): return self.after_call(stream=stream, model_response=model_response, **kwargs) @abstractmethod - async def _async_call(self, **kwargs) -> ModelResponse: - """ - :param kwargs: - :return: - """ + async def _async_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse: + pass async def async_call(self, **kwargs) -> ModelResponse: - """ 异步不需要stream - :param kwargs: - :return: - """ with Timer(self.__class__.__name__, time_log_type="none") as t: - self.before_call(**kwargs) + model_response = ModelResponse(m_type=self.m_type) + + self.before_call(model_response=model_response, **kwargs) for i in range(self.max_retries): if self.raise_exception: - model_response = self._async_call(**kwargs) + await self._async_call(model_response=model_response, **kwargs) else: try: - model_response = self._async_call(**kwargs) + await self._async_call(model_response=model_response, **kwargs) except Exception as e: - model_response = ModelResponse(m_type=self.m_type, status=False, details=e.args) + model_response.status = False + model_response.details = e.args if not model_response.status: self.logger.warning(f"async_call model={self.model_name} failed! {t.cost_str} retry_cnt={i} " diff --git a/memory_scope/models/llama_index_embedding_model.py b/memory_scope/models/llama_index_embedding_model.py index ddd1e029..0eb6dc51 100644 --- a/memory_scope/models/llama_index_embedding_model.py +++ b/memory_scope/models/llama_index_embedding_model.py @@ -27,11 +27,11 @@ class LlamaIndexEmbeddingModel(BaseModel): MODEL_REGISTRY.register("dashscope_embedding", DashScopeEmbedding) - def before_call(self, **kwargs): + def before_call(self, model_response: ModelResponse, **kwargs): text: str | List[str] = kwargs.pop("text", "") if isinstance(text, str): text = [text] - self.data = dict(texts=text) + model_response.meta_data["data"] = dict(texts=text) def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse: embeddings = model_response.raw @@ -47,7 +47,7 @@ class LlamaIndexEmbeddingModel(BaseModel): model_response.embedding_results = embeddings return model_response - def _call(self, **kwargs) -> ModelResponse: + def _call(self, model_response: ModelResponse, **kwargs): """ Executes a synchronous call to generate embeddings for the input data. @@ -61,9 +61,9 @@ class LlamaIndexEmbeddingModel(BaseModel): Returns: ModelResponse: An object containing the embedding results and the model type. """ - return ModelResponse(m_type=self.m_type, raw=self.model.get_text_embedding_batch(**self.data)) + model_response.raw = self.model.get_text_embedding_batch(**model_response.meta_data["data"]) - async def _async_call(self, **kwargs) -> ModelResponse: + async def _async_call(self, model_response: ModelResponse, **kwargs): """ Executes an asynchronous call to generate embeddings for the input data. @@ -77,4 +77,4 @@ class LlamaIndexEmbeddingModel(BaseModel): Returns: ModelResponse: An object encapsulating the embedding output and the model's type. """ - return ModelResponse(m_type=self.m_type, raw=await self.model.aget_text_embedding_batch(**self.data)) + model_response.raw = await self.model.aget_text_embedding_batch(**model_response.meta_data["data"]) diff --git a/memory_scope/models/llama_index_generation_model.py b/memory_scope/models/llama_index_generation_model.py index 80107266..cc0d15b4 100644 --- a/memory_scope/models/llama_index_generation_model.py +++ b/memory_scope/models/llama_index_generation_model.py @@ -23,7 +23,7 @@ class LlamaIndexGenerationModel(BaseModel): MODEL_REGISTRY.register("dashscope_generation", DashScope) - def before_call(self, **kwargs): + def before_call(self, model_response: ModelResponse, **kwargs): """ Prepares the input data before making a call to the language model. It accepts either a 'prompt' directly or a list of 'messages'. @@ -32,6 +32,7 @@ class LlamaIndexGenerationModel(BaseModel): Raises an error if neither 'prompt' nor 'messages' are supplied. Args: + model_response: model_response **kwargs: Arbitrary keyword arguments including 'prompt' and 'messages'. Raises: @@ -41,15 +42,16 @@ class LlamaIndexGenerationModel(BaseModel): messages: List[Message] | List[dict] = kwargs.pop("messages", []) if prompt: - self.data = {"prompt": prompt} + data = {"prompt": prompt} elif messages: if isinstance(messages[0], dict): - self.data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]} + data = {"messages": [ChatMessage(role=msg["role"], content=msg["content"]) for msg in messages]} else: - self.data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]} + data = {"messages": [ChatMessage(role=msg.role, content=msg.content) for msg in messages]} else: raise RuntimeError("prompt and messages are both empty!") - self.data.update(**kwargs) + data.update(**kwargs) + model_response.meta_data["data"] = data def after_call(self, model_response: ModelResponse, @@ -76,24 +78,23 @@ class LlamaIndexGenerationModel(BaseModel): return model_response - def _call(self, stream: bool = False, **kwargs) -> ModelResponse | ModelResponseGen: - assert "prompt" in self.data or "messages" in self.data - results = ModelResponse(m_type=self.m_type) + def _call(self, model_response: ModelResponse, stream: bool = False, **kwargs): + data = model_response.meta_data["data"] - if "prompt" in self.data: + if "prompt" in data: if stream: - response = self.model.stream_complete(**self.data) + model_response.raw = self.model.stream_complete(**data) else: - response = self.model.complete(**self.data) + model_response.raw = self.model.complete(**data) + elif "messages" in data: + if stream: + model_response.raw = self.model.stream_chat(**data) + else: + model_response.raw = self.model.chat(**data) else: - if stream: - response = self.model.stream_chat(**self.data) - else: - response = self.model.chat(**self.data) - results.raw = response - return results + raise RuntimeError("prompt or messages is missing!") - async def _async_call(self, **kwargs) -> ModelResponse: + async def _async_call(self, model_response: ModelResponse, **kwargs): """ Asynchronously calls the language model with the provided prompt or message history, and packages the raw response into a ModelResponse object. @@ -108,13 +109,11 @@ class LlamaIndexGenerationModel(BaseModel): Returns: ModelResponse: An object containing the raw response from the language model. """ - assert "prompt" in self.data or "messages" in self.data - results = ModelResponse(m_type=self.m_type) + data = model_response.meta_data["data"] - if "prompt" in self.data: - response = await self.model.acomplete(**self.data) + if "prompt" in data: + model_response.raw = await self.model.acomplete(**data) + elif "messages" in data: + model_response.raw = await self.model.achat(**data) else: - response = await self.model.achat(**self.data) - - results.raw = response - return results + raise RuntimeError("prompt or messages is missing!") diff --git a/memory_scope/models/llama_index_rank_model.py b/memory_scope/models/llama_index_rank_model.py index dc88621f..62210183 100644 --- a/memory_scope/models/llama_index_rank_model.py +++ b/memory_scope/models/llama_index_rank_model.py @@ -20,26 +20,29 @@ class LlamaIndexRankModel(BaseModel): MODEL_REGISTRY.register("dashscope_rank", DashScopeRerank) - def before_call(self, **kwargs) -> None: + def before_call(self, model_response: ModelResponse, **kwargs): """ Prepares necessary data before the ranking call by extracting the query and documents, ensuring they are valid, and initializing nodes with dummy scores. Args: + model_response: model response **kwargs: Keyword arguments containing 'query' and 'documents'. """ query: str = kwargs.pop("query", "") documents: List[str] = kwargs.pop("documents", []) if isinstance(documents, str): documents = [documents] - - assert query and documents, f"query or documents is empty! query={query}, documents={len(documents)}" + assert query and documents and all(documents), \ + f"query or documents is empty! query={query}, documents={len(documents)}" # Using -1.0 as dummy scores nodes = [NodeWithScore(node=Node(text=doc), score=-1.0) for doc in documents] - self._get_documents_mapping(documents) - self.data = {"nodes": nodes, "query_str": query} + model_response.meta_data.update({ + "data": {"nodes": nodes, "query_str": query}, + "documents_map": {doc: idx for idx, doc in enumerate(documents)}, + }) def after_call(self, model_response: ModelResponse, **kwargs) -> ModelResponse: """ @@ -56,13 +59,14 @@ class LlamaIndexRankModel(BaseModel): if not model_response.rank_scores: model_response.rank_scores = {} + documents_map = model_response.meta_data["documents_map"] for node in model_response.raw: text = node.node.text - idx = self.documents_map[text] + idx = documents_map[text] model_response.rank_scores[idx] = node.score return model_response - def _call(self, **kwargs) -> ModelResponse: + def _call(self, model_response: ModelResponse, **kwargs): """ Executes the ranking process by passing prepared data to the model's postprocessing method. @@ -72,7 +76,7 @@ class LlamaIndexRankModel(BaseModel): Returns: ModelResponse: A response object encapsulating the ranked nodes. """ - return ModelResponse(m_type=self.m_type, raw=self.model.postprocess_nodes(**self.data)) + model_response.raw = self.model.postprocess_nodes(**model_response.meta_data["data"]) async def _async_call(self, **kwargs) -> ModelResponse: """ @@ -84,15 +88,4 @@ class LlamaIndexRankModel(BaseModel): Returns: ModelResponse: A response object encapsulating the ranked nodes. """ - return self._call(**kwargs) - - def _get_documents_mapping(self, documents): - """ - Generates a mapping of each document to its index within the provided document list. - - Args: - documents (List[str]): The list of documents. - """ - self.documents_map = {} - for idx, doc in enumerate(documents): - self.documents_map[doc] = idx + raise NotImplementedError diff --git a/memory_scope/scheme/model_response.py b/memory_scope/scheme/model_response.py index 8e961332..294b6a8a 100644 --- a/memory_scope/scheme/model_response.py +++ b/memory_scope/scheme/model_response.py @@ -26,6 +26,8 @@ class ModelResponse(BaseModel): raw: Any = Field("", description="Raw response from model call") + meta_data: Dict[str, Any] = Field({}, description="meta data for model response") + def __str__(self, max_size=100, **kwargs): result = {} for key, value in self.model_dump().items(): diff --git a/memory_scope/utils/memory_handler.py b/memory_scope/utils/memory_handler.py index 06c9ec97..c218fce3 100644 --- a/memory_scope/utils/memory_handler.py +++ b/memory_scope/utils/memory_handler.py @@ -88,10 +88,28 @@ class MemoryHandler(object): memory_ids: List[str] = self._key_id_dict.get(key.strip()) if memory_ids: - memories.update({x: self._id_memory_dict[x] for x in memory_ids}) + memories.update({x: self._id_memory_dict[x] for x in memory_ids if x in self._id_memory_dict}) return list(memories.values()) + def delete_memories(self, nodes: MemoryNode | List[MemoryNode], key: str = None): + if isinstance(nodes, MemoryNode): + nodes = [nodes] + + for n in nodes: + _id = n.memory_id + if _id in self._id_memory_dict: + self._id_memory_dict.pop(_id, None) + + if key is None: + for _, id_list in self._key_id_dict.items(): + if _id in id_list: + id_list.remove(_id) + else: + id_list = self._key_id_dict[key] + if _id in id_list: + id_list.remove(_id) + def update_memories(self, keys: str = "", nodes: MemoryNode | List[MemoryNode] = None): update_memories: Dict[str, MemoryNode] = {n.memory_id: n for n in self.get_memories(keys=keys)}