feat(mem_agent): implement memory agent architecture with specialized summarizers and retrievers

This commit is contained in:
jinli.yl 2026-01-07 18:07:49 +08:00
parent f493ba2f3a
commit 416206d332
33 changed files with 1039 additions and 79 deletions

View file

@ -4,7 +4,7 @@ import asyncio
import copy
import inspect
from pathlib import Path
from typing import Callable, Any, Optional
from typing import Callable, Optional
from loguru import logger
from tqdm import tqdm
@ -136,7 +136,7 @@ class BaseOp:
return {k: self.context[k] for k in parameters.properties.keys() if (k in required_keys or k in self.context)}
@property
def output(self) -> Any:
def output(self):
"""Get the single output value from context."""
output_properties = self.tool_call.output.properties
if not output_properties:
@ -149,7 +149,7 @@ class BaseOp:
return None
@output.setter
def output(self, value: Any):
def output(self, value):
"""Set the single output value into context."""
output_properties = self.tool_call.output.properties
if not output_properties:
@ -322,15 +322,21 @@ class BaseOp:
for name, op in sub_ops.items():
assert self.async_mode == op.async_mode, "Async mode mismatch!"
op.name = name
if self.language:
op.language = self.language
self.sub_ops.append(op)
elif isinstance(sub_ops, list):
for op in sub_ops:
assert self.async_mode == op.async_mode, "Async mode mismatch!"
if self.language:
op.language = self.language
self.sub_ops.append(op)
else:
assert self.async_mode == sub_ops.async_mode, "Async mode mismatch!"
if self.language:
sub_ops.language = self.language
self.sub_ops.append(sub_ops)
def add_sub_op(self, sub_op: "BaseOp"):

View file

@ -341,7 +341,7 @@ class ESVectorStore(BaseVectorStore):
actions = []
for node in nodes_to_update:
doc = {
doc: dict = {
"vector_id": node.vector_id,
"content": node.content,
"metadata": node.metadata,

View file

@ -1,9 +1,15 @@
"""Agent module providing chat operations."""
"""memory agent"""
from . import retriever
from . import summarizer
from .base_memory_agent import BaseMemoryAgent
from .simple_chat import SimpleChat
from .stream_chat import StreamChat
__all__ = [
"retriever",
"summarizer",
"BaseMemoryAgent",
"StreamChat",
"SimpleChat",
]

View file

@ -18,19 +18,25 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta):
def __init__(
self,
max_steps: int = 20,
tool_call_interval: float = 0,
tools: list[BaseMemoryTool],
add_think_tool: bool = False, # only for instruct model
tools: list[BaseMemoryTool] | None = None,
force_tool_language: bool = True,
tool_call_interval: float = 0,
max_steps: int = 20,
**kwargs,
):
super().__init__(**kwargs)
self.max_steps: int = max_steps
self.tools: list[BaseMemoryTool] = tools or []
if add_think_tool:
self.tools.append(ThinkTool())
if force_tool_language and self.language:
for tool in self.tools:
tool.language = self.language
self.tool_call_interval: float = tool_call_interval
self.add_think_tool: bool = add_think_tool
assert not self.sub_ops, "sub_ops must be empty, use `tools`~"
if tools:
self.sub_ops.extend([t.set_language(self.language) for t in tools])
self.max_steps: int = max_steps
self.messages: list[Message] = []
self.success: bool = True
def _build_tool_call(self) -> ToolCall:
return ToolCall(
@ -66,19 +72,6 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta):
},
)
@property
def tools(self) -> list[BaseMemoryTool]:
"""Returns the list of memory tools available to the agent."""
tools: list[BaseMemoryTool] = [o for o in self.sub_ops if isinstance(o, BaseMemoryTool)]
if self.add_think_tool:
tools.append(ThinkTool(language=self.language))
return tools
@tools.setter
def tools(self, tools: list[BaseMemoryTool] | BaseMemoryTool):
"""Sets the memory tools for the agent."""
self.sub_ops = tools
def get_messages(self) -> list[Message]:
"""Extracts and returns messages from the context query or messages."""
if self.context.get("query"):
@ -141,26 +134,29 @@ class BaseMemoryAgent(BaseOp, metaclass=ABCMeta):
async def react(self, messages: list[Message]):
"""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)
if not should_act:
success = True
break
tool_result_messages = await self._acting_step(assistant_message, step)
messages.extend(tool_result_messages)
return messages
return messages, success
async def execute(self):
messages = await self.build_messages()
for i, message in enumerate(messages):
logger.info(f"step0.{i} {message.role} {message.name or ''} {message.simple_dump()}")
messages = await self.react(messages)
self.output = [
m.simple_dump(add_name=True, add_reasoning=True, add_time_created=True, add_metadata=True) for m in messages
]
self.messages, self.success = await self.react(messages)
if self.success and self.messages:
self.output = self.messages[-1].content
else:
self.output = ""
@property
def memory_target(self) -> str:

View file

@ -0,0 +1,9 @@
"""memory retriever"""
from .reme_retriever import ReMeRetriever
from .remy_agent import ReMyAgent
__all__ = [
"ReMeRetriever",
"ReMyAgent",
]

View file

@ -0,0 +1,42 @@
"""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, enable_tool_memory: bool = True, **kwargs):
"""Initialize retriever with tool memory option."""
super().__init__(**kwargs)
self.enable_tool_memory = enable_tool_memory
async def _read_meta_memories(self) -> str:
"""Read and return meta memories as string."""
from ...mem_tool import ReadMetaMemory
op = ReadMetaMemory(enable_tool_memory=self.enable_tool_memory, enable_identity_memory=False)
await op.call()
return str(op.output)
async def build_messages(self) -> List[Message]:
"""Build messages with system prompt and user message."""
system_prompt = self.prompt_format(
prompt_name="system_prompt",
now_time=get_now_time(),
meta_memory_info=await self._read_meta_memories(),
context=format_messages(self.get_messages()),
)
messages = [
Message(role=Role.SYSTEM, content=system_prompt),
Message(role=Role.USER, content=self.get_prompt("user_message")),
]
return messages

View file

@ -0,0 +1,50 @@
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 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 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.
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, clearly summarize the retrieved information.
- If multiple attempts still yield no relevant memory, 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.

View file

@ -0,0 +1,51 @@
"""ReMy agent with identity and meta memory capabilities."""
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
@C.register_op()
class ReMyAgent(BaseMemoryAgent):
"""Memory agent with identity awareness and meta memory retrieval."""
def __init__(self, enable_tool_memory: bool = True, enable_identity_memory: bool = True, **kwargs):
"""Initialize ReMy agent with memory options."""
super().__init__(**kwargs)
self.enable_tool_memory = enable_tool_memory
self.enable_identity_memory = enable_identity_memory
@staticmethod
async def _read_identity_memory() -> str:
"""Read and return identity memory as string."""
from ...mem_tool import ReadIdentityMemory
op = ReadIdentityMemory()
await op.call()
return str(op.output)
async def _read_meta_memories(self) -> str:
"""Read and return meta memories as string."""
from ...mem_tool import ReadMetaMemory
op = ReadMetaMemory(
enable_tool_memory=self.enable_tool_memory,
enable_identity_memory=self.enable_identity_memory,
)
await op.call()
return str(op.output)
async def build_messages(self) -> List[Message]:
"""Build messages with system prompt and user messages."""
system_prompt = 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(),
)
return [Message(role=Role.SYSTEM, content=system_prompt)] + self.get_messages()

View file

@ -0,0 +1,36 @@
tool: |
Conversational AI assistant with integrated memory capabilities.
Use this tool to engage in natural conversations with users while leveraging
stored identity and memory context. The agent can access historical information,
user preferences, and procedural knowledge through its memory system, and can
use various tools to accomplish tasks and answer questions.
system_prompt: |
You are ReMy, an intelligent AI assistant with memory capabilities.
## Current Time
{now_time}
## Self-Awareness
{identity_memory}
## Available Meta Memories
Format: "- <memory_type>(<memory_target>): <description>"
{meta_memory_info}
## Guiding Principles
1. **Be Helpful and Accurate**: Provide clear and correct information.
2. **Use Memory Wisely**: Retrieve relevant memories when they can improve your response.
3. **Use Tools Appropriately**: Select the right tool for each task.
4. **Stay Conversational**: Maintain a natural and friendly tone.
5. **Seek Clarification**: Ask questions if the user’s intent is unclear.
6. **Acknowledge Limitations**: Be honest about what you can and cannot do.
## How to Use the Memory Retrieval Tool
When using `vector_retrieve_memory` to search memories:
- Choose an appropriate `memory_type` and `memory_target` from the "Available Meta Memories" list above.
- Formulate a clear and specific query based on the information you need.
- **Important**: When retrieving tool-related memories (`memory_type` is "tool"), the query must use the tool’s exact name (not a description or a question).
- If retrieval results include a `ref_memory_id` and you need more details, use `read_history_memory` with the `ref_memory_id` as the `memory_id` parameter.
- If the initial retrieval yields no results, try rephrasing your query or using a different memory type.
- You may generate multiple queries with different phrasings or perspectives for the same memory type/target.

View file

@ -1,4 +1,4 @@
"""Simple chat agent for non-streaming conversations."""
"""Simple chat for test."""
from loguru import logger

View file

@ -1,4 +1,4 @@
"""Streaming chat agent for real-time conversation streaming."""
"""Streaming chat for test."""
from loguru import logger

View file

@ -0,0 +1,15 @@
"""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",
]

View file

@ -0,0 +1,33 @@
"""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)

View file

@ -0,0 +1,28 @@
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.

View file

@ -0,0 +1,41 @@
"""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
from ...core.utils import get_now_time, format_messages
@C.register_op()
class PersonalSummarizer(BaseMemoryAgent):
"""Extracts and stores personal information about individuals from conversations."""
memory_type: MemoryType = MemoryType.PERSONAL
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 memory_target, memory_type, and author context."""
return await super()._acting_step(
assistant_message,
step,
memory_target=self.memory_target,
memory_type=self.memory_type.value,
author=self.author,
**kwargs,
)

View file

@ -0,0 +1,54 @@
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 specializing in the domain of **{memory_target}**. 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 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.
- Each memory entry must be self-contained and understandable without additional context.
- Avoid storing trivial or temporary 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 to find existing, potentially relevant memories.
- Retrieve related memories for comparison to check for duplication or associations.
3. **Compare and Decide** on memory operations:
- Compare the newly extracted memories with historical ones to ensure the final memory store contains no duplicates or contradictions.
- 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 update 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 store.
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 accurate: ensure extracted content faithfully reflects the original context.
- Avoid redundancy: always check for similar existing memories before adding new ones.
- Include relevant metadata (e.g., timestamps) when appropriate.
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.

View file

@ -0,0 +1,42 @@
"""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,
)

View file

@ -0,0 +1,70 @@
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 in the domain of **{memory_target}**. 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.

View file

@ -0,0 +1,105 @@
"""Orchestrator for complete memory summarization workflow across all memory types."""
import re
from typing import List
from loguru import logger
from ..base_memory_agent import BaseMemoryAgent
from ...core.context import C
from ...core.enumeration import Role
from ...core.schema import Message, MemoryNode
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, enable_tool_memory: bool = True, enable_identity_memory: bool = True, **kwargs):
"""Initialize with flags to enable/disable tool and identity memory processing."""
super().__init__(**kwargs)
self.enable_tool_memory = enable_tool_memory
self.enable_identity_memory = enable_identity_memory
async def _add_history_memory(self) -> MemoryNode:
"""Store conversation history and return the memory node."""
from ...mem_tool import AddHistoryMemory
op = AddHistoryMemory()
await op.call(messages=self.get_messages())
return op.output
@staticmethod
async def _read_identity_memory() -> str:
"""Retrieve agent's self-perception memory."""
from ...mem_tool import ReadIdentityMemory
op = ReadIdentityMemory()
await op.call()
return op.output
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_tool_memory=self.enable_tool_memory,
enable_identity_memory=self.enable_identity_memory,
)
await op.call()
return str(op.output)
async def build_messages(self) -> List[Message]:
"""Construct initial messages with context, identity, and meta-memory information."""
memory_node: MemoryNode = await self._add_history_memory()
self.context["ref_memory_id"] = memory_node.memory_id
now_time = get_now_time()
identity_memory = await self._read_identity_memory()
meta_memory_info = await self._read_meta_memories()
context = format_messages(self.get_messages())
logger.info(
f"now_time={now_time} "
f"memory_node={memory_node} "
f"identity_memory={identity_memory} "
f"meta_memory_info={meta_memory_info} "
f"context={context}",
)
system_prompt = self.prompt_format(
prompt_name="system_prompt",
now_time=now_time,
identity_memory=identity_memory,
meta_memory_info=meta_memory_info,
context=context,
)
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."""
meta_memory_info = await self._read_meta_memories()
system_messages = [message for message in messages if message.role is Role.SYSTEM]
if system_messages:
system_message = system_messages[0]
pattern = r'("- <memory_type>\(<memory_target>\): <description>"\n)(.*?)(\n\n)'
replacement = rf"\g<1>{meta_memory_info}\g<3>"
system_message.content = re.sub(pattern, replacement, system_message.content, flags=re.DOTALL)
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,
ref_memory_id=self.context["ref_memory_id"],
author=self.author,
**kwargs,
)

View file

@ -0,0 +1,52 @@
tool: |
Orchestrate the complete memory summarization workflow for the agent.
This tool receives conversation context and performs necessary memory updates including:
1. Creating new meta-memory entries if needed
2. Adding summary memory for quick future recall
3. Delegating to specialized memory agents for detailed memory extraction and update
system_prompt: |
# Context
{context}
You are a Memory Agent responsible for performing necessary updates and summaries of the main Agent's memories based on the **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. Create New Meta Memory (if needed)
When the context contains significant personal or procedural information not yet covered by existing meta memories:
- Use `add_meta_memory` to create one or more new meta memory entries.
- For personal memories: specify `memory_type="personal"` and `memory_target=<person's name>`.
- For procedural memories: specify `memory_type="procedural"` and `memory_target=<topic or domain>`.
- Each meta memory entry will instantiate a dedicated specialized Memory Agent for that dimension.
### 2. Add Summary Memory (if valuable)
When the context includes information worth remembering for quick future recall:
- 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.
### 3. Delegate to Specialized Memory Agents (Core Task)
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 or meaningless small talk), 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.

View file

@ -0,0 +1,42 @@
"""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,
)

View file

@ -0,0 +1,55 @@
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.

View file

@ -0,0 +1,149 @@
"""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"], force_agent_language: bool = True, **kwargs):
super().__init__(**kwargs)
self.memory_agent_dict: dict[MemoryType, "BaseMemoryAgent"] = {}
if memory_agents:
for agent in memory_agents:
if agent.memory_type is None:
continue
self.memory_agent_dict[agent.memory_type] = agent
if force_agent_language and self.language:
agent.language = self.language
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": [
MemoryType.IDENTITY.value,
MemoryType.PERSONAL.value,
MemoryType.PROCEDURAL.value,
MemoryType.TOOL.value,
],
},
"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_copy = self.memory_agent_dict[memory_type].copy()
agent_list.append({
"agent": agent_copy,
"memory_type": memory_type,
"memory_target": memory_target,
})
logger.info(f"Task {i}: Submitting {memory_type.value} agent for target={memory_target}")
self.submit_async_task(
agent_copy.call,
query=self.context.get("query", ""),
messages=self.context.get("messages", []),
memory_target=memory_target,
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)
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.set_output(f"Successfully executed {len(results)} memory tasks:\n{results_str}")

View file

@ -0,0 +1,19 @@
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.

View file

@ -38,7 +38,6 @@ class AddHistoryMemory(BaseMemoryTool):
return
messages = [Message(**m) if isinstance(m, dict) else m for m in messages]
memory_content = format_messages(messages)
memory_node = self._build_memory_node(memory_content=memory_content, memory_type=MemoryType.HISTORY)

View file

@ -9,22 +9,31 @@ from ...core.schema import MemoryNode
@C.register_op()
class AddMemory(BaseMemoryTool):
"""Add memories to vector store with optional when_to_use and metadata.
"""Add memories to vector store with optional when_to_use and custom metadata fields.
Supports single/multiple addition modes via `enable_multiple` parameter.
Metadata fields can be customized via `metadata_desc` parameter.
"""
def __init__(self, add_when_to_use: bool = False, add_metadata: bool = True, **kwargs):
def __init__(self, add_when_to_use: bool = False, metadata_desc: dict[str, str] | None = None, **kwargs):
"""Initialize AddMemory.
Args:
add_when_to_use: Include when_to_use field for better retrieval.
add_metadata: Include metadata field for additional info.
metadata_desc: Dictionary defining metadata fields and their descriptions.
Example:
{
"year": "The `year` information associated with the memory(Optional)",
"month": "The `month` information associated with the memory(Optional)",
"day": "The `day` information associated with the memory(Optional)",
"hour": "The `hour` information associated with the memory(Optional)",
}
If None or empty dict, metadata field will not be included.
**kwargs: Additional arguments for BaseMemoryTool.
"""
super().__init__(**kwargs)
self.add_when_to_use: bool = add_when_to_use
self.add_metadata: bool = add_metadata
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.
@ -48,10 +57,19 @@ class AddMemory(BaseMemoryTool):
}
required.append("memory_content")
if self.add_metadata:
# Add metadata field if metadata_desc is provided and not empty
if self.metadata_desc:
metadata_properties = {
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
}
# Generate dynamic description based on metadata_desc fields
field_descriptions = "\n".join([f" - {key}: {desc}" for key, desc in self.metadata_desc.items()])
metadata_description = f"Optional metadata for the memory. Available fields:\n{field_descriptions}"
properties["metadata"] = {
"type": "object",
"description": self.get_prompt("metadata"),
"description": metadata_description,
"properties": metadata_properties,
}
return properties, required
@ -95,7 +113,12 @@ class AddMemory(BaseMemoryTool):
"""
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.add_metadata else {}
# Only extract metadata if metadata_desc is configured
# Convert all metadata values to strings
metadata = {}
if self.metadata_desc:
raw_metadata = mem_dict.get("metadata", {})
metadata = {key: str(value).strip() for key, value in raw_metadata.items() if value}
return memory_content, when_to_use, metadata
async def execute(self):

View file

@ -26,12 +26,5 @@ memory_content: |
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.
metadata: |
Optional metadata for the memory, providing additional context. Can include:
- time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00")
- source: Where this information came from (e.g., "user_input", "documentation", "observation")
- tags: List of tags for categorization (e.g., ["authentication", "security"])
- Any other custom key-value pairs relevant to the memory
memories: |
A list of memory objects to store.

View file

@ -14,19 +14,20 @@ class AddSummaryMemory(AddMemory):
- Single memory mode only (enable_multiple=False)
- Uses 'summary_memory' parameter instead of 'memory_content'
- No when_to_use field (add_when_to_use=False)
- Metadata fields can be customized via `metadata_desc` parameter
"""
def __init__(self, add_metadata: bool = True, **kwargs):
def __init__(self, metadata_desc: dict[str, str] | None = None, **kwargs):
"""Initialize AddSummaryMemory.
Args:
add_metadata: Include metadata field for additional info.
metadata_desc: Dictionary defining metadata fields and their descriptions.
**kwargs: Additional arguments for AddMemory.
"""
# Force single mode and disable when_to_use
kwargs["enable_multiple"] = False
kwargs["add_when_to_use"] = False
super().__init__(add_metadata=add_metadata, **kwargs)
super().__init__(metadata_desc=metadata_desc, **kwargs)
def _build_parameters(self) -> dict:
"""Build input schema for summary memory addition."""
@ -38,10 +39,19 @@ class AddSummaryMemory(AddMemory):
}
required = ["summary_memory"]
if self.add_metadata:
# Add metadata field if metadata_desc is provided and not empty
if self.metadata_desc:
metadata_properties = {
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
}
# Generate dynamic description based on metadata_desc fields
field_descriptions = "\n".join([f" - {key}: {desc}" for key, desc in self.metadata_desc.items()])
metadata_description = f"Optional metadata for the memory. Available fields:\n{field_descriptions}"
properties["metadata"] = {
"type": "object",
"description": self.get_prompt("metadata"),
"description": metadata_description,
"properties": metadata_properties,
}
return {

View file

@ -17,11 +17,3 @@ summary_memory: |
- "User prefers Python for backend development and has experience with FastAPI framework"
- "Project deadline is January 15th, requires authentication, payment integration, and admin dashboard"
- "Bug in user registration was caused by missing email validation, fixed by adding regex check"
metadata: |
Optional metadata for the memory, providing additional context. Can include:
- time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00")
- source: Where this information came from (e.g., "conversation", "meeting", "observation")
- tags: List of tags for categorization (e.g., ["project", "decision"])
- summary_type: Type of summary (e.g., "conversation", "decision", "event", "task")
- Any other custom key-value pairs relevant to the memory

View file

@ -12,24 +12,25 @@ class UpdateMemory(BaseMemoryTool):
"""Update memories by deleting old ones and inserting new ones.
Supports single/multiple update modes via `enable_multiple` parameter.
Metadata fields can be customized via `metadata_desc` parameter.
"""
def __init__(
self,
add_when_to_use: bool = False,
add_metadata: bool = True,
metadata_desc: dict[str, str] | None = None,
**kwargs,
):
"""Initialize UpdateMemory.
Args:
add_when_to_use: Include when_to_use field for better retrieval.
add_metadata: Include metadata field for additional info.
metadata_desc: Dictionary defining metadata fields and their descriptions.
**kwargs: Additional arguments for BaseMemoryTool.
"""
super().__init__(**kwargs)
self.add_when_to_use: bool = add_when_to_use
self.add_metadata: bool = add_metadata
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.
@ -57,10 +58,19 @@ class UpdateMemory(BaseMemoryTool):
}
required.append("memory_content")
if self.add_metadata:
# Add metadata field if metadata_desc is provided and not empty
if self.metadata_desc:
metadata_properties = {
key: {"type": "string", "description": desc} for key, desc in self.metadata_desc.items()
}
# Generate dynamic description based on metadata_desc fields
field_descriptions = "\n".join([f" - {key}: {desc}" for key, desc in self.metadata_desc.items()])
metadata_description = f"Optional metadata for the memory. Available fields:\n{field_descriptions}"
properties["metadata"] = {
"type": "object",
"description": self.get_prompt("metadata"),
"description": metadata_description,
"properties": metadata_properties,
}
return properties, required
@ -105,7 +115,12 @@ class UpdateMemory(BaseMemoryTool):
memory_id = mem_dict.get("memory_id", "")
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.add_metadata else {}
# Only extract metadata if metadata_desc is configured
# Convert all metadata values to strings
metadata = {}
if self.metadata_desc:
raw_metadata = mem_dict.get("metadata", {})
metadata = {key: str(value).strip() for key, value in raw_metadata.items() if value}
return memory_id, memory_content, when_to_use, metadata
async def execute(self):

View file

@ -23,23 +23,11 @@ memory_id: |
when_to_use: |
Optional condition description for when to retrieve this memory.
This field is used for vector embedding to improve retrieval accuracy by providing contextual information.
Examples:
- "when user asks about authentication"
- "when deploying to production"
- "when using search_tool"
- "when handling error cases"
memory_content: |
The new content of the memory to store.
Should be a clear, concise statement that captures the updated information to remember.
Keep it focused on a single piece of information for better retrieval accuracy.
metadata: |
Optional metadata for the new memory, providing additional context. Can include:
- time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00")
- source: Where this information came from (e.g., "user_input", "documentation", "observation")
- tags: List of tags for categorization (e.g., ["authentication", "security"])
- Any other custom key-value pairs relevant to the memory
memories: |
A list of memory update objects.

View file

@ -15,12 +15,14 @@ class VectorRetrieveMemory(BaseMemoryTool):
Supports single/multiple query modes via `enable_multiple` parameter.
When `add_memory_type_target` is False, memory_type/memory_target are from context.
Metadata filters can be customized via `metadata_desc` parameter for pre-retrieval filtering.
"""
def __init__(
self,
enable_summary_memory: bool = False,
add_memory_type_target: bool = False,
metadata_desc: dict[str, str] | None = None,
top_k: int = 10,
**kwargs,
):
@ -29,12 +31,20 @@ class VectorRetrieveMemory(BaseMemoryTool):
Args:
enable_summary_memory: Include summary memories in results.
add_memory_type_target: Include memory_type/memory_target in schema (else from context).
metadata_desc: Dictionary defining metadata filter fields and their descriptions.
These fields will be used as filters in vector search before similarity matching.
Example:
{
"year": "The year to filter memories(Optional)",
"month": "The month to filter memories(Optional)",
}
top_k: Max memories to retrieve per query.
**kwargs: Additional args for BaseMemoryTool.
"""
super().__init__(**kwargs)
self.enable_summary_memory: bool = enable_summary_memory
self.add_memory_type_target: bool = add_memory_type_target
self.metadata_desc: dict[str, str] = metadata_desc or {}
self.top_k: int = top_k
def _build_query_schema(self) -> tuple[dict, list[str]]:
@ -69,6 +79,23 @@ class VectorRetrieveMemory(BaseMemoryTool):
}
required.append("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_parameters(self) -> dict:
@ -113,6 +140,7 @@ class VectorRetrieveMemory(BaseMemoryTool):
memory_type: str,
memory_target: str,
query: str,
metadata_filters: dict | None = None,
) -> list[MemoryNode]:
"""Retrieve memories by query using vector similarity search.
@ -120,6 +148,7 @@ class VectorRetrieveMemory(BaseMemoryTool):
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.
@ -133,6 +162,13 @@ class VectorRetrieveMemory(BaseMemoryTool):
"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,
top_k=self.top_k,
@ -189,6 +225,7 @@ class VectorRetrieveMemory(BaseMemoryTool):
for item in query_items:
memory_type = item.get("memory_type") or default_memory_type
memory_target = item.get("memory_target") or default_memory_target
metadata_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}")
@ -198,6 +235,7 @@ class VectorRetrieveMemory(BaseMemoryTool):
memory_type=memory_type,
memory_target=memory_target,
query=item["query"],
metadata_filters=metadata_filters,
)
memories.extend(retrieved)

View file

@ -29,3 +29,4 @@ query: |
query_items: |
A list of query items for vector similarity search.
Each item can include metadata_filters to narrow down search results.