chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 = []
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
Reference in New Issue
Block a user