From 74c1386a697bc2e9f767cdb8b1bedfc8fab9ba3c Mon Sep 17 00:00:00 2001 From: "jinli.yl" Date: Mon, 19 Jan 2026 00:46:52 +0800 Subject: [PATCH] refactor(mem_agent): optimize agent execution and enhance evaluation pipeline --- bench/halumem/compute_qa_stats_v4.py | 220 +++++++----------- bench/halumem/eval_reme_simple_v4.py | 37 ++- bench/halumem/eval_tools.py | 37 +++ bench/halumem/halumem.yaml | 24 ++ reme_ai/core/config/default.yaml | 1 + reme_ai/mem_agent/base_memory_agent.py | 34 ++- reme_ai/mem_agent/v4/personal_retriever_v4.py | 44 ++-- .../mem_agent/v4/personal_retriever_v4.yaml | 21 +- .../mem_agent/v4/personal_summarizer_v4.py | 47 ++-- .../mem_agent/v4/personal_summarizer_v4.yaml | 20 +- reme_ai/mem_agent/v4/reme_retriever_v4.py | 78 ++++++- reme_ai/mem_tool/v4/hands_off.py | 3 + reme_ai/mem_tool/v4/read_history.py | 2 +- reme_ai/mem_tool/v4/read_user_profile.py | 35 ++- reme_ai/mem_tool/v4/retrieve_memory.py | 2 +- reme_ai/mem_tool/v4/update_user_profile.py | 2 +- reme_ai/reme.py | 4 +- 17 files changed, 374 insertions(+), 237 deletions(-) diff --git a/bench/halumem/compute_qa_stats_v4.py b/bench/halumem/compute_qa_stats_v4.py index 7cdee50a..c7bdfab2 100644 --- a/bench/halumem/compute_qa_stats_v4.py +++ b/bench/halumem/compute_qa_stats_v4.py @@ -1,11 +1,9 @@ """ Compute Question Answering statistics from eval_reme_simple_v4.py results. -This script processes the output from eval_reme_simple_v4.py and computes -comprehensive QA metrics. - Usage: python bench/halumem/compute_qa_stats_v4.py --results_file bench_results/reme_simple_v4/eval_results.jsonl + python bench/halumem/compute_qa_stats_v4.py --tmp_dir bench_results/reme_simple_v4/tmp """ import json @@ -13,8 +11,6 @@ import os from pathlib import Path from typing import Any -from loguru import logger - def compute_qa_metrics(qa_records: list[dict]) -> dict[str, Any]: """Compute question answering metrics.""" @@ -31,51 +27,37 @@ def compute_qa_metrics(qa_records: list[dict]) -> dict[str, Any]: "qa_num": 0 } - correct = 0 - hallucination = 0 - omission = 0 - valid = 0 + correct = hallucination = omission = valid = 0 for qa in qa_records: result_type = qa.get("result_type", "") - - if result_type in ["Correct", "Hallucination", "Omission"]: + if result_type == "Correct": + correct += 1 + valid += 1 + elif result_type == "Hallucination": + hallucination += 1 + valid += 1 + elif result_type == "Omission": + omission += 1 valid += 1 - if result_type == "Correct": - correct += 1 - elif result_type == "Hallucination": - hallucination += 1 - elif result_type == "Omission": - omission += 1 metrics = { "correct_qa_ratio(all)": correct / total, "hallucination_qa_ratio(all)": hallucination / total, "omission_qa_ratio(all)": omission / total, + "correct_qa_ratio(valid)": correct / valid if valid > 0 else 0, + "hallucination_qa_ratio(valid)": hallucination / valid if valid > 0 else 0, + "omission_qa_ratio(valid)": omission / valid if valid > 0 else 0, "qa_valid_num": valid, "qa_num": total } - if valid > 0: - metrics.update({ - "correct_qa_ratio(valid)": correct / valid, - "hallucination_qa_ratio(valid)": hallucination / valid, - "omission_qa_ratio(valid)": omission / valid - }) - else: - metrics.update({ - "correct_qa_ratio(valid)": 0, - "hallucination_qa_ratio(valid)": 0, - "omission_qa_ratio(valid)": 0 - }) - return metrics def compute_time_metrics(results_file: str) -> dict[str, float]: """Compute timing metrics from evaluation results.""" - add_duration = 0 - search_duration = 0 + add_duration = search_duration = 0 with open(results_file, "r", encoding="utf-8") as f: for line in f: @@ -85,12 +67,10 @@ def compute_time_metrics(results_file: str) -> dict[str, float]: for session in user_data.get("sessions", []): add_duration += session.get("add_dialogue_duration_ms", 0) - eval_results = session.get("evaluation_results", {}) for qa in eval_results.get("question_answering_records", []): search_duration += qa.get("search_duration_ms", 0) - # Convert to minutes return { "add_dialogue_duration_time": add_duration / 1000 / 60, "search_memory_duration_time": search_duration / 1000 / 60, @@ -98,36 +78,27 @@ def compute_time_metrics(results_file: str) -> dict[str, float]: } -def load_from_tmp_dir(tmp_dir: str) -> tuple[str, list[dict]]: +def load_from_tmp_dir(tmp_dir: str) -> str: """Load data from tmp directory and generate eval_results.jsonl file.""" tmp_path = Path(tmp_dir) - parent_dir = tmp_path.parent - eval_results_file = parent_dir / "eval_results.jsonl" + eval_results_file = tmp_path.parent / "eval_results.jsonl" - print(f"\nšŸ“ Loading data from tmp directory: {tmp_dir}") - print(f"šŸ“ Will generate: {eval_results_file}") + print(f"\nšŸ“ Loading from: {tmp_dir}") + print(f"šŸ“ Generating: {eval_results_file}") - # Collect all user directories user_dirs = [d for d in tmp_path.iterdir() if d.is_dir()] - print(f" Found {len(user_dirs)} user directories") + print(f" Found {len(user_dirs)} users") users_data = [] - for user_dir in user_dirs: - user_name = user_dir.name - - # Load all session files for this user (sorted by session number) session_files = sorted( - [f for f in user_dir.iterdir() - if f.name.startswith("session_") and f.suffix == ".json"], - key=lambda f: int(f.stem.split("_")[1]) # Sort by session number + [f for f in user_dir.iterdir() if f.name.startswith("session_") and f.suffix == ".json"], + key=lambda f: int(f.stem.split("_")[1]) ) if not session_files: - print(f" āš ļø No session files found for user: {user_name}") continue - # Load first session to get user metadata with open(session_files[0], "r", encoding="utf-8") as f: first_session = json.load(f) @@ -137,52 +108,46 @@ def load_from_tmp_dir(tmp_dir: str) -> tuple[str, list[dict]]: "sessions": [] } - # Load all sessions for session_file in session_files: with open(session_file, "r", encoding="utf-8") as f: session_data = json.load(f) - # Remove redundant user metadata session_data.pop("uuid", None) session_data.pop("user_name", None) user_data["sessions"].append(session_data) users_data.append(user_data) - print(f" āœ“ Loaded user {user_name}: {len(session_files)} sessions") + print(f" āœ“ {user_dir.name}: {len(session_files)} sessions") - # Write to eval_results.jsonl with open(eval_results_file, "w", encoding="utf-8") as f: for user_data in users_data: f.write(json.dumps(user_data, ensure_ascii=False) + "\n") print(f" āœ… Generated: {eval_results_file}") - - return str(eval_results_file), users_data + return str(eval_results_file) def main(input_path: str): """Main function to compute statistics from eval results.""" if not os.path.exists(input_path): - logger.error(f"Input path not found: {input_path}") + print(f"āŒ Error: Path not found: {input_path}") return print("\n" + "=" * 80) - print("COMPUTING QUESTION ANSWERING STATISTICS - REME V4") + print("REME V4 - QUESTION ANSWERING STATISTICS") print("=" * 80) - # Determine if input is a directory (tmp) or file (eval_results.jsonl) + # Load or generate eval_results.jsonl if os.path.isdir(input_path): - results_file, users_data = load_from_tmp_dir(input_path) + results_file = load_from_tmp_dir(input_path) else: results_file = input_path - users_data = None - print(f"\nšŸ“ Using existing results file: {results_file}") + print(f"\nšŸ“ Using: {results_file}") - # Collect all QA records with metadata + # Collect QA records with metadata qa_records = [] - qa_records_with_metadata = [] # Store records with user/session/question info - user_count = 0 - session_count = 0 + qa_with_metadata = [] + user_count = session_count = 0 with open(results_file, "r", encoding="utf-8") as f: for line in f: @@ -192,38 +157,38 @@ def main(input_path: str): user_count += 1 user_name = user_data.get("user_name", "Unknown") - valid_session_idx = 0 # Track the index of valid (non-skipped) sessions + valid_session_idx = 0 for original_idx, session in enumerate(user_data.get("sessions", [])): if session.get("is_generated_qa_session"): continue session_count += 1 eval_results = session.get("evaluation_results", {}) - session_qa_records = eval_results.get("question_answering_records", []) - # Add records with metadata - for qa_idx, qa in enumerate(session_qa_records): + for qa_idx, qa in enumerate(eval_results.get("question_answering_records", [])): qa_records.append(qa) - qa_records_with_metadata.append({ + qa_with_metadata.append({ "user_name": user_name, "session_idx": valid_session_idx, - "original_session_idx": original_idx, "question_idx": qa_idx, "qa_record": qa }) valid_session_idx += 1 - print(f"\nšŸ“Š Data loaded:") + print(f"\nšŸ“Š Data Summary:") print(f" Users: {user_count}") print(f" Sessions: {session_count}") print(f" QA Records: {len(qa_records)}") # Compute metrics - print("\nšŸ”„ Computing metrics...") qa_metrics = compute_qa_metrics(qa_records) time_metrics = compute_time_metrics(results_file) + # Save results + output_dir = Path(results_file).parent + report_file = output_dir / "reme_eval_stat_result.json" + final_results = { "overall_score": { "question_answering": qa_metrics, @@ -232,22 +197,16 @@ def main(input_path: str): "question_answering_records": qa_records } - # Save final report - output_dir = Path(results_file).parent - report_file = output_dir / "reme_eval_stat_result.json" - with open(report_file, "w", encoding="utf-8") as f: json.dump(final_results, f, ensure_ascii=False, indent=4) - print(f"\nāœ… Statistics saved to: {report_file}") + print(f"\nāœ… Results saved to: {report_file}") - # Print summary + # Print metrics print("\n" + "=" * 80) - print("EVALUATION SUMMARY - REME V4") + print("šŸ“Š QUESTION ANSWERING METRICS") print("=" * 80) - - print("\nšŸ“Š Question Answering:") - print(f" Correct (all): {qa_metrics['correct_qa_ratio(all)']:.4f}") + print(f"\n Correct (all): {qa_metrics['correct_qa_ratio(all)']:.4f}") print(f" Hallucination (all): {qa_metrics['hallucination_qa_ratio(all)']:.4f}") print(f" Omission (all): {qa_metrics['omission_qa_ratio(all)']:.4f}") print(f" Correct (valid): {qa_metrics['correct_qa_ratio(valid)']:.4f}") @@ -255,42 +214,56 @@ def main(input_path: str): print(f" Omission (valid): {qa_metrics['omission_qa_ratio(valid)']:.4f}") print(f" Valid/Total: {qa_metrics['qa_valid_num']}/{qa_metrics['qa_num']}") - print(f"\nā±ļø Time Metrics:") + print(f"\nā±ļø TIME METRICS") print(f" Memory Addition: {time_metrics['add_dialogue_duration_time']:.2f} min") print(f" Memory Search: {time_metrics['search_memory_duration_time']:.2f} min") print(f" Total: {time_metrics['total_duration_time']:.2f} min") - # Print non-Correct QA records + # Print error records print("\n" + "=" * 80) - print("NON-CORRECT QA RECORDS") + print("āŒ ERROR RECORDS (Non-Correct)") print("=" * 80) - non_correct_records = [ - record for record in qa_records_with_metadata - if record["qa_record"].get("result_type") not in ["Correct", ""] - ] + error_records = [r for r in qa_with_metadata if r["qa_record"].get("result_type") not in ["Correct", ""]] - if non_correct_records: - print(f"\nFound {len(non_correct_records)} non-correct records:\n") - for record in non_correct_records: - user_name = record["user_name"] - session_idx = record["session_idx"] - original_idx = record["original_session_idx"] - question_idx = record["question_idx"] - qa = record["qa_record"] - result_type = qa.get("result_type", "Unknown") - question = qa.get("question", "N/A") - answer = qa.get("answer", "N/A") - - print(f"šŸ‘¤ User: {user_name}") - print(f"šŸ“… Session: {original_idx} (valid session index: {session_idx})") - print(f"ā“ Question #{question_idx}") - print(f"šŸ·ļø Result Type: {result_type}") - print(f"šŸ’¬ Question: {question}") - print(f"šŸ’” Answer: {answer}") - print("-" * 80) + if not error_records: + print("\nāœ… All QA records are correct!") else: - print("\nāœ… All QA records are Correct!") + print(f"\nFound {len(error_records)} error records:\n") + + for idx, record in enumerate(error_records, 1): + qa = record["qa_record"] + + print(f"\n{'━' * 80}") + print(f"āŒ ERROR #{idx}") + print(f"{'━' * 80}") + print(f"šŸ‘¤ User: {record['user_name']}") + print(f"šŸ“… Session: {record['session_idx']} | Question: {record['question_idx']}") + print(f"šŸ·ļø Result Type: {qa.get('result_type', 'Unknown')}") + print(f"\nā“ Question:") + print(f" {qa.get('question', 'N/A')}") + print(f"\nāœ… Expected Answer:") + print(f" {qa.get('answer', 'N/A')}") + print(f"\nšŸ¤– System Response:") + print(f" {qa.get('system_response', 'N/A')}") + print(f"\nšŸ’­ Reasoning:") + reason = qa.get('question_answering_reasoning', 'N/A') + # Wrap long reasoning text + if len(reason) > 80: + words = reason.split() + lines = [] + current_line = " " + for word in words: + if len(current_line) + len(word) + 1 <= 80: + current_line += word + " " + else: + lines.append(current_line.rstrip()) + current_line = " " + word + " " + if current_line.strip(): + lines.append(current_line.rstrip()) + print("\n".join(lines)) + else: + print(f" {reason}") print("\n" + "=" * 80) @@ -298,30 +271,15 @@ def main(input_path: str): if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser( - description="Compute QA statistics from eval_reme_simple_v4.py results" - ) - parser.add_argument( - "--results_file", - type=str, - required=False, - help="Path to eval_results.jsonl file (e.g., bench_results/reme_simple_v4/eval_results.jsonl)" - ) - parser.add_argument( - "--tmp_dir", - type=str, - required=False, - help="Path to tmp directory (e.g., bench_results/reme_simple_v4/tmp)" - ) + parser = argparse.ArgumentParser(description="Compute QA statistics from eval_reme_simple_v4.py results") + parser.add_argument("--results_file", type=str, help="Path to eval_results.jsonl file") + parser.add_argument("--tmp_dir", type=str, help="Path to tmp directory") args = parser.parse_args() - # Determine input path if args.tmp_dir: - input_path = args.tmp_dir + main(input_path=args.tmp_dir) elif args.results_file: - input_path = args.results_file + main(input_path=args.results_file) else: parser.error("Either --results_file or --tmp_dir must be provided") - - main(input_path=input_path) diff --git a/bench/halumem/eval_reme_simple_v4.py b/bench/halumem/eval_reme_simple_v4.py index c17622a6..825697cb 100644 --- a/bench/halumem/eval_reme_simple_v4.py +++ b/bench/halumem/eval_reme_simple_v4.py @@ -26,7 +26,7 @@ from typing import Any from loguru import logger -from eval_tools import evaluation_for_question2 +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 @@ -239,23 +239,35 @@ class MemoryProcessor: query: str, user_id: str, top_k: int = 20 - ) -> tuple[str, list, float]: + ) -> tuple[dict, list, float]: """ - Search memory using ReMe and return response. + Search memory using ReMe and return structured answer with reasoning. Returns: - tuple: (response, agent_messages, duration_ms) + tuple: (answer_dict, agent_messages, duration_ms) + answer_dict contains: {"reasoning": str, "answer": str, "memories": str} """ start = time.time() - response, agent_messages, success = await self.reme.retrieve_v4( + # Retrieve memories from ReMe + memories_response, agent_messages, success = await self.reme.retrieve_v4( query=query, user_id=user_id, top_k=top_k ) + # Use LLM to generate structured answer from memories + answer_result = await answer_question_with_memories( + question=query, + memories=memories_response, + user_id=user_id + ) + + # Add original memories to the result + answer_result["memories"] = memories_response + duration_ms = (time.time() - start) * 1000 - return response, agent_messages, duration_ms + return answer_result, agent_messages, duration_ms # ==================== Evaluation ==================== @@ -279,19 +291,24 @@ class QuestionAnsweringEvaluator: results = [] for qa in questions: - response, agent_messages, duration_ms = await self.memory_processor.search_memory( + answer_dict, agent_messages, duration_ms = await self.memory_processor.search_memory( query=qa["question"], user_id=user_name, top_k=self.top_k ) + # Extract answer and reasoning from the structured response + system_answer = answer_dict.get("answer", "") + system_reasoning = answer_dict.get("reasoning", "") + retrieved_memories = answer_dict.get("memories", "") + # 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, - response, + system_answer, formatted_dialogue ) @@ -300,7 +317,9 @@ class QuestionAnsweringEvaluator: **qa, "uuid": uuid, "session_id": session_id, - "system_response": response, + "system_response": system_answer, + "system_reasoning": system_reasoning, + "retrieved_memories": retrieved_memories, "retrieve_messages": [m.model_dump() for m in agent_messages], "search_duration_ms": duration_ms, "result_type": eval_result.get("evaluation_result"), diff --git a/bench/halumem/eval_tools.py b/bench/halumem/eval_tools.py index bf68ce2d..8377e87d 100644 --- a/bench/halumem/eval_tools.py +++ b/bench/halumem/eval_tools.py @@ -131,3 +131,40 @@ async def evaluation_for_question2( result = await llm_request_for_json(prompt) return result + + +async def answer_question_with_memories( + question: str, + memories: str, + user_id: str = None, +): + """ + Answer a question using retrieved memories with PROMPT_MEMZERO_JSON template. + + Args: + question: The question to answer + memories: The retrieved memories (formatted as context) + user_id: Optional user ID for context formatting + + Returns: + dict with 'reasoning' and 'answer' fields + """ + # Format context with memories + if user_id: + context = _PROMPTS["TEMPLATE_MEMOS"].format( + user_id=user_id, + memories=memories + ) + else: + context = f"Memories:\n{memories}" + + # Use PROMPT_MEMZERO_JSON template for structured JSON response + prompt = _PROMPTS["PROMPT_MEMZERO_JSON"].format( + context=context, + question=question + ) + + # result = await llm_request_for_json(prompt, model_name="qwen3-max") + result = await llm_request_for_json(prompt, model_name="qwen3-30b-a3b-instruct-2507") + + return result diff --git a/bench/halumem/halumem.yaml b/bench/halumem/halumem.yaml index cd7a2e6f..08c77ecc 100644 --- a/bench/halumem/halumem.yaml +++ b/bench/halumem/halumem.yaml @@ -2,6 +2,30 @@ 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: | You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. diff --git a/reme_ai/core/config/default.yaml b/reme_ai/core/config/default.yaml index 5891d180..c175e6a9 100644 --- a/reme_ai/core/config/default.yaml +++ b/reme_ai/core/config/default.yaml @@ -17,6 +17,7 @@ llm: backend: openai model_name: qwen3-30b-a3b-instruct-2507 max_concurrency: 20 + temperature: 0.0001 qwen3_max_instruct: backend: openai diff --git a/reme_ai/mem_agent/base_memory_agent.py b/reme_ai/mem_agent/base_memory_agent.py index 15c6dc04..d53cc8d9 100644 --- a/reme_ai/mem_agent/base_memory_agent.py +++ b/reme_ai/mem_agent/base_memory_agent.py @@ -22,7 +22,7 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta): tools: list[BaseMemoryTool], add_think_tool: bool = False, # only for instruct model tool_call_interval: float = 0, - max_steps: int = 20, + max_steps: int = 8, **kwargs, ): tools = tools or [] @@ -35,10 +35,11 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta): self.max_steps: int = max_steps self.messages: list[Message] = [] + self.tool_messages: list[Message] = [] self.success: bool = True - self.retrieved_nodes: list[MemoryNode] = [] self.memory_nodes: list[MemoryNode | str] = [] + self.meta_info: str = "" def _build_tool_call(self) -> ToolCall: return ToolCall( @@ -97,35 +98,37 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta): """Builds and returns the initial messages for the agent.""" return self.get_messages() - async def _reasoning_step(self, messages: list[Message], step: int, **kwargs) -> tuple[Message, bool]: + async def _reasoning_step(self, messages: list[Message], step: int, stage: str = "", **kwargs) -> tuple[Message, bool]: assistant_message: Message = await self.llm.chat( messages=messages, tools=[t.tool_call for t in self.tools], **kwargs, ) messages.append(assistant_message) + stage_prefix = f"-{stage}" if stage else "" logger.info( - f"[{self.__class__.__name__}] " + f"[{self.__class__.__name__}{stage_prefix}] " f"step{step + 1}.assistant={assistant_message.simple_dump(enable_json_dump=True)}", ) should_act = bool(assistant_message.tool_calls) return assistant_message, should_act - async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]: + async def _acting_step(self, assistant_message: Message, step: int, stage: str = "", **kwargs) -> list[Message]: if not assistant_message.tool_calls: return [] tool_list: list[BaseMemoryTool] = [] tool_result_messages: list[Message] = [] tool_dict = {t.tool_call.name: t for t in self.tools} + stage_prefix = f"-{stage}" if stage else "" for j, tool_call in enumerate(assistant_message.tool_calls): if tool_call.name not in tool_dict: - logger.warning(f"[{self.__class__.__name__}] unknown tool_call.name={tool_call.name}") + logger.warning(f"[{self.__class__.__name__}{stage_prefix}] unknown tool_call.name={tool_call.name}") continue logger.info( - f"[{self.__class__.__name__}] step{step + 1}.{j} " + f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} " f"submit tool_calls={tool_call.name} argument={tool_call.arguments}", ) tool_copy: BaseMemoryTool = tool_dict[tool_call.name].copy() @@ -143,7 +146,7 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta): self.memory_nodes.extend(op.memory_nodes) if hasattr(op, "messages") and op.messages: - self.messages.extend(op.messages) + self.tool_messages.extend(op.messages) tool_result = str(op.output) tool_message = Message( @@ -152,20 +155,27 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta): tool_call_id=op.tool_call.id, ) tool_result_messages.append(tool_message) - logger.info(f"[{self.__class__.__name__}] step{step + 1}.{j} join tool_result={tool_result[:2000]}...\n\n") + + # # Collect tool call information to meta_info + # tool_info = f"\n## Tool Call {step + 1}.{j + 1}: {op.tool_call.name}\n" + # tool_info += f"Arguments: {json.dumps(assistant_message.tool_calls[j].argument_dict, ensure_ascii=False)}\n" + # tool_info += f"Result: {tool_result}\n" + self.meta_info += tool_result + "\n" + + logger.info(f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} join tool_result={tool_result[:2000]}...\n\n") return tool_result_messages - async def react(self, messages: list[Message]): + async def react(self, messages: list[Message], stage: str = ""): """Performs reasoning and acting steps until completion or max steps reached.""" success: bool = False for step in range(self.max_steps): - assistant_message, should_act = await self._reasoning_step(messages, step) + assistant_message, should_act = await self._reasoning_step(messages, step, stage=stage) if not should_act: success = True break - tool_result_messages = await self._acting_step(assistant_message, step) + tool_result_messages = await self._acting_step(assistant_message, step, stage=stage) messages.extend(tool_result_messages) return messages, success diff --git a/reme_ai/mem_agent/v4/personal_retriever_v4.py b/reme_ai/mem_agent/v4/personal_retriever_v4.py index 2b33897e..5135acd9 100644 --- a/reme_ai/mem_agent/v4/personal_retriever_v4.py +++ b/reme_ai/mem_agent/v4/personal_retriever_v4.py @@ -9,32 +9,25 @@ class PersonalRetrieverV4(BaseMemoryAgent): 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 = format_messages(self.context.messages) - else: + context = self.context.query if self.context.get("query") else format_messages(self.context.messages) if self.context.get("messages") else None + if not context: raise ValueError("input must have either `query` or `messages`") - read_profile_tool = ReadUserProfile() + read_profile_tool = ReadUserProfile(show_ids="history") await read_profile_tool.call(memory_type=self.memory_type.value, memory_target=self.memory_target) + self.context.user_profile = user_profile = read_profile_tool.output - messages = [ - 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=read_profile_tool.output, - context=context, - )), + return [ Message( role=Role.USER, - content=self.get_prompt("user_message"), - ), + content=self.prompt_format( + prompt_name="user_message", + memory_type=self.memory_type.value, + memory_target=self.memory_target, + user_profile=user_profile, + context=context, + )) ] - return messages async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]: return await super()._acting_step( @@ -44,3 +37,16 @@ class PersonalRetrieverV4(BaseMemoryAgent): memory_target=self.memory_target, **kwargs, ) + + async def execute(self): + """Execute the retriever and determine success based on output markers.""" + await super().execute() + + # Check for memory found/not found markers in the output + if self.output: + if "" in self.output: + self.success = True + elif "" in self.output: + self.success = False + + self.meta_info = self.context.user_profile + "\n" + self.meta_info \ No newline at end of file diff --git a/reme_ai/mem_agent/v4/personal_retriever_v4.yaml b/reme_ai/mem_agent/v4/personal_retriever_v4.yaml index cc5dcc34..85f7e013 100644 --- a/reme_ai/mem_agent/v4/personal_retriever_v4.yaml +++ b/reme_ai/mem_agent/v4/personal_retriever_v4.yaml @@ -1,7 +1,7 @@ tool: | Retrieve relevant personal memories to answer user questions through vector search and history reading. -system_prompt: | +user_message: | You are a memory agent managing **{memory_type}** memories about **{memory_target}**. ## User Profile @@ -10,28 +10,23 @@ system_prompt: | ## Question {context} - ## Retrieval Strategy + ## Task + Search for relevant memories to answer the question above. - **Tool 1: Vector Search (`retrieve_memory`) - - Try at least 3-5 different queries before moving to next tool: + **Tool 1: Vector Search (`retrieve_memory`)** + - Try at least 3-5 different queries: * Direct question * Reformulated phrasings * Entity-focused queries * Different keyword combinations - - If no results: retry with different time ranges or remove time constraints: [start, end] in YYYYMMDD format + - If no results: retry with different time ranges [start, end] in YYYYMMDD format * Example: [20200101, 20200102] for 20200101 <= time <= 20200102 * Single-sided: [0, 20200102] or [20200101, 99999999] **Tool 2: Read Context (`read_history`) - ONLY AFTER Tool 1** - - Use this ONLY after completing multiple retrieve_memory attempts - Use history_id from retrieved memories to read original conversations - - Prioritize most relevant or recent entries - Read multiple if needed for complete context **Response** - - **CRITICAL: Answer ONLY based on retrieved memories. Do NOT hallucinate or infer information not present in the search results.** - - Try multiple angles before giving up - - State "nothing found after thorough search" if nothing found after thorough search - -user_message: | - Answer the question using the retrieval strategy. + - If found relevant memories: respond exactly `` + - If no memory found after thorough search: respond exactly `` diff --git a/reme_ai/mem_agent/v4/personal_summarizer_v4.py b/reme_ai/mem_agent/v4/personal_summarizer_v4.py index 4b867bed..c0e1d4f1 100644 --- a/reme_ai/mem_agent/v4/personal_summarizer_v4.py +++ b/reme_ai/mem_agent/v4/personal_summarizer_v4.py @@ -13,17 +13,13 @@ class PersonalSummarizerV4(BaseMemoryAgent): history_node: MemoryNode = self.context.history_node messages = [ Message( - role=Role.SYSTEM, + role=Role.USER, content=self.prompt_format( - prompt_name="system_prompt_phase1", + prompt_name="user_message_phase1", 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_phase1"), - ), ] return messages @@ -32,25 +28,22 @@ class PersonalSummarizerV4(BaseMemoryAgent): history_node: MemoryNode = self.context.history_node messages = [ Message( - role=Role.SYSTEM, + role=Role.USER, content=self.prompt_format( - prompt_name="system_prompt_phase2", + prompt_name="user_message_phase2", context=history_node.content, memory_type=self.memory_type.value, memory_target=self.memory_target, user_profile=user_profile, )), - Message( - role=Role.USER, - content=self.get_prompt("user_message_phase2"), - ), ] return messages - async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]: + async def _acting_step(self, assistant_message: Message, step: int, stage: str = "", **kwargs) -> list[Message]: return await super()._acting_step( assistant_message, step, + stage=stage, memory_type=self.memory_type.value, memory_target=self.memory_target, history_node=self.history_node, @@ -68,7 +61,7 @@ class PersonalSummarizerV4(BaseMemoryAgent): ) # Phase 1: AddSummaryMemory - logger.info(f"[{self.__class__.__name__}] Starting Phase 1: AddSummaryMemory") + logger.info(f"[{self.__class__.__name__}-S1] Starting Phase 1: AddSummaryMemory") # Filter tools for phase 1 (only AddSummaryMemory) original_tools = self.tools.copy() @@ -77,16 +70,16 @@ class PersonalSummarizerV4(BaseMemoryAgent): messages_phase1 = await self.build_messages_phase1() for i, message in enumerate(messages_phase1): logger.info( - f"[{self.__class__.__name__}] phase1.step0.{i} {message.role} " + f"[{self.__class__.__name__}-S1] phase1.step0.{i} {message.role} " f"{message.simple_dump(enable_json_dump=True)}", ) - messages_phase1, success_phase1 = await self.react(messages_phase1) + messages_phase1, success_phase1 = await self.react(messages_phase1, stage="S1") if not success_phase1: - logger.warning(f"[{self.__class__.__name__}] Phase 1 did not complete successfully") + logger.warning(f"[{self.__class__.__name__}-S1] Phase 1 did not complete successfully") # Phase 2: Read user profile and UpdateUserProfile - logger.info(f"[{self.__class__.__name__}] Starting Phase 2: UpdateUserProfile") + logger.info(f"[{self.__class__.__name__}-S2] Starting Phase 2: UpdateUserProfile") # Restore original tools and get ReadUserProfile tool self.tools = original_tools @@ -94,13 +87,17 @@ class PersonalSummarizerV4(BaseMemoryAgent): user_profile = "" if read_profile_tool: - # Call ReadUserProfile to load current profile - logger.info(f"[{self.__class__.__name__}] Loading user profile with ReadUserProfile") - await read_profile_tool.call(memory_type=self.memory_type.value, memory_target=self.memory_target) + # Call ReadUserProfile to load current profile (only show profile_id, not history_id) + logger.info(f"[{self.__class__.__name__}-S2] Loading user profile with ReadUserProfile") + await read_profile_tool.call( + memory_type=self.memory_type.value, + memory_target=self.memory_target, + show_ids="profile", + ) user_profile = str(read_profile_tool.output) - logger.info(f"[{self.__class__.__name__}] User profile loaded: {user_profile}...") + logger.info(f"[{self.__class__.__name__}-S2] User profile loaded: {user_profile}...") else: - logger.warning(f"[{self.__class__.__name__}] ReadUserProfile tool not found") + logger.warning(f"[{self.__class__.__name__}-S2] ReadUserProfile tool not found") # Filter tools for phase 2 (only UpdateUserProfile) self.tools = [t for t in self.tools if t.tool_call.name == "update_user_profile"] @@ -108,11 +105,11 @@ class PersonalSummarizerV4(BaseMemoryAgent): messages_phase2 = await self.build_messages_phase2(user_profile) for i, message in enumerate(messages_phase2): logger.info( - f"[{self.__class__.__name__}] phase2.step0.{i} {message.role} " + f"[{self.__class__.__name__}-S2] phase2.step0.{i} {message.role} " f"{message.simple_dump(enable_json_dump=True)}", ) - messages_phase2, success_phase2 = await self.react(messages_phase2) + messages_phase2, success_phase2 = await self.react(messages_phase2, stage="S2") # Restore original tools self.tools = original_tools diff --git a/reme_ai/mem_agent/v4/personal_summarizer_v4.yaml b/reme_ai/mem_agent/v4/personal_summarizer_v4.yaml index 41d3ac61..5e7d12b7 100644 --- a/reme_ai/mem_agent/v4/personal_summarizer_v4.yaml +++ b/reme_ai/mem_agent/v4/personal_summarizer_v4.yaml @@ -2,7 +2,7 @@ tool: | Extract and update personal memories about the user from conversation context. Identify preferences, habits, background, relationships, and key facts. -system_prompt_phase1: | +user_message_phase1: | You are a memory agent managing **{memory_type}** memories about **{memory_target}**. ## Latest Conversation: @@ -17,11 +17,10 @@ system_prompt_phase1: | Summarize all important information about **{memory_target}** - Set `conversation_time` (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable) -user_message_phase1: | Extract personal memories from the conversation using `AddSummaryMemory`. # capturing complete contexts with preconditions, causes, and consequences -system_prompt_phase2: | +user_message_phase2: | You are a memory agent managing **{memory_type}** memories about **{memory_target}**. ## Latest Conversation: @@ -36,10 +35,15 @@ system_prompt_phase2: | ## Task: Update Profile with `UpdateUserProfile` - 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. - - Maintain profiles that are concise, mutually exclusive, and collectively comprehensive with no information loss. + Synchronize profile with new information from the conversation: + - `profile_ids_to_delete`: Remove conflicting, or redundant entries (array of profile IDs). + - `profiles_to_add`: + - `conversation_time`: Time of conversation (format: `YYYY-MM-DD HH:MM:SS`, e.g., `2024-01-15 14:30:00`) + - `profile_content`: Complete, self-contained profile description with full context + + **Profile Requirements**: + - One user profile entry records one dimension of the user portrait, and MUST be complete and self-contained with all necessary context (preconditions, causes, and consequences) + - All profiles MUST be mutually exclusive (non-overlapping) and non-conflicting + - Profiles should collectively be comprehensive with no information loss -user_message_phase2: | Update user profile using `UpdateUserProfile` based on the conversation and current profile. diff --git a/reme_ai/mem_agent/v4/reme_retriever_v4.py b/reme_ai/mem_agent/v4/reme_retriever_v4.py index b8d5fbbe..7dbf2b8a 100644 --- a/reme_ai/mem_agent/v4/reme_retriever_v4.py +++ b/reme_ai/mem_agent/v4/reme_retriever_v4.py @@ -11,6 +11,7 @@ class ReMeRetrieverV4(BaseMemoryAgent): def __init__(self, meta_memories: list[dict] | None = None, **kwargs): super().__init__(**kwargs) self.meta_memories: list[dict] = meta_memories or [] + self.meta_info_dict: dict[str, str] = {} async def _read_meta_memories(self) -> str: from ...mem_tool import ReadMetaMemory @@ -44,10 +45,73 @@ class ReMeRetrieverV4(BaseMemoryAgent): return messages async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]: - return await super()._acting_step( - assistant_message, - step, - query=self.context.get("query", ""), - messages=self.context.get("messages", []), - **kwargs, - ) + import asyncio + from ...mem_tool.v4 import HandsOff + + if not assistant_message.tool_calls: + return [] + + tool_list: list = [] + tool_result_messages: list[Message] = [] + tool_dict = {t.tool_call.name: t for t in self.tools} + stage_prefix = "" + + # Add required context parameters + kwargs["query"] = self.context.get("query", "") + kwargs["messages"] = self.context.get("messages", []) + + for j, tool_call in enumerate(assistant_message.tool_calls): + if tool_call.name not in tool_dict: + logger.warning(f"[{self.__class__.__name__}{stage_prefix}] unknown tool_call.name={tool_call.name}") + continue + + logger.info( + f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} " + f"submit tool_calls={tool_call.name} argument={tool_call.arguments}", + ) + tool_copy = tool_dict[tool_call.name].copy() + tool_copy.tool_call.id = tool_call.id + tool_list.append(tool_copy) + kwargs.update(tool_call.argument_dict) + self.submit_async_task(tool_copy.call, retrieved_nodes=self.retrieved_nodes, **kwargs) + if self.tool_call_interval > 0: + await asyncio.sleep(self.tool_call_interval) + + await self.join_async_tasks() + + for j, op in enumerate(tool_list): + if op.memory_nodes: + self.memory_nodes.extend(op.memory_nodes) + + if hasattr(op, "messages") and op.messages: + self.tool_messages.extend(op.messages) + + # Collect meta_info_dict from HandsOff tool + if isinstance(op, HandsOff) and hasattr(op, "meta_info_dict"): + self.meta_info_dict.update(op.meta_info_dict) + logger.info(f"Collected meta_info_dict from HandsOff: {len(op.meta_info_dict)} entries") + + tool_result = str(op.output) + tool_message = Message( + role=Role.TOOL, + content=tool_result, + tool_call_id=op.tool_call.id, + ) + tool_result_messages.append(tool_message) + + self.meta_info += tool_result + "\n" + + logger.info(f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} join tool_result={tool_result[:2000]}...\n\n") + + return tool_result_messages + + async def execute(self): + await super().execute() + + # Assemble meta_info_dict into output + if self.meta_info_dict: + output_parts = [] + for key, value in self.meta_info_dict.items(): + output_parts.append(f"## {key}\n{value}") + self.output = "\n\n".join(output_parts) + logger.info(f"Assembled output from meta_info_dict with {len(self.meta_info_dict)} entries") \ No newline at end of file diff --git a/reme_ai/mem_tool/v4/hands_off.py b/reme_ai/mem_tool/v4/hands_off.py index 54b61f6d..17a33429 100644 --- a/reme_ai/mem_tool/v4/hands_off.py +++ b/reme_ai/mem_tool/v4/hands_off.py @@ -20,6 +20,7 @@ class HandsOff(BaseMemoryTool): self.sub_ops: list[BaseMemoryAgent] = [a for a in self.sub_ops if isinstance(a, BaseMemoryAgent)] self.messages: list[Message] = [] + self.meta_info_dict: dict[str, str] = {} @property def memory_agent_dict(self) -> dict[MemoryType, "BaseMemoryAgent"]: @@ -105,6 +106,8 @@ class HandsOff(BaseMemoryTool): self.memory_nodes.extend(agent.memory_nodes) if agent.messages: self.messages.extend(agent.messages) + if agent.meta_info: + self.meta_info_dict[f"{memory_type.value} {memory_target}"] = agent.meta_info results.append(f"{memory_type.value} {memory_target} agent result: {agent.output}") diff --git a/reme_ai/mem_tool/v4/read_history.py b/reme_ai/mem_tool/v4/read_history.py index e9ab2a15..78a90eb4 100644 --- a/reme_ai/mem_tool/v4/read_history.py +++ b/reme_ai/mem_tool/v4/read_history.py @@ -34,5 +34,5 @@ class ReadHistory(BaseMemoryTool): return memory = MemoryNode.from_vector_node(nodes[0]) - self.output = memory.content + self.output = f"### Historical Dialogue\n{memory.content}" logger.info(f"Successfully read history memory: {history_id}") diff --git a/reme_ai/mem_tool/v4/read_user_profile.py b/reme_ai/mem_tool/v4/read_user_profile.py index b4aae557..aa8a57f8 100644 --- a/reme_ai/mem_tool/v4/read_user_profile.py +++ b/reme_ai/mem_tool/v4/read_user_profile.py @@ -1,3 +1,4 @@ +from typing import Literal from loguru import logger from ..base_memory_tool import BaseMemoryTool @@ -6,10 +7,11 @@ from ...core.schema.memory_node import MemoryNode class ReadUserProfile(BaseMemoryTool): - def __init__(self, add_memory_type_target: bool = False, **kwargs): + def __init__(self, add_memory_type_target: bool = False, show_ids: Literal["both", "profile", "history", "none"] = "both", **kwargs): kwargs["enable_multiple"] = False super().__init__(**kwargs) self.add_memory_type_target = add_memory_type_target + self.show_ids = show_ids def _build_tool_description(self) -> str: return "Read user profile." @@ -37,12 +39,16 @@ class ReadUserProfile(BaseMemoryTool): "required": [], } - async def execute(self): + async def execute(self): + # Determine which IDs to show + show_profile_id = self.show_ids in ("both", "profile") + show_history_id = self.show_ids in ("both", "history") + cache_key = f"{self.memory_type}_{self.memory_target}".replace(" ", "_").lower() cached_data = self.meta_memory.load(cache_key, auto_clean=False) if not cached_data: - self.output = "" + self.output = "### User Profile\nNo user profile found." logger.info(f"empty cached_data={cache_key}") return @@ -51,12 +57,25 @@ class ReadUserProfile(BaseMemoryTool): memory_formated = [] for node in memory_nodes: - node_formated = f"profile_id={node.memory_id} profile_content={node.content}" + node_formated_parts = [] + + # Add profile_id if enabled + if show_profile_id: + node_formated_parts.append(f"profile_id={node.memory_id}") + + # Always add profile_content + node_formated_parts.append(f"profile_content={node.content}") + + # Add conversation_time if available if "conversation_time" in node.metadata and node.metadata["conversation_time"]: - node_formated += f" conversation_time={node.metadata['conversation_time']}" - if node.ref_memory_id: - node_formated += f" history_id={node.ref_memory_id}" + node_formated_parts.append(f"conversation_time={node.metadata['conversation_time']}") + + # Add history_id if enabled and available + if show_history_id and node.ref_memory_id: + node_formated_parts.append(f"history_id={node.ref_memory_id}") + + node_formated = " ".join(node_formated_parts) memory_formated.append(node_formated.strip()) - self.output = "\n".join(memory_formated) + self.output = "### User Profile\n" + "\n".join(memory_formated) logger.info(f"Read {len(memory_formated)} nodes from cache key: {cache_key}") diff --git a/reme_ai/mem_tool/v4/retrieve_memory.py b/reme_ai/mem_tool/v4/retrieve_memory.py index e7f77f96..a8138bdc 100644 --- a/reme_ai/mem_tool/v4/retrieve_memory.py +++ b/reme_ai/mem_tool/v4/retrieve_memory.py @@ -98,6 +98,6 @@ class RetrieveMemory(BaseMemoryTool): if node.ref_memory_id: line += f"history_id={node.ref_memory_id} " output.append(line.strip()) - self.output = "\n".join(output) + self.output = "### Extracted Memories\n" + "\n".join(output) logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication") diff --git a/reme_ai/mem_tool/v4/update_user_profile.py b/reme_ai/mem_tool/v4/update_user_profile.py index 085daf80..1b9c5c97 100644 --- a/reme_ai/mem_tool/v4/update_user_profile.py +++ b/reme_ai/mem_tool/v4/update_user_profile.py @@ -80,7 +80,7 @@ class UpdateUserProfile(BaseMemoryTool): memory_target=self.memory_target, when_to_use="", content=mem.get("profile_content", ""), - ref_memory_id=self.ref_memory_id, + ref_memory_id=self.history_node.memory_id, author=self.author, metadata={"conversation_time": mem.get("conversation_time", "")}, ) diff --git a/reme_ai/reme.py b/reme_ai/reme.py index 02485159..d0a71e6f 100644 --- a/reme_ai/reme.py +++ b/reme_ai/reme.py @@ -458,7 +458,7 @@ class ReMe(Application): ) await reme_summarizer_v4.call(messages=messages, description=description, **kwargs) - return reme_summarizer_v4.memory_nodes, reme_summarizer_v4.messages, reme_summarizer_v4.success + return reme_summarizer_v4.memory_nodes, reme_summarizer_v4.tool_messages, reme_summarizer_v4.success else: raise NotImplementedError @@ -499,7 +499,7 @@ class ReMe(Application): ) await reme_retriever_v4.call(query=query, messages=messages, description=description, **kwargs) - return reme_retriever_v4.output, reme_retriever_v4.messages, reme_retriever_v4.success + return reme_retriever_v4.output, reme_retriever_v4.tool_messages, reme_retriever_v4.success else: raise NotImplementedError