mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-08 03:10:24 +00:00
feat(mem_agent): introduce version 2 memory agents and tools
- Add ReMeRetrieverV2 and ReMeSummarizerV2 components - Implement new memory tools including AddMemoryDrafts, RetrieveMemories, UpdateMemories - Create PersonalSummarizerV2 with three-step workflow for memory management - Add simplified evaluation script for ReMe on HaluMem benchmark - Update base memory agent with enhanced logging capabilities - Introduce new prompt formats and evaluation methods for question answering - Modify LLM utilities and message formatting with markdown header stripping option
This commit is contained in:
parent
5a0c1309f1
commit
581482eeb0
34 changed files with 2373 additions and 48 deletions
|
|
@ -82,7 +82,7 @@ def compute_f1(precision: float, recall: float) -> float:
|
|||
async def add_memory_async(user_id: str, messages: list[dict]) -> tuple[list[MemoryNode], float]:
|
||||
"""Add memory to ReMe system asynchronously."""
|
||||
start = time.time()
|
||||
result = await reme.summary(messages=messages, user_id=user_id)
|
||||
result = await reme.summary_v2(messages=messages, user_id=user_id)
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
return result, duration_ms
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ async def add_memory_async(user_id: str, messages: list[dict]) -> tuple[list[Mem
|
|||
async def search_memory_async(query: str, user_id: str, top_k: int = 20):
|
||||
"""Search memory from ReMe system asynchronously."""
|
||||
start = time.time()
|
||||
memories = await reme.retrieve(query=query, user_id=user_id, top_k=top_k)
|
||||
memories = await reme.retrieve_v2(query=query, user_id=user_id, top_k=top_k)
|
||||
|
||||
# Format the context
|
||||
context = TEMPLATE_MEMOS.format(user_id=user_id, memories=memories)
|
||||
|
|
|
|||
500
bench/halumem/eval_reme_simple.py
Normal file
500
bench/halumem/eval_reme_simple.py
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
"""
|
||||
Simplified evaluation script for ReMe on HaluMem benchmark - Question Answering only.
|
||||
|
||||
This script performs a simplified evaluation pipeline:
|
||||
1. Load HaluMem data
|
||||
2. Process each user's sessions with ReMe (summary + retrieve)
|
||||
3. Evaluate question answering only
|
||||
4. Generate metrics and statistics
|
||||
|
||||
Usage:
|
||||
python bench/halumem/eval_reme_simple.py --data_path /Users/yuli/workspace/HaluMem/data/HaluMem-Medium.jsonl \
|
||||
--top_k 20 --user_num 100 --max_concurrency 20
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from eval_tools import (
|
||||
_PROMPTS,
|
||||
evaluation_for_question,
|
||||
evaluation_for_question2,
|
||||
)
|
||||
from llms import llm_request
|
||||
from reme_ai.core.enumeration import MemoryType
|
||||
from reme_ai.core.schema import MemoryNode
|
||||
from reme_ai.reme import ReMe
|
||||
|
||||
# Initialize ReMe
|
||||
reme: ReMe = ReMe()
|
||||
|
||||
|
||||
def extract_user_name(persona_info: str):
|
||||
"""Extract user name from persona info."""
|
||||
match = re.search(r"Name:\s*(.*?); Gender:", persona_info)
|
||||
if match:
|
||||
username = match.group(1).strip()
|
||||
return username
|
||||
else:
|
||||
raise ValueError("No name found.")
|
||||
|
||||
|
||||
def iter_jsonl(file_path: str):
|
||||
"""Iterate over lines in a JSONL file."""
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
yield json.loads(line)
|
||||
|
||||
|
||||
# ==================== Main Processing ====================
|
||||
|
||||
|
||||
async def add_memory_async(user_id: str, messages: list[dict]) -> tuple[list[MemoryNode], float]:
|
||||
"""Add memory to ReMe system asynchronously."""
|
||||
start = time.time()
|
||||
result = await reme.summary_v2(messages=messages, user_id=user_id)
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
return result, duration_ms
|
||||
|
||||
|
||||
async def search_memory_async(query: str, user_id: str, top_k: int = 20):
|
||||
"""Search memory and get LLM response directly."""
|
||||
start = time.time()
|
||||
memories = await reme.retrieve_v2(query=query, user_id=user_id, top_k=top_k)
|
||||
|
||||
# Format the context
|
||||
context = f"User: {user_id}\nMemories:\n{memories}"
|
||||
|
||||
# Get LLM response directly
|
||||
prompt = f"Based on the following context, answer the question.\n\nContext:\n{context}\n\nQuestion: {query}\n\nAnswer:"
|
||||
response = await llm_request(prompt)
|
||||
|
||||
duration_ms = (time.time() - start) * 1000
|
||||
return response, duration_ms
|
||||
|
||||
|
||||
async def process_user_stage1(
|
||||
user_data: dict,
|
||||
top_k_value: int,
|
||||
save_path: str,
|
||||
):
|
||||
"""Process user data through ReMe (summary + retrieve + QA evaluation only)."""
|
||||
user_name = extract_user_name(user_data["persona_info"])
|
||||
sessions = user_data["sessions"]
|
||||
|
||||
tmp_dir = os.path.join(save_path, "tmp")
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
tmp_file = os.path.join(tmp_dir, f"{user_data['uuid']}.json")
|
||||
|
||||
new_user_data = {
|
||||
"uuid": user_data["uuid"],
|
||||
"user_name": user_name,
|
||||
"sessions": [],
|
||||
}
|
||||
|
||||
for idx, session in enumerate(sessions):
|
||||
logger.info(f"Processing user {user_name}: session {idx}/{len(sessions)}")
|
||||
new_session = {
|
||||
"memory_points": session["memory_points"],
|
||||
"dialogue": session["dialogue"],
|
||||
}
|
||||
|
||||
# Format dialogue
|
||||
dialogue = session["dialogue"]
|
||||
formatted_dialogue = [
|
||||
{
|
||||
"role": turn["role"],
|
||||
"content": turn["content"],
|
||||
"time_created": datetime.strptime(turn["timestamp"], "%b %d, %Y, %H:%M:%S")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
for turn in dialogue
|
||||
]
|
||||
|
||||
# Process in batches
|
||||
result = []
|
||||
total_duration_ms = 0
|
||||
batch_size = 20
|
||||
|
||||
for i in range(0, len(formatted_dialogue), batch_size):
|
||||
batch = formatted_dialogue[i : i + batch_size]
|
||||
batch_result, duration_ms = await add_memory_async(
|
||||
user_id=user_name,
|
||||
messages=batch,
|
||||
)
|
||||
if batch_result:
|
||||
result.extend(batch_result)
|
||||
total_duration_ms += duration_ms
|
||||
|
||||
duration_ms = total_duration_ms
|
||||
|
||||
# Extract memory content
|
||||
memories = []
|
||||
for memory_node in result:
|
||||
if isinstance(memory_node, MemoryNode) and memory_node.memory_type is not MemoryType.HISTORY:
|
||||
memories.append(memory_node.content)
|
||||
|
||||
if session.get("is_generated_qa_session", False):
|
||||
new_session["add_dialogue_duration_ms"] = duration_ms
|
||||
new_session["is_generated_qa_session"] = True
|
||||
del new_session["dialogue"]
|
||||
del new_session["memory_points"]
|
||||
new_user_data["sessions"].append(new_session)
|
||||
continue
|
||||
|
||||
# Store extracted memories
|
||||
new_session["extracted_memories"] = memories
|
||||
new_session["add_dialogue_duration_ms"] = duration_ms
|
||||
|
||||
# Process questions
|
||||
if "questions" not in session:
|
||||
new_user_data["sessions"].append(new_session)
|
||||
continue
|
||||
|
||||
new_session["questions"] = []
|
||||
|
||||
for qa in session["questions"]:
|
||||
response, duration_ms = await search_memory_async(
|
||||
query=qa["question"],
|
||||
user_id=user_name,
|
||||
top_k=top_k_value,
|
||||
)
|
||||
|
||||
new_qa = copy.deepcopy(qa)
|
||||
new_qa["system_response"] = response
|
||||
new_qa["search_duration_ms"] = duration_ms
|
||||
|
||||
new_session["questions"].append(new_qa)
|
||||
|
||||
# ==================== Evaluation for this session ====================
|
||||
session_eval_results = {
|
||||
"question_answering_records": [],
|
||||
}
|
||||
|
||||
uuid = user_data["uuid"]
|
||||
|
||||
# Evaluate Question Answering
|
||||
if "questions" in new_session:
|
||||
logger.info(f"Evaluating Question Answering for session {idx}...")
|
||||
|
||||
# Format dialogue for evaluation
|
||||
# Format: Each turn contains role, content, and time_created
|
||||
dialogue_for_eval = []
|
||||
for turn in dialogue:
|
||||
dialogue_for_eval.append(
|
||||
f"Role: {turn['role']}\n"
|
||||
f"Content: {turn['content']}\n"
|
||||
f"Time: {datetime.strptime(turn['timestamp'], '%b %d, %Y, %H:%M:%S').replace(tzinfo=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
)
|
||||
formatted_dialogue_str = "\n\n".join(dialogue_for_eval)
|
||||
|
||||
for qa in new_session["questions"]:
|
||||
new_qa = copy.deepcopy(qa)
|
||||
new_qa["uuid"] = uuid
|
||||
new_qa["session_id"] = idx
|
||||
|
||||
result = await evaluation_for_question2(
|
||||
qa["question"],
|
||||
qa["answer"],
|
||||
"\n".join([i["memory_content"] for i in qa["evidence"]]),
|
||||
qa["system_response"],
|
||||
formatted_dialogue_str,
|
||||
)
|
||||
result_type = result.get("evaluation_result")
|
||||
reasoning = result.get("reasoning", "")
|
||||
new_qa["result_type"] = result_type
|
||||
new_qa["question_answering_reasoning"] = reasoning
|
||||
session_eval_results["question_answering_records"].append(new_qa)
|
||||
|
||||
# Store evaluation results in session
|
||||
new_session["evaluation_results"] = session_eval_results
|
||||
|
||||
new_user_data["sessions"].append(new_session)
|
||||
|
||||
# Save results
|
||||
with open(tmp_file, "w", encoding="utf-8") as f:
|
||||
json.dump(new_user_data, f, ensure_ascii=False, indent=2)
|
||||
session_size = len(new_user_data["sessions"])
|
||||
logger.info(f"✅ Saved user {user_name} to {tmp_file} session_size={session_size}")
|
||||
|
||||
logger.info(f"✅ Saved user {user_name} to {tmp_file} all!")
|
||||
return {"uuid": user_data["uuid"], "status": "ok", "path": tmp_file}
|
||||
|
||||
|
||||
# ==================== Evaluation Aggregation ====================
|
||||
|
||||
|
||||
def aggregate_eval_results(eval_results):
|
||||
"""Aggregate evaluation results and compute metrics (QA only)."""
|
||||
|
||||
# Question-Answering Evaluation
|
||||
correct_qa_num = 0
|
||||
hallucination_qa_num = 0
|
||||
omission_qa_num = 0
|
||||
qa_num = 0
|
||||
qa_valid_num = 0
|
||||
|
||||
for item in eval_results["question_answering_records"]:
|
||||
item["is_valid"] = True
|
||||
qa_num += 1
|
||||
|
||||
if item["result_type"] not in ["Correct", "Hallucination", "Omission"]:
|
||||
item["is_valid"] = False
|
||||
continue
|
||||
|
||||
if item["result_type"] == "Correct":
|
||||
correct_qa_num += 1
|
||||
elif item["result_type"] == "Hallucination":
|
||||
hallucination_qa_num += 1
|
||||
elif item["result_type"] == "Omission":
|
||||
omission_qa_num += 1
|
||||
|
||||
qa_valid_num += 1
|
||||
|
||||
if qa_num > 0:
|
||||
eval_results["overall_score"]["question_answering"]["correct_qa_ratio(all)"] = correct_qa_num / qa_num
|
||||
eval_results["overall_score"]["question_answering"]["hallucination_qa_ratio(all)"] = (
|
||||
hallucination_qa_num / qa_num
|
||||
)
|
||||
eval_results["overall_score"]["question_answering"]["omission_qa_ratio(all)"] = omission_qa_num / qa_num
|
||||
else:
|
||||
eval_results["overall_score"]["question_answering"]["correct_qa_ratio(all)"] = 0
|
||||
eval_results["overall_score"]["question_answering"]["hallucination_qa_ratio(all)"] = 0
|
||||
eval_results["overall_score"]["question_answering"]["omission_qa_ratio(all)"] = 0
|
||||
|
||||
if qa_valid_num > 0:
|
||||
eval_results["overall_score"]["question_answering"]["correct_qa_ratio(valid)"] = correct_qa_num / qa_valid_num
|
||||
eval_results["overall_score"]["question_answering"]["hallucination_qa_ratio(valid)"] = (
|
||||
hallucination_qa_num / qa_valid_num
|
||||
)
|
||||
eval_results["overall_score"]["question_answering"]["omission_qa_ratio(valid)"] = omission_qa_num / qa_valid_num
|
||||
else:
|
||||
eval_results["overall_score"]["question_answering"]["correct_qa_ratio(valid)"] = 0
|
||||
eval_results["overall_score"]["question_answering"]["hallucination_qa_ratio(valid)"] = 0
|
||||
eval_results["overall_score"]["question_answering"]["omission_qa_ratio(valid)"] = 0
|
||||
|
||||
eval_results["overall_score"]["question_answering"]["qa_valid_num"] = qa_valid_num
|
||||
eval_results["overall_score"]["question_answering"]["qa_num"] = qa_num
|
||||
|
||||
return eval_results
|
||||
|
||||
|
||||
# ==================== Main Pipeline ====================
|
||||
|
||||
|
||||
async def main_async(
|
||||
data_path: str,
|
||||
top_k: int = 20,
|
||||
user_num: int = 1,
|
||||
max_concurrency: int = 2,
|
||||
):
|
||||
"""Main evaluation pipeline - simplified for QA only."""
|
||||
frame = "reme_simple"
|
||||
save_path = f"bench_results/{frame}/"
|
||||
os.makedirs(save_path, exist_ok=True)
|
||||
|
||||
output_file_stage1 = os.path.join(save_path, f"{frame}_eval_results.jsonl")
|
||||
output_file_final = os.path.join(save_path, f"{frame}_eval_stat_result.json")
|
||||
|
||||
start_time = time.time()
|
||||
await reme.vector_store.delete_all()
|
||||
|
||||
# ==================== Stage 1: Data Processing ====================
|
||||
print("\n" + "=" * 80)
|
||||
print("PROCESSING DATA WITH ReMe (Simplified - QA Only)")
|
||||
print(f"Max Concurrency: {max_concurrency}")
|
||||
print("=" * 80)
|
||||
|
||||
tmp_dir = os.path.join(save_path, "tmp")
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
|
||||
# Load all user data
|
||||
user_data_list = list(iter_jsonl(data_path))
|
||||
total_users = min(len(user_data_list), user_num)
|
||||
user_data_list = user_data_list[:total_users]
|
||||
|
||||
print(f"Processing {total_users} users with max concurrency {max_concurrency}...")
|
||||
|
||||
# Create semaphore to limit concurrency
|
||||
semaphore = asyncio.Semaphore(max_concurrency)
|
||||
|
||||
async def process_single_user(idx: int, user_data: dict):
|
||||
"""Process a single user with semaphore control."""
|
||||
async with semaphore:
|
||||
uuid = user_data['uuid']
|
||||
tmp_file = os.path.join(tmp_dir, f"{uuid}.json")
|
||||
|
||||
if os.path.exists(tmp_file):
|
||||
print(f"⚡ Skipping user {uuid} ({idx}/{total_users}) — cached result found.")
|
||||
return {"uuid": uuid, "status": "cached", "path": tmp_file}
|
||||
|
||||
print(f"[{idx}/{total_users}] Processing user {uuid}...")
|
||||
result = await process_user_stage1(user_data, top_k, save_path)
|
||||
print(f"[{idx}/{total_users}] ✅ Finished {uuid} ({result['status']})")
|
||||
return result
|
||||
|
||||
# Process users in parallel with controlled concurrency
|
||||
tasks = [process_single_user(idx, user_data) for idx, user_data in enumerate(user_data_list, 1)]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# Combine all results into final output
|
||||
with open(output_file_stage1, "w", encoding="utf-8") as f_out:
|
||||
for file in os.listdir(tmp_dir):
|
||||
if file.endswith(".json"):
|
||||
file_path = os.path.join(tmp_dir, file)
|
||||
with open(file_path, "r", encoding="utf-8") as f_in:
|
||||
data = json.load(f_in)
|
||||
f_out.write(json.dumps(data, ensure_ascii=False) + "\n")
|
||||
|
||||
elapsed_stage1 = time.time() - start_time
|
||||
print(f"\n✅ Processing completed in {elapsed_stage1:.2f}s")
|
||||
print(f"✅ Results saved to: {output_file_stage1}")
|
||||
|
||||
# ==================== Aggregate Results ====================
|
||||
print("\n" + "=" * 80)
|
||||
print("AGGREGATING EVALUATION RESULTS")
|
||||
print("=" * 80)
|
||||
|
||||
# Calculate time consuming
|
||||
add_dialogue_duration_time = 0
|
||||
search_memory_duration_time = 0
|
||||
|
||||
for user_data in iter_jsonl(output_file_stage1):
|
||||
sessions = user_data["sessions"]
|
||||
|
||||
for session in sessions:
|
||||
if "add_dialogue_duration_ms" in session:
|
||||
add_dialogue_duration_time += session["add_dialogue_duration_ms"]
|
||||
|
||||
if "questions" in session:
|
||||
for question in session["questions"]:
|
||||
if "search_duration_ms" in question:
|
||||
search_memory_duration_time += question["search_duration_ms"]
|
||||
|
||||
add_dialogue_duration_time = add_dialogue_duration_time / 1000 / 60
|
||||
search_memory_duration_time = search_memory_duration_time / 1000 / 60
|
||||
|
||||
print("\n🔄 Aggregating all user results...")
|
||||
|
||||
eval_results = {
|
||||
"overall_score": {
|
||||
"question_answering": {},
|
||||
"time_consuming": {
|
||||
"add_dialogue_duration_time": add_dialogue_duration_time,
|
||||
"search_memory_duration_time": search_memory_duration_time,
|
||||
"total_duration_time": add_dialogue_duration_time + search_memory_duration_time,
|
||||
},
|
||||
},
|
||||
"question_answering_records": [],
|
||||
}
|
||||
|
||||
# Extract QA records from all users
|
||||
for user_data in iter_jsonl(output_file_stage1):
|
||||
for session in user_data["sessions"]:
|
||||
if session.get("is_generated_qa_session", False):
|
||||
continue
|
||||
|
||||
if "evaluation_results" in session:
|
||||
eval_results["question_answering_records"].extend(
|
||||
session["evaluation_results"].get("question_answering_records", [])
|
||||
)
|
||||
|
||||
eval_results = aggregate_eval_results(eval_results)
|
||||
|
||||
with open(output_file_final, "w", encoding="utf-8") as f:
|
||||
json.dump(eval_results, f, ensure_ascii=False, indent=4)
|
||||
|
||||
elapsed_total = time.time() - start_time
|
||||
print(f"\n✅ All done in {elapsed_total:.2f}s. Results saved to {output_file_final}")
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 80)
|
||||
print("EVALUATION SUMMARY (Question Answering Only)")
|
||||
print("=" * 80)
|
||||
|
||||
print(f"\n📊 Question Answering:")
|
||||
print(
|
||||
f" - Correct (all): {eval_results['overall_score']['question_answering'].get('correct_qa_ratio(all)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Hallucination (all): {eval_results['overall_score']['question_answering'].get('hallucination_qa_ratio(all)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Omission (all): {eval_results['overall_score']['question_answering'].get('omission_qa_ratio(all)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Correct (valid): {eval_results['overall_score']['question_answering'].get('correct_qa_ratio(valid)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Hallucination (valid): {eval_results['overall_score']['question_answering'].get('hallucination_qa_ratio(valid)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Omission (valid): {eval_results['overall_score']['question_answering'].get('omission_qa_ratio(valid)', 0):.4f}",
|
||||
)
|
||||
print(
|
||||
f" - Valid QA: {eval_results['overall_score']['question_answering'].get('qa_valid_num', 0)}/{eval_results['overall_score']['question_answering'].get('qa_num', 0)}",
|
||||
)
|
||||
|
||||
print(f"\n⏱️ Time Consuming:")
|
||||
print(f" - Add Dialogue: {add_dialogue_duration_time:.2f} min")
|
||||
print(f" - Search Memory: {search_memory_duration_time:.2f} min")
|
||||
print(f" - Total: {add_dialogue_duration_time + search_memory_duration_time:.2f} min")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
def main(
|
||||
data_path: str,
|
||||
top_k: int = 20,
|
||||
user_num: int = 1,
|
||||
max_concurrency: int = 2,
|
||||
):
|
||||
"""Synchronous entry point."""
|
||||
asyncio.run(main_async(data_path, top_k, user_num, max_concurrency))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Simplified evaluation for ReMe on HaluMem benchmark (QA only)")
|
||||
parser.add_argument(
|
||||
"--data_path",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to HaluMem data file (e.g., HaluMem-medium.jsonl)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--top_k",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Number of top memories to retrieve (default: 20)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user_num",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of users to evaluate (default: 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_concurrency",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Maximum concurrency for processing (default: 2)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
data_path=args.data_path,
|
||||
top_k=args.top_k,
|
||||
user_num=args.user_num,
|
||||
max_concurrency=args.max_concurrency,
|
||||
)
|
||||
|
|
@ -102,3 +102,32 @@ async def evaluation_for_question(
|
|||
result = await llm_request_for_json(prompt)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def evaluation_for_question2(
|
||||
question: str,
|
||||
reference_answer: str,
|
||||
key_memory_points: str,
|
||||
response: str,
|
||||
dialogue: str,
|
||||
):
|
||||
"""
|
||||
Question-Answering Evaluation with Dialogue Context (Version 2)
|
||||
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: The formatted dialogue history (role, content, time_created).
|
||||
"""
|
||||
|
||||
prompt = _PROMPTS["EVALUATION_PROMPT_FOR_QUESTION2"].format(
|
||||
question=question,
|
||||
reference_answer=reference_answer,
|
||||
key_memory_points=key_memory_points,
|
||||
response=response,
|
||||
dialogue=dialogue,
|
||||
)
|
||||
|
||||
result = await llm_request_for_json(prompt)
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@ EVALUATION_PROMPT_FOR_QUESTION: |
|
|||
EVALUATION_PROMPT_FOR_QUESTION2: |
|
||||
You are an **evaluation expert for AI memory system question answering**.
|
||||
|
||||
* **Dialogue:**
|
||||
**Dialogue:**
|
||||
{dialogue}
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class Message(BaseModel):
|
|||
use_name: bool = False,
|
||||
add_reasoning: bool = True,
|
||||
add_tools: bool = True,
|
||||
strip_markdown_headers: bool = True,
|
||||
strip_markdown_headers: bool = False,
|
||||
) -> str:
|
||||
"""Generates a human-readable string representation of the message."""
|
||||
prefix = f"round{index} " if index is not None else ""
|
||||
|
|
@ -130,26 +130,23 @@ class Message(BaseModel):
|
|||
|
||||
lines = [f"{prefix}{time_str}{header}"]
|
||||
|
||||
if add_reasoning and self.reasoning_content:
|
||||
content = self.reasoning_content
|
||||
def strip_md_func(line):
|
||||
if strip_markdown_headers:
|
||||
content = re.sub(r'\n##+ +', '\n', content)
|
||||
lines.append(content)
|
||||
line = re.sub(r'\n##+ +', '\n', line)
|
||||
return line
|
||||
|
||||
if add_reasoning and self.reasoning_content:
|
||||
lines.append(self.reasoning_content)
|
||||
|
||||
if isinstance(self.content, str):
|
||||
content = self.content
|
||||
if strip_markdown_headers:
|
||||
content = re.sub(r'\n##+ +', '\n', content)
|
||||
lines.append(content)
|
||||
lines.append(strip_md_func(self.content))
|
||||
|
||||
elif isinstance(self.content, list):
|
||||
for block in self.content:
|
||||
text = (
|
||||
block.content if isinstance(block.content, str) else json.dumps(block.content, ensure_ascii=False)
|
||||
)
|
||||
text = block.content if isinstance(block.content, str) else \
|
||||
json.dumps(block.content, ensure_ascii=False)
|
||||
text = str(text)
|
||||
if strip_markdown_headers and isinstance(block.content, str):
|
||||
text = re.sub(r'\n##+ +', '\n', text)
|
||||
lines.append(text)
|
||||
lines.append(strip_md_func(text))
|
||||
|
||||
if add_tools and self.tool_calls:
|
||||
for tc in self.tool_calls:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def format_messages(messages: list[Message | dict], enable_system: bool = False)
|
|||
use_name=True,
|
||||
add_reasoning=True,
|
||||
add_tools=True,
|
||||
strip_markdown_headers=True,
|
||||
),
|
||||
)
|
||||
return "\n".join(formatted_lines)
|
||||
|
|
|
|||
|
|
@ -168,17 +168,18 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta):
|
|||
return messages, success
|
||||
|
||||
async def execute(self):
|
||||
for i, tool in enumerate(self.tools):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}] step0.{i} "
|
||||
f"tool_call={json.dumps(tool.tool_call.simple_input_dump(), ensure_ascii=False)}",
|
||||
)
|
||||
|
||||
messages = await self.build_messages()
|
||||
for i, message in enumerate(messages):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}] step0.{i} {message.role} {message.name or ''} "
|
||||
f"{message.simple_dump(enable_json_dump=True)}",
|
||||
)
|
||||
for i, tool in enumerate(self.tools):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}] step0.{i} "
|
||||
f"tool_call={json.dumps(tool.tool_call.simple_input_dump(), ensure_ascii=False)}",
|
||||
)
|
||||
|
||||
self.messages, self.success = await self.react(messages)
|
||||
if self.success and self.messages:
|
||||
|
|
|
|||
|
|
@ -29,24 +29,24 @@ class ReMeRetriever(BaseMemoryAgent):
|
|||
await op.call()
|
||||
return str(op.output)
|
||||
|
||||
async def build_messages1(self) -> List[Message]:
|
||||
"""Build messages with system prompt and user message."""
|
||||
meta_memory_info = await self._read_meta_memories()
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
now_time=get_now_time(),
|
||||
meta_memory_info=meta_memory_info,
|
||||
)
|
||||
# async def build_messages1(self) -> List[Message]:
|
||||
# """Build messages with system prompt and user message."""
|
||||
# meta_memory_info = await self._read_meta_memories()
|
||||
# system_prompt = self.prompt_format(
|
||||
# prompt_name="system_prompt",
|
||||
# now_time=get_now_time(),
|
||||
# meta_memory_info=meta_memory_info,
|
||||
# )
|
||||
|
||||
messages = [Message(role=Role.SYSTEM, content=system_prompt)]
|
||||
if self.context.get("query"):
|
||||
messages.append(Message(role=Role.USER, content=self.context.query))
|
||||
elif self.context.get("messages"):
|
||||
messages.extend([Message(**m) for m in self.context.messages])
|
||||
else:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
# messages = [Message(role=Role.SYSTEM, content=system_prompt)]
|
||||
# if self.context.get("query"):
|
||||
# messages.append(Message(role=Role.USER, content=self.context.query))
|
||||
# elif self.context.get("messages"):
|
||||
# messages.extend([Message(**m) for m in self.context.messages])
|
||||
# else:
|
||||
# raise ValueError("input must have either `query` or `messages`")
|
||||
|
||||
return messages
|
||||
# return messages
|
||||
|
||||
async def build_messages(self) -> List[Message]:
|
||||
"""Build messages with system prompt and user message."""
|
||||
|
|
|
|||
5
reme_ai/mem_agent/retriever_v2/__init__.py
Normal file
5
reme_ai/mem_agent/retriever_v2/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .reme_retriever_v2 import ReMeRetrieverV2
|
||||
|
||||
__all__ = [
|
||||
"ReMeRetrieverV2",
|
||||
]
|
||||
67
reme_ai/mem_agent/retriever_v2/reme_retriever_v2.py
Normal file
67
reme_ai/mem_agent/retriever_v2/reme_retriever_v2.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""ReMe retriever v2 that autonomously retrieves memories from multiple angles."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReMeRetrieverV2(BaseMemoryAgent):
|
||||
"""Memory agent that autonomously retrieves memories from multiple angles.
|
||||
|
||||
This retriever:
|
||||
- Directly queries memories based on user questions without time constraints
|
||||
- Tries multiple retrieval strategies: direct vector search, metadata filtering, partial filtering
|
||||
- Attempts at least 3 vector retrievals from different perspectives
|
||||
- Falls back to read_history if vector retrieval doesn't find sufficient information
|
||||
"""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
# Check if ReadHistory tool is available in the tools list
|
||||
tools = kwargs.get('tools', [])
|
||||
has_read_history = any(tool.__class__.__name__ == 'ReadHistory' for tool in tools)
|
||||
|
||||
# Use simple prompt if ReadHistory is not available
|
||||
if not has_read_history:
|
||||
super().__init__(prompt_name="reme_retriever_v2_simple", **kwargs)
|
||||
else:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
"""Fetch all meta-memory entries that define specialized memory agents."""
|
||||
from ...mem_tool import ReadMetaMemory
|
||||
|
||||
op = ReadMetaMemory(enable_identity_memory=False)
|
||||
if self.meta_memories:
|
||||
return op.format_memory_metadata(self.meta_memories)
|
||||
else:
|
||||
await op.call()
|
||||
return str(op.output)
|
||||
|
||||
async def build_messages(self) -> List[Message]:
|
||||
"""Build messages with system prompt and user message."""
|
||||
if self.context.get("query"):
|
||||
context = self.context.query
|
||||
elif self.context.get("messages"):
|
||||
context = format_messages(self.context.messages)
|
||||
else:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self._read_meta_memories(),
|
||||
context=context,
|
||||
)
|
||||
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content=system_prompt),
|
||||
Message(role=Role.USER, content=self.get_prompt("user_message")),
|
||||
]
|
||||
|
||||
return messages
|
||||
125
reme_ai/mem_agent/retriever_v2/reme_retriever_v2.yaml
Normal file
125
reme_ai/mem_agent/retriever_v2/reme_retriever_v2.yaml
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
tool: |
|
||||
Autonomously retrieve relevant memories from multiple angles to answer user questions.
|
||||
This retriever will:
|
||||
- Try multiple vector search strategies (direct, metadata-filtered, partial)
|
||||
- Attempt at least 3 different retrieval approaches before giving up
|
||||
- Fall back to reading original conversation history if vector search is insufficient
|
||||
- Clearly state "I don't know" if information cannot be found after exhaustive searching
|
||||
- NEVER hallucinate or fabricate information not present in retrieved memories
|
||||
Use this when you need comprehensive memory retrieval with persistent searching.
|
||||
|
||||
system_prompt: |
|
||||
You are an autonomous memory retrieval agent. Your task is to persistently search for relevant memories from multiple angles to answer the user's question.
|
||||
|
||||
## Available Meta Memories
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## User Context
|
||||
{context}
|
||||
|
||||
## Your Retrieval Strategy
|
||||
|
||||
You MUST use the `retrieve_memories` tool to search for relevant information. This is a MANDATORY step - do not skip it.
|
||||
|
||||
1. **Multi-Angle Vector Retrieval** (REQUIRED - at least 3 attempts):
|
||||
You must try AT LEAST 3 different retrieval approaches using `retrieve_memories`:
|
||||
|
||||
a) **Direct Vector Search**: Use the user's question directly or with minimal reformulation
|
||||
- Query the most relevant memory_type and memory_target
|
||||
- Use straightforward query phrasing
|
||||
|
||||
b) **Alternative Phrasing**: Reformulate the query from a different angle
|
||||
- Use synonyms or different expressions
|
||||
- Break down complex questions into simpler components
|
||||
- Try more specific or more general queries
|
||||
|
||||
c) **Metadata-Filtered Search**: Add metadata filters to narrow down results
|
||||
- **Time-based filtering**: Use year/month/day metadata fields to filter by time periods
|
||||
* Example: {{"year": 2024}} for memories from 2024
|
||||
* Example: {{"year": 2024, "month": 5}} for memories from May 2024
|
||||
* Example: {{"year": 2024, "month": 5, "day": 15}} for memories from a specific date
|
||||
- Combine vector search with metadata constraints
|
||||
- Try partial metadata filtering if full filtering yields nothing (e.g., only year, or year+month)
|
||||
|
||||
d) **Cross-Memory-Type Search**: If applicable, search across different memory types
|
||||
- Try different memory_type and memory_target combinations
|
||||
- Some information might be stored in unexpected memory categories
|
||||
|
||||
e) **Keyword Extraction**: Extract key entities/concepts and search for them
|
||||
- Identify important names, places, concepts
|
||||
- Search for each key element separately
|
||||
|
||||
2. **Evaluate Retrieval Results** (After each attempt):
|
||||
- Review what memories were returned
|
||||
- Assess if they contain sufficient information to answer the question
|
||||
- If insufficient, identify what's missing and adjust your next query accordingly
|
||||
- Track which retrieval strategies you've already tried
|
||||
|
||||
3. **Persist Through Failures**:
|
||||
- DO NOT give up after 1-2 failed attempts
|
||||
- If a retrieval returns no results or irrelevant results, try a different approach
|
||||
- Consider that the information might be phrased differently than expected
|
||||
- Be creative with query reformulation
|
||||
|
||||
4. **Fallback to History Reading** (Only after 3+ vector retrieval attempts):
|
||||
- If after at least 3 different vector retrieval attempts you still lack sufficient information:
|
||||
* If any retrieved memories contain `ref_memory_id`, use `read_history` to read the original conversation
|
||||
* Use `read_history` with the `ref_memory_id` to get complete context
|
||||
* This can reveal details that weren't captured in the memory summaries
|
||||
|
||||
5. **Answer the Question**:
|
||||
- Once you have sufficient information, provide a direct answer based ONLY on retrieved memories
|
||||
- DO NOT fabricate, guess, or infer information not present in the memories
|
||||
- **CRITICAL**: If after 3+ retrieval attempts you still cannot find relevant information:
|
||||
* Simply state: "I don't know. After searching from multiple angles, I could not find relevant information to answer this question."
|
||||
* DO NOT make up answers or hallucinate information
|
||||
* DO NOT provide speculative or guessed responses
|
||||
* It is better to say "I don't know" than to provide incorrect information
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- **Be Persistent**: Always try at least 3 different retrieval strategies before concluding no information exists
|
||||
- **Be Creative**: If one query approach fails, think of alternative ways to phrase or decompose the question
|
||||
- **Use Tools**: You MUST use `retrieve_memories` for vector search. Use `read_history` if you have `ref_memory_id` and need more details
|
||||
- **No Hallucination**: NEVER fabricate, guess, or hallucinate information. Only answer based on what you actually retrieved from memories
|
||||
- **Admit When You Don't Know**: If after 3+ attempts you cannot find relevant information, clearly say "I don't know" rather than making up an answer
|
||||
- **Track Your Attempts**: Keep count of how many different retrieval strategies you've tried
|
||||
- **Metadata Awareness**: Utilize metadata filters when they might help narrow down results
|
||||
* Memories store time information in metadata as year/month/day fields
|
||||
* Use time-based filters when the question involves specific time periods or dates
|
||||
* Try progressive filtering: start with year, then add month, then day if needed
|
||||
|
||||
## Example Retrieval Flow
|
||||
|
||||
**Example 1: Simple Query**
|
||||
Attempt 1: Direct query "user's favorite food"
|
||||
→ Result: No relevant memories found
|
||||
|
||||
Attempt 2: Reformulated query "what does user like to eat"
|
||||
→ Result: Some memories about meals, but not specific preferences
|
||||
|
||||
Attempt 3: Keyword search "food preferences" with metadata filter
|
||||
→ Result: Found relevant memory with ref_memory_id
|
||||
|
||||
Attempt 4: Use read_history with ref_memory_id to get full context
|
||||
→ Result: Found detailed conversation about favorite foods
|
||||
|
||||
Answer: [Provide answer based on retrieved information]
|
||||
|
||||
**Example 2: Time-based Query**
|
||||
Question: "What did the user do last summer?"
|
||||
|
||||
Attempt 1: Direct query "user activities summer" with metadata {{"year": 2025, "month": [6, 7, 8]}}
|
||||
→ Result: Found some vacation memories
|
||||
|
||||
Attempt 2: Broader query "user summer vacation travel" with metadata {{"year": 2025}}
|
||||
→ Result: Found additional travel-related memories
|
||||
|
||||
Attempt 3: Use read_history for memories with ref_memory_id to get detailed context
|
||||
→ Result: Complete picture of summer activities
|
||||
|
||||
Answer: [Provide answer based on retrieved information]
|
||||
|
||||
user_message: |
|
||||
Please retrieve relevant memories and answer the question. Remember to try multiple retrieval approaches before giving up.
|
||||
115
reme_ai/mem_agent/retriever_v2/reme_retriever_v2_simple.yaml
Normal file
115
reme_ai/mem_agent/retriever_v2/reme_retriever_v2_simple.yaml
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
tool: |
|
||||
Autonomously retrieve relevant memories from multiple angles to answer user questions.
|
||||
This retriever will:
|
||||
- Try multiple vector search strategies (direct, metadata-filtered, partial)
|
||||
- Attempt at least 3 different retrieval approaches before giving up
|
||||
- Clearly state "I don't know" if information cannot be found after exhaustive searching
|
||||
- NEVER hallucinate or fabricate information not present in retrieved memories
|
||||
Use this when you need comprehensive memory retrieval with persistent searching.
|
||||
|
||||
system_prompt: |
|
||||
You are an autonomous memory retrieval agent. Your task is to persistently search for relevant memories from multiple angles to answer the user's question.
|
||||
|
||||
## Available Meta Memories
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## User Context
|
||||
{context}
|
||||
|
||||
## Your Retrieval Strategy
|
||||
|
||||
You MUST use the `retrieve_memories` tool to search for relevant information. This is a MANDATORY step - do not skip it.
|
||||
|
||||
1. **Multi-Angle Vector Retrieval** (REQUIRED - at least 3 attempts):
|
||||
You must try AT LEAST 3 different retrieval approaches using `retrieve_memories`:
|
||||
|
||||
a) **Direct Vector Search**: Use the user's question directly or with minimal reformulation
|
||||
- Query the most relevant memory_type and memory_target
|
||||
- Use straightforward query phrasing
|
||||
|
||||
b) **Alternative Phrasing**: Reformulate the query from a different angle
|
||||
- Use synonyms or different expressions
|
||||
- Break down complex questions into simpler components
|
||||
- Try more specific or more general queries
|
||||
|
||||
c) **Metadata-Filtered Search**: Add metadata filters to narrow down results
|
||||
- **Time-based filtering**: Use year/month/day metadata fields to filter by time periods
|
||||
* Example: {{"year": 2024}} for memories from 2024
|
||||
* Example: {{"year": 2024, "month": 5}} for memories from May 2024
|
||||
* Example: {{"year": 2024, "month": 5, "day": 15}} for memories from a specific date
|
||||
- Combine vector search with metadata constraints
|
||||
- Try partial metadata filtering if full filtering yields nothing (e.g., only year, or year+month)
|
||||
|
||||
d) **Cross-Memory-Type Search**: If applicable, search across different memory types
|
||||
- Try different memory_type and memory_target combinations
|
||||
- Some information might be stored in unexpected memory categories
|
||||
|
||||
e) **Keyword Extraction**: Extract key entities/concepts and search for them
|
||||
- Identify important names, places, concepts
|
||||
- Search for each key element separately
|
||||
|
||||
2. **Evaluate Retrieval Results** (After each attempt):
|
||||
- Review what memories were returned
|
||||
- Assess if they contain sufficient information to answer the question
|
||||
- If insufficient, identify what's missing and adjust your next query accordingly
|
||||
- Track which retrieval strategies you've already tried
|
||||
|
||||
3. **Persist Through Failures**:
|
||||
- DO NOT give up after 1-2 failed attempts
|
||||
- If a retrieval returns no results or irrelevant results, try a different approach
|
||||
- Consider that the information might be phrased differently than expected
|
||||
- Be creative with query reformulation
|
||||
|
||||
4. **Answer the Question**:
|
||||
- Once you have sufficient information, provide a direct answer based ONLY on retrieved memories
|
||||
- DO NOT fabricate, guess, or infer information not present in the memories
|
||||
- **CRITICAL**: If after 3+ retrieval attempts you still cannot find relevant information:
|
||||
* Simply state: "I don't know. After searching from multiple angles, I could not find relevant information to answer this question."
|
||||
* DO NOT make up answers or hallucinate information
|
||||
* DO NOT provide speculative or guessed responses
|
||||
* It is better to say "I don't know" than to provide incorrect information
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- **Be Persistent**: Always try at least 3 different retrieval strategies before concluding no information exists
|
||||
- **Be Creative**: If one query approach fails, think of alternative ways to phrase or decompose the question
|
||||
- **Use Tools**: You MUST use `retrieve_memories` for vector search
|
||||
- **No Hallucination**: NEVER fabricate, guess, or hallucinate information. Only answer based on what you actually retrieved from memories
|
||||
- **Admit When You Don't Know**: If after 3+ attempts you cannot find relevant information, clearly say "I don't know" rather than making up an answer
|
||||
- **Track Your Attempts**: Keep count of how many different retrieval strategies you've tried
|
||||
- **Metadata Awareness**: Utilize metadata filters when they might help narrow down results
|
||||
* Memories store time information in metadata as year/month/day fields
|
||||
* Use time-based filters when the question involves specific time periods or dates
|
||||
* Try progressive filtering: start with year, then add month, then day if needed
|
||||
|
||||
## Example Retrieval Flow
|
||||
|
||||
**Example 1: Simple Query**
|
||||
Attempt 1: Direct query "user's favorite food"
|
||||
→ Result: No relevant memories found
|
||||
|
||||
Attempt 2: Reformulated query "what does user like to eat"
|
||||
→ Result: Some memories about meals, but not specific preferences
|
||||
|
||||
Attempt 3: Keyword search "food preferences" with metadata filter
|
||||
→ Result: Found relevant memory
|
||||
|
||||
Answer: [Provide answer based on retrieved information]
|
||||
|
||||
**Example 2: Time-based Query**
|
||||
Question: "What did the user do last summer?"
|
||||
|
||||
Attempt 1: Direct query "user activities summer" with metadata {{"year": 2025, "month": [6, 7, 8]}}
|
||||
→ Result: Found some vacation memories
|
||||
|
||||
Attempt 2: Broader query "user summer vacation travel" with metadata {{"year": 2025}}
|
||||
→ Result: Found additional travel-related memories
|
||||
|
||||
Attempt 3: More specific queries about specific activities
|
||||
→ Result: Complete picture of summer activities
|
||||
|
||||
Answer: [Provide answer based on retrieved information]
|
||||
|
||||
user_message: |
|
||||
Please retrieve relevant memories and answer the question. Remember to try multiple retrieval approaches before giving up.
|
||||
6
reme_ai/mem_agent/summarizer_v2/__init__.py
Normal file
6
reme_ai/mem_agent/summarizer_v2/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""Simplified V2 summarizers for memory management."""
|
||||
|
||||
from .reme_summarizer_v2 import ReMeSummarizerV2
|
||||
from .personal_summarizer_v2 import PersonalSummarizerV2
|
||||
|
||||
__all__ = ["ReMeSummarizerV2", "PersonalSummarizerV2"]
|
||||
110
reme_ai/mem_agent/summarizer_v2/personal_summarizer_v2.py
Normal file
110
reme_ai/mem_agent/summarizer_v2/personal_summarizer_v2.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""Simplified personal memory summarizer using v2 memory tools."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class PersonalSummarizerV2(BaseMemoryAgent):
|
||||
"""Simplified personal memory summarizer that uses v2 memory tools.
|
||||
|
||||
This summarizer follows a three-step workflow:
|
||||
1. AddMemoryDrafts: Generate initial memory drafts from context
|
||||
2. RetrieveRecentAndSimilarMemories: Retrieve similar and recent memories
|
||||
3. UpdateMemories: Delete outdated memories and add new ones
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build tool call schema for the agent."""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": self.get_prompt("tool"),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "role",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "content",
|
||||
},
|
||||
},
|
||||
"required": ["role", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["messages"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Construct messages with context, memory_target, and memory_type information."""
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
context=self.description + "\n" + format_messages(self.get_messages()),
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
)
|
||||
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content=system_prompt),
|
||||
Message(role=Role.USER, content=self.get_prompt("user_message")),
|
||||
]
|
||||
return messages
|
||||
|
||||
async def _reasoning_step(self, messages: list[Message], step: int, **kwargs) -> tuple[Message, bool]:
|
||||
return await super()._reasoning_step(messages, step, **kwargs)
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with memory_target, memory_type, and author context."""
|
||||
messages: list[Message] = await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
ref_memory_id=self.ref_memory_id,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Check if AddMemoryDrafts tool was executed
|
||||
exist_memory_drafts = False
|
||||
if assistant_message.tool_calls:
|
||||
for tool_call in assistant_message.tool_calls:
|
||||
if tool_call.name == "add_memory_drafts":
|
||||
exist_memory_drafts = True
|
||||
break
|
||||
|
||||
# If memory drafts were added, regenerate system prompt with simplified context
|
||||
if exist_memory_drafts:
|
||||
simplified_context = "The conversation context has been summarized in memory drafts."
|
||||
new_system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
context=simplified_context,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
)
|
||||
|
||||
# Update the system message in the message history
|
||||
for i, msg in enumerate(self.messages):
|
||||
if msg.role == Role.SYSTEM:
|
||||
self.messages[i] = Message(role=Role.SYSTEM, content=new_system_prompt)
|
||||
break
|
||||
|
||||
return messages
|
||||
63
reme_ai/mem_agent/summarizer_v2/personal_summarizer_v2.yaml
Normal file
63
reme_ai/mem_agent/summarizer_v2/personal_summarizer_v2.yaml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
tool: |
|
||||
Extract and store personal memories from conversation context using a three-step workflow.
|
||||
Use this tool to analyze dialogues and extract important personal information about users,
|
||||
such as preferences, habits, personal background, relationships, and significant facts.
|
||||
|
||||
system_prompt: |
|
||||
You are a professional memory agent. Your task is to update the main agent's {memory_type} memory regarding {memory_target} based on the context.
|
||||
|
||||
**CRITICAL**: You must extract and store information STRICTLY based on what is explicitly stated in the context. DO NOT infer, assume, fabricate, or add any information that is not directly present in the dialogue. Only extract facts that are clearly and explicitly mentioned.
|
||||
|
||||
## Context:
|
||||
{context}
|
||||
|
||||
**Context Format Explanation**:
|
||||
The context contains formatted conversation messages in the following structure:
|
||||
- Each message is formatted as: `round<index> [<timestamp>] <role/name>: <content>`
|
||||
- The timestamp is in format: `YYYY-MM-DD HH:MM:SS`
|
||||
- Content may include reasoning, tool calls
|
||||
- **Time metadata handling**: When extracting memories with time information, store year/month/day in the metadata. For relative time references (e.g., "last year", "two months ago"), calculate the actual date based on the message's timestamp and store the calculated year/month/day in metadata
|
||||
|
||||
## Memory Objective:
|
||||
You are managing **{memory_type}** memories about **{memory_target}** for the main agent. Focus on extracting and storing information directly related to this person's preferences, habits, personal background, and significant facts.
|
||||
|
||||
## Your Tasks - Three-Step Workflow:
|
||||
|
||||
### Step 1: Generate Memory Drafts
|
||||
Use the `AddMemoryDrafts` tool to create initial memory drafts from the conversation context.
|
||||
- **Analyze the context**: Determine whether the conversation contains important, memorable information, including but not limited to: user preferences, habits, or personal details; key facts, decisions, or conclusions; relationships or contextual background related to people or topics.
|
||||
- **Extract key information**: Create memory drafts using clear and concise phrasing **strictly based on what is explicitly stated in the context**.
|
||||
- **Important**: DO NOT infer, assume, or add any information beyond what is directly mentioned in the conversation.
|
||||
- **Time references**: If the context involves time references (like "last year", "two months ago", etc.), you **must** calculate the actual date based on the context's timestamp metadata. For example, if a memory from May 4, 2022 mentions "went to India last year," then the trip occurred in 2021. Include this calculated time information in the memory metadata (year, month, day).
|
||||
- **Memory granularity**: Each memory should record ONE complete piece of information - don't pack multiple facts into one memory, and don't split a single fact into multiple memories.
|
||||
- **Self-contained**: Each memory entry must be self-contained and understandable without additional context.
|
||||
|
||||
### Step 2: Retrieve Similar and Recent Memories
|
||||
Use the `RetrieveRecentAndSimilarMemories` tool to find existing related memories.
|
||||
- **For EACH memory draft**, perform a semantic similarity search to find existing, potentially relevant memories.
|
||||
- **Example**: For "Person A was born on date X", search for "Person A birth date age".
|
||||
- **Retrieve comprehensively**: Retrieve all related memories for thorough comparison to prevent any duplication or conflicts.
|
||||
|
||||
### Step 3: Update Memories
|
||||
Use the `UpdateMemories` tool to finalize the memory updates.
|
||||
- **Compare and decide**: Compare the newly extracted memory drafts with the retrieved memories from Step 2.
|
||||
- **CRITICAL DEDUPLICATION CHECK**: Before adding ANY new memory:
|
||||
- Check if the SAME INFORMATION already exists in retrieved memories
|
||||
- Consider memories as duplicates even if wording differs, as long as they convey the SAME core fact
|
||||
- Examples of duplicate information:
|
||||
* "Person A was born on date X. He/She is N years old." vs "Person A is a gender born on date X. He/She is currently N years old." → DUPLICATES
|
||||
* "Lives in city" vs "Person A lives in city" → DUPLICATES
|
||||
* "Holds a Bachelor's degree in field" vs "Person A holds a Bachelor's degree in field" → DUPLICATES
|
||||
|
||||
- **Choose the appropriate operation**:
|
||||
- **If the information already exists and is consistent**: SKIP—fill empty array in `memory_ids_to_delete` and `memories_to_add`. Do NOT add duplicate memories.
|
||||
- **If existing memory needs supplementation with NEW details**: Delete the old memory (add its ID to `memory_ids_to_delete`), then add the enhanced consolidated version to `memories_to_add`.
|
||||
- **If existing memory is outdated or contradicted**: Delete it (add ID to `memory_ids_to_delete`), then add the corrected version to `memories_to_add`.
|
||||
- **If multiple memories contain similar/overlapping information**: Delete all duplicates (add IDs to `memory_ids_to_delete`), then add one merged memory to `memories_to_add`.
|
||||
- **If the information is entirely new**: Fill empty array in `memory_ids_to_delete`, and add the new memory to `memories_to_add`.
|
||||
|
||||
user_message: |
|
||||
Please analyze the context and update the memory store following the three-step workflow:
|
||||
1. First use `AddMemoryDrafts` to generate initial memory drafts
|
||||
2. Then use `RetrieveRecentAndSimilarMemories` to find related existing memories
|
||||
3. Finally use `UpdateMemories` to remove outdated memories and add new consolidated memories
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
tool: |
|
||||
Extract and store personal memories from conversation context using a three-step workflow.
|
||||
Use this tool to analyze dialogues and extract important personal information about users,
|
||||
such as preferences, habits, personal background, relationships, and significant facts.
|
||||
|
||||
system_prompt: |
|
||||
You are a professional memory agent. Your task is to update the main agent's {memory_type} memory regarding {memory_target} based on the context.
|
||||
|
||||
**CRITICAL**: You must extract and store information STRICTLY based on what is explicitly stated in the context. DO NOT infer, assume, fabricate, or add any information that is not directly present in the dialogue. Only extract facts that are clearly and explicitly mentioned.
|
||||
|
||||
## Context:
|
||||
{context}
|
||||
|
||||
**Context Format Explanation**:
|
||||
The context contains formatted conversation messages in the following structure:
|
||||
- Each message is formatted as: `round{index} [{timestamp}] {role/name}: {content}`
|
||||
- The timestamp is in format: `YYYY-MM-DD HH:MM:SS`
|
||||
- Content may include reasoning, tool calls
|
||||
- **Time metadata handling**: When extracting memories with time information, store year/month/day in the metadata. For relative time references (e.g., "last year", "two months ago"), calculate the actual date based on the message's timestamp and store the calculated year/month/day in metadata
|
||||
|
||||
## Memory Objective:
|
||||
You are managing **{memory_type}** memories about **{memory_target}** for the main agent. Focus on extracting and storing information directly related to this person's preferences, habits, personal background, and significant facts.
|
||||
|
||||
## Your Tasks - Three-Step Workflow:
|
||||
|
||||
### Step 1: Generate Memory Drafts
|
||||
Use the `AddMemoryDrafts` tool to produce a set of non-redundant, self-contained memory drafts that capture all important information explicitly stated in the context. Each draft should record ONE complete fact with accurate time metadata (year, month, day) when time references are mentioned. If no valuable information exists, output `<NO_MEMORY_NEEDED>` and stop.
|
||||
|
||||
### Step 2: Retrieve Similar and Recent Memories
|
||||
Use the `RetrieveRecentAndSimilarMemories` tool to obtain all existing memories that are semantically related to each memory draft, ensuring comprehensive coverage for deduplication and conflict detection.
|
||||
|
||||
### Step 3: Update Memories
|
||||
Use the `UpdateMemories` tool to produce a final, non-redundant memory set where:
|
||||
- `memory_ids_to_delete` contains IDs of memories that are duplicates, outdated, or being consolidated
|
||||
- `memories_to_add` contains new or updated memories that preserve all information without redundancy or conflicts
|
||||
|
||||
## Guidelines:
|
||||
- **Be selective**: Store only truly important information.
|
||||
- **Stay concise**: Each memory should be clear and atomic, recording ONE complete piece of information.
|
||||
- **Be strictly accurate**: Ensure extracted content faithfully reflects ONLY what is explicitly stated in the original context. DO NOT infer, extrapolate, or fabricate any details.
|
||||
- **AVOID REDUNDANCY AT ALL COSTS**: This is your TOP PRIORITY. Always perform thorough deduplication:
|
||||
* First, deduplicate within newly extracted memory drafts
|
||||
* Then, check against retrieved memories from Step 2
|
||||
* Actively use `memory_ids_to_delete` to remove duplicate or conflicting memories
|
||||
* Use `memories_to_add` to consolidate and integrate information from multiple memories into one
|
||||
* If information semantically matches existing memories, DO NOT add it again
|
||||
* When uncertain, prefer to skip or update existing memories rather than create duplicates
|
||||
- **Include relevant metadata**: Include time-related metadata (year, month, day) when appropriate, especially when time references are mentioned.
|
||||
- **No assumptions**: Only store information that is directly and clearly stated in the conversation.
|
||||
- **Quality over quantity**: It's better to have fewer, well-maintained memories than many duplicate ones.
|
||||
|
||||
user_message: |
|
||||
Please update the memory store following the three-step workflow. If there is no valuable information to remember, output `<NO_MEMORY_NEEDED>` without calling any tools.
|
||||
94
reme_ai/mem_agent/summarizer_v2/reme_summarizer_v2.py
Normal file
94
reme_ai/mem_agent/summarizer_v2/reme_summarizer_v2.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""Simplified orchestrator for memory summarization workflow - V2."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReMeSummarizerV2(BaseMemoryAgent):
|
||||
"""Simplified version that coordinates memory updates using only summary_and_hands_off tool."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
"""Initialize with meta memories list."""
|
||||
super().__init__(**kwargs)
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": self.prompt_format("tool"),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "role",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "content",
|
||||
},
|
||||
},
|
||||
"required": ["role", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["messages"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
"""Fetch meta-memory entries using format_memory_metadata."""
|
||||
from ...mem_tool import ReadMetaMemory
|
||||
|
||||
return ReadMetaMemory().format_memory_metadata(self.meta_memories)
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Construct initial messages with context and meta-memory information."""
|
||||
messages = [Message(**m) if isinstance(m, dict) else m for m in self.context.messages]
|
||||
self.context["messages_formated"] = self.description + "\n" + format_messages(messages)
|
||||
self.context["ref_memory_id"] = MemoryNode(
|
||||
memory_type=MemoryType.HISTORY,
|
||||
content=self.context["messages_formated"],
|
||||
).memory_id
|
||||
|
||||
meta_memory_info = await self._read_meta_memories()
|
||||
logger.info(f"meta_memory_info={meta_memory_info}")
|
||||
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=meta_memory_info,
|
||||
context=self.context["messages_formated"],
|
||||
)
|
||||
|
||||
user_message = self.get_prompt("user_message")
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content=system_prompt),
|
||||
Message(role=Role.USER, content=user_message),
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with ref_memory_id and author context."""
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
messages=self.context.get("messages", []),
|
||||
description=self.context.get("description"),
|
||||
ref_memory_id=self.context["ref_memory_id"],
|
||||
messages_formated=self.context["messages_formated"],
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
25
reme_ai/mem_agent/summarizer_v2/reme_summarizer_v2.yaml
Normal file
25
reme_ai/mem_agent/summarizer_v2/reme_summarizer_v2.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
tool: |
|
||||
Orchestrate the complete memory summarization for the agent.
|
||||
|
||||
system_prompt: |
|
||||
You are a Memory Agent responsible for performing necessary updates and summaries of the main Agent's memories based on the **context**.
|
||||
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Main Agent's Meta Memory
|
||||
Each line of meta memory indicates the existence of a specialized Memory Agent dedicated to deep summarization and updating of memories within a specific dimension (memory_type + memory_target).
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Your Task
|
||||
Use `summary_and_hands_off` tool to:
|
||||
1. Create a concise summary in `summary_content` that captures key points, decisions, or important facts from the context.
|
||||
2. Identify which memory dimensions need updates and specify them in `memory_tasks` (each with `memory_type` and `memory_target`).
|
||||
- The `memory_type` and `memory_target` must exactly match existing entries in the "Main Agent's Meta Memory" listed above.
|
||||
- Multiple tasks can be specified to enable parallel processing by specialized agents.
|
||||
|
||||
Note: If the context contains no memorable information (e.g., simple greetings), output `<NO_MEMORY_NEEDED>`.
|
||||
|
||||
user_message: |
|
||||
Please perform your task based on the context.
|
||||
|
|
@ -33,9 +33,13 @@ class BaseMemoryTool(BaseOp, metaclass=ABCMeta):
|
|||
def _build_multiple_parameters(self) -> dict:
|
||||
return {}
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
"""Build tool description."""
|
||||
return self.get_prompt("tool" + ("_multiple" if self.enable_multiple else ""))
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
tool_call_params: dict = {
|
||||
"description": self.get_prompt("tool" + ("_multiple" if self.enable_multiple else "")),
|
||||
"description": self._build_tool_description(),
|
||||
}
|
||||
|
||||
if self.enable_multiple:
|
||||
|
|
@ -50,7 +54,7 @@ class BaseMemoryTool(BaseOp, metaclass=ABCMeta):
|
|||
parameters["properties"] = {
|
||||
"thinking": {
|
||||
"type": "string",
|
||||
"description": "Your thinking and reasoning about how to fill in the parameters",
|
||||
"description": "Your complete and detailed thinking process about how to fill in each parameter",
|
||||
},
|
||||
**parameters["properties"],
|
||||
}
|
||||
|
|
|
|||
17
reme_ai/mem_tool/v2/__init__.py
Normal file
17
reme_ai/mem_tool/v2/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""Version 2 memory tools with enhanced functionality."""
|
||||
|
||||
from .add_memory_drafts import AddMemoryDrafts
|
||||
from .read_history import ReadHistory
|
||||
from .retrieve_memories import RetrieveMemories
|
||||
from .retrieve_recent_and_similar_memories import RetrieveRecentAndSimilarMemories
|
||||
from .summary_and_hands_off import SummaryAndHandsOff
|
||||
from .update_memories import UpdateMemories
|
||||
|
||||
__all__ = [
|
||||
"AddMemoryDrafts",
|
||||
"ReadHistory",
|
||||
"RetrieveMemories",
|
||||
"RetrieveRecentAndSimilarMemories",
|
||||
"SummaryAndHandsOff",
|
||||
"UpdateMemories",
|
||||
]
|
||||
130
reme_ai/mem_tool/v2/add_memory_drafts.py
Normal file
130
reme_ai/mem_tool/v2/add_memory_drafts.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Add memory drafts operation for vector store."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class AddMemoryDrafts(BaseMemoryTool):
|
||||
"""Add memory drafts without persisting them to the database.
|
||||
|
||||
This tool is useful for creating draft memories that can be reviewed and modified
|
||||
before final submission. Drafts are not persisted to the vector store.
|
||||
Metadata fields can be customized via `metadata_desc` parameter.
|
||||
"""
|
||||
|
||||
def __init__(self, add_when_to_use: bool = False, metadata_desc: dict[str, str] | None = None, **kwargs):
|
||||
"""Initialize AddMemoryDrafts.
|
||||
|
||||
Args:
|
||||
add_when_to_use: Include when_to_use field for better retrieval. Defaults to True.
|
||||
metadata_desc: Dictionary defining metadata fields and their descriptions.
|
||||
**kwargs: Additional arguments for BaseMemoryTool.
|
||||
"""
|
||||
kwargs["enable_multiple"] = True
|
||||
super().__init__(**kwargs)
|
||||
self.add_when_to_use: bool = add_when_to_use
|
||||
self.metadata_desc: dict[str, str] = metadata_desc or {}
|
||||
|
||||
def _build_item_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build shared schema properties and required fields for memory items to add.
|
||||
|
||||
Returns:
|
||||
Tuple of (properties dict, required fields list).
|
||||
"""
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
if self.add_when_to_use:
|
||||
properties["when_to_use"] = {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("when_to_use"),
|
||||
}
|
||||
required.append("when_to_use")
|
||||
|
||||
properties["memory_content"] = {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_content"),
|
||||
}
|
||||
required.append("memory_content")
|
||||
|
||||
# Add metadata field if metadata_desc is provided and not empty
|
||||
if self.metadata_desc:
|
||||
metadata_properties = {
|
||||
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
|
||||
}
|
||||
properties["metadata"] = {
|
||||
"type": "object",
|
||||
"description": "metadata",
|
||||
"properties": metadata_properties,
|
||||
}
|
||||
required.append("metadata")
|
||||
|
||||
return properties, required
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for add drafts operation.
|
||||
|
||||
Only supports batch mode for adding draft memories.
|
||||
"""
|
||||
item_properties, required_fields = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_drafts": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memory_drafts"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": required_fields,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memory_drafts"],
|
||||
}
|
||||
|
||||
def _extract_memory_data(self, mem_dict: dict) -> tuple[str, str, dict]:
|
||||
"""Extract memory data from a dictionary with proper defaults.
|
||||
|
||||
Args:
|
||||
mem_dict: Dictionary containing memory fields.
|
||||
|
||||
Returns:
|
||||
Tuple of (memory_content, when_to_use, metadata).
|
||||
"""
|
||||
memory_content = mem_dict.get("memory_content", "")
|
||||
when_to_use = mem_dict.get("when_to_use", "") if self.add_when_to_use else ""
|
||||
metadata = mem_dict.get("metadata", {}) if self.metadata_desc else {}
|
||||
return memory_content, when_to_use, metadata
|
||||
|
||||
async def execute(self):
|
||||
"""Execute add drafts operation: create memory drafts without persisting to vector store."""
|
||||
# Get memory drafts to add
|
||||
memory_drafts = self.context.get("memory_drafts", [])
|
||||
|
||||
# Validate input
|
||||
if not memory_drafts:
|
||||
self.output = "No memory drafts provided. Please provide at least one draft memory."
|
||||
return
|
||||
|
||||
# Build memory nodes (without persisting)
|
||||
memory_nodes = []
|
||||
for mem in memory_drafts:
|
||||
memory_content, when_to_use, metadata = self._extract_memory_data(mem)
|
||||
if not memory_content:
|
||||
logger.warning("Skipping memory draft with empty content")
|
||||
continue
|
||||
|
||||
memory_nodes.append(self._build_memory_node(memory_content, when_to_use=when_to_use, metadata=metadata))
|
||||
|
||||
if memory_nodes:
|
||||
self.memory_nodes.extend(memory_nodes)
|
||||
draft_count = len(memory_nodes)
|
||||
self.output = f"Successfully created {draft_count} memory draft(s). These drafts are not yet persisted to the vector store."
|
||||
logger.info(self.output)
|
||||
else:
|
||||
self.output = "No valid memory drafts created. Please check your input."
|
||||
logger.warning(self.output)
|
||||
18
reme_ai/mem_tool/v2/add_memory_drafts.yaml
Normal file
18
reme_ai/mem_tool/v2/add_memory_drafts.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
tool_multiple: |
|
||||
Create draft memories for initial recording of information.
|
||||
Use this tool to quickly capture information as drafts that can be reviewed or modified later.
|
||||
**CRITICAL**: Only add memories based on explicitly stated facts. DO NOT store inferred, assumed, or fabricated information.
|
||||
|
||||
memory_drafts: |
|
||||
A list of draft memory objects to create.
|
||||
Each draft represents a piece of information to be recorded initially.
|
||||
|
||||
when_to_use: |
|
||||
When to retrieve this memory.
|
||||
This field is used for vector embedding to improve retrieval accuracy by providing contextual information.
|
||||
|
||||
memory_content: |
|
||||
The content of the memory draft to record.
|
||||
Should be a clear, concise statement that captures the information to remember.
|
||||
Keep it focused on a single piece of information for better retrieval accuracy.
|
||||
**Must be strictly accurate and based only on explicitly stated facts - no inference or fabrication.**
|
||||
57
reme_ai/mem_tool/v2/read_history.py
Normal file
57
reme_ai/mem_tool/v2/read_history.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""Read history memory operation."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReadHistory(BaseMemoryTool):
|
||||
"""Read original history dialogue by reference memory ID.
|
||||
|
||||
Only supports single memory read (enable_multiple=False).
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize ReadHistory.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional args for BaseMemoryTool.
|
||||
"""
|
||||
# Force disable multiple mode
|
||||
kwargs["enable_multiple"] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref_memory_id": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("ref_memory_id"),
|
||||
},
|
||||
},
|
||||
"required": ["ref_memory_id"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
ref_memory_id = self.context.get("ref_memory_id", "")
|
||||
|
||||
if not ref_memory_id:
|
||||
self.output = "No valid reference memory ID provided."
|
||||
logger.warning(self.output)
|
||||
return
|
||||
|
||||
# Query history dialogue by ref_memory_id
|
||||
nodes = await self.vector_store.get(vector_ids=[ref_memory_id])
|
||||
|
||||
if not nodes:
|
||||
self.output = f"No history memory found with ID: {ref_memory_id}"
|
||||
logger.warning(self.output)
|
||||
return
|
||||
|
||||
memory = MemoryNode.from_vector_node(nodes[0])
|
||||
self.output = memory.content
|
||||
logger.info(f"Successfully read history memory: {ref_memory_id}")
|
||||
5
reme_ai/mem_tool/v2/read_history.yaml
Normal file
5
reme_ai/mem_tool/v2/read_history.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
tool: |
|
||||
Read original history dialogue by reference memory ID.
|
||||
|
||||
ref_memory_id: |
|
||||
Reference memory ID to query the original history dialogue.
|
||||
188
reme_ai/mem_tool/v2/retrieve_memories.py
Normal file
188
reme_ai/mem_tool/v2/retrieve_memories.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""Retrieve memories using vector similarity search with multiple queries."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.schema import MemoryNode, VectorNode
|
||||
from ...core.utils import deduplicate_memories
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class RetrieveMemories(BaseMemoryTool):
|
||||
"""Retrieve memories using vector similarity search with multiple queries.
|
||||
|
||||
Always requires memory_type/memory_target in the schema.
|
||||
Only supports multiple query mode (enable_multiple=True).
|
||||
Metadata filters can be customized via `metadata_desc` parameter for pre-retrieval filtering.
|
||||
"""
|
||||
|
||||
def __init__(self, metadata_desc: dict[str, str] | None = None, top_k: int = 20, **kwargs):
|
||||
"""Initialize RetrieveMemories.
|
||||
|
||||
Args:
|
||||
metadata_desc: Dictionary defining metadata filter fields and their descriptions.
|
||||
These fields will be used as filters in vector search before similarity matching.
|
||||
top_k: Max memories to retrieve per query.
|
||||
**kwargs: Additional args for BaseMemoryTool.
|
||||
"""
|
||||
kwargs["enable_multiple"] = True
|
||||
super().__init__(**kwargs)
|
||||
self.metadata_desc: dict[str, str] = metadata_desc or {}
|
||||
self.top_k: int = top_k
|
||||
|
||||
def _build_query_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build schema properties and required fields for query items.
|
||||
|
||||
Returns:
|
||||
Tuple of (properties dict, required fields list).
|
||||
"""
|
||||
properties = {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_type"),
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_target"),
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("query"),
|
||||
},
|
||||
}
|
||||
required = ["memory_type", "memory_target", "query"]
|
||||
|
||||
# Add metadata filter fields if metadata_desc is provided and not empty
|
||||
if self.metadata_desc:
|
||||
metadata_properties = {
|
||||
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
|
||||
}
|
||||
# Generate dynamic description based on metadata_desc fields
|
||||
field_descriptions = "\n".join([f" - {key}: {desc}" for key, desc in self.metadata_desc.items()])
|
||||
metadata_description = (
|
||||
f"Optional metadata filters for narrowing search results. Available fields:\n{field_descriptions}"
|
||||
)
|
||||
|
||||
properties["metadata_filters"] = {
|
||||
"type": "object",
|
||||
"description": metadata_description,
|
||||
"properties": metadata_properties,
|
||||
}
|
||||
|
||||
return properties, required
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for multiple query mode.
|
||||
|
||||
Returns:
|
||||
Schema with query_items array. Each item has memory_type/memory_target/query.
|
||||
"""
|
||||
item_properties, item_required = self._build_query_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_items": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("query_items"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": item_required,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
}
|
||||
|
||||
async def _retrieve_by_query(
|
||||
self,
|
||||
memory_type: str,
|
||||
memory_target: str,
|
||||
query: str,
|
||||
metadata_filters: dict | None = None,
|
||||
) -> list[MemoryNode]:
|
||||
"""Retrieve memories by query using vector similarity search.
|
||||
|
||||
Args:
|
||||
memory_type: Memory type to search.
|
||||
memory_target: Memory target to search.
|
||||
query: Query string for similarity search.
|
||||
metadata_filters: Optional metadata filters to narrow search results.
|
||||
|
||||
Returns:
|
||||
List of matching memories.
|
||||
"""
|
||||
filter_dict = {
|
||||
"memory_type": [memory_type],
|
||||
"memory_target": [memory_target],
|
||||
}
|
||||
|
||||
# Add metadata filters if provided
|
||||
if metadata_filters:
|
||||
for key, value in metadata_filters.items():
|
||||
if value: # Only add non-empty filter values
|
||||
value = str(value).strip()
|
||||
filter_dict[key] = [value] if not isinstance(value, list) else value
|
||||
|
||||
nodes: list[VectorNode] = await self.vector_store.search(query=query, limit=self.top_k, filters=filter_dict)
|
||||
memory_nodes: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
return memory_nodes
|
||||
|
||||
async def execute(self):
|
||||
"""Execute memory retrieval based on multiple query items.
|
||||
|
||||
Outputs formatted results or error message.
|
||||
"""
|
||||
query_items: list[dict] = self.context.get("query_items", [])
|
||||
if not query_items:
|
||||
self.output = "No query items provided for retrieval."
|
||||
return
|
||||
|
||||
# Filter out items without query text
|
||||
query_items = [item for item in query_items if item.get("query")]
|
||||
|
||||
if not query_items:
|
||||
self.output = "No valid query texts provided for retrieval."
|
||||
return
|
||||
|
||||
# Retrieve memory_nodes for all queries
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for item in query_items:
|
||||
memory_type = item.get("memory_type")
|
||||
memory_target = item.get("memory_target")
|
||||
metadata_filters = item.get("metadata_filters", {}) if self.metadata_desc else {}
|
||||
|
||||
if not memory_type or not memory_target:
|
||||
logger.warning(f"Skipping query with missing memory_type or memory_target: {item}")
|
||||
continue
|
||||
|
||||
retrieved = await self._retrieve_by_query(
|
||||
memory_type=memory_type,
|
||||
memory_target=memory_target,
|
||||
query=item["query"],
|
||||
metadata_filters=metadata_filters,
|
||||
)
|
||||
memory_nodes.extend(retrieved)
|
||||
|
||||
# Deduplicate and format output
|
||||
memory_nodes = deduplicate_memories(memory_nodes)
|
||||
|
||||
# Build set of historical memory_ids for fast lookup
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
|
||||
# Filter out already retrieved memories by memory_id
|
||||
new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
|
||||
# Update retrieved_nodes in context with new memories
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
|
||||
# Set output to new memories only (after deduplication)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
self.output = "No new memories found matching the queries (duplicates removed)."
|
||||
else:
|
||||
self.output = "\n".join([m.format_memory() for m in new_memory_nodes])
|
||||
|
||||
logger.info(f"Retrieved {len(memory_nodes)} memories, {len(new_memory_nodes)} new after deduplication")
|
||||
24
reme_ai/mem_tool/v2/retrieve_memories.yaml
Normal file
24
reme_ai/mem_tool/v2/retrieve_memories.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
tool_multiple: |
|
||||
Retrieve memories from the memory store using multiple queries with vector similarity search.
|
||||
Use this tool to find relevant memories based on semantic similarity to multiple queries.
|
||||
This is useful when you need to search for different types of information in a single operation.
|
||||
The search returns the most relevant memories ranked by similarity score for each query.
|
||||
|
||||
Note: Within the same session, this tool automatically deduplicates results across multiple calls.
|
||||
If you call this tool multiple times, only new memories (not previously retrieved) will be returned.
|
||||
This prevents redundant information in subsequent retrievals.
|
||||
|
||||
memory_type: |
|
||||
The type of memory to search for.
|
||||
You MUST select one of the memory_type values that are explicitly provided in the Available Meta-Memories.
|
||||
|
||||
memory_target: |
|
||||
The target of the memory to search within.
|
||||
You MUST select one of the memory_type values that are explicitly provided in the Available Meta-Memories.
|
||||
|
||||
query: |
|
||||
The query text for vector similarity search.
|
||||
Use descriptive queries that capture the semantic meaning of what you're looking for.
|
||||
|
||||
query_items: |
|
||||
A list of query items for vector similarity search.
|
||||
176
reme_ai/mem_tool/v2/retrieve_recent_and_similar_memories.py
Normal file
176
reme_ai/mem_tool/v2/retrieve_recent_and_similar_memories.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Combined memory retrieval: recent + vector similarity search."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.schema import MemoryNode, VectorNode
|
||||
from ...core.utils import deduplicate_memories
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class RetrieveRecentAndSimilarMemories(BaseMemoryTool):
|
||||
"""Retrieve memories using both time-based and vector similarity search.
|
||||
|
||||
First retrieves recent_top_k memories sorted by modification time,
|
||||
then retrieves similar_top_k memories using vector similarity search.
|
||||
Uses memory_type and memory_target from context (self.memory_type, self.memory_target).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
recent_top_k: int = 20,
|
||||
similar_top_k: int = 20,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize RetrieveRecentAndSimilarMemories.
|
||||
|
||||
Args:
|
||||
recent_top_k: Max recent memories to retrieve by time.
|
||||
similar_top_k: Max similar memories to retrieve by vector search.
|
||||
**kwargs: Additional args for BaseMemoryTool.
|
||||
"""
|
||||
kwargs["enable_multiple"] = True
|
||||
super().__init__(**kwargs)
|
||||
self.recent_top_k: int = recent_top_k
|
||||
self.similar_top_k: int = similar_top_k
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
"""Build tool description."""
|
||||
return self.prompt_format("tool_multiple",
|
||||
recent_top_k=self.recent_top_k,
|
||||
similar_top_k=self.similar_top_k)
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for multiple query mode.
|
||||
|
||||
Returns:
|
||||
Schema with query_items array.
|
||||
"""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_items": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("query_items"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("query"),
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
}
|
||||
|
||||
async def _retrieve_recent(self) -> list[MemoryNode]:
|
||||
"""Retrieve recent memories sorted by time_modified.
|
||||
|
||||
Returns:
|
||||
List of recent memories sorted by modification time (newest first).
|
||||
"""
|
||||
filter_dict = {
|
||||
"memory_type": [self.memory_type.value],
|
||||
"memory_target": [self.memory_target],
|
||||
}
|
||||
|
||||
# Use list() with sort_key="time_modified", reverse=True (descending), and limit
|
||||
nodes: list[VectorNode] = await self.vector_store.list(
|
||||
filters=filter_dict,
|
||||
limit=self.recent_top_k,
|
||||
sort_key="time_modified",
|
||||
reverse=True, # Most recent first (descending order)
|
||||
)
|
||||
|
||||
memory_nodes: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
return memory_nodes
|
||||
|
||||
async def _retrieve_by_query(
|
||||
self,
|
||||
query: str,
|
||||
) -> list[MemoryNode]:
|
||||
"""Retrieve memories by query using vector similarity search.
|
||||
|
||||
Args:
|
||||
query: Query string for similarity search.
|
||||
|
||||
Returns:
|
||||
List of matching memories.
|
||||
"""
|
||||
filter_dict = {
|
||||
"memory_type": [self.memory_type.value],
|
||||
"memory_target": [self.memory_target],
|
||||
}
|
||||
|
||||
nodes: list[VectorNode] = await self.vector_store.search(
|
||||
query=query, limit=self.similar_top_k, filters=filter_dict
|
||||
)
|
||||
|
||||
memory_nodes: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
return memory_nodes
|
||||
|
||||
async def execute(self):
|
||||
"""Execute combined memory retrieval (recent + similar).
|
||||
|
||||
First retrieves recent_top_k memories by time, then retrieves similar_top_k
|
||||
memories by vector similarity for each query in query_items.
|
||||
Uses memory_type and memory_target from context. Outputs formatted results or error message.
|
||||
"""
|
||||
if not self.memory_type or not self.memory_target:
|
||||
raise RuntimeError("memory_type and memory_target are required for retrieval.")
|
||||
|
||||
# Get query items
|
||||
query_items: list[dict] = self.context.get("query_items", [])
|
||||
if not query_items:
|
||||
self.output = "No query items provided for retrieval."
|
||||
return
|
||||
|
||||
# Filter out items without query text
|
||||
query_items = [item for item in query_items if item.get("query")]
|
||||
|
||||
if not query_items:
|
||||
self.output = "No valid query texts provided for retrieval."
|
||||
return
|
||||
|
||||
# Step 1: Retrieve recent memories (once, shared across all queries)
|
||||
recent_memory_nodes: list[MemoryNode] = await self._retrieve_recent()
|
||||
logger.info(f"Retrieved {len(recent_memory_nodes)} recent memories")
|
||||
|
||||
# Step 2: Retrieve similar memories by vector search for all queries
|
||||
similar_memory_nodes: list[MemoryNode] = []
|
||||
for item in query_items:
|
||||
retrieved = await self._retrieve_by_query(query=item["query"])
|
||||
similar_memory_nodes.extend(retrieved)
|
||||
# Combine and deduplicate all memories
|
||||
all_memory_nodes = recent_memory_nodes + similar_memory_nodes
|
||||
all_memory_nodes = deduplicate_memories(all_memory_nodes)
|
||||
|
||||
# Build set of historical memory_ids for fast lookup
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
|
||||
# Filter out already retrieved memories by memory_id
|
||||
new_memory_nodes = [node for node in all_memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
|
||||
# Update retrieved_nodes in context with new memories
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
|
||||
# Set output to new memories only (after deduplication)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
self.output = "No new memory_nodes found (duplicates removed)."
|
||||
else:
|
||||
self.output = "\n".join([m.format_memory() for m in new_memory_nodes])
|
||||
|
||||
logger.info(
|
||||
f"Retrieved {len(all_memory_nodes)} total memories "
|
||||
f"({len(recent_memory_nodes)} recent + {len(similar_memory_nodes)} similar), "
|
||||
f"{len(new_memory_nodes)} new after deduplication"
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
tool_multiple: |
|
||||
Retrieve memories using both time-based and multiple vector similarity searches.
|
||||
|
||||
This tool combines two retrieval strategies:
|
||||
1. First retrieves the most recent memories based on modification time (recent top {recent_top_k})
|
||||
2. Then retrieves semantically similar memories for each of your queries (similar top {similar_top_k} per query)
|
||||
|
||||
This is useful when you need to search for different types of information in a single operation,
|
||||
while also considering recent context.
|
||||
|
||||
The results are automatically deduplicated, so you get a combined set of both recent
|
||||
and relevant memories without duplicates.
|
||||
|
||||
Note: Within the same session, this tool automatically deduplicates results across multiple calls.
|
||||
If you call this tool multiple times, only new memories (not previously retrieved) will be returned.
|
||||
This prevents redundant information in subsequent retrievals.
|
||||
|
||||
query: |
|
||||
The query text for vector similarity search.
|
||||
Use descriptive queries that capture the semantic meaning of what you're looking for.
|
||||
|
||||
query_items: |
|
||||
A list of query items for vector similarity search.
|
||||
Each query will be used to find semantically similar memories, which will be combined
|
||||
with the recent memories retrieved based on modification time.
|
||||
158
reme_ai/mem_tool/v2/summary_and_hands_off.py
Normal file
158
reme_ai/mem_tool/v2/summary_and_hands_off.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""Summary and hands-off tool for distributing summarized memory to appropriate agents."""
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...mem_agent import BaseMemoryAgent
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class SummaryAndHandsOff(BaseMemoryTool):
|
||||
"""Distribute summarized memory task to appropriate agent based on memory_type."""
|
||||
|
||||
def __init__(self, memory_agents: list["BaseMemoryAgent"], **kwargs):
|
||||
# Force enable_multiple to True since this tool only supports multiple tasks
|
||||
kwargs["enable_multiple"] = True
|
||||
kwargs["sub_ops"] = memory_agents or []
|
||||
super().__init__(**kwargs)
|
||||
from ...mem_agent import BaseMemoryAgent
|
||||
|
||||
self.sub_ops: list[BaseMemoryAgent] = [a for a in self.sub_ops if isinstance(a, BaseMemoryAgent)]
|
||||
|
||||
@property
|
||||
def memory_agent_dict(self) -> dict[MemoryType, "BaseMemoryAgent"]:
|
||||
"""Returns a dictionary mapping memory types to their corresponding agents."""
|
||||
return {a.memory_type: a for a in self.sub_ops}
|
||||
|
||||
def _build_item_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build shared schema properties and required fields for memory tasks."""
|
||||
properties = {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_type"),
|
||||
"enum": [k.value for k in self.memory_agent_dict],
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_target"),
|
||||
},
|
||||
}
|
||||
required = ["memory_type", "memory_target"]
|
||||
return properties, required
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for multiple summary and hands-off tasks."""
|
||||
item_properties, required_fields = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary_content": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("summary_content"),
|
||||
},
|
||||
"memory_tasks": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memory_tasks"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": required_fields,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["summary_content", "memory_tasks"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_memory_type_target(task: dict):
|
||||
memory_type = task.get("memory_type", "")
|
||||
memory_target = task.get("memory_target", "")
|
||||
return {"memory_type": MemoryType(memory_type), "memory_target": memory_target}
|
||||
|
||||
def _collect_tasks(self) -> list[dict]:
|
||||
"""Collect memory tasks from context."""
|
||||
tasks: list[dict] = []
|
||||
memory_tasks: list[dict] = self.context.get("memory_tasks", [])
|
||||
for task in memory_tasks:
|
||||
tasks.append(self._parse_memory_type_target(task))
|
||||
return tasks
|
||||
|
||||
async def execute(self):
|
||||
"""Execute memory tasks by distributing to appropriate agents in parallel."""
|
||||
summary_content = self.context.get("summary_content", "")
|
||||
assert summary_content, "No summary content provided."
|
||||
|
||||
# Build and store summary node
|
||||
summary_node = MemoryNode(
|
||||
memory_type=MemoryType.HISTORY,
|
||||
memory_target="",
|
||||
when_to_use=summary_content,
|
||||
content=self.messages_formated,
|
||||
ref_memory_id="",
|
||||
author=self.author,
|
||||
metadata={},
|
||||
)
|
||||
logger.info(f"Adding summary node: {summary_node.model_dump_json(indent=2, exclude_none=True)}")
|
||||
self.memory_nodes.append(summary_node)
|
||||
vector_node = summary_node.to_vector_node()
|
||||
await self.vector_store.delete(vector_ids=[vector_node.vector_id])
|
||||
await self.vector_store.insert([vector_node])
|
||||
|
||||
# Collect tasks
|
||||
tasks = self._collect_tasks()
|
||||
if not tasks:
|
||||
self.output = "No valid memory tasks to execute."
|
||||
return
|
||||
|
||||
# Submit tasks to corresponding agents
|
||||
agent_list = []
|
||||
for i, task in enumerate(tasks):
|
||||
memory_type: MemoryType = task["memory_type"]
|
||||
memory_target: str = task["memory_target"]
|
||||
|
||||
if memory_type not in self.memory_agent_dict:
|
||||
logger.warning(f"No agent found for memory_type={memory_type}")
|
||||
continue
|
||||
|
||||
agent = self.memory_agent_dict[memory_type].copy()
|
||||
agent_list.append([agent, memory_type, memory_target])
|
||||
|
||||
logger.info(f"Task {i}: Submitting {memory_type.value} agent with summary for target={memory_target}")
|
||||
self.submit_async_task(
|
||||
agent.call,
|
||||
query=self.context.get("query", ""),
|
||||
messages=self.context.get("messages", []),
|
||||
memory_type=memory_type,
|
||||
memory_target=memory_target,
|
||||
description=self.context.get("description"),
|
||||
ref_memory_id=self.context.get("ref_memory_id", ""),
|
||||
)
|
||||
|
||||
await self.join_async_tasks()
|
||||
|
||||
# Collect results
|
||||
results = []
|
||||
for i, (agent, memory_type, memory_target) in enumerate(agent_list):
|
||||
result_str = str(agent.output)
|
||||
if agent.memory_nodes:
|
||||
self.memory_nodes.extend(agent.memory_nodes)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"memory_type": memory_type.value,
|
||||
"memory_target": memory_target,
|
||||
"result": result_str[:200] + ("..." if len(result_str) > 200 else ""),
|
||||
}
|
||||
)
|
||||
logger.info(f"Task {i}: Completed {memory_type.value} agent for target={memory_target}")
|
||||
|
||||
results_str = json.dumps(results, ensure_ascii=False, indent=2)
|
||||
self.output = f"Successfully executed summary and {len(results)} hands-off task(s):\n{results_str}"
|
||||
18
reme_ai/mem_tool/v2/summary_and_hands_off.yaml
Normal file
18
reme_ai/mem_tool/v2/summary_and_hands_off.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
tool_multiple: |
|
||||
Summarize and distribute memory tasks to appropriate agents in parallel.
|
||||
Use this tool when you have already summarized the content and need to hand it off to specialized agents.
|
||||
Each task will be processed by its corresponding memory agent based on memory_type.
|
||||
|
||||
summary_content: |
|
||||
The summarized content to be stored as memory.
|
||||
Should be a clear, concise summary that captures the key information.
|
||||
|
||||
memory_type: |
|
||||
The type of memory to process. Determines which specialized agent handles the task.
|
||||
|
||||
memory_target: |
|
||||
The target entity for this memory.
|
||||
This helps the agent focus on the specific subject of the memory task.
|
||||
|
||||
memory_tasks: |
|
||||
A list of memory tasks to distribute, each with memory_type and memory_target.
|
||||
169
reme_ai/mem_tool/v2/update_memories.py
Normal file
169
reme_ai/mem_tool/v2/update_memories.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
"""Update memories operation for vector store."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class UpdateMemories(BaseMemoryTool):
|
||||
"""Update memories by removing old ones and adding new ones in a single atomic operation.
|
||||
|
||||
This tool is useful for updating memories when you need to remove outdated information
|
||||
and add updated information at the same time. Only supports batch mode (multiple operations).
|
||||
Metadata fields can be customized via `metadata_desc` parameter.
|
||||
"""
|
||||
|
||||
def __init__(self, add_when_to_use: bool = False, metadata_desc: dict[str, str] | None = None, **kwargs):
|
||||
"""Initialize UpdateMemories.
|
||||
|
||||
Args:
|
||||
add_when_to_use: Include when_to_use field for better retrieval. Defaults to True.
|
||||
metadata_desc: Dictionary defining metadata fields and their descriptions.
|
||||
**kwargs: Additional arguments for BaseMemoryTool.
|
||||
"""
|
||||
kwargs["enable_multiple"] = True
|
||||
super().__init__(**kwargs)
|
||||
self.add_when_to_use: bool = add_when_to_use
|
||||
self.metadata_desc: dict[str, str] = metadata_desc or {}
|
||||
|
||||
def _build_item_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build shared schema properties and required fields for memory items to add.
|
||||
|
||||
Returns:
|
||||
Tuple of (properties dict, required fields list).
|
||||
"""
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
if self.add_when_to_use:
|
||||
properties["when_to_use"] = {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("when_to_use"),
|
||||
}
|
||||
required.append("when_to_use")
|
||||
|
||||
properties["memory_content"] = {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_content"),
|
||||
}
|
||||
required.append("memory_content")
|
||||
|
||||
# Add metadata field if metadata_desc is provided and not empty
|
||||
if self.metadata_desc:
|
||||
metadata_properties = {
|
||||
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
|
||||
}
|
||||
properties["metadata"] = {
|
||||
"type": "object",
|
||||
"description": "metadata",
|
||||
"properties": metadata_properties,
|
||||
}
|
||||
required.append("metadata")
|
||||
|
||||
return properties, required
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for update operation.
|
||||
|
||||
Only supports batch mode with both removal and addition.
|
||||
"""
|
||||
item_properties, required_fields = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_ids_to_delete": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memory_ids_to_delete"),
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"memories_to_add": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memories_to_add"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": required_fields,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memory_ids_to_delete", "memories_to_add"],
|
||||
}
|
||||
|
||||
def _extract_memory_data(self, mem_dict: dict) -> tuple[str, str, dict]:
|
||||
"""Extract memory data from a dictionary with proper defaults.
|
||||
|
||||
Args:
|
||||
mem_dict: Dictionary containing memory fields.
|
||||
|
||||
Returns:
|
||||
Tuple of (memory_content, when_to_use, metadata).
|
||||
"""
|
||||
memory_content = mem_dict.get("memory_content", "")
|
||||
when_to_use = mem_dict.get("when_to_use", "") if self.add_when_to_use else ""
|
||||
metadata = mem_dict.get("metadata", {}) if self.metadata_desc else {}
|
||||
return memory_content, when_to_use, metadata
|
||||
|
||||
async def execute(self):
|
||||
"""Execute update operation: first remove old memories by IDs, then add new updated memories."""
|
||||
# Get removal IDs
|
||||
memory_ids_to_delete = self.context.get("memory_ids_to_delete", [])
|
||||
memory_ids_to_delete = [m for m in memory_ids_to_delete if m]
|
||||
|
||||
# Get memories to add
|
||||
memories_to_add = self.context.get("memories_to_add", [])
|
||||
|
||||
# Validate input
|
||||
if not memory_ids_to_delete and not memories_to_add:
|
||||
self.output = "No memories to remove or add. Please provide at least one operation."
|
||||
return
|
||||
|
||||
removed_count = 0
|
||||
added_count = 0
|
||||
|
||||
# Step 1: Remove old memories
|
||||
if memory_ids_to_delete:
|
||||
await self.vector_store.delete(vector_ids=memory_ids_to_delete)
|
||||
self.memory_nodes.extend(memory_ids_to_delete)
|
||||
removed_count = len(memory_ids_to_delete)
|
||||
logger.info(f"Removed {removed_count} memories from vector_store.")
|
||||
|
||||
# Step 2: Add new updated memories
|
||||
if memories_to_add:
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for mem in memories_to_add:
|
||||
memory_content, when_to_use, metadata = self._extract_memory_data(mem)
|
||||
if not memory_content:
|
||||
logger.warning("Skipping memory with empty content")
|
||||
continue
|
||||
|
||||
memory_nodes.append(self._build_memory_node(memory_content, when_to_use=when_to_use, metadata=metadata))
|
||||
|
||||
if memory_nodes:
|
||||
# Convert to VectorNodes and collect IDs
|
||||
vector_nodes = [node.to_vector_node() for node in memory_nodes]
|
||||
vector_ids: list[str] = [node.vector_id for node in vector_nodes]
|
||||
|
||||
# Delete existing IDs (upsert behavior), then insert
|
||||
await self.vector_store.delete(vector_ids=vector_ids)
|
||||
await self.vector_store.insert(nodes=vector_nodes)
|
||||
added_count = len(memory_nodes)
|
||||
logger.info(f"Added {added_count} new memories to vector_store.")
|
||||
|
||||
self.memory_nodes.extend(memory_nodes)
|
||||
|
||||
# Generate 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")
|
||||
|
||||
if operations:
|
||||
self.output = f"Successfully {' and '.join(operations)} in vector_store."
|
||||
else:
|
||||
self.output = "No valid operations performed. Please check your input."
|
||||
|
||||
logger.info(self.output)
|
||||
27
reme_ai/mem_tool/v2/update_memories.yaml
Normal file
27
reme_ai/mem_tool/v2/update_memories.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
tool_multiple: |
|
||||
Update memories by removing outdated ones and adding new ones in a single atomic operation.
|
||||
Use this tool when you need to update the memory store by:
|
||||
- Removing outdated or incorrect memories
|
||||
- Adding new, updated information to replace the removed memories
|
||||
- Performing a batch update where old memories are replaced with new, accurate information
|
||||
Memory IDs for removal can be obtained from previous memory retrieval results.
|
||||
**CRITICAL**: Only add memories based on explicitly stated facts. DO NOT store inferred, assumed, or fabricated information.
|
||||
|
||||
memory_ids_to_delete: |
|
||||
A list of unique identifiers (memory_ids) of the memories to remove.
|
||||
Each ID should be a valid memory_id obtained from previous memory retrieval or addition operations.
|
||||
These memories will be removed before adding the new updated memories.
|
||||
|
||||
memories_to_add: |
|
||||
A list of new memory objects to add after removal.
|
||||
These memories typically contain the updated information that replaces the removed memories.
|
||||
|
||||
when_to_use: |
|
||||
When to retrieve this memory.
|
||||
This field is used for vector embedding to improve retrieval accuracy by providing contextual information.
|
||||
|
||||
memory_content: |
|
||||
The content of the memory to store.
|
||||
Should be a clear, concise statement that captures the information to remember.
|
||||
Keep it focused on a single piece of information for better retrieval accuracy.
|
||||
**Must be strictly accurate and based only on explicitly stated facts - no inference or fabrication.**
|
||||
119
reme_ai/reme.py
119
reme_ai/reme.py
|
|
@ -10,17 +10,26 @@ from .core.schema import Message
|
|||
from .core.utils import singleton
|
||||
from .core.vector_store import BaseVectorStore
|
||||
from .mem_agent.retriever import ReMeRetriever
|
||||
from .mem_agent.retriever_v2 import ReMeRetrieverV2
|
||||
from .mem_agent.summarizer import ReMeSummarizer, PersonalSummarizer
|
||||
from .mem_agent.summarizer_v2 import ReMeSummarizerV2, PersonalSummarizerV2
|
||||
from .mem_tool import (
|
||||
HandsOffTool,
|
||||
ReadHistoryMemory,
|
||||
AddMetaMemory,
|
||||
AddMemory,
|
||||
AddSummaryMemory,
|
||||
DeleteMemory,
|
||||
UpdateMemory,
|
||||
VectorRetrieveMemory,
|
||||
)
|
||||
from .mem_tool.v2 import (
|
||||
AddMemoryDrafts,
|
||||
ReadHistory,
|
||||
RetrieveMemories,
|
||||
RetrieveRecentAndSimilarMemories,
|
||||
SummaryAndHandsOff,
|
||||
UpdateMemories,
|
||||
)
|
||||
|
||||
|
||||
@singleton
|
||||
|
|
@ -197,3 +206,111 @@ class ReMe(Application):
|
|||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
async def summary_v2(
|
||||
self,
|
||||
messages: list[dict],
|
||||
description: str = "",
|
||||
user_id: str = "",
|
||||
assistant_id: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
"""Summarizes messages using V2 workflow with simplified tools."""
|
||||
|
||||
if user_id:
|
||||
metadata_desc = {
|
||||
"year": "The year when the memory content occurred.",
|
||||
"month": "The month when the memory content occurred.",
|
||||
"day": "The day when the memory content occurred.",
|
||||
}
|
||||
meta_memories = [
|
||||
{
|
||||
"memory_type": "personal",
|
||||
"memory_target": user_id,
|
||||
},
|
||||
]
|
||||
|
||||
messages = self._prepare_messages(messages, user_id, assistant_id)
|
||||
|
||||
personal_summarizer_v2 = PersonalSummarizerV2(
|
||||
tools=[
|
||||
AddMemoryDrafts(enable_thinking_params=True, metadata_desc=metadata_desc),
|
||||
RetrieveRecentAndSimilarMemories(
|
||||
enable_thinking_params=True,
|
||||
metadata_desc=None,
|
||||
recent_top_k=20,
|
||||
similar_top_k=20,
|
||||
),
|
||||
UpdateMemories(enable_thinking_params=True, metadata_desc=metadata_desc),
|
||||
],
|
||||
)
|
||||
|
||||
reme_summarizer_v2 = ReMeSummarizerV2(
|
||||
meta_memories=meta_memories,
|
||||
tools=[
|
||||
SummaryAndHandsOff(
|
||||
enable_thinking_params=True,
|
||||
metadata_desc=metadata_desc,
|
||||
memory_agents=[personal_summarizer_v2],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
await reme_summarizer_v2.call(messages=messages, description=description, **kwargs)
|
||||
return reme_summarizer_v2.memory_nodes
|
||||
except Exception as e:
|
||||
print(f"Warning: reme_summarizer_v2.call failed: {e}")
|
||||
return []
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
async def retrieve_v2(
|
||||
self,
|
||||
query: str = "",
|
||||
messages: list[dict] | None = None,
|
||||
description: str = "",
|
||||
user_id: str = "",
|
||||
assistant_id: str = "",
|
||||
top_k: int = 20,
|
||||
**kwargs,
|
||||
):
|
||||
"""Retrieves relevant memories using V2 workflow with autonomous retrieval."""
|
||||
|
||||
if user_id:
|
||||
messages = self._prepare_messages(messages, user_id, assistant_id)
|
||||
|
||||
metadata_retrieve = {
|
||||
"year": "The year to filter memories.",
|
||||
"month": "The month to filter memories.",
|
||||
"day": "The day to filter memories.",
|
||||
}
|
||||
meta_memories = [
|
||||
{
|
||||
"memory_type": "personal",
|
||||
"memory_target": user_id,
|
||||
},
|
||||
]
|
||||
|
||||
reme_retriever_v2 = ReMeRetrieverV2(
|
||||
meta_memories=meta_memories,
|
||||
tools=[
|
||||
RetrieveMemories(
|
||||
enable_thinking_params=True,
|
||||
metadata_desc=metadata_retrieve,
|
||||
top_k=top_k,
|
||||
),
|
||||
# ReadHistory(enable_thinking_params=True),
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
await reme_retriever_v2.call(query=query, messages=messages, description=description, **kwargs)
|
||||
return reme_retriever_v2.output
|
||||
except Exception as e:
|
||||
print(f"Warning: reme_retriever_v2.call failed: {e}")
|
||||
return "error, not retrieved"
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ reme = ReMe(
|
|||
async def test_reme():
|
||||
"""Tests ReMe memory system with personal information storage and retrieval."""
|
||||
# 构建一段包含个人信息的对话
|
||||
await reme.vector_store.delete_collection("reme")
|
||||
await reme.vector_store.delete_all()
|
||||
|
||||
messages = [
|
||||
{
|
||||
|
|
@ -55,7 +55,8 @@ async def test_reme():
|
|||
print("=" * 60)
|
||||
|
||||
# 对对话进行总结,生成记忆
|
||||
await reme.summary(
|
||||
# await reme.summary(
|
||||
await reme.summary_v2(
|
||||
messages=messages,
|
||||
user_id="zhangwei",
|
||||
description="用户自我介绍和技术兴趣分享",
|
||||
|
|
@ -81,25 +82,25 @@ async def test_reme():
|
|||
# 测试问题1: 检索用户姓名
|
||||
query1 = "用户叫什么名字?"
|
||||
print(f"\n问题1: {query1}")
|
||||
result1 = await reme.retrieve(query=query1, user_id="zhangwei")
|
||||
result1 = await reme.retrieve_v2(query=query1, user_id="zhangwei")
|
||||
print(f"检索结果:\n{result1}")
|
||||
|
||||
# 测试问题2: 检索技术背景
|
||||
query2 = "用户擅长什么编程语言和技术方向?"
|
||||
print(f"\n问题2: {query2}")
|
||||
result2 = await reme.retrieve(query=query2, user_id="zhangwei")
|
||||
result2 = await reme.retrieve_v2(query=query2, user_id="zhangwei")
|
||||
print(f"检索结果:\n{result2}")
|
||||
|
||||
# 测试问题3: 检索个人信息
|
||||
query3 = "用户的工作地点和联系方式是什么?"
|
||||
print(f"\n问题3: {query3}")
|
||||
result3 = await reme.retrieve(query=query3, user_id="zhangwei")
|
||||
result3 = await reme.retrieve_v2(query=query3, user_id="zhangwei")
|
||||
print(f"检索结果:\n{result3}")
|
||||
|
||||
# 测试问题4: 检索兴趣爱好
|
||||
query4 = "用户平时有什么爱好或活动?"
|
||||
print(f"\n问题4: {query4}")
|
||||
result4 = await reme.retrieve(query=query4, user_id="zhangwei")
|
||||
result4 = await reme.retrieve_v2(query=query4, user_id="zhangwei")
|
||||
print(f"检索结果:\n{result4}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue