From e63a3fa6326471f17f07e092abc544fe438b63ce Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Tue, 27 Jan 2026 01:07:39 +0800 Subject: [PATCH 01/17] feat(core): add simple request methods and improve memory management --- .../halumem/eval_reme.py | 299 +++++++--- benchmark/halumem/eval_reme.yaml | 548 ++++++++++++++++++ reme/agent/memory/base_memory_agent.py | 7 + .../memory/default/personal_retriever.py | 12 +- .../memory/default/personal_summarizer.py | 21 +- .../memory/default/personal_summarizer.yaml | 8 +- reme/agent/memory/default/reme_retriever.py | 14 +- reme/agent/memory/default/reme_summarizer.py | 12 +- reme/core/llm/base_llm.py | 38 ++ reme/core/op/base_react.py | 2 +- reme/core/utils/llm_utils.py | 17 +- reme/reme.py | 240 +++++++- reme/tool/memory/base_memory_tool.py | 10 +- reme/tool/memory/history/read_history.py | 7 +- .../memory/user_profile/read_user_profile.py | 9 +- .../user_profile/update_user_profile.py | 15 +- reme/tool/memory/vector/add_memory.py | 2 +- reme/tool/memory/vector/retrieve_memory.py | 1 - tests/test_reme.py | 14 +- 19 files changed, 1116 insertions(+), 160 deletions(-) rename bench_old/halumem/eval_reme_simple_v4.py => benchmark/halumem/eval_reme.py (74%) create mode 100644 benchmark/halumem/eval_reme.yaml diff --git a/bench_old/halumem/eval_reme_simple_v4.py b/benchmark/halumem/eval_reme.py similarity index 74% rename from bench_old/halumem/eval_reme_simple_v4.py rename to benchmark/halumem/eval_reme.py index c68ee4d1..50e8b925 100644 --- a/bench_old/halumem/eval_reme_simple_v4.py +++ b/benchmark/halumem/eval_reme.py @@ -8,7 +8,7 @@ A modular evaluation pipeline that: 4. Generates comprehensive metrics Usage: - python bench/halumem/eval_reme_simple_v4.py \ + python benchmark/halumem/eval_reme.py \ --data_path /Users/yuli/workspace/HaluMem/data/HaluMem-Medium.jsonl \ --top_k 20 --user_num 100 --max_concurrency 20 """ @@ -26,14 +26,13 @@ from typing import Any from loguru import logger -from eval_tools import evaluation_for_question2, answer_question_with_memories -from reme_ai.core.enumeration import MemoryType -from reme_ai.core.schema import MemoryNode -from reme_ai.reme import ReMe +from reme.core.schema import MemoryNode, Message +from reme.reme import ReMe # ==================== Configuration ==================== + @dataclass class EvalConfig: """Evaluation configuration parameters.""" @@ -42,7 +41,7 @@ class EvalConfig: user_num: int = 1 max_concurrency: int = 2 batch_size: int = 20 - output_dir: str = "bench_results/reme_simple_v4" + output_dir: str = "bench_results/reme" # ==================== Utilities ==================== @@ -138,7 +137,7 @@ class FileManager: """Check if user has cached results.""" user_dir = self.get_user_dir(user_name) return any(f.name.startswith("session_") and f.suffix == ".json" - for f in user_dir.iterdir()) + for f in user_dir.iterdir()) def combine_results(self, output_file: str): """Combine all user session files into a single JSONL file.""" @@ -177,6 +176,94 @@ class FileManager: f_out.write(json.dumps(user_data, ensure_ascii=False) + "\n") +# ==================== Evaluation Functions ==================== + +async def answer_question_with_memories( + reme: ReMe, + question: str, + memories: str, + user_id: str = None, + model_name: str = "qwen3-30b-a3b-instruct-2507" +): + """ + Answer a question using retrieved memories with PROMPT_MEMZERO_JSON template. + + Args: + reme: ReMe instance with default_llm and prompt_handler + question: The question to answer + memories: The retrieved memories (formatted as context) + user_id: Optional user ID for context formatting + model_name: Model name to use for LLM request + + Returns: + dict with 'reasoning' and 'answer' fields + """ + # Format context with memories + if user_id: + context = reme.prompt_handler.prompt_format( + "TEMPLATE_MEMOS", + user_id=user_id, + memories=memories + ) + else: + context = f"Memories:\n{memories}" + + # Use PROMPT_MEMZERO_JSON template for structured JSON response + prompt = reme.prompt_handler.prompt_format( + "PROMPT_MEMZERO_JSON", + context=context, + question=question + ) + + result = await reme.llm.simple_request_for_json( + prompt=prompt, + model_name=model_name + ) + + return result + + +async def evaluation_for_question( + reme: ReMe, + question: str, + reference_answer: str, + key_memory_points: str, + response: str, + dialogue: str = None, + model_name: str = "qwen3-max" +): + """ + Question-Answering Evaluation with optional Dialogue Context. + + Args: + reme: ReMe instance with default_llm and prompt_handler + question: The question string to be evaluated. + reference_answer: The reference (gold-standard) answer. + key_memory_points: The memory points used to derive the reference answer. + response: The answer produced by the memory system. + dialogue: Optional formatted dialogue history (role, content, time_created). + model_name: Model name to use for LLM request + + Returns: + dict with 'reasoning' and 'evaluation_result' fields + """ + prompt = reme.prompt_handler.prompt_format( + "EVALUATION_PROMPT_FOR_QUESTION", + question=question, + reference_answer=reference_answer, + key_memory_points=key_memory_points, + response=response, + dialogue=dialogue if dialogue else "" + ) + + result = await reme.llm.simple_request_for_json( + prompt=prompt, + model_name=model_name + ) + + return result + + # ==================== Memory Operations ==================== class MemoryProcessor: @@ -186,59 +273,48 @@ class MemoryProcessor: self.reme = reme async def add_memories( - self, - user_id: str, - messages: list[dict], - batch_size: int = 10000 - ) -> tuple[list[str], list[list[dict]], float]: + self, + user_id: str, + messages: list[dict], + batch_size: int = 10000 + ) -> tuple[list[str], list, float]: """ Add memories in batches using ReMe and return extracted memory contents. Returns: tuple: (extracted_memories, agent_messages, total_duration_ms) """ - added_memories: list[MemoryNode] = [] - deleted_memories: list[str] = [] - all_agent_messages: list = [] + extracted_memories = [] + summary_messages = [] total_duration_ms = 0 for i in range(0, len(messages), batch_size): batch = messages[i:i + batch_size] start = time.time() - memory_nodes, agent_messages, success = await self.reme.summary_v4( + # Use new summary API + result = await self.reme.summary_memory( messages=batch, - user_id=user_id + user_name=user_id, + version="default", + return_dict=True, ) duration_ms = (time.time() - start) * 1000 total_duration_ms += duration_ms - # Save agent messages for this batch - if agent_messages: - all_agent_messages.extend(agent_messages) + memory_nodes: list[MemoryNode] = result["answer"] + messages: list[Message] = result["messages"] + extracted_memories.extend([m.model_dump_json(exclude_none=True) for m in memory_nodes]) + summary_messages.extend([m.simple_dump() for m in messages]) - if memory_nodes: - for node in memory_nodes: - if isinstance(node, MemoryNode) and node.memory_type == MemoryType.HISTORY: - continue - - if isinstance(node, MemoryNode): - added_memories.append(node) - - if isinstance(node, str): - deleted_memories.append(node) - - extracted_memories = deleted_memories - extracted_memories += ["[delete]" + n.format_memory() for n in added_memories if n.memory_id in deleted_memories] - extracted_memories += ["[add]" + n.format_memory() for n in added_memories if n.memory_id not in deleted_memories] - return extracted_memories, all_agent_messages, total_duration_ms + return extracted_memories, messages, total_duration_ms async def search_memory( - self, - query: str, - user_id: str, - top_k: int = 20 + self, + query: str, + user_id: str, + top_k: int = 20 ) -> tuple[dict, list, float]: """ Search memory using ReMe and return structured answer with reasoning. @@ -249,25 +325,34 @@ class MemoryProcessor: """ start = time.time() - # Retrieve memories from ReMe - memories_response, agent_messages, success = await self.reme.retrieve_v4( + # Retrieve memories from ReMe using new API + result = await self.reme.retrieve_memory( query=query, - user_id=user_id, - top_k=top_k + top_k=top_k, + user_name=user_id, + version="default", + return_dict=True, ) + # Extract memories from response + memories = result["answer"] + messages = [x.model_dump_json(exclude_none=True) for x in result["messages"]] + retrieved_nodes = [x.model_dump_json(exclude_none=True) for x in result["retrieved_nodes"]] + # Use LLM to generate structured answer from memories answer_result = await answer_question_with_memories( + reme=self.reme, question=query, - memories=memories_response, + memories=memories, user_id=user_id ) # Add original memories to the result - answer_result["memories"] = memories_response + answer_result["memories"] = memories + answer_result["retrieved_nodes"] = retrieved_nodes duration_ms = (time.time() - start) * 1000 - return answer_result, agent_messages, duration_ms + return answer_result, messages, duration_ms # ==================== Evaluation ==================== @@ -275,17 +360,18 @@ class MemoryProcessor: class QuestionAnsweringEvaluator: """Evaluates question answering performance.""" - def __init__(self, memory_processor: MemoryProcessor, top_k: int): + def __init__(self, memory_processor: MemoryProcessor, reme: ReMe, top_k: int): self.memory_processor = memory_processor + self.reme = reme self.top_k = top_k async def evaluate_questions( - self, - questions: list[dict], - user_name: str, - uuid: str, - session_id: int, - formatted_dialogue: str + self, + questions: list[dict], + user_name: str, + uuid: str, + session_id: int, + formatted_dialogue: str ) -> list[dict]: """Evaluate all questions for a session.""" results = [] @@ -301,15 +387,17 @@ class QuestionAnsweringEvaluator: system_answer = answer_dict.get("answer", "") system_reasoning = answer_dict.get("reasoning", "") retrieved_memories = answer_dict.get("memories", "") + retrieved_nodes = answer_dict.get("retrieved_nodes", "") # Evaluate response evidence_text = "\n".join([e["memory_content"] for e in qa["evidence"]]) - eval_result = await evaluation_for_question2( - qa["question"], - qa["answer"], - evidence_text, - system_answer, - formatted_dialogue + eval_result = await evaluation_for_question( + reme=self.reme, + question=qa["question"], + reference_answer=qa["answer"], + key_memory_points=evidence_text, + response=system_answer, + dialogue=formatted_dialogue ) # Build result record @@ -320,7 +408,8 @@ class QuestionAnsweringEvaluator: "system_response": system_answer, "system_reasoning": system_reasoning, "retrieved_memories": retrieved_memories, - "retrieve_messages": [m.model_dump() for m in agent_messages], + "retrieved_nodes": retrieved_nodes, + "retrieve_messages": agent_messages, "search_duration_ms": duration_ms, "result_type": eval_result.get("evaluation_result"), "question_answering_reasoning": eval_result.get("reasoning", "") @@ -418,25 +507,50 @@ class MetricsAggregator: # ==================== Main Pipeline ==================== -class HaluMemEvaluatorV4: +class HaluMemEvaluator: + """HaluMem evaluator with proper resource management.""" def __init__(self, config: EvalConfig): self.config = config self.reme = ReMe() + + # Load evaluation prompts into ReMe's prompt handler + prompts_yaml_path = Path(__file__).parent / "eval_reme.yaml" + self.reme.prompt_handler.load_prompt_by_file(prompts_yaml_path) + self.file_manager = FileManager(config.output_dir) self.memory_processor = MemoryProcessor(self.reme) self.qa_evaluator = QuestionAnsweringEvaluator( self.memory_processor, + self.reme, config.top_k ) self.data_loader = DataLoader() + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit with cleanup.""" + await self.reme.close() + return False + + def __enter__(self): + """Sync context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Sync context manager exit with cleanup.""" + self.reme.close_sync() + return False + async def process_session( - self, - session: dict, - session_id: int, - user_name: str, - uuid: str + self, + session: dict, + session_id: int, + user_name: str, + uuid: str ) -> dict: """Process a single session using ReMe.""" session_data = { @@ -463,7 +577,7 @@ class HaluMemEvaluatorV4: session_data.update({ "dialogue": dialogue, "extracted_memories": extracted_memories, - "summary_messages": [m.model_dump() for m in agent_messages], + "summary_messages": agent_messages, "add_dialogue_duration_ms": duration_ms }) @@ -509,6 +623,19 @@ class HaluMemEvaluatorV4: """Run the complete evaluation pipeline using ReMe.""" start_time = time.time() + # Load user data first to get user names + all_users = self.data_loader.load_jsonl(self.config.data_path) + users_to_process = all_users[:self.config.user_num] + + # Extract all user names and delete all profiles + all_user_names = [ + self.data_loader.extract_user_name(user_data["persona_info"]) + for user_data in all_users + ] + if all_user_names: + await self.reme.delete_all_profiles(all_user_names) + logger.info(f"Deleted all profiles for {len(all_user_names)} users") + # Clear existing data await self.reme.vector_store.delete_all() @@ -519,10 +646,6 @@ class HaluMemEvaluatorV4: logger.info(f"Cleared meta_memory directory: {meta_memory_path}") meta_memory_path.mkdir(parents=True, exist_ok=True) - # Load user data - all_users = self.data_loader.load_jsonl(self.config.data_path) - users_to_process = all_users[:self.config.user_num] - print("\n" + "=" * 80) print("HALUMEM EVALUATION - REME - QUESTION ANSWERING") print(f"Users: {len(users_to_process)} | Concurrency: {self.config.max_concurrency}") @@ -631,13 +754,13 @@ class HaluMemEvaluatorV4: # ==================== Entry Point ==================== -def main( - data_path: str, - top_k: int = 20, - user_num: int = 1, - max_concurrency: int = 2 +async def main_async( + data_path: str, + top_k: int, + user_num: int, + max_concurrency: int ): - """Main entry point for ReMe evaluation.""" + """Main async entry point for ReMe evaluation with proper resource cleanup.""" config = EvalConfig( data_path=data_path, top_k=top_k, @@ -645,8 +768,24 @@ def main( max_concurrency=max_concurrency ) - evaluator = HaluMemEvaluatorV4(config) - asyncio.run(evaluator.run_evaluation()) + # Use async context manager for automatic cleanup + async with HaluMemEvaluator(config) as evaluator: + await evaluator.run_evaluation() + + +def main( + data_path: str, + top_k: int, + user_num: int, + max_concurrency: int +): + """Main entry point for ReMe evaluation.""" + asyncio.run(main_async( + data_path=data_path, + top_k=top_k, + user_num=user_num, + max_concurrency=max_concurrency + )) if __name__ == "__main__": @@ -676,7 +815,7 @@ if __name__ == "__main__": parser.add_argument( "--max_concurrency", type=int, - default=2, + default=100, help="Maximum concurrent user processing (default: 2)" ) diff --git a/benchmark/halumem/eval_reme.yaml b/benchmark/halumem/eval_reme.yaml new file mode 100644 index 00000000..56dde2db --- /dev/null +++ b/benchmark/halumem/eval_reme.yaml @@ -0,0 +1,548 @@ +TEMPLATE_MEMOS: | + Memories for user {user_id}: + {memories} + +PROMPT_MEMZERO_JSON: | + # CONTEXT: + {context} + + # CONTEXT PRIORITY: + When the context contains information from multiple sources, follow this strict priority order: + 1. **Historical Dialogue** (highest priority) - Direct conversation content + 2. **Extracted Memories** (medium priority) - Summarized memory points + 3. **User Profile** (lowest priority) - General user information + + # Question: + {question} + + # OUTPUT FORMAT: + Do not hallucinate; strictly answer the user's question based on the content of the CONTEXT. + Please provide your response in the following JSON format: + + ```json + {{ + "reasoning": "reasoning content", + "answer": "Provide a detailed answer" + }} + ``` + +PROMPT_MEMZERO_JSON2: | + # CONTEXT: + {context} + + # CONTEXT PRIORITY: + When the context contains information from multiple sources, follow this strict priority order: + 1. **Historical Dialogue** (highest priority) - Direct conversation content + 2. **Extracted Memories** (medium priority) - Summarized memory points + 3. **User Profile** (lowest priority) - General user information + + # Question: + {question} + + # OUTPUT FORMAT: + Do not hallucinate; strictly answer the user's question based on the content of the CONTEXT. + Please provide your response in the following JSON format: + + ```json + {{ + "reasoning": "reasoning content", + "answer": "Provide a detailed answer" + }} + ``` + +PROMPT_MEMZERO: | + You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. + + # CONTEXT: + You have access to memories from two speakers in a conversation. These memories contain + timestamped information that may be relevant to answering the question. + + # INSTRUCTIONS: + 1. Carefully analyze all provided memories from both speakers + 2. Pay special attention to the timestamps to determine the answer + 3. If the question asks about a specific event or fact, look for direct evidence in the memories + 4. If the memories contain contradictory information, prioritize the most recent memory + 5. If there is a question about time references (like "last year", "two months ago", etc.), + calculate the actual date based on the memory timestamp. For example, if a memory from + 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. + 6. Always convert relative time references to specific dates, months, or years. For example, + convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory + timestamp. Ignore the reference while answering the question. + 7. Focus only on the content of the memories from both speakers. Do not confuse character + names mentioned in memories with the actual users who created those memories. + 8. The answer should be less than 5-6 words. + + # APPROACH (Think step by step): + 1. First, examine all memories that contain information related to the question + 2. Examine the timestamps and content of these memories carefully + 3. Look for explicit mentions of dates, times, locations, or events that answer the question + 4. If the answer requires calculation (e.g., converting relative time references), show your work + 5. Formulate a precise, concise answer based solely on the evidence in the memories + 6. Double-check that your answer directly addresses the question asked + 7. Ensure your final answer is specific and avoids vague time references + + {context} + + Question: {question} + + Answer: + +PROMPT_ZEP: | + You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. + + # CONTEXT: + You have access to memories from a conversation. These memories contain + timestamped information that may be relevant to answering the question. + + # INSTRUCTIONS: + 1. Carefully analyze all provided memories + 2. Pay special attention to the timestamps to determine the answer + 3. If the question asks about a specific event or fact, look for direct evidence in the memories + 4. If the memories contain contradictory information, prioritize the most recent memory + 5. If there is a question about time references (like "last year", "two months ago", etc.), + calculate the actual date based on the memory timestamp. For example, if a memory from + 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. + 6. Always convert relative time references to specific dates, months, or years. For example, + convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory + timestamp. Ignore the reference while answering the question. + 7. Focus only on the content of the memories. Do not confuse character + names mentioned in memories with the actual users who created those memories. + 8. The answer should be less than 5-6 words. + + # APPROACH (Think step by step): + 1. First, examine all memories that contain information related to the question + 2. Examine the timestamps and content of these memories carefully + 3. Look for explicit mentions of dates, times, locations, or events that answer the question + 4. If the answer requires calculation (e.g., converting relative time references), show your work + 5. Formulate a precise, concise answer based solely on the evidence in the memories + 6. Double-check that your answer directly addresses the question asked + 7. Ensure your final answer is specific and avoids vague time references + + Context: + + {context} + + Question: {question} + Answer: + +PROMPT_MEMOS: | + You are a knowledgeable and helpful AI assistant. + + # CONTEXT: + You have access to memories from two speakers in a conversation. These memories contain + timestamped information that may be relevant to answering the question. + + # INSTRUCTIONS: + 1. Carefully analyze all provided memories. Synthesize information across different entries if needed to form a complete answer. + 2. Pay close attention to the timestamps to determine the answer. If memories contain contradictory information, the **most recent memory** is the source of truth. + 3. If the question asks about a specific event or fact, look for direct evidence in the memories. + 4. Your answer must be grounded in the memories. However, you may use general world knowledge to interpret or complete information found within a memory (e.g., identifying a landmark mentioned by description). + 5. If the question involves time references (like "last year", "two months ago", etc.), you **must** calculate the actual date based on the memory's timestamp. For example, if a memory from 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. + 6. Always convert relative time references to specific dates, months, or years in your final answer. + 7. Do not confuse character names mentioned in memories with the actual users who created them. + 8. The answer must be brief (under 5-6 words) and direct, with no extra description. + + # APPROACH (Think step by step): + 1. First, examine all memories that contain information related to the question. + 2. Synthesize findings from multiple memories if a single entry is insufficient. + 3. Examine timestamps and content carefully, looking for explicit dates, times, locations, or events. + 4. If the answer requires calculation (e.g., converting relative time references), perform the calculation. + 5. Formulate a precise, concise answer based on the evidence from the memories (and allowed world knowledge). + 6. Double-check that your answer directly addresses the question asked and adheres to all instructions. + 7. Ensure your final answer is specific and avoids vague time references. + + {context} + + Question: {question} + + Answer: + +PROMPT_MEMOBASE: | + You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. + + # CONTEXT: + You have access to memories from two speakers in a conversation. These memories contain + timestamped information that may be relevant to answering the question. + + # INSTRUCTIONS: + 1. Carefully analyze all provided memories from both speakers + 2. Pay special attention to the timestamps to determine the answer + 3. If the question asks about a specific event or fact, look for direct evidence in the memories + 4. If the memories contain contradictory information, prioritize the most recent memory + 5. If there is a question about time references (like "last year", "two months ago", etc.), calculate the actual date based on the memory timestamp. For example, if a memory from 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. + 6. Always convert relative time references to specific dates, months, or years. For example, convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory timestamp. Ignore the reference while answering the question. + 7. Focus only on the content of the memories from both speakers. Do not confuse character names mentioned in memories with the actual users who created those memories. + 8. The answer should be less than 5-6 words. + + # APPROACH (Think step by step): + 1. First, examine all memories that contain information related to the question + 2. Examine the timestamps and content of these memories carefully + 3. Look for explicit mentions of dates, times, locations, or events that answer the question + 4. If the answer requires calculation (e.g., converting relative time references), show your work + 5. Formulate a precise, concise answer based solely on the evidence in the memories + 6. Double-check that your answer directly addresses the question asked + 7. Ensure your final answer is specific and avoids vague time references + + {context} + + Question: {question} + + Answer: + + +EVALUATION_PROMPT_FOR_MEMORY_INTEGRITY: | + You are a strict **"Memory Integrity" evaluator**. + Your core task is to assess whether an AI memory system has **missed any key memory points** after processing a conversation. This evaluation measures the system’s **memory integrity**, i.e., its ability to resist **amnesia** or **omission**. + + # Evaluation Context & Data: + + 1. **Extracted Memories:** + These are all the memory items actually extracted by the memory system. + {memories} + + 2. **Expected Memory Point:** + The key memory point that *should* have been extracted. + {expected_memory_point} + + # Evaluation Instructions: + + 1. For each **Expected Memory Point**, search within the **Extracted Memories** list for corresponding or related information. Ignore unrelated items. + 2. Based on the following scoring rubric, rate how well the memory system captured the **Expected Memory Point** and provide a detailed explanation. + + # Scoring Rubric: + + * **2:** Fully covered or implied. + One or more items in “Extracted Memories” fully cover or logically imply all information in the “Expected Memory Point.” + + * **1:** Partially covered or mentioned. + Some information in “Extracted Memories” mentions part of the “Expected Memory Point,” but key information is missing, inaccurate, or slightly incorrect. + + * **0:** Not mentioned or incorrect. + “Extracted Memories” contains no mention of the “Expected Memory Point,” or the corresponding information is entirely wrong. + + # Scoring Notes: + + * For **compound Expected Memory Points** (with multiple elements such as person/event/time/location/preference, etc.): + + * All elements correct → **2 points** + * Some elements correct / uncertain → **1 point** + * Key elements missing or wrong → **0 points** + + * Semantic matching is acceptable; exact wording is **not** required. + + * If “Extracted Memories” contains **conflicting information**, assign the **best possible coverage score** and mention the conflict in your reasoning. + + * Extra or stylistically different memories do **not** reduce the score; only the coverage of the **Expected Memory Point** matters. + + * For uncertain wording (“might,” “probably,” “tends to,” etc.): + + * If the Expected Memory Point is a definite statement, usually assign **1 point**. + + * If critical fields (e.g., time, entity name, relationship) are partly wrong but others match → **1 point**. + + * If all key fields are wrong or missing → **0 points**. + + # Output Format: + + Please output your result in the following JSON format: + + ```json + {{ + "reasoning": "Provide a concise justification for the score", + "score": "2|1|0" + }} + ``` + +EVALUATION_PROMPT_FOR_MEMORY_ACCURACY: | + You are a **Dialogue Memory Accuracy Evaluator.** Your task is to evaluate the **accuracy** of a memory extracted by an AI memory system, based on three given inputs: the dialogue content, the *target (gold)* memory points (the correct annotated memories), and the *candidate* memory to be evaluated. The goal is to output a **structured evaluation result**. + + # Input Content + + * **Dialogue:** + {dialogue} + + * **Golden Memories (Target Memory Points):** + The correct memory points pre-annotated for this dialogue in the evaluation dataset. + {golden_memories} + + * **Candidate Memory:** + The memory extracted by the system to be evaluated. + {candidate_memory} + + # Evaluation Principles and Definitions + + ### 1) Support / Entailment + + * An **information point** (atomic fact) in the candidate memory is considered *supported* if it can be directly stated or semantically entailed (via synonym, paraphrase, or equivalent expression) by the *Dialogue* or *Golden Memories*. + * Only the given dialogue and golden memories can be used for judgment — **no external knowledge** or assumptions are allowed. + Any information not appearing in or inferable from these two sources is considered *unsupported*. + * Pay careful attention to **negation**, **quantities**, **time**, and **subjects**. + If the candidate statement contradicts the dialogue or golden memories, it is considered a **conflict**. + + ### 2) Memory Accuracy Score (integer: 0 / 1 / 2) + + * **2 points:** Every information point in the candidate memory is supported by the dialogue or golden memories, with **no contradictions or hallucinations**. + * **1 point:** The candidate memory is *partially correct* (at least one supported information point) but also includes *unsupported* or *contradictory* content. + * **0 points:** The candidate memory is **entirely unsupported or contradictory** to the sources (i.e., a “hallucinated memory”). + + > Note: + > + > * If a candidate memory contains multiple information points, **any unsupported or contradictory element** prevents a full score (2). + > * If both supported and unsupported/conflicting content appear, assign a score of **1**. + + ### 3) Inclusion in Golden Memories (Boolean field-level judgment) + + **Definition:** + + * **Atomic information point:** the smallest factual unit in the candidate memory (e.g., *name = Li Si*, *age = 25*, *location = Beijing*, *preference = coffee*, *budget ≤ 2000*, *meeting_time = Wednesday 10:00*, *tool = Zoom*, etc.). + * **Field / Slot:** the semantic dimension of an information point (e.g., *name*, *age*, *residence*, *food preference*, *budget*, *meeting time*, *meeting tool*, etc.). + + **Judgment Rules (independent of correctness):** + + * **true:** + Every atomic information point in the candidate memory has a corresponding **field** in the golden memories (allowing for synonyms, paraphrases, or equivalent expressions; ignore value, polarity, or quantity differences). + + * Note: A single field in the gold list may match multiple candidate points (e.g., multiple “drink preference” facts can be covered by one “drink preference” field in gold). + * **false:** + If **any** atomic information point’s field in the candidate memory cannot be found in the golden memories, mark as *false*. + + **Important Notes:** + + * Field matching is restricted to fields that are **explicitly present or semantically recognizable** in the golden memories — no external knowledge may be used to expand the field set. + * Differences in **values** (e.g., “Zhang San” vs. “Li Si”), **polarity** (like/dislike), or **exact number/time** do **not** affect this Boolean judgment. + + # Evaluation Procedure + + For each candidate memory: + + 1. **Decompose** it into atomic information points (e.g., name, number, location, preference). + 2. For each information point, **search** the dialogue and golden memories for supporting or contradictory evidence. + 3. Assign the **accuracy_score** (0 / 1 / 2) according to the rules above. + 4. Determine **is_included_in_golden_memories (true/false)**: + + * Identify each information point’s field; + * If *all* fields exist in the golden memories, mark as *true*; otherwise, *false*. + 5. Provide a **concise Chinese explanation** in `"reason"`, citing key evidence (short excerpts allowed), and clearly state any unsupported or contradictory parts if applicable. + + # Output Format (strictly required) + + Output **only one JSON object**, with the following three fields: + + * `"accuracy_score"`: `"0"` or `"1"` or `"2"` + * `"is_included_in_golden_memories"`: `"true"` or `"false"` + * `"reason"`: `"brief explanation in Chinese"` + + Do **not** include any other text, explanation, or fields. + Do **not** include the candidate memory text inside the JSON. + + Please output **only** the following JSON (in a code block): + + ```json + {{ + "accuracy_score": "2 | 1 | 0", + "is_included_in_golden_memories": "true | false", + "reason": "Brief explanation in Chinese" + }} + ``` + +EVALUATION_PROMPT_FOR_UPDATE_MEMORY: | + Your task is to **evaluate the update accuracy** of an AI memory system. + Based on the information provided below, determine whether the system-generated **“Generated Memories”** correctly **includes** the **Target Memory for Update**. + + # Background Information + + The following information is provided for evaluation: + + 1. **Generated Memories:** + This is the list of memory points generated by the system after the current dialogue. + {memories} + + 2. **Target Memory for Update:** + This is the correct, updated version of the memory point that should have been produced — the one we focus on in this evaluation. + {updated_memory} + + 3. **Original Memory Content:** + This is the original version of the target memory before the update. + {original_memory} + + # Evaluation Criteria + + Please make your judgment **strictly based on the content update of the “Target Memory for Update.”** + Use the following categories: + + ### Correct Update + + * **Generated Memories** **contains all information points** from the “Target Memory for Update,” accurately and completely reflecting the intended update. + * **Key fields** (e.g., date, time, values, proper nouns, etc.) must match exactly. + * The **original memory** is effectively replaced or marked as outdated. + * Synonymous or slightly rephrased expressions are acceptable. + + ### Hallucinated Update + + * **Factual error:** The **Generated Memories** includes a new memory related to the “Target Memory for Update,” but its content contains factual mistakes or contradictions compared to the correct update. + + ### Omitted Update + + * **Completely omitted:** The **Generated Memories** contains no new memory related to the “Target Memory for Update.” + * **Partially omitted:** A related new memory was generated in **Generated Memories**, but it **misses key information** that should have been included. + + ### Other + + Used for update failures that do **not clearly fall** into the above categories of “Hallucination” or “Omission.” + + # Output Requirements + + Please return your evaluation strictly in the following JSON format and provide a concise explanation. + + ```json + {{ + "reason": "Briefly explain your reasoning here and why it fits this category.", + "evaluation_result": "Correct | Hallucination | Omission | Other" + }} + ``` + +EVALUATION_PROMPT_FOR_QUESTION: | + You are an **evaluation expert for AI memory system question answering**. + Based **only** on the provided **“Question”**, **“Reference Answer”**, and **“Key Memory Points”** (the essential facts needed to derive the reference answer), strictly evaluate the **accuracy** of the **“Memory System Response.”** Classify it as one of **“Correct”**, **“Hallucination”**, or **“Omission.”** Do **not** use any external knowledge or subjective inference. Finally, output your judgment **strictly** in the specified JSON format. + + # Evaluation Criteria + + ## Answer Type Classification + + ### 1. Correct + + * The “Memory System Response” accurately answers the “Question,” and its content is **semantically equivalent** to the “Reference Answer.” + * It contains **no contradictions** with the “Key Memory Points” or “Reference Answer.” + * It introduces **no unsupported details** beyond the “Key Memory Points” that could alter the conclusion. + * Synonyms, paraphrasing, and reasonable summarization are acceptable. + + ### 2. Hallucination + + * The “Memory System Response” includes information or facts that **contradict or are inconsistent** with the “Reference Answer” or the “Key Memory Points.” + * When the “Reference Answer” is labeled as *unknown/uncertain*, yet the response provides a specific verifiable fact or conclusion. + * Extra irrelevant information that does **not change** the conclusion is **not** considered hallucination by itself; however, if it **changes or misleads** the conclusion, or **contradicts** the “Key Memory Points,” it should be judged as a **Hallucination**. + + ### 3. Omission + + * The response is **incomplete** compared to the “Reference Answer.” + * It explicitly states “don’t know,” “can’t remember,” or “no related memory,” even though relevant information exists in the “Key Memory Points.” + * For multi-element questions, **all elements must be correct and present**; omission of **any** element is considered an **Omission**. + + ## Priority Rules (Conflict Handling) + + * If the response contains **both missing necessary information** and **fabricated/contradictory information**, classify it as **Hallucination**. + * If there is **no fabrication/contradiction** but some necessary information is missing, classify it as **Omission**. + * Only when the meaning is **fully equivalent** to the reference answer should it be classified as **Correct**. + + ## Detailed Guidelines and Tolerance + + * Equivalent expressions of numbers, times, and units are acceptable, but the **numerical values themselves must not differ**. + * For multi-element questions, **all elements must be complete and accurate**; missing any element counts as **Omission**. + * If the reference answer is *“unknown / cannot be determined”* and the system provides a definite fact, that is a **Hallucination**. + If the system also answers *“unknown”* (without guessing), it may be **Correct**. + * The evaluation must rely **only** on the *Reference Answer*, *Key Memory Points*, and *System Response* — no external context, world knowledge, or speculative reasoning is allowed. + + # Information for Evaluation + + * **Question:** + {question} + + * **Reference Answer:** + {reference_answer} + + * **Key Memory Points:** + {key_memory_points} + + * **Memory System Response:** + {response} + + # Output Requirements + + Please provide your evaluation result **strictly** in the JSON format below. + Do **not** add any extra explanation or comments outside the JSON block. + + ```json + {{ + "reasoning": "Provide a concise and traceable evaluation rationale: first compare the system’s response with the Key Memory Points (which were correctly used, which were missing, and whether there was any fabrication/contradiction), then assess its consistency with the Reference Answer, and finally state the classification basis.", + "evaluation_result": "Correct | Hallucination | Omission" + }} + ``` + + +EVALUATION_PROMPT_FOR_QUESTION2: | + You are an **evaluation expert for AI memory system question answering**. + + Based **only** on the provided **"Question"**, **"Reference Answer"**, and **"Key Memory Points"** (the essential facts needed to derive the reference answer), strictly evaluate the **accuracy** of the **"Memory System Response."** Classify it as one of **"Correct"**, **"Hallucination"**, or **"Omission."** Do **not** use any external knowledge or subjective inference. Finally, output your judgment **strictly** in the specified JSON format. + + # Evaluation Criteria + + ## Answer Type Classification + + ### 1. Correct + + * The "Memory System Response" accurately answers the "Question," and its content is **semantically equivalent** to the "Reference Answer." + * It contains **no contradictions** with the "Key Memory Points" or "Reference Answer." + * **Extra details not present in the Key Memory Points are allowed and should not be penalized**, as long as they: + - Do not contradict the Key Memory Points or Reference Answer + - Do not change or mislead the core conclusion + - Are reasonable additional context that the memory system may have retained from the conversation + * The memory system may have stored additional information beyond the Key Memory Points. Such extra information should be treated as **supplementary context** rather than hallucination, provided it does not conflict with the core answer. + * Synonyms, paraphrasing, and reasonable summarization are acceptable. + + ### 2. Hallucination + + * The "Memory System Response" includes information or facts that **contradict or are inconsistent** with the "Reference Answer" or the "Key Memory Points." + * The response provides information that **directly contradicts** known facts from the Key Memory Points. + * When the "Reference Answer" is labeled as *unknown/uncertain*, yet the response provides a specific verifiable fact or conclusion. + * **Important:** Extra information that is NOT in Key Memory Points is **NOT automatically a hallucination**. Only classify as hallucination if the extra information: + - Directly contradicts the Key Memory Points or Reference Answer + - Changes or misleads the core conclusion in a way that makes the answer incorrect + - Provides a definitive answer when the Reference Answer indicates uncertainty + + ### 3. Omission + + * The response is **incomplete** compared to the "Reference Answer." + * It explicitly states "don't know," "can't remember," or "no related memory," even though relevant information exists in the "Key Memory Points." + * For multi-element questions, **all elements must be correct and present**; omission of **any** element is considered an **Omission**. + + ## Priority Rules (Conflict Handling) + + * If the response contains **both missing necessary information** and **fabricated/contradictory information**, classify it as **Hallucination**. + * If there is **no fabrication/contradiction** but some necessary information is missing, classify it as **Omission**. + * If the core answer is correct and complete, classify as **Correct** even if there are extra details not in Key Memory Points (as long as they don't contradict or mislead). + + ## Detailed Guidelines and Tolerance + + * Equivalent expressions of numbers, times, and units are acceptable, but the **numerical values themselves must not differ**. + * For multi-element questions, **all elements must be complete and accurate**; missing any element counts as **Omission**. + * If the reference answer is *"unknown / cannot be determined"* and the system provides a definite fact, that is a **Hallucination**. + If the system also answers *"unknown"* (without guessing), it may be **Correct**. + * **Focus on evaluating whether the core answer to the question is correct**, not whether the response is limited to only the Key Memory Points. + * Extra contextual information (e.g., additional preferences, related details) should be viewed as enrichment, not as errors, unless they contradict or mislead. + + # Information for Evaluation + + * **Question:** + {question} + + * **Reference Answer:** + {reference_answer} + + * **Key Memory Points:** + {key_memory_points} + + * **Memory System Response:** + {response} + + # Output Requirements + + Please provide your evaluation result **strictly** in the JSON format below. + Do **not** add any extra explanation or comments outside the JSON block. + + ```json + {{ + "reasoning": "Provide a concise and traceable evaluation rationale: first verify that the system's response correctly includes all required elements from the Reference Answer, then check if any information contradicts the Key Memory Points or Reference Answer. Extra details not in Key Memory Points should be noted but not penalized unless they contradict or mislead. Finally state the classification basis.", + "evaluation_result": "Correct | Hallucination | Omission" + }} + ``` + """ \ No newline at end of file diff --git a/reme/agent/memory/base_memory_agent.py b/reme/agent/memory/base_memory_agent.py index 361a6676..6ad3499d 100644 --- a/reme/agent/memory/base_memory_agent.py +++ b/reme/agent/memory/base_memory_agent.py @@ -73,3 +73,10 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): def author(self) -> str: """Returns the LLM model name as the author identifier.""" return self.llm.model_name + + @property + def retrieved_nodes(self) -> list[MemoryNode]: + """Returns the retrieved nodes.""" + if "retrieved_nodes" not in self.context: + self.context.retrieved_nodes = [] + return self.context.retrieved_nodes diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/default/personal_retriever.py index 13197304..1cf68ab7 100644 --- a/reme/agent/memory/default/personal_retriever.py +++ b/reme/agent/memory/default/personal_retriever.py @@ -3,7 +3,7 @@ from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import Role, MemoryType from ....core.op import BaseTool -from ....core.schema import Message, MemoryNode +from ....core.schema import Message from ....core.utils import format_messages @@ -12,10 +12,6 @@ class PersonalRetriever(BaseMemoryAgent): memory_type: MemoryType = MemoryType.PERSONAL - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.retrieved_nodes: list[MemoryNode] = [] - async def build_messages(self) -> list[Message]: if self.context.get("query"): context = self.context.query @@ -59,3 +55,9 @@ class PersonalRetriever(BaseMemoryAgent): retrieved_nodes=self.retrieved_nodes, **kwargs, ) + + async def execute(self): + result = await super().execute() + result["retrieved_nodes"] = self.retrieved_nodes + + return result diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index ef14a1d0..eafbfa05 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -85,19 +85,26 @@ class PersonalSummarizer(BaseMemoryAgent): messages_phase1 = await self._build_phase1_messages() for i, message in enumerate(messages_phase1): role = message.name or message.role - logger.info(f"[{self.__class__.__name__}-S1] role={role} {message.simple_dump(as_dict=False)}") + logger.info(f"[{self.__class__.__name__} S1] role={role} {message.simple_dump(as_dict=False)}") tools_phase1, messages_phase1, success_phase1 = await self.react(messages_phase1, tools[:-1], stage="S1") messages_phase2 = await self._build_phase2_messages() for i, message in enumerate(messages_phase2): role = message.name or message.role - logger.info(f"[{self.__class__.__name__}-S2] role={role} {message.simple_dump(as_dict=False)}") + logger.info(f"[{self.__class__.__name__} S2] role={role} {message.simple_dump(as_dict=False)}") tools_phase2, messages_phase2, success_phase2 = await self.react(messages_phase2, tools[-1:], stage="S2") + success = success_phase1 and success_phase2 + messages = messages_phase1 + messages_phase2 + tools = tools_phase1 + tools_phase2 + memory_nodes = [] + for tool in tools: + if tool.memory_nodes: + memory_nodes.extend(tool.memory_nodes) + return { - "answer": (messages_phase1[-1].content if success_phase1 else "") - + (messages_phase2[-1].content if success_phase2 else ""), - "success": success_phase1 and success_phase2, - "messages": messages_phase1 + messages_phase2, - "tools": tools_phase1 + tools_phase2, + "answer": memory_nodes, + "success": success, + "messages": messages, + "tools": tools, } diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index 77da7843..4fbd7348 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -16,7 +16,7 @@ system_prompt_phase1: | ### Step 2: Add New Memories Use `add_memory` to add new memories: - Extract and summarize important information about **{memory_target}** - - Set `conversation_time` (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable) + - Set `update_time` (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable) - If the information is completely identical to existing memory, skip adding user_message_phase1: | @@ -30,15 +30,15 @@ system_prompt_phase2: | {context} ## Current User Profile: - UserProfile format: `profile_id= conversation_time= `. + UserProfile format: `profile_id= update_time= `. {user_profile} ## Task: Update Profile with `UpdateUserProfile` **CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate. Synchronize profile/memories with new information from the conversation, including **{memory_target}**' current status: - - `profile_ids_to_delete`: Remove outdated, conflicting, or redundant entries. - - `profiles_to_add`: Add new profiles/memories with `conversation_time`, e.g. `YYYY-MM-DD HH:MM:SS`, {memory_target} did something. + - `profile_ids_to_delete`: Remove conflicting, or redundant entries. + - `profiles_to_add`: Add new profiles/memories with `update_time`, e.g. `YYYY-MM-DD HH:MM:SS`, {memory_target} did something. - Maintain profiles that are concise, mutually exclusive, and collectively comprehensive with no information loss. user_message_phase2: | diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/default/reme_retriever.py index 46657344..88375ab7 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/default/reme_retriever.py @@ -62,19 +62,23 @@ class ReMeRetriever(BaseMemoryAgent): hands_off_tool = tools[0] agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"] - answer = "" + answer = [] success = True messages = [] tools = [] + retrieved_nodes = [] + for agent in agents: - answer += "\n" + agent.response.answer + answer.append(agent.response.answer) success = success and agent.response.success - messages += agent.response.metadata["messages"] - tools += agent.response.metadata["tools"] + messages.extend(agent.response.metadata["messages"]) + tools.extend(agent.response.metadata["tools"]) + retrieved_nodes.extend(agent.response.metadata["retrieved_nodes"]) return { - "answer": answer.strip(), + "answer": "\n".join(answer), "success": True, "messages": self.messages, "tools": tools, + "retrieved_nodes": retrieved_nodes, } diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index 9cae5cbd..6fb91ef8 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -58,19 +58,19 @@ class ReMeSummarizer(BaseMemoryAgent): hands_off_tool = tools[0] agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"] - answer = "" success = True messages = [] tools = [] + memory_nodes = [] for agent in agents: - answer += "\n" + agent.response.answer success = success and agent.response.success - messages += agent.response.metadata["messages"] - tools += agent.response.metadata["tools"] + messages.extend(agent.response.metadata["messages"]) + tools.extend(agent.response.metadata["tools"]) + memory_nodes.extend(agent.response.answer) return { - "answer": answer.strip(), + "answer": memory_nodes, "success": True, - "messages": self.messages, + "messages": messages, "tools": tools, } diff --git a/reme/core/llm/base_llm.py b/reme/core/llm/base_llm.py index be7b5e0d..df5e3c0d 100644 --- a/reme/core/llm/base_llm.py +++ b/reme/core/llm/base_llm.py @@ -12,6 +12,7 @@ from ..enumeration import ChunkEnum, Role from ..schema import Message from ..schema import StreamChunk from ..schema import ToolCall +from ..utils import extract_content class BaseLLM(ABC): @@ -458,6 +459,43 @@ class BaseLLM(ABC): time.sleep(1 + i) return default_value + async def simple_request( + self, + prompt: str, + model_name: str, + callback_fn: Callable[[Message], Any] | None = None, + default_value: Any = None, + **kwargs, + ) -> str: + """Make a simple request using the LLM.""" + assistant_message = await self.chat( + messages=[Message(role=Role.USER, content=prompt)], + model_name=model_name, + callback_fn=callback_fn, + default_value=default_value, + **kwargs, + ) + return assistant_message.content + + async def simple_request_for_json( + self, + prompt: str, + model_name: str, + **kwargs, + ) -> dict: + """Make a simple request using the LLM and extract JSON.""" + + def extract_fn(message: Message) -> dict: + return extract_content(message.content) + + return await self.chat( + messages=[Message(role=Role.USER, content=prompt)], + model_name=model_name, + callback_fn=extract_fn, + default_value={}, + **kwargs, + ) + async def close(self): """Release async resources.""" diff --git a/reme/core/op/base_react.py b/reme/core/op/base_react.py index 95302138..1d46e09e 100644 --- a/reme/core/op/base_react.py +++ b/reme/core/op/base_react.py @@ -93,7 +93,7 @@ class BaseReact(BaseOp): logger.warning(f"{prefix} unknown tool_call={tool_call.name}") continue - logger.info(f"{prefix} submit tool_calls={tool_call.simple_output_dump(as_dict=False)}") + logger.info(f"{prefix} submit tool_call[{tool_call.name}] arguments={tool_call.arguments}") # Create independent tool copy with unique ID tool_copy: BaseTool = tool_dict[tool_call.name].copy() diff --git a/reme/core/utils/llm_utils.py b/reme/core/utils/llm_utils.py index 805ac890..515c07b3 100644 --- a/reme/core/utils/llm_utils.py +++ b/reme/core/utils/llm_utils.py @@ -2,6 +2,7 @@ import json import re + from loguru import logger from ..enumeration import Role @@ -170,18 +171,18 @@ def extract_content(text: str, language_tag: str = "json", greedy: bool = False) pattern = rf"```\s*{re.escape(language_tag)}\s*({quantifier})\s*```" match = re.search(pattern, text, re.DOTALL) - if match: - result = match.group(1).strip() - else: - result = text + if not match: + return None + + content = match.group(1).strip() if language_tag == "json": try: - result = json.loads(result) + return json.loads(content) except json.JSONDecodeError: - result = None - - return result + return None + else: + return content def deduplicate_memories(memories: list[MemoryNode]) -> list[MemoryNode]: diff --git a/reme/reme.py b/reme/reme.py index 255c6a1d..76154919 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -1,23 +1,24 @@ -"""ReMe application classes for simplified configuration and execution.""" +"""ReMe classes for simplified configuration and execution.""" import asyncio import sys from .agent.memory.default import ReMeSummarizer, PersonalSummarizer, PersonalRetriever, ReMeRetriever from .config import ReMeConfigParser -from .core.context import ServiceContext +from .core.context import PromptHandler, ServiceContext from .core.embedding import BaseEmbeddingModel +from .core.enumeration import MemoryType from .core.flow import BaseFlow from .core.llm import BaseLLM -from .core.schema import Response, Message +from .core.schema import Response, Message, MemoryNode, VectorNode from .core.token_counter import BaseTokenCounter -from .core.utils import execute_stream_task +from .core.utils import execute_stream_task, get_now_time from .core.vector_store import BaseVectorStore -from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, HandsOff, ReadHistory +from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, HandsOff, ReadHistory, ReadUserProfile class ReMe: - """ReMe application with config file support and flow execution methods.""" + """ReMe with config file support and flow execution methods.""" def __init__( self, @@ -50,6 +51,8 @@ class ReMe: **kwargs, ) + self.prompt_handler = PromptHandler(language=self.service_context.language) + async def __aenter__(self): """Async context manager entry.""" return self @@ -59,11 +62,11 @@ class ReMe: return self async def close(self): - """Close the application.""" + """Close""" return await self.service_context.close() def close_sync(self): - """Close the application synchronously.""" + """Close synchronously""" self.service_context.close_sync() async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None): @@ -77,26 +80,26 @@ class ReMe: return False @property - def default_llm(self) -> BaseLLM: + def llm(self) -> BaseLLM: """Return the default LLM instance from the service context.""" return self.service_context.llms["default"] @property - def default_embedding_model(self) -> BaseEmbeddingModel: + def embedding_model(self) -> BaseEmbeddingModel: """Return the default embedding model instance from the service context.""" return self.service_context.embedding_models["default"] @property - def default_vector_store(self) -> BaseVectorStore: + def vector_store(self) -> BaseVectorStore: """Return the default vector store instance from the service context.""" return self.service_context.vector_stores["default"] @property - def default_token_counter(self) -> BaseTokenCounter: + def token_counter(self) -> BaseTokenCounter: """Return the default token counter instance from the service context.""" return self.service_context.token_counters["default"] - async def summary( + async def summary_memory( self, messages: list[Message | dict], description: str = "", @@ -104,11 +107,11 @@ class ReMe: enable_thinking_params: bool = False, meta_memories: list[dict] = None, version: str = "default", + return_dict: bool = False, **kwargs, - ): + ) -> str | dict: """Summarize messages and store them in memory for the specified user(s).""" if user_name: - if isinstance(user_name, str): for message in messages: if isinstance(message, dict) and not message.get("name"): @@ -147,17 +150,22 @@ class ReMe: else: raise NotImplementedError - return await reme_summarizer.call( + result = await reme_summarizer.call( messages=messages, description=description, service_context=self.service_context, **kwargs, ) + if return_dict: + return result + else: + return result["answer"] + else: raise NotImplementedError - async def retrieve( + async def retrieve_memory( self, query: str = "", top_k: int = 20, @@ -167,8 +175,9 @@ class ReMe: enable_thinking_params: bool = False, meta_memories: list[dict] = None, version: str = "default", + return_dict: bool = False, **kwargs, - ): + ) -> str | dict: """Retrieve relevant memories for the specified user(s) based on query or messages.""" if user_name: if isinstance(user_name, str): @@ -210,7 +219,7 @@ class ReMe: else: raise NotImplementedError - return await reme_retriever.call( + result = await reme_retriever.call( query=query, messages=messages, description=description, @@ -218,9 +227,200 @@ class ReMe: **kwargs, ) + if return_dict: + return result + else: + return result["answer"] + else: raise NotImplementedError + async def add_memory( + self, + memory_content: str, + user_name: str, + memory_type: str | MemoryType | None = None, + memory_target: str = "", + when_to_use: str = "", + ref_memory_id: str = "", + author: str = "", + score: float = 0, + conversation_time: str = "", + **kwargs, + ) -> MemoryNode: + """Add a new memory to the vector store for the specified user.""" + + if user_name: + memory_type = MemoryType.PERSONAL + memory_target = user_name + else: + memory_type = MemoryType(memory_type) + assert memory_target, "memory_target is required" + + metadata = kwargs.copy() + if conversation_time: + metadata["conversation_time"] = conversation_time + + memory_node = MemoryNode( + memory_type=memory_type, + memory_target=memory_target, + when_to_use=when_to_use, + content=memory_content, + ref_memory_id=ref_memory_id, + author=author, + score=score, + metadata=metadata, + ) + vector_node = memory_node.to_vector_node() + await self.vector_store.delete([vector_node.vector_id]) + await self.vector_store.insert([vector_node]) + + return memory_node + + async def update_memory( + self, + memory_id: str, + memory_content: str, + user_name: str, + memory_type: str | MemoryType | None = None, + memory_target: str = "", + when_to_use: str = "", + ref_memory_id: str = "", + author: str = "", + score: float = 0, + conversation_time: str = "", + **kwargs, + ) -> MemoryNode: + """Update an existing memory in the vector store by its ID.""" + + if user_name: + memory_type = MemoryType.PERSONAL + memory_target = user_name + else: + memory_type = MemoryType(memory_type) + assert memory_target, "memory_target is required" + + metadata = kwargs.copy() + if conversation_time: + metadata["conversation_time"] = conversation_time + + memory_node = MemoryNode( + memory_type=memory_type, + memory_target=memory_target, + when_to_use=when_to_use, + content=memory_content, + ref_memory_id=ref_memory_id, + author=author, + score=score, + metadata=metadata, + ) + vector_node = memory_node.to_vector_node() + await self.vector_store.delete([memory_id, vector_node.vector_id]) + await self.vector_store.insert([vector_node]) + + return memory_node + + async def delete_memory(self, memory_id: str | list[str]): + """Delete one or more memories from the vector store by their IDs.""" + vector_ids = [memory_id] if isinstance(memory_id, str) else memory_id + await self.vector_store.delete(vector_ids) + + async def delete_all_memories(self): + """Delete all memories from the vector store.""" + await self.vector_store.delete_all() + + async def get_memory(self, memory_id: str | list[str]) -> MemoryNode | list[MemoryNode]: + """Retrieve one or more memories from the vector store by their IDs.""" + vector_ids = [memory_id] if isinstance(memory_id, str) else memory_id + vector_nodes = await self.vector_store.get(vector_ids) + if isinstance(vector_nodes, VectorNode): + return vector_nodes.to_memory_node() + else: + return [node.to_memory_node() for node in vector_nodes] + + async def get_all_memories(self) -> list[MemoryNode]: + """Retrieve all memories from the vector store.""" + return [node.to_memory_node() for node in await self.vector_store.list()] + + @staticmethod + async def get_profiles(user_name: str | list[str]) -> str | list[str]: + """Retrieve user profile(s) from the system for the specified user(s).""" + read_profile = ReadUserProfile(show_id="profile") + if isinstance(user_name, str): + return await read_profile.call(memory_target=user_name) + else: + return [await read_profile.call(memory_target=name) for name in user_name] + + @staticmethod + async def add_profile( + profile_key: str, + profile_value: str, + user_name: str, + update_time: str | None = None, + ) -> MemoryNode: + """Add user profile to ReMe system.""" + update_user_profile = UpdateUserProfile() + if update_time is None: + update_time = get_now_time() + + await update_user_profile.call( + profile_ids_to_delete=[], + profiles_to_add=[ + { + "update_time": update_time, + "profile_key": profile_key, + "profile_value": profile_value, + }, + ], + memory_target=user_name, + ) + return update_user_profile.memory_nodes[0] + + @staticmethod + async def update_profile( + profile_id: str, + profile_key: str, + profile_value: str, + user_name: str, + update_time: str | None = None, + ) -> MemoryNode: + """Add user profile to ReMe system.""" + update_user_profile = UpdateUserProfile() + if update_time is None: + update_time = get_now_time() + + await update_user_profile.call( + profile_ids_to_delete=[profile_id], + profiles_to_add=[ + { + "update_time": update_time, + "profile_key": profile_key, + "profile_value": profile_value, + }, + ], + memory_target=user_name, + ) + return update_user_profile.memory_nodes[0] + + @staticmethod + async def delete_all_profiles(user_name: str | list[str]): + """Delete all user profiles from ReMe system.""" + if isinstance(user_name, str): + user_name = [user_name] + + read_profile = ReadUserProfile(show_id="profile") + update_profile = UpdateUserProfile() + for memory_target in user_name: + await read_profile.call(memory_target=memory_target) + profile_ids = [profile.memory_id for profile in read_profile.memory_nodes] + await update_profile.call(profile_ids_to_delete=profile_ids, memory_target=memory_target) + + async def context_offload(self): + """working memory summary""" + + async def context_reload(self): + """working memory retrieve""" + async def execute_flow(self, name: str, **kwargs) -> Response: """Execute a flow with the given name and parameters.""" assert name in self.service_context.flows, f"Flow {name} not found" @@ -248,7 +448,7 @@ class ReMe: def main(): - """Main entry point for running ReMe application from command line.""" + """Main entry point for running ReMe from command line.""" with ReMe(*sys.argv[1:]) as app: app.run_service() diff --git a/reme/tool/memory/base_memory_tool.py b/reme/tool/memory/base_memory_tool.py index adc2f6b2..5a900177 100644 --- a/reme/tool/memory/base_memory_tool.py +++ b/reme/tool/memory/base_memory_tool.py @@ -23,7 +23,6 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): self.enable_multiple: bool = enable_multiple self.enable_thinking_params: bool = enable_thinking_params self.local_memory_path: str = local_memory_path - self.memory_nodes: list[MemoryNode | str] = [] def _build_tool_call(self) -> ToolCall: """Build and return the tool call schema""" @@ -88,9 +87,16 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): @property def retrieved_nodes(self) -> list[MemoryNode]: """Get the retrieved nodes from context.""" - return self.context.get("retrieved_nodes") + return self.context["retrieved_nodes"] @property def author(self) -> str: """Get the author from context.""" return self.context.get("author", "") + + @property + def memory_nodes(self) -> list[MemoryNode | str]: + """Get the memory nodes from context.""" + if "memory_nodes" not in self.context: + self.context.memory_nodes = [] + return self.context["memory_nodes"] diff --git a/reme/tool/memory/history/read_history.py b/reme/tool/memory/history/read_history.py index 089a3309..4da6918e 100644 --- a/reme/tool/memory/history/read_history.py +++ b/reme/tool/memory/history/read_history.py @@ -40,7 +40,8 @@ class ReadHistory(BaseMemoryTool): logger.warning(output) return output - memory = MemoryNode.from_vector_node(nodes[0]) - output = f"Historical Dialogue[{history_id}]\n{memory.content}" - logger.info(f"Successfully read history memory: {history_id}") + memory_node: MemoryNode = MemoryNode.from_vector_node(nodes[0]) + self.retrieved_nodes.append(memory_node) + output = f"Historical Dialogue[{history_id}]\n{memory_node.content}" + logger.info(f"Successfully read history memory_node: {history_id}") return output diff --git a/reme/tool/memory/user_profile/read_user_profile.py b/reme/tool/memory/user_profile/read_user_profile.py index 05346037..5ad5db57 100644 --- a/reme/tool/memory/user_profile/read_user_profile.py +++ b/reme/tool/memory/user_profile/read_user_profile.py @@ -38,7 +38,8 @@ class ReadUserProfile(BaseMemoryTool): return "" nodes = [MemoryNode(**data) for data in cached_data] - nodes.sort(key=lambda n: n.metadata.get("conversation_time", "")) + self.memory_nodes = nodes + nodes.sort(key=lambda n: n.metadata.get("update_time", "")) formatted_profiles = [] for node in nodes: @@ -46,8 +47,8 @@ class ReadUserProfile(BaseMemoryTool): if self.show_id == "profile": parts.append(f"profile_id={node.memory_id}") - if conv_time := node.metadata.get("conversation_time"): - parts.append(f"conversation_time={conv_time}") + if update_time := node.metadata.get("update_time"): + parts.append(f"update_time={update_time}") parts.append(f"{node.when_to_use}: {node.content}") @@ -58,4 +59,4 @@ class ReadUserProfile(BaseMemoryTool): logger.info(f"Read {len(formatted_profiles)} profiles from cache key: {self.memory_cache_key}") - return "### User Profile\n" + "\n".join(formatted_profiles).strip() + return "\n".join(formatted_profiles).strip() diff --git a/reme/tool/memory/user_profile/update_user_profile.py b/reme/tool/memory/user_profile/update_user_profile.py index da561d8e..1c568c5d 100644 --- a/reme/tool/memory/user_profile/update_user_profile.py +++ b/reme/tool/memory/user_profile/update_user_profile.py @@ -3,8 +3,8 @@ from loguru import logger from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall -from ....core.schema.memory_node import MemoryNode +from ....core.enumeration import MemoryType +from ....core.schema import ToolCall, MemoryNode from ....core.utils import deduplicate_memories @@ -34,9 +34,9 @@ class UpdateUserProfile(BaseMemoryTool): "items": { "type": "object", "properties": { - "conversation_time": { + "update_time": { "type": "string", - "description": "Conversation time, e.g. '2020-01-01 00:00:00'", + "description": "Update time, e.g. '2020-01-01 00:00:00'", }, "profile_key": { "type": "string", @@ -47,7 +47,7 @@ class UpdateUserProfile(BaseMemoryTool): "description": "Profile value or content, e.g. 'John Smith'", }, }, - "required": ["conversation_time", "profile_key", "profile_value"], + "required": ["update_time", "profile_key", "profile_value"], }, }, }, @@ -58,6 +58,8 @@ class UpdateUserProfile(BaseMemoryTool): async def execute(self): # Get and deduplicate profile IDs to delete + self.context.memory_type = MemoryType.PERSONAL + profile_ids_to_delete = self.context.get("profile_ids_to_delete", []) profile_ids_to_delete = list(dict.fromkeys([pid for pid in profile_ids_to_delete if pid])) profiles_to_add = self.context.get("profiles_to_add", []) @@ -88,12 +90,13 @@ class UpdateUserProfile(BaseMemoryTool): content=profile.get("profile_value", ""), ref_memory_id=self.history_node.memory_id, author=self.author, - metadata={"conversation_time": profile.get("conversation_time", "")}, + metadata={"update_time": profile.get("update_time", "")}, ) new_nodes.append(node) logger.info(f"Added {len(new_nodes)} new profiles.") # Deduplicate and save updated profiles + self.memory_nodes.extend(new_nodes) updated_nodes = deduplicate_memories(existing_nodes + new_nodes) nodes_data = [node.model_dump(exclude_none=True) for node in updated_nodes] self.local_memory.save(self.memory_cache_key, nodes_data) diff --git a/reme/tool/memory/vector/add_memory.py b/reme/tool/memory/vector/add_memory.py index 3371c790..d59742d6 100644 --- a/reme/tool/memory/vector/add_memory.py +++ b/reme/tool/memory/vector/add_memory.py @@ -102,7 +102,7 @@ class AddMemory(BaseMemoryTool): await self.vector_store.delete(vector_ids=vector_ids) await self.vector_store.insert(nodes=vector_nodes) - self.memory_nodes = memory_nodes + self.memory_nodes.extend(memory_nodes) output = f"Successfully added {len(memory_nodes)} memories to vector_store." logger.info(output) diff --git a/reme/tool/memory/vector/retrieve_memory.py b/reme/tool/memory/vector/retrieve_memory.py index a83a1dae..9d3e42db 100644 --- a/reme/tool/memory/vector/retrieve_memory.py +++ b/reme/tool/memory/vector/retrieve_memory.py @@ -117,7 +117,6 @@ class RetrieveMemory(BaseMemoryTool): retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id} new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids] self.retrieved_nodes.extend(new_memory_nodes) - self.memory_nodes = new_memory_nodes if not new_memory_nodes: output = "No new memory_nodes found matching the query (duplicates removed)." diff --git a/tests/test_reme.py b/tests/test_reme.py index a4f383cf..52b11a64 100644 --- a/tests/test_reme.py +++ b/tests/test_reme.py @@ -11,7 +11,7 @@ reme = ReMe(vector_store={"collection_name": "reme"}) async def test_reme(): """Tests ReMe memory system with personal information storage and retrieval.""" # 构建一段包含个人信息的对话 - await reme.default_vector_store.delete_all() + await reme.vector_store.delete_all() messages = [ { @@ -53,7 +53,7 @@ async def test_reme(): print("=" * 60) # 对对话进行总结,生成记忆 - await reme.summary( + await reme.summary_memory( messages=messages, user_name="zhangwei", description="用户自我介绍和技术兴趣分享", @@ -66,7 +66,7 @@ async def test_reme(): print("=" * 60) # 列出所有存储的记忆节点 - nodes: list[VectorNode] = await reme.default_vector_store.list() + nodes: list[VectorNode] = await reme.vector_store.list() for i, node in enumerate(nodes, 1): memory_node = MemoryNode.from_vector_node(node) print(f"{i} {memory_node.model_dump_json()}") @@ -78,25 +78,25 @@ async def test_reme(): # 测试问题1: 检索用户姓名 query1 = "用户叫什么名字?" print(f"\n问题1: {query1}") - result1 = await reme.retrieve(query=query1, user_name="zhangwei") + result1 = await reme.retrieve_memory(query=query1, user_name="zhangwei") print(f"检索结果:\n{result1}") # 测试问题2: 检索技术背景 query2 = "用户擅长什么编程语言和技术方向?" print(f"\n问题2: {query2}") - result2 = await reme.retrieve(query=query2, user_name="zhangwei") + result2 = await reme.retrieve_memory(query=query2, user_name="zhangwei") print(f"检索结果:\n{result2}") # 测试问题3: 检索个人信息 query3 = "用户的工作地点和联系方式是什么?" print(f"\n问题3: {query3}") - result3 = await reme.retrieve(query=query3, user_name="zhangwei") + result3 = await reme.retrieve_memory(query=query3, user_name="zhangwei") print(f"检索结果:\n{result3}") # 测试问题4: 检索兴趣爱好 query4 = "用户平时有什么爱好或活动?" print(f"\n问题4: {query4}") - result4 = await reme.retrieve(query=query4, user_name="zhangwei") + result4 = await reme.retrieve_memory(query=query4, user_name="zhangwei") print(f"检索结果:\n{result4}") print("\n" + "=" * 60) From c21b69da11b937d0cc43f9ffdd7e8d57aa964595 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Tue, 27 Jan 2026 01:26:17 +0800 Subject: [PATCH 02/17] refactor(memory): update memory management and retrieval implementation --- .../memory/default/personal_retriever.yaml | 4 +-- .../memory/default/personal_summarizer.py | 6 +---- reme/agent/memory/default/reme_retriever.py | 2 +- reme/reme.py | 27 +++++++++++-------- .../memory/user_profile/read_user_profile.py | 3 ++- reme/tool/memory/vector/add_memory.py | 1 + reme/tool/memory/vector/retrieve_memory.py | 2 +- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/reme/agent/memory/default/personal_retriever.yaml b/reme/agent/memory/default/personal_retriever.yaml index 7b81e4cc..9a882c48 100644 --- a/reme/agent/memory/default/personal_retriever.yaml +++ b/reme/agent/memory/default/personal_retriever.yaml @@ -37,9 +37,7 @@ system_prompt: | ## Output Format When answering, structure your response as follows: - - - [timestamp][Relevant memory content from search results] - - [timestamp][Relevant user profile information] + - [timestamp][Relevant history/memory/profile from context] If no relevant information found after thorough search (5+ queries), state: "No relevant information found after thorough search using multiple query strategies." diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index eafbfa05..d6ac1c41 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -5,7 +5,7 @@ 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, MemoryNode +from ....core.schema import Message class PersonalSummarizer(BaseMemoryAgent): @@ -13,10 +13,6 @@ class PersonalSummarizer(BaseMemoryAgent): memory_type: MemoryType = MemoryType.PERSONAL - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.retrieved_nodes: list[MemoryNode] = [] - async def _build_phase1_messages(self) -> list[Message]: """Build messages for phase 1: retrieve and add memory.""" return [ diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/default/reme_retriever.py index 88375ab7..659bf08d 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/default/reme_retriever.py @@ -78,7 +78,7 @@ class ReMeRetriever(BaseMemoryAgent): return { "answer": "\n".join(answer), "success": True, - "messages": self.messages, + "messages": messages, "tools": tools, "retrieved_nodes": retrieved_nodes, } diff --git a/reme/reme.py b/reme/reme.py index 76154919..f611792d 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -199,7 +199,6 @@ class ReMe: ] if version == "default": - reme_retriever = ReMeRetriever( meta_memories=meta_memories, tools=[ @@ -342,17 +341,18 @@ class ReMe: """Retrieve all memories from the vector store.""" return [node.to_memory_node() for node in await self.vector_store.list()] - @staticmethod - async def get_profiles(user_name: str | list[str]) -> str | list[str]: + async def get_profiles(self, user_name: str | list[str]) -> str | list[str]: """Retrieve user profile(s) from the system for the specified user(s).""" read_profile = ReadUserProfile(show_id="profile") if isinstance(user_name, str): - return await read_profile.call(memory_target=user_name) + return await read_profile.call(memory_target=user_name, service_context=self.service_context) else: - return [await read_profile.call(memory_target=name) for name in user_name] + return [ + await read_profile.call(memory_target=name, service_context=self.service_context) for name in user_name + ] - @staticmethod async def add_profile( + self, profile_key: str, profile_value: str, user_name: str, @@ -373,11 +373,12 @@ class ReMe: }, ], memory_target=user_name, + service_context=self.service_context, ) return update_user_profile.memory_nodes[0] - @staticmethod async def update_profile( + self, profile_id: str, profile_key: str, profile_value: str, @@ -399,11 +400,11 @@ class ReMe: }, ], memory_target=user_name, + service_context=self.service_context, ) return update_user_profile.memory_nodes[0] - @staticmethod - async def delete_all_profiles(user_name: str | list[str]): + async def delete_all_profiles(self, user_name: str | list[str]): """Delete all user profiles from ReMe system.""" if isinstance(user_name, str): user_name = [user_name] @@ -411,9 +412,13 @@ class ReMe: read_profile = ReadUserProfile(show_id="profile") update_profile = UpdateUserProfile() for memory_target in user_name: - await read_profile.call(memory_target=memory_target) + await read_profile.call(memory_target=memory_target, service_context=self.service_context) profile_ids = [profile.memory_id for profile in read_profile.memory_nodes] - await update_profile.call(profile_ids_to_delete=profile_ids, memory_target=memory_target) + await update_profile.call( + profile_ids_to_delete=profile_ids, + memory_target=memory_target, + service_context=self.service_context, + ) async def context_offload(self): """working memory summary""" diff --git a/reme/tool/memory/user_profile/read_user_profile.py b/reme/tool/memory/user_profile/read_user_profile.py index 5ad5db57..637b2070 100644 --- a/reme/tool/memory/user_profile/read_user_profile.py +++ b/reme/tool/memory/user_profile/read_user_profile.py @@ -38,7 +38,8 @@ class ReadUserProfile(BaseMemoryTool): return "" nodes = [MemoryNode(**data) for data in cached_data] - self.memory_nodes = nodes + self.memory_nodes.clear() + self.memory_nodes.extend(nodes) nodes.sort(key=lambda n: n.metadata.get("update_time", "")) formatted_profiles = [] diff --git a/reme/tool/memory/vector/add_memory.py b/reme/tool/memory/vector/add_memory.py index d59742d6..2f608f33 100644 --- a/reme/tool/memory/vector/add_memory.py +++ b/reme/tool/memory/vector/add_memory.py @@ -79,6 +79,7 @@ class AddMemory(BaseMemoryTool): memory_target=self.memory_target, content=memory_content, author=self.author, + ref_memory_id=self.history_node.memory_id, metadata=metadata, ) diff --git a/reme/tool/memory/vector/retrieve_memory.py b/reme/tool/memory/vector/retrieve_memory.py index 9d3e42db..7c95b538 100644 --- a/reme/tool/memory/vector/retrieve_memory.py +++ b/reme/tool/memory/vector/retrieve_memory.py @@ -128,7 +128,7 @@ class RetrieveMemory(BaseMemoryTool): line += f"conversation_time={node.metadata['conversation_time']} " line += node.content.strip() + " " if node.ref_memory_id: - line += f"history_id={node.ref_memory_id} " + line += f"history_id={node.ref_memory_id}" outputs.append(line.strip()) output = "\n".join(outputs) From 85a843ee2eed6e36ba898796c19a5a737a875570 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Tue, 27 Jan 2026 01:41:34 +0800 Subject: [PATCH 03/17] feat(benchmark): add configurable evaluation model for ReMe benchmark --- benchmark/halumem/eval_reme.py | 54 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/benchmark/halumem/eval_reme.py b/benchmark/halumem/eval_reme.py index 50e8b925..0bb1df4a 100644 --- a/benchmark/halumem/eval_reme.py +++ b/benchmark/halumem/eval_reme.py @@ -42,6 +42,7 @@ class EvalConfig: max_concurrency: int = 2 batch_size: int = 20 output_dir: str = "bench_results/reme" + eval_model_name: str = "qwen3-max" # ==================== Utilities ==================== @@ -269,8 +270,9 @@ async def evaluation_for_question( class MemoryProcessor: """Handles ReMe memory operations.""" - def __init__(self, reme: ReMe): + def __init__(self, reme: ReMe, eval_model_name: str = "qwen3-max"): self.reme = reme + self.eval_model_name = eval_model_name async def add_memories( self, @@ -303,12 +305,10 @@ class MemoryProcessor: duration_ms = (time.time() - start) * 1000 total_duration_ms += duration_ms - memory_nodes: list[MemoryNode] = result["answer"] - messages: list[Message] = result["messages"] - extracted_memories.extend([m.model_dump_json(exclude_none=True) for m in memory_nodes]) - summary_messages.extend([m.simple_dump() for m in messages]) + extracted_memories.extend([m.model_dump_json(exclude_none=True) for m in result["answer"]]) + summary_messages.extend([m.simple_dump() for m in result["messages"]]) - return extracted_memories, messages, total_duration_ms + return extracted_memories, summary_messages, total_duration_ms async def search_memory( self, @@ -336,7 +336,7 @@ class MemoryProcessor: # Extract memories from response memories = result["answer"] - messages = [x.model_dump_json(exclude_none=True) for x in result["messages"]] + agent_messages = [x.model_dump_json(exclude_none=True) for x in result["messages"]] retrieved_nodes = [x.model_dump_json(exclude_none=True) for x in result["retrieved_nodes"]] # Use LLM to generate structured answer from memories @@ -344,7 +344,8 @@ class MemoryProcessor: reme=self.reme, question=query, memories=memories, - user_id=user_id + user_id=user_id, + model_name=self.eval_model_name ) # Add original memories to the result @@ -352,7 +353,7 @@ class MemoryProcessor: answer_result["retrieved_nodes"] = retrieved_nodes duration_ms = (time.time() - start) * 1000 - return answer_result, messages, duration_ms + return answer_result, agent_messages, duration_ms # ==================== Evaluation ==================== @@ -360,10 +361,11 @@ class MemoryProcessor: class QuestionAnsweringEvaluator: """Evaluates question answering performance.""" - def __init__(self, memory_processor: MemoryProcessor, reme: ReMe, top_k: int): + def __init__(self, memory_processor: MemoryProcessor, reme: ReMe, top_k: int, eval_model_name: str = "qwen3-max"): self.memory_processor = memory_processor self.reme = reme self.top_k = top_k + self.eval_model_name = eval_model_name async def evaluate_questions( self, @@ -397,7 +399,8 @@ class QuestionAnsweringEvaluator: reference_answer=qa["answer"], key_memory_points=evidence_text, response=system_answer, - dialogue=formatted_dialogue + dialogue=formatted_dialogue, + model_name=self.eval_model_name ) # Build result record @@ -519,11 +522,12 @@ class HaluMemEvaluator: self.reme.prompt_handler.load_prompt_by_file(prompts_yaml_path) self.file_manager = FileManager(config.output_dir) - self.memory_processor = MemoryProcessor(self.reme) + self.memory_processor = MemoryProcessor(self.reme, config.eval_model_name) self.qa_evaluator = QuestionAnsweringEvaluator( self.memory_processor, self.reme, - config.top_k + config.top_k, + config.eval_model_name ) self.data_loader = DataLoader() @@ -758,14 +762,16 @@ async def main_async( data_path: str, top_k: int, user_num: int, - max_concurrency: int + max_concurrency: int, + eval_model_name: str = "qwen3-max" ): """Main async entry point for ReMe evaluation with proper resource cleanup.""" config = EvalConfig( data_path=data_path, top_k=top_k, user_num=user_num, - max_concurrency=max_concurrency + max_concurrency=max_concurrency, + eval_model_name=eval_model_name ) # Use async context manager for automatic cleanup @@ -777,14 +783,16 @@ def main( data_path: str, top_k: int, user_num: int, - max_concurrency: int + max_concurrency: int, + eval_model_name: str = "qwen3-max" ): """Main entry point for ReMe evaluation.""" asyncio.run(main_async( data_path=data_path, top_k=top_k, user_num=user_num, - max_concurrency=max_concurrency + max_concurrency=max_concurrency, + eval_model_name=eval_model_name )) @@ -816,7 +824,14 @@ if __name__ == "__main__": "--max_concurrency", type=int, default=100, - help="Maximum concurrent user processing (default: 2)" + help="Maximum concurrent user processing (default: 100)" + ) + parser.add_argument( + "--eval_model_name", + type=str, + # default="qwen3-max", + default="qwen3-235b-a22b-instruct-2507", + help="Model name for evaluation (default: qwen3-max)" ) args = parser.parse_args() @@ -825,5 +840,6 @@ if __name__ == "__main__": data_path=args.data_path, top_k=args.top_k, user_num=args.user_num, - max_concurrency=args.max_concurrency + max_concurrency=args.max_concurrency, + eval_model_name=args.eval_model_name ) From 22a6321661fac67ae7e023662ac03127ce0e0f80 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Tue, 27 Jan 2026 01:57:50 +0800 Subject: [PATCH 04/17] refactor(memory): optimize vector store operations and enhance memory management --- benchmark/halumem/eval_reme.py | 8 ++++---- reme/agent/memory/default/reme_retriever.py | 20 +++++++++++++++++++ reme/agent/memory/default/reme_summarizer.py | 20 +++++++++++++++++++ reme/reme.py | 4 ++-- reme/tool/memory/vector/add_memory.py | 2 +- reme/tool/memory/vector/delete_memory.py | 4 ++-- .../memory/vector/retrieve_recent_memory.py | 2 +- reme/tool/memory/vector/update_memory.py | 2 +- 8 files changed, 51 insertions(+), 11 deletions(-) diff --git a/benchmark/halumem/eval_reme.py b/benchmark/halumem/eval_reme.py index 0bb1df4a..d4bcf615 100644 --- a/benchmark/halumem/eval_reme.py +++ b/benchmark/halumem/eval_reme.py @@ -249,7 +249,7 @@ async def evaluation_for_question( dict with 'reasoning' and 'evaluation_result' fields """ prompt = reme.prompt_handler.prompt_format( - "EVALUATION_PROMPT_FOR_QUESTION", + "EVALUATION_PROMPT_FOR_QUESTION2", question=question, reference_answer=reference_answer, key_memory_points=key_memory_points, @@ -305,7 +305,7 @@ class MemoryProcessor: duration_ms = (time.time() - start) * 1000 total_duration_ms += duration_ms - extracted_memories.extend([m.model_dump_json(exclude_none=True) for m in result["answer"]]) + extracted_memories.extend([m.model_dump(exclude_none=True) for m in result["answer"]]) summary_messages.extend([m.simple_dump() for m in result["messages"]]) return extracted_memories, summary_messages, total_duration_ms @@ -336,8 +336,8 @@ class MemoryProcessor: # Extract memories from response memories = result["answer"] - agent_messages = [x.model_dump_json(exclude_none=True) for x in result["messages"]] - retrieved_nodes = [x.model_dump_json(exclude_none=True) for x in result["retrieved_nodes"]] + agent_messages = [x.model_dump(exclude_none=True) for x in result["messages"]] + retrieved_nodes = [x.model_dump(exclude_none=True) for x in result["retrieved_nodes"]] # Use LLM to generate structured answer from memories answer_result = await answer_question_with_memories( diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/default/reme_retriever.py index 659bf08d..a0639f9b 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/default/reme_retriever.py @@ -56,6 +56,26 @@ class ReMeRetriever(BaseMemoryAgent): **kwargs, ) + async def react(self, messages: list[Message], tools: list["BaseTool"], stage: str = ""): + """Run single ReAct step - only one tool call iteration.""" + success: bool = False + used_tools: list[BaseTool] = [] + + # Reasoning: LLM decides next action + assistant_message, should_act = await self._reasoning_step(messages, tools, step=0, stage=stage) + + if should_act: + # Acting: execute tools and collect results (only once) + t_tools, tool_messages = await self._acting_step(assistant_message, tools, step=0, stage=stage) + used_tools.extend(t_tools) + messages.extend(tool_messages) + success = True + else: + # No tools requested + success = True + + return used_tools, messages, success + async def execute(self): result = await super().execute() tools: list[BaseTool] = result["tools"] diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index 6fb91ef8..38ab85d7 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -52,6 +52,26 @@ class ReMeSummarizer(BaseMemoryAgent): **kwargs, ) + async def react(self, messages: list[Message], tools: list["BaseTool"], stage: str = ""): + """Run single ReAct step - only one tool call iteration.""" + success: bool = False + used_tools: list[BaseTool] = [] + + # Reasoning: LLM decides next action + assistant_message, should_act = await self._reasoning_step(messages, tools, step=0, stage=stage) + + if should_act: + # Acting: execute tools and collect results (only once) + t_tools, tool_messages = await self._acting_step(assistant_message, tools, step=0, stage=stage) + used_tools.extend(t_tools) + messages.extend(tool_messages) + success = True + else: + # No tools requested + success = True + + return used_tools, messages, success + async def execute(self): result = await super().execute() tools: list[BaseTool] = result["tools"] diff --git a/reme/reme.py b/reme/reme.py index f611792d..015b962b 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -314,7 +314,7 @@ class ReMe: metadata=metadata, ) vector_node = memory_node.to_vector_node() - await self.vector_store.delete([memory_id, vector_node.vector_id]) + await self.vector_store.delete(list(set([memory_id, vector_node.vector_id]))) await self.vector_store.insert([vector_node]) return memory_node @@ -322,7 +322,7 @@ class ReMe: async def delete_memory(self, memory_id: str | list[str]): """Delete one or more memories from the vector store by their IDs.""" vector_ids = [memory_id] if isinstance(memory_id, str) else memory_id - await self.vector_store.delete(vector_ids) + await self.vector_store.delete(list(set(vector_ids))) async def delete_all_memories(self): """Delete all memories from the vector store.""" diff --git a/reme/tool/memory/vector/add_memory.py b/reme/tool/memory/vector/add_memory.py index 2f608f33..5d6b8528 100644 --- a/reme/tool/memory/vector/add_memory.py +++ b/reme/tool/memory/vector/add_memory.py @@ -101,7 +101,7 @@ class AddMemory(BaseMemoryTool): vector_nodes = [node.to_vector_node() for node in memory_nodes] vector_ids: list[str] = [node.vector_id for node in vector_nodes] - await self.vector_store.delete(vector_ids=vector_ids) + await self.vector_store.delete(vector_ids=list(set(vector_ids))) await self.vector_store.insert(nodes=vector_nodes) self.memory_nodes.extend(memory_nodes) diff --git a/reme/tool/memory/vector/delete_memory.py b/reme/tool/memory/vector/delete_memory.py index 329cedf6..94a22a75 100644 --- a/reme/tool/memory/vector/delete_memory.py +++ b/reme/tool/memory/vector/delete_memory.py @@ -63,8 +63,8 @@ class DeleteMemory(BaseMemoryTool): logger.info(output) return output - await self.vector_store.delete(vector_ids=memory_ids) - self.memory_nodes = memory_ids + await self.vector_store.delete(vector_ids=list(set(memory_ids))) + self.memory_nodes.extend(memory_ids) output = f"Successfully deleted {len(memory_ids)} memories from vector_store." logger.info(output) diff --git a/reme/tool/memory/vector/retrieve_recent_memory.py b/reme/tool/memory/vector/retrieve_recent_memory.py index 391fc72c..c7c6bc7f 100644 --- a/reme/tool/memory/vector/retrieve_recent_memory.py +++ b/reme/tool/memory/vector/retrieve_recent_memory.py @@ -51,7 +51,7 @@ class RetrieveRecentMemory(BaseMemoryTool): retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id} new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids] self.retrieved_nodes.extend(new_memory_nodes) - self.memory_nodes = new_memory_nodes + self.memory_nodes.extend(new_memory_nodes) if not new_memory_nodes: output = "No new memory_nodes found (duplicates removed)." diff --git a/reme/tool/memory/vector/update_memory.py b/reme/tool/memory/vector/update_memory.py index 3bb8f478..ca52582b 100644 --- a/reme/tool/memory/vector/update_memory.py +++ b/reme/tool/memory/vector/update_memory.py @@ -119,7 +119,7 @@ class UpdateMemory(BaseMemoryTool): all_ids_to_delete = list(set(old_memory_ids + new_vector_ids)) await self.vector_store.delete(vector_ids=all_ids_to_delete) await self.vector_store.insert(nodes=vector_nodes) - self.memory_nodes = memory_nodes + self.memory_nodes.extend(memory_nodes) output = f"Successfully updated {len(memory_nodes)} memories in vector_store." logger.info(output) From 2fffb847a0907cdbce20862c641c5baf72953ce9 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 01:18:06 +0800 Subject: [PATCH 05/17] refactor(memory): restructure memory tools and handlers --- pyproject.toml | 2 +- reme/agent/memory/base_memory_agent.py | 55 ++-- reme/agent/memory/default/reme_retriever.py | 4 +- reme/agent/memory/default/reme_retriever.yaml | 2 +- reme/agent/memory/default/reme_summarizer.py | 16 +- .../agent/memory/default/reme_summarizer.yaml | 2 +- reme/core/op/base_op.py | 1 + reme/core/schema/memory_node.py | 32 +++ reme/reme.py | 241 +++++++++--------- reme/tool/memory/__init__.py | 42 ++- reme/tool/memory/{history => }/add_history.py | 13 +- reme/tool/memory/add_memory.py | 135 ++++++++++ reme/tool/memory/base_memory_tool.py | 36 ++- reme/tool/memory/delegate_task.py | 83 ++++++ .../tool/memory/{vector => }/delete_memory.py | 32 +-- reme/tool/memory/hands_off/__init__.py | 0 reme/tool/memory/hands_off/hands_off.py | 108 -------- reme/tool/memory/history/__init__.py | 0 reme/tool/memory/identity/__init__.py | 0 reme/tool/memory/identity/add_identity.py | 42 --- reme/tool/memory/identity/read_identity.py | 36 --- reme/tool/memory/memory_handler.py | 210 +++++++++++++++ reme/tool/memory/meta/__init__.py | 0 reme/tool/memory/meta/add_meta_memory.py | 89 ------- reme/tool/memory/meta/read_meta_memory.py | 75 ------ reme/tool/memory/profile_handler.py | 199 +++++++++++++++ .../tool/memory/{history => }/read_history.py | 6 +- reme/tool/memory/read_profile.py | 45 ++++ reme/tool/memory/retrieve_memory.py | 121 +++++++++ reme/tool/memory/retrieve_recent_memory.py | 52 ++++ reme/tool/memory/update_memory.py | 139 ++++++++++ reme/tool/memory/update_profile.py | 97 +++++++ reme/tool/memory/user_profile/__init__.py | 0 .../memory/user_profile/read_user_profile.py | 63 ----- .../user_profile/update_user_profile.py | 112 -------- reme/tool/memory/vector/__init__.py | 0 reme/tool/memory/vector/add_memory.py | 110 -------- reme/tool/memory/vector/retrieve_memory.py | 136 ---------- .../memory/vector/retrieve_recent_memory.py | 62 ----- reme/tool/memory/vector/update_memory.py | 126 --------- 40 files changed, 1323 insertions(+), 1201 deletions(-) rename reme/tool/memory/{history => }/add_history.py (77%) create mode 100644 reme/tool/memory/add_memory.py create mode 100644 reme/tool/memory/delegate_task.py rename reme/tool/memory/{vector => }/delete_memory.py (59%) delete mode 100644 reme/tool/memory/hands_off/__init__.py delete mode 100644 reme/tool/memory/hands_off/hands_off.py delete mode 100644 reme/tool/memory/history/__init__.py delete mode 100644 reme/tool/memory/identity/__init__.py delete mode 100644 reme/tool/memory/identity/add_identity.py delete mode 100644 reme/tool/memory/identity/read_identity.py create mode 100644 reme/tool/memory/memory_handler.py delete mode 100644 reme/tool/memory/meta/__init__.py delete mode 100644 reme/tool/memory/meta/add_meta_memory.py delete mode 100644 reme/tool/memory/meta/read_meta_memory.py create mode 100644 reme/tool/memory/profile_handler.py rename reme/tool/memory/{history => }/read_history.py (90%) create mode 100644 reme/tool/memory/read_profile.py create mode 100644 reme/tool/memory/retrieve_memory.py create mode 100644 reme/tool/memory/retrieve_recent_memory.py create mode 100644 reme/tool/memory/update_memory.py create mode 100644 reme/tool/memory/update_profile.py delete mode 100644 reme/tool/memory/user_profile/__init__.py delete mode 100644 reme/tool/memory/user_profile/read_user_profile.py delete mode 100644 reme/tool/memory/user_profile/update_user_profile.py delete mode 100644 reme/tool/memory/vector/__init__.py delete mode 100644 reme/tool/memory/vector/add_memory.py delete mode 100644 reme/tool/memory/vector/retrieve_memory.py delete mode 100644 reme/tool/memory/vector/retrieve_recent_memory.py delete mode 100644 reme/tool/memory/vector/update_memory.py diff --git a/pyproject.toml b/pyproject.toml index 47aa478d..63bedc49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,6 @@ Documentation = "https://reme.agentscope.io/" Repository = "https://github.com/agentscope-ai/ReMe" [project.scripts] -reme = "reme_ai.main:main" +reme = "reme.reme:main" # python -m build && twine upload dist/* diff --git a/reme/agent/memory/base_memory_agent.py b/reme/agent/memory/base_memory_agent.py index 6ad3499d..bdb824f4 100644 --- a/reme/agent/memory/base_memory_agent.py +++ b/reme/agent/memory/base_memory_agent.py @@ -1,9 +1,6 @@ """Base memory agent for handling memory operations with tool-based reasoning.""" from abc import ABCMeta -from typing import Literal - -from loguru import logger from ...core.enumeration import MemoryType from ...core.op import BaseReact @@ -15,40 +12,6 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): memory_type: MemoryType | None = None - @staticmethod - async def read_meta_memories(meta_memories: list[dict]) -> str: - """Read and format meta memory information from the provided metadata list.""" - from ...tool.memory import ReadMetaMemory - - meta_memory_info = ReadMetaMemory().format_memory_metadata(meta_memories) - logger.info(f"meta_memory_info={meta_memory_info}") - return meta_memory_info - - async def read_user_profile(self, show_id: Literal["profile", "history"] = "profile") -> str: - """Read current user profile.""" - from ...tool.memory import ReadUserProfile - - read_tool = ReadUserProfile(show_id=show_id) - await read_tool.call(memory_target=self.memory_target, service_context=self.service_context) - return str(read_tool.response.answer) - - async def add_history_node(self) -> MemoryNode: - """Add history node""" - from ...tool.memory import AddHistory - - add_history_tool = AddHistory() - await add_history_tool.call( - messages=self.messages, - description=self.description, - service_context=self.service_context, - ) - return add_history_tool.context.history_node - - @property - def memory_target(self) -> str: - """memory_target""" - return self.context.get("memory_target", "") - @property def query(self) -> str: """query""" @@ -64,6 +27,11 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): """description""" return self.context.get("description", "") + @property + def memory_target(self) -> str: + """memory_target""" + return self.context.memory_target + @property def history_node(self) -> MemoryNode: """Returns the history node.""" @@ -80,3 +48,16 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): if "retrieved_nodes" not in self.context: self.context.retrieved_nodes = [] return self.context.retrieved_nodes + + @property + def memory_target_type_mapping(self) -> dict[str, MemoryType]: + """Get the memory target type mapping from context.""" + return self.context.memory_target_type_mapping + + @property + def meta_memory_info(self) -> str: + """Get the meta memory info from context.""" + lines = ["Format: - memory_target: memory_type memories about memory_target"] + for memory_target, memory_type in self.memory_target_type_mapping.items(): + lines.append(f"- {memory_target}: {memory_type} memories about {memory_target}") + return "\n".join(lines) diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/default/reme_retriever.py index a0639f9b..b7a917f9 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/default/reme_retriever.py @@ -79,8 +79,8 @@ class ReMeRetriever(BaseMemoryAgent): async def execute(self): result = await super().execute() tools: list[BaseTool] = result["tools"] - hands_off_tool = tools[0] - agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"] + delegate_task_tool = tools[0] + agents: list[BaseMemoryAgent] = delegate_task_tool.response.metadata["agents"] answer = [] success = True diff --git a/reme/agent/memory/default/reme_retriever.yaml b/reme/agent/memory/default/reme_retriever.yaml index 849f562e..11d7186e 100644 --- a/reme/agent/memory/default/reme_retriever.yaml +++ b/reme/agent/memory/default/reme_retriever.yaml @@ -10,7 +10,7 @@ system_prompt: | {meta_memory_info} ## Your Task - Use the `hands_off` tool to retrieve information from specialized agents: + Use the `delegate_task` tool to retrieve information from specialized agents: 1. Analyze the user query and identify which memory dimensions are relevant 2. Specify `memory_type` and `memory_target` for each retrieval task - The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index 38ab85d7..c9b4db81 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -13,6 +13,18 @@ class ReMeSummarizer(BaseMemoryAgent): super().__init__(**kwargs) self.meta_memories: list[dict] = meta_memories or [] + async def add_history_node(self) -> MemoryNode: + """Add history node""" + from ...tool.memory import AddHistory + + add_history_tool = AddHistory() + await add_history_tool.call( + messages=self.messages, + description=self.description, + service_context=self.service_context, + ) + return add_history_tool.context.history_node + async def build_messages(self) -> list[Message]: self.context.history_node = await self.add_history_node() @@ -75,8 +87,8 @@ class ReMeSummarizer(BaseMemoryAgent): async def execute(self): result = await super().execute() tools: list[BaseTool] = result["tools"] - hands_off_tool = tools[0] - agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"] + delegate_task_tool = tools[0] + agents: list[BaseMemoryAgent] = delegate_task_tool.response.metadata["agents"] success = True messages = [] diff --git a/reme/agent/memory/default/reme_summarizer.yaml b/reme/agent/memory/default/reme_summarizer.yaml index d31e0213..d0a6748e 100644 --- a/reme/agent/memory/default/reme_summarizer.yaml +++ b/reme/agent/memory/default/reme_summarizer.yaml @@ -10,7 +10,7 @@ system_prompt: | {meta_memory_info} ## Your Task - Use the `hands_off` tool to distribute memory tasks to specialized agents: + Use the `delegate_task` tool to distribute memory tasks to specialized agents: 1. Analyze the context and identify which memory dimensions require updates 2. Specify `memory_type` and `memory_target` for each task - The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above diff --git a/reme/core/op/base_op.py b/reme/core/op/base_op.py index 9c2dff71..6d32ddc0 100644 --- a/reme/core/op/base_op.py +++ b/reme/core/op/base_op.py @@ -115,6 +115,7 @@ class BaseOp(metaclass=ABCMeta): @property def service_context(self) -> ServiceContext: """Access the service context.""" + assert self.context, "Service context is not initialized!" return self.context.service_context @property diff --git a/reme/core/schema/memory_node.py b/reme/core/schema/memory_node.py index 67ed7c43..3cf096ac 100644 --- a/reme/core/schema/memory_node.py +++ b/reme/core/schema/memory_node.py @@ -36,6 +36,7 @@ class MemoryNode(BaseModel): memory_target: Target or topic this memory relates to. when_to_use: Condition description for vector retrieval. content: Actual memory content. + message_time: Time of the message that generated this memory. ref_memory_id: Reference to related raw history memory. time_created: Creation timestamp. time_modified: Last modification timestamp. @@ -49,6 +50,7 @@ class MemoryNode(BaseModel): memory_target: str = Field(default="", description="Target or topic of the memory") when_to_use: str = Field(default="", description="Condition description for vector retrieval") content: str = Field(default="", description="Actual memory content") + message_time: str = Field(default="", description="Time of the message that generated this memory") ref_memory_id: str = Field(default="", description="Reference to related raw history memory ID") time_created: str = Field(default_factory=get_now_time, description="Creation timestamp") @@ -123,6 +125,7 @@ class MemoryNode(BaseModel): metadata: dict[str, Any] = { "memory_type": self.memory_type.value, "memory_target": self.memory_target, + "message_time": self.message_time, "ref_memory_id": self.ref_memory_id, "time_created": self.time_created, "time_modified": self.time_modified, @@ -145,6 +148,34 @@ class MemoryNode(BaseModel): metadata=metadata, ) + def format( + self, + include_memory_id: bool = True, + include_when_to_use: bool = True, + include_content: bool = True, + include_message_time: bool = True, + ref_memory_id_key: str = "", + ) -> str: + """Format memory node as string with configurable fields.""" + line = "" + + if include_memory_id and self.memory_id: + line += f"memory_id={self.memory_id} " + + if include_message_time and self.message_time: + line += f"[{self.message_time}] " + + if include_when_to_use and self.when_to_use: + line += f"{self.when_to_use} " + + if include_content and self.content: + line += self.content.strip() + + if ref_memory_id_key and self.ref_memory_id: + line += f" {ref_memory_id_key}={self.ref_memory_id}" + + return line.strip() + @classmethod def from_vector_node(cls, node: VectorNode) -> "MemoryNode": """Reconstruct MemoryNode from VectorNode. @@ -189,6 +220,7 @@ class MemoryNode(BaseModel): memory_target=metadata.pop("memory_target", ""), when_to_use=when_to_use, content=content, + message_time=metadata.pop("message_time", ""), ref_memory_id=metadata.pop("ref_memory_id", ""), time_created=metadata.pop("time_created", ""), time_modified=metadata.pop("time_modified", ""), diff --git a/reme/reme.py b/reme/reme.py index 015b962b..0c4a0fc2 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -2,6 +2,9 @@ import asyncio import sys +from pathlib import Path + +from loguru import logger from .agent.memory.default import ReMeSummarizer, PersonalSummarizer, PersonalRetriever, ReMeRetriever from .config import ReMeConfigParser @@ -14,7 +17,8 @@ from .core.schema import Response, Message, MemoryNode, VectorNode from .core.token_counter import BaseTokenCounter from .core.utils import execute_stream_task, get_now_time from .core.vector_store import BaseVectorStore -from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, HandsOff, ReadHistory, ReadUserProfile +from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, DelegateTask, ReadHistory, ReadUserProfile, \ + ProfileHandler class ReMe: @@ -32,8 +36,38 @@ class ReMe: embedding_model: dict | None = None, vector_store: dict | None = None, token_counter: dict | None = None, + personal_memory_target: list[str] | None = None, + procedural_memory_target: list[str] | None = None, + tool_memory_target: list[str] | None = None, + profile_path: str = "reme_profile", + main_summary_version: str = "default", + personal_summary_version: str = "default", + procedural_summary_version: str = "default", + tool_summary_version: str = "default", + main_retrieve_version: str = "default", + personal_retrieve_version: str = "default", + procedural_retrieve_version: str = "default", + tool_retrieve_version: str = "default", **kwargs, ): + # MemoryTarget -> MemoryType + memory_target_type_mapping: dict[str, MemoryType] = {} + if personal_memory_target: + for name in personal_memory_target: + assert name not in memory_target_type_mapping, f"Memory target name {name} is already used." + memory_target_type_mapping[name] = MemoryType.PERSONAL + + if procedural_memory_target: + for name in procedural_memory_target: + assert name not in memory_target_type_mapping, f"Memory target name {name} is already used." + memory_target_type_mapping[name] = MemoryType.PROCEDURAL + + if tool_memory_target: + for name in tool_memory_target: + assert name not in memory_target_type_mapping, f"Memory target name {name} is already used." + memory_target_type_mapping[name] = MemoryType.TOOL + + # ServiceContext self.service_context = ServiceContext( *args, llm_api_key=llm_api_key, @@ -48,64 +82,73 @@ class ReMe: embedding_model=embedding_model, vector_store=vector_store, token_counter=token_counter, + memory_target_type_mapping=memory_target_type_mapping, **kwargs, ) + self.profile_path: str = profile_path + + # PromptHandler self.prompt_handler = PromptHandler(language=self.service_context.language) - async def __aenter__(self): - """Async context manager entry.""" - return self - - def __enter__(self): - """Context manager entry.""" - return self - - async def close(self): - """Close""" - return await self.service_context.close() - - def close_sync(self): - """Close synchronously""" - self.service_context.close_sync() - - async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None): - """Async context manager exit.""" - await self.close() - return False - - def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): - """Context manager exit.""" - self.close_sync() - return False + # LLM & EmbeddingModel & VectorStore & TokenCounter + self.llm: BaseLLM | None = self.service_context.llms.get("default", None) + self.embedding_model: BaseEmbeddingModel | None = self.service_context.embedding_models.get("default", None) + self.vector_store: BaseVectorStore | None = self.service_context.vector_stores.get("default", None) + self.token_counter: BaseTokenCounter | None = self.service_context.token_counters.get("default", None) @property - def llm(self) -> BaseLLM: - """Return the default LLM instance from the service context.""" - return self.service_context.llms["default"] + def memory_target_type_mapping(self) -> dict[str, MemoryType]: + mapping = {} + if self.service_context.personal_memory_target: + for name in self.service_context.personal_memory_target: + assert name not in mapping, f"Memory target name {name} is already used." + mapping[name] = MemoryType.PERSONAL - @property - def embedding_model(self) -> BaseEmbeddingModel: - """Return the default embedding model instance from the service context.""" - return self.service_context.embedding_models["default"] + if self.service_context.procedural_memory_target: + for name in self.service_context.procedural_memory_target: + assert name not in mapping, f"Memory target name {name} is already used." + mapping[name] = MemoryType.PROCEDURAL + + if self.service_context.tool_memory_target: + for name in self.service_context.tool_memory_target: + assert name not in mapping, f"Memory target name {name} is already used." + mapping[name] = MemoryType.TOOL + return mapping + + def add_meta_memory(self, memory_type: str | MemoryType, memory_target: str): + memory_type = MemoryType(memory_type) + if memory_type is MemoryType.PERSONAL: + personal_memory_target = self.service_context.personal_memory_target + if memory_target not in personal_memory_target: + personal_memory_target.append(memory_target) + else: + logger.warning(f"Memory target {memory_target} is already added.") + + elif memory_type is MemoryType.PROCEDURAL: + procedural_memory_target = self.service_context.procedural_memory_target + if memory_target not in procedural_memory_target: + procedural_memory_target.append(memory_target) + else: + logger.warning(f"Memory target {memory_target} is already added.") + + elif memory_type is MemoryType.TOOL: + tool_memory_target = self.service_context.tool_memory_target + if memory_target not in tool_memory_target: + tool_memory_target.append(memory_target) + else: + logger.warning(f"Memory target {memory_target} is already added.") - @property - def vector_store(self) -> BaseVectorStore: - """Return the default vector store instance from the service context.""" - return self.service_context.vector_stores["default"] - @property - def token_counter(self) -> BaseTokenCounter: - """Return the default token counter instance from the service context.""" - return self.service_context.token_counters["default"] async def summary_memory( self, messages: list[Message | dict], description: str = "", - user_name: str | list[str] = "", + user_name: str = "", + task_name: str = "", + tool_name: str = "", enable_thinking_params: bool = False, - meta_memories: list[dict] = None, version: str = "default", return_dict: bool = False, **kwargs, @@ -133,7 +176,7 @@ class ReMe: reme_summarizer = ReMeSummarizer( meta_memories=meta_memories, tools=[ - HandsOff( + DelegateTask( memory_agents=[ PersonalSummarizer( tools=[ @@ -202,7 +245,7 @@ class ReMe: reme_retriever = ReMeRetriever( meta_memories=meta_memories, tools=[ - HandsOff( + DelegateTask( memory_agents=[ PersonalRetriever( tools=[ @@ -341,84 +384,10 @@ class ReMe: """Retrieve all memories from the vector store.""" return [node.to_memory_node() for node in await self.vector_store.list()] - async def get_profiles(self, user_name: str | list[str]) -> str | list[str]: - """Retrieve user profile(s) from the system for the specified user(s).""" - read_profile = ReadUserProfile(show_id="profile") - if isinstance(user_name, str): - return await read_profile.call(memory_target=user_name, service_context=self.service_context) - else: - return [ - await read_profile.call(memory_target=name, service_context=self.service_context) for name in user_name - ] - - async def add_profile( - self, - profile_key: str, - profile_value: str, - user_name: str, - update_time: str | None = None, - ) -> MemoryNode: - """Add user profile to ReMe system.""" - update_user_profile = UpdateUserProfile() - if update_time is None: - update_time = get_now_time() - - await update_user_profile.call( - profile_ids_to_delete=[], - profiles_to_add=[ - { - "update_time": update_time, - "profile_key": profile_key, - "profile_value": profile_value, - }, - ], - memory_target=user_name, - service_context=self.service_context, - ) - return update_user_profile.memory_nodes[0] - - async def update_profile( - self, - profile_id: str, - profile_key: str, - profile_value: str, - user_name: str, - update_time: str | None = None, - ) -> MemoryNode: - """Add user profile to ReMe system.""" - update_user_profile = UpdateUserProfile() - if update_time is None: - update_time = get_now_time() - - await update_user_profile.call( - profile_ids_to_delete=[profile_id], - profiles_to_add=[ - { - "update_time": update_time, - "profile_key": profile_key, - "profile_value": profile_value, - }, - ], - memory_target=user_name, - service_context=self.service_context, - ) - return update_user_profile.memory_nodes[0] - - async def delete_all_profiles(self, user_name: str | list[str]): - """Delete all user profiles from ReMe system.""" - if isinstance(user_name, str): - user_name = [user_name] - - read_profile = ReadUserProfile(show_id="profile") - update_profile = UpdateUserProfile() - for memory_target in user_name: - await read_profile.call(memory_target=memory_target, service_context=self.service_context) - profile_ids = [profile.memory_id for profile in read_profile.memory_nodes] - await update_profile.call( - profile_ids_to_delete=profile_ids, - memory_target=memory_target, - service_context=self.service_context, - ) + def get_profile_handler(self, user_name: str) -> ProfileHandler: + """Get the profile handler for the specified user.""" + profile_path = Path(self.profile_path) / self.vector_store.collection_name + return ProfileHandler(memory_target=user_name, profile_path=profile_path) async def context_offload(self): """working memory summary""" @@ -451,6 +420,32 @@ class ReMe: """Run the configured service (HTTP, MCP, or CMD).""" self.service_context.service.run() + async def __aenter__(self): + """Async context manager entry.""" + return self + + def __enter__(self): + """Context manager entry.""" + return self + + async def close(self): + """Close""" + return await self.service_context.close() + + def close_sync(self): + """Close synchronously""" + self.service_context.close_sync() + + async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None): + """Async context manager exit.""" + await self.close() + return False + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + """Context manager exit.""" + self.close_sync() + return False + def main(): """Main entry point for running ReMe from command line.""" diff --git a/reme/tool/memory/__init__.py b/reme/tool/memory/__init__.py index 9c8bbd11..52e86d80 100644 --- a/reme/tool/memory/__init__.py +++ b/reme/tool/memory/__init__.py @@ -1,40 +1,34 @@ """memory tools""" +from .add_history import AddHistory +from .add_memory import AddMemory from .base_memory_tool import BaseMemoryTool -from .hands_off.hands_off import HandsOff -from .history.add_history import AddHistory -from .history.read_history import ReadHistory -from .identity.add_identity import AddIdentity -from .identity.read_identity import ReadIdentity -from .meta.add_meta_memory import AddMetaMemory -from .meta.read_meta_memory import ReadMetaMemory -from .user_profile.read_user_profile import ReadUserProfile -from .user_profile.update_user_profile import UpdateUserProfile -from .vector.add_memory import AddMemory -from .vector.delete_memory import DeleteMemory -from .vector.retrieve_memory import RetrieveMemory -from .vector.retrieve_recent_memory import RetrieveRecentMemory -from .vector.update_memory import UpdateMemory +from .delegate_task import DelegateTask +from .delete_memory import DeleteMemory +from .profile_handler import ProfileHandler +from .read_history import ReadHistory +from .read_profile import ReadProfile +from .retrieve_memory import RetrieveMemory +from .retrieve_recent_memory import RetrieveRecentMemory +from .update_memory import UpdateMemory +from .update_profile import UpdateProfile from ...core import R __all__ = [ - "BaseMemoryTool", - "HandsOff", "AddHistory", - "ReadHistory", - "AddIdentity", - "ReadIdentity", - "AddMetaMemory", - "ReadMetaMemory", - "ReadUserProfile", - "UpdateUserProfile", "AddMemory", + "BaseMemoryTool", + "DelegateTask", "DeleteMemory", + "ProfileHandler", + "ReadHistory", + "ReadProfile", "RetrieveMemory", "RetrieveRecentMemory", "UpdateMemory", + "UpdateProfile", ] for name in __all__: tool_class = globals()[name] - R.op.register()(tool_class) + R.op.register()(tool_class) \ No newline at end of file diff --git a/reme/tool/memory/history/add_history.py b/reme/tool/memory/add_history.py similarity index 77% rename from reme/tool/memory/history/add_history.py rename to reme/tool/memory/add_history.py index d9527ff8..57a0097e 100644 --- a/reme/tool/memory/history/add_history.py +++ b/reme/tool/memory/add_history.py @@ -2,10 +2,10 @@ from loguru import logger -from ..base_memory_tool import BaseMemoryTool -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall, MemoryNode, Message -from ....core.utils import format_messages +from .base_memory_tool import BaseMemoryTool +from ...core.enumeration import MemoryType +from ...core.schema import ToolCall, MemoryNode, Message +from ...core.utils import format_messages class AddHistory(BaseMemoryTool): @@ -31,10 +31,11 @@ class AddHistory(BaseMemoryTool): async def execute(self): """Execute the add history operation""" self.context.messages = [Message(**m) if isinstance(m, dict) else m for m in self.context.messages] - history_content: str = (self.context.description + "\n" + format_messages(self.context.messages)).strip() + history_content: str = self.context.description + "\n" + format_messages(self.context.messages) + history_content = history_content.strip() history_node = MemoryNode( memory_type=MemoryType.HISTORY, - when_to_use=history_content[:100], + when_to_use=history_content[:1024], content=history_content, author=self.author, ) diff --git a/reme/tool/memory/add_memory.py b/reme/tool/memory/add_memory.py new file mode 100644 index 00000000..8ecdf965 --- /dev/null +++ b/reme/tool/memory/add_memory.py @@ -0,0 +1,135 @@ +"""Add memory to vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall + + +class AddMemory(BaseMemoryTool): + """Tool to add memories to vector store""" + + def __init__( + self, + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, + ): + super().__init__(**kwargs) + self.enable_memory_target: bool = enable_memory_target + self.enable_when_to_use: bool = enable_when_to_use + + def _build_memory_parameters(self) -> dict: + """Build the memory parameters schema based on enabled features.""" + properties = { + "message_time": { + "type": "string", + "description": "message time, e.g. '2020-01-01 00:00:00'", + }, + "memory_content": { + "type": "string", + "description": "content of the memory.", + }, + } + required = ["message_time", "memory_content"] + + if self.enable_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": "description of when to use this memory.", + } + required.append("when_to_use") + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "target memory type for this memory.", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "add a memory to vector store for future retrieval.", + "parameters": self._build_memory_parameters(), + }, + ) + + def _build_multiple_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "add multiple memories to vector store for future retrieval.", + "parameters": { + "type": "object", + "properties": { + "memories": { + "type": "array", + "description": "list of memories to store.", + "items": self._build_memory_parameters(), + }, + }, + "required": ["memories"], + }, + }, + ) + + async def execute(self): + if self.enable_multiple: + memories = self.context.get("memories", []) + else: + memories = [self.context] + + # Group memories by memory_target if enabled + if self.enable_memory_target: + memories_by_target = {} + for mem in memories: + target = mem["memory_target"] + if target not in memories_by_target: + memories_by_target[target] = [] + memories_by_target[target].append(mem) + else: + memories_by_target = {self.memory_target: memories} + + # Process each memory_target group + all_memory_nodes = [] + for target, target_memories in memories_by_target.items(): + # Parse and prepare memory data + memory_dicts = [] + for mem in target_memories: + memory_content = mem.get("memory_content", "") + message_time = mem.get("message_time", "") + when_to_use = mem.get("when_to_use", "") if self.enable_when_to_use else "" + metadata = {} + try: + metadata["time_int"] = int(message_time.split(" ")[0].replace("-", "")) + except Exception: + logger.warning(f"Invalid message time format: {message_time}") + + memory_dicts.append({ + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "ref_memory_id": self.history_node.memory_id, + "author": self.author, + "metadata": metadata, + }) + + if memory_dicts: + handler = MemoryHandler(target, self.service_context) + memory_nodes = await handler.add_batch(memory_dicts) + all_memory_nodes.extend(memory_nodes) + + if not all_memory_nodes: + return "No valid memories provided." + + self.memory_nodes.extend(all_memory_nodes) + output = f"Successfully added {len(all_memory_nodes)} memories." + logger.info(output) + return output diff --git a/reme/tool/memory/base_memory_tool.py b/reme/tool/memory/base_memory_tool.py index 5a900177..0f253b98 100644 --- a/reme/tool/memory/base_memory_tool.py +++ b/reme/tool/memory/base_memory_tool.py @@ -1,12 +1,10 @@ """Base class for memory tool""" from abc import ABCMeta -from pathlib import Path from ...core.enumeration import MemoryType from ...core.op import BaseTool from ...core.schema import ToolCall, MemoryNode, ToolAttr -from ...core.utils import CacheHandler class BaseMemoryTool(BaseTool, metaclass=ABCMeta): @@ -16,13 +14,11 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): self, enable_multiple: bool = True, enable_thinking_params: bool = False, - local_memory_path: str = "./reme_local_memory", **kwargs, ): super().__init__(**kwargs) self.enable_multiple: bool = enable_multiple self.enable_thinking_params: bool = enable_thinking_params - self.local_memory_path: str = local_memory_path def _build_tool_call(self) -> ToolCall: """Build and return the tool call schema""" @@ -59,44 +55,44 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): parameters.required = ["thinking"] return self._tool_call - @property - def local_memory(self) -> CacheHandler: - """Create the meta memory cache handler.""" - return CacheHandler(Path(self.local_memory_path) / self.vector_store.collection_name) - @property def memory_type(self) -> MemoryType: """Get the memory type from context.""" - return MemoryType(self.context.get("memory_type")) + return self.memory_target_type_mapping[self.memory_target] @property def memory_target(self) -> str: """Get the memory target from context.""" - return self.context.get("memory_target", "") - - @property - def memory_cache_key(self) -> str: - """Get the memory cache key from context.""" - return f"{self.memory_type.value}_{self.memory_target}".replace(" ", "_").lower() + if "memory_target" in self.context: + return self.context.memory_target + elif len(self.memory_target_type_mapping) == 1: + return list(self.memory_target_type_mapping.keys())[0] + else: + raise ValueError("memory_target is not specified in context or memory_target_type_mapping!") @property def history_node(self) -> MemoryNode: """Get the history node from context.""" - return self.context.get("history_node") + return self.context.history_node @property def retrieved_nodes(self) -> list[MemoryNode]: """Get the retrieved nodes from context.""" - return self.context["retrieved_nodes"] + return self.context.retrieved_nodes @property def author(self) -> str: """Get the author from context.""" - return self.context.get("author", "") + return self.context.author @property def memory_nodes(self) -> list[MemoryNode | str]: """Get the memory nodes from context.""" if "memory_nodes" not in self.context: self.context.memory_nodes = [] - return self.context["memory_nodes"] + return self.context.memory_nodes + + @property + def memory_target_type_mapping(self) -> dict[str, MemoryType]: + """Get the memory target type mapping from context.""" + return self.context.memory_target_type_mapping diff --git a/reme/tool/memory/delegate_task.py b/reme/tool/memory/delegate_task.py new file mode 100644 index 00000000..13c1ff07 --- /dev/null +++ b/reme/tool/memory/delegate_task.py @@ -0,0 +1,83 @@ +"""Hands-off tool to delegate memory tasks to specific agents""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from ...agent.memory import BaseMemoryAgent +from ...core.enumeration import MemoryType +from ...core.schema import ToolCall + + +class DelegateTask(BaseMemoryTool): + """Tool to delegate memory tasks to appropriate memory agents""" + + def __init__(self, memory_agents: list[BaseMemoryAgent] = None, **kwargs): + kwargs["enable_multiple"] = True + kwargs["sub_ops"] = memory_agents or [] + super().__init__(**kwargs) + self.sub_ops: list[BaseMemoryAgent] = [a for a in self.sub_ops if isinstance(a, BaseMemoryAgent)] + assert all(a.memory_type is not None for a in self.sub_ops) + + @property + def memory_agent_dict(self) -> dict[MemoryType, BaseMemoryAgent]: + """Map memory types to their corresponding agents""" + return {a.memory_type: a for a in self.sub_ops} + + def _build_multiple_tool_call(self) -> ToolCall: + """Build and return the multiple tool call schema""" + return ToolCall( + **{ + "description": "Delegate tasks to appropriate agents.", + "parameters": { + "type": "object", + "properties": { + "tasks": { + "type": "array", + "description": "tasks to delegate to specific agents", + "items": { + "type": "object", + "properties": { + "task_name": { + "type": "string", + "description": "task_name", + }, + }, + "required": ["task_name"], + }, + }, + }, + "required": ["tasks"], + }, + }, + ) + + async def execute(self): + # Deduplicate and validate tasks + tasks = self.context.get("tasks", []) + tasks = sorted(set(tasks)) + + # Submit tasks to agents + agent_list: list[BaseMemoryAgent] = [] + for i, task in enumerate(tasks): + memory_type = self.memory_target_type_mapping[task] + agent = self.memory_agent_dict[memory_type].copy() + agent_list.append(agent) + + logger.info(f"Task {i}: {memory_type.value} agent for {task}") + task_kwargs = {"memory_target": task} + for k in ["query", "messages", "description", "history_node"]: + if k in self.context: + task_kwargs[k] = self.context[k] + self.submit_async_task(agent.call, service_context=self.service_context, **task_kwargs) + await self.join_async_tasks() + + # Collect results + results = [] + for agent in agent_list: + results.append(f"Task: {agent.memory_target}\n{agent.response.answer}") + + logger.info(f"Completed {len(results)} task(s)") + return { + "answer": "\n\n".join(results), + "agents": agent_list, + } diff --git a/reme/tool/memory/vector/delete_memory.py b/reme/tool/memory/delete_memory.py similarity index 59% rename from reme/tool/memory/vector/delete_memory.py rename to reme/tool/memory/delete_memory.py index 94a22a75..e95cfa40 100644 --- a/reme/tool/memory/vector/delete_memory.py +++ b/reme/tool/memory/delete_memory.py @@ -2,15 +2,15 @@ from loguru import logger -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall class DeleteMemory(BaseMemoryTool): """Tool to delete memories from vector store""" def _build_tool_call(self) -> ToolCall: - """Build and return the single tool call schema""" return ToolCall( **{ "description": "delete a memory from vector store using its unique ID.", @@ -19,7 +19,7 @@ class DeleteMemory(BaseMemoryTool): "properties": { "memory_id": { "type": "string", - "description": "unique identifier (memory_id) of the memory to delete.", + "description": "memory_id of the memory to delete.", }, }, "required": ["memory_id"], @@ -28,7 +28,6 @@ class DeleteMemory(BaseMemoryTool): ) def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" return ToolCall( **{ "description": "delete multiple memories from vector store using their unique IDs.", @@ -37,7 +36,7 @@ class DeleteMemory(BaseMemoryTool): "properties": { "memory_ids": { "type": "array", - "description": "list of unique identifiers (memory_ids) of memories to delete.", + "description": "memory_ids of memories to delete.", "items": {"type": "string"}, }, }, @@ -47,25 +46,14 @@ class DeleteMemory(BaseMemoryTool): ) async def execute(self): - memory_ids: list[str] = [] - - # Handle multiple memories (array format) - ids_from_array = self.context.get("memory_ids", []) - if ids_from_array: - memory_ids = [m for m in ids_from_array if m] - else: - memory_id = self.context.get("memory_id", "") - if memory_id: - memory_ids = [memory_id] - + memory_ids = self.context.get("memory_ids") or [] if not memory_ids: - output = "No valid memory IDs provided for deletion." - logger.info(output) - return output + memory_ids = [self.context.get("memory_id", "")] - await self.vector_store.delete(vector_ids=list(set(memory_ids))) + handler = MemoryHandler(self.memory_target, self.service_context) + await handler.delete(memory_ids) self.memory_nodes.extend(memory_ids) - output = f"Successfully deleted {len(memory_ids)} memories from vector_store." + output = f"Successfully deleted {len(memory_ids)} memories." logger.info(output) return output diff --git a/reme/tool/memory/hands_off/__init__.py b/reme/tool/memory/hands_off/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/hands_off/hands_off.py b/reme/tool/memory/hands_off/hands_off.py deleted file mode 100644 index 45822381..00000000 --- a/reme/tool/memory/hands_off/hands_off.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Hands-off tool to delegate memory tasks to specific agents""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....agent.memory import BaseMemoryAgent -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall - - -class HandsOff(BaseMemoryTool): - """Tool to delegate memory tasks to appropriate memory agents""" - - def __init__(self, memory_agents: list[BaseMemoryAgent] = None, **kwargs): - kwargs["enable_multiple"] = True - kwargs["sub_ops"] = memory_agents or [] - super().__init__(**kwargs) - self.sub_ops: list[BaseMemoryAgent] = [ - a for a in self.sub_ops if isinstance(a, BaseMemoryAgent) and a.memory_type is not None - ] - - @property - def memory_agent_dict(self) -> dict[MemoryType, BaseMemoryAgent]: - """Map memory types to their corresponding agents""" - return {a.memory_type: a for a in self.sub_ops} - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "Delegate memory tasks to appropriate agents.", - "parameters": { - "type": "object", - "properties": { - "memory_tasks": { - "type": "array", - "description": "Memory tasks to delegate to specific agents", - "items": { - "type": "object", - "properties": { - "memory_type": { - "type": "string", - "description": "Memory type to handle", - "enum": [k.value for k in self.memory_agent_dict if k], - }, - "memory_target": { - "type": "string", - "description": "Target or context for the memory operation", - }, - }, - "required": ["memory_type", "memory_target"], - }, - }, - }, - "required": ["memory_tasks"], - }, - }, - ) - - async def execute(self): - # Deduplicate and validate tasks - tasks = [] - seen = set() - for task in self.context.get("memory_tasks", []): - memory_type = MemoryType(task.get("memory_type", "")) - memory_target = task.get("memory_target", "") - - task_key = (memory_type, memory_target) - if task_key in seen: - logger.info(f"Skip duplicate: {memory_type.value} - {memory_target}") - continue - seen.add(task_key) - - tasks.append({"memory_type": memory_type, "memory_target": memory_target}) - - if not tasks: - return "No valid memory tasks to execute." - - # Submit tasks to agents - agent_list: list[BaseMemoryAgent] = [] - for i, task in enumerate(tasks): - memory_type: MemoryType = task["memory_type"] - memory_target: str = task["memory_target"] - - agent = self.memory_agent_dict[memory_type].copy() - agent_list.append(agent) - - logger.info(f"Task {i}: {memory_type.value} agent for {memory_target}") - task_kwargs = {"memory_type": memory_type, "memory_target": memory_target} - for k in ["query", "messages", "description", "history_node"]: - if k in self.context: - task_kwargs[k] = self.context[k] - self.submit_async_task(agent.call, service_context=self.service_context, **task_kwargs) - - await self.join_async_tasks() - - # Collect results - results = [] - for agent in agent_list: - memory_type = agent.memory_type - memory_target = agent.memory_target - results.append(f"{memory_type.value}({memory_target}): {agent.response.answer}") - - logger.info(f"Completed {len(results)} task(s)") - return { - "answer": "\n".join(results), - "agents": agent_list, - } diff --git a/reme/tool/memory/history/__init__.py b/reme/tool/memory/history/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/identity/__init__.py b/reme/tool/memory/identity/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/identity/add_identity.py b/reme/tool/memory/identity/add_identity.py deleted file mode 100644 index 5f6ab984..00000000 --- a/reme/tool/memory/identity/add_identity.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Add identity memory tool""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall - - -class AddIdentity(BaseMemoryTool): - """Tool to add or update agent identity memory""" - - def __init__(self, **kwargs): - kwargs["enable_multiple"] = False - super().__init__(**kwargs) - - def _build_tool_call(self) -> ToolCall: - return ToolCall( - **{ - "description": "add or update agent identity memory.", - "parameters": { - "type": "object", - "properties": { - "identity_memory": { - "type": "string", - "description": "Agent identity content, such as role, personality, or current state.", - }, - }, - "required": ["identity_memory"], - }, - }, - ) - - async def execute(self): - identity_memory = self.context.get("identity_memory", "") - - if not identity_memory: - logger.warning("No valid identity memory provided") - return "No valid identity memory provided for update." - - self.local_memory.save("identity_memory", identity_memory) - logger.info(f"Successfully updated identity memory: {identity_memory}") - return "Successfully updated identity memory." diff --git a/reme/tool/memory/identity/read_identity.py b/reme/tool/memory/identity/read_identity.py deleted file mode 100644 index 856aea55..00000000 --- a/reme/tool/memory/identity/read_identity.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Read identity memory tool""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall - - -class ReadIdentity(BaseMemoryTool): - """Tool to read agent identity memory""" - - def __init__(self, **kwargs): - kwargs["enable_multiple"] = False - super().__init__(**kwargs) - - def _build_tool_call(self) -> ToolCall: - return ToolCall( - **{ - "description": "read agent identity memory.", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - ) - - async def execute(self): - identity_memory = self.local_memory.load("identity_memory") - - if not identity_memory: - logger.info("No identity memory found") - return "No identity memory found." - - logger.info(f"Read identity memory: {identity_memory}") - return f"Identity\n{identity_memory}" diff --git a/reme/tool/memory/memory_handler.py b/reme/tool/memory/memory_handler.py new file mode 100644 index 00000000..78151325 --- /dev/null +++ b/reme/tool/memory/memory_handler.py @@ -0,0 +1,210 @@ +from ...core.context import ServiceContext +from ...core.enumeration import MemoryType +from ...core.schema import MemoryNode +from ...core.vector_store import BaseVectorStore + + +class MemoryHandler: + """Handler for managing memory nodes in the vector store.""" + + def __init__(self, memory_target: str, service_context: ServiceContext): + self.memory_target: str = memory_target + self.memory_type: MemoryType = service_context.memory_target_type_mapping[memory_target] + self.vector_store: BaseVectorStore = service_context.vector_stores["default"] + + async def add_batch(self, memories: list[dict]) -> list[MemoryNode]: + """Add multiple memory nodes and return their memory_ids.""" + # First, delete existing memory nodes if memory_ids are provided + memory_ids_to_delete = [mem.get("memory_id") for mem in memories if mem.get("memory_id")] + if memory_ids_to_delete: + await self.vector_store.delete(memory_ids_to_delete) + + # Create MemoryNode objects + memory_nodes = [ + MemoryNode( + memory_type=self.memory_type, + memory_target=self.memory_target, + content=mem.get("content", ""), + when_to_use=mem.get("when_to_use", ""), + message_time=mem.get("message_time", ""), + ref_memory_id=mem.get("ref_memory_id", ""), + author=mem.get("author", ""), + score=mem.get("score", 0.0), + metadata=mem.get("metadata", {}), + ) + for mem in memories + ] + + # Deduplicate memory_nodes by content (keep last occurrence) + memory_dict = {node.content: node for node in memory_nodes} + memory_nodes = list(memory_dict.values()) + + # Convert to VectorNodes and insert + vector_nodes = [node.to_vector_node() for node in memory_nodes] + await self.vector_store.insert(vector_nodes) + return memory_nodes + + async def add( + self, + content: str, + when_to_use: str = "", + message_time: str = "", + ref_memory_id: str = "", + author: str = "", + score: float = 0.0, + **kwargs, + ) -> MemoryNode: + """Add a single memory node and return its memory_id.""" + memory_dict = { + "content": content, + "when_to_use": when_to_use, + "message_time": message_time, + "ref_memory_id": ref_memory_id, + "author": author, + "score": score, + "metadata": kwargs, + } + memory_nodes = await self.add_batch([memory_dict]) + return memory_nodes[0] + + async def delete(self, memory_ids: str | list[str]): + """Delete multiple memory nodes by their memory_ids.""" + # Deduplicate if input is a list + if isinstance(memory_ids, list): + memory_ids = list(dict.fromkeys(memory_ids)) + await self.vector_store.delete(memory_ids) + + async def delete_all(self): + """Delete all memory nodes.""" + await self.vector_store.delete_all() + + async def update_batch(self, updates: list[dict]) -> list[MemoryNode]: + """Update multiple memory nodes with their memory_ids and new values using delete + add.""" + # Deduplicate updates by memory_id (keep last occurrence) + updates_dict = {upd["memory_id"]: upd for upd in updates} + updates = list(updates_dict.values()) + memory_ids = list(updates_dict.keys()) + + # Get existing nodes + vector_nodes = await self.vector_store.get(memory_ids) + if not isinstance(vector_nodes, list): + vector_nodes = [vector_nodes] + + # Update and convert back + updated_nodes: list[MemoryNode] = [] + for vector_node, update in zip(vector_nodes, updates): + memory_node = MemoryNode.from_vector_node(vector_node) + memory_node.memory_target = self.memory_target + memory_node.memory_type = self.memory_type + + if "content" in update: + memory_node.content = update["content"] + if "when_to_use" in update: + memory_node.when_to_use = update["when_to_use"] + if "message_time" in update: + memory_node.message_time = update["message_time"] + if "ref_memory_id" in update: + memory_node.ref_memory_id = update["ref_memory_id"] + if "author" in update: + memory_node.author = update["author"] + if "score" in update: + memory_node.score = update["score"] + if "metadata" in update: + memory_node.metadata.update(update["metadata"]) + updated_nodes.append(memory_node) + + # Delete old nodes first + await self.vector_store.delete(memory_ids) + + # Then add updated nodes + vector_nodes = [node.to_vector_node() for node in updated_nodes] + await self.vector_store.insert(vector_nodes) + + return updated_nodes + + async def update( + self, + memory_id: str, + content: str | None = None, + when_to_use: str | None = None, + message_time: str | None = None, + ref_memory_id: str | None = None, + author: str | None = None, + score: float | None = None, + **kwargs, + ) -> MemoryNode: + """Update a memory node's content, when_to_use, or other fields.""" + update_dict: dict = {"memory_id": memory_id} + if content is not None: + update_dict["content"] = content + if when_to_use is not None: + update_dict["when_to_use"] = when_to_use + if message_time is not None: + update_dict["message_time"] = message_time + if ref_memory_id is not None: + update_dict["ref_memory_id"] = ref_memory_id + if author is not None: + update_dict["author"] = author + if score is not None: + update_dict["score"] = score + if kwargs is not None: + update_dict["metadata"] = kwargs + + memory_nodes = await self.update_batch([update_dict]) + return memory_nodes[0] + + async def search( + self, + query: str | list[str], + limit: int = 5, + filters: dict | None = None, + **kwargs, + ) -> list[MemoryNode]: + """Search for similar memory nodes based on query text.""" + filters = filters or {} + filters["memory_type"] = self.memory_type.value + filters["memory_target"] = self.memory_target + + # Handle single query + if isinstance(query, str): + vector_nodes = await self.vector_store.search(query, limit=limit, filters=filters, **kwargs) + return [MemoryNode.from_vector_node(node) for node in vector_nodes] + + # Handle multiple queries: search each query with the same limit + seen_ids: dict[str, MemoryNode] = {} + + for q in query: + vector_nodes = await self.vector_store.search(q, limit=limit, filters=filters, **kwargs) + for vector_node in vector_nodes: + memory_node = MemoryNode.from_vector_node(vector_node) + if memory_node.memory_id not in seen_ids: + seen_ids[memory_node.memory_id] = memory_node + + return list(seen_ids.values()) + + async def batch_search(self, searches: list[dict]) -> list[MemoryNode]: + """Execute multiple search queries in batch and return deduplicated results.""" + seen_ids: dict[str, MemoryNode] = {} + + for search_params in searches: + search_result = await self.search(**search_params) + for memory_node in search_result: + if memory_node.memory_id not in seen_ids: + seen_ids[memory_node.memory_id] = memory_node + + return list(seen_ids.values()) + + async def list( + self, + filters: dict | None = None, + limit: int | None = None, + sort_key: str | None = None, + reverse: bool = True, + ) -> list[MemoryNode]: + """List memory nodes with optional filtering and sorting.""" + filters = filters or {} + filters["memory_type"] = self.memory_type.value + filters["memory_target"] = self.memory_target + + vector_nodes = await self.vector_store.list(filters=filters, limit=limit, sort_key=sort_key, reverse=reverse) + return [MemoryNode.from_vector_node(node) for node in vector_nodes] diff --git a/reme/tool/memory/meta/__init__.py b/reme/tool/memory/meta/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/meta/add_meta_memory.py b/reme/tool/memory/meta/add_meta_memory.py deleted file mode 100644 index 43e9f7db..00000000 --- a/reme/tool/memory/meta/add_meta_memory.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Add meta memory tool""" - -import json - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall - - -class AddMetaMemory(BaseMemoryTool): - """Tool to add memory metadata entries to meta storage""" - - def __init__(self, **kwargs): - kwargs["enable_multiple"] = True - super().__init__(**kwargs) - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "add memory metadata entries to register memory types and targets. " - "Before using, verify Main Agent's Meta Memory doesn't already contain the " - "same memory_type(memory_target) combinations.", - "parameters": { - "type": "object", - "properties": { - "meta_memories": { - "type": "array", - "description": "List of memory metadata entries to add", - "items": { - "type": "object", - "properties": { - "memory_type": { - "type": "string", - "description": "Type of memory: 'personal' for person-specific preferences, " - "'procedural' for how-to knowledge", - "enum": [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value], - }, - "memory_target": { - "type": "string", - "description": "Target identifier, " - "e.g., person's name ('John') or domain ('deployment')", - }, - }, - "required": ["memory_type", "memory_target"], - }, - }, - }, - "required": ["meta_memories"], - }, - }, - ) - - async def execute(self): - existing_memories: list[dict] = self.local_memory.load("meta_memories") or [] - existing_set = {(m["memory_type"], m["memory_target"]) for m in existing_memories} - - # Filter and build new memories to add - new_memories: list[dict] = [] - meta_memories: list[dict] = self.context.get("meta_memories", []) - - for mem in meta_memories: - memory_type = mem.get("memory_type", "") - memory_target = mem.get("memory_target", "") - - # Check if valid and not duplicate - if ( - memory_type in [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value] - and memory_target - and (memory_type, memory_target) not in existing_set - ): - new_memories.append({"memory_type": memory_type, "memory_target": memory_target}) - existing_set.add((memory_type, memory_target)) - - if not new_memories: - output = "No new meta memories to add (all entries already exist or invalid)." - logger.info(output) - return output - - # Merge, sort and save - all_memories = sorted(existing_memories + new_memories, key=lambda m: (m["memory_type"], m["memory_target"])) - self.local_memory.save("meta_memories", all_memories) - - # Format output - output = f"Successfully update meta memory entries: {json.dumps(new_memories, ensure_ascii=False)}" - logger.info(output) - return output diff --git a/reme/tool/memory/meta/read_meta_memory.py b/reme/tool/memory/meta/read_meta_memory.py deleted file mode 100644 index f1936394..00000000 --- a/reme/tool/memory/meta/read_meta_memory.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Read meta memory tool""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall - - -class ReadMetaMemory(BaseMemoryTool): - """Tool to read memory metadata from meta storage""" - - TYPE_DESC_DICT = { - MemoryType.IDENTITY.value: "self-cognition memory storing agent's identity and state", - MemoryType.PERSONAL.value: "person-specific memory storing preferences and context", - MemoryType.PROCEDURAL.value: "procedural memory storing how-to knowledge and processes", - } - - def __init__(self, enable_identity_memory: bool = False, **kwargs): - kwargs["enable_multiple"] = False - super().__init__(**kwargs) - self.enable_identity_memory = enable_identity_memory - - def _build_tool_call(self) -> ToolCall: - return ToolCall( - **{ - "description": "read memory metadata registry to see what types of memories are being tracked.", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - ) - - def format_memory_metadata(self, memories: list[dict[str, str]]) -> str: - """Format memory metadata into a readable string.""" - if not memories: - return "" - - lines = [] - for memory in memories: - memory_type = memory["memory_type"] - memory_target = memory["memory_target"] - description = self.TYPE_DESC_DICT[memory_type] - lines.append(f"- {memory_type}({memory_target}): {description}") - - return "\n".join(lines) - - async def execute(self): - # Load and filter meta memories - result = self.local_memory.load("meta_memories") - all_memories = result if result is not None else [] - - memories = [ - m for m in all_memories if m.get("memory_type") in [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value] - ] - - if self.enable_identity_memory: - memories.append( - { - "memory_type": MemoryType.IDENTITY.value, - "memory_target": "self", - }, - ) - - # Format output - output = self.format_memory_metadata(memories) - if output: - logger.info(f"Retrieved {len(memories)} meta memory entries") - else: - output = "No memory metadata found." - logger.info(output) - - return output diff --git a/reme/tool/memory/profile_handler.py b/reme/tool/memory/profile_handler.py new file mode 100644 index 00000000..4a950171 --- /dev/null +++ b/reme/tool/memory/profile_handler.py @@ -0,0 +1,199 @@ +"""Profile Handler for managing user profiles in local memory""" +from pathlib import Path + +from loguru import logger + +from ...core.enumeration import MemoryType +from ...core.schema import MemoryNode +from ...core.utils import CacheHandler, deduplicate_memories + + +class ProfileHandler: + """User profile CRUD handler""" + + def __init__(self, profile_path: str | Path, memory_target: str, max_capacity: int = 100): + """init""" + self.memory_target: str = memory_target + self.cache_key: str = self.memory_target.replace(" ", "_").lower() + self.cache_handler: CacheHandler = CacheHandler(profile_path) + self.max_capacity: int = max_capacity + + def _load_nodes(self) -> list[MemoryNode]: + """Load profile nodes""" + cached_data = self.cache_handler.load(self.cache_key, auto_clean=False) + if not cached_data: + return [] + return [MemoryNode(**data) for data in cached_data] + + def _save_nodes(self, nodes: list[MemoryNode], apply_limits: bool = True): + """Save nodes with optional deduplication and capacity enforcement""" + if apply_limits: + nodes = deduplicate_memories(nodes) + + # Enforce capacity limit by removing the oldest profiles + if len(nodes) > self.max_capacity: + sorted_nodes = sorted(nodes, key=lambda n: n.message_time) + removed_count = len(sorted_nodes) - self.max_capacity + nodes = sorted_nodes[removed_count:] + logger.info( + f"Capacity limit reached: removed {removed_count} oldest profiles (kept {len(nodes)}/{self.max_capacity})") + + nodes_data = [node.model_dump(exclude_none=True) for node in nodes] + self.cache_handler.save(self.cache_key, nodes_data) + logger.info(f"Saved {len(nodes)} profiles to {self.cache_key}") + + def delete(self, profile_id: str | list[str]) -> bool | int: + """Delete profile by ID(s), returns True/False for single ID or count for batch delete""" + nodes = self._load_nodes() + original_count = len(nodes) + + # Batch delete mode + if isinstance(profile_id, list): + profile_ids_set = set(profile_id) + nodes = [n for n in nodes if n.memory_id not in profile_ids_set] + deleted_count = original_count - len(nodes) + + if deleted_count == 0: + logger.warning(f"No profiles found to delete from {len(profile_id)} IDs") + return 0 + + self._save_nodes(nodes, apply_limits=False) + logger.info(f"Batch deleted {deleted_count} profiles") + return deleted_count + + # Single delete mode + nodes = [n for n in nodes if n.memory_id != profile_id] + + if len(nodes) == original_count: + logger.warning(f"Profile {profile_id} not found") + return False + + self._save_nodes(nodes, apply_limits=False) + logger.info(f"Deleted profile {profile_id}") + return True + + def delete_all(self) -> int: + """Delete all profiles, returns count deleted""" + nodes = self._load_nodes() + count = len(nodes) + self._save_nodes([], apply_limits=False) + logger.info(f"Deleted all {count} profiles") + return count + + def add(self, message_time: str, profile_key: str, profile_value: str, ref_memory_id: str = "") -> MemoryNode: + """Add new profile, returns created MemoryNode""" + nodes = self._load_nodes() + + new_node = MemoryNode( + memory_type=MemoryType.PERSONAL, + memory_target=self.memory_target, + when_to_use=profile_key, + content=profile_value, + message_time=message_time, + ref_memory_id=ref_memory_id, + ) + + nodes.append(new_node) + self._save_nodes(nodes) + logger.info(f"Added profile: {profile_key}={profile_value}") + return new_node + + def add_batch(self, profiles: list[dict], ref_memory_id: str = "") -> list[MemoryNode]: + """Add multiple profiles in batch, returns list of created MemoryNodes""" + if not profiles: + return [] + + nodes = self._load_nodes() + + new_nodes = [ + MemoryNode( + memory_type=MemoryType.PERSONAL, + memory_target=self.memory_target, + when_to_use=p.get("profile_key", ""), + content=p.get("profile_value", ""), + message_time=p.get("message_time", ""), + ref_memory_id=ref_memory_id, + ) + for p in profiles + ] + + nodes.extend(new_nodes) + self._save_nodes(nodes) + logger.info(f"Batch added {len(new_nodes)} profiles") + return new_nodes + + def update(self, profile_id: str, message_time: str, profile_key: str, profile_value: str) -> MemoryNode | None: + """Update profile by ID, returns updated node or None if not found""" + nodes = self._load_nodes() + + target_node = None + for node in nodes: + if node.memory_id == profile_id: + node.when_to_use = profile_key + node.content = profile_value + node.message_time = message_time + target_node = node + break + + if target_node is None: + logger.warning(f"Profile {profile_id} not found") + return None + + self._save_nodes(nodes, apply_limits=False) + logger.info(f"Updated profile {profile_id}: {profile_key}={profile_value}") + return target_node + + def get_by(self, *, profile_id: str | None = None, profile_key: str | None = None) -> MemoryNode | None: + """Get profile by ID or key""" + if not profile_id and not profile_key: + raise ValueError("Must provide either profile_id or profile_key") + + nodes = self._load_nodes() + for node in nodes: + if profile_id and node.memory_id == profile_id: + return node + if profile_key and node.when_to_use == profile_key: + return node + return None + + def get_by_id(self, profile_id: str) -> MemoryNode | None: + """Get profile by ID (convenience method)""" + return self.get_by(profile_id=profile_id) + + def get_by_key(self, profile_key: str) -> MemoryNode | None: + """Get profile by key (convenience method)""" + return self.get_by(profile_key=profile_key) + + def get_all(self) -> list[MemoryNode]: + """Get all profiles, sorted by message_time""" + nodes = self._load_nodes() + nodes.sort(key=lambda n: n.message_time) + return nodes + + @staticmethod + def _format_node(node: MemoryNode, add_profile_id: bool = False, add_history_id: bool = False) -> str: + """Format a single node to string""" + parts = [] + + if add_profile_id: + parts.append(f"profile_id={node.memory_id}") + + if node.message_time: + parts.append(f"[{node.message_time}]") + + parts.append(f"{node.when_to_use}: {node.content}") + + if add_history_id: + parts.append(f"history_id={node.ref_memory_id}") + + return " ".join(parts) + + def read_all(self, add_profile_id: bool = False, add_history_id: bool = False) -> str: + """Read all profiles and return formatted string""" + nodes = self.get_all() + formatted_profiles = [ + self._format_node(node, add_profile_id, add_history_id) + for node in nodes + ] + logger.info(f"Read {len(formatted_profiles)} profiles from {self.cache_key}") + return "\n".join(formatted_profiles).strip() diff --git a/reme/tool/memory/history/read_history.py b/reme/tool/memory/read_history.py similarity index 90% rename from reme/tool/memory/history/read_history.py rename to reme/tool/memory/read_history.py index 4da6918e..500d4bd9 100644 --- a/reme/tool/memory/history/read_history.py +++ b/reme/tool/memory/read_history.py @@ -2,8 +2,8 @@ from loguru import logger -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import MemoryNode, ToolCall +from .base_memory_tool import BaseMemoryTool +from ...core.schema import MemoryNode, ToolCall class ReadHistory(BaseMemoryTool): @@ -36,7 +36,7 @@ class ReadHistory(BaseMemoryTool): nodes = await self.vector_store.get(vector_ids=[history_id]) if not nodes: - output = f"No history: {history_id}" + output = f"No history_id={history_id} data." logger.warning(output) return output diff --git a/reme/tool/memory/read_profile.py b/reme/tool/memory/read_profile.py new file mode 100644 index 00000000..b3286194 --- /dev/null +++ b/reme/tool/memory/read_profile.py @@ -0,0 +1,45 @@ +"""Read user profile tool""" +from pathlib import Path + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .profile_handler import ProfileHandler +from ...core.schema import ToolCall + + +class ReadProfile(BaseMemoryTool): + """Tool to read all user profiles""" + + def __init__(self, profile_path: str, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + self.profile_path: str = profile_path + + def _build_tool_call(self) -> ToolCall: + """Build and return the tool call schema""" + return ToolCall( + **{ + "description": "Read all user profiles.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + ) + + async def execute(self): + profile_handler = ProfileHandler( + profile_path=Path(self.profile_path) / self.vector_store.collection_name, + memory_target=self.memory_target, + ) + + profiles_str = profile_handler.read_all() + if not profiles_str: + output = "No profiles found." + logger.info(output) + return output + + logger.info(f"Successfully read profiles") + return profiles_str diff --git a/reme/tool/memory/retrieve_memory.py b/reme/tool/memory/retrieve_memory.py new file mode 100644 index 00000000..f8845405 --- /dev/null +++ b/reme/tool/memory/retrieve_memory.py @@ -0,0 +1,121 @@ +"""Retrieve memory from vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall, MemoryNode +from ...core.utils import deduplicate_memories + + +class RetrieveMemory(BaseMemoryTool): + """Tool to retrieve memories using similarity search""" + + def __init__(self, top_k: int = 20, enable_memory_target: bool = False, **kwargs): + super().__init__(**kwargs) + self.top_k: int = top_k + self.enable_memory_target: bool = enable_memory_target + + def _build_query_parameters(self) -> dict: + """Build the query parameters schema based on enabled features.""" + properties = { + "query": { + "type": "string", + "description": "query text for vector similarity search.", + }, + "time_range": { + "type": "string", + "description": "optional time range filter. Format: '20200101' or '20200101,20200102'", + }, + } + required = ["query"] + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "target memory type to search in.", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "retrieve memories using vector similarity search.", + "parameters": self._build_query_parameters(), + }, + ) + + def _build_multiple_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "retrieve memories using multiple queries with vector similarity search.", + "parameters": { + "type": "object", + "properties": { + "query_items": { + "type": "array", + "description": "list of query items for vector similarity search.", + "items": self._build_query_parameters(), + }, + }, + "required": ["query_items"], + }, + }, + ) + + async def execute(self): + if self.enable_multiple: + query_items = self.context.get("query_items", []) + else: + query_items = [self.context] + + queries_by_target: dict[str, list[dict]] = {} + for item in query_items: + if self.enable_memory_target: + target = item["memory_target"] + else: + target = self.memory_target + if target not in queries_by_target: + queries_by_target[target] = [] + + filters = {} + time_range = item.get("time_range") + if time_range: + time_range = time_range.strip() + if "," in time_range: + start, end = time_range.split(",") + filters = {"time_int": [int(start.strip()), int(end.strip())]} + else: + filters = {"time_int": [int(time_range), int(time_range)]} + + queries_by_target[target].append({ + "query": item["query"], + "limit": self.top_k, + "filters": filters, + }) + + # Execute batch searches for each target + memory_nodes: list[MemoryNode] = [] + for target, searches in queries_by_target.items(): + handler = MemoryHandler(target, self.service_context) + nodes = await handler.batch_search(searches) + memory_nodes.extend(nodes) + + memory_nodes = deduplicate_memories(memory_nodes) + retrieved_ids = {n.memory_id for n in self.retrieved_nodes if n.memory_id} + new_nodes = [n for n in memory_nodes if n.memory_id not in retrieved_ids] + self.retrieved_nodes.extend(new_nodes) + + if not new_nodes: + output = "No new memories found." + else: + output = "\n".join([n.format(ref_memory_id_key="history_id") for n in new_nodes]) + + logger.info(f"Retrieved {len(memory_nodes)} memories, {len(new_nodes)} new after deduplication") + return output diff --git a/reme/tool/memory/retrieve_recent_memory.py b/reme/tool/memory/retrieve_recent_memory.py new file mode 100644 index 00000000..05b1a777 --- /dev/null +++ b/reme/tool/memory/retrieve_recent_memory.py @@ -0,0 +1,52 @@ +"""Retrieve most recent memories from vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall, MemoryNode +from ...core.utils import deduplicate_memories + + +class RetrieveRecentMemory(BaseMemoryTool): + """Tool to retrieve most recent memories sorted by time""" + + def __init__(self, top_k: int = 20, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + self.top_k: int = top_k + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "retrieve the most recent memories sorted by message time (newest first).", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + ) + + async def execute(self): + handler = MemoryHandler(self.memory_target, self.service_context) + + memory_nodes: list[MemoryNode] = await handler.list( + limit=self.top_k, + sort_key="message_time", + reverse=True, + ) + memory_nodes = deduplicate_memories(memory_nodes) + + retrieved_ids = {n.memory_id for n in self.retrieved_nodes if n.memory_id} + new_nodes = [n for n in memory_nodes if n.memory_id not in retrieved_ids] + self.retrieved_nodes.extend(new_nodes) + self.memory_nodes.extend(new_nodes) + + if not new_nodes: + output = "No new memories found." + else: + output = "\n".join([n.format(ref_memory_id_key="history_id") for n in new_nodes]) + + logger.info(f"Retrieved {len(memory_nodes)} memories, {len(new_nodes)} new after deduplication") + return output diff --git a/reme/tool/memory/update_memory.py b/reme/tool/memory/update_memory.py new file mode 100644 index 00000000..1370e180 --- /dev/null +++ b/reme/tool/memory/update_memory.py @@ -0,0 +1,139 @@ +"""Update memory in vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall + + +class UpdateMemory(BaseMemoryTool): + """Tool to update memories in vector store""" + + def __init__( + self, + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, + ): + super().__init__(**kwargs) + self.enable_memory_target: bool = enable_memory_target + self.enable_when_to_use: bool = enable_when_to_use + + def _build_update_parameters(self) -> dict: + """Build the update parameters schema based on enabled features.""" + properties = { + "memory_id": { + "type": "string", + "description": "unique identifier of memory to update.", + }, + "message_time": { + "type": "string", + "description": "message time, e.g. '2020-01-01 00:00:00'", + }, + "memory_content": { + "type": "string", + "description": "new content of the memory.", + }, + } + required = ["memory_id", "message_time", "memory_content"] + + if self.enable_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": "description of when to use this memory.", + } + required.append("when_to_use") + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "target memory type for this memory.", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "update a memory in vector store by replacing old memory with new content.", + "parameters": self._build_update_parameters(), + }, + ) + + def _build_multiple_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "update multiple memories in vector store by replacing old memories with new content.", + "parameters": { + "type": "object", + "properties": { + "memories": { + "type": "array", + "description": "list of memory update objects.", + "items": self._build_update_parameters(), + }, + }, + "required": ["memories"], + }, + }, + ) + + async def execute(self): + if self.enable_multiple: + memories = self.context.get("memories", []) + else: + memories = [self.context] + + # Group memories by memory_target if enabled + if self.enable_memory_target: + memories_by_target = {} + for mem in memories: + target = mem["memory_target"] + if target not in memories_by_target: + memories_by_target[target] = [] + memories_by_target[target].append(mem) + else: + memories_by_target = {self.memory_target: memories} + + # Process each memory_target group + all_memory_nodes = [] + for target, target_memories in memories_by_target.items(): + # Parse and prepare update data + update_dicts = [] + for mem in target_memories: + memory_content = mem.get("memory_content", "") + message_time = mem.get("message_time", "") + when_to_use = mem.get("when_to_use", "") if self.enable_when_to_use else "" + metadata = {} + try: + metadata["time_int"] = int(message_time.split(" ")[0].replace("-", "")) + except Exception: + logger.warning(f"Invalid message time format: {message_time}") + + update_dicts.append({ + "memory_id": mem.get("memory_id", ""), + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "author": self.author, + "metadata": metadata, + }) + + if update_dicts: + handler = MemoryHandler(target, self.service_context) + memory_nodes = await handler.update_batch(update_dicts) + all_memory_nodes.extend(memory_nodes) + + if not all_memory_nodes: + return "No valid memories provided." + + self.memory_nodes.extend(all_memory_nodes) + output = f"Successfully updated {len(all_memory_nodes)} memories." + logger.info(output) + return output diff --git a/reme/tool/memory/update_profile.py b/reme/tool/memory/update_profile.py new file mode 100644 index 00000000..2032964d --- /dev/null +++ b/reme/tool/memory/update_profile.py @@ -0,0 +1,97 @@ +"""Update user profile tool""" +from pathlib import Path + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .profile_handler import ProfileHandler +from ...core.schema import ToolCall + + +class UpdateProfile(BaseMemoryTool): + """Tool to update user profile by adding or removing profile entries""" + + def __init__(self, profile_path: str, **kwargs): + kwargs["enable_multiple"] = True + super().__init__(**kwargs) + + self.profile_path: str = profile_path + + def _build_multiple_tool_call(self) -> ToolCall: + """Build and return the multiple tool call schema""" + return ToolCall( + **{ + "description": "update user profile by removing and adding profile entries.", + "parameters": { + "type": "object", + "properties": { + "profile_ids_to_delete": { + "type": "array", + "description": "List of profile IDs to delete", + "items": { + "type": "string" + }, + }, + "profiles_to_add": { + "type": "array", + "description": "List of profiles to add", + "items": { + "type": "object", + "properties": { + "message_time": { + "type": "string", + "description": "Message time, e.g. '2020-01-01 00:00:00'", + }, + "profile_key": { + "type": "string", + "description": "Profile key or category, e.g. 'name'", + }, + "profile_value": { + "type": "string", + "description": "Profile value or content, e.g. 'John Smith'", + }, + }, + "required": ["message_time", "profile_key", "profile_value"], + }, + }, + }, + "required": ["profile_ids_to_delete", "profiles_to_add"], + }, + }, + ) + + async def execute(self): + profile_handler = ProfileHandler( + profile_path=Path(self.profile_path) / self.vector_store.collection_name, + memory_target=self.memory_target, + ) + + # Get parameters + profile_ids_to_delete = self.context.get("profile_ids_to_delete", []) + profile_ids_to_delete = sorted(set([pid for pid in profile_ids_to_delete if pid])) + profiles_to_add = self.context.get("profiles_to_add", []) + + if not profile_ids_to_delete and not profiles_to_add: + return "No profiles to remove or add, operation completed." + + # Delete profiles using ProfileHandler (batch mode) + removed_count = 0 + if profile_ids_to_delete: + removed_count = profile_handler.delete(profile_ids_to_delete) + + # Add new profiles using ProfileHandler (batch mode) + added_count = 0 + if profiles_to_add: + new_nodes = profile_handler.add_batch(profiles=profiles_to_add, ref_memory_id=self.history_node.memory_id) + self.memory_nodes.extend(new_nodes) + added_count = len(new_nodes) + + # Build output message + operations = [] + if removed_count > 0: + operations.append(f"removed {removed_count} old profiles.") + if added_count > 0: + operations.append(f"added {added_count} new profiles.") + operations.append("Operation completed.") + logger.info("\n".join(operations)) + return "\n".join(operations) diff --git a/reme/tool/memory/user_profile/__init__.py b/reme/tool/memory/user_profile/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/user_profile/read_user_profile.py b/reme/tool/memory/user_profile/read_user_profile.py deleted file mode 100644 index 637b2070..00000000 --- a/reme/tool/memory/user_profile/read_user_profile.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Read user profile tool""" - -from typing import Literal - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall, MemoryNode - - -class ReadUserProfile(BaseMemoryTool): - """Tool to read user profile from local memory""" - - def __init__(self, show_id: Literal["profile", "history"] = "profile", **kwargs): - kwargs["enable_multiple"] = False - super().__init__(**kwargs) - self.show_id = show_id - - def _build_tool_call(self) -> ToolCall: - return ToolCall( - **{ - "description": "read user profile.", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - ) - - async def execute(self): - self.context.memory_type = MemoryType.PERSONAL - cached_data = self.local_memory.load(self.memory_cache_key, auto_clean=False) - - if not cached_data: - logger.info(f"No cached data found for {self.memory_cache_key}") - return "" - - nodes = [MemoryNode(**data) for data in cached_data] - self.memory_nodes.clear() - self.memory_nodes.extend(nodes) - nodes.sort(key=lambda n: n.metadata.get("update_time", "")) - - formatted_profiles = [] - for node in nodes: - parts = [] - if self.show_id == "profile": - parts.append(f"profile_id={node.memory_id}") - - if update_time := node.metadata.get("update_time"): - parts.append(f"update_time={update_time}") - - parts.append(f"{node.when_to_use}: {node.content}") - - if self.show_id == "history": - parts.append(f"history_id={node.ref_memory_id}") - - formatted_profiles.append(" ".join(parts)) - - logger.info(f"Read {len(formatted_profiles)} profiles from cache key: {self.memory_cache_key}") - - return "\n".join(formatted_profiles).strip() diff --git a/reme/tool/memory/user_profile/update_user_profile.py b/reme/tool/memory/user_profile/update_user_profile.py deleted file mode 100644 index 1c568c5d..00000000 --- a/reme/tool/memory/user_profile/update_user_profile.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Update user profile tool""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.enumeration import MemoryType -from ....core.schema import ToolCall, MemoryNode -from ....core.utils import deduplicate_memories - - -class UpdateUserProfile(BaseMemoryTool): - """Tool to update user profile by adding or removing profile entries""" - - def __init__(self, **kwargs): - kwargs["enable_multiple"] = True - super().__init__(**kwargs) - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "update user profile by adding or removing profile entries.", - "parameters": { - "type": "object", - "properties": { - "profile_ids_to_delete": { - "type": "array", - "description": "List of profile IDs to delete", - "items": {"type": "string"}, - }, - "profiles_to_add": { - "type": "array", - "description": "List of profiles to add", - "items": { - "type": "object", - "properties": { - "update_time": { - "type": "string", - "description": "Update time, e.g. '2020-01-01 00:00:00'", - }, - "profile_key": { - "type": "string", - "description": "Profile key or category, e.g. 'name'", - }, - "profile_value": { - "type": "string", - "description": "Profile value or content, e.g. 'John Smith'", - }, - }, - "required": ["update_time", "profile_key", "profile_value"], - }, - }, - }, - "required": ["profile_ids_to_delete", "profiles_to_add"], - }, - }, - ) - - async def execute(self): - # Get and deduplicate profile IDs to delete - self.context.memory_type = MemoryType.PERSONAL - - profile_ids_to_delete = self.context.get("profile_ids_to_delete", []) - profile_ids_to_delete = list(dict.fromkeys([pid for pid in profile_ids_to_delete if pid])) - profiles_to_add = self.context.get("profiles_to_add", []) - - if not profile_ids_to_delete and not profiles_to_add: - return "No profiles to remove or add. Operation completed." - - # Load existing profiles from local memory - cached_data = self.local_memory.load(self.memory_cache_key, auto_clean=False) - existing_nodes = [MemoryNode(**data) for data in cached_data] if cached_data else [] - - # Remove profiles - removed_count = 0 - if profile_ids_to_delete: - original_count = len(existing_nodes) - existing_nodes = [n for n in existing_nodes if n.memory_id not in profile_ids_to_delete] - removed_count = original_count - len(existing_nodes) - logger.info(f"Removed {removed_count} profiles.") - - # Add new profiles - new_nodes = [] - if profiles_to_add: - for profile in profiles_to_add: - node = MemoryNode( - memory_type=self.memory_type, - memory_target=self.memory_target, - when_to_use=profile.get("profile_key", ""), - content=profile.get("profile_value", ""), - ref_memory_id=self.history_node.memory_id, - author=self.author, - metadata={"update_time": profile.get("update_time", "")}, - ) - new_nodes.append(node) - logger.info(f"Added {len(new_nodes)} new profiles.") - - # Deduplicate and save updated profiles - self.memory_nodes.extend(new_nodes) - updated_nodes = deduplicate_memories(existing_nodes + new_nodes) - nodes_data = [node.model_dump(exclude_none=True) for node in updated_nodes] - self.local_memory.save(self.memory_cache_key, nodes_data) - - # Build output message - operations = [] - if removed_count > 0: - operations.append(f"removed {removed_count} old profiles.") - if len(new_nodes) > 0: - operations.append(f"added {len(new_nodes)} new profiles.") - operations.append("Operation completed.") - logger.info("\n".join(operations)) - return "\n".join(operations) diff --git a/reme/tool/memory/vector/__init__.py b/reme/tool/memory/vector/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/reme/tool/memory/vector/add_memory.py b/reme/tool/memory/vector/add_memory.py deleted file mode 100644 index 5d6b8528..00000000 --- a/reme/tool/memory/vector/add_memory.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Add memory to vector store""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall, MemoryNode - - -class AddMemory(BaseMemoryTool): - """Tool to add memories to vector store""" - - def _build_tool_call(self) -> ToolCall: - """Build and return the single tool call schema""" - return ToolCall( - **{ - "description": "add a memory to vector store for future retrieval.", - "parameters": { - "type": "object", - "properties": { - "conversation_time": { - "type": "string", - "description": "conversation time, e.g. '2020-01-01 00:00:00'", - }, - "memory_content": { - "type": "string", - "description": "content of the memory.", - }, - }, - "required": ["conversation_time", "memory_content"], - }, - }, - ) - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "add multiple memories to vector store for future retrieval.", - "parameters": { - "type": "object", - "properties": { - "memories": { - "type": "array", - "description": "list of memories to store.", - "items": { - "type": "object", - "properties": { - "conversation_time": { - "type": "string", - "description": "conversation time, e.g. '2020-01-01 00:00:00'", - }, - "memory_content": { - "type": "string", - "description": "content of the memory.", - }, - }, - "required": ["conversation_time", "memory_content"], - }, - }, - }, - "required": ["memories"], - }, - }, - ) - - def _create_memory_node(self, data: dict) -> MemoryNode: - """Create a MemoryNode from a dictionary.""" - memory_content = data.get("memory_content", "") - conversation_time = data.get("conversation_time", "") - metadata: dict = {"conversation_time": conversation_time} - - try: - metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", "")) - except Exception: - logger.warning(f"Invalid conversation time format. {conversation_time}") - - return MemoryNode( - memory_type=self.memory_type, - memory_target=self.memory_target, - content=memory_content, - author=self.author, - ref_memory_id=self.history_node.memory_id, - metadata=metadata, - ) - - async def execute(self): - memory_nodes: list[MemoryNode] = [] - memories: list[dict] = self.context.get("memories", []) - - if not memories: - memory_nodes.append(self._create_memory_node(self.context)) - else: - for mem in memories: - memory_nodes.append(self._create_memory_node(mem)) - - if not memory_nodes: - output = "No valid memories provided for addition." - logger.info(output) - return output - - vector_nodes = [node.to_vector_node() for node in memory_nodes] - vector_ids: list[str] = [node.vector_id for node in vector_nodes] - - await self.vector_store.delete(vector_ids=list(set(vector_ids))) - await self.vector_store.insert(nodes=vector_nodes) - self.memory_nodes.extend(memory_nodes) - - output = f"Successfully added {len(memory_nodes)} memories to vector_store." - logger.info(output) - return output diff --git a/reme/tool/memory/vector/retrieve_memory.py b/reme/tool/memory/vector/retrieve_memory.py deleted file mode 100644 index 7c95b538..00000000 --- a/reme/tool/memory/vector/retrieve_memory.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Retrieve memory from vector store""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall, MemoryNode, VectorNode -from ....core.utils import deduplicate_memories - - -class RetrieveMemory(BaseMemoryTool): - """Tool to retrieve memories from vector store using similarity search""" - - def __init__(self, top_k: int = 20, **kwargs): - super().__init__(**kwargs) - self.top_k: int = top_k - - @staticmethod - def _build_query_parameters() -> dict: - """Build query parameters schema for retrieval""" - return { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "query text for vector similarity search.", - }, - "time_range": { - "type": "string", - "description": "optional time range filter. " - "Format: single date '20200101' or range '20200101,20200102'", - }, - }, - "required": ["query"], - } - - def _build_tool_call(self) -> ToolCall: - """Build and return the tool call schema""" - return ToolCall( - **{ - "description": "retrieve memories using vector similarity search.", - "parameters": self._build_query_parameters(), - }, - ) - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "retrieve memories using multiple queries with vector similarity search.", - "parameters": { - "type": "object", - "properties": { - "query_items": { - "type": "array", - "description": "list of query items for vector similarity search.", - "items": self._build_query_parameters(), - }, - }, - "required": ["query_items"], - }, - }, - ) - - async def _retrieve_by_query( - self, - memory_type: str, - memory_target: str, - query: str, - time_range: str | None = None, - ) -> list[MemoryNode]: - """Retrieve memories by query with filters""" - filter_dict: dict = { - "memory_type": memory_type, - "memory_target": memory_target, - } - - if time_range: - time_range = time_range.strip() - if "," in time_range: - parts = time_range.split(",") - start_time = int(parts[0].strip()) - end_time = int(parts[1].strip()) - filter_dict["time_int"] = [start_time, end_time] - else: - single_time = int(time_range) - filter_dict["time_int"] = [single_time, single_time] - - nodes: list[VectorNode] = await self.vector_store.search(query=query, limit=self.top_k, filters=filter_dict) - return [MemoryNode.from_vector_node(n) for n in nodes] - - async def execute(self): - memory_type: str = self.memory_type.value - memory_target: str = self.memory_target - - if self.enable_multiple: - query_items: list[dict] = self.context.get("query_items", []) - else: - query_items: list[dict] = [ - { - "query": self.context.get("query", ""), - "time_range": self.context.get("time_range", ""), - }, - ] - - query_items = [item for item in query_items if item.get("query")] - memory_nodes: list[MemoryNode] = [] - for item in query_items: - retrieved = await self._retrieve_by_query( - memory_type=memory_type, - memory_target=memory_target, - query=item["query"], - time_range=item.get("time_range", ""), - ) - memory_nodes.extend(retrieved) - - memory_nodes = deduplicate_memories(memory_nodes) - retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id} - new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids] - self.retrieved_nodes.extend(new_memory_nodes) - - if not new_memory_nodes: - output = "No new memory_nodes found matching the query (duplicates removed)." - else: - outputs = [] - for node in new_memory_nodes: - line = "" - if "conversation_time" in node.metadata and node.metadata["conversation_time"]: - line += f"conversation_time={node.metadata['conversation_time']} " - line += node.content.strip() + " " - if node.ref_memory_id: - line += f"history_id={node.ref_memory_id}" - outputs.append(line.strip()) - output = "\n".join(outputs) - - logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication") - return output diff --git a/reme/tool/memory/vector/retrieve_recent_memory.py b/reme/tool/memory/vector/retrieve_recent_memory.py deleted file mode 100644 index c7c6bc7f..00000000 --- a/reme/tool/memory/vector/retrieve_recent_memory.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Retrieve most recent memories from vector store""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall, MemoryNode, VectorNode -from ....core.utils import deduplicate_memories - - -class RetrieveRecentMemory(BaseMemoryTool): - """Tool to retrieve most recent memories sorted by conversation time""" - - def __init__(self, top_k: int = 20, **kwargs): - kwargs["enable_multiple"] = False - super().__init__(**kwargs) - self.top_k: int = top_k - - def _build_tool_call(self) -> ToolCall: - """Build and return the tool call schema""" - return ToolCall( - **{ - "description": "retrieve the most recent memories sorted by conversation time (newest first).", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - ) - - async def _retrieve_recent(self) -> list[MemoryNode]: - """Retrieve recent memories sorted by conversation_time descending""" - filter_dict = { - "memory_type": self.memory_type.value, - "memory_target": self.memory_target, - } - - nodes: list[VectorNode] = await self.vector_store.list( - filters=filter_dict, - limit=self.top_k, - sort_key="conversation_time", - reverse=True, - ) - - return [MemoryNode.from_vector_node(n) for n in nodes] - - async def execute(self): - memory_nodes: list[MemoryNode] = await self._retrieve_recent() - memory_nodes = deduplicate_memories(memory_nodes) - - retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id} - new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids] - self.retrieved_nodes.extend(new_memory_nodes) - self.memory_nodes.extend(new_memory_nodes) - - if not new_memory_nodes: - output = "No new memory_nodes found (duplicates removed)." - else: - output = "\n".join([m.format_memory() for m in new_memory_nodes]) - - logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication") - return output diff --git a/reme/tool/memory/vector/update_memory.py b/reme/tool/memory/vector/update_memory.py deleted file mode 100644 index ca52582b..00000000 --- a/reme/tool/memory/vector/update_memory.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Update memory in vector store""" - -from loguru import logger - -from ..base_memory_tool import BaseMemoryTool -from ....core.schema import ToolCall, MemoryNode - - -class UpdateMemory(BaseMemoryTool): - """Tool to update memories in vector store""" - - def _build_tool_call(self) -> ToolCall: - """Build and return the single tool call schema""" - return ToolCall( - **{ - "description": "update a memory in vector store by replacing old memory with new content.", - "parameters": { - "type": "object", - "properties": { - "memory_id": { - "type": "string", - "description": "unique identifier of memory to update.", - }, - "conversation_time": { - "type": "string", - "description": "conversation time, e.g. '2020-01-01 00:00:00'", - }, - "memory_content": { - "type": "string", - "description": "new content of the memory.", - }, - }, - "required": ["memory_id", "conversation_time", "memory_content"], - }, - }, - ) - - def _build_multiple_tool_call(self) -> ToolCall: - """Build and return the multiple tool call schema""" - return ToolCall( - **{ - "description": "update multiple memories in vector store by replacing old memories with new content.", - "parameters": { - "type": "object", - "properties": { - "memories": { - "type": "array", - "description": "list of memory update objects.", - "items": { - "type": "object", - "properties": { - "memory_id": { - "type": "string", - "description": "unique identifier of memory to update.", - }, - "conversation_time": { - "type": "string", - "description": "conversation time, e.g. '2020-01-01 00:00:00'", - }, - "memory_content": { - "type": "string", - "description": "new content of the memory.", - }, - }, - "required": ["memory_id", "conversation_time", "memory_content"], - }, - }, - }, - "required": ["memories"], - }, - }, - ) - - def _create_memory_node(self, data: dict) -> tuple[str, MemoryNode]: - """Create a MemoryNode from a dictionary.""" - memory_id = data.get("memory_id", "") - memory_content = data.get("memory_content", "") - conversation_time = data.get("conversation_time", "") - metadata: dict = {"conversation_time": conversation_time} - - try: - metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", "")) - except Exception: - logger.warning(f"Invalid conversation time format. {conversation_time}") - - memory_node = MemoryNode( - memory_type=self.memory_type, - memory_target=self.memory_target, - content=memory_content, - author=self.author, - metadata=metadata, - ) - - return memory_id, memory_node - - async def execute(self): - old_memory_ids: list[str] = [] - memory_nodes: list[MemoryNode] = [] - memories: list[dict] = self.context.get("memories", []) - - if not memories: - old_id, node = self._create_memory_node(self.context) - old_memory_ids.append(old_id) - memory_nodes.append(node) - else: - for mem in memories: - old_id, node = self._create_memory_node(mem) - old_memory_ids.append(old_id) - memory_nodes.append(node) - - if not memory_nodes: - output = "No valid memories provided for update." - logger.info(output) - return output - - vector_nodes = [node.to_vector_node() for node in memory_nodes] - new_vector_ids: list[str] = [node.vector_id for node in vector_nodes] - - all_ids_to_delete = list(set(old_memory_ids + new_vector_ids)) - await self.vector_store.delete(vector_ids=all_ids_to_delete) - await self.vector_store.insert(nodes=vector_nodes) - self.memory_nodes.extend(memory_nodes) - - output = f"Successfully updated {len(memory_nodes)} memories in vector_store." - logger.info(output) - return output From afa4eb911408db624f5fe5643d8c302daa5e218a Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 01:47:49 +0800 Subject: [PATCH 06/17] refactor(memory): update memory reference handling and agent orchestration --- reme/agent/memory/default/reme_retriever.py | 16 +------- reme/agent/memory/default/reme_retriever.yaml | 24 +++++------ reme/agent/memory/default/reme_summarizer.py | 40 ++++++------------- .../agent/memory/default/reme_summarizer.yaml | 20 +++++----- reme/core/op/base_react.py | 7 ++++ reme/tool/memory/add_memory.py | 2 +- reme/tool/memory/base_memory_tool.py | 6 ++- reme/tool/memory/delegate_task.py | 22 ++++------ reme/tool/memory/update_profile.py | 2 +- 9 files changed, 58 insertions(+), 81 deletions(-) diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/default/reme_retriever.py index b7a917f9..2f3f78bb 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/default/reme_retriever.py @@ -10,10 +10,6 @@ from ....core.utils import format_messages class ReMeRetriever(BaseMemoryAgent): """Orchestrate multiple memory agents to retrieve information.""" - def __init__(self, meta_memories: list[dict] | None = None, **kwargs): - super().__init__(**kwargs) - self.meta_memories: list[dict] = meta_memories or [] - async def build_messages(self) -> list[Message]: if self.context.get("query"): context = self.context.query @@ -27,7 +23,7 @@ class ReMeRetriever(BaseMemoryAgent): role=Role.SYSTEM, content=self.prompt_format( prompt_name="system_prompt", - meta_memory_info=await self.read_meta_memories(self.meta_memories), + meta_memory_info=self.meta_memory_info, context=context.strip(), ), ), @@ -58,21 +54,14 @@ class ReMeRetriever(BaseMemoryAgent): async def react(self, messages: list[Message], tools: list["BaseTool"], stage: str = ""): """Run single ReAct step - only one tool call iteration.""" - success: bool = False used_tools: list[BaseTool] = [] - - # Reasoning: LLM decides next action assistant_message, should_act = await self._reasoning_step(messages, tools, step=0, stage=stage) + success = True if should_act: - # Acting: execute tools and collect results (only once) t_tools, tool_messages = await self._acting_step(assistant_message, tools, step=0, stage=stage) used_tools.extend(t_tools) messages.extend(tool_messages) - success = True - else: - # No tools requested - success = True return used_tools, messages, success @@ -87,7 +76,6 @@ class ReMeRetriever(BaseMemoryAgent): messages = [] tools = [] retrieved_nodes = [] - for agent in agents: answer.append(agent.response.answer) success = success and agent.response.success diff --git a/reme/agent/memory/default/reme_retriever.yaml b/reme/agent/memory/default/reme_retriever.yaml index 11d7186e..8db310db 100644 --- a/reme/agent/memory/default/reme_retriever.yaml +++ b/reme/agent/memory/default/reme_retriever.yaml @@ -1,23 +1,23 @@ system_prompt: | - You are a Memory Orchestrator responsible for routing memory retrieval tasks to specialized agents based on the user query. + You are a Memory Orchestrator responsible for routing memory retrieval tasks to specialized agents based on the context. - # User Query + # Context {context} ## Available Memory Agents - Each line indicates a specialized Memory Agent dedicated to storing and retrieving memories within a specific dimension (). - Format: "- (): " + Each line indicates a specialized Memory Agent that is an expert for retrieving memories about a specific memory_target. {meta_memory_info} ## Your Task - Use the `delegate_task` tool to retrieve information from specialized agents: - 1. Analyze the user query and identify which memory dimensions are relevant - 2. Specify `memory_type` and `memory_target` for each retrieval task - - The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above - - Do NOT query agents that don't exist above - 3. Multiple tasks can be specified to enable parallel retrieval from specialized agents + Analyze the context and delegate retrieval tasks to appropriate specialized agents: + 1. Examine the context content and identify which memory_target(s) are relevant for retrieving information + 2. For each relevant memory_target, delegate the retrieval task to its corresponding specialized agent + - The memory_target must **exactly match** existing entries in the "Available Memory Agents" listed above + - Do NOT delegate to agents that don't exist above + - Each memory_target should be assigned **only once** - do not duplicate assignments + 3. Use the `delegate_task` tool **once** with all relevant memory_target(s) to enable parallel processing by specialized agents - Note: If the retrieved information is insufficient to answer the query, respond: "nothing found after thorough search." + Note: If the context contains no memorable information (e.g., simple greetings), return ``. user_message: | - Please analyze the user query and retrieve relevant information from the appropriate existing agents. \ No newline at end of file + Please analyze the context and delegate retrieval tasks to the appropriate specialized agents. \ No newline at end of file diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index c9b4db81..86b49f2c 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -4,37 +4,30 @@ from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import Role from ....core.op import BaseTool from ....core.schema import Message +from ....core.utils import format_messages class ReMeSummarizer(BaseMemoryAgent): """Orchestrates multiple memory agents to summarize and store information across different memory types.""" - def __init__(self, meta_memories: list[dict] | None = None, **kwargs): - super().__init__(**kwargs) - self.meta_memories: list[dict] = meta_memories or [] - - async def add_history_node(self) -> MemoryNode: - """Add history node""" - from ...tool.memory import AddHistory - - add_history_tool = AddHistory() - await add_history_tool.call( - messages=self.messages, - description=self.description, - service_context=self.service_context, - ) - return add_history_tool.context.history_node - async def build_messages(self) -> list[Message]: - self.context.history_node = await self.add_history_node() + add_history_tool: BaseTool | None = self.pop_tool("add_history") + if add_history_tool is not None: + await add_history_tool.call( + messages=self.messages, + description=self.description, + service_context=self.service_context, + ) + self.context.history_node = add_history_tool.context.history_node + context = self.context.description + "\n" + format_messages(self.context.messages) messages = [ Message( role=Role.SYSTEM, content=self.prompt_format( prompt_name="system_prompt", - meta_memory_info=await self.read_meta_memories(self.meta_memories), - context=self.context.history_node.content, + meta_memory_info=self.meta_memory_info, + context=context.strip(), ), ), Message( @@ -66,21 +59,14 @@ class ReMeSummarizer(BaseMemoryAgent): async def react(self, messages: list[Message], tools: list["BaseTool"], stage: str = ""): """Run single ReAct step - only one tool call iteration.""" - success: bool = False used_tools: list[BaseTool] = [] - - # Reasoning: LLM decides next action assistant_message, should_act = await self._reasoning_step(messages, tools, step=0, stage=stage) + success = True if should_act: - # Acting: execute tools and collect results (only once) t_tools, tool_messages = await self._acting_step(assistant_message, tools, step=0, stage=stage) used_tools.extend(t_tools) messages.extend(tool_messages) - success = True - else: - # No tools requested - success = True return used_tools, messages, success diff --git a/reme/agent/memory/default/reme_summarizer.yaml b/reme/agent/memory/default/reme_summarizer.yaml index d0a6748e..01a2f4bc 100644 --- a/reme/agent/memory/default/reme_summarizer.yaml +++ b/reme/agent/memory/default/reme_summarizer.yaml @@ -1,23 +1,23 @@ system_prompt: | - You are a Memory Orchestrator responsible for routing memory tasks to specialized agents based on the context. + You are a Memory Orchestrator responsible for routing memory summarization tasks to specialized agents based on the context. # Context {context} ## Available Memory Agents - Each line indicates a specialized Memory Agent dedicated to deep summarization and updating of memories within a specific dimension (). - Format: "- (): " + Each line indicates a specialized Memory Agent that is an expert for summarizing memories about a specific memory_target. {meta_memory_info} ## Your Task - Use the `delegate_task` tool to distribute memory tasks to specialized agents: - 1. Analyze the context and identify which memory dimensions require updates - 2. Specify `memory_type` and `memory_target` for each task - - The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above - - Do NOT create new agents or use () combinations that don't exist above - 3. Multiple tasks can be specified to enable parallel processing by specialized agents + Analyze the context and delegate summarization tasks to appropriate specialized agents: + 1. Examine the context content and identify which memory_target(s) are relevant for storing information + 2. For each relevant memory_target, delegate the summarization task to its corresponding specialized agent + - The memory_target must **exactly match** existing entries in the "Available Memory Agents" listed above + - Do NOT delegate to agents that don't exist above + - Each memory_target should be assigned **only once** - do not duplicate assignments + 3. Use the `delegate_task` tool **once** with all relevant memory_target(s) to enable parallel processing by specialized agents Note: If the context contains no memorable information (e.g., simple greetings), return ``. user_message: | - Please analyze the context and route memory tasks to the appropriate existing agents. + Please analyze the context and delegate summarization tasks to the appropriate specialized agents. diff --git a/reme/core/op/base_react.py b/reme/core/op/base_react.py index 1d46e09e..38f3a42c 100644 --- a/reme/core/op/base_react.py +++ b/reme/core/op/base_react.py @@ -38,6 +38,13 @@ class BaseReact(BaseOp): """Return available tools for the agent.""" return self.sub_ops + def pop_tool(self, name: str) -> "BaseTool | None": + """Remove and return a tool from self.tools by name.""" + for i, tool in enumerate(self.sub_ops): + if tool.tool_call.name == name: + return self.sub_ops.pop(i) + return None + async def build_messages(self) -> list[Message]: """Build initial message list from context query or messages.""" if self.context.get("query"): diff --git a/reme/tool/memory/add_memory.py b/reme/tool/memory/add_memory.py index 8ecdf965..1293634f 100644 --- a/reme/tool/memory/add_memory.py +++ b/reme/tool/memory/add_memory.py @@ -116,7 +116,7 @@ class AddMemory(BaseMemoryTool): "content": memory_content, "when_to_use": when_to_use, "message_time": message_time, - "ref_memory_id": self.history_node.memory_id, + "ref_memory_id": self.history_id, "author": self.author, "metadata": metadata, }) diff --git a/reme/tool/memory/base_memory_tool.py b/reme/tool/memory/base_memory_tool.py index 0f253b98..3d46e272 100644 --- a/reme/tool/memory/base_memory_tool.py +++ b/reme/tool/memory/base_memory_tool.py @@ -71,9 +71,11 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): raise ValueError("memory_target is not specified in context or memory_target_type_mapping!") @property - def history_node(self) -> MemoryNode: + def history_id(self) -> str: """Get the history node from context.""" - return self.context.history_node + if "history_node" in self.context: + return self.context.history_node.memory_id + return "" @property def retrieved_nodes(self) -> list[MemoryNode]: diff --git a/reme/tool/memory/delegate_task.py b/reme/tool/memory/delegate_task.py index 13c1ff07..8d9fecab 100644 --- a/reme/tool/memory/delegate_task.py +++ b/reme/tool/memory/delegate_task.py @@ -33,16 +33,10 @@ class DelegateTask(BaseMemoryTool): "properties": { "tasks": { "type": "array", - "description": "tasks to delegate to specific agents", + "description": "tasks to delegate to specific agents, each task is a memory_target", "items": { - "type": "object", - "properties": { - "task_name": { - "type": "string", - "description": "task_name", - }, - }, - "required": ["task_name"], + "type": "string", + "description": "memory_target to delegate to specific agents", }, }, }, @@ -58,13 +52,13 @@ class DelegateTask(BaseMemoryTool): # Submit tasks to agents agent_list: list[BaseMemoryAgent] = [] - for i, task in enumerate(tasks): - memory_type = self.memory_target_type_mapping[task] + for i, memory_target in enumerate(tasks): + memory_type = self.memory_target_type_mapping[memory_target] agent = self.memory_agent_dict[memory_type].copy() agent_list.append(agent) - logger.info(f"Task {i}: {memory_type.value} agent for {task}") - task_kwargs = {"memory_target": task} + logger.info(f"Task {i}: {memory_type.value} agent for {memory_target}") + task_kwargs = {"memory_target": memory_target} for k in ["query", "messages", "description", "history_node"]: if k in self.context: task_kwargs[k] = self.context[k] @@ -76,7 +70,7 @@ class DelegateTask(BaseMemoryTool): for agent in agent_list: results.append(f"Task: {agent.memory_target}\n{agent.response.answer}") - logger.info(f"Completed {len(results)} task(s)") + logger.info(f"Completed {len(results)} memory_target(s)") return { "answer": "\n\n".join(results), "agents": agent_list, diff --git a/reme/tool/memory/update_profile.py b/reme/tool/memory/update_profile.py index 2032964d..f854f3ab 100644 --- a/reme/tool/memory/update_profile.py +++ b/reme/tool/memory/update_profile.py @@ -82,7 +82,7 @@ class UpdateProfile(BaseMemoryTool): # Add new profiles using ProfileHandler (batch mode) added_count = 0 if profiles_to_add: - new_nodes = profile_handler.add_batch(profiles=profiles_to_add, ref_memory_id=self.history_node.memory_id) + new_nodes = profile_handler.add_batch(profiles=profiles_to_add, ref_memory_id=self.history_id) self.memory_nodes.extend(new_nodes) added_count = len(new_nodes) From c174aade760a69754cf5f593cebb709f0debcccf Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 16:54:28 +0800 Subject: [PATCH 07/17] feat(memory): extend memory system with procedural and tool memory support --- reme/agent/__init__.py | 2 + reme/agent/memory/default/__init__.py | 15 +- .../memory/default/personal_retriever.py | 10 +- .../memory/default/personal_retriever.yaml | 29 ++-- .../memory/default/personal_summarizer.py | 64 +++++--- .../memory/default/personal_summarizer.yaml | 120 +++++++++----- .../memory/default/procedural_retriever.py | 6 + .../memory/default/procedural_summarizer.py | 6 + reme/agent/memory/default/tool_retriever.py | 6 + reme/agent/memory/default/tool_summarizer.py | 6 + reme/core/__init__.py | 2 + reme/core/application.py | 106 ++++++++++++ reme/reme.py | 99 ++--------- reme/tool/memory/__init__.py | 12 +- .../memory/add_draft_and_read_all_profiles.py | 105 ++++++++++++ .../add_draft_and_retrieve_similar_memory.py | 107 ++++++++++++ .../{read_profile.py => read_all_profiles.py} | 4 +- reme/tool/memory/retrieve_memory.py | 37 +++-- reme/tool/memory/update_memory_v2.py | 155 ++++++++++++++++++ 19 files changed, 709 insertions(+), 182 deletions(-) create mode 100644 reme/agent/memory/default/procedural_retriever.py create mode 100644 reme/agent/memory/default/procedural_summarizer.py create mode 100644 reme/agent/memory/default/tool_retriever.py create mode 100644 reme/agent/memory/default/tool_summarizer.py create mode 100644 reme/core/application.py create mode 100644 reme/tool/memory/add_draft_and_read_all_profiles.py create mode 100644 reme/tool/memory/add_draft_and_retrieve_similar_memory.py rename reme/tool/memory/{read_profile.py => read_all_profiles.py} (91%) create mode 100644 reme/tool/memory/update_memory_v2.py diff --git a/reme/agent/__init__.py b/reme/agent/__init__.py index 45fed6e8..a2a0aa17 100644 --- a/reme/agent/__init__.py +++ b/reme/agent/__init__.py @@ -1,7 +1,9 @@ """A simple chatbot.""" from . import chat +from . import memory __all__ = [ "chat", + "memory", ] diff --git a/reme/agent/memory/default/__init__.py b/reme/agent/memory/default/__init__.py index 516a81c1..65eb3463 100644 --- a/reme/agent/memory/default/__init__.py +++ b/reme/agent/memory/default/__init__.py @@ -1,13 +1,26 @@ -"""Default memory agents for personal and ReMe memory operations.""" +"""Default memory agents for personal, procedural, tool and ReMe memory operations.""" from .personal_retriever import PersonalRetriever from .personal_summarizer import PersonalSummarizer +from .procedural_retriever import ProceduralRetriever +from .procedural_summarizer import ProceduralSummarizer from .reme_retriever import ReMeRetriever from .reme_summarizer import ReMeSummarizer +from .tool_retriever import ToolRetriever +from .tool_summarizer import ToolSummarizer +from ....core import R __all__ = [ "PersonalRetriever", "PersonalSummarizer", + "ProceduralRetriever", + "ProceduralSummarizer", "ReMeRetriever", "ReMeSummarizer", + "ToolRetriever", + "ToolSummarizer", ] + +for name in __all__: + tool_class = globals()[name] + R.op.register()(tool_class) diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/default/personal_retriever.py index 1cf68ab7..7fc8a0c8 100644 --- a/reme/agent/memory/default/personal_retriever.py +++ b/reme/agent/memory/default/personal_retriever.py @@ -20,6 +20,13 @@ class PersonalRetriever(BaseMemoryAgent): 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, @@ -27,7 +34,7 @@ class PersonalRetriever(BaseMemoryAgent): prompt_name="system_prompt", memory_type=self.memory_type.value, memory_target=self.memory_target, - user_profile=await self.read_user_profile(show_id="history"), + user_profile=all_profiles, context=context.strip(), ), ), @@ -59,5 +66,4 @@ class PersonalRetriever(BaseMemoryAgent): async def execute(self): result = await super().execute() result["retrieved_nodes"] = self.retrieved_nodes - return result diff --git a/reme/agent/memory/default/personal_retriever.yaml b/reme/agent/memory/default/personal_retriever.yaml index 9a882c48..da8eead2 100644 --- a/reme/agent/memory/default/personal_retriever.yaml +++ b/reme/agent/memory/default/personal_retriever.yaml @@ -1,45 +1,44 @@ system_prompt: | - You are a memory agent managing **{memory_type}** memories about **{memory_target}**. + You are a memory retrieval Agent responsible for retrieving {memory_type} memories about {memory_target}. ## User Profile {user_profile} - ## Question + ## User Question {context} - + ## Retrieval Strategy - - **Tool 1: Vector Search (`retrieve_memory`)** + ### Phase 1 `retrieve_memory` - Purpose: Search for relevant memories using semantic similarity - - Try at least 3-5 different queries before moving to next tool: + - Try at least 3-5 different queries before moving to next phase: + * Direct question * Direct question reformulation * Different phrasings and perspectives * Entity-focused queries (names, places, events) * Various keyword combinations - - Time range filtering (optional): + - Time filter (optional): * Format: single date '20200101' or range '20200101,20200102' * Example: '20200101,20200102' for 20200101 <= time <= 20200102 * Single-sided: '0,20200102' (before date) or '20200101,99999999' (after date) - If no results: retry with different time ranges or remove time constraints - **Tool 2: Read History (`read_history`) - ONLY AFTER Tool 1** + ### Phase 2 `read_history` - Purpose: Read full original conversation context - Use this ONLY after completing multiple retrieve_memory attempts - - Extract history_id from retrieved memory results + - Extract history_id from context - Prioritize most relevant or recent history entries - Read multiple histories if needed for complete understanding ## Response Requirements - - Answer ONLY based on retrieved memories and user profile - NO hallucination or inference + - Answer ONLY based on retrieved memories / user profile / history - NO hallucination or inference - Always cite the source: reference specific memories with their timestamps - If information conflicts, present all versions with their respective times - Try multiple search angles before concluding no information exists - ## Output Format - When answering, structure your response as follows: - - [timestamp][Relevant history/memory/profile from context] - - If no relevant information found after thorough search (5+ queries), state: + ### Output Format + 1. When answering, structure your response as follows: + - [timestamp][Relevant retrieved memories / user profile / history from context] + 2. If no relevant information found after thorough search (5+ queries), state: "No relevant information found after thorough search using multiple query strategies." user_message: | diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index d6ac1c41..5885ed23 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -1,5 +1,4 @@ """Personal memory summarizer agent for two-phase personal memory processing.""" - from loguru import logger from ..base_memory_agent import BaseMemoryAgent @@ -13,13 +12,12 @@ class PersonalSummarizer(BaseMemoryAgent): memory_type: MemoryType = MemoryType.PERSONAL - async def _build_phase1_messages(self) -> list[Message]: - """Build messages for phase 1: retrieve and add memory.""" + async def _build_s1_messages(self) -> list[Message]: return [ Message( role=Role.SYSTEM, content=self.prompt_format( - prompt_name="system_prompt_phase1", + prompt_name="system_prompt_s1", context=self.context.history_node.content, memory_type=self.memory_type.value, memory_target=self.memory_target, @@ -27,26 +25,24 @@ class PersonalSummarizer(BaseMemoryAgent): ), Message( role=Role.USER, - content=self.get_prompt("user_message_phase1"), + content=self.get_prompt("user_message_s1"), ), ] - async def _build_phase2_messages(self) -> list[Message]: - """Build messages for phase 2: update user profile.""" + async def _build_s2_messages(self) -> list[Message]: return [ Message( role=Role.SYSTEM, content=self.prompt_format( - prompt_name="system_prompt_phase2", + prompt_name="system_prompt_s2", context=self.context.history_node.content, memory_type=self.memory_type.value, memory_target=self.memory_target, - user_profile=await self.read_user_profile(show_id="profile"), ), ), Message( role=Role.USER, - content=self.get_prompt("user_message_phase2"), + content=self.get_prompt("user_message_s2"), ), ] @@ -73,34 +69,52 @@ class PersonalSummarizer(BaseMemoryAgent): ) async def execute(self): - """Execute two-phase memory processing: retrieve/add -> update profile.""" - tools = self.tools - for i, tool in enumerate(tools): + 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)}") - messages_phase1 = await self._build_phase1_messages() - for i, message in enumerate(messages_phase1): + 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__} S1] role={role} {message.simple_dump(as_dict=False)}") - tools_phase1, messages_phase1, success_phase1 = await self.react(messages_phase1, tools[:-1], stage="S1") + 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) - messages_phase2 = await self._build_phase2_messages() - for i, message in enumerate(messages_phase2): - role = message.name or message.role - logger.info(f"[{self.__class__.__name__} S2] role={role} {message.simple_dump(as_dict=False)}") - tools_phase2, messages_phase2, success_phase2 = await self.react(messages_phase2, tools[-1:], stage="S2") + if profile_tools: + stage = "s2-profile" + messages_s2 = await self._build_s2_messages() + 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 - success = success_phase1 and success_phase2 - messages = messages_phase1 + messages_phase2 - tools = tools_phase1 + tools_phase2 + 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) + profile_nodes = [] + for tool in tools: + if tool.profile_nodes: + profile_nodes.extend(tool.profile_nodes) + return { "answer": memory_nodes, "success": success, "messages": messages, "tools": tools, + "profile_nodes": profile_nodes, } diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index 4fbd7348..ada57fd4 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -1,45 +1,91 @@ -system_prompt_phase1: | - You are a memory agent managing **{memory_type}** memories about **{memory_target}**. - - ## Latest Conversation: - Message format: `round [] : ` (timestamp: YYYY-MM-DD HH:MM:SS). +system_prompt_s1_zh: | + 你是一个记忆Agent,负责管理关于 {memory_target} 的 {memory_type} 类型记忆。 + + ## 最新对话 + Format: round [] : {context} + + ## 任务 + ### 步骤1 + 根据`最新对话`的内容,在 `add_draft_and_retrieve_similar_memory` 中创建记忆草稿 `memory_draft`。 + 工具会根据memory_draft的内容行向量检索,返回历史相似记忆,确保在第二步的时候更好的管理记忆库记忆。 + + ### 步骤2 + 使用`update_memory`更新向量库记忆。 + 通过`memory_ids_to_delete`删除历史记忆,`memories_to_add`添加新记忆,包括message_time和memory_content。 + 要求: + - 原样提取最新对话中的内容,不得推断、假设或编造。 + - 最后记忆库包含所有的历史记忆和新的记忆,例如记录在同一个主题下用户不同时间的变化。 + - 最后记忆库有比较好的组织,同一主题的记忆放到同一条中,不要有重复/多余的记忆。 - ## Task: Retrieve Similar Memories and Add New Memories - **CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate. +user_message_s1_zh: | + 严格按照步骤1和步骤2完成任务 - ### Step 1: Retrieve Similar Memories - Use `retrieve_memory` to search for existing similar memories about **{memory_target}**. - - Use appropriate queries to find relevant existing memories - - Check if new information already exists in the memory store - - ### Step 2: Add New Memories - Use `add_memory` to add new memories: - - Extract and summarize important information about **{memory_target}** - - Set `update_time` (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable) - - If the information is completely identical to existing memory, skip adding - -user_message_phase1: | - First retrieve similar memories, then extract and add new personal memories from the conversation. - -system_prompt_phase2: | - You are a memory agent managing **{memory_type}** memories about **{memory_target}**. - - ## Latest Conversation: - Message format: `round [] : ` (timestamp: YYYY-MM-DD HH:MM:SS). +system_prompt_s2_zh: | + 你是一个Profile Agent,负责管理关于 {memory_target} 的 Profile。 + + ## 最新对话 + Format: round [] : {context} + + ## 任务 + ### 步骤1 + 根据`最新对话`的内容,在 `add_draft_and_read_all_profiles` 中创建记忆草稿 `profile_draft`。 + 工具会直接返回所有的Profile,确保在第二步的时候更好的管理Profile。 + + ### 步骤2 + 使用`update_profile`更新profile库。 + 通过`profile_ids_to_delete`删除历史Profile,`profiles_to_add`添加新Profile,包括message_time、profile_key和profile_value。 + 要求: + - 原样提取最新对话中的内容,不得推断、假设或编造。 + - 最后Profile库只保留用户最新的状态。例如用户开始喜欢吃苹果,后来只吃喜欢香蕉,可以记录:水果偏好:香蕉 + - 最后Profile库有比较好的组织,同一主题的Profile放到同一条中,不要有重复/多余的Profile。 - ## Current User Profile: - UserProfile format: `profile_id= update_time= `. - {user_profile} +user_message_s2_zh: | + 严格按照步骤1和步骤2完成任务 - ## Task: Update Profile with `UpdateUserProfile` - **CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate. +system_prompt_s1: | + You are a Memory Agent responsible for managing {memory_type} type memories about {memory_target}. + + ## Latest Conversation + Format: round [] : + {context} + + ## Task + ### Step 1 + Based on the content of `Latest Conversation`, create a memory draft `memory_draft` in `add_draft_and_retrieve_similar_memory`. + The tool will perform vector retrieval based on the content of memory_draft and return historically similar memories to better manage the memory store in Step 2. + + ### Step 2 + Use `update_memory` to update the vector store memories. + Delete historical memories through `memory_ids_to_delete`, add new memories through `memories_to_add`, including message_time and memory_content. + Requirements: + - Extract content from the latest conversation as-is, without inference, assumption, or fabrication. + - The final memory store should contain all historical memories and new memories, for example, recording user changes at different times under the same topic. + - The final memory store should be well-organized, with memories on the same topic placed in one entry, without duplicate/redundant memories. - Synchronize profile/memories with new information from the conversation, including **{memory_target}**' current status: - - `profile_ids_to_delete`: Remove conflicting, or redundant entries. - - `profiles_to_add`: Add new profiles/memories with `update_time`, e.g. `YYYY-MM-DD HH:MM:SS`, {memory_target} did something. - - Maintain profiles that are concise, mutually exclusive, and collectively comprehensive with no information loss. +user_message_s1: | + Strictly complete the task following Step 1 and Step 2 -user_message_phase2: | - Update user profile using `UpdateUserProfile` based on the conversation and current profile. \ No newline at end of file +system_prompt_s2: | + You are a Profile Agent responsible for managing the Profile about {memory_target}. + + ## Latest Conversation + Format: round [] : + {context} + + ## Task + ### Step 1 + Based on the content of `Latest Conversation`, create a profile draft `profile_draft` in `add_draft_and_read_all_profiles`. + The tool will directly return all Profiles to better manage the Profile store in Step 2. + + ### Step 2 + Use `update_profile` to update the profile store. + Delete historical Profiles through `profile_ids_to_delete`, add new Profiles through `profiles_to_add`, including message_time, profile_key, and profile_value. + Requirements: + - Extract content from the latest conversation as-is, without inference, assumption, or fabrication. + - The final Profile store should only keep the user's latest state. For example, if the user initially liked apples but later only likes bananas, record: Fruit preference: banana + - The final Profile store should be well-organized, with Profiles on the same topic placed in one entry, without duplicate/redundant Profiles. + +user_message_s2: | + Strictly complete the task following Step 1 and Step 2 diff --git a/reme/agent/memory/default/procedural_retriever.py b/reme/agent/memory/default/procedural_retriever.py new file mode 100644 index 00000000..63d4856b --- /dev/null +++ b/reme/agent/memory/default/procedural_retriever.py @@ -0,0 +1,6 @@ +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ProceduralRetriever(BaseMemoryAgent): + memory_type: MemoryType = MemoryType.PROCEDURAL diff --git a/reme/agent/memory/default/procedural_summarizer.py b/reme/agent/memory/default/procedural_summarizer.py new file mode 100644 index 00000000..0be6bd98 --- /dev/null +++ b/reme/agent/memory/default/procedural_summarizer.py @@ -0,0 +1,6 @@ +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ProceduralSummarizer(BaseMemoryAgent): + memory_type: MemoryType = MemoryType.PROCEDURAL diff --git a/reme/agent/memory/default/tool_retriever.py b/reme/agent/memory/default/tool_retriever.py new file mode 100644 index 00000000..53e603fc --- /dev/null +++ b/reme/agent/memory/default/tool_retriever.py @@ -0,0 +1,6 @@ +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ToolRetriever(BaseMemoryAgent): + memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/agent/memory/default/tool_summarizer.py b/reme/agent/memory/default/tool_summarizer.py new file mode 100644 index 00000000..5064b6db --- /dev/null +++ b/reme/agent/memory/default/tool_summarizer.py @@ -0,0 +1,6 @@ +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ToolSummarizer(BaseMemoryAgent): + memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/core/__init__.py b/reme/core/__init__.py index 251c2433..2b56fb08 100644 --- a/reme/core/__init__.py +++ b/reme/core/__init__.py @@ -11,6 +11,7 @@ from . import service from . import token_counter from . import utils from . import vector_store +from .application import Application from .context import R __all__ = [ @@ -25,5 +26,6 @@ __all__ = [ "token_counter", "utils", "vector_store", + "Application", "R", ] diff --git a/reme/core/application.py b/reme/core/application.py new file mode 100644 index 00000000..a03c3e73 --- /dev/null +++ b/reme/core/application.py @@ -0,0 +1,106 @@ +import asyncio + +from .context import PromptHandler, ServiceContext +from .embedding import BaseEmbeddingModel +from .flow import BaseFlow +from .llm import BaseLLM +from .schema import Response +from .token_counter import BaseTokenCounter +from .utils import execute_stream_task, PydanticConfigParser +from .vector_store import BaseVectorStore + + +class Application: + + def __init__( + self, + *args, + llm_api_key: str | None = None, + llm_api_base: str | None = None, + embedding_api_key: str | None = None, + embedding_api_base: str | None = None, + enable_logo: bool = True, + parser: type[PydanticConfigParser] | None = None, + llm: dict | None = None, + embedding_model: dict | None = None, + vector_store: dict | None = None, + token_counter: dict | None = None, + **kwargs, + ): + # ServiceContext + self.service_context = ServiceContext( + *args, + llm_api_key=llm_api_key, + llm_api_base=llm_api_base, + embedding_api_key=embedding_api_key, + embedding_api_base=embedding_api_base, + service_config=None, + parser=parser, + config_path=None, + enable_logo=enable_logo, + llm=llm, + embedding_model=embedding_model, + vector_store=vector_store, + token_counter=token_counter, + **kwargs, + ) + + # PromptHandler + self.prompt_handler = PromptHandler(language=self.service_context.language) + + # LLM & EmbeddingModel & VectorStore & TokenCounter + self.llm: BaseLLM | None = self.service_context.llms.get("default", None) + self.embedding_model: BaseEmbeddingModel | None = self.service_context.embedding_models.get("default", None) + self.vector_store: BaseVectorStore | None = self.service_context.vector_stores.get("default", None) + self.token_counter: BaseTokenCounter | None = self.service_context.token_counters.get("default", None) + + async def __aenter__(self): + """Async context manager entry.""" + return self + + def __enter__(self): + """Context manager entry.""" + return self + + async def close(self): + """Close""" + return await self.service_context.close() + + def close_sync(self): + """Close synchronously""" + self.service_context.close_sync() + + async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None): + """Async context manager exit.""" + await self.close() + return False + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + """Context manager exit.""" + self.close_sync() + return False + + async def execute_flow(self, name: str, **kwargs) -> Response: + """Execute a flow with the given name and parameters.""" + assert name in self.service_context.flows, f"Flow {name} not found" + flow: BaseFlow = self.service_context.flows[name] + return await flow.call(**kwargs) + + async def execute_stream_flow(self, name: str, **kwargs): + """Execute a stream flow with the given name and parameters.""" + assert name in self.service_context.flows, f"Flow {name} not found" + flow: BaseFlow = self.service_context.flows[name] + assert flow.stream is True, "non-stream flow is not supported in execute_stream_flow!" + stream_queue = asyncio.Queue() + task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs)) + async for chunk in execute_stream_task( + stream_queue=stream_queue, + task=task, + task_name=name, + as_bytes=False, + ): + yield chunk + + def run_service(self): + """Run the configured service (HTTP, MCP, or CMD).""" + self.service_context.service.run() diff --git a/reme/reme.py b/reme/reme.py index 0c4a0fc2..bca18f80 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -6,6 +6,7 @@ from pathlib import Path from loguru import logger +from .core import Application from .agent.memory.default import ReMeSummarizer, PersonalSummarizer, PersonalRetriever, ReMeRetriever from .config import ReMeConfigParser from .core.context import PromptHandler, ServiceContext @@ -21,7 +22,7 @@ from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, DelegateT ProfileHandler -class ReMe: +class ReMe(Application): """ReMe with config file support and flow execution methods.""" def __init__( @@ -50,7 +51,20 @@ class ReMe: tool_retrieve_version: str = "default", **kwargs, ): - # MemoryTarget -> MemoryType + super().__init__( + *args, + llm_api_key=llm_api_key, + llm_api_base=llm_api_base, + embedding_api_key=embedding_api_key, + embedding_api_base=embedding_api_base, + enable_logo=enable_logo, + parser=ReMeConfigParser, + llm=llm, + embedding_model=embedding_model, + vector_store=vector_store, + token_counter=token_counter, + **kwargs, + ) memory_target_type_mapping: dict[str, MemoryType] = {} if personal_memory_target: for name in personal_memory_target: @@ -66,37 +80,9 @@ class ReMe: for name in tool_memory_target: assert name not in memory_target_type_mapping, f"Memory target name {name} is already used." memory_target_type_mapping[name] = MemoryType.TOOL - - # ServiceContext - self.service_context = ServiceContext( - *args, - llm_api_key=llm_api_key, - llm_api_base=llm_api_base, - embedding_api_key=embedding_api_key, - embedding_api_base=embedding_api_base, - service_config=None, - parser=ReMeConfigParser, - config_path=None, - enable_logo=enable_logo, - llm=llm, - embedding_model=embedding_model, - vector_store=vector_store, - token_counter=token_counter, - memory_target_type_mapping=memory_target_type_mapping, - **kwargs, - ) - + self.service_context.memory_target_type_mapping = memory_target_type_mapping self.profile_path: str = profile_path - # PromptHandler - self.prompt_handler = PromptHandler(language=self.service_context.language) - - # LLM & EmbeddingModel & VectorStore & TokenCounter - self.llm: BaseLLM | None = self.service_context.llms.get("default", None) - self.embedding_model: BaseEmbeddingModel | None = self.service_context.embedding_models.get("default", None) - self.vector_store: BaseVectorStore | None = self.service_context.vector_stores.get("default", None) - self.token_counter: BaseTokenCounter | None = self.service_context.token_counters.get("default", None) - @property def memory_target_type_mapping(self) -> dict[str, MemoryType]: mapping = {} @@ -395,57 +381,6 @@ class ReMe: async def context_reload(self): """working memory retrieve""" - async def execute_flow(self, name: str, **kwargs) -> Response: - """Execute a flow with the given name and parameters.""" - assert name in self.service_context.flows, f"Flow {name} not found" - flow: BaseFlow = self.service_context.flows[name] - return await flow.call(**kwargs) - - async def execute_stream_flow(self, name: str, **kwargs): - """Execute a stream flow with the given name and parameters.""" - assert name in self.service_context.flows, f"Flow {name} not found" - flow: BaseFlow = self.service_context.flows[name] - assert flow.stream is True, "non-stream flow is not supported in execute_stream_flow!" - stream_queue = asyncio.Queue() - task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs)) - async for chunk in execute_stream_task( - stream_queue=stream_queue, - task=task, - task_name=name, - as_bytes=False, - ): - yield chunk - - def run_service(self): - """Run the configured service (HTTP, MCP, or CMD).""" - self.service_context.service.run() - - async def __aenter__(self): - """Async context manager entry.""" - return self - - def __enter__(self): - """Context manager entry.""" - return self - - async def close(self): - """Close""" - return await self.service_context.close() - - def close_sync(self): - """Close synchronously""" - self.service_context.close_sync() - - async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None): - """Async context manager exit.""" - await self.close() - return False - - def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): - """Context manager exit.""" - self.close_sync() - return False - def main(): """Main entry point for running ReMe from command line.""" diff --git a/reme/tool/memory/__init__.py b/reme/tool/memory/__init__.py index 52e86d80..f0f61870 100644 --- a/reme/tool/memory/__init__.py +++ b/reme/tool/memory/__init__.py @@ -1,31 +1,39 @@ """memory tools""" +from .add_draft_and_read_all_profiles import AddDraftAndReadAllProfiles +from .add_draft_and_retrieve_similar_memory import AddDraftAndRetrieveSimilarMemory from .add_history import AddHistory from .add_memory import AddMemory from .base_memory_tool import BaseMemoryTool from .delegate_task import DelegateTask from .delete_memory import DeleteMemory +from .memory_handler import MemoryHandler from .profile_handler import ProfileHandler +from .read_all_profiles import ReadAllProfiles from .read_history import ReadHistory -from .read_profile import ReadProfile from .retrieve_memory import RetrieveMemory from .retrieve_recent_memory import RetrieveRecentMemory from .update_memory import UpdateMemory +from .update_memory_v2 import UpdateMemoryV2 from .update_profile import UpdateProfile from ...core import R __all__ = [ + "AddDraftAndReadAllProfiles", + "AddDraftAndRetrieveSimilarMemory", "AddHistory", "AddMemory", "BaseMemoryTool", "DelegateTask", "DeleteMemory", + "MemoryHandler", "ProfileHandler", + "ReadAllProfiles", "ReadHistory", - "ReadProfile", "RetrieveMemory", "RetrieveRecentMemory", "UpdateMemory", + "UpdateMemoryV2", "UpdateProfile", ] diff --git a/reme/tool/memory/add_draft_and_read_all_profiles.py b/reme/tool/memory/add_draft_and_read_all_profiles.py new file mode 100644 index 00000000..c11fc82d --- /dev/null +++ b/reme/tool/memory/add_draft_and_read_all_profiles.py @@ -0,0 +1,105 @@ +"""Add draft profile and read all profiles from local storage""" +from pathlib import Path + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .profile_handler import ProfileHandler +from ...core.schema import ToolCall + + +class AddDraftAndReadAllProfiles(BaseMemoryTool): + """Tool to add draft profile and read all profiles""" + + def __init__(self, profile_path: str, enable_memory_target: bool = False, **kwargs): + super().__init__(**kwargs) + self.profile_path: str = profile_path + self.enable_memory_target: bool = enable_memory_target + + def _build_query_parameters(self) -> dict: + """Build the query parameters schema""" + properties = { + "profile_draft": { + "type": "string", + "description": "profile_draft", + }, + } + required = ["profile_draft"] + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "memory_target", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "Add draft profile and read all profiles from local storage.", + "parameters": self._build_query_parameters(), + }, + ) + + def _build_multiple_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "Add draft profile and read all profiles from local storage.", + "parameters": { + "type": "object", + "properties": { + "draft_items": { + "type": "array", + "description": "List of draft profile items.", + "items": self._build_query_parameters(), + }, + }, + "required": ["draft_items"], + }, + }, + ) + + async def execute(self): + if self.enable_multiple: + draft_items = self.context.get("draft_items", []) + else: + draft_items = [self.context] + + # Collect all profiles from all targets + all_profiles = [] + targets_processed = set() + + for item in draft_items: + if self.enable_memory_target: + target = item["memory_target"] + else: + target = self.memory_target + + # Skip if already processed this target + if target in targets_processed: + continue + targets_processed.add(target) + + profile_handler = ProfileHandler( + profile_path=Path(self.profile_path) / self.vector_store.collection_name, + memory_target=target, + ) + + profiles_str = profile_handler.read_all(add_profile_id=True) + if profiles_str: + all_profiles.append(f"## Profiles for {target}:\n{profiles_str}") + + if not all_profiles: + output = "No profiles found." + logger.info(output) + return output + + output = "\n\n".join(all_profiles) + logger.info(f"Successfully read profiles for {len(targets_processed)} target(s)") + return output diff --git a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py new file mode 100644 index 00000000..8be42636 --- /dev/null +++ b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py @@ -0,0 +1,107 @@ +"""Add draft memory and retrieve similar memories from vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall, MemoryNode +from ...core.utils import deduplicate_memories + + +class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): + """Tool to add draft memory and retrieve similar memories""" + + def __init__(self, top_k: int = 20, enable_memory_target: bool = False, **kwargs): + super().__init__(**kwargs) + self.top_k: int = top_k + self.enable_memory_target: bool = enable_memory_target + + def _build_query_parameters(self) -> dict: + """Build the query parameters schema""" + properties = { + "memory_draft": { + "type": "string", + "description": "memory_draft", + }, + } + required = ["memory_draft"] + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "memory_target", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "Add draft memory and retrieve similar memories from the vector store.", + "parameters": self._build_query_parameters(), + }, + ) + + def _build_multiple_tool_call(self) -> ToolCall: + return ToolCall( + **{ + "description": "Add draft memory and retrieve similar memories from the vector store.", + "parameters": { + "type": "object", + "properties": { + "draft_items": { + "type": "array", + "description": "List of draft memory items.", + "items": self._build_query_parameters(), + }, + }, + "required": ["draft_items"], + }, + }, + ) + + async def execute(self): + if self.enable_multiple: + draft_items = self.context.get("draft_items", []) + else: + draft_items = [self.context] + + queries_by_target: dict[str, list[dict]] = {} + for item in draft_items: + if self.enable_memory_target: + target = item["memory_target"] + else: + target = self.memory_target + if target not in queries_by_target: + queries_by_target[target] = [] + + queries_by_target[target].append({ + "query": item["memory_draft"], + "limit": self.top_k, + "filters": {}, + }) + + # Execute batch searches for each target + memory_nodes: list[MemoryNode] = [] + for target, searches in queries_by_target.items(): + handler = MemoryHandler(target, self.service_context) + nodes = await handler.batch_search(searches) + memory_nodes.extend(nodes) + + memory_nodes = deduplicate_memories(memory_nodes) + retrieved_ids = {n.memory_id for n in self.retrieved_nodes if n.memory_id} + new_nodes = [n for n in memory_nodes if n.memory_id not in retrieved_ids] + self.retrieved_nodes.extend(new_nodes) + + if not new_nodes: + output = "No similar memories found." + else: + output = "\n".join([n.format(ref_memory_id_key="history_id") for n in new_nodes]) + + logger.info(f"Retrieved {len(memory_nodes)} similar memories, {len(new_nodes)} new after deduplication") + return output diff --git a/reme/tool/memory/read_profile.py b/reme/tool/memory/read_all_profiles.py similarity index 91% rename from reme/tool/memory/read_profile.py rename to reme/tool/memory/read_all_profiles.py index b3286194..3d492ef7 100644 --- a/reme/tool/memory/read_profile.py +++ b/reme/tool/memory/read_all_profiles.py @@ -8,7 +8,7 @@ from .profile_handler import ProfileHandler from ...core.schema import ToolCall -class ReadProfile(BaseMemoryTool): +class ReadAllProfiles(BaseMemoryTool): """Tool to read all user profiles""" def __init__(self, profile_path: str, **kwargs): @@ -35,7 +35,7 @@ class ReadProfile(BaseMemoryTool): memory_target=self.memory_target, ) - profiles_str = profile_handler.read_all() + profiles_str = profile_handler.read_all(add_profile_id=True) if not profiles_str: output = "No profiles found." logger.info(output) diff --git a/reme/tool/memory/retrieve_memory.py b/reme/tool/memory/retrieve_memory.py index f8845405..48fa47dc 100644 --- a/reme/tool/memory/retrieve_memory.py +++ b/reme/tool/memory/retrieve_memory.py @@ -11,29 +11,34 @@ from ...core.utils import deduplicate_memories class RetrieveMemory(BaseMemoryTool): """Tool to retrieve memories using similarity search""" - def __init__(self, top_k: int = 20, enable_memory_target: bool = False, **kwargs): + def __init__(self, top_k: int = 20, enable_memory_target: bool = False, enable_time_filter: bool = False, **kwargs): super().__init__(**kwargs) self.top_k: int = top_k self.enable_memory_target: bool = enable_memory_target + self.enable_time_filter: bool = enable_time_filter def _build_query_parameters(self) -> dict: """Build the query parameters schema based on enabled features.""" properties = { "query": { "type": "string", - "description": "query text for vector similarity search.", - }, - "time_range": { - "type": "string", - "description": "optional time range filter. Format: '20200101' or '20200101,20200102'", + "description": "query", }, } required = ["query"] + if self.enable_time_filter: + properties["time_filter"] = { + "type": "string", + "description": "Optional time filter to narrow down search results by date. " + "Format: single date '20200101' for exact date match, " + "or date range '20200101,20200102' for inclusive range filtering.", + } + if self.enable_memory_target: properties["memory_target"] = { "type": "string", - "description": "target memory type to search in.", + "description": "memory_target", } required.append("memory_target") @@ -46,7 +51,7 @@ class RetrieveMemory(BaseMemoryTool): def _build_tool_call(self) -> ToolCall: return ToolCall( **{ - "description": "retrieve memories using vector similarity search.", + "description": "Retrieve relevant memories from the vector store using semantic similarity search.", "parameters": self._build_query_parameters(), }, ) @@ -54,13 +59,13 @@ class RetrieveMemory(BaseMemoryTool): def _build_multiple_tool_call(self) -> ToolCall: return ToolCall( **{ - "description": "retrieve memories using multiple queries with vector similarity search.", + "description": "Retrieve relevant memories from the vector store using semantic similarity search.", "parameters": { "type": "object", "properties": { "query_items": { "type": "array", - "description": "list of query items for vector similarity search.", + "description": "List of query items.", "items": self._build_query_parameters(), }, }, @@ -85,14 +90,14 @@ class RetrieveMemory(BaseMemoryTool): queries_by_target[target] = [] filters = {} - time_range = item.get("time_range") - if time_range: - time_range = time_range.strip() - if "," in time_range: - start, end = time_range.split(",") + time_filter = item.get("time_filter") + if time_filter: + time_filter = time_filter.strip() + if "," in time_filter: + start, end = time_filter.split(",") filters = {"time_int": [int(start.strip()), int(end.strip())]} else: - filters = {"time_int": [int(time_range), int(time_range)]} + filters = {"time_int": [int(time_filter), int(time_filter)]} queries_by_target[target].append({ "query": item["query"], diff --git a/reme/tool/memory/update_memory_v2.py b/reme/tool/memory/update_memory_v2.py new file mode 100644 index 00000000..f9167e69 --- /dev/null +++ b/reme/tool/memory/update_memory_v2.py @@ -0,0 +1,155 @@ +"""Update memory in vector store""" + +from loguru import logger + +from .base_memory_tool import BaseMemoryTool +from .memory_handler import MemoryHandler +from ...core.schema import ToolCall + + +class UpdateMemoryV2(BaseMemoryTool): + """Tool to update memories in vector store by deleting and adding memory entries""" + + def __init__( + self, + name="update_memory", + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, + ): + kwargs["enable_multiple"] = True + super().__init__(name=name, **kwargs) + self.enable_memory_target: bool = enable_memory_target + self.enable_when_to_use: bool = enable_when_to_use + + def _build_add_memory_parameters(self) -> dict: + """Build the add memory parameters schema based on enabled features.""" + properties = { + "message_time": { + "type": "string", + "description": "message time, e.g. '2020-01-01 00:00:00'", + }, + "memory_content": { + "type": "string", + "description": "content of the memory.", + }, + } + required = ["message_time", "memory_content"] + + if self.enable_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": "description of when to use this memory.", + } + required.append("when_to_use") + + if self.enable_memory_target: + properties["memory_target"] = { + "type": "string", + "description": "target memory type for this memory.", + } + required.append("memory_target") + + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_tool_call(self) -> ToolCall: + """Build and return the multiple tool call schema""" + return ToolCall( + **{ + "description": "update memories by removing and adding memory entries.", + "parameters": { + "type": "object", + "properties": { + "memory_ids_to_delete": { + "type": "array", + "description": "List of memory IDs to delete", + "items": { + "type": "string" + }, + }, + "memories_to_add": { + "type": "array", + "description": "List of memories to add", + "items": self._build_add_memory_parameters(), + }, + }, + "required": ["memory_ids_to_delete", "memories_to_add"], + }, + }, + ) + + async def execute(self): + # Get parameters + memory_ids_to_delete = self.context.get("memory_ids_to_delete", []) + memory_ids_to_delete = sorted(set([mid for mid in memory_ids_to_delete if mid])) + memories_to_add = self.context.get("memories_to_add", []) + + if not memory_ids_to_delete and not memories_to_add: + return "No memories to remove or add, operation completed." + + # Group memories by memory_target if enabled + if self.enable_memory_target: + memories_by_target = {} + for mem in memories_to_add: + target = mem.get("memory_target", self.memory_target) + if target not in memories_by_target: + memories_by_target[target] = [] + memories_by_target[target].append(mem) + else: + memories_by_target = {self.memory_target: memories_to_add} + + # Delete memories (all at once, regardless of target) + removed_count = 0 + if memory_ids_to_delete: + # Use the default memory_target handler for deletion + handler = MemoryHandler(self.memory_target, self.service_context) + await handler.delete(memory_ids_to_delete) + removed_count = len(memory_ids_to_delete) + + # Add new memories by target + added_count = 0 + all_memory_nodes = [] + for target, target_memories in memories_by_target.items(): + # Parse and prepare add data + add_dicts = [] + for mem in target_memories: + memory_content = mem.get("memory_content", "") + message_time = mem.get("message_time", "") + when_to_use = mem.get("when_to_use", "") if self.enable_when_to_use else "" + metadata = {} + try: + metadata["time_int"] = int(message_time.split(" ")[0].replace("-", "")) + except Exception: + logger.warning(f"Invalid message time format: {message_time}") + + add_dicts.append({ + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "ref_memory_id": self.history_id, + "author": self.author, + "metadata": metadata, + }) + + if add_dicts: + handler = MemoryHandler(target, self.service_context) + memory_nodes = await handler.add_batch(add_dicts) + all_memory_nodes.extend(memory_nodes) + added_count += len(memory_nodes) + + # Extend memory_nodes for tracking + self.memory_nodes.extend(all_memory_nodes) + + # Build output message + operations = [] + if removed_count > 0: + operations.append(f"removed {removed_count} old memories.") + if added_count > 0: + operations.append(f"added {added_count} new memories.") + operations.append("Operation completed.") + logger.info("\n".join(operations)) + return "\n".join(operations) From c06fd78763ac8d263f1ef0aa544c82fd0215845f Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 20:10:38 +0800 Subject: [PATCH 08/17] refactor(memory): refactor memory tools and handlers for improved structure --- .../memory/default/personal_retriever.py | 6 +- .../memory/default/personal_retriever.yaml | 2 +- .../memory/default/personal_summarizer.py | 1 + .../memory/default/personal_summarizer.yaml | 24 +- .../memory/default/procedural_retriever.py | 4 + .../memory/default/procedural_summarizer.py | 4 + reme/agent/memory/default/tool_retriever.py | 4 + reme/agent/memory/default/tool_summarizer.py | 4 + reme/core/application.py | 37 +- reme/core/schema/memory_node.py | 12 +- reme/reme.py | 492 ++++++++---------- reme/tool/memory/__init__.py | 2 +- .../memory/add_draft_and_read_all_profiles.py | 9 +- .../add_draft_and_retrieve_similar_memory.py | 12 +- reme/tool/memory/add_memory.py | 26 +- reme/tool/memory/base_memory_tool.py | 8 + reme/tool/memory/memory_handler.py | 56 +- reme/tool/memory/profile_handler.py | 10 +- reme/tool/memory/read_all_profiles.py | 12 +- reme/tool/memory/retrieve_memory.py | 16 +- reme/tool/memory/update_memory.py | 26 +- reme/tool/memory/update_memory_v2.py | 32 +- reme/tool/memory/update_profile.py | 14 +- 23 files changed, 390 insertions(+), 423 deletions(-) diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/default/personal_retriever.py index 7fc8a0c8..3ff50eaf 100644 --- a/reme/agent/memory/default/personal_retriever.py +++ b/reme/agent/memory/default/personal_retriever.py @@ -22,8 +22,10 @@ class PersonalRetriever(BaseMemoryAgent): 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) + all_profiles = await read_all_profiles_tool.call( + memory_target=self.memory_target, + service_context=self.service_context, + ) else: all_profiles = "" diff --git a/reme/agent/memory/default/personal_retriever.yaml b/reme/agent/memory/default/personal_retriever.yaml index da8eead2..47de7c6d 100644 --- a/reme/agent/memory/default/personal_retriever.yaml +++ b/reme/agent/memory/default/personal_retriever.yaml @@ -6,7 +6,7 @@ system_prompt: | ## User Question {context} - + ## Retrieval Strategy ### Phase 1 `retrieve_memory` - Purpose: Search for relevant memories using semantic similarity diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index 5885ed23..9f8b85de 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -1,4 +1,5 @@ """Personal memory summarizer agent for two-phase personal memory processing.""" + from loguru import logger from ..base_memory_agent import BaseMemoryAgent diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index ada57fd4..014da6e5 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -1,15 +1,15 @@ system_prompt_s1_zh: | 你是一个记忆Agent,负责管理关于 {memory_target} 的 {memory_type} 类型记忆。 - + ## 最新对话 Format: round [] : {context} - + ## 任务 ### 步骤1 根据`最新对话`的内容,在 `add_draft_and_retrieve_similar_memory` 中创建记忆草稿 `memory_draft`。 工具会根据memory_draft的内容行向量检索,返回历史相似记忆,确保在第二步的时候更好的管理记忆库记忆。 - + ### 步骤2 使用`update_memory`更新向量库记忆。 通过`memory_ids_to_delete`删除历史记忆,`memories_to_add`添加新记忆,包括message_time和memory_content。 @@ -23,16 +23,16 @@ user_message_s1_zh: | system_prompt_s2_zh: | 你是一个Profile Agent,负责管理关于 {memory_target} 的 Profile。 - + ## 最新对话 Format: round [] : {context} - + ## 任务 ### 步骤1 根据`最新对话`的内容,在 `add_draft_and_read_all_profiles` 中创建记忆草稿 `profile_draft`。 工具会直接返回所有的Profile,确保在第二步的时候更好的管理Profile。 - + ### 步骤2 使用`update_profile`更新profile库。 通过`profile_ids_to_delete`删除历史Profile,`profiles_to_add`添加新Profile,包括message_time、profile_key和profile_value。 @@ -46,16 +46,16 @@ user_message_s2_zh: | system_prompt_s1: | You are a Memory Agent responsible for managing {memory_type} type memories about {memory_target}. - + ## Latest Conversation Format: round [] : {context} - + ## Task ### Step 1 Based on the content of `Latest Conversation`, create a memory draft `memory_draft` in `add_draft_and_retrieve_similar_memory`. The tool will perform vector retrieval based on the content of memory_draft and return historically similar memories to better manage the memory store in Step 2. - + ### Step 2 Use `update_memory` to update the vector store memories. Delete historical memories through `memory_ids_to_delete`, add new memories through `memories_to_add`, including message_time and memory_content. @@ -69,16 +69,16 @@ user_message_s1: | system_prompt_s2: | You are a Profile Agent responsible for managing the Profile about {memory_target}. - + ## Latest Conversation Format: round [] : {context} - + ## Task ### Step 1 Based on the content of `Latest Conversation`, create a profile draft `profile_draft` in `add_draft_and_read_all_profiles`. The tool will directly return all Profiles to better manage the Profile store in Step 2. - + ### Step 2 Use `update_profile` to update the profile store. Delete historical Profiles through `profile_ids_to_delete`, add new Profiles through `profiles_to_add`, including message_time, profile_key, and profile_value. diff --git a/reme/agent/memory/default/procedural_retriever.py b/reme/agent/memory/default/procedural_retriever.py index 63d4856b..090d66f0 100644 --- a/reme/agent/memory/default/procedural_retriever.py +++ b/reme/agent/memory/default/procedural_retriever.py @@ -1,6 +1,10 @@ +"""Procedural memory retriever agent implementation.""" + from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import MemoryType class ProceduralRetriever(BaseMemoryAgent): + """Agent responsible for retrieving procedural memories.""" + memory_type: MemoryType = MemoryType.PROCEDURAL diff --git a/reme/agent/memory/default/procedural_summarizer.py b/reme/agent/memory/default/procedural_summarizer.py index 0be6bd98..72876568 100644 --- a/reme/agent/memory/default/procedural_summarizer.py +++ b/reme/agent/memory/default/procedural_summarizer.py @@ -1,6 +1,10 @@ +"""Procedural memory summarizer agent implementation.""" + from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import MemoryType class ProceduralSummarizer(BaseMemoryAgent): + """Agent responsible for summarizing procedural memories.""" + memory_type: MemoryType = MemoryType.PROCEDURAL diff --git a/reme/agent/memory/default/tool_retriever.py b/reme/agent/memory/default/tool_retriever.py index 53e603fc..6a2d206b 100644 --- a/reme/agent/memory/default/tool_retriever.py +++ b/reme/agent/memory/default/tool_retriever.py @@ -1,6 +1,10 @@ +"""Tool memory retriever agent implementation.""" + from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import MemoryType class ToolRetriever(BaseMemoryAgent): + """Agent responsible for retrieving tool-related memories.""" + memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/agent/memory/default/tool_summarizer.py b/reme/agent/memory/default/tool_summarizer.py index 5064b6db..85222d4f 100644 --- a/reme/agent/memory/default/tool_summarizer.py +++ b/reme/agent/memory/default/tool_summarizer.py @@ -1,6 +1,10 @@ +"""Tool memory summarizer agent implementation.""" + from ..base_memory_agent import BaseMemoryAgent from ....core.enumeration import MemoryType class ToolSummarizer(BaseMemoryAgent): + """Agent responsible for summarizing tool-related memories.""" + memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/core/application.py b/reme/core/application.py index a03c3e73..f96b1b03 100644 --- a/reme/core/application.py +++ b/reme/core/application.py @@ -1,3 +1,5 @@ +"""High-level entry point for configuring and running ReMe services and flows.""" + import asyncio from .context import PromptHandler, ServiceContext @@ -11,21 +13,22 @@ from .vector_store import BaseVectorStore class Application: + """Application wrapper that wires together service context, flows, and runtimes.""" def __init__( - self, - *args, - llm_api_key: str | None = None, - llm_api_base: str | None = None, - embedding_api_key: str | None = None, - embedding_api_base: str | None = None, - enable_logo: bool = True, - parser: type[PydanticConfigParser] | None = None, - llm: dict | None = None, - embedding_model: dict | None = None, - vector_store: dict | None = None, - token_counter: dict | None = None, - **kwargs, + self, + *args, + llm_api_key: str | None = None, + llm_api_base: str | None = None, + embedding_api_key: str | None = None, + embedding_api_base: str | None = None, + enable_logo: bool = True, + parser: type[PydanticConfigParser] | None = None, + llm: dict | None = None, + embedding_model: dict | None = None, + vector_store: dict | None = None, + token_counter: dict | None = None, + **kwargs, ): # ServiceContext self.service_context = ServiceContext( @@ -94,10 +97,10 @@ class Application: stream_queue = asyncio.Queue() task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs)) async for chunk in execute_stream_task( - stream_queue=stream_queue, - task=task, - task_name=name, - as_bytes=False, + stream_queue=stream_queue, + task=task, + task_name=name, + as_bytes=False, ): yield chunk diff --git a/reme/core/schema/memory_node.py b/reme/core/schema/memory_node.py index 3cf096ac..e6fde012 100644 --- a/reme/core/schema/memory_node.py +++ b/reme/core/schema/memory_node.py @@ -149,12 +149,12 @@ class MemoryNode(BaseModel): ) def format( - self, - include_memory_id: bool = True, - include_when_to_use: bool = True, - include_content: bool = True, - include_message_time: bool = True, - ref_memory_id_key: str = "", + self, + include_memory_id: bool = True, + include_when_to_use: bool = True, + include_content: bool = True, + include_message_time: bool = True, + ref_memory_id_key: str = "", ) -> str: """Format memory node as string with configurable fields.""" line = "" diff --git a/reme/reme.py b/reme/reme.py index bca18f80..08ca92ca 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -1,25 +1,36 @@ """ReMe classes for simplified configuration and execution.""" -import asyncio import sys from pathlib import Path -from loguru import logger - -from .core import Application -from .agent.memory.default import ReMeSummarizer, PersonalSummarizer, PersonalRetriever, ReMeRetriever +from reme.agent.memory import BaseMemoryAgent +from .agent.memory.default import ( + ReMeSummarizer, + PersonalSummarizer, + PersonalRetriever, + ReMeRetriever, + ProceduralSummarizer, + ToolSummarizer, + ProceduralRetriever, + ToolRetriever, +) from .config import ReMeConfigParser -from .core.context import PromptHandler, ServiceContext -from .core.embedding import BaseEmbeddingModel +from .core import Application from .core.enumeration import MemoryType -from .core.flow import BaseFlow -from .core.llm import BaseLLM -from .core.schema import Response, Message, MemoryNode, VectorNode -from .core.token_counter import BaseTokenCounter -from .core.utils import execute_stream_task, get_now_time -from .core.vector_store import BaseVectorStore -from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, DelegateTask, ReadHistory, ReadUserProfile, \ - ProfileHandler +from .core.schema import Message +from .tool.memory import ( + RetrieveMemory, + DelegateTask, + ReadHistory, + ProfileHandler, + MemoryHandler, + AddDraftAndRetrieveSimilarMemory, + UpdateMemoryV2, + AddDraftAndReadAllProfiles, + UpdateProfile, + AddHistory, + ReadAllProfiles, +) class ReMe(Application): @@ -37,18 +48,10 @@ class ReMe(Application): embedding_model: dict | None = None, vector_store: dict | None = None, token_counter: dict | None = None, - personal_memory_target: list[str] | None = None, - procedural_memory_target: list[str] | None = None, - tool_memory_target: list[str] | None = None, - profile_path: str = "reme_profile", - main_summary_version: str = "default", - personal_summary_version: str = "default", - procedural_summary_version: str = "default", - tool_summary_version: str = "default", - main_retrieve_version: str = "default", - personal_retrieve_version: str = "default", - procedural_retrieve_version: str = "default", - tool_retrieve_version: str = "default", + personal_memory_target: list[str] | None = None, + procedural_memory_target: list[str] | None = None, + tool_memory_target: list[str] | None = None, + profile_dir: str = "reme_profile", **kwargs, ): super().__init__( @@ -80,300 +83,239 @@ class ReMe(Application): for name in tool_memory_target: assert name not in memory_target_type_mapping, f"Memory target name {name} is already used." memory_target_type_mapping[name] = MemoryType.TOOL + self.service_context.memory_target_type_mapping = memory_target_type_mapping - self.profile_path: str = profile_path - - @property - def memory_target_type_mapping(self) -> dict[str, MemoryType]: - mapping = {} - if self.service_context.personal_memory_target: - for name in self.service_context.personal_memory_target: - assert name not in mapping, f"Memory target name {name} is already used." - mapping[name] = MemoryType.PERSONAL - - if self.service_context.procedural_memory_target: - for name in self.service_context.procedural_memory_target: - assert name not in mapping, f"Memory target name {name} is already used." - mapping[name] = MemoryType.PROCEDURAL - - if self.service_context.tool_memory_target: - for name in self.service_context.tool_memory_target: - assert name not in mapping, f"Memory target name {name} is already used." - mapping[name] = MemoryType.TOOL - return mapping + self.profile_dir: str = profile_dir def add_meta_memory(self, memory_type: str | MemoryType, memory_target: str): - memory_type = MemoryType(memory_type) - if memory_type is MemoryType.PERSONAL: - personal_memory_target = self.service_context.personal_memory_target - if memory_target not in personal_memory_target: - personal_memory_target.append(memory_target) - else: - logger.warning(f"Memory target {memory_target} is already added.") - - elif memory_type is MemoryType.PROCEDURAL: - procedural_memory_target = self.service_context.procedural_memory_target - if memory_target not in procedural_memory_target: - procedural_memory_target.append(memory_target) - else: - logger.warning(f"Memory target {memory_target} is already added.") - - elif memory_type is MemoryType.TOOL: - tool_memory_target = self.service_context.tool_memory_target - if memory_target not in tool_memory_target: - tool_memory_target.append(memory_target) - else: - logger.warning(f"Memory target {memory_target} is already added.") - - + """Register or validate a memory target with the given memory type.""" + if memory_target in self.service_context.memory_target_type_mapping: + assert self.service_context.memory_target_type_mapping[memory_target] is memory_type + else: + self.service_context.memory_target_type_mapping[memory_target] = MemoryType(memory_type) async def summary_memory( self, messages: list[Message | dict], description: str = "", - user_name: str = "", - task_name: str = "", - tool_name: str = "", + user_name: str | list[str] = "", + task_name: str | list[str] = "", + tool_name: str | list[str] = "", enable_thinking_params: bool = False, version: str = "default", + retrieve_top_k: int = 20, return_dict: bool = False, **kwargs, ) -> str | dict: - """Summarize messages and store them in memory for the specified user(s).""" - if user_name: - if isinstance(user_name, str): - for message in messages: - if isinstance(message, dict) and not message.get("name"): - message["name"] = user_name - elif isinstance(message, Message) and not message.name: - message.name = user_name - user_name = [user_name] + """Summarize personal, procedural and tool memories for the given context.""" + format_messages: list[Message] = [] + for message in messages: + if isinstance(message, dict): + assert message.get("time_created"), "message must have time_created field." + message = Message(**message) + format_messages.append(message) - if not meta_memories: - meta_memories = [ - { - "memory_type": "personal", - "memory_target": name, - } - for name in user_name - ] - - if version == "default": - reme_summarizer = ReMeSummarizer( - meta_memories=meta_memories, - tools=[ - DelegateTask( - memory_agents=[ - PersonalSummarizer( - tools=[ - RetrieveMemory(enable_thinking_params=enable_thinking_params), - AddMemory(enable_thinking_params=enable_thinking_params), - UpdateUserProfile(enable_thinking_params=enable_thinking_params), - ], - ), - ], - ), - ], - ) - - else: - raise NotImplementedError - - result = await reme_summarizer.call( - messages=messages, - description=description, - service_context=self.service_context, - **kwargs, + personal_summarizer: BaseMemoryAgent + if version: + personal_summarizer = PersonalSummarizer( + tools=[ + AddDraftAndRetrieveSimilarMemory( + enable_thinking_params=enable_thinking_params, + top_k=retrieve_top_k, + ), + UpdateMemoryV2(enable_thinking_params=enable_thinking_params), + AddDraftAndReadAllProfiles( + enable_thinking_params=enable_thinking_params, + profile_dir=self.profile_dir, + ), + UpdateProfile( + enable_thinking_params=enable_thinking_params, + profile_dir=self.profile_dir, + ), + ], ) - - if return_dict: - return result - else: - return result["answer"] - else: raise NotImplementedError + procedural_summarizer: BaseMemoryAgent + if version == "default": + procedural_summarizer = ProceduralSummarizer(tools=[]) + else: + raise NotImplementedError + + tool_summarizer: BaseMemoryAgent + if version == "default": + tool_summarizer = ToolSummarizer(tools=[]) + else: + raise NotImplementedError + + memory_agents = [] + if user_name: + if isinstance(user_name, str): + for message in format_messages: + message.name = user_name + self.add_meta_memory(MemoryType.PERSONAL, user_name) + elif isinstance(user_name, list): + for name in user_name: + self.add_meta_memory(MemoryType.PERSONAL, name) + else: + raise RuntimeError("user_name must be str or list[str]") + memory_agents.append(personal_summarizer) + + if task_name: + if isinstance(task_name, str): + self.add_meta_memory(MemoryType.PROCEDURAL, task_name) + elif isinstance(task_name, list): + for name in task_name: + self.add_meta_memory(MemoryType.PROCEDURAL, name) + else: + raise RuntimeError("task_name must be str or list[str]") + memory_agents.append(procedural_summarizer) + + if tool_name: + if isinstance(tool_name, str): + self.add_meta_memory(MemoryType.TOOL, tool_name) + elif isinstance(tool_name, list): + for name in tool_name: + self.add_meta_memory(MemoryType.TOOL, name) + else: + raise RuntimeError("tool_name must be str or list[str]") + memory_agents.append(tool_summarizer) + + if not memory_agents: + memory_agents = [personal_summarizer, procedural_summarizer, tool_summarizer] + + reme_summarizer: BaseMemoryAgent + if version == "default": + reme_summarizer = ReMeSummarizer(tools=[AddHistory(), DelegateTask(memory_agents=memory_agents)]) + else: + raise NotImplementedError + + result = await reme_summarizer.call( + messages=messages, + description=description, + service_context=self.service_context, + **kwargs, + ) + + if return_dict: + return result + else: + return result["answer"] + async def retrieve_memory( self, query: str = "", - top_k: int = 20, description: str = "", messages: list[dict] | None = None, user_name: str | list[str] = "", + task_name: str | list[str] = "", + tool_name: str | list[str] = "", enable_thinking_params: bool = False, - meta_memories: list[dict] = None, version: str = "default", + retrieve_top_k: int = 20, + enable_memory_target: bool = True, return_dict: bool = False, **kwargs, ) -> str | dict: - """Retrieve relevant memories for the specified user(s) based on query or messages.""" - if user_name: - if isinstance(user_name, str): - if messages: - for message in messages: - if isinstance(message, dict) and not message.get("name"): - message["name"] = user_name - elif isinstance(message, Message) and not message.name: - message.name = user_name - user_name = [user_name] + """Retrieve relevant personal, procedural and tool memories for a query.""" - if not meta_memories: - meta_memories = [ - { - "memory_type": "personal", - "memory_target": name, - } - for name in user_name - ] - - if version == "default": - reme_retriever = ReMeRetriever( - meta_memories=meta_memories, - tools=[ - DelegateTask( - memory_agents=[ - PersonalRetriever( - tools=[ - RetrieveMemory(enable_thinking_params=enable_thinking_params, top_k=top_k), - ReadHistory(enable_thinking_params=enable_thinking_params), - ], - ), - ], - ), - ], - ) - - else: - raise NotImplementedError - - result = await reme_retriever.call( - query=query, - messages=messages, - description=description, - service_context=self.service_context, - **kwargs, + personal_retriever: BaseMemoryAgent + if version: + personal_retriever = PersonalRetriever( + tools=[ + ReadAllProfiles( + enable_thinking_params=enable_thinking_params, + profile_dir=self.profile_dir, + ), + RetrieveMemory( + enable_thinking_params=enable_thinking_params, + top_k=retrieve_top_k, + enable_memory_target=enable_memory_target, + ), + ReadHistory(enable_thinking_params=enable_thinking_params), + ], ) - - if return_dict: - return result - else: - return result["answer"] - else: raise NotImplementedError - async def add_memory( - self, - memory_content: str, - user_name: str, - memory_type: str | MemoryType | None = None, - memory_target: str = "", - when_to_use: str = "", - ref_memory_id: str = "", - author: str = "", - score: float = 0, - conversation_time: str = "", - **kwargs, - ) -> MemoryNode: - """Add a new memory to the vector store for the specified user.""" + procedural_retriever: BaseMemoryAgent + if version == "default": + procedural_retriever = ProceduralRetriever(tools=[]) + else: + raise NotImplementedError + tool_retriever: BaseMemoryAgent + if version == "default": + tool_retriever = ToolRetriever(tools=[]) + else: + raise NotImplementedError + + memory_agents = [] if user_name: - memory_type = MemoryType.PERSONAL - memory_target = user_name + if isinstance(user_name, str): + self.add_meta_memory(MemoryType.PERSONAL, user_name) + elif isinstance(user_name, list): + for name in user_name: + self.add_meta_memory(MemoryType.PERSONAL, name) + else: + raise RuntimeError("user_name must be str or list[str]") + memory_agents.append(personal_retriever) + + if task_name: + if isinstance(task_name, str): + self.add_meta_memory(MemoryType.PROCEDURAL, task_name) + elif isinstance(task_name, list): + for name in task_name: + self.add_meta_memory(MemoryType.PROCEDURAL, name) + else: + raise RuntimeError("task_name must be str or list[str]") + memory_agents.append(procedural_retriever) + + if tool_name: + if isinstance(tool_name, str): + self.add_meta_memory(MemoryType.TOOL, tool_name) + elif isinstance(tool_name, list): + for name in tool_name: + self.add_meta_memory(MemoryType.TOOL, name) + else: + raise RuntimeError("tool_name must be str or list[str]") + memory_agents.append(tool_retriever) + + if not memory_agents: + memory_agents = [personal_retriever, procedural_retriever, tool_retriever] + + reme_retriever: BaseMemoryAgent + if version == "default": + reme_retriever = ReMeRetriever(tools=[DelegateTask(memory_agents=memory_agents)]) else: - memory_type = MemoryType(memory_type) - assert memory_target, "memory_target is required" + raise NotImplementedError - metadata = kwargs.copy() - if conversation_time: - metadata["conversation_time"] = conversation_time - - memory_node = MemoryNode( - memory_type=memory_type, - memory_target=memory_target, - when_to_use=when_to_use, - content=memory_content, - ref_memory_id=ref_memory_id, - author=author, - score=score, - metadata=metadata, + result = await reme_retriever.call( + query=query, + messages=messages, + description=description, + service_context=self.service_context, + **kwargs, ) - vector_node = memory_node.to_vector_node() - await self.vector_store.delete([vector_node.vector_id]) - await self.vector_store.insert([vector_node]) - return memory_node - - async def update_memory( - self, - memory_id: str, - memory_content: str, - user_name: str, - memory_type: str | MemoryType | None = None, - memory_target: str = "", - when_to_use: str = "", - ref_memory_id: str = "", - author: str = "", - score: float = 0, - conversation_time: str = "", - **kwargs, - ) -> MemoryNode: - """Update an existing memory in the vector store by its ID.""" - - if user_name: - memory_type = MemoryType.PERSONAL - memory_target = user_name + if return_dict: + return result else: - memory_type = MemoryType(memory_type) - assert memory_target, "memory_target is required" + return result["answer"] - metadata = kwargs.copy() - if conversation_time: - metadata["conversation_time"] = conversation_time + @property + def profile_path(self) -> Path: + """Get the path to the profile directory.""" + return Path(self.profile_dir) / self.vector_store.collection_name - memory_node = MemoryNode( - memory_type=memory_type, - memory_target=memory_target, - when_to_use=when_to_use, - content=memory_content, - ref_memory_id=ref_memory_id, - author=author, - score=score, - metadata=metadata, - ) - vector_node = memory_node.to_vector_node() - await self.vector_store.delete(list(set([memory_id, vector_node.vector_id]))) - await self.vector_store.insert([vector_node]) - - return memory_node - - async def delete_memory(self, memory_id: str | list[str]): - """Delete one or more memories from the vector store by their IDs.""" - vector_ids = [memory_id] if isinstance(memory_id, str) else memory_id - await self.vector_store.delete(list(set(vector_ids))) - - async def delete_all_memories(self): - """Delete all memories from the vector store.""" - await self.vector_store.delete_all() - - async def get_memory(self, memory_id: str | list[str]) -> MemoryNode | list[MemoryNode]: - """Retrieve one or more memories from the vector store by their IDs.""" - vector_ids = [memory_id] if isinstance(memory_id, str) else memory_id - vector_nodes = await self.vector_store.get(vector_ids) - if isinstance(vector_nodes, VectorNode): - return vector_nodes.to_memory_node() - else: - return [node.to_memory_node() for node in vector_nodes] - - async def get_all_memories(self) -> list[MemoryNode]: - """Retrieve all memories from the vector store.""" - return [node.to_memory_node() for node in await self.vector_store.list()] + def get_memory_handler(self, memory_target: str) -> MemoryHandler: + """Get the memory handler for the specified memory target.""" + return MemoryHandler(memory_target=memory_target, service_context=self.service_context) def get_profile_handler(self, user_name: str) -> ProfileHandler: """Get the profile handler for the specified user.""" - profile_path = Path(self.profile_path) / self.vector_store.collection_name - return ProfileHandler(memory_target=user_name, profile_path=profile_path) + return ProfileHandler(memory_target=user_name, profile_path=self.profile_path) async def context_offload(self): """working memory summary""" diff --git a/reme/tool/memory/__init__.py b/reme/tool/memory/__init__.py index f0f61870..cab968dd 100644 --- a/reme/tool/memory/__init__.py +++ b/reme/tool/memory/__init__.py @@ -39,4 +39,4 @@ __all__ = [ for name in __all__: tool_class = globals()[name] - R.op.register()(tool_class) \ No newline at end of file + R.op.register()(tool_class) diff --git a/reme/tool/memory/add_draft_and_read_all_profiles.py b/reme/tool/memory/add_draft_and_read_all_profiles.py index c11fc82d..da4e559c 100644 --- a/reme/tool/memory/add_draft_and_read_all_profiles.py +++ b/reme/tool/memory/add_draft_and_read_all_profiles.py @@ -1,5 +1,4 @@ """Add draft profile and read all profiles from local storage""" -from pathlib import Path from loguru import logger @@ -11,9 +10,8 @@ from ...core.schema import ToolCall class AddDraftAndReadAllProfiles(BaseMemoryTool): """Tool to add draft profile and read all profiles""" - def __init__(self, profile_path: str, enable_memory_target: bool = False, **kwargs): + def __init__(self, enable_memory_target: bool = False, **kwargs): super().__init__(**kwargs) - self.profile_path: str = profile_path self.enable_memory_target: bool = enable_memory_target def _build_query_parameters(self) -> dict: @@ -86,10 +84,7 @@ class AddDraftAndReadAllProfiles(BaseMemoryTool): continue targets_processed.add(target) - profile_handler = ProfileHandler( - profile_path=Path(self.profile_path) / self.vector_store.collection_name, - memory_target=target, - ) + profile_handler = ProfileHandler(profile_path=self.profile_path, memory_target=target) profiles_str = profile_handler.read_all(add_profile_id=True) if profiles_str: diff --git a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py index 8be42636..cb537fc4 100644 --- a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py +++ b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py @@ -80,11 +80,13 @@ class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): if target not in queries_by_target: queries_by_target[target] = [] - queries_by_target[target].append({ - "query": item["memory_draft"], - "limit": self.top_k, - "filters": {}, - }) + queries_by_target[target].append( + { + "query": item["memory_draft"], + "limit": self.top_k, + "filters": {}, + }, + ) # Execute batch searches for each target memory_nodes: list[MemoryNode] = [] diff --git a/reme/tool/memory/add_memory.py b/reme/tool/memory/add_memory.py index 1293634f..833c2b54 100644 --- a/reme/tool/memory/add_memory.py +++ b/reme/tool/memory/add_memory.py @@ -11,10 +11,10 @@ class AddMemory(BaseMemoryTool): """Tool to add memories to vector store""" def __init__( - self, - enable_memory_target: bool = False, - enable_when_to_use: bool = False, - **kwargs, + self, + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, ): super().__init__(**kwargs) self.enable_memory_target: bool = enable_memory_target @@ -112,14 +112,16 @@ class AddMemory(BaseMemoryTool): except Exception: logger.warning(f"Invalid message time format: {message_time}") - memory_dicts.append({ - "content": memory_content, - "when_to_use": when_to_use, - "message_time": message_time, - "ref_memory_id": self.history_id, - "author": self.author, - "metadata": metadata, - }) + memory_dicts.append( + { + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "ref_memory_id": self.history_id, + "author": self.author, + "metadata": metadata, + }, + ) if memory_dicts: handler = MemoryHandler(target, self.service_context) diff --git a/reme/tool/memory/base_memory_tool.py b/reme/tool/memory/base_memory_tool.py index 3d46e272..d3cb4541 100644 --- a/reme/tool/memory/base_memory_tool.py +++ b/reme/tool/memory/base_memory_tool.py @@ -1,6 +1,7 @@ """Base class for memory tool""" from abc import ABCMeta +from pathlib import Path from ...core.enumeration import MemoryType from ...core.op import BaseTool @@ -14,11 +15,13 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): self, enable_multiple: bool = True, enable_thinking_params: bool = False, + profile_dir: str = "", **kwargs, ): super().__init__(**kwargs) self.enable_multiple: bool = enable_multiple self.enable_thinking_params: bool = enable_thinking_params + self.profile_dir: str = profile_dir def _build_tool_call(self) -> ToolCall: """Build and return the tool call schema""" @@ -98,3 +101,8 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): def memory_target_type_mapping(self) -> dict[str, MemoryType]: """Get the memory target type mapping from context.""" return self.context.memory_target_type_mapping + + @property + def profile_path(self) -> Path: + """Get the path to the profile directory for the current collection.""" + return Path(self.profile_dir) / self.vector_store.collection_name diff --git a/reme/tool/memory/memory_handler.py b/reme/tool/memory/memory_handler.py index 78151325..5b896449 100644 --- a/reme/tool/memory/memory_handler.py +++ b/reme/tool/memory/memory_handler.py @@ -1,3 +1,5 @@ +"""Memory handler""" + from ...core.context import ServiceContext from ...core.enumeration import MemoryType from ...core.schema import MemoryNode @@ -45,14 +47,14 @@ class MemoryHandler: return memory_nodes async def add( - self, - content: str, - when_to_use: str = "", - message_time: str = "", - ref_memory_id: str = "", - author: str = "", - score: float = 0.0, - **kwargs, + self, + content: str, + when_to_use: str = "", + message_time: str = "", + ref_memory_id: str = "", + author: str = "", + score: float = 0.0, + **kwargs, ) -> MemoryNode: """Add a single memory node and return its memory_id.""" memory_dict = { @@ -123,15 +125,15 @@ class MemoryHandler: return updated_nodes async def update( - self, - memory_id: str, - content: str | None = None, - when_to_use: str | None = None, - message_time: str | None = None, - ref_memory_id: str | None = None, - author: str | None = None, - score: float | None = None, - **kwargs, + self, + memory_id: str, + content: str | None = None, + when_to_use: str | None = None, + message_time: str | None = None, + ref_memory_id: str | None = None, + author: str | None = None, + score: float | None = None, + **kwargs, ) -> MemoryNode: """Update a memory node's content, when_to_use, or other fields.""" update_dict: dict = {"memory_id": memory_id} @@ -154,11 +156,11 @@ class MemoryHandler: return memory_nodes[0] async def search( - self, - query: str | list[str], - limit: int = 5, - filters: dict | None = None, - **kwargs, + self, + query: str | list[str], + limit: int = 5, + filters: dict | None = None, + **kwargs, ) -> list[MemoryNode]: """Search for similar memory nodes based on query text.""" filters = filters or {} @@ -195,11 +197,11 @@ class MemoryHandler: return list(seen_ids.values()) async def list( - self, - filters: dict | None = None, - limit: int | None = None, - sort_key: str | None = None, - reverse: bool = True, + self, + filters: dict | None = None, + limit: int | None = None, + sort_key: str | None = None, + reverse: bool = True, ) -> list[MemoryNode]: """List memory nodes with optional filtering and sorting.""" filters = filters or {} diff --git a/reme/tool/memory/profile_handler.py b/reme/tool/memory/profile_handler.py index 4a950171..0f51d014 100644 --- a/reme/tool/memory/profile_handler.py +++ b/reme/tool/memory/profile_handler.py @@ -1,4 +1,5 @@ """Profile Handler for managing user profiles in local memory""" + from pathlib import Path from loguru import logger @@ -36,7 +37,9 @@ class ProfileHandler: removed_count = len(sorted_nodes) - self.max_capacity nodes = sorted_nodes[removed_count:] logger.info( - f"Capacity limit reached: removed {removed_count} oldest profiles (kept {len(nodes)}/{self.max_capacity})") + f"Capacity limit reached: removed {removed_count} oldest profiles " + f"(kept {len(nodes)}/{self.max_capacity})", + ) nodes_data = [node.model_dump(exclude_none=True) for node in nodes] self.cache_handler.save(self.cache_key, nodes_data) @@ -191,9 +194,6 @@ class ProfileHandler: def read_all(self, add_profile_id: bool = False, add_history_id: bool = False) -> str: """Read all profiles and return formatted string""" nodes = self.get_all() - formatted_profiles = [ - self._format_node(node, add_profile_id, add_history_id) - for node in nodes - ] + formatted_profiles = [self._format_node(node, add_profile_id, add_history_id) for node in nodes] logger.info(f"Read {len(formatted_profiles)} profiles from {self.cache_key}") return "\n".join(formatted_profiles).strip() diff --git a/reme/tool/memory/read_all_profiles.py b/reme/tool/memory/read_all_profiles.py index 3d492ef7..d18be220 100644 --- a/reme/tool/memory/read_all_profiles.py +++ b/reme/tool/memory/read_all_profiles.py @@ -1,5 +1,4 @@ """Read user profile tool""" -from pathlib import Path from loguru import logger @@ -11,10 +10,9 @@ from ...core.schema import ToolCall class ReadAllProfiles(BaseMemoryTool): """Tool to read all user profiles""" - def __init__(self, profile_path: str, **kwargs): + def __init__(self, **kwargs): kwargs["enable_multiple"] = False super().__init__(**kwargs) - self.profile_path: str = profile_path def _build_tool_call(self) -> ToolCall: """Build and return the tool call schema""" @@ -30,16 +28,12 @@ class ReadAllProfiles(BaseMemoryTool): ) async def execute(self): - profile_handler = ProfileHandler( - profile_path=Path(self.profile_path) / self.vector_store.collection_name, - memory_target=self.memory_target, - ) - + profile_handler = ProfileHandler(profile_path=self.profile_path, memory_target=self.memory_target) profiles_str = profile_handler.read_all(add_profile_id=True) if not profiles_str: output = "No profiles found." logger.info(output) return output - logger.info(f"Successfully read profiles") + logger.info("Successfully read profiles") return profiles_str diff --git a/reme/tool/memory/retrieve_memory.py b/reme/tool/memory/retrieve_memory.py index 48fa47dc..db542643 100644 --- a/reme/tool/memory/retrieve_memory.py +++ b/reme/tool/memory/retrieve_memory.py @@ -31,8 +31,8 @@ class RetrieveMemory(BaseMemoryTool): properties["time_filter"] = { "type": "string", "description": "Optional time filter to narrow down search results by date. " - "Format: single date '20200101' for exact date match, " - "or date range '20200101,20200102' for inclusive range filtering.", + "Format: single date '20200101' for exact date match, " + "or date range '20200101,20200102' for inclusive range filtering.", } if self.enable_memory_target: @@ -99,11 +99,13 @@ class RetrieveMemory(BaseMemoryTool): else: filters = {"time_int": [int(time_filter), int(time_filter)]} - queries_by_target[target].append({ - "query": item["query"], - "limit": self.top_k, - "filters": filters, - }) + queries_by_target[target].append( + { + "query": item["query"], + "limit": self.top_k, + "filters": filters, + }, + ) # Execute batch searches for each target memory_nodes: list[MemoryNode] = [] diff --git a/reme/tool/memory/update_memory.py b/reme/tool/memory/update_memory.py index 1370e180..ba4f8d5b 100644 --- a/reme/tool/memory/update_memory.py +++ b/reme/tool/memory/update_memory.py @@ -11,10 +11,10 @@ class UpdateMemory(BaseMemoryTool): """Tool to update memories in vector store""" def __init__( - self, - enable_memory_target: bool = False, - enable_when_to_use: bool = False, - **kwargs, + self, + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, ): super().__init__(**kwargs) self.enable_memory_target: bool = enable_memory_target @@ -116,14 +116,16 @@ class UpdateMemory(BaseMemoryTool): except Exception: logger.warning(f"Invalid message time format: {message_time}") - update_dicts.append({ - "memory_id": mem.get("memory_id", ""), - "content": memory_content, - "when_to_use": when_to_use, - "message_time": message_time, - "author": self.author, - "metadata": metadata, - }) + update_dicts.append( + { + "memory_id": mem.get("memory_id", ""), + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "author": self.author, + "metadata": metadata, + }, + ) if update_dicts: handler = MemoryHandler(target, self.service_context) diff --git a/reme/tool/memory/update_memory_v2.py b/reme/tool/memory/update_memory_v2.py index f9167e69..b855c15b 100644 --- a/reme/tool/memory/update_memory_v2.py +++ b/reme/tool/memory/update_memory_v2.py @@ -11,11 +11,11 @@ class UpdateMemoryV2(BaseMemoryTool): """Tool to update memories in vector store by deleting and adding memory entries""" def __init__( - self, - name="update_memory", - enable_memory_target: bool = False, - enable_when_to_use: bool = False, - **kwargs, + self, + name="update_memory", + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, ): kwargs["enable_multiple"] = True super().__init__(name=name, **kwargs) @@ -68,7 +68,7 @@ class UpdateMemoryV2(BaseMemoryTool): "type": "array", "description": "List of memory IDs to delete", "items": { - "type": "string" + "type": "string", }, }, "memories_to_add": { @@ -85,7 +85,7 @@ class UpdateMemoryV2(BaseMemoryTool): async def execute(self): # Get parameters memory_ids_to_delete = self.context.get("memory_ids_to_delete", []) - memory_ids_to_delete = sorted(set([mid for mid in memory_ids_to_delete if mid])) + memory_ids_to_delete = sorted({mid for mid in memory_ids_to_delete if mid}) memories_to_add = self.context.get("memories_to_add", []) if not memory_ids_to_delete and not memories_to_add: @@ -126,14 +126,16 @@ class UpdateMemoryV2(BaseMemoryTool): except Exception: logger.warning(f"Invalid message time format: {message_time}") - add_dicts.append({ - "content": memory_content, - "when_to_use": when_to_use, - "message_time": message_time, - "ref_memory_id": self.history_id, - "author": self.author, - "metadata": metadata, - }) + add_dicts.append( + { + "content": memory_content, + "when_to_use": when_to_use, + "message_time": message_time, + "ref_memory_id": self.history_id, + "author": self.author, + "metadata": metadata, + }, + ) if add_dicts: handler = MemoryHandler(target, self.service_context) diff --git a/reme/tool/memory/update_profile.py b/reme/tool/memory/update_profile.py index f854f3ab..5ab7c962 100644 --- a/reme/tool/memory/update_profile.py +++ b/reme/tool/memory/update_profile.py @@ -1,5 +1,4 @@ """Update user profile tool""" -from pathlib import Path from loguru import logger @@ -11,12 +10,10 @@ from ...core.schema import ToolCall class UpdateProfile(BaseMemoryTool): """Tool to update user profile by adding or removing profile entries""" - def __init__(self, profile_path: str, **kwargs): + def __init__(self, **kwargs): kwargs["enable_multiple"] = True super().__init__(**kwargs) - self.profile_path: str = profile_path - def _build_multiple_tool_call(self) -> ToolCall: """Build and return the multiple tool call schema""" return ToolCall( @@ -29,7 +26,7 @@ class UpdateProfile(BaseMemoryTool): "type": "array", "description": "List of profile IDs to delete", "items": { - "type": "string" + "type": "string", }, }, "profiles_to_add": { @@ -61,14 +58,11 @@ class UpdateProfile(BaseMemoryTool): ) async def execute(self): - profile_handler = ProfileHandler( - profile_path=Path(self.profile_path) / self.vector_store.collection_name, - memory_target=self.memory_target, - ) + profile_handler = ProfileHandler(profile_path=self.profile_path, memory_target=self.memory_target) # Get parameters profile_ids_to_delete = self.context.get("profile_ids_to_delete", []) - profile_ids_to_delete = sorted(set([pid for pid in profile_ids_to_delete if pid])) + profile_ids_to_delete = sorted({pid for pid in profile_ids_to_delete if pid}) profiles_to_add = self.context.get("profiles_to_add", []) if not profile_ids_to_delete and not profiles_to_add: From 3989a3c1c4fb7320568cb245edc98d31e99e1dc8 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 20:38:34 +0800 Subject: [PATCH 09/17] refactor(memory): update memory target mappings and profile handling --- .gitignore | 2 +- benchmark/halumem/eval_reme.py | 6 +++--- reme/agent/memory/base_memory_agent.py | 2 +- .../memory/default/personal_summarizer.py | 6 ------ reme/agent/memory/default/reme_summarizer.py | 1 + reme/tool/memory/base_memory_tool.py | 2 +- reme/tool/memory/delegate_task.py | 18 +++++++++--------- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index efc9bea1..7ee1a323 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ test_compact_storage/* test_working_memory/* *.code-workspace local_vector_store/* -reme_local_memory/* +reme_profile/* chroma_vector_store/* bench_results/* meta_memory/* diff --git a/benchmark/halumem/eval_reme.py b/benchmark/halumem/eval_reme.py index d4bcf615..9b3ab7e7 100644 --- a/benchmark/halumem/eval_reme.py +++ b/benchmark/halumem/eval_reme.py @@ -26,7 +26,6 @@ from typing import Any from loguru import logger -from reme.core.schema import MemoryNode, Message from reme.reme import ReMe @@ -328,7 +327,7 @@ class MemoryProcessor: # Retrieve memories from ReMe using new API result = await self.reme.retrieve_memory( query=query, - top_k=top_k, + retrieve_top_k=top_k, user_name=user_id, version="default", return_dict=True, @@ -637,7 +636,8 @@ class HaluMemEvaluator: for user_data in all_users ] if all_user_names: - await self.reme.delete_all_profiles(all_user_names) + for user_name in all_user_names: + self.reme.get_profile_handler(user_name).delete_all() logger.info(f"Deleted all profiles for {len(all_user_names)} users") # Clear existing data diff --git a/reme/agent/memory/base_memory_agent.py b/reme/agent/memory/base_memory_agent.py index bdb824f4..0e945c02 100644 --- a/reme/agent/memory/base_memory_agent.py +++ b/reme/agent/memory/base_memory_agent.py @@ -52,7 +52,7 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): @property def memory_target_type_mapping(self) -> dict[str, MemoryType]: """Get the memory target type mapping from context.""" - return self.context.memory_target_type_mapping + return self.context.service_context.memory_target_type_mapping @property def meta_memory_info(self) -> str: diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index 9f8b85de..11b26d81 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -107,15 +107,9 @@ class PersonalSummarizer(BaseMemoryAgent): if tool.memory_nodes: memory_nodes.extend(tool.memory_nodes) - profile_nodes = [] - for tool in tools: - if tool.profile_nodes: - profile_nodes.extend(tool.profile_nodes) - return { "answer": memory_nodes, "success": success, "messages": messages, "tools": tools, - "profile_nodes": profile_nodes, } diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index 86b49f2c..5473e71e 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -16,6 +16,7 @@ class ReMeSummarizer(BaseMemoryAgent): await add_history_tool.call( messages=self.messages, description=self.description, + author=self.author, service_context=self.service_context, ) self.context.history_node = add_history_tool.context.history_node diff --git a/reme/tool/memory/base_memory_tool.py b/reme/tool/memory/base_memory_tool.py index d3cb4541..3431b164 100644 --- a/reme/tool/memory/base_memory_tool.py +++ b/reme/tool/memory/base_memory_tool.py @@ -100,7 +100,7 @@ class BaseMemoryTool(BaseTool, metaclass=ABCMeta): @property def memory_target_type_mapping(self) -> dict[str, MemoryType]: """Get the memory target type mapping from context.""" - return self.context.memory_target_type_mapping + return self.context.service_context.memory_target_type_mapping @property def profile_path(self) -> Path: diff --git a/reme/tool/memory/delegate_task.py b/reme/tool/memory/delegate_task.py index 8d9fecab..2914d4d5 100644 --- a/reme/tool/memory/delegate_task.py +++ b/reme/tool/memory/delegate_task.py @@ -31,28 +31,28 @@ class DelegateTask(BaseMemoryTool): "parameters": { "type": "object", "properties": { - "tasks": { + "memory_target_tasks": { "type": "array", - "description": "tasks to delegate to specific agents, each task is a memory_target", + "description": "List of memory_target tasks to delegate to specific memory agents", "items": { "type": "string", - "description": "memory_target to delegate to specific agents", + "description": "A memory_target identifier to delegate to the corresponding agent", }, }, }, - "required": ["tasks"], + "required": ["memory_target_tasks"], }, }, ) async def execute(self): - # Deduplicate and validate tasks - tasks = self.context.get("tasks", []) - tasks = sorted(set(tasks)) + # Deduplicate and validate memory_target_tasks + memory_target_tasks = self.context.get("memory_target_tasks", []) + memory_target_tasks = sorted(set(memory_target_tasks)) - # Submit tasks to agents + # Submit memory_target_tasks to agents agent_list: list[BaseMemoryAgent] = [] - for i, memory_target in enumerate(tasks): + for i, memory_target in enumerate(memory_target_tasks): memory_type = self.memory_target_type_mapping[memory_target] agent = self.memory_agent_dict[memory_type].copy() agent_list.append(agent) From 3684eb25eb2091e62a7770df173bf141561fc616 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Wed, 28 Jan 2026 20:47:04 +0800 Subject: [PATCH 10/17] feat(memory): update profile management with time-based filtering --- reme/reme.py | 4 ++-- .../tool/memory/add_draft_and_read_all_profiles.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/reme/reme.py b/reme/reme.py index 08ca92ca..30d97bf0 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -214,7 +214,7 @@ class ReMe(Application): enable_thinking_params: bool = False, version: str = "default", retrieve_top_k: int = 20, - enable_memory_target: bool = True, + enable_time_filter: bool = True, return_dict: bool = False, **kwargs, ) -> str | dict: @@ -231,7 +231,7 @@ class ReMe(Application): RetrieveMemory( enable_thinking_params=enable_thinking_params, top_k=retrieve_top_k, - enable_memory_target=enable_memory_target, + enable_time_filter=enable_time_filter, ), ReadHistory(enable_thinking_params=enable_thinking_params), ], diff --git a/reme/tool/memory/add_draft_and_read_all_profiles.py b/reme/tool/memory/add_draft_and_read_all_profiles.py index da4e559c..0f9ba241 100644 --- a/reme/tool/memory/add_draft_and_read_all_profiles.py +++ b/reme/tool/memory/add_draft_and_read_all_profiles.py @@ -17,12 +17,20 @@ class AddDraftAndReadAllProfiles(BaseMemoryTool): def _build_query_parameters(self) -> dict: """Build the query parameters schema""" properties = { - "profile_draft": { + "message_time": { "type": "string", - "description": "profile_draft", + "description": "Message time, e.g. '2020-01-01 00:00:00'", + }, + "profile_key": { + "type": "string", + "description": "Profile key or category, e.g. 'name'", + }, + "profile_value": { + "type": "string", + "description": "Profile value or content, e.g. 'John Smith'", }, } - required = ["profile_draft"] + required = ["message_time", "profile_key", "profile_value"] if self.enable_memory_target: properties["memory_target"] = { From f841ccc4b956ad77269403cae6fc7399fb535f17 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 00:12:03 +0800 Subject: [PATCH 11/17] feat(memory): enhance memory management with improved tool parameters and agent coordination --- reme/agent/memory/base_memory_agent.py | 9 +- .../memory/default/personal_retriever.yaml | 87 ++++++++++++------- .../memory/default/personal_summarizer.py | 4 +- .../memory/default/personal_summarizer.yaml | 82 ++++------------- reme/agent/memory/default/reme_retriever.yaml | 14 ++- reme/agent/memory/default/reme_summarizer.py | 2 +- .../agent/memory/default/reme_summarizer.yaml | 14 ++- reme/reme.py | 4 +- .../add_draft_and_retrieve_similar_memory.py | 32 +++++-- reme/tool/memory/delegate_task.py | 24 +++-- 10 files changed, 147 insertions(+), 125 deletions(-) diff --git a/reme/agent/memory/base_memory_agent.py b/reme/agent/memory/base_memory_agent.py index 0e945c02..6b97a1c2 100644 --- a/reme/agent/memory/base_memory_agent.py +++ b/reme/agent/memory/base_memory_agent.py @@ -1,5 +1,6 @@ """Base memory agent for handling memory operations with tool-based reasoning.""" +import json from abc import ABCMeta from ...core.enumeration import MemoryType @@ -57,7 +58,11 @@ class BaseMemoryAgent(BaseReact, metaclass=ABCMeta): @property def meta_memory_info(self) -> str: """Get the meta memory info from context.""" - lines = ["Format: - memory_target: memory_type memories about memory_target"] + lines = [] for memory_target, memory_type in self.memory_target_type_mapping.items(): - lines.append(f"- {memory_target}: {memory_type} memories about {memory_target}") + line = { + "agent": f"Agent managing {memory_type} memories for {memory_target}", + "memory_target": memory_target, + } + lines.append(json.dumps(line, ensure_ascii=False)) return "\n".join(lines) diff --git a/reme/agent/memory/default/personal_retriever.yaml b/reme/agent/memory/default/personal_retriever.yaml index 47de7c6d..b1044f9b 100644 --- a/reme/agent/memory/default/personal_retriever.yaml +++ b/reme/agent/memory/default/personal_retriever.yaml @@ -1,5 +1,5 @@ system_prompt: | - You are a memory retrieval Agent responsible for retrieving {memory_type} memories about {memory_target}. + You are a Memory Retrieval Agent specialized in retrieving {memory_type} memories about {memory_target}. ## User Profile {user_profile} @@ -7,39 +7,62 @@ system_prompt: | ## User Question {context} - ## Retrieval Strategy - ### Phase 1 `retrieve_memory` - - Purpose: Search for relevant memories using semantic similarity - - Try at least 3-5 different queries before moving to next phase: - * Direct question - * Direct question reformulation - * Different phrasings and perspectives - * Entity-focused queries (names, places, events) - * Various keyword combinations - - Time filter (optional): - * Format: single date '20200101' or range '20200101,20200102' - * Example: '20200101,20200102' for 20200101 <= time <= 20200102 - * Single-sided: '0,20200102' (before date) or '20200101,99999999' (after date) - - If no results: retry with different time ranges or remove time constraints + ## Multi-Phase Retrieval Strategy + Follow these phases sequentially to gather comprehensive information: - ### Phase 2 `read_history` - - Purpose: Read full original conversation context - - Use this ONLY after completing multiple retrieve_memory attempts - - Extract history_id from context - - Prioritize most relevant or recent history entries - - Read multiple histories if needed for complete understanding + ### Phase 1: Semantic Search (No Time Filter) + **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 - ## Response Requirements - - Answer ONLY based on retrieved memories / user profile / history - NO hallucination or inference - - Always cite the source: reference specific memories with their timestamps - - If information conflicts, present all versions with their respective times - - Try multiple search angles before concluding no information exists + ### Phase 2: Temporal Search (Optional) + **Tool**: `retrieve_memory` (with time filter) + **When to use**: Only if the user question contains temporal references (dates, time periods, "when", "recent", "last year", etc.) + **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 - ### Output Format - 1. When answering, structure your response as follows: - - [timestamp][Relevant retrieved memories / user profile / history from context] - 2. If no relevant information found after thorough search (5+ queries), state: - "No relevant information found after thorough search using multiple query strategies." + ### Phase 3: Deep Dive into History + **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 + + ## Response Guidelines + **Critical Rules**: + - 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 + + **Output Format**: + When information is found: + ``` + [timestamp] Relevant memory/profile/history content + [timestamp] Additional relevant content + ``` + + When no information is found after thorough search (5+ queries across phases): + ``` + No relevant information found after exhaustive search using multiple query strategies and retrieval phases. + ``` user_message: | - Answer the question following the retrieval strategy and response requirements above. \ No newline at end of file + Retrieve relevant memories following the multi-phase strategy outlined above. \ No newline at end of file diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/default/personal_summarizer.py index 11b26d81..624cbcce 100644 --- a/reme/agent/memory/default/personal_summarizer.py +++ b/reme/agent/memory/default/personal_summarizer.py @@ -99,6 +99,7 @@ class PersonalSummarizer(BaseMemoryAgent): 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 @@ -108,8 +109,9 @@ class PersonalSummarizer(BaseMemoryAgent): memory_nodes.extend(tool.memory_nodes) return { - "answer": memory_nodes, + "answer": answer, "success": success, "messages": messages, "tools": tools, + "memory_nodes": memory_nodes, } diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index 014da6e5..4867409e 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -1,51 +1,5 @@ -system_prompt_s1_zh: | - 你是一个记忆Agent,负责管理关于 {memory_target} 的 {memory_type} 类型记忆。 - - ## 最新对话 - Format: round [] : - {context} - - ## 任务 - ### 步骤1 - 根据`最新对话`的内容,在 `add_draft_and_retrieve_similar_memory` 中创建记忆草稿 `memory_draft`。 - 工具会根据memory_draft的内容行向量检索,返回历史相似记忆,确保在第二步的时候更好的管理记忆库记忆。 - - ### 步骤2 - 使用`update_memory`更新向量库记忆。 - 通过`memory_ids_to_delete`删除历史记忆,`memories_to_add`添加新记忆,包括message_time和memory_content。 - 要求: - - 原样提取最新对话中的内容,不得推断、假设或编造。 - - 最后记忆库包含所有的历史记忆和新的记忆,例如记录在同一个主题下用户不同时间的变化。 - - 最后记忆库有比较好的组织,同一主题的记忆放到同一条中,不要有重复/多余的记忆。 - -user_message_s1_zh: | - 严格按照步骤1和步骤2完成任务 - -system_prompt_s2_zh: | - 你是一个Profile Agent,负责管理关于 {memory_target} 的 Profile。 - - ## 最新对话 - Format: round [] : - {context} - - ## 任务 - ### 步骤1 - 根据`最新对话`的内容,在 `add_draft_and_read_all_profiles` 中创建记忆草稿 `profile_draft`。 - 工具会直接返回所有的Profile,确保在第二步的时候更好的管理Profile。 - - ### 步骤2 - 使用`update_profile`更新profile库。 - 通过`profile_ids_to_delete`删除历史Profile,`profiles_to_add`添加新Profile,包括message_time、profile_key和profile_value。 - 要求: - - 原样提取最新对话中的内容,不得推断、假设或编造。 - - 最后Profile库只保留用户最新的状态。例如用户开始喜欢吃苹果,后来只吃喜欢香蕉,可以记录:水果偏好:香蕉 - - 最后Profile库有比较好的组织,同一主题的Profile放到同一条中,不要有重复/多余的Profile。 - -user_message_s2_zh: | - 严格按照步骤1和步骤2完成任务 - system_prompt_s1: | - You are a Memory Agent responsible for managing {memory_type} type memories about {memory_target}. + You are a Memory Agent responsible for managing {memory_type} memories about {memory_target}. ## Latest Conversation Format: round [] : @@ -53,22 +7,23 @@ system_prompt_s1: | ## Task ### Step 1 - Based on the content of `Latest Conversation`, create a memory draft `memory_draft` in `add_draft_and_retrieve_similar_memory`. - The tool will perform vector retrieval based on the content of memory_draft and return historically similar memories to better manage the memory store in Step 2. + Create a memory draft in `add_draft_and_retrieve_similar_memory` based on the latest conversation. + Use actual names from the conversation (e.g., "Bob likes apples") instead of generic references (e.g., "user likes apples"). Always record memories with real names. + The tool will retrieve similar historical memories via vector search to help you consolidate the memory store in Step 2. ### Step 2 - Use `update_memory` to update the vector store memories. - Delete historical memories through `memory_ids_to_delete`, add new memories through `memories_to_add`, including message_time and memory_content. + Update the vector store using `update_memory`. + Remove outdated memories via `memory_ids_to_delete` and add new ones via `memories_to_add` with their message_time and memory_content. Requirements: - - Extract content from the latest conversation as-is, without inference, assumption, or fabrication. - - The final memory store should contain all historical memories and new memories, for example, recording user changes at different times under the same topic. - - The final memory store should be well-organized, with memories on the same topic placed in one entry, without duplicate/redundant memories. + - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. + - Preserve all relevant historical and new memories, capturing how things change over time within the same topic. + - Keep the memory store well-organized: group related memories together and eliminate redundancy. user_message_s1: | - Strictly complete the task following Step 1 and Step 2 + Complete the task by following Step 1 and Step 2 in order system_prompt_s2: | - You are a Profile Agent responsible for managing the Profile about {memory_target}. + You are a Profile Agent responsible for managing profiles about {memory_target}. ## Latest Conversation Format: round [] : @@ -76,16 +31,15 @@ system_prompt_s2: | ## Task ### Step 1 - Based on the content of `Latest Conversation`, create a profile draft `profile_draft` in `add_draft_and_read_all_profiles`. - The tool will directly return all Profiles to better manage the Profile store in Step 2. + Create a profile draft in `add_draft_and_read_all_profiles` based on the latest conversation. + The tool will return all existing profiles to help you maintain the profile store in Step 2. ### Step 2 - Use `update_profile` to update the profile store. - Delete historical Profiles through `profile_ids_to_delete`, add new Profiles through `profiles_to_add`, including message_time, profile_key, and profile_value. + Update the profile store using `update_profile`. + Remove conflicting or redundant entries profiles via `profile_ids_to_delete` and add new ones via `profiles_to_add` with their message_time, profile_key, and profile_value. Requirements: - - Extract content from the latest conversation as-is, without inference, assumption, or fabrication. - - The final Profile store should only keep the user's latest state. For example, if the user initially liked apples but later only likes bananas, record: Fruit preference: banana - - The final Profile store should be well-organized, with Profiles on the same topic placed in one entry, without duplicate/redundant Profiles. + - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. + - Keep the profile store well-organized: group related profiles together and eliminate duplicates. user_message_s2: | - Strictly complete the task following Step 1 and Step 2 + Complete the task by following Step 1 and Step 2 in order diff --git a/reme/agent/memory/default/reme_retriever.yaml b/reme/agent/memory/default/reme_retriever.yaml index 8db310db..84fb6b4e 100644 --- a/reme/agent/memory/default/reme_retriever.yaml +++ b/reme/agent/memory/default/reme_retriever.yaml @@ -5,15 +5,21 @@ system_prompt: | {context} ## Available Memory Agents - Each line indicates a specialized Memory Agent that is an expert for retrieving memories about a specific memory_target. + Each line below is a JSON object representing a specialized Memory Agent: + - `agent`: Description of what this agent specializes in + - `memory_target`: The unique identifier for this agent (THIS IS WHAT YOU MUST USE) + {meta_memory_info} ## Your Task Analyze the context and delegate retrieval tasks to appropriate specialized agents: - 1. Examine the context content and identify which memory_target(s) are relevant for retrieving information + 1. Examine the context content and identify which memory_target(s) from the "Available Memory Agents" list above should be queried for relevant information 2. For each relevant memory_target, delegate the retrieval task to its corresponding specialized agent - - The memory_target must **exactly match** existing entries in the "Available Memory Agents" listed above - - Do NOT delegate to agents that don't exist above + - **CRITICAL**: The memory_target must be EXACTLY one of the `memory_target` field values from the JSON objects listed in "Available Memory Agents" above + - **DO NOT** extract or create new memory_target names from the context content + - **DO NOT** use topic names, entity names, descriptions, or any other identifiers from the context as memory_targets + - **DO NOT** use the agent description text as memory_target + - **ONLY** use the exact string values from the `memory_target` fields in the JSON objects above - Each memory_target should be assigned **only once** - do not duplicate assignments 3. Use the `delegate_task` tool **once** with all relevant memory_target(s) to enable parallel processing by specialized agents diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/default/reme_summarizer.py index 5473e71e..934863bb 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/default/reme_summarizer.py @@ -85,7 +85,7 @@ class ReMeSummarizer(BaseMemoryAgent): success = success and agent.response.success messages.extend(agent.response.metadata["messages"]) tools.extend(agent.response.metadata["tools"]) - memory_nodes.extend(agent.response.answer) + memory_nodes.extend(agent.response.metadata["memory_nodes"]) return { "answer": memory_nodes, diff --git a/reme/agent/memory/default/reme_summarizer.yaml b/reme/agent/memory/default/reme_summarizer.yaml index 01a2f4bc..7d235060 100644 --- a/reme/agent/memory/default/reme_summarizer.yaml +++ b/reme/agent/memory/default/reme_summarizer.yaml @@ -5,15 +5,21 @@ system_prompt: | {context} ## Available Memory Agents - Each line indicates a specialized Memory Agent that is an expert for summarizing memories about a specific memory_target. + Each line below is a JSON object representing a specialized Memory Agent: + - `agent`: Description of what this agent specializes in + - `memory_target`: The unique identifier for this agent (THIS IS WHAT YOU MUST USE) + {meta_memory_info} ## Your Task Analyze the context and delegate summarization tasks to appropriate specialized agents: - 1. Examine the context content and identify which memory_target(s) are relevant for storing information + 1. Examine the context content and identify which memory_target(s) from the "Available Memory Agents" list above should receive this information 2. For each relevant memory_target, delegate the summarization task to its corresponding specialized agent - - The memory_target must **exactly match** existing entries in the "Available Memory Agents" listed above - - Do NOT delegate to agents that don't exist above + - **CRITICAL**: The memory_target must be EXACTLY one of the `memory_target` field values from the JSON objects listed in "Available Memory Agents" above + - **DO NOT** extract or create new memory_target names from the context content + - **DO NOT** use topic names, preference categories, descriptions, or any other identifiers from the context as memory_targets + - **DO NOT** use the agent description text as memory_target + - **ONLY** use the exact string values from the `memory_target` fields in the JSON objects above - Each memory_target should be assigned **only once** - do not duplicate assignments 3. Use the `delegate_task` tool **once** with all relevant memory_target(s) to enable parallel processing by specialized agents diff --git a/reme/reme.py b/reme/reme.py index 30d97bf0..d23daf6e 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -101,7 +101,7 @@ class ReMe(Application): user_name: str | list[str] = "", task_name: str | list[str] = "", tool_name: str | list[str] = "", - enable_thinking_params: bool = False, + enable_thinking_params: bool = True, version: str = "default", retrieve_top_k: int = 20, return_dict: bool = False, @@ -211,7 +211,7 @@ class ReMe(Application): user_name: str | list[str] = "", task_name: str | list[str] = "", tool_name: str | list[str] = "", - enable_thinking_params: bool = False, + enable_thinking_params: bool = True, version: str = "default", retrieve_top_k: int = 20, enable_time_filter: bool = True, diff --git a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py index cb537fc4..fd3d10c8 100644 --- a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py +++ b/reme/tool/memory/add_draft_and_retrieve_similar_memory.py @@ -11,25 +11,43 @@ from ...core.utils import deduplicate_memories class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): """Tool to add draft memory and retrieve similar memories""" - def __init__(self, top_k: int = 20, enable_memory_target: bool = False, **kwargs): + def __init__( + self, + top_k: int = 20, + enable_memory_target: bool = False, + enable_when_to_use: bool = False, + **kwargs, + ): super().__init__(**kwargs) self.top_k: int = top_k self.enable_memory_target: bool = enable_memory_target + self.enable_when_to_use: bool = enable_when_to_use def _build_query_parameters(self) -> dict: """Build the query parameters schema""" properties = { - "memory_draft": { + "message_time": { "type": "string", - "description": "memory_draft", + "description": "message time, e.g. '2020-01-01 00:00:00'", + }, + "memory_content": { + "type": "string", + "description": "content of the memory.", }, } - required = ["memory_draft"] + required = ["message_time", "memory_content"] + + if self.enable_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": "description of when to use this memory.", + } + required.append("when_to_use") if self.enable_memory_target: properties["memory_target"] = { "type": "string", - "description": "memory_target", + "description": "target memory type for this memory.", } required.append("memory_target") @@ -56,7 +74,7 @@ class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): "properties": { "draft_items": { "type": "array", - "description": "List of draft memory items.", + "description": "draft_items", "items": self._build_query_parameters(), }, }, @@ -82,7 +100,7 @@ class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): queries_by_target[target].append( { - "query": item["memory_draft"], + "query": item["memory_content"], "limit": self.top_k, "filters": {}, }, diff --git a/reme/tool/memory/delegate_task.py b/reme/tool/memory/delegate_task.py index 2914d4d5..d27f4cd4 100644 --- a/reme/tool/memory/delegate_task.py +++ b/reme/tool/memory/delegate_task.py @@ -31,24 +31,32 @@ class DelegateTask(BaseMemoryTool): "parameters": { "type": "object", "properties": { - "memory_target_tasks": { + "tasks": { "type": "array", - "description": "List of memory_target tasks to delegate to specific memory agents", + "description": "List of tasks to delegate to specific memory agents", "items": { - "type": "string", - "description": "A memory_target identifier to delegate to the corresponding agent", + "type": "object", + "description": "A task item", + "properties": { + "memory_target": { + "type": "string", + "description": "The memory_target identifier to " + "delegate to the corresponding agent", + }, + }, + "required": ["memory_target"], }, }, }, - "required": ["memory_target_tasks"], + "required": ["tasks"], }, }, ) async def execute(self): - # Deduplicate and validate memory_target_tasks - memory_target_tasks = self.context.get("memory_target_tasks", []) - memory_target_tasks = sorted(set(memory_target_tasks)) + # Deduplicate and validate tasks + tasks = self.context.get("tasks", []) + memory_target_tasks = sorted(set(task["memory_target"] for task in tasks)) # Submit memory_target_tasks to agents agent_list: list[BaseMemoryAgent] = [] From 380cf5da1525852683998552533558f373bf3a04 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 01:08:04 +0800 Subject: [PATCH 12/17] fix(memory): handle MEMORY_NOT_FOUND case and update retrieval logic --- reme/agent/memory/default/personal_retriever.py | 15 +++++++++++++++ .../agent/memory/default/personal_retriever.yaml | 16 +++++++--------- .../memory/default/personal_summarizer.yaml | 4 +++- reme/reme.py | 2 +- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/default/personal_retriever.py index 3ff50eaf..895a7d26 100644 --- a/reme/agent/memory/default/personal_retriever.py +++ b/reme/agent/memory/default/personal_retriever.py @@ -67,5 +67,20 @@ class PersonalRetriever(BaseMemoryAgent): 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/default/personal_retriever.yaml b/reme/agent/memory/default/personal_retriever.yaml index b1044f9b..32b85e45 100644 --- a/reme/agent/memory/default/personal_retriever.yaml +++ b/reme/agent/memory/default/personal_retriever.yaml @@ -24,7 +24,7 @@ system_prompt: | ### Phase 2: Temporal Search (Optional) **Tool**: `retrieve_memory` (with time filter) - **When to use**: Only if the user question contains temporal references (dates, time periods, "when", "recent", "last year", etc.) + **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) @@ -53,16 +53,14 @@ system_prompt: | - Exhaust all search strategies before concluding information doesn't exist **Output Format**: - When information is found: - ``` - [timestamp] Relevant memory/profile/history content - [timestamp] Additional relevant content - ``` + - When information is found: + + [timestamp] All relevant memory/profile/history content - When no information is found after thorough search (5+ queries across phases): - ``` + - When no information is found after thorough search (5+ queries across phases): + No relevant information found after exhaustive search using multiple query strategies and retrieval phases. - ``` + user_message: | Retrieve relevant memories following the multi-phase strategy outlined above. \ No newline at end of file diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index 4867409e..45708fbf 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -13,11 +13,12 @@ system_prompt_s1: | ### Step 2 Update the vector store using `update_memory`. - Remove outdated memories via `memory_ids_to_delete` and add new ones via `memories_to_add` with their message_time and memory_content. + Remove redundant memories via `memory_ids_to_delete` and add new ones via `memories_to_add` with their message_time and memory_content. Requirements: - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. - Preserve all relevant historical and new memories, capturing how things change over time within the same topic. - Keep the memory store well-organized: group related memories together and eliminate redundancy. + - **CAUTION on memory_ids_to_delete**: Only delete memories that are truly redundant or completely superseded by new memories. user_message_s1: | Complete the task by following Step 1 and Step 2 in order @@ -40,6 +41,7 @@ system_prompt_s2: | Requirements: - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. - Keep the profile store well-organized: group related profiles together and eliminate duplicates. + - **CAUTION on profile_ids_to_delete**: Only delete profiles that are explicitly contradicted. user_message_s2: | Complete the task by following Step 1 and Step 2 in order diff --git a/reme/reme.py b/reme/reme.py index d23daf6e..326cf7a6 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -192,7 +192,7 @@ class ReMe(Application): raise NotImplementedError result = await reme_summarizer.call( - messages=messages, + messages=format_messages, description=description, service_context=self.service_context, **kwargs, From 12911e5148a1e4c72de61b2013fd61b566c13a80 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 01:26:53 +0800 Subject: [PATCH 13/17] fix(halumem): correct tmp directory path and update evaluation model default --- benchmark/halumem/eval_reme.py | 6 +- .../memory/default/personal_retriever.py | 2 +- .../memory/default/personal_summarizer.yaml | 58 ++++++++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/benchmark/halumem/eval_reme.py b/benchmark/halumem/eval_reme.py index 9b3ab7e7..a68ac3b2 100644 --- a/benchmark/halumem/eval_reme.py +++ b/benchmark/halumem/eval_reme.py @@ -105,7 +105,7 @@ class FileManager: def __init__(self, base_dir: str): self.base_dir = Path(base_dir) - self.tmp_dir = self.base_dir / "tmp" + self.tmp_dir = self.base_dir self.tmp_dir.mkdir(parents=True, exist_ok=True) def get_user_dir(self, user_name: str) -> Path: @@ -829,8 +829,8 @@ if __name__ == "__main__": parser.add_argument( "--eval_model_name", type=str, - # default="qwen3-max", - default="qwen3-235b-a22b-instruct-2507", + default="qwen3-max", + # default="qwen3-235b-a22b-instruct-2507", help="Model name for evaluation (default: qwen3-max)" ) diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/default/personal_retriever.py index 895a7d26..5b2fa88c 100644 --- a/reme/agent/memory/default/personal_retriever.py +++ b/reme/agent/memory/default/personal_retriever.py @@ -79,7 +79,7 @@ class PersonalRetriever(BaseMemoryAgent): ref_memory_id_key="", ) for n in self.retrieved_nodes - ] + ], ) result["retrieved_nodes"] = self.retrieved_nodes diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/default/personal_summarizer.yaml index 45708fbf..f30dabf6 100644 --- a/reme/agent/memory/default/personal_summarizer.yaml +++ b/reme/agent/memory/default/personal_summarizer.yaml @@ -6,19 +6,28 @@ system_prompt_s1: | {context} ## Task - ### Step 1 + ### Step 1: Create Memory Draft Create a memory draft in `add_draft_and_retrieve_similar_memory` based on the latest conversation. - Use actual names from the conversation (e.g., "Bob likes apples") instead of generic references (e.g., "user likes apples"). Always record memories with real names. - The tool will retrieve similar historical memories via vector search to help you consolidate the memory store in Step 2. + - Use actual names from the conversation (e.g., "Bob likes apples") instead of generic references (e.g., "user likes apples") + - Always record memories with real names + - The tool will retrieve similar historical memories via vector search to help you consolidate in Step 2 - ### Step 2 - Update the vector store using `update_memory`. - Remove redundant memories via `memory_ids_to_delete` and add new ones via `memories_to_add` with their message_time and memory_content. - Requirements: - - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. - - Preserve all relevant historical and new memories, capturing how things change over time within the same topic. - - Keep the memory store well-organized: group related memories together and eliminate redundancy. - - **CAUTION on memory_ids_to_delete**: Only delete memories that are truly redundant or completely superseded by new memories. + ### Step 2: Update Memory Store + Update the vector store using `update_memory` to keep it well-organized and consolidated: + + **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 + + ## Requirements + - Extract only what's explicitly stated—no inferences, assumptions, or fabrications + - Preserve temporal evolution: capture how things change over time within the same topic + - Maintain organization: group related memories by topic and eliminate all redundancy user_message_s1: | Complete the task by following Step 1 and Step 2 in order @@ -31,17 +40,26 @@ system_prompt_s2: | {context} ## Task - ### Step 1 + ### Step 1: Create Profile Draft Create a profile draft in `add_draft_and_read_all_profiles` based on the latest conversation. - The tool will return all existing profiles to help you maintain the profile store in Step 2. + - The tool will return all existing profiles to help you maintain the profile store in Step 2 - ### Step 2 - Update the profile store using `update_profile`. - Remove conflicting or redundant entries profiles via `profile_ids_to_delete` and add new ones via `profiles_to_add` with their message_time, profile_key, and profile_value. - Requirements: - - Extract only what's explicitly stated in the conversation—no inferences, assumptions, or fabrications. - - Keep the profile store well-organized: group related profiles together and eliminate duplicates. - - **CAUTION on profile_ids_to_delete**: Only delete profiles that are explicitly contradicted. + ### Step 2: Update Profile Store + Update the profile store using `update_profile` to keep it well-organized and consolidated: + + **What to Delete** (via `profile_ids_to_delete`): + - Duplicate profiles with identical keys or values + - Conflicting profiles that contradict the new information + - Profiles that should be merged into a single consolidated entry + + **What to Add** (via `profiles_to_add` with message_time, profile_key, and profile_value): + - For each profile key with changes: add ONE consolidated profile that merges related information + - New distinct profiles that don't overlap with existing ones + - Updated profiles that capture the latest state + + ## Requirements + - Extract only what's explicitly stated—no inferences, assumptions, or fabrications + - Maintain organization: group related profiles by key and eliminate all redundancy user_message_s2: | Complete the task by following Step 1 and Step 2 in order From cf62a7322801cd2261d0f841d4e620ef2f91524f Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 11:21:50 +0800 Subject: [PATCH 14/17] refactor(memory): restructure memory modules and add versioned personal memory agents --- benchmark/halumem/eval_reme.py | 41 ++++-- benchmark/halumem/scripts.sh | 1 + reme/agent/memory/__init__.py | 27 +++- reme/agent/memory/default/__init__.py | 26 ---- reme/agent/memory/default/tool_retriever.py | 10 -- reme/agent/memory/default/tool_summarizer.py | 10 -- reme/agent/memory/personal/__init__.py | 0 .../personal_retriever.py | 0 .../personal_retriever.yaml | 0 .../personal_summarizer.py | 0 .../personal_summarizer.yaml | 0 .../memory/personal/personal_v1_retriever.py | 86 +++++++++++++ .../memory/personal/personal_v1_summarizer.py | 117 ++++++++++++++++++ .../personal/personal_v1_summarizer.yaml | 65 ++++++++++ .../procedural_retriever.py | 0 .../procedural_summarizer.py | 0 .../memory/{default => }/reme_retriever.py | 10 +- .../memory/{default => }/reme_retriever.yaml | 2 - .../memory/{default => }/reme_summarizer.py | 10 +- .../memory/{default => }/reme_summarizer.yaml | 0 reme/core/schema/message.py | 9 +- reme/core/schema/tool_call.py | 10 +- reme/reme.py | 58 +++++++-- reme/tool/memory/__init__.py | 52 ++++---- reme/tool/memory/history/__init__.py | 0 reme/tool/memory/{ => history}/add_history.py | 8 +- .../tool/memory/{ => history}/read_history.py | 4 +- reme/tool/memory/profiles/__init__.py | 0 .../add_draft_and_read_all_profiles.py | 4 +- .../memory/{ => profiles}/profile_handler.py | 6 +- .../{ => profiles}/read_all_profiles.py | 4 +- .../memory/{ => profiles}/update_profile.py | 4 +- reme/tool/memory/vector/__init__.py | 0 .../add_draft_and_retrieve_similar_memory.py | 6 +- reme/tool/memory/{ => vector}/add_memory.py | 4 +- .../tool/memory/{ => vector}/delete_memory.py | 4 +- .../memory/{ => vector}/memory_handler.py | 8 +- .../memory/{ => vector}/retrieve_memory.py | 6 +- .../{ => vector}/retrieve_recent_memory.py | 6 +- .../tool/memory/{ => vector}/update_memory.py | 4 +- .../memory/{ => vector}/update_memory_v2.py | 4 +- tests/test_reme.py | 8 ++ 42 files changed, 463 insertions(+), 151 deletions(-) create mode 100755 benchmark/halumem/scripts.sh delete mode 100644 reme/agent/memory/default/__init__.py delete mode 100644 reme/agent/memory/default/tool_retriever.py delete mode 100644 reme/agent/memory/default/tool_summarizer.py create mode 100644 reme/agent/memory/personal/__init__.py rename reme/agent/memory/{default => personal}/personal_retriever.py (100%) rename reme/agent/memory/{default => personal}/personal_retriever.yaml (100%) rename reme/agent/memory/{default => personal}/personal_summarizer.py (100%) rename reme/agent/memory/{default => personal}/personal_summarizer.yaml (100%) create mode 100644 reme/agent/memory/personal/personal_v1_retriever.py create mode 100644 reme/agent/memory/personal/personal_v1_summarizer.py create mode 100644 reme/agent/memory/personal/personal_v1_summarizer.yaml rename reme/agent/memory/{default => procedural}/procedural_retriever.py (100%) rename reme/agent/memory/{default => procedural}/procedural_summarizer.py (100%) rename reme/agent/memory/{default => }/reme_retriever.py (93%) rename reme/agent/memory/{default => }/reme_retriever.yaml (93%) rename reme/agent/memory/{default => }/reme_summarizer.py (94%) rename reme/agent/memory/{default => }/reme_summarizer.yaml (100%) create mode 100644 reme/tool/memory/history/__init__.py rename reme/tool/memory/{ => history}/add_history.py (89%) rename reme/tool/memory/{ => history}/read_history.py (93%) create mode 100644 reme/tool/memory/profiles/__init__.py rename reme/tool/memory/{ => profiles}/add_draft_and_read_all_profiles.py (97%) rename reme/tool/memory/{ => profiles}/profile_handler.py (98%) rename reme/tool/memory/{ => profiles}/read_all_profiles.py (92%) rename reme/tool/memory/{ => profiles}/update_profile.py (97%) create mode 100644 reme/tool/memory/vector/__init__.py rename reme/tool/memory/{ => vector}/add_draft_and_retrieve_similar_memory.py (96%) rename reme/tool/memory/{ => vector}/add_memory.py (98%) rename reme/tool/memory/{ => vector}/delete_memory.py (95%) rename reme/tool/memory/{ => vector}/memory_handler.py (97%) rename reme/tool/memory/{ => vector}/retrieve_memory.py (96%) rename reme/tool/memory/{ => vector}/retrieve_recent_memory.py (92%) rename reme/tool/memory/{ => vector}/update_memory.py (98%) rename reme/tool/memory/{ => vector}/update_memory_v2.py (98%) diff --git a/benchmark/halumem/eval_reme.py b/benchmark/halumem/eval_reme.py index a68ac3b2..eb6b18ef 100644 --- a/benchmark/halumem/eval_reme.py +++ b/benchmark/halumem/eval_reme.py @@ -42,6 +42,7 @@ class EvalConfig: batch_size: int = 20 output_dir: str = "bench_results/reme" eval_model_name: str = "qwen3-max" + algo_version: str = "v1" # ==================== Utilities ==================== @@ -217,7 +218,7 @@ async def answer_question_with_memories( result = await reme.llm.simple_request_for_json( prompt=prompt, - model_name=model_name + model_name="qwen3-30b-a3b-instruct-2507" ) return result @@ -269,9 +270,10 @@ async def evaluation_for_question( class MemoryProcessor: """Handles ReMe memory operations.""" - def __init__(self, reme: ReMe, eval_model_name: str = "qwen3-max"): + def __init__(self, reme: ReMe, eval_model_name: str = "qwen3-max", algo_version: str = "v1"): self.reme = reme self.eval_model_name = eval_model_name + self.algo_version = algo_version async def add_memories( self, @@ -297,7 +299,7 @@ class MemoryProcessor: result = await self.reme.summary_memory( messages=batch, user_name=user_id, - version="default", + version=self.algo_version, return_dict=True, ) @@ -305,7 +307,7 @@ class MemoryProcessor: total_duration_ms += duration_ms extracted_memories.extend([m.model_dump(exclude_none=True) for m in result["answer"]]) - summary_messages.extend([m.simple_dump() for m in result["messages"]]) + summary_messages.extend([m.simple_dump(enable_argument_dict=True) for m in result["messages"]]) return extracted_memories, summary_messages, total_duration_ms @@ -329,13 +331,13 @@ class MemoryProcessor: query=query, retrieve_top_k=top_k, user_name=user_id, - version="default", + version=self.algo_version, return_dict=True, ) # Extract memories from response memories = result["answer"] - agent_messages = [x.model_dump(exclude_none=True) for x in result["messages"]] + agent_messages = [x.simple_dump(enable_argument_dict=True) for x in result["messages"]] retrieved_nodes = [x.model_dump(exclude_none=True) for x in result["retrieved_nodes"]] # Use LLM to generate structured answer from memories @@ -521,7 +523,11 @@ class HaluMemEvaluator: self.reme.prompt_handler.load_prompt_by_file(prompts_yaml_path) self.file_manager = FileManager(config.output_dir) - self.memory_processor = MemoryProcessor(self.reme, config.eval_model_name) + self.memory_processor = MemoryProcessor( + self.reme, + config.eval_model_name, + config.algo_version + ) self.qa_evaluator = QuestionAnsweringEvaluator( self.memory_processor, self.reme, @@ -763,7 +769,8 @@ async def main_async( top_k: int, user_num: int, max_concurrency: int, - eval_model_name: str = "qwen3-max" + eval_model_name: str = "qwen3-max", + algo_version: str = "v1" ): """Main async entry point for ReMe evaluation with proper resource cleanup.""" config = EvalConfig( @@ -771,7 +778,8 @@ async def main_async( top_k=top_k, user_num=user_num, max_concurrency=max_concurrency, - eval_model_name=eval_model_name + eval_model_name=eval_model_name, + algo_version=algo_version ) # Use async context manager for automatic cleanup @@ -784,7 +792,8 @@ def main( top_k: int, user_num: int, max_concurrency: int, - eval_model_name: str = "qwen3-max" + eval_model_name: str = "qwen3-max", + algo_version: str = "v1" ): """Main entry point for ReMe evaluation.""" asyncio.run(main_async( @@ -792,7 +801,8 @@ def main( top_k=top_k, user_num=user_num, max_concurrency=max_concurrency, - eval_model_name=eval_model_name + eval_model_name=eval_model_name, + algo_version=algo_version )) @@ -833,6 +843,12 @@ if __name__ == "__main__": # default="qwen3-235b-a22b-instruct-2507", help="Model name for evaluation (default: qwen3-max)" ) + parser.add_argument( + "--algo_version", + type=str, + default="v1", + help="Algorithm version for summary and retrieval (default: v1)" + ) args = parser.parse_args() @@ -841,5 +857,6 @@ if __name__ == "__main__": top_k=args.top_k, user_num=args.user_num, max_concurrency=args.max_concurrency, - eval_model_name=args.eval_model_name + eval_model_name=args.eval_model_name, + algo_version=args.algo_version ) diff --git a/benchmark/halumem/scripts.sh b/benchmark/halumem/scripts.sh new file mode 100755 index 00000000..0f589175 --- /dev/null +++ b/benchmark/halumem/scripts.sh @@ -0,0 +1 @@ +cat bench_results/reme/Martin\ Mark/session* | grep '"result_type": "' | awk -F'"' '{total++; if($4=="Correct") count++} END {printf "Correct Rate: %.2f%% (%d/%d)\n", (count/total)*100, count, total}' \ No newline at end of file diff --git a/reme/agent/memory/__init__.py b/reme/agent/memory/__init__.py index c1bac883..024b68c4 100644 --- a/reme/agent/memory/__init__.py +++ b/reme/agent/memory/__init__.py @@ -1,9 +1,32 @@ """memory agent""" -from . import default from .base_memory_agent import BaseMemoryAgent +from .personal.personal_retriever import PersonalRetriever +from .personal.personal_summarizer import PersonalSummarizer +from .personal.personal_v1_retriever import PersonalV1Retriever +from .personal.personal_v1_summarizer import PersonalV1Summarizer +from .procedural.procedural_retriever import ProceduralRetriever +from .procedural.procedural_summarizer import ProceduralSummarizer +from .reme_retriever import ReMeRetriever +from .reme_summarizer import ReMeSummarizer +from .tool.tool_retriever import ToolRetriever +from .tool.tool_summarizer import ToolSummarizer +from ...core import R __all__ = [ - "default", "BaseMemoryAgent", + "PersonalRetriever", + "PersonalSummarizer", + "PersonalV1Retriever", + "PersonalV1Summarizer", + "ProceduralRetriever", + "ProceduralSummarizer", + "ReMeRetriever", + "ReMeSummarizer", + "ToolRetriever", + "ToolSummarizer", ] + +for name in __all__: + tool_class = globals()[name] + R.op.register()(tool_class) diff --git a/reme/agent/memory/default/__init__.py b/reme/agent/memory/default/__init__.py deleted file mode 100644 index 65eb3463..00000000 --- a/reme/agent/memory/default/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Default memory agents for personal, procedural, tool and ReMe memory operations.""" - -from .personal_retriever import PersonalRetriever -from .personal_summarizer import PersonalSummarizer -from .procedural_retriever import ProceduralRetriever -from .procedural_summarizer import ProceduralSummarizer -from .reme_retriever import ReMeRetriever -from .reme_summarizer import ReMeSummarizer -from .tool_retriever import ToolRetriever -from .tool_summarizer import ToolSummarizer -from ....core import R - -__all__ = [ - "PersonalRetriever", - "PersonalSummarizer", - "ProceduralRetriever", - "ProceduralSummarizer", - "ReMeRetriever", - "ReMeSummarizer", - "ToolRetriever", - "ToolSummarizer", -] - -for name in __all__: - tool_class = globals()[name] - R.op.register()(tool_class) diff --git a/reme/agent/memory/default/tool_retriever.py b/reme/agent/memory/default/tool_retriever.py deleted file mode 100644 index 6a2d206b..00000000 --- a/reme/agent/memory/default/tool_retriever.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Tool memory retriever agent implementation.""" - -from ..base_memory_agent import BaseMemoryAgent -from ....core.enumeration import MemoryType - - -class ToolRetriever(BaseMemoryAgent): - """Agent responsible for retrieving tool-related memories.""" - - memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/agent/memory/default/tool_summarizer.py b/reme/agent/memory/default/tool_summarizer.py deleted file mode 100644 index 85222d4f..00000000 --- a/reme/agent/memory/default/tool_summarizer.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Tool memory summarizer agent implementation.""" - -from ..base_memory_agent import BaseMemoryAgent -from ....core.enumeration import MemoryType - - -class ToolSummarizer(BaseMemoryAgent): - """Agent responsible for summarizing tool-related memories.""" - - memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/agent/memory/personal/__init__.py b/reme/agent/memory/personal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/agent/memory/default/personal_retriever.py b/reme/agent/memory/personal/personal_retriever.py similarity index 100% rename from reme/agent/memory/default/personal_retriever.py rename to reme/agent/memory/personal/personal_retriever.py diff --git a/reme/agent/memory/default/personal_retriever.yaml b/reme/agent/memory/personal/personal_retriever.yaml similarity index 100% rename from reme/agent/memory/default/personal_retriever.yaml rename to reme/agent/memory/personal/personal_retriever.yaml diff --git a/reme/agent/memory/default/personal_summarizer.py b/reme/agent/memory/personal/personal_summarizer.py similarity index 100% rename from reme/agent/memory/default/personal_summarizer.py rename to reme/agent/memory/personal/personal_summarizer.py diff --git a/reme/agent/memory/default/personal_summarizer.yaml b/reme/agent/memory/personal/personal_summarizer.yaml similarity index 100% rename from reme/agent/memory/default/personal_summarizer.yaml rename to reme/agent/memory/personal/personal_summarizer.yaml diff --git a/reme/agent/memory/personal/personal_v1_retriever.py b/reme/agent/memory/personal/personal_v1_retriever.py new file mode 100644 index 00000000..34b53e09 --- /dev/null +++ b/reme/agent/memory/personal/personal_v1_retriever.py @@ -0,0 +1,86 @@ +"""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 PersonalV1Retriever(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.get_prompt("user_message"), + ), + ] + + 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_v1_summarizer.py b/reme/agent/memory/personal/personal_v1_summarizer.py new file mode 100644 index 00000000..864cbd3b --- /dev/null +++ b/reme/agent/memory/personal/personal_v1_summarizer.py @@ -0,0 +1,117 @@ +"""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 PersonalV1Summarizer(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"), + ), + ] + + async def _build_s2_messages(self) -> 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, + ), + ), + Message( + role=Role.USER, + content=self.get_prompt("user_message_s2"), + ), + ] + + 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: + stage = "s2-profile" + messages_s2 = await self._build_s2_messages() + 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_v1_summarizer.yaml b/reme/agent/memory/personal/personal_v1_summarizer.yaml new file mode 100644 index 00000000..f30dabf6 --- /dev/null +++ b/reme/agent/memory/personal/personal_v1_summarizer.yaml @@ -0,0 +1,65 @@ +system_prompt_s1: | + You are a Memory Agent responsible for managing {memory_type} memories about {memory_target}. + + ## Latest Conversation + Format: round [] : + {context} + + ## Task + ### Step 1: Create Memory Draft + Create a memory draft in `add_draft_and_retrieve_similar_memory` based on the latest conversation. + - Use actual names from the conversation (e.g., "Bob likes apples") instead of generic references (e.g., "user likes apples") + - Always record memories with real names + - The tool will retrieve similar historical memories via vector search to help you consolidate in Step 2 + + ### Step 2: Update Memory Store + Update the vector store using `update_memory` to keep it well-organized and consolidated: + + **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 + + ## Requirements + - Extract only what's explicitly stated—no inferences, assumptions, or fabrications + - Preserve temporal evolution: capture how things change over time within the same topic + - Maintain organization: group related memories by topic and eliminate all redundancy + +user_message_s1: | + Complete the task by following Step 1 and Step 2 in order + +system_prompt_s2: | + You are a Profile Agent responsible for managing profiles about {memory_target}. + + ## Latest Conversation + Format: round [] : + {context} + + ## Task + ### Step 1: Create Profile Draft + Create a profile draft in `add_draft_and_read_all_profiles` based on the latest conversation. + - The tool will return all existing profiles to help you maintain the profile store in Step 2 + + ### Step 2: Update Profile Store + Update the profile store using `update_profile` to keep it well-organized and consolidated: + + **What to Delete** (via `profile_ids_to_delete`): + - Duplicate profiles with identical keys or values + - Conflicting profiles that contradict the new information + - Profiles that should be merged into a single consolidated entry + + **What to Add** (via `profiles_to_add` with message_time, profile_key, and profile_value): + - For each profile key with changes: add ONE consolidated profile that merges related information + - New distinct profiles that don't overlap with existing ones + - Updated profiles that capture the latest state + + ## Requirements + - Extract only what's explicitly stated—no inferences, assumptions, or fabrications + - Maintain organization: group related profiles by key and eliminate all redundancy + +user_message_s2: | + Complete the task by following Step 1 and Step 2 in order diff --git a/reme/agent/memory/default/procedural_retriever.py b/reme/agent/memory/procedural/procedural_retriever.py similarity index 100% rename from reme/agent/memory/default/procedural_retriever.py rename to reme/agent/memory/procedural/procedural_retriever.py diff --git a/reme/agent/memory/default/procedural_summarizer.py b/reme/agent/memory/procedural/procedural_summarizer.py similarity index 100% rename from reme/agent/memory/default/procedural_summarizer.py rename to reme/agent/memory/procedural/procedural_summarizer.py diff --git a/reme/agent/memory/default/reme_retriever.py b/reme/agent/memory/reme_retriever.py similarity index 93% rename from reme/agent/memory/default/reme_retriever.py rename to reme/agent/memory/reme_retriever.py index 2f3f78bb..9de51aa2 100644 --- a/reme/agent/memory/default/reme_retriever.py +++ b/reme/agent/memory/reme_retriever.py @@ -1,10 +1,10 @@ """ReMe retriever agent that orchestrates multiple memory agents to retrieve information.""" -from ..base_memory_agent import BaseMemoryAgent -from ....core.enumeration import Role -from ....core.op import BaseTool -from ....core.schema import Message -from ....core.utils import format_messages +from .base_memory_agent import BaseMemoryAgent +from ...core.enumeration import Role +from ...core.op import BaseTool +from ...core.schema import Message +from ...core.utils import format_messages class ReMeRetriever(BaseMemoryAgent): diff --git a/reme/agent/memory/default/reme_retriever.yaml b/reme/agent/memory/reme_retriever.yaml similarity index 93% rename from reme/agent/memory/default/reme_retriever.yaml rename to reme/agent/memory/reme_retriever.yaml index 84fb6b4e..9b5ea8af 100644 --- a/reme/agent/memory/default/reme_retriever.yaml +++ b/reme/agent/memory/reme_retriever.yaml @@ -23,7 +23,5 @@ system_prompt: | - Each memory_target should be assigned **only once** - do not duplicate assignments 3. Use the `delegate_task` tool **once** with all relevant memory_target(s) to enable parallel processing by specialized agents - Note: If the context contains no memorable information (e.g., simple greetings), return ``. - user_message: | Please analyze the context and delegate retrieval tasks to the appropriate specialized agents. \ No newline at end of file diff --git a/reme/agent/memory/default/reme_summarizer.py b/reme/agent/memory/reme_summarizer.py similarity index 94% rename from reme/agent/memory/default/reme_summarizer.py rename to reme/agent/memory/reme_summarizer.py index 934863bb..8b263f64 100644 --- a/reme/agent/memory/default/reme_summarizer.py +++ b/reme/agent/memory/reme_summarizer.py @@ -1,10 +1,10 @@ """ReMe summarizer agent that orchestrates multiple memory agents to summarize information.""" -from ..base_memory_agent import BaseMemoryAgent -from ....core.enumeration import Role -from ....core.op import BaseTool -from ....core.schema import Message -from ....core.utils import format_messages +from .base_memory_agent import BaseMemoryAgent +from ...core.enumeration import Role +from ...core.op import BaseTool +from ...core.schema import Message +from ...core.utils import format_messages class ReMeSummarizer(BaseMemoryAgent): diff --git a/reme/agent/memory/default/reme_summarizer.yaml b/reme/agent/memory/reme_summarizer.yaml similarity index 100% rename from reme/agent/memory/default/reme_summarizer.yaml rename to reme/agent/memory/reme_summarizer.yaml diff --git a/reme/core/schema/message.py b/reme/core/schema/message.py index 54acde63..d9201979 100644 --- a/reme/core/schema/message.py +++ b/reme/core/schema/message.py @@ -84,6 +84,7 @@ class Message(BaseModel): add_reasoning: bool = True, add_time_created: bool = False, add_metadata: bool = False, + enable_argument_dict: bool = False, as_dict: bool = True, ) -> dict | str: """Transforms the message into a simplified dictionary for standard APIs.""" @@ -98,7 +99,13 @@ class Message(BaseModel): result["reasoning_content"] = self.reasoning_content if self.tool_calls: - result["tool_calls"] = [tc.simple_output_dump() for tc in self.tool_calls] + result["tool_calls"] = [ + tc.simple_output_dump( + as_dict=True, + enable_argument_dict=enable_argument_dict, + ) + for tc in self.tool_calls + ] if self.tool_call_id: result["tool_call_id"] = self.tool_call_id diff --git a/reme/core/schema/tool_call.py b/reme/core/schema/tool_call.py index 9cd58c7f..19d7e106 100644 --- a/reme/core/schema/tool_call.py +++ b/reme/core/schema/tool_call.py @@ -145,17 +145,13 @@ class ToolCall(BaseModel): } return result if as_dict else json.dumps(result, ensure_ascii=False) - def simple_output_dump(self, as_dict: bool = True) -> dict | str: - """Convert ToolCall to output format dictionary or JSON string for API responses. - - Args: - as_dict: If True, returns dict; if False, returns JSON string. - """ + def simple_output_dump(self, as_dict: bool = True, enable_argument_dict: bool = False) -> dict | str: + """Convert ToolCall to output format dictionary or JSON string for API responses.""" result = { "index": self.index, "id": self.id, self.type: { - "arguments": self.arguments, + "arguments": self.argument_dict if enable_argument_dict else self.arguments, "name": self.name, }, "type": self.type, diff --git a/reme/reme.py b/reme/reme.py index 326cf7a6..b7c1ea5e 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -3,15 +3,17 @@ import sys from pathlib import Path -from reme.agent.memory import BaseMemoryAgent -from .agent.memory.default import ( +from .agent.memory import ( + BaseMemoryAgent, ReMeSummarizer, + ReMeRetriever, + PersonalV1Summarizer, + PersonalV1Retriever, PersonalSummarizer, PersonalRetriever, - ReMeRetriever, ProceduralSummarizer, - ToolSummarizer, ProceduralRetriever, + ToolSummarizer, ToolRetriever, ) from .config import ReMeConfigParser @@ -116,7 +118,7 @@ class ReMe(Application): format_messages.append(message) personal_summarizer: BaseMemoryAgent - if version: + if version == "default": personal_summarizer = PersonalSummarizer( tools=[ AddDraftAndRetrieveSimilarMemory( @@ -134,17 +136,33 @@ class ReMe(Application): ), ], ) + + elif version == "v1": + personal_summarizer = PersonalV1Summarizer( + tools=[ + AddDraftAndRetrieveSimilarMemory( + enable_thinking_params=enable_thinking_params, + top_k=retrieve_top_k, + ), + UpdateMemoryV2(enable_thinking_params=enable_thinking_params), + AddDraftAndReadAllProfiles( + enable_thinking_params=enable_thinking_params, + profile_dir=self.profile_dir, + ), + ], + ) + else: raise NotImplementedError procedural_summarizer: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: procedural_summarizer = ProceduralSummarizer(tools=[]) else: raise NotImplementedError tool_summarizer: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: tool_summarizer = ToolSummarizer(tools=[]) else: raise NotImplementedError @@ -186,7 +204,7 @@ class ReMe(Application): memory_agents = [personal_summarizer, procedural_summarizer, tool_summarizer] reme_summarizer: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: reme_summarizer = ReMeSummarizer(tools=[AddHistory(), DelegateTask(memory_agents=memory_agents)]) else: raise NotImplementedError @@ -221,7 +239,7 @@ class ReMe(Application): """Retrieve relevant personal, procedural and tool memories for a query.""" personal_retriever: BaseMemoryAgent - if version: + if version == "default": personal_retriever = PersonalRetriever( tools=[ ReadAllProfiles( @@ -236,17 +254,33 @@ class ReMe(Application): ReadHistory(enable_thinking_params=enable_thinking_params), ], ) + + elif version == "v1": + personal_retriever = PersonalV1Retriever( + tools=[ + ReadAllProfiles( + enable_thinking_params=enable_thinking_params, + profile_dir=self.profile_dir, + ), + RetrieveMemory( + enable_thinking_params=enable_thinking_params, + top_k=retrieve_top_k, + enable_time_filter=enable_time_filter, + ), + ], + ) + else: raise NotImplementedError procedural_retriever: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: procedural_retriever = ProceduralRetriever(tools=[]) else: raise NotImplementedError tool_retriever: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: tool_retriever = ToolRetriever(tools=[]) else: raise NotImplementedError @@ -286,7 +320,7 @@ class ReMe(Application): memory_agents = [personal_retriever, procedural_retriever, tool_retriever] reme_retriever: BaseMemoryAgent - if version == "default": + if version in ["default", "v1"]: reme_retriever = ReMeRetriever(tools=[DelegateTask(memory_agents=memory_agents)]) else: raise NotImplementedError diff --git a/reme/tool/memory/__init__.py b/reme/tool/memory/__init__.py index cab968dd..ff58d44f 100644 --- a/reme/tool/memory/__init__.py +++ b/reme/tool/memory/__init__.py @@ -1,42 +1,48 @@ """memory tools""" -from .add_draft_and_read_all_profiles import AddDraftAndReadAllProfiles -from .add_draft_and_retrieve_similar_memory import AddDraftAndRetrieveSimilarMemory -from .add_history import AddHistory -from .add_memory import AddMemory from .base_memory_tool import BaseMemoryTool from .delegate_task import DelegateTask -from .delete_memory import DeleteMemory -from .memory_handler import MemoryHandler -from .profile_handler import ProfileHandler -from .read_all_profiles import ReadAllProfiles -from .read_history import ReadHistory -from .retrieve_memory import RetrieveMemory -from .retrieve_recent_memory import RetrieveRecentMemory -from .update_memory import UpdateMemory -from .update_memory_v2 import UpdateMemoryV2 -from .update_profile import UpdateProfile +from .history.add_history import AddHistory +from .history.read_history import ReadHistory +from .profiles.add_draft_and_read_all_profiles import AddDraftAndReadAllProfiles +from .profiles.profile_handler import ProfileHandler +from .profiles.read_all_profiles import ReadAllProfiles +from .profiles.update_profile import UpdateProfile +from .vector.add_draft_and_retrieve_similar_memory import AddDraftAndRetrieveSimilarMemory +from .vector.add_memory import AddMemory +from .vector.delete_memory import DeleteMemory +from .vector.memory_handler import MemoryHandler +from .vector.retrieve_memory import RetrieveMemory +from .vector.retrieve_recent_memory import RetrieveRecentMemory +from .vector.update_memory import UpdateMemory +from .vector.update_memory_v2 import UpdateMemoryV2 from ...core import R __all__ = [ - "AddDraftAndReadAllProfiles", - "AddDraftAndRetrieveSimilarMemory", - "AddHistory", - "AddMemory", + # Base "BaseMemoryTool", "DelegateTask", - "DeleteMemory", - "MemoryHandler", + # History + "AddHistory", + "ReadHistory", + # Profiles + "AddDraftAndReadAllProfiles", "ProfileHandler", "ReadAllProfiles", - "ReadHistory", + "UpdateProfile", + # Vector + "AddDraftAndRetrieveSimilarMemory", + "AddMemory", + "DeleteMemory", + "MemoryHandler", "RetrieveMemory", "RetrieveRecentMemory", "UpdateMemory", "UpdateMemoryV2", - "UpdateProfile", ] for name in __all__: tool_class = globals()[name] - R.op.register()(tool_class) + # Only register classes that inherit from BaseMemoryTool + if isinstance(tool_class, type) and issubclass(tool_class, BaseMemoryTool) and tool_class is not BaseMemoryTool: + R.op.register()(tool_class) diff --git a/reme/tool/memory/history/__init__.py b/reme/tool/memory/history/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/tool/memory/add_history.py b/reme/tool/memory/history/add_history.py similarity index 89% rename from reme/tool/memory/add_history.py rename to reme/tool/memory/history/add_history.py index 57a0097e..629f4532 100644 --- a/reme/tool/memory/add_history.py +++ b/reme/tool/memory/history/add_history.py @@ -2,10 +2,10 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool -from ...core.enumeration import MemoryType -from ...core.schema import ToolCall, MemoryNode, Message -from ...core.utils import format_messages +from ..base_memory_tool import BaseMemoryTool +from ....core.enumeration import MemoryType +from ....core.schema import ToolCall, MemoryNode, Message +from ....core.utils import format_messages class AddHistory(BaseMemoryTool): diff --git a/reme/tool/memory/read_history.py b/reme/tool/memory/history/read_history.py similarity index 93% rename from reme/tool/memory/read_history.py rename to reme/tool/memory/history/read_history.py index 500d4bd9..1f328eb2 100644 --- a/reme/tool/memory/read_history.py +++ b/reme/tool/memory/history/read_history.py @@ -2,8 +2,8 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool -from ...core.schema import MemoryNode, ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import MemoryNode, ToolCall class ReadHistory(BaseMemoryTool): diff --git a/reme/tool/memory/profiles/__init__.py b/reme/tool/memory/profiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/tool/memory/add_draft_and_read_all_profiles.py b/reme/tool/memory/profiles/add_draft_and_read_all_profiles.py similarity index 97% rename from reme/tool/memory/add_draft_and_read_all_profiles.py rename to reme/tool/memory/profiles/add_draft_and_read_all_profiles.py index 0f9ba241..716c4e5e 100644 --- a/reme/tool/memory/add_draft_and_read_all_profiles.py +++ b/reme/tool/memory/profiles/add_draft_and_read_all_profiles.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .profile_handler import ProfileHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class AddDraftAndReadAllProfiles(BaseMemoryTool): diff --git a/reme/tool/memory/profile_handler.py b/reme/tool/memory/profiles/profile_handler.py similarity index 98% rename from reme/tool/memory/profile_handler.py rename to reme/tool/memory/profiles/profile_handler.py index 0f51d014..6f646ac3 100644 --- a/reme/tool/memory/profile_handler.py +++ b/reme/tool/memory/profiles/profile_handler.py @@ -4,9 +4,9 @@ from pathlib import Path from loguru import logger -from ...core.enumeration import MemoryType -from ...core.schema import MemoryNode -from ...core.utils import CacheHandler, deduplicate_memories +from ....core.enumeration import MemoryType +from ....core.schema import MemoryNode +from ....core.utils import CacheHandler, deduplicate_memories class ProfileHandler: diff --git a/reme/tool/memory/read_all_profiles.py b/reme/tool/memory/profiles/read_all_profiles.py similarity index 92% rename from reme/tool/memory/read_all_profiles.py rename to reme/tool/memory/profiles/read_all_profiles.py index d18be220..892bbb58 100644 --- a/reme/tool/memory/read_all_profiles.py +++ b/reme/tool/memory/profiles/read_all_profiles.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .profile_handler import ProfileHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class ReadAllProfiles(BaseMemoryTool): diff --git a/reme/tool/memory/update_profile.py b/reme/tool/memory/profiles/update_profile.py similarity index 97% rename from reme/tool/memory/update_profile.py rename to reme/tool/memory/profiles/update_profile.py index 5ab7c962..5edd990b 100644 --- a/reme/tool/memory/update_profile.py +++ b/reme/tool/memory/profiles/update_profile.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .profile_handler import ProfileHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class UpdateProfile(BaseMemoryTool): diff --git a/reme/tool/memory/vector/__init__.py b/reme/tool/memory/vector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py b/reme/tool/memory/vector/add_draft_and_retrieve_similar_memory.py similarity index 96% rename from reme/tool/memory/add_draft_and_retrieve_similar_memory.py rename to reme/tool/memory/vector/add_draft_and_retrieve_similar_memory.py index fd3d10c8..040c475a 100644 --- a/reme/tool/memory/add_draft_and_retrieve_similar_memory.py +++ b/reme/tool/memory/vector/add_draft_and_retrieve_similar_memory.py @@ -2,10 +2,10 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall, MemoryNode -from ...core.utils import deduplicate_memories +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall, MemoryNode +from ....core.utils import deduplicate_memories class AddDraftAndRetrieveSimilarMemory(BaseMemoryTool): diff --git a/reme/tool/memory/add_memory.py b/reme/tool/memory/vector/add_memory.py similarity index 98% rename from reme/tool/memory/add_memory.py rename to reme/tool/memory/vector/add_memory.py index 833c2b54..3a049fc9 100644 --- a/reme/tool/memory/add_memory.py +++ b/reme/tool/memory/vector/add_memory.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class AddMemory(BaseMemoryTool): diff --git a/reme/tool/memory/delete_memory.py b/reme/tool/memory/vector/delete_memory.py similarity index 95% rename from reme/tool/memory/delete_memory.py rename to reme/tool/memory/vector/delete_memory.py index e95cfa40..da8094fe 100644 --- a/reme/tool/memory/delete_memory.py +++ b/reme/tool/memory/vector/delete_memory.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class DeleteMemory(BaseMemoryTool): diff --git a/reme/tool/memory/memory_handler.py b/reme/tool/memory/vector/memory_handler.py similarity index 97% rename from reme/tool/memory/memory_handler.py rename to reme/tool/memory/vector/memory_handler.py index 5b896449..8b6f8d78 100644 --- a/reme/tool/memory/memory_handler.py +++ b/reme/tool/memory/vector/memory_handler.py @@ -1,9 +1,9 @@ """Memory handler""" -from ...core.context import ServiceContext -from ...core.enumeration import MemoryType -from ...core.schema import MemoryNode -from ...core.vector_store import BaseVectorStore +from ....core.context import ServiceContext +from ....core.enumeration import MemoryType +from ....core.schema import MemoryNode +from ....core.vector_store import BaseVectorStore class MemoryHandler: diff --git a/reme/tool/memory/retrieve_memory.py b/reme/tool/memory/vector/retrieve_memory.py similarity index 96% rename from reme/tool/memory/retrieve_memory.py rename to reme/tool/memory/vector/retrieve_memory.py index db542643..af19bc1e 100644 --- a/reme/tool/memory/retrieve_memory.py +++ b/reme/tool/memory/vector/retrieve_memory.py @@ -2,10 +2,10 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall, MemoryNode -from ...core.utils import deduplicate_memories +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall, MemoryNode +from ....core.utils import deduplicate_memories class RetrieveMemory(BaseMemoryTool): diff --git a/reme/tool/memory/retrieve_recent_memory.py b/reme/tool/memory/vector/retrieve_recent_memory.py similarity index 92% rename from reme/tool/memory/retrieve_recent_memory.py rename to reme/tool/memory/vector/retrieve_recent_memory.py index 05b1a777..fc94d23d 100644 --- a/reme/tool/memory/retrieve_recent_memory.py +++ b/reme/tool/memory/vector/retrieve_recent_memory.py @@ -2,10 +2,10 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall, MemoryNode -from ...core.utils import deduplicate_memories +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall, MemoryNode +from ....core.utils import deduplicate_memories class RetrieveRecentMemory(BaseMemoryTool): diff --git a/reme/tool/memory/update_memory.py b/reme/tool/memory/vector/update_memory.py similarity index 98% rename from reme/tool/memory/update_memory.py rename to reme/tool/memory/vector/update_memory.py index ba4f8d5b..dbdf9ce6 100644 --- a/reme/tool/memory/update_memory.py +++ b/reme/tool/memory/vector/update_memory.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class UpdateMemory(BaseMemoryTool): diff --git a/reme/tool/memory/update_memory_v2.py b/reme/tool/memory/vector/update_memory_v2.py similarity index 98% rename from reme/tool/memory/update_memory_v2.py rename to reme/tool/memory/vector/update_memory_v2.py index b855c15b..5b8f0950 100644 --- a/reme/tool/memory/update_memory_v2.py +++ b/reme/tool/memory/vector/update_memory_v2.py @@ -2,9 +2,9 @@ from loguru import logger -from .base_memory_tool import BaseMemoryTool from .memory_handler import MemoryHandler -from ...core.schema import ToolCall +from ..base_memory_tool import BaseMemoryTool +from ....core.schema import ToolCall class UpdateMemoryV2(BaseMemoryTool): diff --git a/tests/test_reme.py b/tests/test_reme.py index 52b11a64..7ac30e3c 100644 --- a/tests/test_reme.py +++ b/tests/test_reme.py @@ -17,34 +17,42 @@ async def test_reme(): { "role": "user", "content": "你好,我是张伟,今年28岁,是一名软件工程师。", + "time_created": "2026-01-29 10:00:00", }, { "role": "assistant", "content": "你好张伟!很高兴认识你。作为一名软件工程师,你主要从事什么方向的开发工作呢?", + "time_created": "2026-01-29 10:00:05", }, { "role": "user", "content": "我主要做后端开发,擅长Python和Go语言。最近在研究AI Agent相关的技术。", + "time_created": "2026-01-29 10:00:30", }, { "role": "assistant", "content": "很棒!Python和Go都是非常实用的语言。AI Agent是当前很热门的方向,你在这方面有什么具体的研究重点吗?", + "time_created": "2026-01-29 10:00:35", }, { "role": "user", "content": "我特别关注记忆系统的设计,希望能让AI Agent具有长期记忆能力。我的工作地点在北京,平时喜欢看技术博客和参加技术分享会。", + "time_created": "2026-01-29 10:01:00", }, { "role": "assistant", "content": "记忆系统确实是AI Agent的核心能力之一。北京有很多优秀的技术社区和活动,相信你能找到很多志同道合的朋友。", + "time_created": "2026-01-29 10:01:05", }, { "role": "user", "content": "是的,我每周末都会去参加一些技术沙龙。对了,我的邮箱是zhangwei@example.com,如果有好的技术资料可以发给我。", + "time_created": "2026-01-29 10:01:30", }, { "role": "assistant", "content": "好的,我记下了。保持学习的热情很重要,祝你在AI Agent领域的研究越来越深入!", + "time_created": "2026-01-29 10:01:35", }, ] From ede831897c1259fc7ce3c8f24acfee2ae3e36d0f Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 11:22:13 +0800 Subject: [PATCH 15/17] feat(memory): add tool memory agents and retrieval strategy --- .../personal/personal_v1_retriever.yaml | 66 +++++++++++++++++++ reme/agent/memory/procedural/__init__.py | 0 reme/agent/memory/tool/__init__.py | 0 reme/agent/memory/tool/tool_retriever.py | 10 +++ reme/agent/memory/tool/tool_summarizer.py | 10 +++ 5 files changed, 86 insertions(+) create mode 100644 reme/agent/memory/personal/personal_v1_retriever.yaml create mode 100644 reme/agent/memory/procedural/__init__.py create mode 100644 reme/agent/memory/tool/__init__.py create mode 100644 reme/agent/memory/tool/tool_retriever.py create mode 100644 reme/agent/memory/tool/tool_summarizer.py diff --git a/reme/agent/memory/personal/personal_v1_retriever.yaml b/reme/agent/memory/personal/personal_v1_retriever.yaml new file mode 100644 index 00000000..32b85e45 --- /dev/null +++ b/reme/agent/memory/personal/personal_v1_retriever.yaml @@ -0,0 +1,66 @@ +system_prompt: | + You are a Memory Retrieval Agent specialized in retrieving {memory_type} memories about {memory_target}. + + ## User Profile + {user_profile} + + ## User Question + {context} + + ## Multi-Phase Retrieval Strategy + Follow these phases sequentially to gather comprehensive information: + + ### Phase 1: Semantic Search (No Time Filter) + **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 + + ### Phase 2: Temporal Search (Optional) + **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 + + ### Phase 3: Deep Dive into History + **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 + + ## Response Guidelines + **Critical Rules**: + - 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 + + **Output Format**: + - When information is found: + + [timestamp] All relevant memory/profile/history content + + - When no information is found after thorough search (5+ queries across phases): + + No relevant information found after exhaustive search using multiple query strategies and retrieval phases. + + +user_message: | + Retrieve relevant memories following the multi-phase strategy outlined above. \ No newline at end of file diff --git a/reme/agent/memory/procedural/__init__.py b/reme/agent/memory/procedural/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/agent/memory/tool/__init__.py b/reme/agent/memory/tool/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme/agent/memory/tool/tool_retriever.py b/reme/agent/memory/tool/tool_retriever.py new file mode 100644 index 00000000..6a2d206b --- /dev/null +++ b/reme/agent/memory/tool/tool_retriever.py @@ -0,0 +1,10 @@ +"""Tool memory retriever agent implementation.""" + +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ToolRetriever(BaseMemoryAgent): + """Agent responsible for retrieving tool-related memories.""" + + memory_type: MemoryType = MemoryType.TOOL diff --git a/reme/agent/memory/tool/tool_summarizer.py b/reme/agent/memory/tool/tool_summarizer.py new file mode 100644 index 00000000..85222d4f --- /dev/null +++ b/reme/agent/memory/tool/tool_summarizer.py @@ -0,0 +1,10 @@ +"""Tool memory summarizer agent implementation.""" + +from ..base_memory_agent import BaseMemoryAgent +from ....core.enumeration import MemoryType + + +class ToolSummarizer(BaseMemoryAgent): + """Agent responsible for summarizing tool-related memories.""" + + memory_type: MemoryType = MemoryType.TOOL From 9a61a6fe7c31fd7f631f1cb821d18d113723c296 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 11:24:10 +0800 Subject: [PATCH 16/17] chore(config): update entry point reference in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 63bedc49..47aa478d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,6 @@ Documentation = "https://reme.agentscope.io/" Repository = "https://github.com/agentscope-ai/ReMe" [project.scripts] -reme = "reme.reme:main" +reme = "reme_ai.main:main" # python -m build && twine upload dist/* From af75924b46906c8a3df98a2668f5c99f086379a9 Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Thu, 29 Jan 2026 11:51:51 +0800 Subject: [PATCH 17/17] feat(retrieval): integrate read history component into retrieval chain --- reme/reme.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reme/reme.py b/reme/reme.py index b7c1ea5e..5f587ea5 100644 --- a/reme/reme.py +++ b/reme/reme.py @@ -267,6 +267,7 @@ class ReMe(Application): top_k=retrieve_top_k, enable_time_filter=enable_time_filter, ), + ReadHistory(enable_thinking_params=enable_thinking_params), ], )