feat(memory): 添加用户配置文件增删功能并优化内存管理

- 新增 AddProfile 工具类用于添加用户配置文件
- 新增 DeleteProfile 工具类用于删除指定ID的配置文件
- 从 __init__.py 中移除已废弃的 UpdateProfileFilterOlder 工具
- 在 reme.py 中注释掉 AddProfile 和 DeleteProfile 的导入
- 从 reme.py 的工具列表中移除 UpdateProfileFilterOlder 相关代码
- 优化 ES 和 Qdrant 向量存储客户端初始化的日志记录
- 更新个人记忆检索器和摘要器的 YAML 配置文件格式
- 修复 personal_halumem_summarizer.py 中的参数传递格式问题
- 在 UpdateProfile 工具中添加按 memory_target 分组的功能
This commit is contained in:
方应 2026-01-30 17:03:58 +08:00
parent 74238ba9bd
commit 07905a0358
11 changed files with 150 additions and 35 deletions

View file

@ -1,7 +1,7 @@
<p align="center">
<img src="docs/_static/figure/reme_logo.png" alt="ReMe Logo" width="50%">
</p>
<p align="center">
<a href="https://pypi.org/project/reme-ai/"><img src="https://img.shields.io/badge/python-3.10+-blue" alt="Python Version"></a>
<a href="https://pypi.org/project/reme-ai/"><img src="https://img.shields.io/pypi/v/reme-ai.svg?logo=pypi" alt="PyPI Version"></a>

View file

@ -44,27 +44,27 @@ user_message: |
## User Question
{context}
# Core Objective:
Before responding to the user, you must strictly follow the **[Memory Retrieve -> Original Source Tracing -> Broad Search Fallback]** retrieval strategy. It is strictly forbidden to directly opt for an indiscriminate search of massive historical original texts.
## Retrieval Strategy & Workflow (Strictly Enforced Chain of Thought)
### Phase 1: Intent Decomposition and Primary Retrieval (Summary First)
1. **Analyze Intent**: Analyze the user's current Query, decomposing it into 1-3 core search intents.2. **Summary Priority**: First, retrieve from **high-level memories**.
- **Action**: Call `vector_retrieve_memory` using at least two different `query`.
- **Filter**: (Optional) Set metadata filter {{"timestamp": "YYYY-MM-DD"}}
- **Goal**: Obtain refined conclusions such as entity attributes, task status, user preferences, or environmental information.
### Phase 2: Memory Evaluation and Deep Tracing (Drill Down)
Check the retrieval results of Phase 1:
- **Case A (Sufficient Information)**: If the summarized memory contains all the details needed for the answer, proceed directly to Phase 4 for the response.
- **Case B (Vague/Complex Information)**: If summarized memory exists (e.g., 'discussed project architecture') but lacks specific details (e.g., 'specific parameter configuration'), use clues from the summary to trace the original text.
- **Action**: Call `read_history` using ref_memory_id from the retrieved memory.
- **Goal**: Obtain the specific conversation context at that time.
### Phase 3: Fallback Retrieval and Strategy Adjustment (Fallback & Expand)
If no valid information is found in both Phase 1 and Phase 2 (result is empty or similarity is too low): Rewrite the Query based on the context (remove non-keywords, synonym substitution), and search again.
### Phase 4: Result Compilation and Response
- Combine the retrieved content (summary or original text) with the current conversation context.
- If all retrieved results are irrelevant, **it is strictly forbidden to fabricate memories**; directly inform the user that no relevant information was found.

View file

@ -113,9 +113,8 @@ class PersonalHalumemSummarizer(BaseMemoryAgent):
else:
all_profiles = ""
stage = "s2-profile"
messages_s2 = await self._build_s2_messages(user_profile = all_profiles)
messages_s2 = await self._build_s2_messages(user_profile=all_profiles)
for i, message in enumerate(messages_s2):
role = message.name or message.role
logger.info(f"[{self.__class__.__name__} {stage}] role={role} {message.simple_dump(as_dict=False)}")

View file

@ -21,19 +21,19 @@ user_message_s1: |
## Latest Conversation
Format: round<index> [<timestamp>] <role/name>: <content>
{context}
## Task
### Step 1: Create Memory
- At this step, you can call the tool multiple times to store memories, or you can call it once to store multiple memories.
### Step 2: Update Memory Store
- Update the vector store using `update_memory` to keep it well-organized and consolidated.
## Storage Scope (Biographical & Behavioral ONLY)
- Personal Biography: Significant milestones, past experiences, and life events.
- Behavioral Patterns: How the agent reacts, specific actions taken, and recurring habits.
- **EXCLUSION**: DO NOT record objective world facts, general knowledge, or user-specific health/states.
Extraction & Formatting Rules
- Fact Filtering: Only extract information that builds the biography of **{memory_target}**.
- Subject Splitting: If a conversation mentions multiple subject (e.g., the User's childhood and their Father's career), create separate memory entries for each subject.
@ -65,14 +65,14 @@ user_message_s2: |
## Current User Profile:
{user_profile}
## Task
### Step 1: ADD Profile
- Add new, relevant, and up-to-date information to the user profile using `update_profile` (via `profiles_to_add`).
### Step 2: DELETE Profile
- Delete outdated, redundant, or resolved states from the user profile using `update_profile` (via `profile_ids_to_delete`).
## Storage Scope (Current States ONLY)
- **EXCLUSION PRINCIPLE**: DO NOT record any user *actions*, *requests*, *queries*, or *interactions with the system* (e.g., "asked for code", "solved a puzzle", "requested translation"). These are interaction logs, not user states.
- Identity: Geography, job title, work content, income.
@ -83,13 +83,13 @@ user_message_s2: |
- Subject Splitting (CRITICAL): If the conversation mentions multiple subjects (e.g., the User's job and their Spouse's health), you MUST create separate profile entries for each unique subject.
- Conflict Resolution: Use profile_ids_to_delete to remove outdated, redundant, or resolved states (e.g., if a user is "Recovered," delete the "Illness" entry).
- Each profile entry MUST describe a **persistent or temporary state of the user themselves** (e.g., who they are, what they like, what they’re dealing with), NOT an event they participated in or a request they made.
## Profile Format
- **ONLY record what the user EXPLICITLY STATES about themselves as a state or preference.**
- The key in the record represents the category of memory, and the value should record the specific content. For example:
{{"message_time": "YYYY-MM-DD HH:MM:SS", "profile_key": "the category of memory", "profile_value": "content" }}
- When there is no information conflict or outdated information, you don't need to delete any of the memory. If there is no information that meets the requirements, it is also acceptable not to add it.
## Forbidden Case
1.There is no need to record user behavior: {{ "profile_key": "workouts", "profile_content": "confident in new running shoes' suitability for chosen route; they have significantly improved morning jogs"}}
2. There is no need to record the users' plans or requirements: {{ "profile_key": "plans.vacation", "profile_content": "planning to go to a nearby city for a week and ask for a job change"}}

View file

@ -80,7 +80,7 @@ class ESVectorStore(BaseVectorStore):
async def _get_client(self) -> AsyncElasticsearch:
"""Create or return the existing AsyncElasticsearch client.
This lazy initialization ensures the client is created in the correct event loop.
"""
if self._client is None:
@ -93,7 +93,7 @@ class ESVectorStore(BaseVectorStore):
headers=self.headers,
)
logger.info("AsyncElasticsearch client initialized")
return self._client
async def list_collections(self) -> list[str]:

View file

@ -109,7 +109,7 @@ class QdrantVectorStore(BaseVectorStore):
async def _get_client(self) -> AsyncQdrantClient:
"""Create or return the existing AsyncQdrantClient.
This lazy initialization ensures the client is created in the correct event loop.
"""
if self._client is None:
@ -125,7 +125,7 @@ class QdrantVectorStore(BaseVectorStore):
**self.client_kwargs,
)
logger.info("AsyncQdrantClient initialized")
return self._client
async def list_collections(self) -> list[str]:

View file

@ -32,9 +32,8 @@ from .tool.memory import (
UpdateMemoryV2,
AddDraftAndReadAllProfiles,
UpdateProfile,
UpdateProfileFilterOlder,
DeleteProfile,
AddProfile,
# DeleteProfile,
# AddProfile,
AddHistory,
ReadAllProfiles,
AddMemory,
@ -110,7 +109,6 @@ class ReMe(Application):
task_name: str | list[str] = "",
tool_name: str | list[str] = "",
enable_thinking_params: bool = True,
enable_time_filter: bool = True,
version: str = "default",
retrieve_top_k: int = 20,
return_dict: bool = False,
@ -181,24 +179,18 @@ class ReMe(Application):
top_k=retrieve_top_k,
),
UpdateMemoryV2(
enable_thinking_params=enable_thinking_params
enable_thinking_params=enable_thinking_params,
),
# RetrieveMemory(
# enable_thinking_params=enable_thinking_params,
# top_k=retrieve_top_k,
# enable_time_filter=enable_time_filter,
# ),
# 处理userprofile
ReadAllProfiles(
enable_thinking_params=enable_thinking_params,
profile_dir=self.profile_dir,
),
# UpdateProfileFilterOlder(
# enable_thinking_params=enable_thinking_params,
# max_profile_count=50,
# profile_dir=self.profile_dir,
# ),
UpdateProfile(
enable_thinking_params=enable_thinking_params,
profile_dir=self.profile_dir,
@ -328,7 +320,7 @@ class ReMe(Application):
top_k=retrieve_top_k,
enable_thinking_params=enable_thinking_params,
enable_time_filter=enable_time_filter,
enable_multiple=True
enable_multiple=True,
),
ReadHistory(enable_thinking_params=enable_thinking_params),
],
@ -346,7 +338,7 @@ class ReMe(Application):
enable_time_filter=enable_time_filter,
),
ReadHistory(enable_thinking_params=enable_thinking_params),
]
],
)
else:
raise NotImplementedError

View file

@ -9,7 +9,6 @@ from .profiles.profile_handler import ProfileHandler
from .profiles.read_all_profiles import ReadAllProfiles
from .profiles.add_profile import AddProfile
from .profiles.update_profile import UpdateProfile
from .profiles.update_profile_filter_older import UpdateProfileFilterOlder
from .profiles.delete_profile import DeleteProfile
from .vector.add_draft_and_retrieve_similar_memory import AddAndRetrieveSimilarMemory
from .vector.add_memory import AddMemory
@ -30,10 +29,10 @@ __all__ = [
"ReadHistory",
# Profiles
"AddDraftAndReadAllProfiles",
"AddProfile",
"ProfileHandler",
"ReadAllProfiles",
"UpdateProfile",
"UpdateProfileFilterOlder",
"DeleteProfile",
# Vector
"AddAndRetrieveSimilarMemory",

View file

@ -0,0 +1,71 @@
"""Add user profile tool"""
from loguru import logger
from .profile_handler import ProfileHandler
from ..base_memory_tool import BaseMemoryTool
from ....core.schema import ToolCall
class AddProfile(BaseMemoryTool):
"""Tool to add a single profile entry"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
def _build_tool_call(self) -> ToolCall:
"""Build and return the tool call schema"""
return ToolCall(
**{
"description": "Add a new profile entry for the user.",
"parameters": {
"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"],
},
},
)
async def execute(self):
profile_handler = ProfileHandler(profile_path=self.profile_path, memory_target=self.memory_target)
# Get parameters
message_time = self.context.get("message_time", "")
profile_key = self.context.get("profile_key", "")
profile_value = self.context.get("profile_value", "")
if not profile_key or not profile_value:
return "Missing required parameters (profile_key or profile_value), operation cancelled."
# Build profile dict
profile = {
"message_time": message_time,
"profile_key": profile_key,
"profile_value": profile_value,
}
# Add profile using ProfileHandler
new_nodes = profile_handler.add_batch(profiles=[profile], ref_memory_id=self.history_id)
self.memory_nodes.extend(new_nodes)
if new_nodes:
output = f"Successfully added profile: [{profile_key}] = {profile_value}"
logger.info(output)
return output
else:
output = "Failed to add profile."
logger.warning(output)
return output

View file

@ -0,0 +1,53 @@
"""Delete user profile tool"""
from loguru import logger
from .profile_handler import ProfileHandler
from ..base_memory_tool import BaseMemoryTool
from ....core.schema import ToolCall
class DeleteProfile(BaseMemoryTool):
"""Tool to delete a single profile entry by ID"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
def _build_tool_call(self) -> ToolCall:
"""Build and return the tool call schema"""
return ToolCall(
**{
"description": "Delete a profile entry by profile ID.",
"parameters": {
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The unique ID of the profile to delete.",
},
},
"required": ["profile_id"],
},
},
)
async def execute(self):
profile_handler = ProfileHandler(profile_path=self.profile_path, memory_target=self.memory_target)
# Get profile_id parameter
profile_id = self.context.get("profile_id", "")
if not profile_id:
return "No profile_id provided, operation cancelled."
# Delete profile using ProfileHandler
success = profile_handler.delete(profile_id)
if success:
output = f"Successfully deleted profile with ID: {profile_id}"
logger.info(output)
return output
else:
output = f"Profile with ID '{profile_id}' not found."
logger.warning(output)
return output

View file

@ -90,6 +90,7 @@ class UpdateProfile(BaseMemoryTool):
if self.enable_memory_target:
# Group profiles by memory_target
from collections import defaultdict
profiles_by_target = defaultdict(list)
for profile in profiles_to_add:
target = profile.get("memory_target", self.memory_target)