mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-10 03:30:56 +00:00
commit
b02d3ab269
146 changed files with 1675 additions and 7309 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,6 +34,7 @@ test_compact_storage/*
|
|||
test_working_memory/*
|
||||
*.code-workspace
|
||||
local_vector_store/*
|
||||
reme_local_memory/*
|
||||
chroma_vector_store/*
|
||||
bench_results/*
|
||||
meta_memory/*
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from . import config
|
|||
from . import core
|
||||
from . import tool
|
||||
from . import workflow
|
||||
from .reme_app import ReMeApp
|
||||
from .reme import ReMe
|
||||
|
||||
__all__ = [
|
||||
"agent",
|
||||
|
|
@ -13,7 +13,7 @@ __all__ = [
|
|||
"core",
|
||||
"tool",
|
||||
"workflow",
|
||||
"ReMeApp",
|
||||
"ReMe",
|
||||
]
|
||||
|
||||
__version__ = "0.3.0.0a1"
|
||||
|
|
|
|||
9
reme/agent/memory/__init__.py
Normal file
9
reme/agent/memory/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""memory agent"""
|
||||
|
||||
from . import default
|
||||
from .base_memory_agent import BaseMemoryAgent
|
||||
|
||||
__all__ = [
|
||||
"default",
|
||||
"BaseMemoryAgent",
|
||||
]
|
||||
75
reme/agent/memory/base_memory_agent.py
Normal file
75
reme/agent/memory/base_memory_agent.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""Base memory agent for handling memory operations with tool-based reasoning."""
|
||||
|
||||
from abc import ABCMeta
|
||||
from typing import Literal
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ...core.enumeration import MemoryType
|
||||
from ...core.op import BaseReact
|
||||
from ...core.schema import MemoryNode
|
||||
|
||||
|
||||
class BaseMemoryAgent(BaseReact, metaclass=ABCMeta):
|
||||
"""Base class for memory agents that handle memory operations with tool-based reasoning."""
|
||||
|
||||
memory_type: MemoryType | None = None
|
||||
|
||||
@staticmethod
|
||||
async def read_meta_memories(meta_memories: list[dict]) -> str:
|
||||
"""Read and format meta memory information from the provided metadata list."""
|
||||
from ...tool.memory import ReadMetaMemory
|
||||
|
||||
meta_memory_info = ReadMetaMemory().format_memory_metadata(meta_memories)
|
||||
logger.info(f"meta_memory_info={meta_memory_info}")
|
||||
return meta_memory_info
|
||||
|
||||
async def read_user_profile(self, show_id: Literal["profile", "history"] = "profile") -> str:
|
||||
"""Read current user profile."""
|
||||
from ...tool.memory import ReadUserProfile
|
||||
|
||||
read_tool = ReadUserProfile(show_id=show_id)
|
||||
await read_tool.call(memory_target=self.memory_target, service_context=self.service_context)
|
||||
return str(read_tool.response.answer)
|
||||
|
||||
async def add_history_node(self) -> MemoryNode:
|
||||
"""Add history node"""
|
||||
from ...tool.memory import AddHistory
|
||||
|
||||
add_history_tool = AddHistory()
|
||||
await add_history_tool.call(
|
||||
messages=self.messages,
|
||||
description=self.description,
|
||||
service_context=self.service_context,
|
||||
)
|
||||
return add_history_tool.context.history_node
|
||||
|
||||
@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
|
||||
13
reme/agent/memory/default/__init__.py
Normal file
13
reme/agent/memory/default/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Default memory agents for personal and ReMe memory operations."""
|
||||
|
||||
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",
|
||||
]
|
||||
61
reme/agent/memory/default/personal_retriever.py
Normal file
61
reme/agent/memory/default/personal_retriever.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Personal memory retriever agent for retrieving personal memories through vector search."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ....core.enumeration import Role, MemoryType
|
||||
from ....core.op import BaseTool
|
||||
from ....core.schema import Message, MemoryNode
|
||||
from ....core.utils import format_messages
|
||||
|
||||
|
||||
class PersonalRetriever(BaseMemoryAgent):
|
||||
"""Retrieve personal memories through vector search and history reading."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.retrieved_nodes: list[MemoryNode] = []
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
if self.context.get("query"):
|
||||
context = self.context.query
|
||||
elif self.context.get("messages"):
|
||||
context = self.description + "\n" + format_messages(self.context.messages)
|
||||
else:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
|
||||
return [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
user_profile=await self.read_user_profile(show_id="history"),
|
||||
context=context.strip(),
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
tools: list[BaseTool],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
"""Execute tool calls with memory context."""
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
tools,
|
||||
step,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
retrieved_nodes=self.retrieved_nodes,
|
||||
**kwargs,
|
||||
)
|
||||
48
reme/agent/memory/default/personal_retriever.yaml
Normal file
48
reme/agent/memory/default/personal_retriever.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
system_prompt: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## User Profile
|
||||
{user_profile}
|
||||
|
||||
## Question
|
||||
{context}
|
||||
|
||||
## Retrieval Strategy
|
||||
|
||||
**Tool 1: Vector Search (`retrieve_memory`)**
|
||||
- Purpose: Search for relevant memories using semantic similarity
|
||||
- Try at least 3-5 different queries before moving to next tool:
|
||||
* Direct question reformulation
|
||||
* Different phrasings and perspectives
|
||||
* Entity-focused queries (names, places, events)
|
||||
* Various keyword combinations
|
||||
- Time range filtering (optional):
|
||||
* Format: single date '20200101' or range '20200101,20200102'
|
||||
* Example: '20200101,20200102' for 20200101 <= time <= 20200102
|
||||
* Single-sided: '0,20200102' (before date) or '20200101,99999999' (after date)
|
||||
- If no results: retry with different time ranges or remove time constraints
|
||||
|
||||
**Tool 2: Read History (`read_history`) - ONLY AFTER Tool 1**
|
||||
- Purpose: Read full original conversation context
|
||||
- Use this ONLY after completing multiple retrieve_memory attempts
|
||||
- Extract history_id from retrieved memory results
|
||||
- Prioritize most relevant or recent history entries
|
||||
- Read multiple histories if needed for complete understanding
|
||||
|
||||
## Response Requirements
|
||||
- Answer ONLY based on retrieved memories and user profile - NO hallucination or inference
|
||||
- Always cite the source: reference specific memories with their timestamps
|
||||
- If information conflicts, present all versions with their respective times
|
||||
- Try multiple search angles before concluding no information exists
|
||||
|
||||
## Output Format
|
||||
When answering, structure your response as follows:
|
||||
|
||||
- [timestamp][Relevant memory content from search results]
|
||||
- [timestamp][Relevant user profile information]
|
||||
|
||||
If no relevant information found after thorough search (5+ queries), state:
|
||||
"No relevant information found after thorough search using multiple query strategies."
|
||||
|
||||
user_message: |
|
||||
Answer the question following the retrieval strategy and response requirements above.
|
||||
103
reme/agent/memory/default/personal_summarizer.py
Normal file
103
reme/agent/memory/default/personal_summarizer.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Personal memory summarizer agent for two-phase personal memory processing."""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ....core.enumeration import Role, MemoryType
|
||||
from ....core.op import BaseTool
|
||||
from ....core.schema import Message, MemoryNode
|
||||
|
||||
|
||||
class PersonalSummarizer(BaseMemoryAgent):
|
||||
"""Two-phase personal memory processor: retrieve/add memories then update profile."""
|
||||
|
||||
memory_type: MemoryType = MemoryType.PERSONAL
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.retrieved_nodes: list[MemoryNode] = []
|
||||
|
||||
async def _build_phase1_messages(self) -> list[Message]:
|
||||
"""Build messages for phase 1: retrieve and add memory."""
|
||||
return [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt_phase1",
|
||||
context=self.context.history_node.content,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message_phase1"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _build_phase2_messages(self) -> list[Message]:
|
||||
"""Build messages for phase 2: update user profile."""
|
||||
return [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt_phase2",
|
||||
context=self.context.history_node.content,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
user_profile=await self.read_user_profile(show_id="profile"),
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message_phase2"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
tools: list[BaseTool],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
"""Execute tool calls with memory context."""
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
tools,
|
||||
step,
|
||||
stage=stage,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
history_node=self.history_node,
|
||||
author=self.author,
|
||||
retrieved_nodes=self.retrieved_nodes,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Execute two-phase memory processing: retrieve/add -> update profile."""
|
||||
tools = self.tools
|
||||
for i, tool in enumerate(tools):
|
||||
logger.info(f"[{self.__class__.__name__}] tool_call[{i}]={tool.tool_call.simple_input_dump(as_dict=False)}")
|
||||
|
||||
messages_phase1 = await self._build_phase1_messages()
|
||||
for i, message in enumerate(messages_phase1):
|
||||
role = message.name or message.role
|
||||
logger.info(f"[{self.__class__.__name__}-S1] role={role} {message.simple_dump(as_dict=False)}")
|
||||
tools_phase1, messages_phase1, success_phase1 = await self.react(messages_phase1, tools[:-1], stage="S1")
|
||||
|
||||
messages_phase2 = await self._build_phase2_messages()
|
||||
for i, message in enumerate(messages_phase2):
|
||||
role = message.name or message.role
|
||||
logger.info(f"[{self.__class__.__name__}-S2] role={role} {message.simple_dump(as_dict=False)}")
|
||||
tools_phase2, messages_phase2, success_phase2 = await self.react(messages_phase2, tools[-1:], stage="S2")
|
||||
|
||||
return {
|
||||
"answer": (messages_phase1[-1].content if success_phase1 else "")
|
||||
+ (messages_phase2[-1].content if success_phase2 else ""),
|
||||
"success": success_phase1 and success_phase2,
|
||||
"messages": messages_phase1 + messages_phase2,
|
||||
"tools": tools_phase1 + tools_phase2,
|
||||
}
|
||||
45
reme/agent/memory/default/personal_summarizer.yaml
Normal file
45
reme/agent/memory/default/personal_summarizer.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
system_prompt_phase1: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Latest Conversation:
|
||||
Message format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
{context}
|
||||
|
||||
## Task: Retrieve Similar Memories and Add New Memories
|
||||
**CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate.
|
||||
|
||||
### Step 1: Retrieve Similar Memories
|
||||
Use `retrieve_memory` to search for existing similar memories about **{memory_target}**.
|
||||
- Use appropriate queries to find relevant existing memories
|
||||
- Check if new information already exists in the memory store
|
||||
|
||||
### Step 2: Add New Memories
|
||||
Use `add_memory` to add new memories:
|
||||
- Extract and summarize important information about **{memory_target}**
|
||||
- Set `conversation_time` (format: 2020-01-01 00:00:00; use 0000-00-00 00:00:00 if unavailable)
|
||||
- If the information is completely identical to existing memory, skip adding
|
||||
|
||||
user_message_phase1: |
|
||||
First retrieve similar memories, then extract and add new personal memories from the conversation.
|
||||
|
||||
system_prompt_phase2: |
|
||||
You are a memory agent managing **{memory_type}** memories about **{memory_target}**.
|
||||
|
||||
## Latest Conversation:
|
||||
Message format: `round<index> [<timestamp>] <role/name>: <content>` (timestamp: YYYY-MM-DD HH:MM:SS).
|
||||
{context}
|
||||
|
||||
## Current User Profile:
|
||||
UserProfile format: `profile_id=<id> conversation_time=<timestamp> <content>`.
|
||||
{user_profile}
|
||||
|
||||
## Task: Update Profile with `UpdateUserProfile`
|
||||
**CRITICAL**: Extract ONLY explicitly stated information. DO NOT infer, assume, or fabricate.
|
||||
|
||||
Synchronize profile/memories with new information from the conversation, including **{memory_target}**' current status:
|
||||
- `profile_ids_to_delete`: Remove outdated, conflicting, or redundant entries.
|
||||
- `profiles_to_add`: Add new profiles/memories with `conversation_time`, e.g. `YYYY-MM-DD HH:MM:SS`, {memory_target} did something.
|
||||
- Maintain profiles that are concise, mutually exclusive, and collectively comprehensive with no information loss.
|
||||
|
||||
user_message_phase2: |
|
||||
Update user profile using `UpdateUserProfile` based on the conversation and current profile.
|
||||
80
reme/agent/memory/default/reme_retriever.py
Normal file
80
reme/agent/memory/default/reme_retriever.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""ReMe retriever agent that orchestrates multiple memory agents to retrieve information."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ....core.enumeration import Role
|
||||
from ....core.op import BaseTool
|
||||
from ....core.schema import Message
|
||||
from ....core.utils import format_messages
|
||||
|
||||
|
||||
class ReMeRetriever(BaseMemoryAgent):
|
||||
"""Orchestrate multiple memory agents to retrieve information."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
if self.context.get("query"):
|
||||
context = self.context.query
|
||||
elif self.context.get("messages"):
|
||||
context = self.description + "\n" + format_messages(self.context.messages)
|
||||
else:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
|
||||
return [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self.read_meta_memories(self.meta_memories),
|
||||
context=context.strip(),
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message"),
|
||||
),
|
||||
]
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
tools: list[BaseTool],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
tools,
|
||||
step,
|
||||
description=self.description,
|
||||
messages=self.messages,
|
||||
query=self.query,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
result = await super().execute()
|
||||
tools: list[BaseTool] = result["tools"]
|
||||
hands_off_tool = tools[0]
|
||||
agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"]
|
||||
|
||||
answer = ""
|
||||
success = True
|
||||
messages = []
|
||||
tools = []
|
||||
for agent in agents:
|
||||
answer += "\n" + agent.response.answer
|
||||
success = success and agent.response.success
|
||||
messages += agent.response.metadata["messages"]
|
||||
tools += agent.response.metadata["tools"]
|
||||
|
||||
return {
|
||||
"answer": answer.strip(),
|
||||
"success": True,
|
||||
"messages": self.messages,
|
||||
"tools": tools,
|
||||
}
|
||||
23
reme/agent/memory/default/reme_retriever.yaml
Normal file
23
reme/agent/memory/default/reme_retriever.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
system_prompt: |
|
||||
You are a Memory Orchestrator responsible for routing memory retrieval tasks to specialized agents based on the user query.
|
||||
|
||||
# User Query
|
||||
{context}
|
||||
|
||||
## Available Memory Agents
|
||||
Each line indicates a specialized Memory Agent dedicated to storing and retrieving 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 retrieve information from specialized agents:
|
||||
1. Analyze the user query and identify which memory dimensions are relevant
|
||||
2. Specify `memory_type` and `memory_target` for each retrieval task
|
||||
- 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
|
||||
3. Multiple tasks can be specified to enable parallel retrieval from specialized agents
|
||||
|
||||
Note: If the retrieved information is insufficient to answer the query, respond: "nothing found after thorough search."
|
||||
|
||||
user_message: |
|
||||
Please analyze the user query and retrieve relevant information from the appropriate existing agents.
|
||||
76
reme/agent/memory/default/reme_summarizer.py
Normal file
76
reme/agent/memory/default/reme_summarizer.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"""ReMe summarizer agent that orchestrates multiple memory agents to summarize information."""
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ....core.enumeration import Role
|
||||
from ....core.op import BaseTool
|
||||
from ....core.schema import Message
|
||||
|
||||
|
||||
class ReMeSummarizer(BaseMemoryAgent):
|
||||
"""Orchestrates multiple memory agents to summarize and store information across different memory types."""
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
self.context.history_node = await self.add_history_node()
|
||||
|
||||
messages = [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self.read_meta_memories(self.meta_memories),
|
||||
context=self.context.history_node.content,
|
||||
),
|
||||
),
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.get_prompt("user_message"),
|
||||
),
|
||||
]
|
||||
|
||||
return messages
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
tools: list[BaseTool],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list[BaseTool], list[Message]]:
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
tools,
|
||||
step,
|
||||
description=self.description,
|
||||
messages=self.messages,
|
||||
history_node=self.history_node,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
result = await super().execute()
|
||||
tools: list[BaseTool] = result["tools"]
|
||||
hands_off_tool = tools[0]
|
||||
agents: list[BaseMemoryAgent] = hands_off_tool.response.metadata["agents"]
|
||||
|
||||
answer = ""
|
||||
success = True
|
||||
messages = []
|
||||
tools = []
|
||||
for agent in agents:
|
||||
answer += "\n" + agent.response.answer
|
||||
success = success and agent.response.success
|
||||
messages += agent.response.metadata["messages"]
|
||||
tools += agent.response.metadata["tools"]
|
||||
|
||||
return {
|
||||
"answer": answer.strip(),
|
||||
"success": True,
|
||||
"messages": self.messages,
|
||||
"tools": tools,
|
||||
}
|
||||
|
|
@ -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>`.
|
||||
|
|
@ -101,7 +101,6 @@ class PromptHandler(BaseContext):
|
|||
prompt_file_path = Path(prompt_file_path)
|
||||
|
||||
if not prompt_file_path.exists():
|
||||
logger.warning(f"Prompt file not found: {prompt_file_path}")
|
||||
return self
|
||||
|
||||
suffix = prompt_file_path.suffix.lower()
|
||||
|
|
@ -117,7 +116,6 @@ class PromptHandler(BaseContext):
|
|||
f"Unsupported file format: {suffix}. " f"Supported formats: .yaml, .yml, .json",
|
||||
)
|
||||
|
||||
logger.info(f"Loaded {len(prompt_dict or {})} prompts from {prompt_file_path}")
|
||||
self.load_prompt_dict(prompt_dict, overwrite=overwrite)
|
||||
|
||||
except (yaml.YAMLError, json.JSONDecodeError) as e:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from .base_op import BaseOp
|
||||
from .base_ray_op import BaseRayOp
|
||||
from .base_react import BaseReact
|
||||
from .base_tool import BaseTool
|
||||
from .mcp_tool import MCPTool
|
||||
from .parallel_op import ParallelOp
|
||||
|
|
@ -11,6 +12,7 @@ from ..context import R
|
|||
__all__ = [
|
||||
"BaseOp",
|
||||
"BaseRayOp",
|
||||
"BaseReact",
|
||||
"BaseTool",
|
||||
"MCPTool",
|
||||
"ParallelOp",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ 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):
|
||||
"""Capture initialization arguments for object cloning."""
|
||||
instance = super().__new__(cls)
|
||||
|
|
@ -52,7 +54,7 @@ class BaseOp(metaclass=ABCMeta):
|
|||
**kwargs,
|
||||
):
|
||||
"""Initialize operator configurations and internal state."""
|
||||
self.name = name or camel_to_snake(self.__class__.__name__)
|
||||
self.name = name or self.__alias_name__ or camel_to_snake(self.__class__.__name__)
|
||||
self.async_mode = async_mode
|
||||
self.language = language
|
||||
self.prompt = self._get_prompt_handler(prompt_name, prompt_path)
|
||||
|
|
@ -92,12 +94,12 @@ class BaseOp(metaclass=ABCMeta):
|
|||
|
||||
def _handle_failure(self, e: Exception, attempt: int) -> str | None:
|
||||
"""Log failures and handle final retry logic."""
|
||||
message = f"[{self.__class__.__name__}] {self.name} failed (attempt {attempt + 1}): {e}"
|
||||
message = f"[{self.__class__.__name__}] failed (attempt {attempt + 1}): {e}"
|
||||
if attempt == self.max_retries - 1:
|
||||
logger.exception(message)
|
||||
if self.raise_exception:
|
||||
raise e
|
||||
return f"{self.name} failed: {e}"
|
||||
return f"[{self.__class__.__name__}] failed: {e}"
|
||||
else:
|
||||
logger.warning(message)
|
||||
return None
|
||||
|
|
@ -176,7 +178,7 @@ class BaseOp(metaclass=ABCMeta):
|
|||
if k == "answer":
|
||||
self.response.answer = v
|
||||
elif k == "success":
|
||||
self.response.success = v.lower() == "true"
|
||||
self.response.success = v if isinstance(v, bool) else v.lower() == "true"
|
||||
else:
|
||||
self.response.metadata[k] = v
|
||||
else:
|
||||
|
|
@ -260,7 +262,7 @@ class BaseOp(metaclass=ABCMeta):
|
|||
if isinstance(result, list):
|
||||
results.extend(result)
|
||||
else:
|
||||
result.append(result)
|
||||
results.append(result)
|
||||
self._pending_tasks.clear()
|
||||
return results
|
||||
|
||||
|
|
|
|||
164
reme/core/op/base_react.py
Normal file
164
reme/core/op/base_react.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""Base memory agent for handling memory operations with tool-based reasoning."""
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
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,
|
||||
):
|
||||
"""Initialize ReAct agent with tools and execution parameters."""
|
||||
kwargs["sub_ops"] = tools or []
|
||||
super().__init__(**kwargs)
|
||||
# Filter only BaseTool instances from sub_ops
|
||||
from . import BaseTool
|
||||
|
||||
self.sub_ops: list[BaseTool] = [t for t in self.sub_ops if isinstance(t, BaseTool)]
|
||||
self.tool_call_interval: float = tool_call_interval
|
||||
self.max_steps: int = max_steps
|
||||
|
||||
@property
|
||||
def tools(self) -> list["BaseTool"]:
|
||||
"""Return available tools for the agent."""
|
||||
return self.sub_ops
|
||||
|
||||
async def build_messages(self) -> list[Message]:
|
||||
"""Build initial message list from 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 _reasoning_step(
|
||||
self,
|
||||
messages: list[Message],
|
||||
tools: list["BaseTool"],
|
||||
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 tools]
|
||||
|
||||
# Generate assistant response with potential tool calls
|
||||
assistant_message: Message = await self.llm.chat(messages=messages, tools=tool_calls, **kwargs)
|
||||
messages.append(assistant_message)
|
||||
assistant_content: str = assistant_message.simple_dump(as_dict=False)
|
||||
logger.info(f"[{self.__class__.__name__} {stage or ''} step{step + 1}] assistant={assistant_content}")
|
||||
|
||||
# Determine if tools should be called
|
||||
should_act = bool(assistant_message.tool_calls)
|
||||
return assistant_message, should_act
|
||||
|
||||
async def _acting_step(
|
||||
self,
|
||||
assistant_message: Message,
|
||||
tools: list["BaseTool"],
|
||||
step: int,
|
||||
stage: str = "",
|
||||
**kwargs,
|
||||
) -> tuple[list["BaseTool"], list[Message]]:
|
||||
"""Execute tool calls requested by the assistant and collect results."""
|
||||
tool_list: list["BaseTool"] = []
|
||||
tool_messages: list[Message] = []
|
||||
|
||||
if not assistant_message.tool_calls:
|
||||
return tool_list, tool_messages
|
||||
|
||||
# Create tool name to tool instance mapping
|
||||
tool_dict = {t.tool_call.name: t for t in tools}
|
||||
for j, tool_call in enumerate(assistant_message.tool_calls):
|
||||
prefix: str = f"[{self.__class__.__name__} {stage or ''} step{step + 1}.{j}]"
|
||||
if tool_call.name not in tool_dict:
|
||||
logger.warning(f"{prefix} unknown tool_call={tool_call.name}")
|
||||
continue
|
||||
|
||||
logger.info(f"{prefix} submit tool_calls={tool_call.simple_output_dump(as_dict=False)}")
|
||||
|
||||
# Create independent tool copy with unique ID
|
||||
tool_copy: BaseTool = tool_dict[tool_call.name].copy()
|
||||
tool_copy.tool_call.id = tool_call.id
|
||||
tool_list.append(tool_copy)
|
||||
|
||||
# Create isolated kwargs for each tool call to avoid parameter conflicts
|
||||
tool_kwargs = {**kwargs, **tool_call.argument_dict}
|
||||
self.submit_async_task(tool_copy.call, service_context=self.service_context, **tool_kwargs)
|
||||
if self.tool_call_interval > 0:
|
||||
await asyncio.sleep(self.tool_call_interval)
|
||||
|
||||
# Wait for all tool executions to complete
|
||||
await self.join_async_tasks()
|
||||
|
||||
# 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,
|
||||
),
|
||||
)
|
||||
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
|
||||
|
||||
async def react(self, messages: list[Message], tools: list["BaseTool"], stage: str = ""):
|
||||
"""Run ReAct loop alternating between reasoning and acting until completion."""
|
||||
success: bool = False
|
||||
used_tools: list[BaseTool] = []
|
||||
for step in range(self.max_steps):
|
||||
# Reasoning: LLM decides next action
|
||||
assistant_message, should_act = await self._reasoning_step(messages, tools, step=step, stage=stage)
|
||||
|
||||
if not should_act:
|
||||
# No tools requested, task complete
|
||||
success = True
|
||||
break
|
||||
|
||||
# Acting: execute tools and collect results
|
||||
t_tools, tool_messages = await self._acting_step(assistant_message, tools, step=step, stage=stage)
|
||||
used_tools.extend(t_tools)
|
||||
messages.extend(tool_messages)
|
||||
|
||||
return used_tools, messages, success
|
||||
|
||||
async def execute(self):
|
||||
"""Execute the ReAct agent and return final results."""
|
||||
# Log available tools
|
||||
for i, tool in enumerate(self.tools):
|
||||
logger.info(f"[{self.__class__.__name__}] tool_call[{i}]={tool.tool_call.simple_input_dump(as_dict=False)}")
|
||||
|
||||
# Build and log initial messages
|
||||
messages = await self.build_messages()
|
||||
for i, message in enumerate(messages):
|
||||
role = message.name or message.role
|
||||
logger.info(f"[{self.__class__.__name__}] role={role} {message.simple_dump(as_dict=False)}")
|
||||
|
||||
# Run ReAct loop
|
||||
t_tools, messages, success = await self.react(messages, self.tools)
|
||||
return {
|
||||
"answer": messages[-1].content if success else "",
|
||||
"success": success,
|
||||
"messages": messages,
|
||||
"tools": t_tools,
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
"""MCP (Model Context Protocol) tool integration for remote tool execution."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from .base_tool import BaseTool
|
||||
|
|
@ -16,9 +14,9 @@ class MCPTool(BaseTool):
|
|||
self,
|
||||
mcp_server: str = "",
|
||||
tool_name: str = "",
|
||||
parameter_required: List[str] | None = None,
|
||||
parameter_optional: List[str] | None = None,
|
||||
parameter_deleted: List[str] | None = None,
|
||||
parameter_required: list[str] | None = None,
|
||||
parameter_optional: list[str] | None = None,
|
||||
parameter_deleted: list[str] | None = None,
|
||||
max_retries: int = 3,
|
||||
timeout: float | None = None,
|
||||
raise_exception: bool = False,
|
||||
|
|
@ -28,9 +26,9 @@ class MCPTool(BaseTool):
|
|||
|
||||
self.mcp_server: str = mcp_server
|
||||
self.tool_name: str = tool_name
|
||||
self.parameter_required: List[str] | None = parameter_required
|
||||
self.parameter_optional: List[str] | None = parameter_optional
|
||||
self.parameter_deleted: List[str] | None = parameter_deleted
|
||||
self.parameter_required: list[str] | None = parameter_required
|
||||
self.parameter_optional: list[str] | None = parameter_optional
|
||||
self.parameter_deleted: list[str] | None = parameter_deleted
|
||||
self.timeout: float | None = timeout
|
||||
|
||||
# Example MCP marketplace: https://bailian.console.aliyun.com/?tab=mcp#/mcp-market
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class Message(BaseModel):
|
|||
add_reasoning: bool = True,
|
||||
add_time_created: bool = False,
|
||||
add_metadata: bool = False,
|
||||
enable_json_dump: bool = False,
|
||||
as_dict: bool = True,
|
||||
) -> dict | str:
|
||||
"""Transforms the message into a simplified dictionary for standard APIs."""
|
||||
result = {}
|
||||
|
|
@ -109,10 +109,7 @@ class Message(BaseModel):
|
|||
if add_metadata:
|
||||
result["metadata"] = self.metadata
|
||||
|
||||
if enable_json_dump:
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
else:
|
||||
return result
|
||||
return result if as_dict else json.dumps(result, ensure_ascii=False)
|
||||
|
||||
def format_message(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""MCP Tool Schema definitions for recursive JSON Schema representation."""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Union, Optional
|
||||
|
||||
from mcp.types import Tool
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
|
||||
|
|
@ -16,10 +16,10 @@ class ToolAttr(BaseModel):
|
|||
|
||||
type: str = Field(default=str(JsonSchemaEnum.STRING), description="The data type of the attribute")
|
||||
description: Optional[str] = Field(default=None, description="Description of the attribute")
|
||||
required: Optional[List[str]] = Field(default=None, description="Required property names for object types")
|
||||
properties: Optional[Dict[str, "ToolAttr"]] = Field(default=None, description="Child properties for objects")
|
||||
items: Optional[Union[Dict[str, Any], "ToolAttr"]] = Field(default=None, description="Schema for array items")
|
||||
enum: Optional[List[str]] = Field(default=None, description="Allowed values for the attribute")
|
||||
required: Optional[list[str]] = Field(default=None, description="Required property names for object types")
|
||||
properties: Optional[dict[str, "ToolAttr"]] = Field(default=None, description="Child properties for objects")
|
||||
items: Optional[Union[dict[str, Any], "ToolAttr"]] = Field(default=None, description="Schema for array items")
|
||||
enum: Optional[list[str]] = Field(default=None, description="Allowed values for the attribute")
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
|
|
@ -129,9 +129,13 @@ class ToolCall(BaseModel):
|
|||
|
||||
return data
|
||||
|
||||
def simple_input_dump(self) -> dict:
|
||||
"""Returns a standardized tool definition dictionary."""
|
||||
return {
|
||||
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.
|
||||
"""
|
||||
result = {
|
||||
"type": self.type,
|
||||
self.type: {
|
||||
"name": self.name,
|
||||
|
|
@ -139,10 +143,15 @@ class ToolCall(BaseModel):
|
|||
"parameters": self.parameters.simple_input_dump(),
|
||||
},
|
||||
}
|
||||
return result if as_dict else json.dumps(result, ensure_ascii=False)
|
||||
|
||||
def simple_output_dump(self) -> dict:
|
||||
"""Convert ToolCall to output format dictionary for API responses."""
|
||||
return {
|
||||
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.
|
||||
"""
|
||||
result = {
|
||||
"index": self.index,
|
||||
"id": self.id,
|
||||
self.type: {
|
||||
|
|
@ -151,6 +160,7 @@ class ToolCall(BaseModel):
|
|||
},
|
||||
"type": self.type,
|
||||
}
|
||||
return result if as_dict else json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@property
|
||||
def argument_dict(self) -> dict:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""Defines the data structure for individual vector embedding nodes within a retrieval system."""
|
||||
|
||||
from typing import List, Dict
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
|
@ -11,5 +10,5 @@ class VectorNode(BaseModel):
|
|||
|
||||
vector_id: str = Field(default_factory=lambda: uuid4().hex)
|
||||
content: str = Field(default="")
|
||||
vector: List[float] | None = Field(default=None)
|
||||
metadata: Dict[str, str | bool | int | float] = Field(default_factory=dict)
|
||||
vector: list[float] | None = Field(default=None)
|
||||
metadata: dict[str, str | bool | int | float] = Field(default_factory=dict)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
|
@ -45,7 +44,7 @@ class HttpClient:
|
|||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def execute_flow(self, flow_name: str, **kwargs) -> Optional[Response]:
|
||||
async def execute_flow(self, flow_name: str, **kwargs) -> Response | None:
|
||||
"""Execute a flow with automated retry logic."""
|
||||
endpoint = f"{self.base_url}/{flow_name}"
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ class BaseVectorStore(ABC):
|
|||
async def get(self, vector_ids: str | list[str]) -> VectorNode | list[VectorNode]:
|
||||
"""Fetch specific vector nodes from the collection by their IDs."""
|
||||
|
||||
async def dump(self) -> list[VectorNode]:
|
||||
"""Dump the vector store to a list of vector nodes."""
|
||||
return await self.list()
|
||||
|
||||
async def load(self, nodes: list[VectorNode]):
|
||||
"""Load the vector store from a list of vector nodes."""
|
||||
await self.delete_all()
|
||||
if nodes:
|
||||
await self.insert(nodes)
|
||||
|
||||
@abstractmethod
|
||||
async def list(
|
||||
self,
|
||||
|
|
|
|||
257
reme/reme.py
Normal file
257
reme/reme.py
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
"""ReMe application classes for simplified configuration and execution."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from .agent.memory.default import ReMeSummarizer, PersonalSummarizer, PersonalRetriever, ReMeRetriever
|
||||
from .config import ReMeConfigParser
|
||||
from .core.context import ServiceContext
|
||||
from .core.embedding import BaseEmbeddingModel
|
||||
from .core.flow import BaseFlow
|
||||
from .core.llm import BaseLLM
|
||||
from .core.schema import Response, Message
|
||||
from .core.token_counter import BaseTokenCounter
|
||||
from .core.utils import execute_stream_task
|
||||
from .core.vector_store import BaseVectorStore
|
||||
from .tool.memory import UpdateUserProfile, RetrieveMemory, AddMemory, HandsOff, ReadHistory
|
||||
|
||||
|
||||
class ReMe:
|
||||
"""ReMe application with config file support and flow execution methods."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
llm_api_key: str | None = None,
|
||||
llm_api_base: str | None = None,
|
||||
embedding_api_key: str | None = None,
|
||||
embedding_api_base: str | None = None,
|
||||
enable_logo: bool = True,
|
||||
llm: dict | None = None,
|
||||
embedding_model: dict | None = None,
|
||||
vector_store: dict | None = None,
|
||||
token_counter: dict | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.service_context = ServiceContext(
|
||||
*args,
|
||||
llm_api_key=llm_api_key,
|
||||
llm_api_base=llm_api_base,
|
||||
embedding_api_key=embedding_api_key,
|
||||
embedding_api_base=embedding_api_base,
|
||||
service_config=None,
|
||||
parser=ReMeConfigParser,
|
||||
config_path=None,
|
||||
enable_logo=enable_logo,
|
||||
llm=llm,
|
||||
embedding_model=embedding_model,
|
||||
vector_store=vector_store,
|
||||
token_counter=token_counter,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
return self
|
||||
|
||||
async def close(self):
|
||||
"""Close the application."""
|
||||
return await self.service_context.close()
|
||||
|
||||
def close_sync(self):
|
||||
"""Close the application synchronously."""
|
||||
self.service_context.close_sync()
|
||||
|
||||
async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
return False
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
"""Context manager exit."""
|
||||
self.close_sync()
|
||||
return False
|
||||
|
||||
@property
|
||||
def default_llm(self) -> BaseLLM:
|
||||
"""Return the default LLM instance from the service context."""
|
||||
return self.service_context.llms["default"]
|
||||
|
||||
@property
|
||||
def default_embedding_model(self) -> BaseEmbeddingModel:
|
||||
"""Return the default embedding model instance from the service context."""
|
||||
return self.service_context.embedding_models["default"]
|
||||
|
||||
@property
|
||||
def default_vector_store(self) -> BaseVectorStore:
|
||||
"""Return the default vector store instance from the service context."""
|
||||
return self.service_context.vector_stores["default"]
|
||||
|
||||
@property
|
||||
def default_token_counter(self) -> BaseTokenCounter:
|
||||
"""Return the default token counter instance from the service context."""
|
||||
return self.service_context.token_counters["default"]
|
||||
|
||||
async def summary(
|
||||
self,
|
||||
messages: list[Message | dict],
|
||||
description: str = "",
|
||||
user_name: str | list[str] = "",
|
||||
enable_thinking_params: bool = False,
|
||||
meta_memories: list[dict] = None,
|
||||
version: str = "default",
|
||||
**kwargs,
|
||||
):
|
||||
"""Summarize messages and store them in memory for the specified user(s)."""
|
||||
if user_name:
|
||||
|
||||
if isinstance(user_name, str):
|
||||
for message in messages:
|
||||
if isinstance(message, dict) and not message.get("name"):
|
||||
message["name"] = user_name
|
||||
elif isinstance(message, Message) and not message.name:
|
||||
message.name = user_name
|
||||
user_name = [user_name]
|
||||
|
||||
if not meta_memories:
|
||||
meta_memories = [
|
||||
{
|
||||
"memory_type": "personal",
|
||||
"memory_target": name,
|
||||
}
|
||||
for name in user_name
|
||||
]
|
||||
|
||||
if version == "default":
|
||||
reme_summarizer = ReMeSummarizer(
|
||||
meta_memories=meta_memories,
|
||||
tools=[
|
||||
HandsOff(
|
||||
memory_agents=[
|
||||
PersonalSummarizer(
|
||||
tools=[
|
||||
RetrieveMemory(enable_thinking_params=enable_thinking_params),
|
||||
AddMemory(enable_thinking_params=enable_thinking_params),
|
||||
UpdateUserProfile(enable_thinking_params=enable_thinking_params),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return await reme_summarizer.call(
|
||||
messages=messages,
|
||||
description=description,
|
||||
service_context=self.service_context,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
async def retrieve(
|
||||
self,
|
||||
query: str = "",
|
||||
top_k: int = 20,
|
||||
description: str = "",
|
||||
messages: list[dict] | None = None,
|
||||
user_name: str | list[str] = "",
|
||||
enable_thinking_params: bool = False,
|
||||
meta_memories: list[dict] = None,
|
||||
version: str = "default",
|
||||
**kwargs,
|
||||
):
|
||||
"""Retrieve relevant memories for the specified user(s) based on query or messages."""
|
||||
if user_name:
|
||||
if isinstance(user_name, str):
|
||||
if messages:
|
||||
for message in messages:
|
||||
if isinstance(message, dict) and not message.get("name"):
|
||||
message["name"] = user_name
|
||||
elif isinstance(message, Message) and not message.name:
|
||||
message.name = user_name
|
||||
user_name = [user_name]
|
||||
|
||||
if not meta_memories:
|
||||
meta_memories = [
|
||||
{
|
||||
"memory_type": "personal",
|
||||
"memory_target": name,
|
||||
}
|
||||
for name in user_name
|
||||
]
|
||||
|
||||
if version == "default":
|
||||
|
||||
reme_retriever = ReMeRetriever(
|
||||
meta_memories=meta_memories,
|
||||
tools=[
|
||||
HandsOff(
|
||||
memory_agents=[
|
||||
PersonalRetriever(
|
||||
tools=[
|
||||
RetrieveMemory(enable_thinking_params=enable_thinking_params, top_k=top_k),
|
||||
ReadHistory(enable_thinking_params=enable_thinking_params),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return await reme_retriever.call(
|
||||
query=query,
|
||||
messages=messages,
|
||||
description=description,
|
||||
service_context=self.service_context,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
async def execute_flow(self, name: str, **kwargs) -> Response:
|
||||
"""Execute a flow with the given name and parameters."""
|
||||
assert name in self.service_context.flows, f"Flow {name} not found"
|
||||
flow: BaseFlow = self.service_context.flows[name]
|
||||
return await flow.call(**kwargs)
|
||||
|
||||
async def execute_stream_flow(self, name: str, **kwargs):
|
||||
"""Execute a stream flow with the given name and parameters."""
|
||||
assert name in self.service_context.flows, f"Flow {name} not found"
|
||||
flow: BaseFlow = self.service_context.flows[name]
|
||||
assert flow.stream is True, "non-stream flow is not supported in execute_stream_flow!"
|
||||
stream_queue = asyncio.Queue()
|
||||
task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs))
|
||||
async for chunk in execute_stream_task(
|
||||
stream_queue=stream_queue,
|
||||
task=task,
|
||||
task_name=name,
|
||||
as_bytes=False,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
def run_service(self):
|
||||
"""Run the configured service (HTTP, MCP, or CMD)."""
|
||||
self.service_context.service.run()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for running ReMe application from command line."""
|
||||
with ReMe(*sys.argv[1:]) as app:
|
||||
app.run_service()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
"""ReMe application classes for simplified configuration and execution."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from .config import ReMeConfigParser
|
||||
from .core.context import ServiceContext
|
||||
from .core.flow import BaseFlow
|
||||
from .core.schema import Response
|
||||
from .core.utils import execute_stream_task
|
||||
|
||||
|
||||
class ReMeApp:
|
||||
"""ReMe application with config file support and flow execution methods."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
llm_api_key: str | None = None,
|
||||
llm_api_base: str | None = None,
|
||||
embedding_api_key: str | None = None,
|
||||
embedding_api_base: str | None = None,
|
||||
enable_logo: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
self.service_context = ServiceContext(
|
||||
*args,
|
||||
llm_api_key=llm_api_key,
|
||||
llm_api_base=llm_api_base,
|
||||
embedding_api_key=embedding_api_key,
|
||||
embedding_api_base=embedding_api_base,
|
||||
service_config=None,
|
||||
parser=ReMeConfigParser,
|
||||
config_path=None,
|
||||
enable_logo=enable_logo,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
"""Async context manager exit."""
|
||||
await self.service_context.close()
|
||||
return False
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
"""Context manager exit."""
|
||||
self.service_context.close_sync()
|
||||
return False
|
||||
|
||||
async def execute_flow(self, name: str, **kwargs) -> Response:
|
||||
"""Execute a flow with the given name and parameters."""
|
||||
assert name in self.service_context.flows, f"Flow {name} not found"
|
||||
flow: BaseFlow = self.service_context.flows[name]
|
||||
return await flow.call(**kwargs)
|
||||
|
||||
async def execute_stream_flow(self, name: str, **kwargs):
|
||||
"""Execute a stream flow with the given name and parameters."""
|
||||
assert name in self.service_context.flows, f"Flow {name} not found"
|
||||
flow: BaseFlow = self.service_context.flows[name]
|
||||
assert flow.stream is True, "non-stream flow is not supported in execute_stream_flow!"
|
||||
stream_queue = asyncio.Queue()
|
||||
task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs))
|
||||
async for chunk in execute_stream_task(
|
||||
stream_queue=stream_queue,
|
||||
task=task,
|
||||
task_name=name,
|
||||
as_bytes=False,
|
||||
):
|
||||
yield chunk
|
||||
|
||||
def run_service(self):
|
||||
"""Run the configured service (HTTP, MCP, or CMD)."""
|
||||
self.service_context.service.run()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for running ReMe application from command line."""
|
||||
with ReMeApp(*sys.argv[1:]) as app:
|
||||
app.run_service()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -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.delete_memory import DeleteMemory
|
||||
from .vector.retrieve_memory import RetrieveMemory
|
||||
from .vector.retrieve_recent_memory import RetrieveRecentMemory
|
||||
from .vector.update_memory import UpdateMemory
|
||||
from ...core import R
|
||||
|
||||
__all__ = [
|
||||
"BaseMemoryTool",
|
||||
"HandsOff",
|
||||
"AddHistory",
|
||||
"ReadHistory",
|
||||
"AddIdentity",
|
||||
|
|
@ -21,6 +28,11 @@ __all__ = [
|
|||
"ReadMetaMemory",
|
||||
"ReadUserProfile",
|
||||
"UpdateUserProfile",
|
||||
"AddMemory",
|
||||
"DeleteMemory",
|
||||
"RetrieveMemory",
|
||||
"RetrieveRecentMemory",
|
||||
"UpdateMemory",
|
||||
]
|
||||
|
||||
for name in __all__:
|
||||
|
|
|
|||
108
reme/tool/memory/hands_off/hands_off.py
Normal file
108
reme/tool/memory/hands_off/hands_off.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""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, service_context=self.service_context, **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 {
|
||||
"answer": "\n".join(results),
|
||||
"agents": agent_list,
|
||||
}
|
||||
|
|
@ -38,10 +38,11 @@ class AddHistory(BaseMemoryTool):
|
|||
content=history_content,
|
||||
author=self.author,
|
||||
)
|
||||
logger.info(f"Adding history node: {history_node.model_dump_json(indent=2, exclude={'content'})}")
|
||||
self.context.history_node = history_node
|
||||
logger.info(f"Adding history node: {history_node.model_dump_json(indent=2)}")
|
||||
|
||||
vector_node = history_node.to_vector_node()
|
||||
await self.vector_store.delete(vector_node.memory_id)
|
||||
await self.vector_store.delete(vector_node.vector_id)
|
||||
await self.vector_store.insert([vector_node])
|
||||
|
||||
return f"Successfully added history: {history_node.memory_id}"
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from typing import Literal
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....core.schema import ToolCall
|
||||
from ....core.schema.memory_node import MemoryNode
|
||||
from ....core.enumeration import MemoryType
|
||||
from ....core.schema import ToolCall, MemoryNode
|
||||
|
||||
|
||||
class ReadUserProfile(BaseMemoryTool):
|
||||
|
|
@ -30,6 +30,7 @@ class ReadUserProfile(BaseMemoryTool):
|
|||
)
|
||||
|
||||
async def execute(self):
|
||||
self.context.memory_type = MemoryType.PERSONAL
|
||||
cached_data = self.local_memory.load(self.memory_cache_key, auto_clean=False)
|
||||
|
||||
if not cached_data:
|
||||
|
|
|
|||
|
|
@ -106,4 +106,4 @@ class UpdateUserProfile(BaseMemoryTool):
|
|||
operations.append(f"added {len(new_nodes)} new profiles.")
|
||||
operations.append("Operation completed.")
|
||||
logger.info("\n".join(operations))
|
||||
return operations
|
||||
return "\n".join(operations)
|
||||
|
|
|
|||
109
reme/tool/memory/vector/add_memory.py
Normal file
109
reme/tool/memory/vector/add_memory.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Add memory to vector store"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....core.schema import ToolCall, MemoryNode
|
||||
|
||||
|
||||
class AddMemory(BaseMemoryTool):
|
||||
"""Tool to add memories to vector store"""
|
||||
|
||||
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"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "add multiple memories to vector store for future retrieval.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"description": "list of memories to store.",
|
||||
"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.",
|
||||
},
|
||||
},
|
||||
"required": ["conversation_time", "memory_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memories"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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}")
|
||||
|
||||
return MemoryNode(
|
||||
memory_type=self.memory_type,
|
||||
memory_target=self.memory_target,
|
||||
content=memory_content,
|
||||
author=self.author,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
|
||||
if not memories:
|
||||
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:
|
||||
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]
|
||||
|
||||
await self.vector_store.delete(vector_ids=vector_ids)
|
||||
await self.vector_store.insert(nodes=vector_nodes)
|
||||
self.memory_nodes = memory_nodes
|
||||
|
||||
output = f"Successfully added {len(memory_nodes)} memories to vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
71
reme/tool/memory/vector/delete_memory.py
Normal file
71
reme/tool/memory/vector/delete_memory.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""Delete memory from vector store"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....core.schema import ToolCall
|
||||
|
||||
|
||||
class DeleteMemory(BaseMemoryTool):
|
||||
"""Tool to delete memories from vector store"""
|
||||
|
||||
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"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "delete multiple memories from vector store using their unique IDs.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_ids": {
|
||||
"type": "array",
|
||||
"description": "list of unique identifiers (memory_ids) of memories to delete.",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["memory_ids"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
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:
|
||||
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
|
||||
|
||||
output = f"Successfully deleted {len(memory_ids)} memories from vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
137
reme/tool/memory/vector/retrieve_memory.py
Normal file
137
reme/tool/memory/vector/retrieve_memory.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Retrieve memory 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 RetrieveMemory(BaseMemoryTool):
|
||||
"""Tool to retrieve memories from vector store using similarity search"""
|
||||
|
||||
def __init__(self, top_k: int = 20, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.top_k: int = top_k
|
||||
|
||||
@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"],
|
||||
}
|
||||
|
||||
def _build_tool_call(self) -> ToolCall:
|
||||
"""Build and return the tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "retrieve memories using vector similarity search.",
|
||||
"parameters": self._build_query_parameters(),
|
||||
},
|
||||
)
|
||||
|
||||
def _build_multiple_tool_call(self) -> ToolCall:
|
||||
"""Build and return the multiple tool call schema"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "retrieve memories using multiple queries with vector similarity search.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_items": {
|
||||
"type": "array",
|
||||
"description": "list of query items for vector similarity search.",
|
||||
"items": self._build_query_parameters(),
|
||||
},
|
||||
},
|
||||
"required": ["query_items"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
async def _retrieve_by_query(
|
||||
self,
|
||||
memory_type: str,
|
||||
memory_target: str,
|
||||
query: str,
|
||||
time_range: str | None = None,
|
||||
) -> list[MemoryNode]:
|
||||
"""Retrieve memories by query with filters"""
|
||||
filter_dict: dict = {
|
||||
"memory_type": memory_type,
|
||||
"memory_target": memory_target,
|
||||
}
|
||||
|
||||
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)
|
||||
return [MemoryNode.from_vector_node(n) for n in nodes]
|
||||
|
||||
async def execute(self):
|
||||
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", [])
|
||||
else:
|
||||
query_items: list[dict] = [
|
||||
{
|
||||
"query": self.context.get("query", ""),
|
||||
"time_range": self.context.get("time_range", ""),
|
||||
},
|
||||
]
|
||||
|
||||
query_items = [item for item in query_items if item.get("query")]
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
for item in query_items:
|
||||
retrieved = await self._retrieve_by_query(
|
||||
memory_type=memory_type,
|
||||
memory_target=memory_target,
|
||||
query=item["query"],
|
||||
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:
|
||||
output = "No new memory_nodes found matching the query (duplicates removed)."
|
||||
else:
|
||||
outputs = []
|
||||
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} "
|
||||
outputs.append(line.strip())
|
||||
output = "\n".join(outputs)
|
||||
|
||||
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 RetrieveRecentMemory(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
|
||||
126
reme/tool/memory/vector/update_memory.py
Normal file
126
reme/tool/memory/vector/update_memory.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""Update memory in vector store"""
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ....core.schema import ToolCall, MemoryNode
|
||||
|
||||
|
||||
class UpdateMemory(BaseMemoryTool):
|
||||
"""Tool to update memories in vector store"""
|
||||
|
||||
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"""
|
||||
return ToolCall(
|
||||
**{
|
||||
"description": "update multiple memories in vector store by replacing old memories with new content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"description": "list of memory update objects.",
|
||||
"items": {
|
||||
"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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["memories"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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}
|
||||
|
||||
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=memory_content,
|
||||
author=self.author,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return memory_id, memory_node
|
||||
|
||||
async def execute(self):
|
||||
old_memory_ids: list[str] = []
|
||||
memory_nodes: list[MemoryNode] = []
|
||||
memories: list[dict] = self.context.get("memories", [])
|
||||
|
||||
if not memories:
|
||||
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)
|
||||
|
||||
if not memory_nodes:
|
||||
output = "No valid memories provided for update."
|
||||
logger.info(output)
|
||||
return output
|
||||
|
||||
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 = memory_nodes
|
||||
|
||||
output = f"Successfully updated {len(memory_nodes)} memories in vector_store."
|
||||
logger.info(output)
|
||||
return output
|
||||
0
reme/workflow/procedural_memory/summary/__init__.py
Normal file
0
reme/workflow/procedural_memory/summary/__init__.py
Normal file
|
|
@ -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,52 +0,0 @@
|
|||
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
|
||||
|
||||
|
||||
class PersonalRetrieverV4(BaseMemoryAgent):
|
||||
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
|
||||
if not context:
|
||||
raise ValueError("input must have either `query` or `messages`")
|
||||
|
||||
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
|
||||
|
||||
return [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.prompt_format(
|
||||
prompt_name="user_message",
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
user_profile=user_profile,
|
||||
context=context,
|
||||
))
|
||||
]
|
||||
|
||||
async def _acting_step(self, assistant_message: Message, step: int, **kwargs) -> list[Message]:
|
||||
return await super()._acting_step(
|
||||
assistant_message,
|
||||
step,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Execute the retriever and determine success based on output markers."""
|
||||
await super().execute()
|
||||
|
||||
# Check for memory found/not found markers in the output
|
||||
if self.output:
|
||||
if "<MEMORY_FOUND>" in self.output:
|
||||
self.success = True
|
||||
elif "<MEMORY_NOT_FOUND>" in self.output:
|
||||
self.success = False
|
||||
|
||||
self.meta_info = self.context.user_profile + "\n" + self.meta_info
|
||||
|
|
@ -1,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,124 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_agent import BaseMemoryAgent
|
||||
from ...core.enumeration import Role, MemoryType
|
||||
from ...core.schema import Message, MemoryNode
|
||||
|
||||
|
||||
class PersonalSummarizerV4(BaseMemoryAgent):
|
||||
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 = [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.prompt_format(
|
||||
prompt_name="user_message_phase1",
|
||||
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 = [
|
||||
Message(
|
||||
role=Role.USER,
|
||||
content=self.prompt_format(
|
||||
prompt_name="user_message_phase2",
|
||||
context=history_node.content,
|
||||
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(
|
||||
assistant_message,
|
||||
step,
|
||||
stage=stage,
|
||||
memory_type=self.memory_type.value,
|
||||
memory_target=self.memory_target,
|
||||
history_node=self.history_node,
|
||||
author=self.author,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def execute(self):
|
||||
"""Execute in two phases: 1) AddSummaryMemory, 2) UpdateUserProfile"""
|
||||
# Log available tools
|
||||
for i, tool in enumerate(self.tools):
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}] step0.{i} "
|
||||
f"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)
|
||||
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)}",
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
# 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
|
||||
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")
|
||||
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)}",
|
||||
)
|
||||
|
||||
messages_phase2, success_phase2 = await self.react(messages_phase2, stage="S2")
|
||||
|
||||
# Restore original tools
|
||||
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."
|
||||
|
|
@ -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,117 +0,0 @@
|
|||
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
|
||||
|
||||
|
||||
class ReMeRetrieverV4(BaseMemoryAgent):
|
||||
|
||||
def __init__(self, meta_memories: list[dict] | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.meta_memories: list[dict] = meta_memories or []
|
||||
self.meta_info_dict: dict[str, str] = {}
|
||||
|
||||
async def _read_meta_memories(self) -> str:
|
||||
from ...mem_tool import ReadMetaMemory
|
||||
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:
|
||||
raise ValueError("Input must have either `query` or `messages`")
|
||||
|
||||
messages = [
|
||||
Message(
|
||||
role=Role.SYSTEM,
|
||||
content=self.prompt_format(
|
||||
prompt_name="system_prompt",
|
||||
meta_memory_info=await self._read_meta_memories(),
|
||||
user_query=user_query,
|
||||
),
|
||||
),
|
||||
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]:
|
||||
import asyncio
|
||||
from ...mem_tool.v4 import HandsOff
|
||||
|
||||
if not assistant_message.tool_calls:
|
||||
return []
|
||||
|
||||
tool_list: list = []
|
||||
tool_result_messages: list[Message] = []
|
||||
tool_dict = {t.tool_call.name: t for t in self.tools}
|
||||
stage_prefix = ""
|
||||
|
||||
# Add required context parameters
|
||||
kwargs["query"] = self.context.get("query", "")
|
||||
kwargs["messages"] = self.context.get("messages", [])
|
||||
|
||||
for j, tool_call in enumerate(assistant_message.tool_calls):
|
||||
if tool_call.name not in tool_dict:
|
||||
logger.warning(f"[{self.__class__.__name__}{stage_prefix}] unknown tool_call.name={tool_call.name}")
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} "
|
||||
f"submit tool_calls={tool_call.name} argument={tool_call.arguments}",
|
||||
)
|
||||
tool_copy = tool_dict[tool_call.name].copy()
|
||||
tool_copy.tool_call.id = tool_call.id
|
||||
tool_list.append(tool_copy)
|
||||
kwargs.update(tool_call.argument_dict)
|
||||
self.submit_async_task(tool_copy.call, retrieved_nodes=self.retrieved_nodes, **kwargs)
|
||||
if self.tool_call_interval > 0:
|
||||
await asyncio.sleep(self.tool_call_interval)
|
||||
|
||||
await self.join_async_tasks()
|
||||
|
||||
for j, op in enumerate(tool_list):
|
||||
if op.memory_nodes:
|
||||
self.memory_nodes.extend(op.memory_nodes)
|
||||
|
||||
if hasattr(op, "messages") and op.messages:
|
||||
self.tool_messages.extend(op.messages)
|
||||
|
||||
# Collect meta_info_dict from HandsOff tool
|
||||
if isinstance(op, HandsOff) and hasattr(op, "meta_info_dict"):
|
||||
self.meta_info_dict.update(op.meta_info_dict)
|
||||
logger.info(f"Collected meta_info_dict from HandsOff: {len(op.meta_info_dict)} entries")
|
||||
|
||||
tool_result = str(op.output)
|
||||
tool_message = Message(
|
||||
role=Role.TOOL,
|
||||
content=tool_result,
|
||||
tool_call_id=op.tool_call.id,
|
||||
)
|
||||
tool_result_messages.append(tool_message)
|
||||
|
||||
self.meta_info += tool_result + "\n"
|
||||
|
||||
logger.info(f"[{self.__class__.__name__}{stage_prefix}] step{step + 1}.{j} join tool_result={tool_result[:2000]}...\n\n")
|
||||
|
||||
return tool_result_messages
|
||||
|
||||
async def execute(self):
|
||||
await super().execute()
|
||||
|
||||
# Assemble meta_info_dict into output
|
||||
if self.meta_info_dict:
|
||||
output_parts = []
|
||||
for key, value in self.meta_info_dict.items():
|
||||
output_parts.append(f"## {key}\n{value}")
|
||||
self.output = "\n\n".join(output_parts)
|
||||
logger.info(f"Assembled output from meta_info_dict with {len(self.meta_info_dict)} entries")
|
||||
|
|
@ -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,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,144 +0,0 @@
|
|||
"""Hands-off tool for distributing memory tasks 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mem_agent import BaseMemoryAgent
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class HandsOffTool(BaseMemoryTool):
|
||||
"""Distribute memory tasks to appropriate agents based on memory_type."""
|
||||
|
||||
def __init__(self, memory_agents: list["BaseMemoryAgent"], **kwargs):
|
||||
kwargs["sub_ops"] = memory_agents or []
|
||||
super().__init__(**kwargs)
|
||||
from ..mem_agent import BaseMemoryAgent
|
||||
|
||||
self.sub_ops: list[BaseMemoryAgent] = [a for a in self.sub_ops if isinstance(a, BaseMemoryAgent)]
|
||||
|
||||
@property
|
||||
def memory_agent_dict(self) -> dict[MemoryType, "BaseMemoryAgent"]:
|
||||
"""Returns a dictionary mapping memory types to their corresponding agents."""
|
||||
return {a.memory_type: a for a in self.sub_ops}
|
||||
|
||||
def _build_item_schema(self) -> tuple[dict, list[str]]:
|
||||
"""Build shared schema properties and required fields for memory tasks."""
|
||||
properties = {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_type"),
|
||||
"enum": [k.value for k in self.memory_agent_dict],
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_target"),
|
||||
},
|
||||
}
|
||||
required = ["memory_type", "memory_target"]
|
||||
return properties, required
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
"""Build input schema for single memory task distribution."""
|
||||
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 task distribution."""
|
||||
item_properties, required_fields = self._build_item_schema()
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_tasks": {
|
||||
"type": "array",
|
||||
"description": self.get_prompt("memory_tasks"),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": item_properties,
|
||||
"required": required_fields,
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["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 based on enable_multiple flag."""
|
||||
tasks: list[dict] = []
|
||||
if self.enable_multiple:
|
||||
memory_tasks: list[dict] = self.context.get("memory_tasks", [])
|
||||
for task in memory_tasks:
|
||||
tasks.append(self._parse_memory_type_target(task))
|
||||
else:
|
||||
tasks.append(self._parse_memory_type_target(self.context))
|
||||
return tasks
|
||||
|
||||
async def execute(self):
|
||||
"""Execute memory tasks by distributing to appropriate agents in parallel."""
|
||||
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 for target={memory_target}")
|
||||
self.submit_async_task(
|
||||
agent.call,
|
||||
query=self.context.get("query", ""),
|
||||
messages=self.context.get("messages", []),
|
||||
memory_type=memory_type,
|
||||
memory_target=memory_target,
|
||||
description=self.context.get("description"),
|
||||
ref_memory_id=self.context.get("ref_memory_id", ""),
|
||||
)
|
||||
|
||||
await self.join_async_tasks()
|
||||
|
||||
# Collect results
|
||||
results = []
|
||||
for i, (agent, memory_type, memory_target) in enumerate(agent_list):
|
||||
result_str = str(agent.output)
|
||||
if agent.memory_nodes:
|
||||
self.memory_nodes.extend(agent.memory_nodes)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"memory_type": memory_type.value,
|
||||
"memory_target": memory_target,
|
||||
"result": result_str[:200] + ("..." if len(result_str) > 200 else ""),
|
||||
},
|
||||
)
|
||||
logger.info(f"Task {i}: Completed {memory_type.value} agent for target={memory_target}")
|
||||
|
||||
results_str = json.dumps(results, ensure_ascii=False, indent=2)
|
||||
self.output = f"Successfully executed {len(results)} memory tasks:\n{results_str}"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
tool: |
|
||||
Distribute a memory task to the appropriate agent based on memory_type.
|
||||
Use this tool to hand off memory summarization to specialized agents.
|
||||
Examples: summarizing user preferences, extracting procedural knowledge, or analyzing tool usage patterns.
|
||||
|
||||
tool_multiple: |
|
||||
Distribute multiple memory tasks to appropriate agents in parallel.
|
||||
Use this tool to hand off multiple memory summarization tasks efficiently.
|
||||
Each task will be processed by its corresponding specialized agent based on memory_type.
|
||||
|
||||
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,54 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from ..base_memory_tool import BaseMemoryTool
|
||||
from ...core.context import C
|
||||
from ...core.schema.memory_node import MemoryNode
|
||||
|
||||
|
||||
@C.register_op()
|
||||
class ReadLocalMemories(BaseMemoryTool):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["enable_multiple"] = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _build_parameters(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_type": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_type"),
|
||||
},
|
||||
"memory_target": {
|
||||
"type": "string",
|
||||
"description": self.get_prompt("memory_target"),
|
||||
},
|
||||
},
|
||||
"required": ["memory_type", "memory_target"],
|
||||
}
|
||||
|
||||
async def execute(self):
|
||||
memory_type = self.context.get("memory_type", "")
|
||||
memory_target = self.context.get("memory_target", "")
|
||||
|
||||
if not memory_type or not memory_target:
|
||||
self.output = "memory_type and memory_target are required."
|
||||
return
|
||||
|
||||
cache_key = f"{memory_type}_{memory_target}"
|
||||
cached_data = self.meta_memory.load(cache_key, auto_clean=False)
|
||||
|
||||
if not cached_data:
|
||||
self.output = f"Local memory not found: {memory_type}_{memory_target}"
|
||||
logger.info(self.output)
|
||||
return
|
||||
|
||||
memory_nodes = [MemoryNode(**node_data) for node_data in cached_data]
|
||||
|
||||
if not memory_nodes:
|
||||
self.output = f"No valid memory nodes found in {memory_type}_{memory_target}"
|
||||
return
|
||||
|
||||
self.output = memory_nodes
|
||||
logger.info(f"Read {len(memory_nodes)} nodes from cache key: {cache_key}")
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
tool: |
|
||||
Read memory nodes from local memory files.
|
||||
|
||||
memory_type: |
|
||||
The type of local memory to read.
|
||||
|
||||
memory_target: |
|
||||
The target identifier for the local memory.
|
||||
|
|
@ -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"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue