chore: 添加虚拟环境到仓库

- 添加 backend_service/venv 虚拟环境
- 包含所有Python依赖包
- 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
2025-12-03 10:19:25 +08:00
parent a6c2027caa
commit c4f851d387
12655 changed files with 3009376 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""The memory module."""
from ._in_memory_memory import InMemoryMemory
from ._long_term_memory_base import LongTermMemoryBase
from ._mem0_long_term_memory import Mem0LongTermMemory
from ._memory_base import MemoryBase
from ._reme import (
ReMePersonalLongTermMemory,
ReMeTaskLongTermMemory,
ReMeToolLongTermMemory,
)
__all__ = [
"MemoryBase",
"InMemoryMemory",
"LongTermMemoryBase",
"Mem0LongTermMemory",
"ReMePersonalLongTermMemory",
"ReMeTaskLongTermMemory",
"ReMeToolLongTermMemory",
]

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""The dialogue memory class"""
from typing import Union, Iterable, Any
from ._memory_base import MemoryBase
from ..message import Msg
class InMemoryMemory(MemoryBase):
"""The in-memory memory class for storing messages."""
def __init__(
self,
) -> None:
"""Initialize the in-memory memory object."""
super().__init__()
self.content: list[Msg] = []
def state_dict(self) -> dict:
"""Convert the current memory into JSON data format."""
return {
"content": [_.to_dict() for _ in self.content],
}
def load_state_dict(
self,
state_dict: dict,
strict: bool = True,
) -> None:
"""Load the memory from JSON data.
Args:
state_dict (`dict`):
The state dictionary to load, which should have a "content"
field.
strict (`bool`, defaults to `True`):
If `True`, raises an error if any key in the module is not
found in the state_dict. If `False`, skips missing keys.
"""
self.content = []
for data in state_dict["content"]:
data.pop("type", None)
self.content.append(Msg.from_dict(data))
async def size(self) -> int:
"""The size of the memory."""
return len(self.content)
async def retrieve(self, *args: Any, **kwargs: Any) -> None:
"""Retrieve items from the memory."""
raise NotImplementedError(
"The retrieve method is not implemented in "
f"{self.__class__.__name__} class.",
)
async def delete(self, index: Union[Iterable, int]) -> None:
"""Delete the specified item by index(es).
Args:
index (`Union[Iterable, int]`):
The index to delete.
"""
if isinstance(index, int):
index = [index]
invalid_index = [_ for _ in index if 0 > _ or _ >= len(self.content)]
if invalid_index:
raise IndexError(
f"The index {invalid_index} does not exist.",
)
self.content = [
_ for idx, _ in enumerate(self.content) if idx not in index
]
async def add(
self,
memories: Union[list[Msg], Msg, None],
allow_duplicates: bool = False,
) -> None:
"""Add message into the memory.
Args:
memories (`Union[list[Msg], Msg, None]`):
The message to add.
allow_duplicates (`bool`, defaults to `False`):
If allow adding duplicate messages (with the same id) into
the memory.
"""
if memories is None:
return
if isinstance(memories, Msg):
memories = [memories]
if not isinstance(memories, list):
raise TypeError(
f"The memories should be a list of Msg or a single Msg, "
f"but got {type(memories)}.",
)
for msg in memories:
if not isinstance(msg, Msg):
raise TypeError(
f"The memories should be a list of Msg or a single Msg, "
f"but got {type(msg)}.",
)
if not allow_duplicates:
existing_ids = [_.id for _ in self.content]
memories = [_ for _ in memories if _.id not in existing_ids]
self.content.extend(memories)
async def get_memory(self) -> list[Msg]:
"""Get the memory content."""
return self.content
async def clear(self) -> None:
"""Clear the memory content."""
self.content = []

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""The long-term memory base class."""
from typing import Any
from ..message import Msg
from ..module import StateModule
from ..tool import ToolResponse
class LongTermMemoryBase(StateModule):
"""The long-term memory base class, which should be a time-series
memory management system.
The `record_to_memory` and `retrieve_from_memory` methods are two tool
functions for agent to manage the long-term memory voluntarily. You can
choose not to implement these two functions.
The `record` and `retrieve` methods are for developers to use. For example,
retrieving/recording memory at the beginning of each reply, and adding
the retrieved memory to the system prompt.
"""
async def record(
self,
msgs: list[Msg | None],
**kwargs: Any,
) -> None:
"""A developer-designed method to record information from the given
input message(s) to the long-term memory."""
raise NotImplementedError(
"The `record` method is not implemented. ",
)
async def retrieve(
self,
msg: Msg | list[Msg] | None,
**kwargs: Any,
) -> str:
"""A developer-designed method to retrieve information from the
long-term memory based on the given input message(s). The retrieved
information will be added to the system prompt of the agent."""
raise NotImplementedError(
"The `retrieve` method is not implemented. ",
)
async def record_to_memory(
self,
thinking: str,
content: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Use this function to record important information that you may
need later. The target content should be specific and concise, e.g.
who, when, where, do what, why, how, etc.
Args:
thinking (`str`):
Your thinking and reasoning about what to record
content (`list[str]`):
The content to remember, which is a list of strings.
"""
raise NotImplementedError(
"The `record_to_memory` method is not implemented. "
"You can implement it in your own long-term memory class.",
)
async def retrieve_from_memory(
self,
keywords: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Retrieve the memory based on the given keywords.
Args:
keywords (`list[str]`):
The keywords to search for in the memory, which should be
specific and concise, e.g. the person's name, the date, the
location, etc.
Returns:
`list[Msg]`:
A list of messages that match the keywords.
"""
raise NotImplementedError(
"The `retrieve_from_memory` method is not implemented. "
"You can implement it in your own long-term memory class.",
)

View File

@@ -0,0 +1,574 @@
# -*- coding: utf-8 -*-
"""Long-term memory implementation using mem0 library.
This module provides a long-term memory implementation that integrates
with the mem0 library to provide persistent memory storage and retrieval
capabilities for AgentScope agents.
"""
import json
from typing import Any, TYPE_CHECKING
from importlib import metadata
from pydantic import field_validator
from ..embedding import EmbeddingModelBase
from ._long_term_memory_base import LongTermMemoryBase
from ..message import Msg, TextBlock
from ..model import ChatModelBase
from ..tool import ToolResponse
if TYPE_CHECKING:
from mem0.configs.base import MemoryConfig
from mem0.vector_stores.configs import VectorStoreConfig
else:
MemoryConfig = Any
VectorStoreConfig = Any
def _create_agentscope_config_classes() -> tuple:
"""Create custom config classes for agentscope providers."""
from mem0.embeddings.configs import EmbedderConfig
from mem0.llms.configs import LlmConfig
class _ASLlmConfig(LlmConfig):
"""Custom LLM config class that updates the validate_config method.
Attention: in mem0, the validate_config hardcodes the provider, so we
need to override the validate_config method to support the agentscope
providers. We will follow up with the mem0 to improve this.
"""
@field_validator("config")
@classmethod
def validate_config(cls, v: Any, values: Any) -> Any:
"""Validate the LLM configuration."""
from mem0.utils.factory import LlmFactory
provider = values.data.get("provider")
if provider in LlmFactory.provider_to_class:
return v
raise ValueError(f"Unsupported LLM provider: {provider}")
class _ASEmbedderConfig(EmbedderConfig):
"""Custom embedder config class that updates the validate_config
method."""
@field_validator("config")
@classmethod
def validate_config(cls, v: Any, values: Any) -> Any:
"""Validate the embedder configuration."""
from mem0.utils.factory import EmbedderFactory
provider = values.data.get("provider")
if provider in EmbedderFactory.provider_to_class:
return v
raise ValueError(f"Unsupported Embedder provider: {provider}")
return _ASLlmConfig, _ASEmbedderConfig
class Mem0LongTermMemory(LongTermMemoryBase):
"""A class that implements the LongTermMemoryBase interface using mem0."""
def __init__(
self,
agent_name: str | None = None,
user_name: str | None = None,
run_name: str | None = None,
model: ChatModelBase | None = None,
embedding_model: EmbeddingModelBase | None = None,
vector_store_config: VectorStoreConfig | None = None,
mem0_config: MemoryConfig | None = None,
default_memory_type: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize the Mem0LongTermMemory instance
Args:
agent_name (`str | None`, optional):
The name of the agent. Default is None.
user_name (`str | None`, optional):
The name of the user. Default is None.
run_name (`str | None`, optional):
The name of the run/session. Default is None.
.. note::
1. At least one of `agent_name`, `user_name`, or `run_name` is
required.
2. During memory recording, these parameters become metadata
for the stored memories.
3. **Important**: mem0 will extract memories from messages
containing role of "user" by default. If you want to
extract memories from messages containing role of
"assistant", you need to provide `agent_name`.
4. During memory retrieval, only memories with matching
metadata values will be returned.
model (`ChatModelBase | None`, optional):
The chat model to use for the long-term memory. If
mem0_config is provided, this will override the LLM
configuration. If mem0_config is None, this is required.
embedding_model (`EmbeddingModelBase | None`, optional):
The embedding model to use for the long-term memory. If
mem0_config is provided, this will override the embedder
configuration. If mem0_config is None, this is required.
vector_store_config (`VectorStoreConfig | None`, optional):
The vector store config to use for the long-term memory.
If mem0_config is provided, this will override the vector store
configuration. If mem0_config is None and this is not
provided, defaults to Qdrant with on_disk=True.
mem0_config (`MemoryConfig | None`, optional):
The mem0 config to use for the long-term memory.
If provided, individual
model/embedding_model/vector_store_config parameters will
override the corresponding configurations in mem0_config. If
None, a new MemoryConfig will be created using the provided
parameters.
default_memory_type (`str | None`, optional):
The type of memory to use. Default is None, to create a
semantic memory.
Raises:
`ValueError`:
If `mem0_config` is None and either `model` or
`embedding_model` is None.
"""
super().__init__()
try:
import mem0
from mem0.configs.llms.base import BaseLlmConfig
from mem0.utils.factory import LlmFactory, EmbedderFactory
from packaging import version
# Check mem0 version
current_version = metadata.version("mem0ai")
is_mem0_version_low = version.parse(
current_version,
) <= version.parse("0.1.115")
# Register the agentscope providers with mem0
EmbedderFactory.provider_to_class[
"agentscope"
] = "agentscope.memory._mem0_utils.AgentScopeEmbedding"
if is_mem0_version_low:
# For mem0 version <= 0.1.115, use the old style
LlmFactory.provider_to_class[
"agentscope"
] = "agentscope.memory._mem0_utils.AgentScopeLLM"
else:
# For mem0 version > 0.1.115, use the new style
LlmFactory.provider_to_class["agentscope"] = (
"agentscope.memory._mem0_utils.AgentScopeLLM",
BaseLlmConfig,
)
except ImportError as e:
raise ImportError(
"Please install the mem0 library by `pip install mem0ai`",
) from e
# Create the custom config classes for agentscope providers dynamically
_ASLlmConfig, _ASEmbedderConfig = _create_agentscope_config_classes()
if agent_name is None and user_name is None and run_name is None:
raise ValueError(
"at least one of agent_name, user_name, and run_name is "
"required",
)
# Store agent and user identifiers for memory management
self.agent_id = agent_name
self.user_id = user_name
self.run_id = run_name
# Configuration logic: Handle mem0_config parameter
if mem0_config is not None:
# Case 1: mem0_config is provided - override specific
# configurations if individual params are given
# Override LLM configuration if model is provided
if model is not None:
mem0_config.llm = _ASLlmConfig(
provider="agentscope",
config={"model": model},
)
# Override embedder configuration if embedding_model is provided
if embedding_model is not None:
mem0_config.embedder = _ASEmbedderConfig(
provider="agentscope",
config={"model": embedding_model},
)
# Override vector store configuration if vector_store_config is
# provided
if vector_store_config is not None:
mem0_config.vector_store = vector_store_config
else:
# Case 2: mem0_config is not provided - create new configuration
# from individual parameters
# Validate that required parameters are provided
if model is None or embedding_model is None:
raise ValueError(
"model and embedding_model are required if mem0_config "
"is not provided",
)
# Create new MemoryConfig with provided LLM and embedder
mem0_config = mem0.configs.base.MemoryConfig(
llm=_ASLlmConfig(
provider="agentscope",
config={"model": model},
),
embedder=_ASEmbedderConfig(
provider="agentscope",
config={"model": embedding_model},
),
)
# Set vector store configuration
if vector_store_config is not None:
# Use provided vector store configuration
mem0_config.vector_store = vector_store_config
else:
# Use default Qdrant configuration with on-disk storage for
# persistence set on_disk to True to enable persistence,
# otherwise it will be in memory only
on_disk = kwargs.get("on_disk", True)
mem0_config.vector_store = (
mem0.vector_stores.configs.VectorStoreConfig(
config={"on_disk": on_disk},
)
)
# Initialize the async memory instance with the configured settings
self.long_term_working_memory = mem0.AsyncMemory(mem0_config)
# Store the default memory type for future use
self.default_memory_type = default_memory_type
async def record_to_memory(
self,
thinking: str,
content: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Use this function to record important information that you may
need later. The target content should be specific and concise, e.g.
who, when, where, do what, why, how, etc.
Args:
thinking (`str`):
Your thinking and reasoning about what to record.
content (`list[str]`):
The content to remember, which is a list of strings.
"""
# Multi-strategy recording approach to ensure content persistence:
#
# This method employs a three-tier fallback strategy to maximize
# successful memory recording:
#
# 1. Primary: Record as "user" role message
# - This is the default approach for capturing user-related
# content
# - Mem0 extracts and infers memories from messages containing
# role of "user"
#
# 2. Fallback (if agent_id exists): Record as "assistant" role
# message
# - Triggered when primary recording yields no results
# - In this case, mem0 will use the AGENT_MEMORY_EXTRACTION_PROMPT
# in mem0/mem0/configs/prompts.py to extract memories from
# messages containing role of "assistant", if agent_id is
# provided, otherwise it will use the
# USER_MEMORY_EXTRACTION_PROMPT in mem0/mem0/configs/prompts.py
# to extract memories.
#
# 3. Last resort: Record as "assistant" with infer=False
# - Used when both previous attempts yield no results
# - Bypasses mem0's inference mechanism, which means no
# inference is performed, mem0 will only record the content
# as is.
#
# This graduated approach ensures that even if mem0's inference fails
# to extract meaningful memories, the raw content is still preserved.
try:
if thinking:
content = [thinking] + content
# Strategy 1: Record as user message first
results = await self._mem0_record(
[
{
"role": "user",
"content": "\n".join(content),
"name": "user",
},
],
**kwargs,
)
# Strategy 2: Fallback to assistant message. In this case, if
# agent_id is provided, mem0 will use the
# AGENT_MEMORY_EXTRACTION_PROMPT in mem0/mem0/configs/prompts.py
# to extract memories from messages containing role of
# "assistant". If agent_id is not provided, mem0 will still use
# the USER_MEMORY_EXTRACTION_PROMPT in
# mem0/mem0/configs/prompts.py to extract memories.
if (
results
and isinstance(results, dict)
and "results" in results
and len(results["results"]) == 0
):
results = await self._mem0_record(
[
{
"role": "assistant",
"content": "\n".join(content),
"name": "assistant",
},
],
**kwargs,
)
# Strategy 3: Last resort - direct recording without inference.
# In this case, mem0 will not use any prompts to extract
# memories, it will only record the content as is.
if (
results
and isinstance(results, dict)
and "results" in results
and len(results["results"]) == 0
):
results = await self._mem0_record(
[
{
"role": "assistant",
"content": "\n".join(content),
"name": "assistant",
},
],
infer=False,
**kwargs,
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Successfully recorded content to memory "
f"{results}",
),
],
)
except Exception as e:
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error recording memory: {str(e)}",
),
],
)
async def retrieve_from_memory(
self,
keywords: list[str],
limit: int = 5,
**kwargs: Any,
) -> ToolResponse:
"""Retrieve the memory based on the given keywords.
Args:
keywords (`list[str]`):
The keywords to search for in the memory, which should be
specific and concise, e.g. the person's name, the date, the
location, etc.
limit (`int`, optional):
The maximum number of memories to retrieve per search.
Returns:
`ToolResponse`:
A ToolResponse containing the retrieved memories as JSON text.
"""
try:
results = []
for keyword in keywords:
result = await self.long_term_working_memory.search(
query=keyword,
agent_id=self.agent_id,
user_id=self.user_id,
run_id=self.run_id,
limit=limit,
)
if result:
results.extend(
[item["memory"] for item in result["results"]],
)
return ToolResponse(
content=[
TextBlock(
type="text",
text="\n".join(results),
),
],
)
except Exception as e:
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error retrieving memory: {str(e)}",
),
],
)
async def record(
self,
msgs: list[Msg | None],
memory_type: str | None = None,
infer: bool = True,
**kwargs: Any,
) -> None:
"""Record the content to the long-term memory.
Args:
msgs (`list[Msg | None]`):
The messages to record to memory.
memory_type (`str | None`, optional):
The type of memory to use. Default is None, to create a
semantic memory. "procedural_memory" is explicitly used for
procedural memories.
infer (`bool`, optional):
Whether to infer memory from the content. Default is True.
**kwargs (`Any`):
Additional keyword arguments for the mem0 recording.
"""
if isinstance(msgs, Msg):
msgs = [msgs]
# Filter out None
msg_list = [_ for _ in msgs if _]
if not all(isinstance(_, Msg) for _ in msg_list):
raise TypeError(
"The input messages must be a list of Msg objects.",
)
messages = [
{
"role": "assistant",
"content": "\n".join([str(_.content) for _ in msg_list]),
"name": "assistant",
},
]
await self._mem0_record(
messages,
memory_type=memory_type,
infer=infer,
**kwargs,
)
async def _mem0_record(
self,
messages: str | list[dict],
memory_type: str | None = None,
infer: bool = True,
**kwargs: Any,
) -> dict:
"""Record the content to the long-term memory.
Args:
messages (`str`):
The content to remember, which is a string or a list of
dictionaries representing messages.
memory_type (`str | None`, optional):
The type of memory to use. Default is None, to create a
semantic memory. "procedural_memory" is explicitly used for
procedural memories.
infer (`bool`, optional):
Whether to infer memory from the content. Default is True.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`dict`:
The result from the memory recording operation.
"""
results = await self.long_term_working_memory.add(
messages=messages,
agent_id=self.agent_id,
user_id=self.user_id,
run_id=self.run_id,
memory_type=(
memory_type
if memory_type is not None
else self.default_memory_type
),
infer=infer,
**kwargs,
)
return results
async def retrieve(
self,
msg: Msg | list[Msg] | None,
limit: int = 5,
**kwargs: Any,
) -> str:
"""Retrieve the content from the long-term memory.
Args:
msg (`Msg | list[Msg] | None`):
The message to search for in the memory, which should be
specific and concise, e.g. the person's name, the date, the
location, etc.
limit (`int`, optional):
The maximum number of memories to retrieve per search.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`str`:
The retrieved memory
"""
if isinstance(msg, Msg):
msg = [msg]
if not isinstance(msg, list) or not all(
isinstance(_, Msg) for _ in msg
):
raise TypeError(
"The input message must be a Msg or a list of Msg objects.",
)
msg_strs = [
json.dumps(_.to_dict()["content"], ensure_ascii=False) for _ in msg
]
results = []
for item in msg_strs:
result = await self.long_term_working_memory.search(
query=item,
agent_id=self.agent_id,
user_id=self.user_id,
run_id=self.run_id,
limit=limit,
)
if result:
results.extend([item["memory"] for item in result["results"]])
return "\n".join(results)

View File

@@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
"""Utility classes for integrating AgentScope with mem0 library.
This module provides wrapper classes that allow AgentScope models to be used
with the mem0 library for long-term memory functionality.
"""
import asyncio
from typing import Any, Dict, List, Literal
from mem0.configs.embeddings.base import BaseEmbedderConfig
from mem0.configs.llms.base import BaseLlmConfig
from mem0.embeddings.base import EmbeddingBase
from mem0.llms.base import LLMBase
from ..embedding import EmbeddingModelBase
from ..model import ChatModelBase, ChatResponse
class AgentScopeLLM(LLMBase):
"""Wrapper for the AgentScope LLM.
This class is a wrapper for the AgentScope LLM. It is used to generate
responses using the AgentScope LLM in mem0.
"""
def __init__(self, config: BaseLlmConfig | None = None):
"""Initialize the AgentScopeLLM wrapper.
Args:
config (`BaseLlmConfig | None`, optional):
Configuration object for the LLM. Default is None.
"""
super().__init__(config)
if self.config.model is None:
raise ValueError("`model` parameter is required")
if not isinstance(self.config.model, ChatModelBase):
raise ValueError("`model` must be an instance of ChatModelBase")
self.agentscope_model = self.config.model
def generate_response(
self,
messages: List[Dict[str, str]],
response_format: Any | None = None,
tools: List[Dict] | None = None,
tool_choice: str = "auto",
) -> str:
"""Generate a response based on the given messages using agentscope.
Args:
messages (`List[Dict[str, str]]`):
List of message dicts containing 'role' and 'content'.
response_format (`Any | None`, optional):
Format of the response. Not used in AgentScope.
tools (`List[Dict] | None`, optional):
List of tools that the model can call. Not used in AgentScope.
tool_choice (`str`, optional):
Tool choice method. Not used in AgentScope.
Returns:
`str`:
The generated response.
"""
# pylint: disable=unused-argument
try:
# Convert the messages to AgentScope's format
agentscope_messages = []
for message in messages:
role = message["role"]
content = message["content"]
if role in ["system", "user", "assistant"]:
agentscope_messages.append(
{"role": role, "content": content},
)
if not agentscope_messages:
raise ValueError(
"No valid messages found in the messages list",
)
# Use the agentscope model to generate response (async call)
async def _async_call() -> ChatResponse:
# TODO: handle the streaming response or forbidden streaming
# mode
return await self.agentscope_model( # type: ignore
agentscope_messages,
tools=tools,
)
response = asyncio.run(_async_call())
# Extract text from the response content blocks
if not response.content:
return ""
# Collect all text from different block types
text_parts = []
thinking_parts = []
tool_parts = []
for block in response.content:
# Handle TextBlock
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
# Handle ThinkingBlock
elif (
isinstance(block, dict) and block.get("type") == "thinking"
):
thinking_parts.append(
f"[Thinking: {block.get('thinking', '')}]",
)
# Handle ToolUseBlock
elif (
isinstance(block, dict) and block.get("type") == "tool_use"
):
tool_name = block.get("name")
tool_input = block.get("input", {})
tool_parts.append(
f"[Tool: {tool_name} - {str(tool_input)}]",
)
# Combine all parts in order: thinking, text, tools
all_parts: list[str] = thinking_parts + text_parts + tool_parts
if all_parts:
return "\n".join(all_parts)
# If no recognized blocks found, try to convert the entire
# content to string
return str(response.content)
except Exception as e:
raise RuntimeError(
f"Error generating response using agentscope model: {str(e)}",
) from e
class AgentScopeEmbedding(EmbeddingBase):
"""Wrapper for the AgentScope Embedding model.
This class is a wrapper for the AgentScope Embedding model. It is used
to generate embeddings using the AgentScope Embedding model in mem0.
"""
def __init__(self, config: BaseEmbedderConfig | None = None):
"""Initialize the AgentScopeEmbedding wrapper.
Args:
config (`BaseEmbedderConfig | None`, optional):
Configuration object for the embedder. Default is None.
"""
super().__init__(config)
if self.config.model is None:
raise ValueError("`model` parameter is required")
if not isinstance(self.config.model, EmbeddingModelBase):
raise ValueError(
"`model` must be an instance of EmbeddingModelBase",
)
self.agentscope_model = self.config.model
def embed(
self,
text: str | List[str],
memory_action: Literal[ # pylint: disable=unused-argument
"add",
"search",
"update",
]
| None = None,
) -> List[float]:
"""Get the embedding for the given text using AgentScope.
Args:
text (`str | List[str]`):
The text to embed.
memory_action (`Literal["add", "search", "update"] | None`, \
optional):
The type of embedding to use. Must be one of "add", "search",
or "update". Defaults to None.
Returns:
`List[float]`:
The embedding vector.
"""
try:
# Convert single text to list for AgentScope embedding model
text_list = [text] if isinstance(text, str) else text
# Use the agentscope model to generate embedding (async call)
async def _async_call() -> Any:
response = await self.agentscope_model(text_list)
return response
response = asyncio.run(_async_call())
# Extract the embedding vector from the first Embedding object
# response.embeddings is a list of Embedding objects
# Each Embedding object has an 'embedding' attribute containing
# the vector
embedding = response.embeddings[0]
if embedding is None:
raise ValueError("Failed to extract embedding from response")
return embedding
except Exception as e:
raise RuntimeError(
f"Error generating embedding using agentscope model: {str(e)}",
) from e

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""The memory base class."""
from abc import abstractmethod
from typing import Any
from ..message import Msg
from ..module import StateModule
class MemoryBase(StateModule):
"""The base class for memory in agentscope."""
@abstractmethod
async def add(self, *args: Any, **kwargs: Any) -> None:
"""Add items to the memory."""
@abstractmethod
async def delete(self, *args: Any, **kwargs: Any) -> None:
"""Delete items from the memory."""
@abstractmethod
async def retrieve(self, *args: Any, **kwargs: Any) -> None:
"""Retrieve items from the memory."""
@abstractmethod
async def size(self) -> int:
"""Get the size of the memory."""
@abstractmethod
async def clear(self) -> None:
"""Clear the memory content."""
@abstractmethod
async def get_memory(self, *args: Any, **kwargs: Any) -> list[Msg]:
"""Get the memory content."""
@abstractmethod
def state_dict(self) -> dict:
"""Get the state dictionary of the memory."""
@abstractmethod
def load_state_dict(self, state_dict: dict, strict: bool = True) -> None:
"""Load the state dictionary of the memory."""

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""The reme memory module."""
from ._reme_personal_long_term_memory import ReMePersonalLongTermMemory
from ._reme_task_long_term_memory import ReMeTaskLongTermMemory
from ._reme_tool_long_term_memory import ReMeToolLongTermMemory
__all__ = [
"ReMePersonalLongTermMemory",
"ReMeTaskLongTermMemory",
"ReMeToolLongTermMemory",
]

View File

@@ -0,0 +1,374 @@
# -*- coding: utf-8 -*-
"""Base long-term memory implementation using ReMe library.
This module provides a base class for long-term memory implementations
that integrate with the ReMe library. ReMe enables agents to maintain
persistent, searchable memories across sessions and contexts.
The module handles the integration between AgentScope's memory system and
the ReMe library, including:
- Model configuration and API credential management
- Context lifecycle management (async context managers)
- Graceful handling of missing dependencies
- Error handling with helpful installation instructions
Key Features:
- Supports both DashScope and OpenAI model providers
- Automatic extraction of API credentials and endpoints
- Flexible configuration via config files or kwargs
- Safe fallback behavior when reme_ai is not installed
Dependencies:
The ReMe library is an optional dependency that must be installed:
.. code-block:: bash
pip install reme-ai
Python 3.12 or greater is required to use ReMe.
For more information, visit: https://github.com/modelscope/reMe
Subclasses:
This base class is extended by specific memory type implementations:
- ReMeToolLongTermMemory: For tool execution patterns and guidelines
- ReMeTaskLongTermMemory: For task execution experiences and learnings
- ReMePersonalLongTermMemory: For user preferences and personal information
Example:
.. code-block:: python
from agentscope.models import OpenAIChatModel
from agentscope.embedding import OpenAITextEmbedding
from agentscope.memory._reme import ReMeToolLongTermMemory
# Initialize models
model = OpenAIChatModel(model_name="gpt-4", api_key="...")
embedding = OpenAITextEmbedding(
model_name="text-embedding-3-small", api_key="...")
# Create memory instance
memory = ReMeToolLongTermMemory(
agent_name="my_agent",
user_name="user_123",
model=model,
embedding_model=embedding
)
# Use memory in async context
async with memory:
# Record tool execution
await memory.record_to_memory(
thinking="This tool worked well for data processing",
content=['{"tool_name": "process_data", "success": true, ...}']
)
# Retrieve tool guidelines
result = await memory.retrieve_from_memory(
keywords=["process_data"]
)
"""
from abc import ABCMeta
from typing import Any
from .._long_term_memory_base import LongTermMemoryBase
from ...embedding import (
DashScopeTextEmbedding,
OpenAITextEmbedding,
)
from ...model import (
DashScopeChatModel,
OpenAIChatModel,
)
class ReMeLongTermMemoryBase(LongTermMemoryBase, metaclass=ABCMeta):
"""Base class for ReMe-based long-term memory implementations.
This class provides the foundation for integrating AgentScope with the ReMe
library, enabling agents to maintain and retrieve long-term memories across
different contexts.
The ReMe library must be installed separately:
pip install reme-ai
Requirements:
Python 3.12 or greater is required to use ReMe.
If the library is not installed, a warning will be issued during
initialization,
and runtime errors with installation instructions will be raised
when attempting
to use memory operations.
"""
def __init__(
self,
agent_name: str | None = None,
user_name: str | None = None,
run_name: str | None = None,
model: DashScopeChatModel | OpenAIChatModel | None = None,
embedding_model: (
DashScopeTextEmbedding | OpenAITextEmbedding | None
) = None,
reme_config_path: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize the ReMe-based long-term memory.
This constructor sets up the connection to the ReMe
library and configures
the necessary models for memory operations. The ReMe app
will be initialized
with the provided model configurations.
Args:
agent_name (`str | None`, optional):
Name identifier for the agent. Used for organizing
memories by agent.
user_name (`str | None`, optional):
Unique identifier for the user or workspace. This maps
to workspace_id in ReMe and helps isolate memories across
different users/workspaces.
run_name (`str | None`, optional):
Name identifier for the current execution run or session.
model (`DashScopeChatModel | OpenAIChatModel | None`, optional):
The chat model to use for memory operations. The model's
API credentials and endpoint will be extracted and
passed to ReMe.
embedding_model (`DashScopeTextEmbedding | OpenAITextEmbedding | \
None`, optional):
The embedding model to use for semantic memory retrieval.
The model's API credentials and endpoint will be
extracted and passed to ReMe.
reme_config_path (`str | None`, optional):
Path to a custom ReMe configuration file. If not provided, ReMe
will use its default configuration.
**kwargs (`Any`):
Additional keyword arguments to pass to the
ReMeApp constructor.
These can include custom ReMe configuration parameters.
Raises:
`ValueError`:
If the provided model is not a DashScopeChatModel or
OpenAIChatModel, or if the embedding_model is not a
DashScopeTextEmbedding or OpenAITextEmbedding.
Note:
If the reme_ai library is not installed, a warning will be
issued and self.app will be set to None. Subsequent memory
operations will raise RuntimeError with installation
instructions.
Example:
.. code-block:: python
from agentscope.models import OpenAIChatModel
from agentscope.embedding import OpenAITextEmbedding
from agentscope.memory._reme import ReMeToolLongTermMemory
# Initialize models
model = OpenAIChatModel(
model_name="gpt-4",
api_key="your-api-key"
)
embedding = OpenAITextEmbedding(
model_name="text-embedding-3-small",
api_key="your-api-key"
)
# Create memory instance
memory = ReMeToolLongTermMemory(
agent_name="my_agent",
user_name="user_123",
run_name="session_001",
model=model,
embedding_model=embedding
)
# Use with async context manager
async with memory:
# Memory operations...
pass
"""
super().__init__()
# Store agent and workspace identifiers
self.agent_name = agent_name
# Maps to ReMe's workspace_id concept
self.workspace_id = user_name
self.run_name = run_name
# Build configuration arguments for ReMeApp
# These will be passed as command-line style config overrides
config_args = []
# Extract LLM API credentials based on model type
# DashScope uses a fixed endpoint, OpenAI can have custom base_url
if isinstance(model, DashScopeChatModel):
llm_api_base = "https://dashscope.aliyuncs.com/compatible-mode/v1"
llm_api_key = model.api_key
elif isinstance(model, OpenAIChatModel):
llm_api_base = str(getattr(model.client, "base_url", None))
llm_api_key = str(getattr(model.client, "api_key", None))
else:
raise ValueError(
f"model must be a DashScopeChatModel or "
f"OpenAIChatModel instance. "
f"Got {type(model).__name__} instead.",
)
# Extract model name and add to config if provided
llm_model_name = model.model_name
if llm_model_name:
config_args.append(f"llm.default.model_name={llm_model_name}")
# Extract embedding model API credentials based on type
# Similar to LLM, DashScope uses fixed endpoint,
# OpenAI can be customized
if isinstance(embedding_model, DashScopeTextEmbedding):
embedding_api_base = (
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
embedding_api_key = embedding_model.api_key
elif isinstance(embedding_model, OpenAITextEmbedding):
embedding_api_base = getattr(
embedding_model.client,
"base_url",
None,
)
embedding_api_key = getattr(
embedding_model.client,
"api_key",
None,
)
else:
raise ValueError(
"embedding_model must be a DashScopeTextEmbedding or "
"OpenAITextEmbedding instance. "
f"Got {type(embedding_model).__name__} instead.",
)
# Extract embedding model name and add to config if provided
embedding_model_name = embedding_model.model_name
if embedding_model_name:
config_args.append(
f"embedding_model.default.model_name={embedding_model_name}",
)
# Attempt to import and initialize ReMe
# If import fails, set app to None and issue a warning
# This allows the class to be instantiated even without
# reme_ai installed
try:
from reme_ai import ReMeApp
except ImportError as e:
raise ImportError(
"The 'reme_ai' library is required for ReMe-based "
"long-term memory. Please install it by `pip install reme-ai`,"
"and visit: https://github.com/modelscope/reMe for more "
"information.",
) from e
# Initialize ReMe with extracted configurations
self.app = ReMeApp(
*config_args, # Config overrides as positional 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,
# Optional custom config file
config_path=reme_config_path,
# Additional ReMe-specific configurations
**kwargs,
)
# Track if the app context is active (started via __aenter__)
self._app_started = False
async def __aenter__(self) -> "ReMeLongTermMemoryBase":
"""Async context manager entry point.
This method is called when entering an async context
(using 'async with'). It initializes the ReMe app context if
available, enabling memory operations within the context block.
Returns:
`ReMeLongTermMemoryBase`:
The memory instance itself, allowing it to be used in
the context.
Example:
.. code-block:: python
memory = ReMeToolLongTermMemory(
agent_name="my_agent",
model=model,
embedding_model=embedding
)
async with memory:
# Memory operations can be performed here
await memory.record_to_memory(
thinking="Recording tool usage",
content=[...]
)
"""
if self.app is not None:
await self.app.__aenter__()
self._app_started = True
return self
async def __aexit__(
self,
exc_type: Any,
exc_val: Any,
exc_tb: Any,
) -> None:
"""Async context manager exit point.
This method is called when exiting an async context (at the end
of 'async with' block or when an exception occurs). It properly
cleans up the ReMe app context and resources.
Args:
exc_type (`Any`):
The type of exception that occurred, if any. None if no
exception.
exc_val (`Any`):
The exception instance that occurred, if any. None if no
exception.
exc_tb (`Any`):
The traceback object for the exception, if any. None if
no exception.
.. note:: This method will gracefully handle the case where self.app
is None (reme_ai not installed) by skipping the cleanup but still
marking the app as stopped. It will also always set _app_started
to False, ensuring the memory state is properly reset.
Example:
.. code-block:: python
async with memory:
try:
# Memory operations
await memory.record_to_memory(...)
except Exception as e:
# __aexit__ will be called even if an exception occurs
print(f"Error: {e}")
# __aexit__ has been called and resources are cleaned up
"""
if self.app is not None:
await self.app.__aexit__(exc_type, exc_val, exc_tb)
self._app_started = False

View File

@@ -0,0 +1,410 @@
# -*- coding: utf-8 -*-
"""Personal memory implementation using ReMe library.
This module provides a personal memory implementation that integrates
with the ReMe library to provide persistent personal memory storage and
retrieval capabilities for AgentScope agents.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
from typing import Any
from ._reme_long_term_memory_base import ReMeLongTermMemoryBase
from ..._logging import logger
from ...message import Msg, TextBlock
from ...tool import ToolResponse
class ReMePersonalLongTermMemory(ReMeLongTermMemoryBase):
"""Personal memory implementation using ReMe library.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
async def record_to_memory(
self,
thinking: str,
content: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Record important user information to long-term memory.
Record important user information to long-term memory for future
reference.
Use this function to save user's personal information,
preferences, habits, and facts that you may need in future
conversations. This enables you to provide personalized and
contextually relevant responses.
When to record:
- User shares personal preferences (e.g., "I prefer homestays
when traveling")
- User mentions habits or routines (e.g., "I start work at 9 AM")
- User states likes/dislikes (e.g., "I enjoy drinking green tea")
- User provides personal facts (e.g., "I work as a software
engineer")
What to record: Be specific and structured. Include who, when,
where, what, why, and how when relevant.
Args:
thinking (`str`):
Your reasoning about why this information is worth
recording and how it might be useful later.
content (`list[str]`):
List of specific facts to remember. Each string should be
a clear, standalone piece of information. Examples:
["User prefers homestays in Hangzhou", "User likes
visiting West Lake in the morning"].
**kwargs (`Any`):
Additional keyword arguments for the recording operation.
Returns:
`ToolResponse`:
Confirmation message indicating successful memory
recording.
"""
logger.info(
"[ReMePersonalMemory] Entering record_to_memory - "
"thinking: %s, content: %s, kwargs: %s",
thinking,
content,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Prepare messages for personal memory recording
messages = []
# Add thinking as a user message if provided
if thinking:
messages.append(
{
"role": "user",
"content": thinking,
},
)
# Add content items as user messages
for item in content:
messages.append(
{
"role": "user",
"content": item,
},
)
# Add a simple assistant acknowledgment
messages.append(
{
"role": "assistant",
"content": (
"I understand and will remember this "
"information."
),
},
)
result = await self.app.async_execute(
name="summary_personal_memory",
workspace_id=self.workspace_id,
trajectories=[
{
"messages": messages,
},
],
**kwargs,
)
# Extract metadata about stored memories if available
metadata = result.get("metadata", {})
memory_list = metadata.get("memory_list", [])
if memory_list:
summary_text = (
f"Successfully recorded {len(memory_list)} "
f"memory/memories to personal memory."
)
else:
summary_text = "Memory recording completed."
return ToolResponse(
content=[
TextBlock(
type="text",
text=summary_text,
),
],
metadata={"result": result},
)
except Exception as e:
logger.exception("Error recording memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error recording memory: {str(e)}",
),
],
)
async def retrieve_from_memory(
self,
keywords: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Search and retrieve relevant information from long-term memory.
.. note:: You should call this function BEFORE answering
questions about the user's preferences, past information, or
personal details. This ensures you provide accurate information
based on stored memories rather than guessing.
Use this when:
- User asks "what do I like?", "what are my preferences?",
"what do you know about me?"
- User asks about their past behaviors, habits, or stated
preferences
- User refers to information they shared in previous
conversations
- You need to personalize responses based on user's history
Args:
keywords (`list[str]`):
Keywords to search for in memory. Be specific and use
multiple keywords for better results. Examples:
["travel preferences", "Hangzhou"], ["work habits",
"morning routine"], ["food preferences", "tea"].
**kwargs (`Any`):
Additional keyword arguments for the retrieval operation.
Returns:
`ToolResponse`:
Retrieved memories matching the keywords. If no memories
found, you'll receive a message indicating that.
"""
logger.info(
"[ReMePersonalMemory] Entering retrieve_from_memory - "
"keywords: %s, kwargs: %s",
keywords,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
results = []
# Search for each keyword
limit = kwargs.get("limit", 3)
for keyword in keywords:
result = await self.app.async_execute(
name="retrieve_personal_memory",
workspace_id=self.workspace_id,
query=keyword,
top_k=limit,
**kwargs,
)
# Extract the answer from the result
answer = result.get("answer", "")
if answer:
results.append(f"Keyword '{keyword}':\n{answer}")
# Combine all results
if results:
combined_text = "\n\n".join(results)
else:
combined_text = "No memories found for the given keywords."
return ToolResponse(
content=[
TextBlock(
type="text",
text=combined_text,
),
],
)
except Exception as e:
logger.exception("Error retrieving memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error retrieving memory: {str(e)}",
),
],
)
async def record(
self,
msgs: list[Msg | None],
**kwargs: Any,
) -> None:
"""Record the content to the long-term memory.
This method converts AgentScope messages to ReMe's format and
records them using the personal memory flow.
Args:
msgs (`list[Msg | None]`):
The messages to record to memory.
**kwargs (`Any`):
Additional keyword arguments for the mem0 recording.
"""
if isinstance(msgs, Msg):
msgs = [msgs]
# Filter out None
msg_list = [_ for _ in msgs if _]
if not msg_list:
return
if not all(isinstance(_, Msg) for _ in msg_list):
raise TypeError(
"The input messages must be a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Convert AgentScope messages to ReMe format
messages = []
for msg in msg_list:
# Extract content as string
if isinstance(msg.content, str):
content_str = msg.content
elif isinstance(msg.content, list):
# Join content blocks into a single string
content_parts = []
for block in msg.content:
if isinstance(block, dict) and "text" in block:
content_parts.append(block["text"])
elif isinstance(block, dict) and "thinking" in block:
content_parts.append(block["thinking"])
content_str = "\n".join(content_parts)
else:
content_str = str(msg.content)
messages.append(
{
"role": msg.role,
"content": content_str,
},
)
await self.app.async_execute(
name="summary_personal_memory",
workspace_id=self.workspace_id,
trajectories=[
{
"messages": messages,
},
],
**kwargs,
)
except Exception as e:
# Log the error but don't raise to maintain compatibility
logger.exception("Error recording messages to memory: %s", str(e))
import warnings
warnings.warn(f"Error recording messages to memory: {str(e)}")
async def retrieve(
self,
msg: Msg | list[Msg] | None,
**kwargs: Any,
) -> str:
"""Retrieve the content from the long-term memory.
Args:
msg (`Msg | list[Msg] | None`):
The message to search for in the memory, which should be
specific and concise, e.g. the person's name, the date, the
location, etc.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`str`:
The retrieved memory as a string.
"""
if msg is None:
return ""
if isinstance(msg, Msg):
msg = [msg]
if not isinstance(msg, list) or not all(
isinstance(_, Msg) for _ in msg
):
raise TypeError(
"The input message must be a Msg or a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Only use the last message's content for retrieval
last_msg = msg[-1]
query = ""
if isinstance(last_msg.content, str):
query = last_msg.content
elif isinstance(last_msg.content, list):
# Extract text from content blocks
content_parts = []
for block in last_msg.content:
if isinstance(block, dict) and "text" in block:
content_parts.append(block["text"])
elif isinstance(block, dict) and "thinking" in block:
content_parts.append(block["thinking"])
query = "\n".join(content_parts)
if not query:
return ""
# Retrieve using the query from the last message
# Extract top_k from kwargs if available, default to 3
top_k = kwargs.get("top_k", 3)
result = await self.app.async_execute(
name="retrieve_personal_memory",
workspace_id=self.workspace_id,
query=query,
top_k=top_k,
**kwargs,
)
return result.get("answer", "")
except Exception as e:
logger.exception("Error retrieving memory: %s", str(e))
import warnings
warnings.warn(f"Error retrieving memory: {str(e)}")
return ""

View File

@@ -0,0 +1,429 @@
# -*- coding: utf-8 -*-
"""Task memory implementation using ReMe library.
This module provides a task memory implementation that integrates
with the ReMe library to learn from execution trajectories and
retrieve relevant task experiences.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
from typing import Any
from ._reme_long_term_memory_base import ReMeLongTermMemoryBase
from ..._logging import logger
from ...message import Msg, TextBlock
from ...tool import ToolResponse
class ReMeTaskLongTermMemory(ReMeLongTermMemoryBase):
"""Task memory implementation using ReMe library.
Task memory learns from execution trajectories and provides
retrieval of relevant task experiences.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
async def record_to_memory(
self,
thinking: str,
content: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Record task execution experiences and learnings.
Record task execution experiences and learnings to long-term
memory.
Use this function to save valuable task-related knowledge that
can help with future similar tasks. This enables learning from
experience and improving over time.
When to record:
- After solving technical problems or completing tasks
- When discovering useful techniques or approaches
- After implementing solutions with specific steps
- When learning best practices or important lessons
What to record: Be detailed and actionable. Include:
- Task description and context
- Step-by-step execution details
- Specific techniques and methods used
- Results, outcomes, and effectiveness
- Lessons learned and considerations
Args:
thinking (`str`):
Your reasoning about why this task experience is valuable
and what makes it worth remembering for future reference.
content (`list[str]`):
List of specific task insights to remember. Each string
should be a clear, actionable piece of information.
Examples: ["Add indexes on WHERE clause columns to speed
up queries", "Use EXPLAIN ANALYZE to identify missing
indexes"].
**kwargs (`Any`):
Additional keyword arguments. Can include 'score' (float)
to indicate the quality/success of this approach
(default: 1.0).
Returns:
`ToolResponse`:
Confirmation message indicating successful memory
recording.
"""
logger.info(
"[ReMeTaskMemory] Entering record_to_memory - "
"thinking: %s, content: %s, kwargs: %s",
thinking,
content,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Prepare messages for task memory recording
messages = []
# Add thinking as a user message if provided
if thinking:
messages.append(
{
"role": "user",
"content": thinking,
},
)
# Add content items as user-assistant pairs
for item in content:
messages.append(
{
"role": "user",
"content": item,
},
)
# Add a simple assistant acknowledgment
messages.append(
{
"role": "assistant",
"content": "Task information recorded.",
},
)
result = await self.app.async_execute(
name="summary_task_memory",
workspace_id=self.workspace_id,
trajectories=[
{
"messages": messages,
"score": kwargs.pop("score", 1.0),
},
],
**kwargs,
)
# Extract metadata if available
summary_text = (
f"Successfully recorded {len(content)} task memory/memories."
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=summary_text,
),
],
metadata={"result": result},
)
except Exception as e:
logger.exception("Error recording task memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error recording task memory: {str(e)}",
),
],
)
async def retrieve_from_memory(
self,
keywords: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Search and retrieve relevant task experiences.
Search and retrieve relevant task experiences from long-term
memory.
IMPORTANT: You should call this function BEFORE attempting to
solve problems or answer technical questions. This ensures you
leverage experiences and proven solutions rather than
starting from scratch.
Use this when:
- Asked to solve a technical problem or implement a solution
- Asked for recommendations, best practices, or approaches
- Asked "what do you know about...?" or "have you seen this
before?"
- Dealing with tasks that may be similar to experiences
- Need to recall specific techniques or methods
Benefits of retrieving first:
- Learn from past successes and mistakes
- Provide more accurate, battle-tested solutions
- Avoid reinventing the wheel
- Give consistent, informed recommendations
Args:
keywords (`list[str]`):
Keywords describing the task or problem domain. Be
specific and use technical terms. Examples:
["database optimization", "slow queries"], ["API design",
"rate limiting"], ["code refactoring", "Python"].
**kwargs (`Any`):
Additional keyword arguments. Can include 'top_k' (int)
to specify number of experiences to retrieve
(default: 3).
Returns:
`ToolResponse`:
Retrieved task experiences and learnings. If no relevant
experiences found, you'll receive a message indicating
that.
"""
logger.info(
"[ReMeTaskMemory] Entering retrieve_from_memory - "
"keywords: %s, kwargs: %s",
keywords,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
results = []
# Search for each keyword
top_k = kwargs.get("top_k", 3)
for keyword in keywords:
result = await self.app.async_execute(
name="retrieve_task_memory",
workspace_id=self.workspace_id,
query=keyword,
top_k=top_k,
**kwargs,
)
# Extract the answer from the result
answer = result.get("answer", "")
if answer:
results.append(f"Keyword '{keyword}':\n{answer}")
# Combine all results
if results:
combined_text = "\n\n".join(results)
else:
combined_text = (
"No task experiences found for the given keywords."
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=combined_text,
),
],
)
except Exception as e:
logger.exception("Error retrieving task memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error retrieving task memory: {str(e)}",
),
],
)
async def record(
self,
msgs: list[Msg | None],
**kwargs: Any,
) -> None:
"""Record the content to the task memory.
This method converts AgentScope messages to ReMe's format and
records them as a task execution trajectory.
Args:
msgs (`list[Msg | None]`):
The messages to record to memory.
**kwargs (`Any`):
Additional keyword arguments for the recording.
Can include 'score' (float) for trajectory scoring
(default: 1.0).
"""
if isinstance(msgs, Msg):
msgs = [msgs]
# Filter out None
msg_list = [_ for _ in msgs if _]
if not msg_list:
return
if not all(isinstance(_, Msg) for _ in msg_list):
raise TypeError(
"The input messages must be a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Convert AgentScope messages to ReMe format
messages = []
for msg in msg_list:
# Extract content as string
if isinstance(msg.content, str):
content_str = msg.content
elif isinstance(msg.content, list):
# Join content blocks into a single string
content_parts = []
for block in msg.content:
if isinstance(block, dict) and "text" in block:
content_parts.append(block["text"])
elif isinstance(block, dict) and "thinking" in block:
content_parts.append(block["thinking"])
content_str = "\n".join(content_parts)
else:
content_str = str(msg.content)
messages.append(
{
"role": msg.role,
"content": content_str,
},
)
# Extract score from kwargs if provided, default to 1.0
score = kwargs.pop("score", 1.0)
await self.app.async_execute(
name="summary_task_memory",
workspace_id=self.workspace_id,
trajectories=[
{
"messages": messages,
"score": score,
},
],
**kwargs,
)
except Exception as e:
# Log the error but don't raise to maintain compatibility
logger.exception(
"Error recording messages to task memory: %s",
str(e),
)
import warnings
warnings.warn(
f"Error recording messages to task memory: {str(e)}",
)
async def retrieve(
self,
msg: Msg | list[Msg] | None,
**kwargs: Any,
) -> str:
"""Retrieve relevant task experiences from memory.
Args:
msg (`Msg | list[Msg] | None`):
The message to search for relevant task experiences.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`str`:
The retrieved task experiences as a string.
"""
if msg is None:
return ""
if isinstance(msg, Msg):
msg = [msg]
if not isinstance(msg, list) or not all(
isinstance(_, Msg) for _ in msg
):
raise TypeError(
"The input message must be a Msg or a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Only use the last message's content for retrieval
last_msg = msg[-1]
query = ""
if isinstance(last_msg.content, str):
query = last_msg.content
elif isinstance(last_msg.content, list):
# Extract text from content blocks
content_parts = []
for block in last_msg.content:
if isinstance(block, dict) and "text" in block:
content_parts.append(block["text"])
elif isinstance(block, dict) and "thinking" in block:
content_parts.append(block["thinking"])
query = "\n".join(content_parts)
if not query:
return ""
# Retrieve using the query from the last message
top_k = kwargs.get("top_k", 3)
result = await self.app.async_execute(
name="retrieve_task_memory",
workspace_id=self.workspace_id,
query=query,
top_k=top_k,
**kwargs,
)
return result.get("answer", "")
except Exception as e:
logger.exception("Error retrieving task memory: %s", str(e))
import warnings
warnings.warn(f"Error retrieving task memory: {str(e)}")
return ""

View File

@@ -0,0 +1,534 @@
# -*- coding: utf-8 -*-
"""Tool memory implementation using ReMe library.
This module provides a tool memory implementation that integrates
with the ReMe library to record tool execution results and retrieve
tool usage guidelines.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
from typing import Any
from ._reme_long_term_memory_base import ReMeLongTermMemoryBase
from ..._logging import logger
from ...message import Msg, TextBlock
from ...tool import ToolResponse
class ReMeToolLongTermMemory(ReMeLongTermMemoryBase):
"""Tool memory implementation using ReMe library.
Tool memory records tool execution results and generates usage
guidelines from the execution history.
Requirements:
Python 3.12 or greater is required to use ReMe.
"""
async def record_to_memory(
self,
thinking: str,
content: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Record tool execution results to build tool usage patterns.
Record tool execution results to build a knowledge base of tool
usage patterns.
Use this function after successfully using tools to capture
execution details, results, and performance metrics. Over time,
this builds comprehensive usage guidelines and best practices
for each tool.
When to record:
- After successfully executing any tool
- After tool failures (to learn what doesn't work)
- When discovering effective parameter combinations
- After noteworthy tool usage patterns
What to record: Each tool execution should include complete
execution details.
Args:
thinking (`str`):
Your reasoning about why this tool execution is worth
recording. Mention what worked well, what could be
improved, or lessons learned.
content (`list[str]`):
List of JSON strings, each representing a tool execution.
Each JSON must have these fields:
- create_time: Timestamp in format "YYYY-MM-DD HH:MM:SS"
- tool_name: Name of the tool executed
- input: Input parameters as a dict
- output: Tool's output as a string
- token_cost: Token cost (integer)
- success: Whether execution succeeded (boolean)
- time_cost: Execution time in seconds (float)
Example: '{"create_time": "2024-01-01 10:00:00",
"tool_name": "search", "input": {"query": "Python"},
"output": "Found 10 results", "token_cost": 100,
"success": true, "time_cost": 1.2}'
**kwargs (`Any`):
Additional keyword arguments for the recording operation.
Returns:
`ToolResponse`:
Confirmation message with number of executions recorded
and guidelines generated.
"""
logger.info(
"[ReMeToolMemory] Entering record_to_memory - "
"thinking: %s, content: %s, kwargs: %s",
thinking,
content,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
import json
# Parse each content item as a tool_call_result
tool_call_results = []
tool_names_set = set()
for item in content:
try:
# Parse JSON string to dict
tool_call_result = json.loads(item)
tool_call_results.append(tool_call_result)
# Track tool names for summary
if "tool_name" in tool_call_result:
tool_names_set.add(tool_call_result["tool_name"])
except json.JSONDecodeError as e:
# Skip invalid JSON items
import warnings
warnings.warn(
f"Failed to parse tool call result JSON: {item}. "
f"Error: {str(e)}",
)
continue
if not tool_call_results:
return ToolResponse(
content=[
TextBlock(
type="text",
text="No valid tool call results to record.",
),
],
)
# First, add the tool call results
await self.app.async_execute(
name="add_tool_call_result",
workspace_id=self.workspace_id,
tool_call_results=tool_call_results,
**kwargs,
)
# Then, summarize the tool memory for the affected tools
if tool_names_set:
tool_names_list = list(tool_names_set)
await self.app.async_execute(
name="summary_tool_memory",
workspace_id=self.workspace_id,
tool_names=tool_names_list,
**kwargs,
)
num_results = len(tool_call_results)
summary_text = (
f"Successfully recorded {num_results} tool execution "
f"result{'s' if num_results > 1 else ''} and generated "
f"usage guidelines."
)
return ToolResponse(
content=[
TextBlock(
type="text",
text=summary_text,
),
],
)
except Exception as e:
logger.exception("Error recording tool memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error recording tool memory: {str(e)}",
),
],
)
async def retrieve_from_memory(
self,
keywords: list[str],
**kwargs: Any,
) -> ToolResponse:
"""Retrieve usage guidelines and best practices for tools.
Retrieve usage guidelines and best practices for specific tools.
.. note:: You should call this function BEFORE using a tool,
especially if you're uncertain about its proper usage or want to
follow established best practices. This retrieves synthesized
guidelines based on past tool executions.
Use this when:
- About to use a tool and want to know the best practices
- Uncertain about tool parameters or usage patterns
- Want to learn from past successful/failed tool executions
- User asks "how should I use this tool?" or "what's the best
way to..."
- Need to understand tool performance characteristics or
limitations
Benefits of retrieving first:
- Learn from accumulated tool usage experience
- Avoid common mistakes and pitfalls
- Use optimal parameter combinations
- Understand tool performance and cost characteristics
- Follow established best practices
Args:
keywords (`list[str]`):
List of tool names to retrieve guidelines for. Use the
exact tool names. Examples: ["search"],
["database_query", "cache_get"], ["api_call"].
**kwargs (`Any`):
Additional keyword arguments for the retrieval operation.
Returns:
`ToolResponse`:
Retrieved usage guidelines and best practices for the
specified tools. If no guidelines exist yet, you'll
receive a message indicating that.
"""
logger.info(
"[ReMeToolMemory] Entering retrieve_from_memory - "
"keywords: %s, kwargs: %s",
keywords,
kwargs,
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Join all tool names with comma
tool_names = ",".join(keywords)
# Retrieve tool guidelines for all tools at once
result = await self.app.async_execute(
name="retrieve_tool_memory",
workspace_id=self.workspace_id,
tool_names=tool_names,
**kwargs,
)
# Extract the answer from the result
answer = result.get("answer", "")
if answer:
combined_text = answer
else:
combined_text = f"No tool guidelines found for: {tool_names}"
return ToolResponse(
content=[
TextBlock(
type="text",
text=combined_text,
),
],
)
except Exception as e:
logger.exception("Error retrieving tool memory: %s", str(e))
return ToolResponse(
content=[
TextBlock(
type="text",
text=f"Error retrieving tool memory: {str(e)}",
),
],
)
def _extract_content_from_messages(self, msg_list: list[Msg]) -> list[str]:
"""Extract content strings from messages.
Args:
msg_list (`list[Msg]`):
List of messages to extract content from.
Returns:
`list[str]`:
List of extracted content strings.
"""
content_list = []
for msg in msg_list:
if isinstance(msg.content, str):
content_list.append(msg.content)
elif isinstance(msg.content, list):
content_list.extend(
self._extract_text_from_blocks(msg.content),
)
return content_list
def _extract_text_from_blocks(self, blocks: list) -> list[str]:
"""Extract text from content blocks.
Args:
blocks (`list`):
List of content blocks.
Returns:
`list[str]`:
List of extracted text strings.
"""
texts = []
for block in blocks:
if isinstance(block, dict) and block.get("type") == "text":
texts.append(block.get("text", ""))
elif isinstance(block, str):
texts.append(block)
return texts
def _parse_tool_call_results(
self,
content_list: list[str],
) -> tuple[list[dict], set[str]]:
"""Parse JSON content strings into tool call results.
Args:
content_list (`list[str]`):
List of JSON strings to parse.
Returns:
`tuple[list[dict], set[str]]`:
Tuple of (tool_call_results, tool_names_set).
"""
import json
import warnings
tool_call_results = []
tool_names_set = set()
for item in content_list:
try:
tool_call_result = json.loads(item)
tool_call_results.append(tool_call_result)
if "tool_name" in tool_call_result:
tool_names_set.add(tool_call_result["tool_name"])
except json.JSONDecodeError as e:
warnings.warn(
f"Failed to parse tool call result JSON: {item}. "
f"Error: {str(e)}",
)
return tool_call_results, tool_names_set
async def record(
self,
msgs: list[Msg | None],
**kwargs: Any,
) -> None:
"""Record the content to the tool memory.
This method extracts content from messages and treats them as
JSON strings representing tool_call_results, similar to
record_to_memory.
Args:
msgs (`list[Msg | None]`):
The messages to record to memory. Each message's content
should be a JSON string or list of JSON strings
representing tool_call_results.
**kwargs (`Any`):
Additional keyword arguments for the recording.
"""
if isinstance(msgs, Msg):
msgs = [msgs]
# Filter out None
msg_list = [_ for _ in msgs if _]
if not msg_list:
return
if not all(isinstance(_, Msg) for _ in msg_list):
raise TypeError(
"The input messages must be a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Extract content from messages and parse as tool_call_results
content_list = self._extract_content_from_messages(msg_list)
if not content_list:
return
# Parse each content item as a tool_call_result
tool_call_results, tool_names_set = self._parse_tool_call_results(
content_list,
)
if not tool_call_results:
return
# First, add the tool call results
await self.app.async_execute(
name="add_tool_call_result",
workspace_id=self.workspace_id,
tool_call_results=tool_call_results,
**kwargs,
)
# Then, summarize the tool memory for the affected tools
if tool_names_set:
tool_names_list = list(tool_names_set)
await self.app.async_execute(
name="summary_tool_memory",
workspace_id=self.workspace_id,
tool_names=tool_names_list,
**kwargs,
)
except Exception as e:
# Log the error but don't raise to maintain compatibility
logger.exception(
"Error recording tool messages to memory: %s",
str(e),
)
import warnings
warnings.warn(
f"Error recording tool messages to memory: {str(e)}",
)
def _extract_tool_names_from_message(self, msg: Msg) -> str:
"""Extract tool names from a message.
Args:
msg (`Msg`):
Message to extract tool names from.
Returns:
`str`:
Extracted tool names as a string.
"""
if isinstance(msg.content, str):
return msg.content
if isinstance(msg.content, list):
content_parts = []
for block in msg.content:
if isinstance(block, dict) and "text" in block:
content_parts.append(block["text"])
return " ".join(content_parts)
return ""
def _format_retrieve_result(self, result: Any) -> str:
"""Format the retrieve result into a string.
Args:
result (`Any`):
Result from the retrieve operation.
Returns:
`str`:
Formatted result string.
"""
if isinstance(result, dict) and "answer" in result:
return result["answer"]
if isinstance(result, str):
return result
return str(result)
async def retrieve(
self,
msg: Msg | list[Msg] | None,
**kwargs: Any,
) -> str:
"""Retrieve tool guidelines from memory.
Retrieve tool guidelines from memory based on message content.
Args:
msg (`Msg | list[Msg] | None`):
The message containing tool names or queries to
retrieve guidelines for.
**kwargs (`Any`):
Additional keyword arguments.
Returns:
`str`:
The retrieved tool guidelines as a string.
"""
if msg is None:
return ""
if isinstance(msg, Msg):
msg = [msg]
if not isinstance(msg, list) or not all(
isinstance(_, Msg) for _ in msg
):
raise TypeError(
"The input message must be a Msg or a list of Msg objects.",
)
if not self._app_started:
raise RuntimeError(
"ReMeApp context not started. "
"Please use 'async with' to initialize the app.",
)
try:
# Extract tool names from the last message
last_msg = msg[-1]
tool_names = self._extract_tool_names_from_message(last_msg)
if not tool_names:
return ""
# Retrieve tool guidelines
result = await self.app.async_execute(
name="retrieve_tool_memory",
workspace_id=self.workspace_id,
tool_names=tool_names,
**kwargs,
)
return self._format_retrieve_result(result)
except Exception as e:
logger.exception("Error retrieving tool guidelines: %s", str(e))
import warnings
warnings.warn(f"Error retrieving tool guidelines: {str(e)}")
return ""