mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-07 08:26:06 +00:00
feat(memory): restructure memory agent modules and enhance vector store operations
This commit is contained in:
parent
9f8ad20c53
commit
035703029f
122 changed files with 630 additions and 6401 deletions
|
|
@ -0,0 +1,9 @@
|
|||
"""memory agent"""
|
||||
|
||||
from . import default
|
||||
from .base_memory_agent import BaseMemoryAgent
|
||||
|
||||
__all__ = [
|
||||
"default",
|
||||
"BaseMemoryAgent",
|
||||
]
|
||||
|
|
@ -4,7 +4,38 @@ from abc import ABCMeta
|
|||
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.op import BaseReact
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
class BaseMemoryAgent(BaseReact, metaclass=ABCMeta):
|
||||
memory_type: MemoryType | None = None
|
||||
|
||||
@property
|
||||
def memory_target(self) -> str:
|
||||
"""memory_target"""
|
||||
return self.context.get("memory_target", "")
|
||||
|
||||
@property
|
||||
def query(self) -> str:
|
||||
"""query"""
|
||||
return self.context.get("query", "")
|
||||
|
||||
@property
|
||||
def messages(self) -> list:
|
||||
"""messages"""
|
||||
return self.context.get("messages", [])
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""description"""
|
||||
return self.context.get("description", "")
|
||||
|
||||
@property
|
||||
def history_node(self) -> MemoryNode:
|
||||
"""Returns the history node."""
|
||||
return self.context.history_node
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
"""Returns the LLM model name as the author identifier."""
|
||||
return self.llm.model_name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
from .personal_retriever import PersonalRetriever
|
||||
from .personal_summarizer import PersonalSummarizer
|
||||
from .reme_retriever import ReMeRetriever
|
||||
from .reme_summarizer import ReMeSummarizer
|
||||
|
||||
__all__ = [
|
||||
"PersonalRetriever",
|
||||
"PersonalSummarizer",
|
||||
"ReMeRetriever",
|
||||
"ReMeSummarizer",
|
||||
]
|
||||
|
|
@ -1,18 +1,27 @@
|
|||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import format_messages
|
||||
from ...mem_tool.v4 import ReadUserProfile
|
||||
from ....core.enumeration import Role, MemoryType
|
||||
from ....core.schema import Message
|
||||
from ....core.utils import format_messages
|
||||
|
||||
|
||||
class PersonalRetrieverV4(BaseMemoryAgent):
|
||||
class PersonalRetriever(BaseMemoryAgent):
|
||||
"""Retrieve personal memories through vector search and history reading."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
context = self.context.query if self.context.get("query") else format_messages(self.context.messages) if self.context.get("messages") else None
|
||||
from ....tool.memory.vector import ReadUserProfile
|
||||
|
||||
# Get context from query or messages
|
||||
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 user profile with history IDs
|
||||
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
|
||||
|
|
@ -26,7 +35,8 @@ class PersonalRetrieverV4(BaseMemoryAgent):
|
|||
memory_target=self.memory_target,
|
||||
user_profile=user_profile,
|
||||
context=context,
|
||||
))
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
|
|
@ -39,14 +49,14 @@ class PersonalRetrieverV4(BaseMemoryAgent):
|
|||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Execute the retriever and determine success based on output markers."""
|
||||
"""Execute retriever and check for memory found markers."""
|
||||
await super().execute()
|
||||
|
||||
# Check for memory found/not found markers in the output
|
||||
# Check output markers
|
||||
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
|
||||
self.meta_info = self.context.user_profile + "\n" + self.meta_info
|
||||
16
reme/agent/memory/default/personal_retriever.yaml
Normal file
16
reme/agent/memory/default/personal_retriever.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
tool: |
|
||||
Retrieve personal memories via vector search and history reading to answer user questions.
|
||||
|
||||
user_message: |
|
||||
You manage **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Profile
|
||||
{user_profile}
|
||||
|
||||
## Question
|
||||
{context}
|
||||
|
||||
## Task
|
||||
1. Vector search (`retrieve_memory`): Try 3-5 queries with different phrasings, entities, keywords, and time ranges [start, end] in YYYYMMDD
|
||||
2. Read context (`read_history`): Use history_id from results to get full conversations
|
||||
3. Respond: `<MEMORY_FOUND>` if found, `<MEMORY_NOT_FOUND>` if not found after thorough search
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode
|
||||
from ....core.enumeration import Role, MemoryType
|
||||
from ....core.schema import Message
|
||||
|
||||
|
||||
class PersonalSummarizerV4(BaseMemoryAgent):
|
||||
class PersonalSummarizer(BaseMemoryAgent):
|
||||
"""Extract and update personal memories in two phases: add summaries then update profile."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
async def build_messages_phase1(self) -> list[Message]:
|
||||
"""Build messages for phase 1: AddSummaryMemory"""
|
||||
history_node: MemoryNode = self.context.history_node
|
||||
messages = [
|
||||
"""Phase 1: AddSummaryMemory"""
|
||||
history_node = self.context.history_node
|
||||
return [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.prompt_format(
|
||||
|
|
@ -19,14 +21,14 @@ class PersonalSummarizerV4(BaseMemoryAgent):
|
|||
context=history_node.content,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
)),
|
||||
),
|
||||
),
|
||||
]
|
||||
return messages
|
||||
|
||||
async def build_messages_phase2(self, user_profile: str) -> list[Message]:
|
||||
"""Build messages for phase 2: UpdateUserProfile"""
|
||||
history_node: MemoryNode = self.context.history_node
|
||||
messages = [
|
||||
"""Phase 2: UpdateUserProfile"""
|
||||
history_node = self.context.history_node
|
||||
return [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.prompt_format(
|
||||
|
|
@ -35,9 +37,9 @@ class PersonalSummarizerV4(BaseMemoryAgent):
|
|||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
user_profile=user_profile,
|
||||
)),
|
||||
),
|
||||
),
|
||||
]
|
||||
return messages
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, stage: str = "", **kwargs) -> list[Message]:
|
||||
return await super()._acting_step(
|
||||
|
|
@ -52,73 +54,61 @@ class PersonalSummarizerV4(BaseMemoryAgent):
|
|||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Execute in two phases: 1) AddSummaryMemory, 2) UpdateUserProfile"""
|
||||
# Log available tools
|
||||
"""Execute two phases: AddSummaryMemory -> UpdateUserProfile"""
|
||||
from ....tool.memory.vector import ReadUserProfile
|
||||
|
||||
# Log tools
|
||||
for i, tool in enumerate(self.tools):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}] step0.{i} "
|
||||
f"tool_call={tool.tool_call.name}",
|
||||
)
|
||||
logger.info(f"[{self.__class__.__name__}] step0.{i} tool_call={tool.tool_call.name}")
|
||||
|
||||
# Phase 1: AddSummaryMemory
|
||||
logger.info(f"[{self.__class__.__name__}-S1] Starting Phase 1: AddSummaryMemory")
|
||||
|
||||
# Filter tools for phase 1 (only AddSummaryMemory)
|
||||
logger.info(f"[{self.__class__.__name__}-S1] Phase 1: AddSummaryMemory")
|
||||
original_tools = self.tools.copy()
|
||||
self.tools = [t for t in self.tools if t.tool_call.name == "add_summary_memory"]
|
||||
|
||||
messages_phase1 = await self.build_messages_phase1()
|
||||
for i, message in enumerate(messages_phase1):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}-S1] phase1.step0.{i} {message.role} "
|
||||
f"{message.simple_dump(enable_json_dump=True)}",
|
||||
f"[{self.__class__.__name__}-S1] phase1.step0.{i} {message.role} {message.simple_dump(enable_json_dump=True)}"
|
||||
)
|
||||
|
||||
messages_phase1, success_phase1 = await self.react(messages_phase1, stage="S1")
|
||||
if not success_phase1:
|
||||
logger.warning(f"[{self.__class__.__name__}-S1] Phase 1 did not complete successfully")
|
||||
logger.warning(f"[{self.__class__.__name__}-S1] Phase 1 incomplete")
|
||||
|
||||
# Phase 2: Read user profile and UpdateUserProfile
|
||||
logger.info(f"[{self.__class__.__name__}-S2] Starting Phase 2: UpdateUserProfile")
|
||||
|
||||
# Restore original tools and get ReadUserProfile tool
|
||||
# Phase 2: UpdateUserProfile
|
||||
logger.info(f"[{self.__class__.__name__}-S2] Phase 2: UpdateUserProfile")
|
||||
self.tools = original_tools
|
||||
read_profile_tool = next((t for t in self.tools if t.tool_call.name == "read_user_profile"), None)
|
||||
|
||||
user_profile = ""
|
||||
if read_profile_tool:
|
||||
# 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")
|
||||
logger.info(f"[{self.__class__.__name__}-S2] Loading user profile")
|
||||
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__}-S2] User profile loaded: {user_profile}...")
|
||||
else:
|
||||
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"]
|
||||
|
||||
messages_phase2 = await self.build_messages_phase2(user_profile)
|
||||
for i, message in enumerate(messages_phase2):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}-S2] phase2.step0.{i} {message.role} "
|
||||
f"{message.simple_dump(enable_json_dump=True)}",
|
||||
f"[{self.__class__.__name__}-S2] phase2.step0.{i} {message.role} {message.simple_dump(enable_json_dump=True)}"
|
||||
)
|
||||
|
||||
messages_phase2, success_phase2 = await self.react(messages_phase2, stage="S2")
|
||||
|
||||
# Restore original tools
|
||||
# Restore tools and set output
|
||||
self.tools = original_tools
|
||||
|
||||
# Set final output and messages
|
||||
self.messages = messages_phase1 + messages_phase2
|
||||
self.success = success_phase1 and success_phase2
|
||||
|
||||
if self.success and messages_phase2:
|
||||
self.output = messages_phase2[-1].content
|
||||
else:
|
||||
self.output = "Memory processing completed with issues."
|
||||
self.output = (
|
||||
messages_phase2[-1].content
|
||||
if self.success and messages_phase2
|
||||
else "Memory processing completed with issues."
|
||||
)
|
||||
26
reme/agent/memory/default/personal_summarizer.yaml
Normal file
26
reme/agent/memory/default/personal_summarizer.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
user_message_phase1: |
|
||||
You manage **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Conversation
|
||||
{context}
|
||||
|
||||
Format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
|
||||
## Task
|
||||
Extract memories with `add_memory`. Set `conversation_time` (YYYY-MM-DD HH:MM:SS, use 0000-00-00 00:00:00 if unavailable). Extract ONLY explicit information, no inference.
|
||||
|
||||
user_message_phase2: |
|
||||
You manage **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Conversation
|
||||
{context}
|
||||
|
||||
## Profile
|
||||
{user_profile}
|
||||
|
||||
## Task
|
||||
Update profile with `UpdateUserProfile`:
|
||||
- `profile_ids_to_delete`: Remove conflicting/redundant entries
|
||||
- `profiles_to_add`: Add new entries with `conversation_time` and `profile_content` (complete, self-contained, mutually exclusive)
|
||||
|
||||
Extract ONLY explicit information, no inference.
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import format_messages
|
||||
from ....core.enumeration import Role
|
||||
from ....core.schema import Message
|
||||
from ....core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeRetrieverV4(BaseMemoryAgent):
|
||||
class ReMeRetriever(BaseMemoryAgent):
|
||||
"""Orchestrate multiple memory agents to retrieve information."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -14,20 +15,22 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
self.meta_info_dict: dict[str, str] = {}
|
||||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
from ...mem_tool import ReadMetaMemory
|
||||
from ....tool.memory import ReadMetaMemory
|
||||
|
||||
meta_memory_info = ReadMetaMemory().format_memory_metadata(self.meta_memories)
|
||||
logger.info(f"meta_memory_info={meta_memory_info}")
|
||||
return meta_memory_info
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
if self.context.get("query"):
|
||||
user_query = self.context.query
|
||||
elif self.context.get("messages"):
|
||||
user_query = format_messages(self.context.messages)
|
||||
else:
|
||||
user_query = (
|
||||
self.context.query
|
||||
if self.context.get("query")
|
||||
else format_messages(self.context.messages) if self.context.get("messages") else None
|
||||
)
|
||||
if not user_query:
|
||||
raise ValueError("Input must have either `query` or `messages`")
|
||||
|
||||
messages = [
|
||||
return [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
|
|
@ -42,11 +45,9 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
),
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
import asyncio
|
||||
from ...mem_tool.v4 import HandsOff
|
||||
from ....tool.memory import HandsOff
|
||||
|
||||
if not assistant_message.tool_calls:
|
||||
return []
|
||||
|
|
@ -56,7 +57,7 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
tool_dict = {t.tool_call.name: t for t in self.tools}
|
||||
stage_prefix = ""
|
||||
|
||||
# Add required context parameters
|
||||
# Add context parameters
|
||||
kwargs["query"] = self.context.get("query", "")
|
||||
kwargs["messages"] = self.context.get("messages", [])
|
||||
|
||||
|
|
@ -66,8 +67,7 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
continue
|
||||
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} "
|
||||
f"submit tool_calls={tool_call.name} argument={tool_call.arguments}",
|
||||
f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} 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
|
||||
|
|
@ -86,7 +86,7 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
if hasattr(op, "messages") and op.messages:
|
||||
self.tool_messages.extend(op.messages)
|
||||
|
||||
# Collect meta_info_dict from HandsOff tool
|
||||
# Collect meta_info_dict from HandsOff
|
||||
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")
|
||||
|
|
@ -98,20 +98,19 @@ class ReMeRetrieverV4(BaseMemoryAgent):
|
|||
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")
|
||||
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):
|
||||
"""Execute and assemble meta_info_dict into output."""
|
||||
await super().execute()
|
||||
|
||||
# Assemble meta_info_dict into output
|
||||
# Assemble meta_info_dict
|
||||
if self.meta_info_dict:
|
||||
output_parts = []
|
||||
for key, value in self.meta_info_dict.items():
|
||||
output_parts.append(f"## {key}\n{value}")
|
||||
output_parts = [f"## {key}\n{value}" for key, value in self.meta_info_dict.items()]
|
||||
self.output = "\n\n".join(output_parts)
|
||||
logger.info(f"Assembled output from meta_info_dict with {len(self.meta_info_dict)} entries")
|
||||
logger.info(f"Assembled output from meta_info_dict with {len(self.meta_info_dict)} entries")
|
||||
20
reme/agent/memory/default/reme_retriever.yaml
Normal file
20
reme/agent/memory/default/reme_retriever.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
tool: |
|
||||
Retrieve information from specialized memory agents.
|
||||
|
||||
system_prompt: |
|
||||
You orchestrate memory agents to answer user queries.
|
||||
|
||||
# Query
|
||||
{user_query}
|
||||
|
||||
## Agents
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Task
|
||||
1. Use `hands_off` to query agents (memory_type and memory_target must exactly match existing agents)
|
||||
2. Answer based on results
|
||||
3. If insufficient: "nothing found after thorough search."
|
||||
|
||||
user_message: |
|
||||
Retrieve information from agents and answer based on results.
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ....core.enumeration import Role, MemoryType
|
||||
from ....core.schema import Message, MemoryNode
|
||||
from ....core.utils import format_messages
|
||||
from ....core.enumeration import Role
|
||||
from ....core.op import BaseTool
|
||||
from ....core.schema import Message
|
||||
|
||||
|
||||
class ReMeSummarizer(BaseMemoryAgent):
|
||||
|
|
@ -14,26 +14,17 @@ class ReMeSummarizer(BaseMemoryAgent):
|
|||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
from ....tool.memory import ReadMetaMemory
|
||||
|
||||
meta_memory_info = ReadMetaMemory().format_memory_metadata(self.meta_memories)
|
||||
logger.info(f"meta_memory_info={meta_memory_info}")
|
||||
return meta_memory_info
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
self.context.messages = [Message(**m) if isinstance(m, dict) else m for m in self.context.messages]
|
||||
history_content = self.description + "\n" + format_messages(self.context.messages)
|
||||
self.context.history_node = history_node = MemoryNode(
|
||||
memory_type=MemoryType.HISTORY,
|
||||
memory_target="",
|
||||
when_to_use=history_content[:100],
|
||||
content=history_content,
|
||||
ref_memory_id="",
|
||||
author=self.author,
|
||||
metadata={},
|
||||
)
|
||||
from ....tool.memory import AddHistory
|
||||
|
||||
logger.info(f"Adding summary node: {history_node.model_dump_json(indent=2, exclude_none=True)}")
|
||||
await self.vector_store.delete(history_node.memory_id)
|
||||
await self.vector_store.insert([history_node.to_vector_node()])
|
||||
add_history_tool = AddHistory()
|
||||
await add_history_tool.call()
|
||||
self.context.history_node = add_history_tool.context.add_history
|
||||
|
||||
messages = [
|
||||
Message(
|
||||
|
|
@ -41,7 +32,7 @@ class ReMeSummarizer(BaseMemoryAgent):
|
|||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self._read_meta_memories(),
|
||||
context=history_node.content,
|
||||
context=self.context.history_node.content,
|
||||
),
|
||||
),
|
||||
Message(
|
||||
|
|
@ -52,10 +43,17 @@ class ReMeSummarizer(BaseMemoryAgent):
|
|||
|
||||
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,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
description=self.description,
|
||||
messages=self.context.messages,
|
||||
history_node=self.context.history_node,
|
||||
author=self.author,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
tool: |
|
||||
Orchestrate memory updates across specialized memory agents.
|
||||
|
||||
system_prompt: |
|
||||
You are a Memory Orchestrator responsible for routing memory tasks to specialized agents based on the context.
|
||||
|
||||
|
|
@ -8,7 +5,7 @@ system_prompt: |
|
|||
{context}
|
||||
|
||||
## Available Memory Agents
|
||||
Each line indicates a specialized Memory Agent dedicated to deep summarization and updating of memories within a specific dimension (memory_type + memory_target).
|
||||
Each line indicates 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}
|
||||
|
||||
|
|
@ -17,7 +14,7 @@ system_prompt: |
|
|||
1. Analyze the context and identify which memory dimensions require updates
|
||||
2. Specify `memory_type` and `memory_target` for each task
|
||||
- The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above
|
||||
- Do NOT create new agents or use memory_type/memory_target combinations that don't exist above
|
||||
- Do NOT create new agents or use <memory_type>(<memory_target>) combinations that don't exist above
|
||||
3. Multiple tasks can be specified to enable parallel processing by specialized agents
|
||||
|
||||
Note: If the context contains no memorable information (e.g., simple greetings), return `<NO_MEMORY_NEEDED>`.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from ..vector_store import BaseVectorStore
|
|||
|
||||
class BaseOp(metaclass=ABCMeta):
|
||||
"""Base operator class for LLM workflow execution and composition."""
|
||||
|
||||
__alias_name__: str = ""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
"""Base memory agent for handling memory operations with tool-based reasoning."""
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from . import BaseTool
|
||||
from ..enumeration import Role
|
||||
from ..op import BaseOp
|
||||
from ..schema import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import BaseTool
|
||||
|
||||
|
||||
class BaseReact(BaseOp):
|
||||
"""ReAct agent that performs reasoning and acting cycles with tools."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tools: list[BaseTool],
|
||||
tool_call_interval: float = 0,
|
||||
max_steps: int = 10,
|
||||
**kwargs,
|
||||
self,
|
||||
tools: list[BaseTool],
|
||||
tool_call_interval: float = 0,
|
||||
max_steps: int = 10,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize ReAct agent with tools and execution parameters."""
|
||||
kwargs["sub_ops"] = tools or []
|
||||
|
|
@ -44,11 +47,12 @@ class BaseReact(BaseOp):
|
|||
return messages
|
||||
|
||||
async def _reasoning_step(
|
||||
self,
|
||||
messages: list[Message],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs) -> tuple[Message, bool]:
|
||||
self,
|
||||
messages: list[Message],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[Message, bool]:
|
||||
"""Execute one reasoning step where LLM decides whether to use tools."""
|
||||
# Get tool definitions for LLM
|
||||
tool_calls = [t.tool_call for t in self.tools]
|
||||
|
|
@ -62,11 +66,11 @@ class BaseReact(BaseOp):
|
|||
return assistant_message, should_act
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs
|
||||
self,
|
||||
assistant_message: Message,
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
"""Execute tool calls requested by the assistant and collect results."""
|
||||
tool_list: list[BaseTool] = []
|
||||
|
|
@ -101,11 +105,13 @@ class BaseReact(BaseOp):
|
|||
|
||||
# Collect tool results as messages
|
||||
for j, tool in enumerate(tool_list):
|
||||
tool_messages.append(Message(
|
||||
role=Role.TOOL,
|
||||
content=tool.response.answer,
|
||||
tool_call_id=tool.tool_call.id,
|
||||
))
|
||||
tool_messages.append(
|
||||
Message(
|
||||
role=Role.TOOL,
|
||||
content=tool.response.answer,
|
||||
tool_call_id=tool.tool_call.id,
|
||||
),
|
||||
)
|
||||
prefix: str = f"[{self.__class__.__name__} {stage or ''} step{step + 1}.{j}]"
|
||||
logger.info(f"{prefix} join tool={tool.name} result={tool.response.answer}")
|
||||
return tool_list, tool_messages
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class ToolCall(BaseModel):
|
|||
|
||||
def simple_input_dump(self, as_dict: bool = True) -> dict | str:
|
||||
"""Returns a standardized tool definition dictionary or JSON string.
|
||||
|
||||
|
||||
Args:
|
||||
as_dict: If True, returns dict; if False, returns JSON string.
|
||||
"""
|
||||
|
|
@ -147,7 +147,7 @@ class ToolCall(BaseModel):
|
|||
|
||||
def simple_output_dump(self, as_dict: bool = True) -> dict | str:
|
||||
"""Convert ToolCall to output format dictionary or JSON string for API responses.
|
||||
|
||||
|
||||
Args:
|
||||
as_dict: If True, returns dict; if False, returns JSON string.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""memory tools"""
|
||||
|
||||
from .base_memory_tool import BaseMemoryTool
|
||||
from .hands_off.hands_off import HandsOff
|
||||
from .history.add_history import AddHistory
|
||||
from .history.read_history import ReadHistory
|
||||
from .identity.add_identity import AddIdentity
|
||||
|
|
@ -9,10 +10,16 @@ from .meta.add_meta_memory import AddMetaMemory
|
|||
from .meta.read_meta_memory import ReadMetaMemory
|
||||
from .user_profile.read_user_profile import ReadUserProfile
|
||||
from .user_profile.update_user_profile import UpdateUserProfile
|
||||
from .vector.add_memory import AddMemory
|
||||
from .vector.update_memory import UpdateMemory
|
||||
from .vector.retrieve_memory import VectorRetrieveMemory
|
||||
from .vector.retrieve_recent_memory import VectorRetrieveRecentMemory
|
||||
from .vector.delete_memory import DeleteMemory
|
||||
from ...core import R
|
||||
|
||||
__all__ = [
|
||||
"BaseMemoryTool",
|
||||
"HandsOff",
|
||||
"AddHistory",
|
||||
"ReadHistory",
|
||||
"AddIdentity",
|
||||
|
|
@ -21,6 +28,11 @@ __all__ = [
|
|||
"ReadMetaMemory",
|
||||
"ReadUserProfile",
|
||||
"UpdateUserProfile",
|
||||
"AddMemory",
|
||||
"UpdateMemory",
|
||||
"VectorRetrieveMemory",
|
||||
"VectorRetrieveRecentMemory",
|
||||
"DeleteMemory",
|
||||
]
|
||||
|
||||
for name in __all__:
|
||||
|
|
|
|||
105
reme/tool/memory/hands_off/hands_off.py
Normal file
105
reme/tool/memory/hands_off/hands_off.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""Hands-off tool to delegate memory tasks to specific agents"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....agent.memory import BaseMemoryAgent
|
||||
from ....core.enumeration import MemoryType
|
||||
from ....core.schema import ToolCall
|
||||
|
||||
|
||||
class HandsOff(BaseMemoryTool):
|
||||
"""Tool to delegate memory tasks to appropriate memory agents"""
|
||||
|
||||
def __init__(self, memory_agents: list[BaseMemoryAgent] = None, **kwargs):
|
||||
kwargs["enable_multiple"] = True
|
||||
kwargs["sub_ops"] = memory_agents or []
|
||||
super().__init__(**kwargs)
|
||||
self.sub_ops: list[BaseMemoryAgent] = [
|
||||
a for a in self.sub_ops if isinstance(a, BaseMemoryAgent) and a.memory_type is not None
|
||||
]
|
||||
|
||||
@property
|
||||
def memory_agent_dict(self) -> dict[MemoryType, BaseMemoryAgent]:
|
||||
"""Map memory types to their corresponding agents"""
|
||||
return {a.memory_type: a for a in self.sub_ops}
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "Delegate memory tasks to appropriate agents.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_tasks": {
|
||||
"type": "array",
|
||||
"description": "Memory tasks to delegate to specific agents",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": "Memory type to handle",
|
||||
"enum": [k.value for k in self.memory_agent_dict if k],
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": "Target or context for the memory operation",
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memory_tasks"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
# Deduplicate and validate tasks
|
||||
tasks = []
|
||||
seen = set()
|
||||
for task in self.context.get("memory_tasks", []):
|
||||
memory_type = MemoryType(task.get("memory_type", ""))
|
||||
memory_target = task.get("memory_target", "")
|
||||
|
||||
task_key = (memory_type, memory_target)
|
||||
if task_key in seen:
|
||||
logger.info(f"Skip duplicate: {memory_type.value} - {memory_target}")
|
||||
continue
|
||||
seen.add(task_key)
|
||||
|
||||
tasks.append({"memory_type": memory_type, "memory_target": memory_target})
|
||||
|
||||
if not tasks:
|
||||
return "No valid memory tasks to execute."
|
||||
|
||||
# Submit tasks to agents
|
||||
agent_list: list[BaseMemoryAgent] = []
|
||||
for i, task in enumerate(tasks):
|
||||
memory_type: MemoryType = task["memory_type"]
|
||||
memory_target: str = task["memory_target"]
|
||||
|
||||
agent = self.memory_agent_dict[memory_type].copy()
|
||||
agent_list.append(agent)
|
||||
|
||||
logger.info(f"Task {i}: {memory_type.value} agent for {memory_target}")
|
||||
task_kwargs = {"memory_type": memory_type, "memory_target": memory_target}
|
||||
for k in ["query", "messages", "description", "history_node"]:
|
||||
if k in self.context:
|
||||
task_kwargs[k] = self.context[k]
|
||||
self.submit_async_task(agent.call, **task_kwargs)
|
||||
|
||||
await self.join_async_tasks()
|
||||
|
||||
# Collect results
|
||||
results = []
|
||||
for agent in agent_list:
|
||||
memory_type = agent.memory_type
|
||||
memory_target = agent.memory_target
|
||||
results.append(f"{memory_type.value}({memory_target}): {agent.response.answer}")
|
||||
|
||||
logger.info(f"Completed {len(results)} task(s)")
|
||||
return "\n".join(results)
|
||||
|
|
@ -38,6 +38,7 @@ class AddHistory(BaseMemoryTool):
|
|||
content=history_content,
|
||||
author=self.author,
|
||||
)
|
||||
self.context.history_node = history_node
|
||||
logger.info(f"Adding history node: {history_node.model_dump_json(indent=2, exclude={'content'})}")
|
||||
|
||||
vector_node = history_node.to_vector_node()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,20 @@ class ReadMetaMemory(BaseMemoryTool):
|
|||
},
|
||||
)
|
||||
|
||||
def format_memory_metadata(self, memories: list[dict[str, str]]) -> str:
|
||||
"""Format memory metadata into a readable string."""
|
||||
if not memories:
|
||||
return ""
|
||||
|
||||
lines = []
|
||||
for memory in memories:
|
||||
memory_type = memory["memory_type"]
|
||||
memory_target = memory["memory_target"]
|
||||
description = self.TYPE_DESC_DICT[memory_type]
|
||||
lines.append(f"- {memory_type}({memory_target}): {description}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
async def execute(self):
|
||||
# Load and filter meta memories
|
||||
result = self.local_memory.load("meta_memories")
|
||||
|
|
@ -51,13 +65,8 @@ class ReadMetaMemory(BaseMemoryTool):
|
|||
)
|
||||
|
||||
# Format output
|
||||
if memories:
|
||||
lines = [
|
||||
f"- {m['memory_type']}({m['memory_target']}): {self.TYPE_DESC_DICT.get(m['memory_type'], '')}"
|
||||
for m in memories
|
||||
]
|
||||
|
||||
output = "\n".join(lines)
|
||||
output = self.format_memory_metadata(memories)
|
||||
if output:
|
||||
logger.info(f"Retrieved {len(memories)} meta memory entries")
|
||||
else:
|
||||
output = "No memory metadata found."
|
||||
|
|
|
|||
|
|
@ -9,9 +9,27 @@ from ....core.schema import ToolCall, MemoryNode
|
|||
class AddMemory(BaseMemoryTool):
|
||||
"""Tool to add memories to vector store"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['enable_multiple'] = True
|
||||
super().__init__(**kwargs)
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the single tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "add a memory to vector store for future retrieval.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation time, e.g. '2020-01-01 00:00:00'",
|
||||
},
|
||||
"memory_content": {
|
||||
"type": "string",
|
||||
"description": "content of the memory.",
|
||||
},
|
||||
},
|
||||
"required": ["conversation_time", "memory_content"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
|
|
@ -27,16 +45,16 @@ class AddMemory(BaseMemoryTool):
|
|||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation time, e.g. '2020-01-01 00:00:00'",
|
||||
},
|
||||
"memory_content": {
|
||||
"type": "string",
|
||||
"description": "content of the memory.",
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "metadata for the memory.",
|
||||
}
|
||||
},
|
||||
"required": ["memory_content"],
|
||||
"required": ["conversation_time", "memory_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -45,42 +63,39 @@ class AddMemory(BaseMemoryTool):
|
|||
},
|
||||
)
|
||||
|
||||
def _extract_memory_data(self, mem_dict: dict) -> tuple[str, dict]:
|
||||
"""Extract memory content and metadata from dict"""
|
||||
memory_content = mem_dict.get("memory_content", "")
|
||||
raw_metadata = mem_dict.get("metadata", {})
|
||||
metadata = {key: str(value).strip() for key, value in raw_metadata.items() if value}
|
||||
return memory_content, metadata
|
||||
def _create_memory_node(self, data: dict) -> MemoryNode:
|
||||
"""Create a MemoryNode from a dictionary."""
|
||||
memory_content = data.get("memory_content", "")
|
||||
conversation_time = data.get("conversation_time", "")
|
||||
metadata: dict = {"conversation_time": conversation_time}
|
||||
|
||||
try:
|
||||
metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", ""))
|
||||
except Exception:
|
||||
logger.warning(f"Invalid conversation time format. {conversation_time}")
|
||||
|
||||
def _build_memory_node(self, content: str, metadata: dict = None) -> MemoryNode:
|
||||
"""Build a memory node"""
|
||||
return MemoryNode(
|
||||
memory_type=self.memory_type,
|
||||
memory_target=self.memory_target,
|
||||
content=content,
|
||||
content=memory_content,
|
||||
author=self.author,
|
||||
metadata=metadata or {},
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
|
||||
if not memories:
|
||||
self.output = "No memories provided for addition."
|
||||
return
|
||||
|
||||
for mem in memories:
|
||||
memory_content, 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, metadata=metadata))
|
||||
memory_nodes.append(self._create_memory_node(self.context))
|
||||
else:
|
||||
for mem in memories:
|
||||
memory_nodes.append(self._create_memory_node(mem))
|
||||
|
||||
if not memory_nodes:
|
||||
self.output = "No valid memories provided for addition."
|
||||
return
|
||||
output = "No valid memories provided for addition."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
||||
vector_nodes = [node.to_vector_node() for node in memory_nodes]
|
||||
vector_ids: list[str] = [node.vector_id for node in vector_nodes]
|
||||
|
|
@ -89,5 +104,6 @@ class AddMemory(BaseMemoryTool):
|
|||
await self.vector_store.insert(nodes=vector_nodes)
|
||||
self.memory_nodes = memory_nodes
|
||||
|
||||
self.output = f"Successfully added {len(memory_nodes)} memories to vector_store."
|
||||
logger.info(self.output)
|
||||
output = f"Successfully added {len(memory_nodes)} memories to vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -9,9 +9,23 @@ from ....core.schema import ToolCall
|
|||
class DeleteMemory(BaseMemoryTool):
|
||||
"""Tool to delete memories from vector store"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['enable_multiple'] = True
|
||||
super().__init__(**kwargs)
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the single tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "delete a memory from vector store using its unique ID.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"description": "unique identifier (memory_id) of the memory to delete.",
|
||||
},
|
||||
},
|
||||
"required": ["memory_id"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
|
|
@ -33,13 +47,25 @@ class DeleteMemory(BaseMemoryTool):
|
|||
)
|
||||
|
||||
async def execute(self):
|
||||
memory_ids = [m for m in self.context.get("memory_ids", []) if m]
|
||||
memory_ids: list[str] = []
|
||||
|
||||
# Handle multiple memories (array format)
|
||||
ids_from_array = self.context.get("memory_ids", [])
|
||||
if ids_from_array:
|
||||
memory_ids = [m for m in ids_from_array if m]
|
||||
else:
|
||||
memory_id = self.context.get("memory_id", "")
|
||||
if memory_id:
|
||||
memory_ids = [memory_id]
|
||||
|
||||
if not memory_ids:
|
||||
self.output = "No valid memory IDs provided for deletion."
|
||||
return
|
||||
output = "No valid memory IDs provided for deletion."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
||||
await self.vector_store.delete(vector_ids=memory_ids)
|
||||
self.memory_nodes = memory_ids
|
||||
self.output = f"Successfully deleted {len(memory_ids)} memories from vector_store."
|
||||
logger.info(self.output)
|
||||
|
||||
output = f"Successfully deleted {len(memory_ids)} memories from vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -11,65 +11,40 @@ from ....core.utils import deduplicate_memories
|
|||
class VectorRetrieveMemory(BaseMemoryTool):
|
||||
"""Tool to retrieve memories from vector store using similarity search"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
add_memory_type_target: bool = False,
|
||||
top_k: int = 20,
|
||||
enable_metadata: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
def __init__(self, top_k: int = 20, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_memory_type_target: bool = add_memory_type_target
|
||||
self.top_k: int = top_k
|
||||
self.enable_metadata: bool = enable_metadata
|
||||
|
||||
def _build_query_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build query schema for single/multiple retrieval"""
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
if self.add_memory_type_target:
|
||||
properties["memory_type"] = {
|
||||
"type": "string",
|
||||
"description": "type of memory to search for.",
|
||||
}
|
||||
properties["memory_target"] = {
|
||||
"type": "string",
|
||||
"description": "target of memory to search within.",
|
||||
}
|
||||
required.extend(["memory_type", "memory_target"])
|
||||
|
||||
properties["query"] = {
|
||||
"type": "string",
|
||||
"description": "query text for vector similarity search.",
|
||||
@staticmethod
|
||||
def _build_query_parameters() -> dict:
|
||||
"""Build query parameters schema for retrieval"""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "query text for vector similarity search.",
|
||||
},
|
||||
"time_range": {
|
||||
"type": "string",
|
||||
"description": "optional time range filter. "
|
||||
"Format: single date '20200101' or range '20200101,20200102'",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
required.append("query")
|
||||
|
||||
if self.enable_metadata:
|
||||
properties["metadata"] = {
|
||||
"type": "object",
|
||||
"description": "optional metadata filters for narrowing search results.",
|
||||
}
|
||||
|
||||
return properties, required
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the tool call schema"""
|
||||
properties, required = self._build_query_schema()
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "retrieve memories using vector similarity search.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
},
|
||||
"parameters": self._build_query_parameters(),
|
||||
},
|
||||
)
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
item_properties, item_required = self._build_query_schema()
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "retrieve memories using multiple queries with vector similarity search.",
|
||||
|
|
@ -79,11 +54,7 @@ class VectorRetrieveMemory(BaseMemoryTool):
|
|||
"query_items": {
|
||||
"type": "array",
|
||||
"description": "list of query items for vector similarity search.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": item_required,
|
||||
},
|
||||
"items": self._build_query_parameters(),
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
|
|
@ -96,90 +67,63 @@ class VectorRetrieveMemory(BaseMemoryTool):
|
|||
memory_type: str,
|
||||
memory_target: str,
|
||||
query: str,
|
||||
metadata: dict | None = None,
|
||||
time_range: str | None = None,
|
||||
) -> list[MemoryNode]:
|
||||
"""Retrieve memories by query with filters"""
|
||||
filter_dict = {
|
||||
"memory_type": [memory_type],
|
||||
"memory_target": [memory_target],
|
||||
filter_dict: dict = {
|
||||
"memory_type": memory_type,
|
||||
"memory_target": memory_target,
|
||||
}
|
||||
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
if value:
|
||||
value = str(value).strip()
|
||||
filter_dict[key] = [value] if not isinstance(value, list) else value
|
||||
if time_range:
|
||||
time_range = time_range.strip()
|
||||
if "," in time_range:
|
||||
parts = time_range.split(",")
|
||||
start_time = int(parts[0].strip())
|
||||
end_time = int(parts[1].strip())
|
||||
filter_dict["time_int"] = [start_time, end_time]
|
||||
else:
|
||||
single_time = int(time_range)
|
||||
filter_dict["time_int"] = [single_time, single_time]
|
||||
|
||||
nodes: list[VectorNode] = await self.vector_store.search(query=query, limit=self.top_k, filters=filter_dict)
|
||||
|
||||
memory_nodes: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
filtered_memory_nodes = [
|
||||
m for m in memory_nodes if not (m.memory_type == MemoryType.TOOL and m.when_to_use != query)
|
||||
]
|
||||
|
||||
return filtered_memory_nodes
|
||||
return [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
async def execute(self):
|
||||
default_memory_type: str = self.context.get("memory_type", "")
|
||||
default_memory_target: str = self.context.get("memory_target", "")
|
||||
memory_type: str = self.memory_type.value
|
||||
memory_target: str = self.memory_target
|
||||
|
||||
if self.enable_multiple:
|
||||
query_items: list[dict] = self.context.get("query_items", [])
|
||||
if not query_items:
|
||||
self.output = "No query items provided for retrieval."
|
||||
return
|
||||
else:
|
||||
query = self.context.get("query", "")
|
||||
if not query:
|
||||
self.output = "No query provided for retrieval."
|
||||
return
|
||||
|
||||
query_items = [
|
||||
query_items: list[dict] = [
|
||||
{
|
||||
"memory_type": default_memory_type,
|
||||
"memory_target": default_memory_target,
|
||||
"query": query,
|
||||
"query": self.context.get("query", ""),
|
||||
"time_range": self.context.get("time_range", ""),
|
||||
},
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for item in query_items:
|
||||
memory_type = item.get("memory_type") or default_memory_type
|
||||
memory_target = item.get("memory_target") or default_memory_target
|
||||
metadata = item.get("metadata", {}) if self.enable_metadata else None
|
||||
|
||||
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=metadata,
|
||||
time_range=item.get("time_range", ""),
|
||||
)
|
||||
memory_nodes.extend(retrieved)
|
||||
|
||||
memory_nodes = deduplicate_memories(memory_nodes)
|
||||
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
|
||||
new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
self.output = "No new memory_nodes found matching the query (duplicates removed)."
|
||||
output = "No new memory_nodes found matching the query (duplicates removed)."
|
||||
else:
|
||||
self.output = "\n".join([m.format_memory() for m in new_memory_nodes])
|
||||
output = "\n".join([m.format_memory() for m in new_memory_nodes])
|
||||
|
||||
logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication")
|
||||
return output
|
||||
|
|
|
|||
62
reme/tool/memory/vector/retrieve_recent_memory.py
Normal file
62
reme/tool/memory/vector/retrieve_recent_memory.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Retrieve most recent memories from vector store"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....core.schema import ToolCall, MemoryNode, VectorNode
|
||||
from ....core.utils import deduplicate_memories
|
||||
|
||||
|
||||
class VectorRetrieveRecentMemory(BaseMemoryTool):
|
||||
"""Tool to retrieve most recent memories sorted by conversation time"""
|
||||
|
||||
def __init__(self, top_k: int = 20, **kwargs):
|
||||
kwargs["enable_multiple"] = False
|
||||
super().__init__(**kwargs)
|
||||
self.top_k: int = top_k
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "retrieve the most recent memories sorted by conversation time (newest first).",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def _retrieve_recent(self) -> list[MemoryNode]:
|
||||
"""Retrieve recent memories sorted by conversation_time descending"""
|
||||
filter_dict = {
|
||||
"memory_type": self.memory_type.value,
|
||||
"memory_target": self.memory_target,
|
||||
}
|
||||
|
||||
nodes: list[VectorNode] = await self.vector_store.list(
|
||||
filters=filter_dict,
|
||||
limit=self.top_k,
|
||||
sort_key="conversation_time",
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
async def execute(self):
|
||||
memory_nodes: list[MemoryNode] = await self._retrieve_recent()
|
||||
memory_nodes = deduplicate_memories(memory_nodes)
|
||||
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
output = "No new memory_nodes found (duplicates removed)."
|
||||
else:
|
||||
output = "\n".join([m.format_memory() for m in new_memory_nodes])
|
||||
|
||||
logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication")
|
||||
return output
|
||||
|
|
@ -9,9 +9,31 @@ from ....core.schema import ToolCall, MemoryNode
|
|||
class UpdateMemory(BaseMemoryTool):
|
||||
"""Tool to update memories in vector store"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['enable_multiple'] = True
|
||||
super().__init__(**kwargs)
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the single tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "update a memory in vector store by replacing old memory with new content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"description": "unique identifier of memory to update.",
|
||||
},
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation time, e.g. '2020-01-01 00:00:00'",
|
||||
},
|
||||
"memory_content": {
|
||||
"type": "string",
|
||||
"description": "new content of the memory.",
|
||||
},
|
||||
},
|
||||
"required": ["memory_id", "conversation_time", "memory_content"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
|
|
@ -31,16 +53,16 @@ class UpdateMemory(BaseMemoryTool):
|
|||
"type": "string",
|
||||
"description": "unique identifier of memory to update.",
|
||||
},
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation time, e.g. '2020-01-01 00:00:00'",
|
||||
},
|
||||
"memory_content": {
|
||||
"type": "string",
|
||||
"description": "new content of the memory.",
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "metadata for the memory.",
|
||||
}
|
||||
},
|
||||
"required": ["memory_id", "memory_content", "metadata"],
|
||||
"required": ["memory_id", "conversation_time", "memory_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -49,52 +71,56 @@ class UpdateMemory(BaseMemoryTool):
|
|||
},
|
||||
)
|
||||
|
||||
def _extract_memory_data(self, mem_dict: dict) -> tuple[str, str, dict]:
|
||||
"""Extract memory id, content and metadata from dict"""
|
||||
memory_id = mem_dict.get("memory_id", "")
|
||||
memory_content = mem_dict.get("memory_content", "")
|
||||
raw_metadata = mem_dict.get("metadata", {})
|
||||
metadata = {key: str(value).strip() for key, value in raw_metadata.items() if value}
|
||||
return memory_id, memory_content, metadata
|
||||
def _create_memory_node(self, data: dict) -> tuple[str, MemoryNode]:
|
||||
"""Create a MemoryNode from a dictionary."""
|
||||
memory_id = data.get("memory_id", "")
|
||||
memory_content = data.get("memory_content", "")
|
||||
conversation_time = data.get("conversation_time", "")
|
||||
metadata: dict = {"conversation_time": conversation_time}
|
||||
|
||||
def _build_memory_node(self, content: str, metadata: dict = None) -> MemoryNode:
|
||||
"""Build a memory node"""
|
||||
return MemoryNode(
|
||||
try:
|
||||
metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", ""))
|
||||
except Exception:
|
||||
logger.warning(f"Invalid conversation time format. {conversation_time}")
|
||||
|
||||
memory_node = MemoryNode(
|
||||
memory_type=self.memory_type,
|
||||
memory_target=self.memory_target,
|
||||
content=content,
|
||||
content=memory_content,
|
||||
author=self.author,
|
||||
metadata=metadata or {},
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return memory_id, memory_node
|
||||
|
||||
async def execute(self):
|
||||
old_memory_ids: list[str] = []
|
||||
new_memory_nodes: list[MemoryNode] = []
|
||||
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
|
||||
if not memories:
|
||||
self.output = "No memories provided for update."
|
||||
return
|
||||
old_id, node = self._create_memory_node(self.context)
|
||||
old_memory_ids.append(old_id)
|
||||
memory_nodes.append(node)
|
||||
else:
|
||||
for mem in memories:
|
||||
old_id, node = self._create_memory_node(mem)
|
||||
old_memory_ids.append(old_id)
|
||||
memory_nodes.append(node)
|
||||
|
||||
for mem in memories:
|
||||
memory_id, memory_content, metadata = self._extract_memory_data(mem)
|
||||
if not memory_id or not memory_content:
|
||||
logger.warning(f"Skipping memory with missing id or content: {mem}")
|
||||
continue
|
||||
old_memory_ids.append(memory_id)
|
||||
new_memory_nodes.append(self._build_memory_node(memory_content, metadata=metadata))
|
||||
if not memory_nodes:
|
||||
output = "No valid memories provided for update."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
||||
if not old_memory_ids or not new_memory_nodes:
|
||||
self.output = "No valid memories provided for update."
|
||||
return
|
||||
|
||||
vector_nodes = [node.to_vector_node() for node in new_memory_nodes]
|
||||
new_vector_ids = [node.vector_id for node in vector_nodes]
|
||||
vector_nodes = [node.to_vector_node() for node in memory_nodes]
|
||||
new_vector_ids: list[str] = [node.vector_id for node in vector_nodes]
|
||||
|
||||
all_ids_to_delete = list(set(old_memory_ids + new_vector_ids))
|
||||
await self.vector_store.delete(vector_ids=all_ids_to_delete)
|
||||
await self.vector_store.insert(nodes=vector_nodes)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
self.memory_nodes = memory_nodes
|
||||
|
||||
self.output = f"Successfully updated {len(new_memory_nodes)} memories in vector_store."
|
||||
logger.info(self.output)
|
||||
output = f"Successfully updated {len(memory_nodes)} memories in vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@
|
|||
|
||||
# examples
|
||||
# bench 里的llm ,辛苦改成 app = ReMeApp() app.default_llm
|
||||
# clear && pre-commit run --all-files
|
||||
# clear && pre-commit run --all-files
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
"""memory agent"""
|
||||
|
||||
from . import chat
|
||||
from . import retriever
|
||||
from . import summarizer
|
||||
from .base_memory_agent import BaseMemoryAgent
|
||||
|
||||
__all__ = [
|
||||
"chat",
|
||||
"retriever",
|
||||
"summarizer",
|
||||
"BaseMemoryAgent",
|
||||
]
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
"""Base memory agent for handling memory operations with tool-based reasoning."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from abc import ABCMeta
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core.enumeration import Role, MemoryType
|
||||
from ..core.op import BaseOp
|
||||
from ..core.schema import Message, ToolCall, MemoryNode
|
||||
from ..mem_tool import BaseMemoryTool, ThinkTool
|
||||
|
||||
|
||||
class BaseMemoryAgent(BaseOp, metaclass=ABCMeta):
|
||||
"""Base class for memory agents that perform reasoning and acting with memory tools."""
|
||||
|
||||
memory_type: MemoryType | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tools: list[BaseMemoryTool],
|
||||
add_think_tool: bool = False, # only for instruct model
|
||||
tool_call_interval: float = 0,
|
||||
max_steps: int = 8,
|
||||
**kwargs,
|
||||
):
|
||||
tools = tools or []
|
||||
if add_think_tool:
|
||||
tools.append(ThinkTool())
|
||||
kwargs["sub_ops"] = tools
|
||||
super().__init__(**kwargs)
|
||||
self.sub_ops: list[BaseMemoryTool] = [t for t in self.sub_ops if isinstance(t, BaseMemoryTool)]
|
||||
self.tool_call_interval: float = tool_call_interval
|
||||
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(
|
||||
**{
|
||||
"description": self.get_prompt("tool"),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "query",
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "role",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "content",
|
||||
},
|
||||
},
|
||||
"required": ["role", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def tools(self) -> list[BaseMemoryTool]:
|
||||
"""Returns the list of memory tools available to this agent."""
|
||||
return self.sub_ops
|
||||
|
||||
@tools.setter
|
||||
def tools(self, tools: list[BaseMemoryTool]):
|
||||
self.sub_ops = tools
|
||||
|
||||
def get_messages(self) -> list[Message] | str:
|
||||
"""Extracts and returns messages from the context query or messages."""
|
||||
if self.context.get("query"):
|
||||
messages = [Message(role=Role.USER, content=self.context.query)]
|
||||
elif self.context.get("messages"):
|
||||
messages = [Message(**m) if isinstance(m, dict) else m for m in self.context.messages]
|
||||
else:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
return messages
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Builds and returns the initial messages for the agent."""
|
||||
return self.get_messages()
|
||||
|
||||
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__}{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, 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__}{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: BaseMemoryTool = 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)
|
||||
|
||||
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)
|
||||
|
||||
# # 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], 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, stage=stage)
|
||||
|
||||
if not should_act:
|
||||
success = True
|
||||
break
|
||||
|
||||
tool_result_messages = await self._acting_step(assistant_message, step, stage=stage)
|
||||
messages.extend(tool_result_messages)
|
||||
|
||||
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)}",
|
||||
)
|
||||
|
||||
self.messages, self.success = await self.react(messages)
|
||||
if self.success and self.messages:
|
||||
self.output = self.messages[-1].content
|
||||
else:
|
||||
self.output = "No relevant memories found."
|
||||
|
||||
@property
|
||||
def memory_target(self) -> str:
|
||||
"""Returns the target memory identifier from context."""
|
||||
return self.context.get("memory_target", "")
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""Returns the description of the messages."""
|
||||
return self.context.get("description", "")
|
||||
|
||||
@property
|
||||
def ref_memory_id(self) -> str:
|
||||
"""Returns the reference memory ID from context."""
|
||||
return self.context.get("ref_memory_id", "")
|
||||
|
||||
@property
|
||||
def author(self) -> str:
|
||||
"""Returns the LLM model name as the author identifier."""
|
||||
return self.llm.model_name
|
||||
|
||||
@property
|
||||
def history_node(self):
|
||||
"""Returns the history node."""
|
||||
return self.context.get("history_node", None)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"""memory retriever"""
|
||||
|
||||
from .reme_retriever import ReMeRetriever
|
||||
|
||||
__all__ = [
|
||||
"ReMeRetriever",
|
||||
]
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
"""ReMe retriever that builds messages with meta memories."""
|
||||
|
||||
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 get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReMeRetriever(BaseMemoryAgent):
|
||||
"""Memory agent that retrieves and builds messages with meta memory context."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
# super().__init__(prompt_name="", **kwargs)
|
||||
super().__init__(prompt_name="reme_retriever2", **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_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`")
|
||||
|
||||
# return messages
|
||||
|
||||
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",
|
||||
now_time=get_now_time(),
|
||||
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
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
tool: |
|
||||
Retrieve relevant memories to assist in answering questions.
|
||||
Use this tool when you need to search for historical information, user preferences,
|
||||
procedural knowledge, or any other stored memories that may help answer the current query.
|
||||
The agent will analyze the context, determine what information is needed, and perform
|
||||
semantic searches across different memory types to find the most relevant memories.
|
||||
|
||||
system_prompt: |
|
||||
You are a memory agent. Please analyze the context, retrieve relevant memories when needed, and directly answer the user's question based on the retrieved information.
|
||||
|
||||
**CRITICAL**: You must ONLY answer based on the retrieved memories. DO NOT fabricate, infer, or add any information that is not explicitly present in the retrieved memories. If the retrieved memories do not contain enough information to answer the question, you must acknowledge this limitation.
|
||||
|
||||
## Current Time
|
||||
{now_time}
|
||||
|
||||
## Available Meta Memories
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Your Tasks
|
||||
|
||||
1. **Analyze** the context to determine whether retrieval is necessary:
|
||||
- If the question can be directly answered using the existing context, output `<NO_RETRIEVAL_NEEDED>` and stop.
|
||||
- If additional information is required, proceed to retrieval.
|
||||
- Consider which types of meta memory from the "Available Meta Memories" list are most relevant.
|
||||
|
||||
2. **Retrieve** relevant memories using `vector_retrieve_memory`:
|
||||
- Select the appropriate `memory_type` and `memory_target` from the "Available Meta Memories" list.
|
||||
- Clearly define the needed information and construct suitable queries.
|
||||
- Design queries flexibly based on actual needs:
|
||||
* Generate different queries for different `memory_type`/`memory_target` combinations.
|
||||
* For the same combination, create multiple queries using different phrasings or perspectives.
|
||||
* Choose the optimal combination strategy based on the retrieval scenario.
|
||||
- **Important**: When retrieving tool-related memories (`memory_type` is "tool"), the query must use the tool’s exact name (not a description or paraphrase of the problem).
|
||||
- If retrieval results include a `ref_memory_id` and more details are needed—or if vector retrieval proves insufficient—use `read_history_memory` with the `ref_memory_id` as the `memory_id` parameter.
|
||||
- **Important**: When using `read_history_memory` with multiple `ref_memory_ids`, ensure all IDs are unique and do not provide duplicate IDs.
|
||||
|
||||
3. **Iterate if necessary**:
|
||||
- If the initial retrieval fails, try alternative phrasings or perspectives.
|
||||
- If multiple memory types exist, attempt retrievals across different types.
|
||||
- Before concluding that no relevant memory exists, perform at least 2–3 retrieval attempts using varied phrasings or viewpoints.
|
||||
- If repeated vector retrievals still fail to yield sufficient information, use `read_history_memory` to fetch the original message content.
|
||||
|
||||
4. **Output** the result:
|
||||
- If no retrieval is needed, output `<NO_RETRIEVAL_NEEDED>`.
|
||||
- If relevant memories are found and you can answer the user's question, provide a concise, direct answer **strictly based on the retrieved memories only**. DO NOT add any information, inference, or speculation beyond what is explicitly stated in the retrieved memories.
|
||||
- If after multiple retrieval attempts from various angles you still cannot find relevant information, output `<NO_RELEVANT_MEMORY>`.
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
tool: |
|
||||
Retrieve relevant memories from the memory bank to assist in answering questions.
|
||||
Use this tool when you need to search for historical information, user preferences,
|
||||
procedural knowledge, or any other stored memories that may help answer the current query.
|
||||
The agent will analyze the context, determine what information is needed, and perform
|
||||
semantic searches across different memory types to find the most relevant memories.
|
||||
|
||||
system_prompt: |
|
||||
You are a memory retrieval agent. Please analyze the context, retrieve relevant information from the memory bank when needed, and return a summary of the retrieved memories to assist in answering the user's question.
|
||||
|
||||
## Context
|
||||
{context}
|
||||
|
||||
## Current Time
|
||||
{now_time}
|
||||
|
||||
## Available Meta-Memories
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Your Tasks
|
||||
1. **Analyze** the conversation context to determine whether retrieval is necessary:
|
||||
- If the question can be answered directly from the existing context, output `<NO_RETRIEVAL_NEEDED>` and stop.
|
||||
- If additional information is required, proceed with retrieval.
|
||||
- Consider which types of meta-memories from the "Available Meta-Memories" list are most relevant.
|
||||
|
||||
2. **Retrieve** relevant memories using `vector_retrieve_memory`:
|
||||
- Select `memory_type` and `memory_target` from the "Available Meta-Memories" list.
|
||||
- Clearly identify the needed information and construct appropriate queries.
|
||||
- Design queries flexibly based on actual needs:
|
||||
* Generate different queries for different `memory_type`/`memory_target` combinations.
|
||||
* For the same combination, generate multiple queries with varied phrasings or angles if needed.
|
||||
* Use the combination strategy that best fits the retrieval scenario.
|
||||
- **Important**: When retrieving tool memories (`memory_type` is "tool"), use the actual tool name as the query (not a description or question).
|
||||
- If retrieval results include a `ref_memory_id` and more detail is needed—or if vector retrieval proves insufficient—use `read_history_memory` with the `ref_memory_id` as the `memory_id` parameter.
|
||||
|
||||
3. **Iterate if necessary**:
|
||||
- If the initial retrieval yields no matches, try alternative phrasings or perspectives.
|
||||
- If multiple memory types exist, attempt retrieval across different types.
|
||||
- Before concluding that no relevant memory exists, perform at least 2–3 additional retrieval attempts using varied phrasings or angles.
|
||||
- If repeated vector retrievals still fail to provide adequate information, use `read_history_memory` to fetch the original message content.
|
||||
|
||||
4. **Output** the result:
|
||||
- If no retrieval is needed, output `<NO_RETRIEVAL_NEEDED>`.
|
||||
- If relevant memories are found, clearly summarize the retrieved information.
|
||||
- If multiple attempts still yield no relevant memories, output `<NO_RELEVANT_MEMORY>`.
|
||||
|
||||
user_message: |
|
||||
Please analyze the context, retrieve relevant information from the memory bank when needed, and return a summary of the retrieved memories to assist in answering the user's question.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from .reme_retriever_v2 import ReMeRetrieverV2
|
||||
|
||||
__all__ = [
|
||||
"ReMeRetrieverV2",
|
||||
]
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
"""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
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
"""memory summarizer"""
|
||||
|
||||
from .identity_summarizer import IdentitySummarizer
|
||||
from .personal_summarizer import PersonalSummarizer
|
||||
from .procedural_summarizer import ProceduralSummarizer
|
||||
from .reme_summarizer import ReMeSummarizer
|
||||
from .tool_summarizer import ToolSummarizer
|
||||
|
||||
__all__ = [
|
||||
"IdentitySummarizer",
|
||||
"PersonalSummarizer",
|
||||
"ProceduralSummarizer",
|
||||
"ReMeSummarizer",
|
||||
"ToolSummarizer",
|
||||
]
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"""Specialized agent for extracting and updating agent self-cognition memories."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class IdentitySummarizer(BaseMemoryAgent):
|
||||
"""Analyzes conversations to extract and update agent's self-perception."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.IDENTITY
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Construct system and user messages with formatted context and timestamp."""
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
now_time=get_now_time(),
|
||||
context=format_messages(self.get_messages()),
|
||||
memory_type=self.memory_type.value,
|
||||
)
|
||||
|
||||
messages = [
|
||||
Message(role=Role.SYSTEM, content=system_prompt),
|
||||
Message(role=Role.USER, content=self.get_prompt("user_message")),
|
||||
]
|
||||
return messages
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with workspace_id and author context."""
|
||||
return await super()._acting_step(assistant_message, step, author=self.author, **kwargs)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
tool: |
|
||||
Update agent self-cognition based on conversation context.
|
||||
First read existing self-cognition using `read_identity_memory`, then analyze the context to determine if updates are needed, and use `update_identity_memory` to update when necessary.
|
||||
|
||||
system_prompt: |
|
||||
You are a specialized memory agent in the domain of self-awareness. Your task is to update the main agent's self-perception based on the provided context.
|
||||
|
||||
## Context:
|
||||
{context}
|
||||
|
||||
## Current Time:
|
||||
{now_time}
|
||||
|
||||
## Your Responsibilities:
|
||||
|
||||
1. **Read the main agent's current self-perception**: Retrieve it using `read_identity_memory`.
|
||||
|
||||
2. **Analyze the context** to determine whether an update to self-perception is needed:
|
||||
- Extract any self-perception–related information from the dialogue (e.g., self-awareness, personality traits, current state, etc.).
|
||||
- If no relevant self-perception information is found, output `<NO_MEMORY_NEEDED>` and halt further processing.
|
||||
|
||||
3. **Update if necessary**: Use `update_identity_memory` to perform the update:
|
||||
- Compare the extracted information with the existing self-perception.
|
||||
- If an update is required (due to new information, corrections, or additions), invoke `update_identity_memory`.
|
||||
- If no update is needed, output `<NO_MEMORY_NEEDED>`.
|
||||
|
||||
user_message: |
|
||||
Please analyze the context and update the main agent's self-perception if necessary.
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
"""Specialized agent for extracting and managing personal memories about specific individuals."""
|
||||
|
||||
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 get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class PersonalSummarizer(BaseMemoryAgent):
|
||||
"""Extracts and stores personal information about individuals from conversations."""
|
||||
|
||||
def __init__(self, recent_top_k: int = 20, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.recent_top_k: int = recent_top_k
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
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 _retrieve_recent_memories(self) -> str:
|
||||
"""Retrieve recent memories sorted by time_modified."""
|
||||
from ...mem_tool import RetrieveRecentMemory
|
||||
|
||||
op = RetrieveRecentMemory(top_k=self.recent_top_k)
|
||||
await op.call(memory_type="personal", memory_target=self.memory_target, retrieved_nodes=self.retrieved_nodes)
|
||||
return op.output
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Construct messages with context, memory_target, and memory_type information."""
|
||||
await self._retrieve_recent_memories()
|
||||
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
now_time=get_now_time(),
|
||||
recent_memories="\n".join([n.format_memory() for n in self.retrieved_nodes]),
|
||||
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 _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with memory_target, memory_type, and author context."""
|
||||
return 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,
|
||||
)
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
tool: |
|
||||
Extract and store personal memories from conversation context.
|
||||
Use this tool to analyze dialogues and extract important personal information about users,
|
||||
such as preferences, habits, personal background, relationships, and significant facts.
|
||||
The agent will determine whether the information is worth remembering, check for duplicates
|
||||
or conflicts with existing memories, and perform add, update, or delete operations as needed.
|
||||
|
||||
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}
|
||||
|
||||
## Current Time:
|
||||
{now_time}
|
||||
|
||||
## Recent Memories:
|
||||
{recent_memories}
|
||||
|
||||
## 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:
|
||||
|
||||
1. **Analyze and Extract** potential memories from the dialogue 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.
|
||||
- If the dialogue is casual chatter or contains no valuable information, output `<NO_MEMORY_NEEDED>` and stop.
|
||||
- Extract key information 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.
|
||||
- Each memory entry must be self-contained and understandable without additional context.
|
||||
- Avoid storing trivial or temporary information.
|
||||
- **CRITICAL**: After extraction, immediately deduplicate within the extracted memories themselves - if multiple extracted items convey the same core information (even with slightly different wording), keep ONLY the most complete and accurate one.
|
||||
- Before proceeding, list all deduplicated extracted memories in your response.
|
||||
|
||||
2. **Retrieve similar historical memories** using `vector_retrieve_memory`:
|
||||
- For EACH extracted memory, perform a semantic similarity search to find existing, potentially relevant memories (e.g., for "Person A was born on date X", search for "Person A birth date age").
|
||||
- Retrieve all related memories for thorough comparison to prevent any duplication.
|
||||
|
||||
3. **Compare and Decide** on memory operations with STRICT deduplication:
|
||||
- Compare the newly extracted memories with **both Recent Memories and historical memories** retrieved in the previous step.
|
||||
- **CRITICAL DEDUPLICATION CHECK**: Before adding ANY new memory:
|
||||
- Check if the SAME INFORMATION already exists in Recent Memories or retrieved historical 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
|
||||
- **Use `update_memory` and `delete_memory` to actively deduplicate and resolve conflicts:**
|
||||
- If multiple existing memories contain duplicate or overlapping information: use `delete_memory` to remove redundant ones, then use `update_memory` to consolidate all information into a single, comprehensive memory.
|
||||
- If memories conflict (contradictory information): use `delete_memory` to remove outdated/incorrect ones, then use `update_memory` or `add_memory` to store the correct version.
|
||||
- Choose the appropriate operation based on the situation:
|
||||
- **If the information already exists in Recent Memories or historical memories and is consistent: SKIP—no action needed. Do NOT add duplicate memories.**
|
||||
- If existing memory (recent or historical) needs supplementation with NEW details: use `update_memory` to enhance and consolidate it.
|
||||
- If existing memory (recent or historical) is outdated or contradicted by new information: use `delete_memory` to remove it, then `add_memory` for the corrected version.
|
||||
- If multiple memories contain similar/overlapping information: use `delete_memory` to remove duplicates, then `update_memory` to merge into one.
|
||||
- If the information is entirely new and not present in either Recent Memories or historical memories: use `add_memory` to add it to the memory store.
|
||||
- **When in doubt, prefer updating or consolidating existing memories over adding new ones to avoid redundancy.**
|
||||
|
||||
4. **Output** the result:
|
||||
- If no memory operation is required, output `<NO_MEMORY_NEEDED>`.
|
||||
- If memories were added, updated, or deleted, summarize the operations performed.
|
||||
|
||||
## Guidelines:
|
||||
- Be selective: store only truly important information.
|
||||
- Stay concise: each memory should be clear and atomic.
|
||||
- **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 memories
|
||||
* Then, check against Recent Memories (provided above)
|
||||
* Finally, use `vector_retrieve_memory` to check against historical memories
|
||||
* **Actively use `delete_memory` to remove duplicate or conflicting memories**
|
||||
* **Use `update_memory` 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 (e.g., timestamps) when appropriate.
|
||||
- **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 analyze the context to determine whether important information should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""Specialized agent for extracting and managing procedural knowledge and workflows."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ProceduralSummarizer(BaseMemoryAgent):
|
||||
"""Extracts step-by-step procedures, best practices, and task-completion strategies."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.PROCEDURAL
|
||||
|
||||
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",
|
||||
now_time=get_now_time(),
|
||||
context=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 _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with ref_memory_id, memory_target, memory_type, and author context."""
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
ref_memory_id=self.ref_memory_id,
|
||||
memory_target=self.memory_target,
|
||||
memory_type=self.memory_type.value,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
tool: |
|
||||
Extract and store procedural memories from conversation context.
|
||||
Use this tool to analyze dialogues and extract important procedural knowledge,
|
||||
such as step-by-step workflows, how-to guides, best practices, problem-solving methods,
|
||||
debugging techniques, and task completion strategies.
|
||||
The agent will also reflect on task outcomes - extracting lessons from failures
|
||||
and successful strategies from successes to improve future performance.
|
||||
|
||||
system_prompt: |
|
||||
You are a professional memory Agent specializing. Your task is to update the main agent's {memory_type} memory regarding {memory_target} based on the context.
|
||||
|
||||
## Context:
|
||||
{context}
|
||||
|
||||
## Current Time:
|
||||
{now_time}
|
||||
|
||||
## Memory Objective:
|
||||
You are managing **{memory_type}** memories about **{memory_target}** for the main Agent. Focus on extracting and storing procedural knowledge, such as:
|
||||
- Step-by-step procedures and workflows
|
||||
- Operational guides and instructions
|
||||
- Best practices and methodologies
|
||||
- Established routines and processes
|
||||
- Problem-solving techniques and troubleshooting tips
|
||||
- Task-completion strategies
|
||||
|
||||
## Your Tasks:
|
||||
|
||||
1. **Analyze and Extract** potential memories from the conversation context:
|
||||
- Determine whether the dialogue contains procedural knowledge worth remembering, including but not limited to:
|
||||
- Multi-step procedures or workflows
|
||||
- Instructions for completing specific tasks
|
||||
- Best practices or recommended approaches
|
||||
- Problem-solving methods or debugging tips
|
||||
- Configuration or setup processes
|
||||
- If the context includes task outcome information:
|
||||
- **Successful tasks**: Extract and reflect on successful experiences; summarize key success factors, effective methods, and reusable strategies.
|
||||
- **Failed tasks**: Extract and reflect on lessons learned; analyze root causes of failure, pitfalls to avoid, and improvement suggestions.
|
||||
- **Both success and failure**: Conduct comparative reflection; identify critical differences and distill key decision factors and best practices.
|
||||
- If the conversation is casual chat or contains no valuable information, output `<NO_MEMORY_NEEDED>` and stop.
|
||||
- Express extracted information clearly and concisely.
|
||||
- Each memory entry should be self-contained and understandable without additional context.
|
||||
- Avoid storing trivial or transient information.
|
||||
- Before proceeding, list all extracted memories in your response.
|
||||
|
||||
2. **Retrieve similar historical memories** using `vector_retrieve_memory`:
|
||||
- Perform a semantic similarity search based on the extracted memories.
|
||||
- Retrieve potentially relevant existing memories for comparison to check for duplicates or associations.
|
||||
|
||||
3. **Compare and Decide** on memory operations:
|
||||
- Compare extracted memories against historical ones to ensure no duplicates or conflicts exist in the final memory repository.
|
||||
- Choose the appropriate operation based on the situation:
|
||||
- If the information already exists and is consistent: skip (no action needed).
|
||||
- If existing memory needs supplementation or correction: use `update_memory` to revise it.
|
||||
- If existing memory is outdated or incorrect: use `delete_memory` to remove it.
|
||||
- If the information is entirely new: use `add_memory` to add it to the memory repository.
|
||||
|
||||
4. **Output** the result:
|
||||
- If no memory operation is needed, output `<NO_MEMORY_NEEDED>`.
|
||||
- If memories were added, updated, or deleted, summarize the performed operations.
|
||||
|
||||
## Guidelines:
|
||||
- **Be selective**: Store only truly important information.
|
||||
- **Stay concise**: Each memory should be clear and atomic.
|
||||
- **Be precise and accurate**: Ensure extracted content faithfully reflects the original context.
|
||||
- **Avoid redundancy**: Always check for similar existing memories before adding new ones.
|
||||
- **Include relevant metadata when appropriate** (e.g., timestamp, preconditions, expected outcomes).
|
||||
|
||||
user_message: |
|
||||
Please analyze the context to determine whether important procedural knowledge should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary.
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
"""Orchestrator for complete memory summarization workflow across all memory types."""
|
||||
|
||||
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 get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReMeSummarizer(BaseMemoryAgent):
|
||||
"""Coordinates memory updates by delegating to specialized memory agents."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, enable_identity_memory: bool = False, **kwargs):
|
||||
"""Initialize with flags to enable/disable identity memory processing."""
|
||||
super().__init__(**kwargs)
|
||||
self.enable_identity_memory = enable_identity_memory
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
# Check if AddMetaMemory is in tools
|
||||
self.enable_add_meta_memory = self._check_add_meta_memory_in_tools()
|
||||
|
||||
def _check_add_meta_memory_in_tools(self) -> bool:
|
||||
"""Check if AddMetaMemory tool is present in the tools list."""
|
||||
from ...mem_tool import AddMetaMemory
|
||||
|
||||
for tool in self.tools:
|
||||
if isinstance(tool, AddMetaMemory):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": self.prompt_format("tool", enable_add_meta_memory=self.enable_add_meta_memory),
|
||||
"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_identity_memory(self) -> str:
|
||||
"""Retrieve agent's self-perception memory."""
|
||||
if self.enable_identity_memory:
|
||||
from ...mem_tool import ReadIdentityMemory
|
||||
|
||||
op = ReadIdentityMemory()
|
||||
await op.call()
|
||||
return op.output
|
||||
else:
|
||||
return ""
|
||||
|
||||
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=self.enable_identity_memory)
|
||||
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]:
|
||||
"""Construct initial messages with context, identity, 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
|
||||
|
||||
now_time = get_now_time()
|
||||
identity_memory = await self._read_identity_memory()
|
||||
meta_memory_info = await self._read_meta_memories()
|
||||
logger.info(f"now_time={now_time} identity_memory={identity_memory} meta_memory_info={meta_memory_info}")
|
||||
|
||||
system_prompt = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
now_time=now_time,
|
||||
identity_memory=identity_memory,
|
||||
meta_memory_info=meta_memory_info,
|
||||
context=self.context["messages_formated"],
|
||||
enable_add_meta_memory=self.enable_add_meta_memory,
|
||||
)
|
||||
|
||||
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 _reasoning_step(self, messages: list[Message], step: int, **kwargs) -> tuple[Message, bool]:
|
||||
"""Refresh meta-memory info in system prompt before each reasoning step."""
|
||||
system_messages = [message for message in messages if message.role is Role.SYSTEM]
|
||||
|
||||
if system_messages:
|
||||
system_message = system_messages[0]
|
||||
system_message.content = self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
now_time=get_now_time(),
|
||||
identity_memory=await self._read_identity_memory(),
|
||||
meta_memory_info=await self._read_meta_memories(),
|
||||
context=self.context["messages_formated"],
|
||||
enable_add_meta_memory=self.enable_add_meta_memory,
|
||||
)
|
||||
|
||||
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 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,
|
||||
)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
tool: |
|
||||
Orchestrate the complete memory summarization workflow for the agent.
|
||||
This tool receives conversation context and performs necessary memory updates including:
|
||||
[enable_add_meta_memory]- Creating new meta-memory entries if needed
|
||||
- Adding summary memory for quick future recall
|
||||
- Delegating to specialized memory agents for detailed memory extraction and update
|
||||
|
||||
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}
|
||||
|
||||
## Current Time
|
||||
{now_time}
|
||||
|
||||
## Main Agent's Self-Perception
|
||||
{identity_memory}
|
||||
|
||||
## 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 Tasks
|
||||
|
||||
### 1. Add Summary Memory
|
||||
- Use `add_summary_memory` to store a concise summary.
|
||||
- The summary should capture key points, decisions, or important facts to aid later recollection of the original conversation.
|
||||
|
||||
[enable_add_meta_memory]### 2. Create New Meta Memory (if needed)
|
||||
[enable_add_meta_memory]When the context contains significant new valuable information, first check if the Main Agent's Meta Memory already contains a corresponding `<memory_type>(<memory_target>)` entry:
|
||||
[enable_add_meta_memory]- If the required `<memory_type>(<memory_target>)` does NOT exist in the Meta Memory, use `add_meta_memory` to create a new meta memory entry.
|
||||
[enable_add_meta_memory]- For personal memories: specify `memory_type="personal"` and `memory_target=<person's name>`.
|
||||
[enable_add_meta_memory]- For procedural memories: specify `memory_type="procedural"` and `memory_target=<topic or domain>`.
|
||||
[enable_add_meta_memory]- Each meta memory entry will instantiate a dedicated specialized Memory Agent for that dimension.
|
||||
[enable_add_meta_memory]- Only create new meta memory entries when necessary; avoid duplicating existing ones.
|
||||
[enable_add_meta_memory]
|
||||
|
||||
### 3. Delegate to Specialized Memory Agents
|
||||
You do not need to summarize or update memories yourself. Instead, analyze the context, identify which memory dimensions (memory_type + memory_target) from the existing meta memory require updates, and delegate using `hands_off`:
|
||||
- The parameters of `hands_off` (`memory_type` and `memory_target`) must exactly match an existing entry in the "Main Agent's Meta Memory" listed above.
|
||||
- You may delegate concurrently to multiple specialized agents to enable parallel memory processing.
|
||||
- Each specialized agent will perform detailed memory extraction, addition, updating, or deletion within its assigned dimension.
|
||||
|
||||
## Output Requirements
|
||||
- If the context contains no memorable information (e.g., simple greetings), output `<NO_MEMORY_NEEDED>`.
|
||||
- If any memory operations were performed, briefly summarize what was done.
|
||||
|
||||
user_message: |
|
||||
Please perform your task based on the context.
|
||||
|
||||
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""Specialized agent for extracting and managing tool usage guidelines and best practices."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import get_now_time, format_messages
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ToolSummarizer(BaseMemoryAgent):
|
||||
"""Analyzes tool executions to extract effective usage patterns and optimization tips."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.TOOL
|
||||
|
||||
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",
|
||||
now_time=get_now_time(),
|
||||
context=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 _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
"""Execute tool calls with ref_memory_id, memory_target, memory_type, and author context."""
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
ref_memory_id=self.ref_memory_id,
|
||||
memory_target=self.memory_target,
|
||||
memory_type=self.memory_type.value,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
tool: |
|
||||
Extract and store tool usage guidelines from tool call execution context.
|
||||
Use this tool to analyze tool calls and their results, extracting valuable insights
|
||||
about how to use tools more effectively, including best practices, common patterns,
|
||||
error handling strategies, and optimization tips.
|
||||
The agent will determine whether the information is worth remembering, check for duplicates
|
||||
or conflicts with existing tool guidelines, and perform add, update, or delete operations as needed.
|
||||
|
||||
system_prompt: |
|
||||
You are a professional memory Agent specializing in the domain of **{memory_target}**. Please analyze the tool execution context, and your task is to update the main Agent's **{memory_type}** memory regarding **{memory_target}** based on this context.
|
||||
|
||||
## Context:
|
||||
{context}
|
||||
|
||||
## Current Time:
|
||||
{now_time}
|
||||
|
||||
## Memory Target:
|
||||
You are managing the main Agent’s **{memory_type}** memory about **{memory_target}**. Focus on extracting and storing guidelines, best practices, and insights on how to effectively use this tool.
|
||||
|
||||
## Your Tasks:
|
||||
|
||||
1. **Analyze and Extract** tool usage guidelines from the execution context:
|
||||
- Determine whether the tool invocation and its results contain valuable insights worth remembering, including but not limited to: successful usage patterns and best practices; common errors and how to avoid them; effective parameter combinations; performance optimization tips; edge cases and special handling requirements.
|
||||
- If the tool execution represents a routine operation with no new insights, output `<NO_MEMORY_NEEDED>` and stop.
|
||||
- Extract key guidelines in a clear and actionable manner.
|
||||
- Each guideline should be self-contained and directly applicable.
|
||||
- Avoid storing trivial or obvious information.
|
||||
- Before proceeding, list all extracted guidelines in your response.
|
||||
|
||||
2. **Retrieve historical guidelines** for this tool by calling `vector_retrieve_memory`, using the `tool_name` as the query parameter to fetch any existing guidelines.
|
||||
|
||||
3. **Compare and Decide** on the appropriate memory operation:
|
||||
- Compare the newly extracted guidelines with the historical ones to ensure the final memory store contains no duplicates or contradictions.
|
||||
- Normally, `vector_retrieve_memory` should return at most one guideline per tool. If multiple guidelines exist for the same tool, use `delete_memory` to remove the redundant entries and merge all useful information into a single, comprehensive guideline.
|
||||
- Choose the appropriate action based on the situation:
|
||||
- If the guideline already exists and is consistent: skip—no action needed.
|
||||
- If the existing guideline needs supplementation or refinement: use `update_memory` to enhance it.
|
||||
- If the existing guideline is outdated or incorrect: use `update_memory` to replace it with the correct version.
|
||||
- If multiple guidelines exist for the same tool: use `delete_memory` to remove duplicates, then use `update_memory` on the remaining entry to consolidate all useful information.
|
||||
- If the guideline is entirely new: use `add_memory` to add it to the memory store.
|
||||
|
||||
4. **Output** the result:
|
||||
- If no memory operation is required, output `<NO_MEMORY_NEEDED>`.
|
||||
- If you added, updated, or deleted any guidelines, summarize the operations performed.
|
||||
|
||||
## Guidelines:
|
||||
- **Be selective**: Only retain insights that genuinely improve tool usage efficiency.
|
||||
- **Keep it actionable**: Each guideline should offer clear, practical advice.
|
||||
- **Ensure accuracy**: Verify that extracted guidelines are supported by actual tool execution results.
|
||||
- **Avoid redundancy**: Always check for similar existing guidelines before adding new ones.
|
||||
- **Include relevant context when appropriate** (e.g., parameter values, error messages).
|
||||
|
||||
user_message: |
|
||||
Please analyze the tool execution context to determine whether important usage guidelines should be extracted and stored as memory, and perform memory addition, deletion, or update operations when necessary.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
"""Simplified V2 summarizers for memory management."""
|
||||
|
||||
from .reme_summarizer_v2 import ReMeSummarizerV2
|
||||
from .personal_summarizer_v2 import PersonalSummarizerV2
|
||||
|
||||
__all__ = ["ReMeSummarizerV2", "PersonalSummarizerV2"]
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
"""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):
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
"""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 _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
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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.
|
||||
|
||||
# - **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.
|
||||
|
||||
system_prompt: |
|
||||
You are a professional memory agent managing **{memory_type}** memories about **{memory_target}** for the main agent.
|
||||
|
||||
## Latest Conversation:
|
||||
The context below contains the most recent conversation. Each message is formatted as: `round<index> [<timestamp>] <role/name>: <content>` where timestamp is `YYYY-MM-DD HH:MM:SS`.
|
||||
{context}
|
||||
|
||||
**CRITICAL**: Extract information ONLY from what is explicitly stated. DO NOT infer, assume, or fabricate any information.
|
||||
|
||||
## Your Tasks
|
||||
|
||||
### Step 1: Generate Memory Drafts
|
||||
Use `AddMemoryDrafts` to extract key facts from the latest conversation.
|
||||
- Extract important information: preferences, habits, currentstatus, personal details, key facts, decisions, or conclusions.
|
||||
- Use clear, concise phrasing based strictly on explicit statements.
|
||||
- Record the timestamp of the source message for each memory including the year, month, and day.
|
||||
|
||||
### Step 2: Retrieve Similar and Recent Memories
|
||||
Use `RetrieveRecentAndSimilarMemories` to query historical memories for each draft.
|
||||
- Search for semantically similar memories and recent memories.
|
||||
- This ensures Step 3 avoids duplicates and properly updates existing memories.
|
||||
|
||||
### Step 3: Update Memories
|
||||
Use `UpdateMemories` to update the memory store by combining drafts with historical memories.
|
||||
- **Delete conflicts**: Remove old memories that contradict the new drafts (keep most recent/accurate).
|
||||
- **Add new**: Add drafts that represent completely new information.
|
||||
- **Skip duplicates**: Do not add drafts that duplicate existing memories.
|
||||
- **Preserve others**: Keep unrelated historical memories unchanged.
|
||||
- Write concise memories using minimum words needed. Ensure no information loss.
|
||||
|
||||
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
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
"""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.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 _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,
|
||||
)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from .personal_summarizer_v3 import PersonalSummarizerV3
|
||||
from .reme_retriever_v3 import ReMeRetrieverV3
|
||||
from .reme_summarizer_v3 import ReMeSummarizerV3
|
||||
|
||||
__all__ = [
|
||||
"PersonalSummarizerV3",
|
||||
"ReMeRetrieverV3",
|
||||
"ReMeSummarizerV3",
|
||||
]
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class PersonalSummarizerV3(BaseMemoryAgent):
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
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,
|
||||
)
|
||||
return messages
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
tool: |
|
||||
Extract and update personal memories about the user from conversation context.
|
||||
Analyze dialogues to identify preferences, habits, background, relationships, and key facts.
|
||||
|
||||
system_prompt: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Latest Conversation:
|
||||
{context}
|
||||
|
||||
Each message format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
|
||||
**CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate.
|
||||
|
||||
## Three-Step Workflow
|
||||
|
||||
### Step 1: Extract Conversation Memories
|
||||
Use `AddMemory` to extract key personal facts from the conversation.
|
||||
- Extract: preferences, habits, status, personal details, decisions, conclusions
|
||||
- **Format**: Use third-person perspective to record what **{memory_target}** said, did, or expressed at specific times
|
||||
- **Consolidation**: Merge related information under the same topic into ONE memory entry
|
||||
- Group similar facts (e.g., multiple food preferences → one food preference entry)
|
||||
- Avoid creating separate entries for closely related information
|
||||
- Keep entries concise and distinct (no duplicates, no omissions)
|
||||
- Record `conversation_time` for each memory (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable)
|
||||
|
||||
### Step 2: Read User Profile
|
||||
Use `ReadUserProfile` to retrieve the current user profile.
|
||||
- Review existing memories to identify conflicts and duplicates
|
||||
|
||||
### Step 3: Update User Profile
|
||||
Use `UpdateUserProfile` to synchronize the profile with new information.
|
||||
- `profile_ids_to_delete`: Remove outdated or conflicting profiles
|
||||
- `profiles_to_add`: Add new profiles that are not duplicates
|
||||
- Use `timestamp` from conversation_time (format: 2020-01-01 00:00:00)
|
||||
- Keep final profiles concise with no information loss
|
||||
|
||||
user_message: |
|
||||
Execute the three-step workflow:
|
||||
1. Use `AddMemory` to extract personal memories from the conversation
|
||||
2. Use `ReadUserProfile` to read existing user profile
|
||||
3. Use `UpdateUserProfile` to remove outdated entries and add new profiles
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"""ReMe retriever v2 that autonomously retrieves memories from multiple angles."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeRetrieverV3(BaseMemoryAgent):
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
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)
|
||||
return op.format_memory_metadata(self.meta_memories)
|
||||
|
||||
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
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
tool: |
|
||||
Autonomously retrieve relevant memories through a three-step strategy to answer user questions.
|
||||
Steps: read user profile → vector search with multiple angles → read original conversations.
|
||||
State "I don't know" if information cannot be found after exhaustive searching.
|
||||
NEVER hallucinate or fabricate information not present in retrieved memories.
|
||||
|
||||
system_prompt: |
|
||||
You are a memory agent. Search for relevant memories to answer the user's question following this strategy:
|
||||
|
||||
## Available Meta Memories
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## User's Question
|
||||
{context}
|
||||
|
||||
## Three-Step Retrieval Strategy
|
||||
|
||||
**STEP 1: Read User Profile (REQUIRED FIRST)**
|
||||
- Use `read_user_profile` with memory_type and memory_target from available meta memories
|
||||
- Check if the user profile directly answers the question
|
||||
|
||||
**STEP 2: Vector Search (If Step 1 insufficient)**
|
||||
- Use `retrieve_memory` with memory_type, memory_target, and query
|
||||
- Try multiple retrieval angles (at least 3 different attempts):
|
||||
* Direct query with user's question
|
||||
* Reformulated queries with different phrasing/keywords
|
||||
* Queries focused on specific entities or concepts
|
||||
|
||||
- **Time Range Filtering** (when applicable):
|
||||
* Format: [start_date, end_date] in YYYYMMDD format
|
||||
* Example: [20200101, 20200102] means 20200101 < time < 20200102
|
||||
* Single-sided: [0, 20200102] for before, [20200101, 99999999] for after
|
||||
* If no results, try broader time ranges or remove time constraints
|
||||
|
||||
- If no results after multiple attempts, try different memory_type/memory_target combinations
|
||||
|
||||
**STEP 3: Read Original Conversations (If Step 2 insufficient)**
|
||||
- Use `read_history` with history_id from retrieved memories
|
||||
- Prioritize reading:
|
||||
* Most recent memories with history_id
|
||||
* Most relevant memories from Step 2 with history_id
|
||||
- Try multiple history_id entries if needed
|
||||
|
||||
## Response Rules
|
||||
- If nothing found after all three steps: State clearly "I don't know. "
|
||||
- Be persistent: try multiple angles in each step before moving to the next
|
||||
|
||||
user_message: |
|
||||
Answer the question using the three-step strategy.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeSummarizerV3(BaseMemoryAgent):
|
||||
|
||||
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.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 _read_meta_memories(self) -> str:
|
||||
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,
|
||||
)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from .reme_summarizer_v4 import ReMeSummarizerV4
|
||||
from .reme_retriever_v4 import ReMeRetrieverV4
|
||||
from .personal_summarizer_v4 import PersonalSummarizerV4
|
||||
from .personal_retriever_v4 import PersonalRetrieverV4
|
||||
|
||||
__all__ = [
|
||||
"ReMeSummarizerV4",
|
||||
"ReMeRetrieverV4",
|
||||
"PersonalSummarizerV4",
|
||||
"PersonalRetrieverV4",
|
||||
]
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
tool: |
|
||||
Retrieve relevant personal memories to answer user questions through vector search and history reading.
|
||||
|
||||
user_message: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## User Profile
|
||||
{user_profile}
|
||||
|
||||
## Question
|
||||
{context}
|
||||
|
||||
## Task
|
||||
Search for relevant memories to answer the question above.
|
||||
|
||||
**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 [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 history_id from retrieved memories to read original conversations
|
||||
- Read multiple if needed for complete context
|
||||
|
||||
**Response**
|
||||
- If found relevant memories: respond exactly `<MEMORY_FOUND>`
|
||||
- If no memory found after thorough search: respond exactly `<MEMORY_NOT_FOUND>`
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
tool: |
|
||||
Extract and update personal memories about the user from conversation context.
|
||||
Identify preferences, habits, background, relationships, and key facts.
|
||||
|
||||
user_message_phase1: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Latest Conversation:
|
||||
{context}
|
||||
|
||||
Message format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
|
||||
**CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate.
|
||||
|
||||
## Task: Extract Memories with `AddSummaryMemory`
|
||||
|
||||
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)
|
||||
|
||||
Extract personal memories from the conversation using `AddSummaryMemory`.
|
||||
|
||||
# capturing complete contexts with preconditions, causes, and consequences
|
||||
user_message_phase2: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Latest Conversation:
|
||||
{context}
|
||||
|
||||
Message format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
|
||||
**CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate.
|
||||
|
||||
## Current User Profile:
|
||||
{user_profile}
|
||||
|
||||
## Task: Update Profile with `UpdateUserProfile`
|
||||
|
||||
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
|
||||
|
||||
Update user profile using `UpdateUserProfile` based on the conversation and current profile.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
tool: |
|
||||
Retrieve information from specialized memory agents to answer user queries.
|
||||
|
||||
system_prompt: |
|
||||
You are a Memory Retrieval Orchestrator responsible for querying specialized agents to answer user questions.
|
||||
|
||||
# User Query
|
||||
{user_query}
|
||||
|
||||
## Available Memory Agents
|
||||
Each line indicates a specialized Memory Agent that stores and retrieves memories within a specific dimension (memory_type + memory_target).
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Your Task
|
||||
1. Use the `hands_off` tool to retrieve information from relevant agents
|
||||
- Specify `memory_type` and `memory_target` for each query
|
||||
- The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above
|
||||
- Do NOT query agents that don't exist above
|
||||
- You can query multiple agents if needed
|
||||
2. Answer the user query STRICTLY based on the `hands_off` results
|
||||
3. If the retrieved information is insufficient to answer the query, respond: "nothing found after thorough search."
|
||||
|
||||
user_message: |
|
||||
Please retrieve relevant information from the existing agents and provide an answer based on the results.
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeSummarizerV4(BaseMemoryAgent):
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
from ...mem_tool import ReadMetaMemory
|
||||
meta_memory_info = ReadMetaMemory().format_memory_metadata(self.meta_memories)
|
||||
logger.info(f"meta_memory_info={meta_memory_info}")
|
||||
return meta_memory_info
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
self.context.messages = [Message(**m) if isinstance(m, dict) else m for m in self.context.messages]
|
||||
history_content = self.description + "\n" + format_messages(self.context.messages)
|
||||
self.context.history_node = history_node = MemoryNode(
|
||||
memory_type=MemoryType.HISTORY,
|
||||
memory_target="",
|
||||
when_to_use=history_content[:100],
|
||||
content=history_content,
|
||||
ref_memory_id="",
|
||||
author=self.author,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
logger.info(f"Adding summary node: {history_node.model_dump_json(indent=2, exclude_none=True)}")
|
||||
await self.vector_store.delete(history_node.memory_id)
|
||||
await self.vector_store.insert([history_node.to_vector_node()])
|
||||
|
||||
messages = [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self._read_meta_memories(),
|
||||
context=history_node.content,
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message"),
|
||||
),
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
messages=self.context.messages,
|
||||
history_node=self.context.history_node,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
tool: |
|
||||
Orchestrate memory updates across specialized memory agents.
|
||||
|
||||
system_prompt: |
|
||||
You are a Memory Orchestrator responsible for routing memory tasks to specialized agents based on the context.
|
||||
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Available Memory Agents
|
||||
Each line indicates a specialized Memory Agent dedicated to deep summarization and updating of memories within a specific dimension (memory_type + memory_target).
|
||||
Format: "- <memory_type>(<memory_target>): <description>"
|
||||
{meta_memory_info}
|
||||
|
||||
## Your Task
|
||||
Use the `hands_off` tool to distribute memory tasks to specialized agents:
|
||||
1. Analyze the context and identify which memory dimensions require updates
|
||||
2. Specify `memory_type` and `memory_target` for each task
|
||||
- The `memory_type` and `memory_target` must **exactly match** existing entries in the "Available Memory Agents" listed above
|
||||
- Do NOT create new agents or use memory_type/memory_target combinations that don't exist above
|
||||
3. Multiple tasks can be specified to enable parallel processing by specialized agents
|
||||
|
||||
Note: If the context contains no memorable information (e.g., simple greetings), return `<NO_MEMORY_NEEDED>`.
|
||||
|
||||
user_message: |
|
||||
Please analyze the context and route memory tasks to the appropriate existing agents.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from .personal_summarizer_wk import PersonalSummarizerWk
|
||||
from .reme_retriever_wk import ReMeRetrieverV2
|
||||
from .reme_summarizer_wk import ReMeSummarizerWk
|
||||
|
||||
__all__ = [
|
||||
"PersonalSummarizerWk",
|
||||
"ReMeRetrieverV2",
|
||||
"ReMeSummarizerWk",
|
||||
]
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class PersonalSummarizerWk(BaseMemoryAgent):
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
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,
|
||||
)
|
||||
return messages
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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 managing **{memory_type}** memories about **{memory_target}** for the main agent.
|
||||
|
||||
## Latest Conversation:
|
||||
The context below contains the most recent conversation. Each message is formatted as: `round<index> [<timestamp>] <role/name>: <content>` where timestamp is `YYYY-MM-DD HH:MM:SS`.
|
||||
{context}
|
||||
|
||||
**CRITICAL**: Extract information ONLY from what is explicitly stated. DO NOT infer, assume, or fabricate any information.
|
||||
|
||||
## Your Tasks
|
||||
|
||||
### Step 1: Generate Memory Drafts
|
||||
Use `AddMemoryDrafts` to extract key facts from the latest conversation.
|
||||
- Extract important information: preferences, habits, currentstatus, personal details, key facts, decisions, or conclusions.
|
||||
- Use clear, concise phrasing based strictly on explicit statements.
|
||||
- Record the timestamp of the source message for each memory including the year, month, and day.
|
||||
|
||||
### Step 2: Retrieve Similar and Recent Memories
|
||||
Use `RetrieveRecentAndSimilarMemories` to query historical memories for each draft.
|
||||
- Search for semantically similar memories and recent memories.
|
||||
- This ensures Step 3 avoids duplicates and properly updates existing memories.
|
||||
|
||||
### Step 3: Update Memories
|
||||
Use `UpdateMemories` to update the memory store by combining drafts with historical memories.
|
||||
- **Delete conflicts**: Remove old memories that contradict the new drafts (keep most recent/accurate).
|
||||
- **Add new**: Add drafts that represent completely new information.
|
||||
- **Skip duplicates**: Do not add drafts that duplicate existing memories.
|
||||
- **Preserve others**: Keep unrelated historical memories unchanged.
|
||||
- Write concise memories using minimum words needed. Ensure no information loss.
|
||||
|
||||
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
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"""ReMe retriever v2 that autonomously retrieves memories from multiple angles."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role
|
||||
from ...core.schema import Message
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeRetrieverV2(BaseMemoryAgent):
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
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)
|
||||
return op.format_memory_metadata(self.meta_memories)
|
||||
|
||||
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
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode, ToolCall
|
||||
from ...core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeSummarizerWk(BaseMemoryAgent):
|
||||
|
||||
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.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 _read_meta_memories(self) -> str:
|
||||
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,
|
||||
)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
"""Memory tool operations."""
|
||||
|
||||
from .base_memory_tool import BaseMemoryTool
|
||||
from .hands_off_tool import HandsOffTool
|
||||
from .history.add_history_memory import AddHistoryMemory
|
||||
from .history.read_history_memory import ReadHistoryMemory
|
||||
from .identity.read_identity_memory import ReadIdentityMemory
|
||||
from .identity.update_identity_memory import UpdateIdentityMemory
|
||||
from .meta.add_meta_memory import AddMetaMemory
|
||||
from .meta.read_meta_memory import ReadMetaMemory
|
||||
from .think_tool import ThinkTool
|
||||
from .vector_store.add_memory import AddMemory
|
||||
from .vector_store.add_summary_memory import AddSummaryMemory
|
||||
from .vector_store.delete_memory import DeleteMemory
|
||||
from .vector_store.retrieve_recent_memory import RetrieveRecentMemory
|
||||
from .vector_store.update_memory import UpdateMemory
|
||||
from .vector_store.vector_retrieve_memory import VectorRetrieveMemory
|
||||
|
||||
__all__ = [
|
||||
"BaseMemoryTool",
|
||||
"HandsOffTool",
|
||||
"AddHistoryMemory",
|
||||
"ReadHistoryMemory",
|
||||
"ReadIdentityMemory",
|
||||
"UpdateIdentityMemory",
|
||||
"AddMetaMemory",
|
||||
"ReadMetaMemory",
|
||||
"ThinkTool",
|
||||
"AddMemory",
|
||||
"AddSummaryMemory",
|
||||
"DeleteMemory",
|
||||
"RetrieveRecentMemory",
|
||||
"UpdateMemory",
|
||||
"VectorRetrieveMemory",
|
||||
]
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
"""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",
|
||||
]
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
"""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."""
|
||||
self.output = f"Successfully created memory draft(s). These drafts are not yet persisted to the vector store."
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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.**
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
"""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}")
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
tool: |
|
||||
Read original history dialogue by reference memory ID.
|
||||
|
||||
ref_memory_id: |
|
||||
Reference memory ID to query the original history dialogue.
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
"""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")
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
"""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)
|
||||
|
||||
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"
|
||||
)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
"""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, Message
|
||||
|
||||
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)]
|
||||
self.messages: list[Message] = []
|
||||
|
||||
@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)
|
||||
|
||||
if agent.messages:
|
||||
self.messages.extend(agent.messages)
|
||||
|
||||
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}"
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
"""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]
|
||||
# Deduplicate memory IDs to avoid redundant deletions
|
||||
memory_ids_to_delete = list(dict.fromkeys(memory_ids_to_delete))
|
||||
|
||||
# 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. Operation has been done."
|
||||
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 = "Operation has been done."
|
||||
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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.
|
||||
**IMPORTANT**: Do NOT add duplicate memory_ids. Each memory_id should appear only once in the list.
|
||||
|
||||
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.**
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from .add_memory import AddMemory
|
||||
from .read_history import ReadHistory
|
||||
from .read_user_profile import ReadUserProfile
|
||||
from .retrieve_memory import RetrieveMemory
|
||||
from .summary_and_hands_off import SummaryAndHandsOff
|
||||
from .update_user_profile import UpdateUserProfile
|
||||
|
||||
__all__ = [
|
||||
"AddMemory",
|
||||
"ReadHistory",
|
||||
"ReadUserProfile",
|
||||
"RetrieveMemory",
|
||||
"SummaryAndHandsOff",
|
||||
"UpdateUserProfile",
|
||||
]
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
class AddMemory(BaseMemoryTool):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['enable_multiple'] = True
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Add multiple memories to the vector store for future retrieval."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"description": "A list of memory objects to store.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_content": {
|
||||
"type": "string",
|
||||
"description": "memory content",
|
||||
},
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation time, e.g. '2020-01-01 00:00:00'",
|
||||
}
|
||||
},
|
||||
"required": ["memory_content", "conversation_time"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memories"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
if not memories:
|
||||
self.output = "No memories provided for addition."
|
||||
return
|
||||
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for mem in memories:
|
||||
memory_content = mem.get("memory_content", "")
|
||||
conversation_time = mem.get("conversation_time", "")
|
||||
metadata: dict = {"conversation_time": conversation_time}
|
||||
try:
|
||||
metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", ""))
|
||||
except Exception:
|
||||
...
|
||||
memory_nodes.append(self._build_memory_node(memory_content, metadata=metadata))
|
||||
|
||||
vector_nodes = [node.to_vector_node() for node in memory_nodes]
|
||||
vector_ids: list[str] = [node.vector_id for node in vector_nodes]
|
||||
|
||||
await self.vector_store.delete(vector_ids=vector_ids)
|
||||
await self.vector_store.insert(nodes=vector_nodes)
|
||||
self.memory_nodes = memory_nodes
|
||||
|
||||
self.output = f"Successfully added {len(memory_nodes)} memories to vector_store."
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
class ReadHistory(BaseMemoryTool):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["enable_multiple"] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Read original history dialogue."
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"history_id": {
|
||||
"type": "string",
|
||||
"description": "history_id",
|
||||
},
|
||||
},
|
||||
"required": ["history_id"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
history_id = self.context.get("history_id", "")
|
||||
nodes = await self.vector_store.get(vector_ids=[history_id])
|
||||
|
||||
if not nodes:
|
||||
self.output = f"No history: {history_id}"
|
||||
logger.warning(self.output)
|
||||
return
|
||||
|
||||
memory = MemoryNode.from_vector_node(nodes[0])
|
||||
self.output = memory.content
|
||||
logger.info(f"Successfully read history memory: {history_id}")
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema.memory_node import MemoryNode
|
||||
|
||||
|
||||
class ReadUserProfile(BaseMemoryTool):
|
||||
|
||||
def __init__(self, add_memory_type_target: bool = True, **kwargs):
|
||||
kwargs["enable_multiple"] = False
|
||||
self.add_memory_type_target = add_memory_type_target
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Read personal memory profile for the current user."
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
if self.add_memory_type_target:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": "memory_type",
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": "memory_target",
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target"],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
cache_key = f"{self.memory_type}_{self.memory_target}"
|
||||
cached_data = self.meta_memory.load(cache_key, auto_clean=False)
|
||||
|
||||
if not cached_data:
|
||||
self.output = f"Local memory not found: {self.memory_type}_{self.memory_target}"
|
||||
logger.info(self.output)
|
||||
return
|
||||
|
||||
# Convert to MemoryNode objects and sort by conversation_time (oldest first)
|
||||
memory_nodes = [MemoryNode(**node_data) for node_data in cached_data]
|
||||
memory_nodes.sort(
|
||||
key=lambda n: n.metadata.get("conversation_time", "")
|
||||
)
|
||||
|
||||
memory_formated = []
|
||||
for node in memory_nodes:
|
||||
node_formated = f"profile_id={node.memory_id} profile_content={node.content}"
|
||||
if "conversation_time" in node.metadata:
|
||||
node_formated += f" conversation_time={node.metadata['conversation_time']}"
|
||||
if node.ref_memory_id:
|
||||
node_formated += f" history_id={node.ref_memory_id}"
|
||||
memory_formated.append(node_formated.strip())
|
||||
|
||||
self.output = "\n".join(memory_formated)
|
||||
logger.info(f"Read {len(memory_formated)} nodes from cache key: {cache_key}")
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema import MemoryNode
|
||||
from ...core.utils import deduplicate_memories
|
||||
|
||||
|
||||
class RetrieveMemory(BaseMemoryTool):
|
||||
|
||||
def __init__(self, top_k: int = 20, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.top_k: int = top_k
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Retrieve memories using vector similarity search."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_items": {
|
||||
"type": "array",
|
||||
"description": "query_items",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": "memory_type",
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": "memory_target",
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "query",
|
||||
},
|
||||
"time_range": {
|
||||
"type": "string",
|
||||
"description": "time_range(optional), e.g. [20200101, 20200101]",
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target", "query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
query_items: list[dict] = self.context.get("query_items", [])
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for query_item in query_items:
|
||||
memory_type = query_item.get("memory_type")
|
||||
memory_target = query_item.get("memory_target")
|
||||
query = query_item.get("query")
|
||||
time_range = query_item.get("time_range", "")
|
||||
|
||||
filter_dict = {
|
||||
"memory_type": memory_type,
|
||||
"memory_target": memory_target,
|
||||
}
|
||||
|
||||
if time_range:
|
||||
time_range = json.loads(time_range)
|
||||
filter_dict["time_range"] = [int(time_range[0]), int(time_range[1])]
|
||||
|
||||
nodes = await self.vector_store.search(query=query, limit=self.top_k, filters=filter_dict)
|
||||
memory_nodes.extend([MemoryNode.from_vector_node(n) for n in nodes])
|
||||
memory_nodes = deduplicate_memories(memory_nodes)
|
||||
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
self.output = "No new memory_nodes found matching the query (duplicates removed)."
|
||||
else:
|
||||
self.output = "\n".join([f"{m.metadata['conversation_time']} {m.content}" for m in new_memory_nodes])
|
||||
logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication")
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.schema import MemoryNode, Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...mem_agent import BaseMemoryAgent
|
||||
|
||||
|
||||
class SummaryAndHandsOff(BaseMemoryTool):
|
||||
def __init__(self, memory_agents: list["BaseMemoryAgent"], **kwargs):
|
||||
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)]
|
||||
self.messages: list[Message] = []
|
||||
|
||||
@property
|
||||
def memory_agent_dict(self) -> dict[MemoryType, "BaseMemoryAgent"]:
|
||||
return {a.memory_type: a for a in self.sub_ops}
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Summarize and distribute memory tasks to appropriate agents."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary_content": {
|
||||
"type": "string",
|
||||
"description": "summary content",
|
||||
},
|
||||
"memory_tasks": {
|
||||
"type": "array",
|
||||
"description": "memory_tasks",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": "memory_type",
|
||||
"enum": [k.value for k in self.memory_agent_dict],
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": "memory_target",
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["summary_content", "memory_tasks"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_memory_type_target(task: dict):
|
||||
return {
|
||||
"memory_type": MemoryType(task.get("memory_type", "")),
|
||||
"memory_target": task.get("memory_target", ""),
|
||||
}
|
||||
|
||||
def _collect_tasks(self) -> list[dict]:
|
||||
tasks = []
|
||||
for task in self.context.get("memory_tasks", []):
|
||||
tasks.append(self._parse_memory_type_target(task))
|
||||
return tasks
|
||||
|
||||
async def execute(self):
|
||||
summary_content = self.context.get("summary_content", "")
|
||||
assert summary_content, "No summary content provided."
|
||||
|
||||
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])
|
||||
|
||||
tasks = self._collect_tasks()
|
||||
if not tasks:
|
||||
self.output = "No valid memory tasks to execute."
|
||||
return
|
||||
|
||||
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 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()
|
||||
|
||||
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)
|
||||
if agent.messages:
|
||||
self.messages.extend(agent.messages)
|
||||
|
||||
results.append({
|
||||
"memory_type": memory_type.value,
|
||||
"memory_target": memory_target,
|
||||
"result": result_str[:100] + ("..." if len(result_str) > 100 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}"
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema.memory_node import MemoryNode
|
||||
|
||||
|
||||
class UpdateUserProfile(BaseMemoryTool):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["enable_multiple"] = True
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Update user profile."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile_ids_to_delete": {
|
||||
"type": "array",
|
||||
"description": "profile_ids_to_delete",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"profiles_to_add": {
|
||||
"type": "array",
|
||||
"description": "profiles_to_add",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile_content": {
|
||||
"type": "string",
|
||||
"description": "profile_content",
|
||||
},
|
||||
"conversation_time": {
|
||||
"type": "string",
|
||||
"description": "conversation_time, e.g. '2020-01-01 00:00:00'",
|
||||
},
|
||||
},
|
||||
"required": ["profile_content", "conversation_time"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["profile_ids_to_delete", "profiles_to_add"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
profile_ids_to_delete = self.context.get("profile_ids_to_delete", [])
|
||||
profile_ids_to_delete = [m for m in profile_ids_to_delete if m]
|
||||
profile_ids_to_delete = list(dict.fromkeys(profile_ids_to_delete))
|
||||
profiles_to_add = self.context.get("profiles_to_add", [])
|
||||
|
||||
if not profile_ids_to_delete and not profiles_to_add:
|
||||
self.output = "No memories to remove or add. Operation has been done."
|
||||
return
|
||||
|
||||
cache_key = f"{self.memory_type}_{self.memory_target}"
|
||||
cached_data = self.meta_memory.load(cache_key, auto_clean=False)
|
||||
existing_memory_nodes = []
|
||||
if cached_data:
|
||||
existing_memory_nodes = [MemoryNode(**node_data) for node_data in cached_data]
|
||||
|
||||
removed_count = 0
|
||||
added_count = 0
|
||||
|
||||
if profile_ids_to_delete:
|
||||
profile_ids_set = set(profile_ids_to_delete)
|
||||
existing_memory_nodes = [node for node in existing_memory_nodes if node.memory_id not in profile_ids_set]
|
||||
removed_count = len(profile_ids_to_delete)
|
||||
logger.info(f"Removed {removed_count} memories from user profile.")
|
||||
|
||||
new_memory_nodes = []
|
||||
if profiles_to_add:
|
||||
for mem in profiles_to_add:
|
||||
profile_content = mem.get("profile_content", "")
|
||||
conversation_time = mem.get("conversation_time", "")
|
||||
new_memory_nodes.append(self._build_memory_node(
|
||||
memory_content=profile_content,
|
||||
when_to_use="",
|
||||
metadata={"conversation_time": conversation_time}
|
||||
))
|
||||
logger.info(f"Added {len(new_memory_nodes)} new memories to user profile.")
|
||||
|
||||
updated_memory_nodes = existing_memory_nodes + new_memory_nodes
|
||||
|
||||
nodes_data = [node.model_dump(exclude_none=True) for node in updated_memory_nodes]
|
||||
self.meta_memory.save(cache_key, nodes_data)
|
||||
|
||||
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 user profile."
|
||||
else:
|
||||
self.output = "Operation has been done."
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from .add_summary_memory import AddSummaryMemory
|
||||
from .hands_off import HandsOff
|
||||
from .read_history import ReadHistory
|
||||
from .read_user_profile import ReadUserProfile
|
||||
from .retrieve_memory import RetrieveMemory
|
||||
from .update_user_profile import UpdateUserProfile
|
||||
|
||||
__all__ = [
|
||||
"AddSummaryMemory",
|
||||
"HandsOff",
|
||||
"ReadHistory",
|
||||
"ReadUserProfile",
|
||||
"RetrieveMemory",
|
||||
"UpdateUserProfile",
|
||||
]
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
class AddSummaryMemory(BaseMemoryTool):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["enable_multiple"] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Add a summary memory to the vector store for future retrieval."
|
||||
|
||||
@staticmethod
|
||||
def _build_item_schema() -> tuple[dict, list[str]]:
|
||||
properties = {
|
||||
"conversation_time": {"type": "string", "description": "conversation time, e.g. '2020-01-01 00:00:00'"},
|
||||
"summary_memory": {"type": "string", "description": "summary_memory"},
|
||||
}
|
||||
return properties, ["conversation_time", "summary_memory"]
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
properties, required = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
summary_memory = self.context.get("summary_memory", "")
|
||||
conversation_time = self.context.get("conversation_time", "")
|
||||
|
||||
if not summary_memory:
|
||||
self.output = "No summary_memory provided for addition."
|
||||
return
|
||||
|
||||
metadata: dict = {"conversation_time": conversation_time}
|
||||
try:
|
||||
metadata["time_int"] = int(conversation_time.split(" ")[0].replace("-", ""))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
memory_node = MemoryNode(
|
||||
memory_type=self.memory_type,
|
||||
memory_target=self.memory_target,
|
||||
when_to_use="",
|
||||
content=summary_memory,
|
||||
ref_memory_id=self.history_node.memory_id,
|
||||
author=self.author,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
vector_node = memory_node.to_vector_node()
|
||||
vector_id = vector_node.vector_id
|
||||
await self.vector_store.delete(vector_ids=[vector_id])
|
||||
await self.vector_store.insert(nodes=[vector_node])
|
||||
self.memory_nodes.append(memory_node)
|
||||
|
||||
self.output = f"Successfully added summary memory to vector_store."
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.schema import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...mem_agent import BaseMemoryAgent
|
||||
|
||||
|
||||
class HandsOff(BaseMemoryTool):
|
||||
|
||||
def __init__(self, memory_agents: list["BaseMemoryAgent"], **kwargs):
|
||||
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)]
|
||||
self.messages: list[Message] = []
|
||||
self.meta_info_dict: dict[str, str] = {}
|
||||
|
||||
@property
|
||||
def memory_agent_dict(self) -> dict[MemoryType, "BaseMemoryAgent"]:
|
||||
return {a.memory_type: a for a in self.sub_ops}
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Distribute memory tasks to appropriate memory agents."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_tasks": {
|
||||
"type": "array",
|
||||
"description": "List of memory tasks to distribute to specific agents",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": "Type of memory to handle",
|
||||
"enum": [k.value for k in self.memory_agent_dict],
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": "Target or context for the memory operation",
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memory_tasks"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
tasks = []
|
||||
seen = set()
|
||||
for task in self.context.get("memory_tasks", []):
|
||||
memory_type = MemoryType(task.get("memory_type", ""))
|
||||
memory_target = task.get("memory_target", "")
|
||||
|
||||
# Deduplicate tasks with same memory_type and memory_target
|
||||
task_key = (memory_type, memory_target)
|
||||
if task_key in seen:
|
||||
logger.info(f"Skipping duplicate task: memory_type={memory_type.value}, memory_target={memory_target}")
|
||||
continue
|
||||
seen.add(task_key)
|
||||
|
||||
tasks.append({
|
||||
"memory_type": memory_type,
|
||||
"memory_target": memory_target,
|
||||
})
|
||||
|
||||
if not tasks:
|
||||
self.output = "No valid memory tasks to execute."
|
||||
return
|
||||
|
||||
agent_list = []
|
||||
for i, task in enumerate(tasks):
|
||||
memory_type: MemoryType = task["memory_type"]
|
||||
memory_target: str = task["memory_target"]
|
||||
|
||||
agent = self.memory_agent_dict[memory_type].copy()
|
||||
agent_list.append([agent, memory_type, memory_target])
|
||||
|
||||
logger.info(f"Task {i}: Submitting {memory_type.value} agent for target={memory_target}")
|
||||
self.submit_async_task(
|
||||
agent.call,
|
||||
memory_type=memory_type,
|
||||
memory_target=memory_target,
|
||||
query=self.context.get("query", ""),
|
||||
messages=self.context.get("messages", []),
|
||||
description=self.context.get("description"),
|
||||
history_node=self.context.get("history_node"),
|
||||
)
|
||||
|
||||
await self.join_async_tasks()
|
||||
|
||||
results = []
|
||||
for i, (agent, memory_type, memory_target) in enumerate(agent_list):
|
||||
if agent.memory_nodes:
|
||||
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}")
|
||||
|
||||
self.output = "\n".join(results)
|
||||
logger.info(f"Completed {len(results)} hands-off task(s):\n{self.output}")
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.schema import MemoryNode
|
||||
from ...core.utils import deduplicate_memories
|
||||
|
||||
|
||||
class RetrieveMemory(BaseMemoryTool):
|
||||
|
||||
def __init__(self, top_k: int = 20, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.top_k: int = top_k
|
||||
|
||||
def _build_tool_description(self) -> str:
|
||||
return "Retrieve memories using vector similarity search."
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_items": {
|
||||
"type": "array",
|
||||
"description": "query_items",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "query",
|
||||
},
|
||||
"time_range": {
|
||||
"type": "string",
|
||||
"description": "time_range(optional), e.g. [20200101, 20200101]",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
query_items: list[dict] = self.context.get("query_items", [])
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for query_item in query_items:
|
||||
query = query_item.get("query")
|
||||
time_range = query_item.get("time_range", "")
|
||||
|
||||
filter_dict: dict = {
|
||||
"memory_type": self.memory_type.value,
|
||||
"memory_target": self.memory_target,
|
||||
}
|
||||
|
||||
if time_range:
|
||||
# Handle different time_range formats
|
||||
if isinstance(time_range, str):
|
||||
try:
|
||||
time_range = json.loads(time_range)
|
||||
except json.JSONDecodeError:
|
||||
# If it's a plain string like "20250907", treat it as a single date
|
||||
time_range = time_range
|
||||
|
||||
# Convert to list format [start, end]
|
||||
if isinstance(time_range, (list, tuple)):
|
||||
if len(time_range) == 1:
|
||||
# Single element list, use it for both start and end
|
||||
filter_dict["time_int"] = [int(time_range[0]), int(time_range[0])]
|
||||
else:
|
||||
# Two element list/tuple
|
||||
filter_dict["time_int"] = [int(time_range[0]), int(time_range[1])]
|
||||
else:
|
||||
# Single value (int or string), use it for both start and end
|
||||
filter_dict["time_int"] = [int(time_range), int(time_range)]
|
||||
logger.info(f"memory_type={self.memory_type} memory_target={self.memory_target} query={query} "
|
||||
f"filter_dict={filter_dict}")
|
||||
|
||||
nodes = await self.vector_store.search(query=query, limit=self.top_k, filters=filter_dict)
|
||||
memory_nodes.extend([MemoryNode.from_vector_node(n) for n in nodes])
|
||||
memory_nodes = deduplicate_memories(memory_nodes)
|
||||
|
||||
retrieved_memory_ids = {node.memory_id for node in self.retrieved_nodes if node.memory_id}
|
||||
new_memory_nodes = [node for node in memory_nodes if node.memory_id not in retrieved_memory_ids]
|
||||
self.retrieved_nodes.extend(new_memory_nodes)
|
||||
self.memory_nodes = new_memory_nodes
|
||||
|
||||
if not new_memory_nodes:
|
||||
self.output = "No new memory_nodes found matching the query (duplicates removed)."
|
||||
else:
|
||||
output = []
|
||||
for node in new_memory_nodes:
|
||||
line = ""
|
||||
if "conversation_time" in node.metadata and node.metadata["conversation_time"]:
|
||||
line += f"conversation_time={node.metadata['conversation_time']} "
|
||||
line += node.content.strip() + " "
|
||||
if node.ref_memory_id:
|
||||
line += f"history_id={node.ref_memory_id} "
|
||||
output.append(line.strip())
|
||||
self.output = "### Extracted Memories\n" + "\n".join(output)
|
||||
|
||||
logger.info(f"Retrieved {len(memory_nodes)} memory_nodes, {len(new_memory_nodes)} new after deduplication")
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
"""Add memory 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 AddMemory(BaseMemoryTool):
|
||||
"""Add memories to vector store with optional when_to_use and custom metadata fields.
|
||||
|
||||
Supports single/multiple addition modes via `enable_multiple` parameter.
|
||||
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 AddMemory.
|
||||
|
||||
Args:
|
||||
add_when_to_use: Include when_to_use field for better retrieval.
|
||||
metadata_desc: Dictionary defining metadata fields and their descriptions.
|
||||
Example:
|
||||
{
|
||||
"year": "The `year` information associated with the memory(Optional)",
|
||||
"month": "The `month` information associated with the memory(Optional)",
|
||||
"day": "The `day` information associated with the memory(Optional)",
|
||||
"hour": "The `hour` information associated with the memory(Optional)",
|
||||
}
|
||||
If None or empty dict, metadata field will not be included.
|
||||
**kwargs: Additional arguments for BaseMemoryTool.
|
||||
"""
|
||||
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.
|
||||
|
||||
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()
|
||||
}
|
||||
# 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 for the memory. Available fields:\n{field_descriptions}"
|
||||
|
||||
properties["metadata"] = {
|
||||
"type": "object",
|
||||
"description": metadata_description,
|
||||
"properties": metadata_properties,
|
||||
}
|
||||
required.append("metadata")
|
||||
|
||||
return properties, required
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
"""Build input schema for single memory addition."""
|
||||
properties, required = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
}
|
||||
|
||||
def _build_multiple_parameters(self) -> dict:
|
||||
"""Build input schema for multiple memory addition."""
|
||||
item_properties, required_fields = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memories"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": required_fields,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memories"],
|
||||
}
|
||||
|
||||
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 ""
|
||||
# Only extract metadata if metadata_desc is configured
|
||||
# Convert all metadata values to strings
|
||||
metadata = {}
|
||||
if self.metadata_desc:
|
||||
raw_metadata = mem_dict.get("metadata", {})
|
||||
metadata = {key: str(value).strip() for key, value in raw_metadata.items() if value}
|
||||
return memory_content, when_to_use, metadata
|
||||
|
||||
async def execute(self):
|
||||
"""Execute addition: delete existing IDs (upsert), then insert new memories."""
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
|
||||
if self.enable_multiple:
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
if not memories:
|
||||
self.output = "No memories provided for addition."
|
||||
return
|
||||
|
||||
for mem in memories:
|
||||
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))
|
||||
|
||||
else:
|
||||
memory_content, when_to_use, metadata = self._extract_memory_data(self.context)
|
||||
if not memory_content:
|
||||
self.output = "No memory content provided for addition."
|
||||
return
|
||||
|
||||
memory_nodes.append(self._build_memory_node(memory_content, when_to_use=when_to_use, metadata=metadata))
|
||||
|
||||
if not memory_nodes:
|
||||
self.output = "No valid memories provided for addition."
|
||||
return
|
||||
|
||||
# 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)
|
||||
self.memory_nodes = memory_nodes
|
||||
|
||||
self.output = f"Successfully added {len(memory_nodes)} memories to vector_store."
|
||||
logger.info(self.output)
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
tool: |
|
||||
Add a memory to the vector store for future retrieval.
|
||||
Use this tool to store important information that should be remembered, such as:
|
||||
- Meta information: "I am very happy"
|
||||
- Personal preferences: "John prefers dark mode", "Alice works in PST timezone"
|
||||
- Procedural knowledge: "To deploy, run build then push", "Always validate input before processing"
|
||||
|
||||
**CRITICAL**: Only add memories based on explicitly stated facts. DO NOT store inferred, assumed, or fabricated information.
|
||||
|
||||
tool_multiple: |
|
||||
Add multiple memories to the vector store for future retrieval.
|
||||
Use this tool to store multiple pieces of important information in a single operation.
|
||||
Each memory can include when_to_use conditions and metadata for better organization and retrieval.
|
||||
Examples: storing multiple user preferences, multiple procedural steps, or multiple tool usage tips.
|
||||
|
||||
**CRITICAL**: Only add memories based on explicitly stated facts. DO NOT store inferred, assumed, or fabricated information in any of the memory entries.
|
||||
|
||||
when_to_use: |
|
||||
Optional condition description for 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.**
|
||||
|
||||
memories: |
|
||||
A list of memory objects to store.
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
"""Add summary memory operation for vector store."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .add_memory import AddMemory
|
||||
from ...core.context import C
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class AddSummaryMemory(AddMemory):
|
||||
"""Add LLM-summarized memories to vector store.
|
||||
|
||||
Differences from AddMemory:
|
||||
- Single memory mode only (enable_multiple=False)
|
||||
- Uses 'summary_memory' parameter instead of 'memory_content'
|
||||
- No when_to_use field (add_when_to_use=False)
|
||||
- Metadata fields can be customized via `metadata_desc` parameter
|
||||
"""
|
||||
|
||||
def __init__(self, metadata_desc: dict[str, str] | None = None, **kwargs):
|
||||
"""Initialize AddSummaryMemory.
|
||||
|
||||
Args:
|
||||
metadata_desc: Dictionary defining metadata fields and their descriptions.
|
||||
**kwargs: Additional arguments for AddMemory.
|
||||
"""
|
||||
# Force single mode and disable when_to_use
|
||||
kwargs["enable_multiple"] = False
|
||||
kwargs["add_when_to_use"] = False
|
||||
super().__init__(metadata_desc=metadata_desc, **kwargs)
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
"""Build input schema for summary memory addition."""
|
||||
properties = {
|
||||
"summary_memory": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("summary_memory"),
|
||||
},
|
||||
}
|
||||
required = ["summary_memory"]
|
||||
|
||||
# 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()
|
||||
}
|
||||
# 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 for the memory. Available fields:\n{field_descriptions}"
|
||||
|
||||
properties["metadata"] = {
|
||||
"type": "object",
|
||||
"description": metadata_description,
|
||||
"properties": metadata_properties,
|
||||
}
|
||||
required.append("metadata")
|
||||
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
}
|
||||
|
||||
def _build_memory_node(
|
||||
self,
|
||||
memory_content: str,
|
||||
memory_type: MemoryType | None = None,
|
||||
memory_target: str = "",
|
||||
ref_memory_id: str = "",
|
||||
when_to_use: str = "",
|
||||
author: str = "",
|
||||
metadata: dict | None = None,
|
||||
) -> MemoryNode:
|
||||
"""Build MemoryNode from content, when_to_use, and metadata."""
|
||||
node = MemoryNode(
|
||||
memory_type=MemoryType.HISTORY,
|
||||
memory_target="",
|
||||
when_to_use=memory_content,
|
||||
content=self.messages_formated,
|
||||
ref_memory_id="",
|
||||
author=self.author,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
logger.info(f"Adding summary memory: {node.model_dump_json(indent=2, exclude_none=True)}")
|
||||
|
||||
return node
|
||||
|
||||
async def execute(self):
|
||||
"""Execute addition: map summary_memory to memory_content and call parent."""
|
||||
# Map summary_memory to memory_content
|
||||
summary_memory = self.context.get("summary_memory", "")
|
||||
if not summary_memory:
|
||||
self.output = "No summary memory content provided for addition."
|
||||
logger.warning(self.output)
|
||||
return
|
||||
|
||||
self.context["memory_content"] = summary_memory
|
||||
await super().execute()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue