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