From 74238ba9bde8b379d57dd1d39a1def23841d36f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E5=BA=94?= Date: Fri, 30 Jan 2026 16:47:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(memory):=20=E6=B7=BB=E5=8A=A0=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=AE=B0=E5=BF=86=E6=A3=80=E7=B4=A2=E5=92=8C=E6=80=BB?= =?UTF-8?q?=E7=BB=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 PersonalHalumemRetriever 类用于向量搜索检索个人记忆 - 实现 PersonalHalumemSummarizer 类用于两阶段个人记忆处理 - 配置个人记忆检索和总结的系统提示词和用户消息模板 - 支持多阶段检索策略包括意图分解和深度追踪 - 实现记忆评估和原始来源追溯功能 - 添加记忆存储范围过滤和配置管理规则 --- .../personal/personal_halumem_retriever.py | 92 ++++++++++++ .../personal/personal_halumem_retriever.yaml | 91 +++++++++++ .../personal/personal_halumem_summarizer.py | 141 ++++++++++++++++++ .../personal/personal_halumem_summarizer.yaml | 96 ++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 reme/agent/memory/personal/personal_halumem_retriever.py create mode 100644 reme/agent/memory/personal/personal_halumem_retriever.yaml create mode 100644 reme/agent/memory/personal/personal_halumem_summarizer.py create mode 100644 reme/agent/memory/personal/personal_halumem_summarizer.yaml diff --git a/reme/agent/memory/personal/personal_halumem_retriever.py b/reme/agent/memory/personal/personal_halumem_retriever.py new file mode 100644 index 00000000..c2974ead --- /dev/null +++ b/reme/agent/memory/personal/personal_halumem_retriever.py @@ -0,0 +1,92 @@ +"""Personal memory retriever agent for retrieving personal memories through vector search.""" + +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import Role, MemoryType +from ....core.op import BaseTool +from ....core.schema import Message +from ....core.utils import format_messages + + +class PersonalHalumemRetriever(BaseMemoryAgent): + """Retrieve personal memories through vector search and history reading.""" + + memory_type: MemoryType = MemoryType.PERSONAL + + async def build_messages(self) -> list[Message]: + if self.context.get("query"): + context = self.context.query + elif self.context.get("messages"): + context = self.description + "\n" + format_messages(self.context.messages) + else: + raise ValueError("input must have either `query` or `messages`") + + read_all_profiles_tool: BaseTool | None = self.pop_tool("read_all_profiles") + if read_all_profiles_tool is not None: + all_profiles = await read_all_profiles_tool.call( + memory_target=self.memory_target, + service_context=self.service_context, + ) + else: + all_profiles = "" + + return [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt", + memory_type=self.memory_type.value, + memory_target=self.memory_target, + user_profile=all_profiles, + context=context.strip(), + ), + ), + Message( + role=Role.USER, + content=self.prompt_format( + prompt_name="user_message", + memory_type=self.memory_type.value, + memory_target=self.memory_target, + user_profile=all_profiles, + context=context.strip(), + ), + ), + ] + + async def _acting_step( + self, + assistant_message: Message, + tools: list[BaseTool], + step: int, + stage: str = "", + **kwargs, + ) -> tuple[list[BaseTool], list[Message]]: + """Execute tool calls with memory context.""" + return await super()._acting_step( + assistant_message, + tools, + step, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + retrieved_nodes=self.retrieved_nodes, + **kwargs, + ) + + async def execute(self): + result = await super().execute() + answer = result["answer"] + if "MEMORY_NOT_FOUND" in answer: + result["answer"] = "\n".join( + [ + n.format( + include_memory_id=False, + include_when_to_use=False, + include_content=True, + include_message_time=False, + ref_memory_id_key="", + ) + for n in self.retrieved_nodes + ], + ) + + result["retrieved_nodes"] = self.retrieved_nodes + return result diff --git a/reme/agent/memory/personal/personal_halumem_retriever.yaml b/reme/agent/memory/personal/personal_halumem_retriever.yaml new file mode 100644 index 00000000..edd941cc --- /dev/null +++ b/reme/agent/memory/personal/personal_halumem_retriever.yaml @@ -0,0 +1,91 @@ +system_prompt: | + # Role Definition: + You are a Memory Retrieval Agent specialized in retrieving {memory_type} memories about {memory_target}. + + ## Multi-Phase Retrieval Strategy + Follow these phases sequentially to gather comprehensive information: + + ### Tool Rules + **Tool**: `retrieve_memory` (without time constraints) + **Objective**: Cast a wide net to find potentially relevant memories + **Approach**: + - Execute 3-5 diverse search queries using different formulations: + * Original question verbatim + * Rephrased variations (different wording, synonyms) + * Entity-focused queries (extract and search specific names, places, events) + * Keyword-based searches (core concepts, topics) + * Related context queries (broader themes) + - Review all results before proceeding to next phase + + **Tool**: `retrieve_memory` (with time filter) + **When to use**: Only if the user question contains temporal references + **Time Filter Format**: + - Single date: `20200101` + - Date range: `20200101,20200102` (inclusive: 20200101 ≤ time ≤ 20200102) + - Before date: `0,20200102` (up to and including 20200102) + - After date: `20200101,99999999` (from 20200101 onwards) + **Approach**: + - Identify temporal constraints from the user question + - Refine Phase 1 queries with appropriate time filters + - Try multiple time ranges if initial searches yield no results + + **Tool**: `read_history` + **When to use**: After exhausting retrieval attempts OR when specific conversation context is needed + **Approach**: + - Extract `history_id` from retrieved memory references + - Prioritize histories that are most relevant or recent + - Read multiple histories if necessary for complete context + - Use this to understand the full conversation surrounding a memory + + +user_message: | + ## User Profile + {user_profile} + + ## User Question + {context} + + # Core Objective: + Before responding to the user, you must strictly follow the **[Memory Retrieve -> Original Source Tracing -> Broad Search Fallback]** retrieval strategy. It is strictly forbidden to directly opt for an indiscriminate search of massive historical original texts. + + ## Retrieval Strategy & Workflow (Strictly Enforced Chain of Thought) + ### Phase 1: Intent Decomposition and Primary Retrieval (Summary First) + 1. **Analyze Intent**: Analyze the user's current Query, decomposing it into 1-3 core search intents.2. **Summary Priority**: First, retrieve from **high-level memories**. + - **Action**: Call `vector_retrieve_memory` using at least two different `query`. + - **Filter**: (Optional) Set metadata filter {{"timestamp": "YYYY-MM-DD"}} + - **Goal**: Obtain refined conclusions such as entity attributes, task status, user preferences, or environmental information. + + ### Phase 2: Memory Evaluation and Deep Tracing (Drill Down) + Check the retrieval results of Phase 1: + - **Case A (Sufficient Information)**: If the summarized memory contains all the details needed for the answer, proceed directly to Phase 4 for the response. + - **Case B (Vague/Complex Information)**: If summarized memory exists (e.g., 'discussed project architecture') but lacks specific details (e.g., 'specific parameter configuration'), use clues from the summary to trace the original text. + - **Action**: Call `read_history` using ref_memory_id from the retrieved memory. + - **Goal**: Obtain the specific conversation context at that time. + + ### Phase 3: Fallback Retrieval and Strategy Adjustment (Fallback & Expand) + If no valid information is found in both Phase 1 and Phase 2 (result is empty or similarity is too low): Rewrite the Query based on the context (remove non-keywords, synonym substitution), and search again. + + ### Phase 4: Result Compilation and Response + - Combine the retrieved content (summary or original text) with the current conversation context. + - If all retrieved results are irrelevant, **it is strictly forbidden to fabricate memories**; directly inform the user that no relevant information was found. + + ## Output Format + Before the final reply, ensure at least 3 tool calls for retrieval, and then output your answer in ten words. + - Base your answer EXCLUSIVELY on retrieved memories, user profile, and history data + - Never infer, assume, or hallucinate information + - Always cite sources with timestamps: `[timestamp] Memory content` + - Present conflicting information transparently with respective timestamps + - Exhaust all search strategies before concluding information doesn't exist + + Before the final reply, ensure at least 3 tool calls for retrieval, and then output the most relevant JSON retrieval summary, followed by your answer: + ```json + {{ + "retrieved_memories": [ + {{"type": "profile", "timestamp":"...", "content": "..."}}, + {{"type": "personal", "timestamp":"...", "content": "..."}}, + {{"type": "history", "timestamp":"...", "content": "..."}}, + .... + ], + "summary": "Fill in your summarized answer here." + }} + ``` diff --git a/reme/agent/memory/personal/personal_halumem_summarizer.py b/reme/agent/memory/personal/personal_halumem_summarizer.py new file mode 100644 index 00000000..22e4265c --- /dev/null +++ b/reme/agent/memory/personal/personal_halumem_summarizer.py @@ -0,0 +1,141 @@ +"""Personal memory summarizer agent for two-phase personal memory processing.""" + +from loguru import logger + +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import Role, MemoryType +from ....core.op import BaseTool +from ....core.schema import Message + + +class PersonalHalumemSummarizer(BaseMemoryAgent): + """Two-phase personal memory processor: retrieve/add memories then update profile.""" + + memory_type: MemoryType = MemoryType.PERSONAL + + async def _build_s1_messages(self) -> list[Message]: + return [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt_s1", + context=self.context.history_node.content, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + ), + ), + Message( + role=Role.USER, + # content=self.get_prompt("user_message_s1"), + content=self.prompt_format( + prompt_name="user_message_s1", + context=self.context.history_node.content, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + ), + ), + ] + + async def _build_s2_messages(self, user_profile: str) -> list[Message]: + return [ + Message( + role=Role.SYSTEM, + content=self.prompt_format( + prompt_name="system_prompt_s2", + context=self.context.history_node.content, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + user_profile=user_profile, + ), + ), + Message( + role=Role.USER, + content=self.prompt_format( + prompt_name="user_message_s2", + context=self.context.history_node.content, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + user_profile=user_profile, + ), + ), + ] + + async def _acting_step( + self, + assistant_message: Message, + tools: list[BaseTool], + step: int, + stage: str = "", + **kwargs, + ) -> tuple[list[BaseTool], list[Message]]: + """Execute tool calls with memory context.""" + return await super()._acting_step( + assistant_message, + tools, + step, + stage=stage, + memory_type=self.memory_type.value, + memory_target=self.memory_target, + history_node=self.history_node, + author=self.author, + retrieved_nodes=self.retrieved_nodes, + **kwargs, + ) + + async def execute(self): + memory_tools = [] + profile_tools = [] + for i, tool in enumerate(self.tools): + tool_name = tool.tool_call.name + if "_memory" in tool_name: + memory_tools.append(tool) + elif "_profile" in tool_name: + profile_tools.append(tool) + else: + raise ValueError(f"[{self.__class__.__name__}] unknown tool_name={tool_name}") + logger.info(f"[{self.__class__.__name__}] tool_call[{i}]={tool.tool_call.simple_input_dump(as_dict=False)}") + + stage = "s1-memory" + messages_s1 = await self._build_s1_messages() + for i, message in enumerate(messages_s1): + role = message.name or message.role + logger.info(f"[{self.__class__.__name__} {stage}] role={role} {message.simple_dump(as_dict=False)}") + tools_s1, messages_s1, success_s1 = await self.react(messages_s1, memory_tools, stage=stage) + + if profile_tools: + + read_all_profiles_tool: BaseTool | None = self.pop_tool("read_all_profiles") + if read_all_profiles_tool is not None: + all_profiles = await read_all_profiles_tool.call( + memory_target=self.memory_target, + service_context=self.service_context, + ) + else: + all_profiles = "" + + + stage = "s2-profile" + messages_s2 = await self._build_s2_messages(user_profile = all_profiles) + for i, message in enumerate(messages_s2): + role = message.name or message.role + logger.info(f"[{self.__class__.__name__} {stage}] role={role} {message.simple_dump(as_dict=False)}") + tools_s2, messages_s2, success_s2 = await self.react(messages_s2, profile_tools, stage=stage) + else: + tools_s2, messages_s2, success_s2 = [], [], True + + answer = (messages_s1[-1].content if success_s1 else "") + (messages_s2[-1].content if success_s2 else "") + success = success_s1 and success_s2 + messages = messages_s1 + messages_s2 + tools = tools_s1 + tools_s2 + memory_nodes = [] + for tool in tools: + if tool.memory_nodes: + memory_nodes.extend(tool.memory_nodes) + + return { + "answer": answer, + "success": success, + "messages": messages, + "tools": tools, + "memory_nodes": memory_nodes, + } diff --git a/reme/agent/memory/personal/personal_halumem_summarizer.yaml b/reme/agent/memory/personal/personal_halumem_summarizer.yaml new file mode 100644 index 00000000..659590f2 --- /dev/null +++ b/reme/agent/memory/personal/personal_halumem_summarizer.yaml @@ -0,0 +1,96 @@ +system_prompt_s1: | + You are a Memory Agent responsible for managing {memory_type} memories about {memory_target}. + + ## Tool Rules + 1. `add_and_retrieve_similar_memory`: Create a memory in the vector store. + - Use this tool to add memories, and it will return the relevant content related to the added memories. + - Use actual names from the conversation (e.g., "Bob likes apples") instead of generic references (e.g., "user likes apples") + - The tool will retrieve similar historical memories via vector search to help you consolidate in Step 2 + + 2. `update_memory`: Update memories in the vector store. + **What to Delete** (via `memory_ids_to_delete`): + - Duplicate memories with identical or highly similar content + - Memories that should be merged into a single consolidated entry + + **What to Add** (via `memories_to_add` with message_time and memory_content): + - For each topic with changes: add ONE consolidated memory that merges related information + - New distinct memories that don't overlap with existing ones + - Updated memories that capture the latest state while preserving temporal evolution + +user_message_s1: | + ## Latest Conversation + Format: round [] : + {context} + + ## Task + ### Step 1: Create Memory + - At this step, you can call the tool multiple times to store memories, or you can call it once to store multiple memories. + + ### Step 2: Update Memory Store + - Update the vector store using `update_memory` to keep it well-organized and consolidated. + + ## Storage Scope (Biographical & Behavioral ONLY) + - Personal Biography: Significant milestones, past experiences, and life events. + - Behavioral Patterns: How the agent reacts, specific actions taken, and recurring habits. + - **EXCLUSION**: DO NOT record objective world facts, general knowledge, or user-specific health/states. + + Extraction & Formatting Rules + - Fact Filtering: Only extract information that builds the biography of **{memory_target}**. + - Subject Splitting: If a conversation mentions multiple subject (e.g., the User's childhood and their Father's career), create separate memory entries for each subject. + - Atomic Content: Each entry should focus on one specific event or trait. Keep descriptions concise to ensure efficient retrieval. + + +system_prompt_s2: | + You are a Profile Agent responsible for managing profiles about {memory_target}. + + ## Tool Rules + 1. Update Profile with `update_profile` + Synchronize profile with new information from the conversation: + - `profile_ids_to_delete`: Remove conflicting, or redundant entries (array of profile IDs). + - `profiles_to_add`: + - `conversation_time`: Time of conversation (format: `YYYY-MM-DD HH:MM:SS`, e.g., `2024-01-15 14:30:00`) + - `profile_content`: Complete, self-contained profile description with full context + Update user profile using `update_profile` based on the conversation and current profile. + + +user_message_s2: | + You are a memory agent managing **{memory_type}** memories about **{memory_target}**. + + ## Latest Conversation: + {context} + + Message format: `round [] : ` (timestamp: YYYY-MM-DD HH:MM:SS). + + **CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate. + + ## Current User Profile: + {user_profile} + + ## Task + ### Step 1: ADD Profile + - Add new, relevant, and up-to-date information to the user profile using `update_profile` (via `profiles_to_add`). + + ### Step 2: DELETE Profile + - Delete outdated, redundant, or resolved states from the user profile using `update_profile` (via `profile_ids_to_delete`). + + ## Storage Scope (Current States ONLY) + - **EXCLUSION PRINCIPLE**: DO NOT record any user *actions*, *requests*, *queries*, or *interactions with the system* (e.g., "asked for code", "solved a puzzle", "requested translation"). These are interaction logs, not user states. + - Identity: Geography, job title, work content, income. + - Background: Education, family, relationships, hobbies, interests, and other personal preferences. + - Temporary States: Physical health (e.g., "Has a cold"), emotional mood, stress levels, and specific prohibitions (e.g., "Cannot drink alcohol due to medication"). + + ## Profile Management Rules + - Subject Splitting (CRITICAL): If the conversation mentions multiple subjects (e.g., the User's job and their Spouse's health), you MUST create separate profile entries for each unique subject. + - Conflict Resolution: Use profile_ids_to_delete to remove outdated, redundant, or resolved states (e.g., if a user is "Recovered," delete the "Illness" entry). + - Each profile entry MUST describe a **persistent or temporary state of the user themselves** (e.g., who they are, what they like, what they’re dealing with), NOT an event they participated in or a request they made. + + ## Profile Format + - **ONLY record what the user EXPLICITLY STATES about themselves as a state or preference.** + - The key in the record represents the category of memory, and the value should record the specific content. For example: + {{"message_time": "YYYY-MM-DD HH:MM:SS", "profile_key": "the category of memory", "profile_value": "content" }} + - When there is no information conflict or outdated information, you don't need to delete any of the memory. If there is no information that meets the requirements, it is also acceptable not to add it. + + ## Forbidden Case + 1.There is no need to record user behavior: {{ "profile_key": "workouts", "profile_content": "confident in new running shoes' suitability for chosen route; they have significantly improved morning jogs"}} + 2. There is no need to record the users' plans or requirements: {{ "profile_key": "plans.vacation", "profile_content": "planning to go to a nearby city for a week and ask for a job change"}} +