diff --git a/reme_ai/core/__init__.py b/reme_ai/core/__init__.py index f04f16ec..8eab5792 100644 --- a/reme_ai/core/__init__.py +++ b/reme_ai/core/__init__.py @@ -3,7 +3,6 @@ # pylint: disable=wrong-import-position # flake8: noqa: F401 -from . import agent from . import config from . import context from . import embedding @@ -14,6 +13,5 @@ from . import op from . import schema from . import service from . import token_counter -from . import tool from . import utils from . import vector_store diff --git a/reme_ai/core/application.py b/reme_ai/core/application.py index 285fb78a..c0561705 100644 --- a/reme_ai/core/application.py +++ b/reme_ai/core/application.py @@ -38,6 +38,15 @@ class Application: Initialize the Application with configuration settings. Args: + *args: Additional arguments passed to parser. Examples: + - "llm.default.model_name=qwen3-30b-a3b-thinking-2507" + - "llm.default.backend=openai_compatible" + - "llm.default.temperature=0.6" + - "embedding_model.default.model_name=text-embedding-v4" + - "embedding_model.default.backend=openai_compatible" + - "embedding_model.default.dimensions=1024" + - "vector_store.default.backend=memory" + - "vector_store.default.embedding_model=default" llm_api_key: API key for LLM service llm_api_base: Base URL for LLM service embedding_api_key: API key for embedding service @@ -50,7 +59,8 @@ class Application: embedding_model: Embedding model configuration dictionary vector_store: Vector store configuration dictionary token_counter: Token counter configuration dictionary - **kwargs: Additional configuration arguments + **kwargs: Additional keyword arguments passed to parser. Same format as args but as kwargs. Examples: + - **{"llm.default.model_name": "qwen3-30b-a3b-thinking-2507"} """ load_env() @@ -89,25 +99,25 @@ class Application: C.print_logo() @staticmethod - def _update_env(key: str, value: str | None) -> None: + def _update_env(key: str, value: str | None): """Update environment variable if value is provided.""" if value: os.environ[key] = value @staticmethod - async def start() -> None: + async def start(): """Initialize the service context and prepare external MCP servers.""" C.initialize_service_context() await C.prepare_mcp_servers() @staticmethod - def start_sync() -> None: + def start_sync(): """Synchronous version of start().""" C.initialize_service_context() run_coro_safely(C.prepare_mcp_servers()) @staticmethod - async def stop(wait_thread_pool: bool = True, wait_ray: bool = True) -> None: + async def stop(wait_thread_pool: bool = True, wait_ray: bool = True): """ Stop the application and cleanup resources. @@ -120,7 +130,7 @@ class Application: C.shutdown_ray(wait=wait_ray) @staticmethod - def stop_sync(wait_thread_pool: bool = True, wait_ray: bool = True) -> None: + def stop_sync(wait_thread_pool: bool = True, wait_ray: bool = True): """Synchronous version of stop().""" C.close_sync() C.shutdown_thread_pool(wait=wait_thread_pool) diff --git a/reme_ai/core/context/service_context.py b/reme_ai/core/context/service_context.py index c5bcde0c..e22147e9 100644 --- a/reme_ai/core/context/service_context.py +++ b/reme_ai/core/context/service_context.py @@ -60,6 +60,9 @@ class ServiceContext(BaseContext): # MCP server mapping: maps server_name -> {tool_name: ToolCall} self.mcp_server_mapping: dict[str, dict] = {} + # Initialization flag: ensures initialize_service_context is called only once + self._initialized: bool = False + def register(self, name: str, register_type: RegistryEnum): """Return a decorator to register a component within a specific registry category. @@ -268,7 +271,12 @@ class ServiceContext(BaseContext): 9. Service backend instance Note: This method should be called after service_config is set. + This method can only be called once. Subsequent calls will be ignored. """ + if self._initialized: + logger.warning("initialize_service_context has already been called. Skipping re-initialization.") + return + self.language = self.service_config.language self.thread_pool = ThreadPoolExecutor(max_workers=self.service_config.thread_pool_max_workers) @@ -286,6 +294,9 @@ class ServiceContext(BaseContext): self._initialize_flow() self._initialize_service() + # Mark as initialized + self._initialized = True + def _initialize_llm(self): """Initialize all configured LLM instances. diff --git a/reme_ai/core/op/base_op.py b/reme_ai/core/op/base_op.py index bc62be87..4c82b64c 100644 --- a/reme_ai/core/op/base_op.py +++ b/reme_ai/core/op/base_op.py @@ -40,7 +40,7 @@ class BaseOp: token_counter: str | BaseTokenCounter = "default", enable_cache: bool = False, cache_path: str = "cache/op", - cache_expire_hours: float = 0.1, + cache_expire_hours: float | None = None, sub_ops: dict[str, "BaseOp"] | list["BaseOp"] | Optional["BaseOp"] = None, input_mapping: dict[str, str] | None = None, output_mapping: dict[str, str] | None = None, diff --git a/reme_ai/core/tool/__init__.py b/reme_ai/core/tool/__init__.py deleted file mode 100644 index 2b6bf84b..00000000 --- a/reme_ai/core/tool/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""tool""" - -from .mcp_tool import MCPTool -from . import search - -__all__ = [ - "MCPTool", - "search", -] diff --git a/reme_ai/core/utils/__init__.py b/reme_ai/core/utils/__init__.py index a3f018de..242f7be9 100644 --- a/reme_ai/core/utils/__init__.py +++ b/reme_ai/core/utils/__init__.py @@ -6,7 +6,7 @@ from .common_utils import run_coro_safely, execute_stream_task from .env_utils import load_env from .execute_tuils import exec_code, run_shell_command from .http_client import HttpClient -from .llm_utils import extract_content, format_messages +from .llm_utils import extract_content, format_messages, deduplicate_memories from .logger_utils import init_logger from .logo_utils import print_logo from .mcp_client import MCPClient @@ -27,6 +27,7 @@ __all__ = [ "HttpClient", "extract_content", "format_messages", + "deduplicate_memories", "init_logger", "print_logo", "MCPClient", diff --git a/reme_ai/core/agent/__init__.py b/reme_ai/mem_agent/__init__.py similarity index 100% rename from reme_ai/core/agent/__init__.py rename to reme_ai/mem_agent/__init__.py diff --git a/reme_ai/core/agent/simple_chat.py b/reme_ai/mem_agent/simple_chat.py similarity index 94% rename from reme_ai/core/agent/simple_chat.py rename to reme_ai/mem_agent/simple_chat.py index 8dd1b8db..0ba33c97 100644 --- a/reme_ai/core/agent/simple_chat.py +++ b/reme_ai/mem_agent/simple_chat.py @@ -2,10 +2,10 @@ from loguru import logger -from ..context import C -from ..enumeration import Role -from ..op import BaseOp -from ..schema import Message, ToolCall +from ..core.context import C +from ..core.enumeration import Role +from ..core.op import BaseOp +from ..core.schema import Message, ToolCall @C.register_op() diff --git a/reme_ai/core/agent/stream_chat.py b/reme_ai/mem_agent/stream_chat.py similarity index 94% rename from reme_ai/core/agent/stream_chat.py rename to reme_ai/mem_agent/stream_chat.py index 7676ed04..95ee6bb5 100644 --- a/reme_ai/core/agent/stream_chat.py +++ b/reme_ai/mem_agent/stream_chat.py @@ -2,10 +2,10 @@ from loguru import logger -from ..context import C -from ..enumeration import Role, ChunkEnum -from ..op import BaseOp -from ..schema import Message, ToolCall +from ..core.context import C +from ..core.enumeration import Role, ChunkEnum +from ..core.op import BaseOp +from ..core.schema import Message, ToolCall @C.register_op() diff --git a/reme_ai/tool/__init__.py b/reme_ai/tool/__init__.py new file mode 100644 index 00000000..d7d25f85 --- /dev/null +++ b/reme_ai/tool/__init__.py @@ -0,0 +1,17 @@ +"""tool""" + +from . import execute +from . import memory +from . import search +from .base_memory_tool import BaseMemoryTool +from .mcp_tool import MCPTool +from .think_tool import ThinkTool + +__all__ = [ + "execute", + "memory", + "search", + "BaseMemoryTool", + "MCPTool", + "ThinkTool", +] diff --git a/reme_ai/tool/base_memory_tool.py b/reme_ai/tool/base_memory_tool.py new file mode 100644 index 00000000..4e57cd1e --- /dev/null +++ b/reme_ai/tool/base_memory_tool.py @@ -0,0 +1,102 @@ +"""Base class for memory tool""" + +from abc import ABCMeta +from pathlib import Path + +from ..core.enumeration import MemoryType +from ..core.op import BaseOp +from ..core.schema import ToolCall, MemoryNode +from ..core.utils import CacheHandler + + +class BaseMemoryTool(BaseOp, metaclass=ABCMeta): + """Base class for memory tool""" + + def __init__( + self, + enable_multiple: bool = True, + enable_thinking_params: bool = False, + meta_memory_path: str = "./meta_memory", + **kwargs, + ): + super().__init__(**kwargs) + self.enable_multiple: bool = enable_multiple + self.enable_thinking_params: bool = enable_thinking_params + self.meta_memory_path: str = meta_memory_path + self._meta_memory: CacheHandler | None = None + + def _build_parameters(self) -> dict: + return {} + + def _build_multiple_parameters(self) -> dict: + return {} + + def _build_tool_call(self) -> ToolCall: + if self.enable_multiple: + parameters = self._build_multiple_parameters() + else: + parameters = self._build_parameters() + + if self.enable_thinking_params and "thinking" not in parameters["properties"]: + parameters["properties"] = { + "thinking": { + "type": "string", + "description": "Your thinking and reasoning about how to fill in the parameters", + }, + **parameters["properties"], + } + parameters["required"] = ["thinking", *parameters["required"]] + + return ToolCall( + **{ + "description": self.get_prompt("tool" + ("_multiple" if self.enable_multiple else "")), + "parameters": parameters, + }, + ) + + @property + def meta_memory(self) -> CacheHandler: + """Get or create the meta memory cache handler.""" + if self._meta_memory is None: + self._meta_memory = CacheHandler(Path(self.meta_memory_path) / self.vector_store.collection_name) + return self._meta_memory + + @property + def memory_type(self) -> MemoryType: + """Get the memory type from context.""" + return MemoryType(self.context.get("memory_type")) + + @property + def memory_target(self) -> str: + """Get the memory target from context.""" + return self.context.get("memory_target", "") + + @property + def ref_memory_id(self) -> str: + """Get the reference memory ID from context.""" + return self.context.get("ref_memory_id", "") + + @property + def author(self) -> str: + """Get the author from context.""" + return self.context.get("author", "") + + def _build_memory_node( + self, + memory_content: str, + when_to_use: str = "", + metadata: dict | None = None, + ) -> MemoryNode: + """Build MemoryNode from content, when_to_use, and metadata. + + This is a shared utility method for subclasses that need to create MemoryNode instances. + """ + return MemoryNode( + memory_type=self.memory_type, + memory_target=self.memory_target, + when_to_use=when_to_use or "", + content=memory_content, + ref_memory_id=self.ref_memory_id, + author=self.author, + metadata=metadata or {}, + ) diff --git a/reme_ai/core/tool/execute/__init__.py b/reme_ai/tool/execute/__init__.py similarity index 100% rename from reme_ai/core/tool/execute/__init__.py rename to reme_ai/tool/execute/__init__.py diff --git a/reme_ai/core/tool/execute/execute_code.py b/reme_ai/tool/execute/execute_code.py similarity index 89% rename from reme_ai/core/tool/execute/execute_code.py rename to reme_ai/tool/execute/execute_code.py index 02dc01a3..ea259487 100644 --- a/reme_ai/core/tool/execute/execute_code.py +++ b/reme_ai/tool/execute/execute_code.py @@ -4,11 +4,11 @@ This module provides an operation that can execute Python code strings and return the output or error messages. """ -from ...context import C -from ...op import BaseOp -from ...schema import ToolCall +from ...core.context import C +from ...core.op import BaseOp +from ...core.schema import ToolCall -from ...utils import exec_code +from ...core.utils import exec_code @C.register_op() diff --git a/reme_ai/core/tool/execute/execute_code.yaml b/reme_ai/tool/execute/execute_code.yaml similarity index 100% rename from reme_ai/core/tool/execute/execute_code.yaml rename to reme_ai/tool/execute/execute_code.yaml diff --git a/reme_ai/core/tool/execute/execute_shell.py b/reme_ai/tool/execute/execute_shell.py similarity index 91% rename from reme_ai/core/tool/execute/execute_shell.py rename to reme_ai/tool/execute/execute_shell.py index 63a31e2d..6e244ddb 100644 --- a/reme_ai/core/tool/execute/execute_shell.py +++ b/reme_ai/tool/execute/execute_shell.py @@ -4,11 +4,11 @@ This module provides an operation that can execute shell commands asynchronously and return the output, error, and exit code. """ -from ...context import C -from ...op import BaseOp -from ...schema import ToolCall +from ...core.context import C +from ...core.op import BaseOp +from ...core.schema import ToolCall -from ...utils import run_shell_command +from ...core.utils import run_shell_command @C.register_op() diff --git a/reme_ai/core/tool/execute/execute_shell.yaml b/reme_ai/tool/execute/execute_shell.yaml similarity index 100% rename from reme_ai/core/tool/execute/execute_shell.yaml rename to reme_ai/tool/execute/execute_shell.yaml diff --git a/reme_ai/core/tool/mcp_tool.py b/reme_ai/tool/mcp_tool.py similarity index 95% rename from reme_ai/core/tool/mcp_tool.py rename to reme_ai/tool/mcp_tool.py index 802e4138..f06275b0 100644 --- a/reme_ai/core/tool/mcp_tool.py +++ b/reme_ai/tool/mcp_tool.py @@ -2,10 +2,10 @@ from typing import List -from ..context import C -from ..op import BaseOp -from ..schema import ToolCall -from ..utils import MCPClient +from ..core.context import C +from ..core.op import BaseOp +from ..core.schema import ToolCall +from ..core.utils import MCPClient @C.register_op() diff --git a/reme_ai/tool/memory/__init__.py b/reme_ai/tool/memory/__init__.py new file mode 100644 index 00000000..b6dfb163 --- /dev/null +++ b/reme_ai/tool/memory/__init__.py @@ -0,0 +1,27 @@ +"""Memory tool operations.""" + +from .history.add_history_memory import AddHistoryMemory +from .history.read_history_memory import ReadHistoryMemory +from .identity.read_identity_memory import ReadIdentityMemory +from .identity.update_identity_memory import UpdateIdentityMemory +from .meta.add_meta_memory import AddMetaMemory +from .meta.read_meta_memory import ReadMetaMemory +from .vector.add_memory import AddMemory +from .vector.add_summary_memory import AddSummaryMemory +from .vector.delete_memory import DeleteMemory +from .vector.update_memory import UpdateMemory +from .vector.vector_retrieve_memory import VectorRetrieveMemory + +__all__ = [ + "AddHistoryMemory", + "ReadHistoryMemory", + "ReadIdentityMemory", + "UpdateIdentityMemory", + "AddMetaMemory", + "ReadMetaMemory", + "AddMemory", + "AddSummaryMemory", + "DeleteMemory", + "UpdateMemory", + "VectorRetrieveMemory", +] diff --git a/reme_ai/tool/memory/history/__init__.py b/reme_ai/tool/memory/history/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/tool/memory/history/add_history_memory.py b/reme_ai/tool/memory/history/add_history_memory.py new file mode 100644 index 00000000..14a00953 --- /dev/null +++ b/reme_ai/tool/memory/history/add_history_memory.py @@ -0,0 +1,111 @@ +"""Add history memory operation.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.schema import MemoryNode + + +@C.register_op() +class AddHistoryMemory(BaseMemoryTool): + """Add history memory from conversation messages.""" + + def __init__(self, add_metadata: bool = True, **kwargs): + super().__init__(**kwargs) + self.add_metadata: bool = add_metadata + + def _build_item_schema(self) -> tuple[dict, list[str]]: + properties = { + "messages": { + "type": "array", + "description": self.get_prompt("messages"), + "items": {"type": "object"}, + }, + } + required = ["messages"] + + if self.add_metadata: + properties["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + } + + return properties, required + + def _build_parameters(self) -> dict: + properties, required = self._build_item_schema() + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_parameters(self) -> dict: + item_properties, required_fields = self._build_item_schema() + return { + "type": "object", + "properties": { + "histories": { + "type": "array", + "description": self.get_prompt("histories"), + "items": { + "type": "object", + "properties": item_properties, + "required": required_fields, + }, + }, + }, + "required": ["histories"], + } + + def _format_messages(self, messages: list) -> str: + return "\n".join([f"{msg.get('role', 'unknown')}: {msg.get('content', '')}" for msg in messages]) + + def _extract_history_data(self, hist_dict: dict) -> tuple[list, dict]: + messages = hist_dict.get("messages", []) + metadata = hist_dict.get("metadata", {}) if self.add_metadata else {} + return messages, metadata + + async def execute(self): + memory_nodes: list[MemoryNode] = [] + + if self.enable_multiple: + histories: list[dict] = self.context.get("histories", []) + if not histories: + self.output = "No histories provided for addition." + return + + for hist in histories: + messages, metadata = self._extract_history_data(hist) + if not messages: + logger.warning("Skipping history with empty messages") + continue + + memory_content = self._format_messages(messages) + memory_nodes.append( + self._build_memory_node(memory_content, when_to_use="", metadata=metadata), + ) + else: + messages, metadata = self._extract_history_data(self.context) + if not messages: + self.output = "No messages provided for addition." + return + + memory_content = self._format_messages(messages) + memory_nodes.append( + self._build_memory_node(memory_content, when_to_use="", metadata=metadata), + ) + + if not memory_nodes: + self.output = "No valid histories provided for addition." + return + + vector_nodes = [node.to_vector_node() for node in memory_nodes] + vector_ids: list[str] = [node.vector_id for node in vector_nodes] + + await self.vector_store.delete(vector_ids=vector_ids) + await self.vector_store.insert(nodes=vector_nodes) + + self.output = f"Successfully added {len(memory_nodes)} history memories to vector_store." + logger.info(self.output) diff --git a/reme_ai/tool/memory/history/add_history_memory.yaml b/reme_ai/tool/memory/history/add_history_memory.yaml new file mode 100644 index 00000000..18fd9163 --- /dev/null +++ b/reme_ai/tool/memory/history/add_history_memory.yaml @@ -0,0 +1,14 @@ +tool: | + Add history memory from conversation messages. + +tool_multiple: | + Add multiple history memories in a single operation. + +messages: | + List of message objects with 'role' and 'content' fields. + +metadata: | + Optional metadata (time, session_id, topic, etc.). + +histories: | + List of history objects, each with messages and optional metadata. diff --git a/reme_ai/tool/memory/history/read_history_memory.py b/reme_ai/tool/memory/history/read_history_memory.py new file mode 100644 index 00000000..ac478401 --- /dev/null +++ b/reme_ai/tool/memory/history/read_history_memory.py @@ -0,0 +1,75 @@ +"""Read history memory operation.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.schema import MemoryNode + + +@C.register_op() +class ReadHistoryMemory(BaseMemoryTool): + """Read history memories by IDs.""" + + def _build_parameters(self) -> dict: + return { + "type": "object", + "properties": { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + }, + }, + "required": ["memory_id"], + } + + def _build_multiple_parameters(self) -> dict: + return { + "type": "object", + "properties": { + "memory_ids": { + "type": "array", + "description": self.get_prompt("memory_ids"), + "items": {"type": "string"}, + }, + }, + "required": ["memory_ids"], + } + + async def execute(self): + if self.enable_multiple: + memory_ids: list[str] = self.context.get("memory_ids", []) + else: + memory_id = self.context.get("memory_id", "") + memory_ids: list[str] = [memory_id] if memory_id else [] + + memory_ids = [mid for mid in memory_ids if mid] + + if not memory_ids: + self.output = "No valid history memory IDs provided for reading." + logger.warning(self.output) + return + + nodes = await self.vector_store.search( + query="", + top_k=len(memory_ids), + filter_dict={"vector_id": memory_ids}, + ) + + if not nodes: + self.output = "No history memories found with the provided IDs." + logger.warning(self.output) + return + + memories: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes] + + output_lines = [] + for memory in memories: + output_lines.append(f"Memory ID: {memory.vector_id}") + output_lines.append(f"Content:\n{memory.content}") + if memory.metadata: + output_lines.append(f"Metadata: {memory.metadata}") + output_lines.append("---") + + self.output = "\n".join(output_lines) + logger.info(f"Successfully read {len(memories)} history memories.") diff --git a/reme_ai/tool/memory/history/read_history_memory.yaml b/reme_ai/tool/memory/history/read_history_memory.yaml new file mode 100644 index 00000000..f50878a7 --- /dev/null +++ b/reme_ai/tool/memory/history/read_history_memory.yaml @@ -0,0 +1,11 @@ +tool: | + Read history memory by ID. + +tool_multiple: | + Read multiple history memories by IDs. + +memory_id: | + Unique identifier of the history memory. + +memory_ids: | + List of unique identifiers of history memories. diff --git a/reme_ai/tool/memory/identity/__init__.py b/reme_ai/tool/memory/identity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/tool/memory/identity/read_identity_memory.py b/reme_ai/tool/memory/identity/read_identity_memory.py new file mode 100644 index 00000000..ecb5b438 --- /dev/null +++ b/reme_ai/tool/memory/identity/read_identity_memory.py @@ -0,0 +1,33 @@ +"""Read identity memory operation.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C + + +@C.register_op() +class ReadIdentityMemory(BaseMemoryTool): + """Read identity memory for agent self-cognition.""" + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def _build_parameters(self) -> dict: + return { + "type": "object", + "properties": {}, + "required": [], + } + + async def execute(self): + result = self.meta_memory.load("identity_memory") + identity_memory = result if result is not None else "" + + if identity_memory: + self.output = f"Identity memory:\n{identity_memory}" + logger.info("Retrieved identity memory") + else: + self.output = "No identity memory found." + logger.info(self.output) diff --git a/reme_ai/tool/memory/identity/read_identity_memory.yaml b/reme_ai/tool/memory/identity/read_identity_memory.yaml new file mode 100644 index 00000000..6e84e4bf --- /dev/null +++ b/reme_ai/tool/memory/identity/read_identity_memory.yaml @@ -0,0 +1,3 @@ +tool: | + Read the identity memory for the agent. + Retrieve self-cognition information such as identity, role, personality, or current state. diff --git a/reme_ai/tool/memory/identity/update_identity_memory.py b/reme_ai/tool/memory/identity/update_identity_memory.py new file mode 100644 index 00000000..ba0e2de7 --- /dev/null +++ b/reme_ai/tool/memory/identity/update_identity_memory.py @@ -0,0 +1,39 @@ +"""Update identity memory operation.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C + + +@C.register_op() +class UpdateIdentityMemory(BaseMemoryTool): + """Update identity memory for agent self-cognition.""" + + def __init__(self, **kwargs): + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + + def _build_parameters(self) -> dict: + return { + "type": "object", + "properties": { + "identity_memory": { + "type": "string", + "description": self.get_prompt("identity_memory"), + }, + }, + "required": ["identity_memory"], + } + + async def execute(self): + identity_memory = self.context.get("identity_memory", "") + + if not identity_memory: + self.output = "No valid identity memory provided for update." + logger.warning(self.output) + return + + self.meta_memory.save("identity_memory", identity_memory) + self.output = "Successfully updated identity memory." + logger.info(self.output) diff --git a/reme_ai/tool/memory/identity/update_identity_memory.yaml b/reme_ai/tool/memory/identity/update_identity_memory.yaml new file mode 100644 index 00000000..48033bfe --- /dev/null +++ b/reme_ai/tool/memory/identity/update_identity_memory.yaml @@ -0,0 +1,7 @@ +tool: | + Update the identity memory for the agent. + Store self-cognition information such as identity, role, personality, or current state. + +identity_memory: | + The identity memory content to store. + Should be a clear statement capturing the agent's self-cognition or current state. diff --git a/reme_ai/tool/memory/meta/__init__.py b/reme_ai/tool/memory/meta/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/tool/memory/meta/add_meta_memory.py b/reme_ai/tool/memory/meta/add_meta_memory.py new file mode 100644 index 00000000..994fdf9e --- /dev/null +++ b/reme_ai/tool/memory/meta/add_meta_memory.py @@ -0,0 +1,121 @@ +"""Add meta memory operation for adding memory metadata.""" + +import json + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.enumeration import MemoryType + + +@C.register_op() +class AddMetaMemory(BaseMemoryTool): + """Add memory metadata (memory_type and memory_target) to meta storage. + + Supports single/multiple addition modes via `enable_multiple` parameter. + """ + + def _build_item_schema(self) -> tuple[dict, list[str]]: + """Build shared schema properties and required fields for meta memory items. + + Returns: + Tuple of (properties dict, required fields list). + """ + properties = { + "memory_type": { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [MemoryType.PERSONAL.value, MemoryType.PROCEDURAL.value], + }, + "memory_target": { + "type": "string", + "description": self.get_prompt("memory_target"), + }, + } + required = ["memory_type", "memory_target"] + return properties, required + + def _build_parameters(self) -> dict: + """Build input schema for single meta memory addition.""" + properties, required = self._build_item_schema() + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_parameters(self) -> dict: + """Build input schema for multiple meta memory addition.""" + item_properties, required_fields = self._build_item_schema() + return { + "type": "object", + "properties": { + "meta_memories": { + "type": "array", + "description": self.get_prompt("meta_memories"), + "items": { + "type": "object", + "properties": item_properties, + "required": required_fields, + }, + }, + }, + "required": ["meta_memories"], + } + + def _load_meta_memories(self) -> list[dict]: + """Load existing meta memories from cache.""" + result = self.meta_memory.load("meta_memories") + return result if result is not None else [] + + def _save_meta_memories(self, memories: list[dict]) -> bool: + """Save meta memories to cache.""" + return self.meta_memory.save("meta_memories", memories) + + async def execute(self): + """Execute addition: load existing, merge with new, and save. + + Duplicates (same memory_type and memory_target) are skipped. + """ + existing_memories: list[dict] = self._load_meta_memories() + existing_set = {(m["memory_type"], m["memory_target"]) for m in existing_memories} + + # Build new memories to add based on mode + new_memories: list[dict] = [] + if self.enable_multiple: + meta_memories: list[dict] = self.context.get("meta_memories", []) + for mem in meta_memories: + memory_type = mem.get("memory_type", "") + memory_target = mem.get("memory_target", "") + if memory_type and (memory_type, memory_target) not in existing_set: + new_memories.append( + { + "memory_type": memory_type, + "memory_target": memory_target, + }, + ) + existing_set.add((memory_type, memory_target)) + else: + memory_type = self.context.get("memory_type", "") + memory_target = self.context.get("memory_target", "") + if memory_type and (memory_type, memory_target) not in existing_set: + new_memories.append( + { + "memory_type": memory_type, + "memory_target": memory_target, + }, + ) + + if not new_memories: + self.output = "No new meta memories to add (all entries already exist or invalid)." + return + + # Merge and save + all_memories = existing_memories + new_memories + self._save_meta_memories(all_memories) + + # Format output + added_str = json.dumps(new_memories, ensure_ascii=False) + self.output = f"Successfully added {len(new_memories)} meta memory entries: {added_str}" + logger.info(self.output) diff --git a/reme_ai/tool/memory/meta/add_meta_memory.yaml b/reme_ai/tool/memory/meta/add_meta_memory.yaml new file mode 100644 index 00000000..ce4645a4 --- /dev/null +++ b/reme_ai/tool/memory/meta/add_meta_memory.yaml @@ -0,0 +1,24 @@ +tool: | + Add a memory metadata entry to register a new memory type and target. + Use this tool to define what types of memories should be tracked, such as: + - Personal memories: "John", "Alice" (person-specific preferences and context) + - Procedural memories: "deployment_process", "code_review_steps" (how-to knowledge) + +tool_multiple: | + Add multiple memory metadata entries to register multiple memory types and targets at once. + Use this tool to define multiple memory tracking categories in a single operation. + Each entry specifies a memory_type and memory_target for organizing different memory domains. + +meta_memories: | + A list of memory metadata entries to add. Each entry contains memory_type and memory_target. + +memory_type: | + The type of memory to register. Valid values are: personal, procedural. + - personal: Person-specific memory storing preferences and context about specific individuals + - procedural: Procedural memory storing how-to knowledge and step-by-step processes + +memory_target: | + The target identifier for this memory category. + Examples: + - For personal memory: person's name (e.g., "John", "Alice") + - For procedural memory: process name (e.g., "deployment", "code_review") diff --git a/reme_ai/tool/memory/meta/read_meta_memory.py b/reme_ai/tool/memory/meta/read_meta_memory.py new file mode 100644 index 00000000..ae31df78 --- /dev/null +++ b/reme_ai/tool/memory/meta/read_meta_memory.py @@ -0,0 +1,110 @@ +"""Read meta memory operation for retrieving memory metadata.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.enumeration import MemoryType + + +@C.register_op() +class ReadMetaMemory(BaseMemoryTool): + """Read memory metadata (memory_type and memory_target) from meta storage. + + This operation reads stored memory metadata and optionally includes + TOOL and IDENTITY type memories. + """ + + def __init__( + self, + enable_tool_memory: bool = False, + enable_identity_memory: bool = False, + **kwargs, + ): + """Initialize ReadMetaMemory. + + Args: + enable_tool_memory: Include TOOL type meta memory. Defaults to False. + enable_identity_memory: Include IDENTITY type meta memory. Defaults to False. + **kwargs: Additional arguments for BaseMemoryTool. + """ + kwargs["enable_multiple"] = False + super().__init__(**kwargs) + self.enable_tool_memory = enable_tool_memory + self.enable_identity_memory = enable_identity_memory + + def _build_parameters(self) -> dict: + """Build input schema for reading meta memory. + + No input parameters required for reading. + """ + return { + "type": "object", + "properties": {}, + "required": [], + } + + def _load_meta_memories(self) -> list[dict[str, str]]: + """Load meta memories from cache and apply filters.""" + result = self.meta_memory.load("meta_memories") + all_memories = result if result is not None else [] + + filtered_memories = [] + for m in all_memories: + memory_type = MemoryType(m.get("memory_type")) + if memory_type in (MemoryType.PERSONAL, MemoryType.PROCEDURAL): + filtered_memories.append(m) + + if self.enable_tool_memory: + filtered_memories.append( + { + "memory_type": MemoryType.TOOL.value, + "memory_target": "tool_guidelines", + }, + ) + + if self.enable_identity_memory: + filtered_memories.append( + { + "memory_type": MemoryType.IDENTITY.value, + "memory_target": "self", + }, + ) + + return filtered_memories + + def _format_memory_metadata(self, memories: list[dict[str, str]]) -> str: + """Format memory metadata into a readable string. + + Args: + memories: List of memory metadata entries. + + Returns: + str: Formatted memory metadata string. + """ + if not memories: + return "" + + lines = [] + for memory in memories: + memory_type = memory["memory_type"] + memory_target = memory["memory_target"] + description = self.get_prompt(f"type_{memory_type}") + lines.append(f"- {memory_type}({memory_target}): {description}") + + return "\n".join(lines) + + async def execute(self): + """Execute the read meta memory operation. + + Reads memory metadata from cache storage and formats output. + """ + memories = self._load_meta_memories() + + if memories: + formatted = self._format_memory_metadata(memories) + self.output = formatted + logger.info(f"Retrieved {len(memories)} meta memory entries") + else: + self.output = "No memory metadata found." + logger.info(self.output) diff --git a/reme_ai/tool/memory/meta/read_meta_memory.yaml b/reme_ai/tool/memory/meta/read_meta_memory.yaml new file mode 100644 index 00000000..dff25278 --- /dev/null +++ b/reme_ai/tool/memory/meta/read_meta_memory.yaml @@ -0,0 +1,16 @@ +tool: | + Read the memory metadata registry to see what types of memories are being tracked. + Use this tool to retrieve all registered memory types and their targets. + This helps understand what memory categories are available for storing and retrieving information. + +type_identity: | + Self-cognition memory storing agent's identity, personality, and current state. + +type_personal: | + Person-specific memory storing preferences and context about specific individuals. + +type_procedural: | + Procedural memory storing how-to knowledge and step-by-step processes. + +type_tool: | + Tool memory storing tool usage patterns, success rates, token consumption, and latency. diff --git a/reme_ai/tool/memory/vector/__init__.py b/reme_ai/tool/memory/vector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reme_ai/tool/memory/vector/add_memory.py b/reme_ai/tool/memory/vector/add_memory.py new file mode 100644 index 00000000..5fc16f2c --- /dev/null +++ b/reme_ai/tool/memory/vector/add_memory.py @@ -0,0 +1,144 @@ +"""Add memory operation for vector store.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.schema import MemoryNode + + +@C.register_op() +class AddMemory(BaseMemoryTool): + """Add memories to vector store with optional when_to_use and metadata. + + Supports single/multiple addition modes via `enable_multiple` parameter. + """ + + def __init__(self, add_when_to_use: bool = False, add_metadata: bool = True, **kwargs): + """Initialize AddMemory. + + Args: + add_when_to_use: Include when_to_use field for better retrieval. + add_metadata: Include metadata field for additional info. + **kwargs: Additional arguments for BaseMemoryTool. + """ + super().__init__(**kwargs) + self.add_when_to_use: bool = add_when_to_use + self.add_metadata: bool = add_metadata + + def _build_item_schema(self) -> tuple[dict, list[str]]: + """Build shared schema properties and required fields for memory items. + + Returns: + Tuple of (properties dict, required fields list). + """ + properties = {} + required = [] + + if self.add_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": self.get_prompt("when_to_use"), + } + required.append("when_to_use") + + properties["memory_content"] = { + "type": "string", + "description": self.get_prompt("memory_content"), + } + required.append("memory_content") + + if self.add_metadata: + properties["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + } + + return properties, required + + def _build_parameters(self) -> dict: + """Build input schema for single memory addition.""" + properties, required = self._build_item_schema() + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_parameters(self) -> dict: + """Build input schema for multiple memory addition.""" + item_properties, required_fields = self._build_item_schema() + return { + "type": "object", + "properties": { + "memories": { + "type": "array", + "description": self.get_prompt("memories"), + "items": { + "type": "object", + "properties": item_properties, + "required": required_fields, + }, + }, + }, + "required": ["memories"], + } + + def _extract_memory_data(self, mem_dict: dict) -> tuple[str, str, dict]: + """Extract memory data from a dictionary with proper defaults. + + Args: + mem_dict: Dictionary containing memory fields. + + Returns: + Tuple of (memory_content, when_to_use, metadata). + """ + memory_content = mem_dict.get("memory_content", "") + when_to_use = mem_dict.get("when_to_use", "") if self.add_when_to_use else "" + metadata = mem_dict.get("metadata", {}) if self.add_metadata else {} + return memory_content, when_to_use, metadata + + async def execute(self): + """Execute addition: delete existing IDs (upsert), then insert new memories.""" + memory_nodes: list[MemoryNode] = [] + + if self.enable_multiple: + memories: list[dict] = self.context.get("memories", []) + if not memories: + self.output = "No memories provided for addition." + return + + for mem in memories: + memory_content, when_to_use, metadata = self._extract_memory_data(mem) + if not memory_content: + logger.warning("Skipping memory with empty content") + continue + + memory_nodes.append( + self._build_memory_node(memory_content, when_to_use, metadata), + ) + + else: + memory_content, when_to_use, metadata = self._extract_memory_data(self.context) + if not memory_content: + self.output = "No memory content provided for addition." + return + + memory_nodes.append( + self._build_memory_node(memory_content, when_to_use, metadata), + ) + + if not memory_nodes: + self.output = "No valid memories provided for addition." + return + + # Convert to VectorNodes and collect IDs + vector_nodes = [node.to_vector_node() for node in memory_nodes] + vector_ids: list[str] = [node.vector_id for node in vector_nodes] + + # Delete existing IDs (upsert behavior), then insert + await self.vector_store.delete(vector_ids=vector_ids) + await self.vector_store.insert(nodes=vector_nodes) + + self.output = f"Successfully added {len(memory_nodes)} memories to vector_store." + logger.info(self.output) diff --git a/reme_ai/tool/memory/vector/add_memory.yaml b/reme_ai/tool/memory/vector/add_memory.yaml new file mode 100644 index 00000000..d484f349 --- /dev/null +++ b/reme_ai/tool/memory/vector/add_memory.yaml @@ -0,0 +1,37 @@ +tool: | + Add a memory to the vector store for future retrieval. + Use this tool to store important information that should be remembered, such as: + - Meta information: "I am very happy" + - Personal preferences: "John prefers dark mode", "Alice works in PST timezone" + - Procedural knowledge: "To deploy, run build then push", "Always validate input before processing" + - Tool usage tips: "search_tool works best with short queries", "Use cache tool for frequently accessed data" + +tool_multiple: | + Add multiple memories to the vector store for future retrieval. + Use this tool to store multiple pieces of important information in a single operation. + Each memory can include when_to_use conditions and metadata for better organization and retrieval. + Examples: storing multiple user preferences, multiple procedural steps, or multiple tool usage tips. + +when_to_use: | + Optional condition description for when to retrieve this memory. + This field is used for vector embedding to improve retrieval accuracy by providing contextual information. + Examples: + - "when user asks about authentication" + - "when deploying to production" + - "when using search_tool" + - "when handling error cases" + +memory_content: | + The content of the memory to store. + Should be a clear, concise statement that captures the information to remember. + Keep it focused on a single piece of information for better retrieval accuracy. + +metadata: | + Optional metadata for the memory, providing additional context. Can include: + - time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00") + - source: Where this information came from (e.g., "user_input", "documentation", "observation") + - tags: List of tags for categorization (e.g., ["authentication", "security"]) + - Any other custom key-value pairs relevant to the memory + +memories: | + A list of memory objects to store. diff --git a/reme_ai/tool/memory/vector/add_summary_memory.py b/reme_ai/tool/memory/vector/add_summary_memory.py new file mode 100644 index 00000000..684300d7 --- /dev/null +++ b/reme_ai/tool/memory/vector/add_summary_memory.py @@ -0,0 +1,67 @@ +"""Add summary memory operation for vector store.""" + +from loguru import logger + +from .add_memory import AddMemory +from ....core.context import C + + +@C.register_op() +class AddSummaryMemory(AddMemory): + """Add LLM-summarized memories to vector store. + + Differences from AddMemory: + - Single memory mode only (enable_multiple=False) + - Uses 'summary_memory' parameter instead of 'memory_content' + - No when_to_use field (add_when_to_use=False) + """ + + def __init__( + self, + add_metadata: bool = True, + **kwargs, + ): + """Initialize AddSummaryMemory. + + Args: + add_metadata: Include metadata field for additional info. + **kwargs: Additional arguments for AddMemory. + """ + # Force single mode and disable when_to_use + kwargs["enable_multiple"] = False + kwargs["add_when_to_use"] = False + super().__init__(add_metadata=add_metadata, **kwargs) + + def _build_parameters(self) -> dict: + """Build input schema for summary memory addition.""" + properties = { + "summary_memory": { + "type": "string", + "description": self.get_prompt("summary_memory"), + }, + } + required = ["summary_memory"] + + if self.add_metadata: + properties["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + } + + return { + "type": "object", + "properties": properties, + "required": required, + } + + async def execute(self): + """Execute addition: map summary_memory to memory_content and call parent.""" + # Map summary_memory to memory_content + summary_memory = self.context.get("summary_memory", "") + if not summary_memory: + self.output = "No summary memory content provided for addition." + logger.warning(self.output) + return + + self.context["memory_content"] = summary_memory + await super().execute() diff --git a/reme_ai/tool/memory/vector/add_summary_memory.yaml b/reme_ai/tool/memory/vector/add_summary_memory.yaml new file mode 100644 index 00000000..e9e469c4 --- /dev/null +++ b/reme_ai/tool/memory/vector/add_summary_memory.yaml @@ -0,0 +1,27 @@ +tool: | + Add a summary memory to the vector store for future retrieval. + Use this tool to store a summarized version of the provided context. + The LLM should first summarize the context, then call this tool with the summarized content. + + This tool is specifically designed for storing summaries of conversations, events, or information + that has been condensed from a larger context. Examples: + - Summarizing a long conversation: "User discussed project requirements for a web app with authentication" + - Summarizing a decision: "Team decided to use PostgreSQL for the database after evaluating options" + - Summarizing an event: "Successfully deployed version 2.0 to production with new features" + +summary_memory: | + The summarized content to store as memory. + Should be a clear, concise summary that captures the key information from the context. + Keep it focused and informative - aim for 1-3 sentences that convey the essential points. + Examples: + - "User prefers Python for backend development and has experience with FastAPI framework" + - "Project deadline is January 15th, requires authentication, payment integration, and admin dashboard" + - "Bug in user registration was caused by missing email validation, fixed by adding regex check" + +metadata: | + Optional metadata for the memory, providing additional context. Can include: + - time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00") + - source: Where this information came from (e.g., "conversation", "meeting", "observation") + - tags: List of tags for categorization (e.g., ["project", "decision"]) + - summary_type: Type of summary (e.g., "conversation", "decision", "event", "task") + - Any other custom key-value pairs relevant to the memory diff --git a/reme_ai/tool/memory/vector/delete_memory.py b/reme_ai/tool/memory/vector/delete_memory.py new file mode 100644 index 00000000..4fb066a5 --- /dev/null +++ b/reme_ai/tool/memory/vector/delete_memory.py @@ -0,0 +1,60 @@ +"""Delete memory operation for vector store.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C + + +@C.register_op() +class DeleteMemory(BaseMemoryTool): + """Delete memories from vector store by IDs. + + Supports single/multiple deletion modes via `enable_multiple` parameter. + """ + + def _build_parameters(self) -> dict: + """Build input schema for single memory deletion.""" + return { + "type": "object", + "properties": { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + }, + }, + "required": ["memory_id"], + } + + def _build_multiple_parameters(self) -> dict: + """Build input schema for multiple memory deletion.""" + return { + "type": "object", + "properties": { + "memory_ids": { + "type": "array", + "description": self.get_prompt("memory_ids"), + "items": {"type": "string"}, + }, + }, + "required": ["memory_ids"], + } + + async def execute(self): + """Execute deletion: remove memories from vector store by IDs.""" + if self.enable_multiple: + memory_ids = self.context.get("memory_ids", []) + else: + single_id = self.context.get("memory_id", "") + memory_ids = [single_id] if single_id else [] + + # Filter out empty IDs + memory_ids = [mid for mid in memory_ids if mid] + + if not memory_ids: + self.output = "No valid memory IDs provided for deletion." + return + + await self.vector_store.delete(vector_ids=memory_ids) + self.output = f"Successfully deleted {len(memory_ids)} memories from vector_store." + logger.info(self.output) diff --git a/reme_ai/tool/memory/vector/delete_memory.yaml b/reme_ai/tool/memory/vector/delete_memory.yaml new file mode 100644 index 00000000..fe5934dd --- /dev/null +++ b/reme_ai/tool/memory/vector/delete_memory.yaml @@ -0,0 +1,25 @@ +tool: | + Delete a memory from the vector store using its unique ID. + Use this tool when: + - The user explicitly requests to remove or forget information + - A memory is identified as outdated, incorrect, or no longer relevant + - Information needs to be removed for privacy or compliance reasons + - Duplicate or conflicting memories need to be cleaned up + Memory ID can be obtained from previous memory retrieval results. + +tool_multiple: | + Delete multiple memories from the vector store using their unique IDs. + Use this tool for batch deletion when: + - The user explicitly requests to remove or forget multiple pieces of information + - Multiple memories are identified as outdated, incorrect, or no longer relevant + - Bulk cleanup of information is needed for privacy or compliance reasons + - Multiple duplicate or conflicting memories need to be removed + Memory IDs can be obtained from previous memory retrieval results. + +memory_id: | + The unique identifier (memory_id) of the memory to delete. + This ID is returned when memories are retrieved or added. + +memory_ids: | + A list of unique identifiers (memory_ids) of the memories to delete. + Each ID should be a valid memory_id obtained from previous operations. diff --git a/reme_ai/tool/memory/vector/update_memory.py b/reme_ai/tool/memory/vector/update_memory.py new file mode 100644 index 00000000..8d3fc7cb --- /dev/null +++ b/reme_ai/tool/memory/vector/update_memory.py @@ -0,0 +1,153 @@ +"""Update memory operation for vector store.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.schema import MemoryNode + + +@C.register_op() +class UpdateMemory(BaseMemoryTool): + """Update memories by deleting old ones and inserting new ones. + + Supports single/multiple update modes via `enable_multiple` parameter. + """ + + def __init__( + self, + add_when_to_use: bool = False, + add_metadata: bool = True, + **kwargs, + ): + """Initialize UpdateMemory. + + Args: + add_when_to_use: Include when_to_use field for better retrieval. + add_metadata: Include metadata field for additional info. + **kwargs: Additional arguments for BaseMemoryTool. + """ + super().__init__(**kwargs) + self.add_when_to_use: bool = add_when_to_use + self.add_metadata: bool = add_metadata + + def _build_item_schema(self) -> tuple[dict, list[str]]: + """Build shared schema properties and required fields for memory items. + + Returns: + Tuple of (properties dict, required fields list). + """ + properties = { + "memory_id": { + "type": "string", + "description": self.get_prompt("memory_id"), + }, + } + required = ["memory_id"] + + if self.add_when_to_use: + properties["when_to_use"] = { + "type": "string", + "description": self.get_prompt("when_to_use"), + } + + properties["memory_content"] = { + "type": "string", + "description": self.get_prompt("memory_content"), + } + required.append("memory_content") + + if self.add_metadata: + properties["metadata"] = { + "type": "object", + "description": self.get_prompt("metadata"), + } + + return properties, required + + def _build_parameters(self) -> dict: + """Build input schema for single memory update.""" + properties, required = self._build_item_schema() + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_parameters(self) -> dict: + """Build input schema for multiple memory update.""" + item_properties, required_fields = self._build_item_schema() + return { + "type": "object", + "properties": { + "memories": { + "type": "array", + "description": self.get_prompt("memories"), + "items": { + "type": "object", + "properties": item_properties, + "required": required_fields, + }, + }, + }, + "required": ["memories"], + } + + def _extract_memory_data(self, mem_dict: dict) -> tuple[str, str, str, dict]: + """Extract memory update data from a dictionary with proper defaults. + + Args: + mem_dict: Dictionary containing memory fields. + + Returns: + Tuple of (memory_id, memory_content, when_to_use, metadata). + """ + memory_id = mem_dict.get("memory_id", "") + memory_content = mem_dict.get("memory_content", "") + when_to_use = mem_dict.get("when_to_use", "") if self.add_when_to_use else "" + metadata = mem_dict.get("metadata", {}) if self.add_metadata else {} + return memory_id, memory_content, when_to_use, metadata + + async def execute(self): + """Execute update: delete old memories by ID, insert new ones with updated content.""" + # Collect old IDs to delete and new nodes to insert + old_memory_ids: list[str] = [] + new_memory_nodes: list[MemoryNode] = [] + + if self.enable_multiple: + memories: list[dict] = self.context.get("memories", []) + if not memories: + self.output = "No memories provided for update." + return + + for mem in memories: + memory_id, memory_content, when_to_use, metadata = self._extract_memory_data(mem) + if not memory_id or not memory_content: + logger.warning(f"Skipping memory with missing id or content: {mem}") + continue + old_memory_ids.append(memory_id) + new_memory_nodes.append(self._build_memory_node(memory_content, when_to_use, metadata)) + + else: + memory_id, memory_content, when_to_use, metadata = self._extract_memory_data(self.context) + if not memory_id or not memory_content: + self.output = "No memory ID or content provided for update." + return + old_memory_ids.append(memory_id) + new_memory_nodes.append(self._build_memory_node(memory_content, when_to_use, metadata)) + + if not old_memory_ids or not new_memory_nodes: + self.output = "No valid memories provided for update." + return + + # Convert to VectorNodes and collect IDs + vector_nodes = [node.to_vector_node() for node in new_memory_nodes] + new_vector_ids = [node.vector_id for node in vector_nodes] + + # Delete old and duplicate new IDs (upsert behavior) + all_ids_to_delete = list(set(old_memory_ids + new_vector_ids)) + await self.vector_store.delete(vector_ids=all_ids_to_delete) + await self.vector_store.insert(nodes=vector_nodes) + + self.output = f"Update: deleted {len(old_memory_ids)} old memories, added {len(new_memory_nodes)} new memories." + logger.info(self.output) diff --git a/reme_ai/tool/memory/vector/update_memory.yaml b/reme_ai/tool/memory/vector/update_memory.yaml new file mode 100644 index 00000000..16e14c57 --- /dev/null +++ b/reme_ai/tool/memory/vector/update_memory.yaml @@ -0,0 +1,45 @@ +tool: | + Update a memory in the vector store by replacing the old memory with new content. + Use this tool when: + - The user wants to modify or correct existing information + - A memory needs to be updated with new details while keeping its relevance + - Information has changed and the old memory is no longer accurate + - You need to refine or improve the clarity of stored information + Memory ID can be obtained from previous memory retrieval results. + +tool_multiple: | + Update multiple memories in the vector store by replacing old memories with new content. + Use this tool for batch updates when: + - The user wants to modify or correct multiple pieces of existing information + - Multiple memories need to be updated with new details while keeping their relevance + - Information has changed across multiple memories + - You need to refine or improve multiple stored memories at once + Memory IDs can be obtained from previous memory retrieval results. + +memory_id: | + The unique identifier (memory_id) of the old memory to be replaced. + This ID is returned when memories are retrieved or added. + +when_to_use: | + Optional condition description for when to retrieve this memory. + This field is used for vector embedding to improve retrieval accuracy by providing contextual information. + Examples: + - "when user asks about authentication" + - "when deploying to production" + - "when using search_tool" + - "when handling error cases" + +memory_content: | + The new content of the memory to store. + Should be a clear, concise statement that captures the updated information to remember. + Keep it focused on a single piece of information for better retrieval accuracy. + +metadata: | + Optional metadata for the new memory, providing additional context. Can include: + - time: The timestamp or date associated with the memory (e.g., "2025-01-06 10:30:00") + - source: Where this information came from (e.g., "user_input", "documentation", "observation") + - tags: List of tags for categorization (e.g., ["authentication", "security"]) + - Any other custom key-value pairs relevant to the memory + +memories: | + A list of memory update objects. diff --git a/reme_ai/tool/memory/vector/vector_retrieve_memory.py b/reme_ai/tool/memory/vector/vector_retrieve_memory.py new file mode 100644 index 00000000..9e0cc6c7 --- /dev/null +++ b/reme_ai/tool/memory/vector/vector_retrieve_memory.py @@ -0,0 +1,212 @@ +"""Vector-based memory retrieval using semantic similarity search.""" + +from loguru import logger + +from ...base_memory_tool import BaseMemoryTool +from ....core.context import C +from ....core.enumeration import MemoryType +from ....core.schema import MemoryNode, VectorNode +from ....core.utils import deduplicate_memories + + +@C.register_op() +class VectorRetrieveMemory(BaseMemoryTool): + """Retrieve memories using vector similarity search. + + Supports single/multiple query modes via `enable_multiple` parameter. + When `add_memory_type_target` is False, memory_type/memory_target are from context. + """ + + def __init__( + self, + enable_summary_memory: bool = False, + add_memory_type_target: bool = False, + top_k: int = 10, + **kwargs, + ): + """Initialize VectorRetrieveMemory. + + Args: + enable_summary_memory: Include summary memories in results. + add_memory_type_target: Include memory_type/memory_target in schema (else from context). + top_k: Max memories to retrieve per query. + **kwargs: Additional args for BaseMemoryTool. + """ + super().__init__(**kwargs) + self.enable_summary_memory: bool = enable_summary_memory + self.add_memory_type_target: bool = add_memory_type_target + self.top_k: int = top_k + + def _build_query_schema(self) -> tuple[dict, list[str]]: + """Build shared schema properties and required fields for query items. + + Returns: + Tuple of (properties dict, required fields list). + """ + properties = {} + required = [] + + if self.add_memory_type_target: + properties["memory_type"] = { + "type": "string", + "description": self.get_prompt("memory_type"), + "enum": [ + MemoryType.IDENTITY.value, + MemoryType.PERSONAL.value, + MemoryType.PROCEDURAL.value, + MemoryType.TOOL.value, + ], + } + properties["memory_target"] = { + "type": "string", + "description": self.get_prompt("memory_target"), + } + required.extend(["memory_type", "memory_target"]) + + properties["query"] = { + "type": "string", + "description": self.get_prompt("query"), + } + required.append("query") + + return properties, required + + def _build_parameters(self) -> dict: + """Build input schema for single query mode. + + Returns: + Schema with memory_type/memory_target/query (if add_memory_type_target) or query only. + """ + properties, required = self._build_query_schema() + return { + "type": "object", + "properties": properties, + "required": required, + } + + def _build_multiple_parameters(self) -> dict: + """Build input schema for multiple query mode. + + Returns: + Schema with query_items array. Each item has memory_type/memory_target/query + (if add_memory_type_target) or query only. + """ + item_properties, item_required = self._build_query_schema() + return { + "type": "object", + "properties": { + "query_items": { + "type": "array", + "description": self.get_prompt("query_items"), + "items": { + "type": "object", + "properties": item_properties, + "required": item_required, + }, + }, + }, + "required": ["query_items"], + } + + async def _retrieve_by_query( + self, + memory_type: str, + memory_target: str, + query: str, + ) -> list[MemoryNode]: + """Retrieve memories by query using vector similarity search. + + Args: + memory_type: Memory type to search. + memory_target: Memory target to search. + query: Query string for similarity search. + + Returns: + List of matching memories. + """ + memory_type_list = [MemoryType(memory_type)] + if self.enable_summary_memory: + memory_type_list.append(MemoryType.SUMMARY) + + filter_dict = { + "memory_type": [mt.value for mt in memory_type_list], + "memory_target": [memory_target], + } + + nodes: list[VectorNode] = await self.vector_store.search( + query=query, + top_k=self.top_k, + filter_dict=filter_dict, + ) + + memory_nodes: list[MemoryNode] = [MemoryNode.from_vector_node(n) for n in nodes] + + # Filter TOOL memories: keep only if when_to_use matches query (tool name) + filtered_memory_nodes = [ + m for m in memory_nodes if not (m.memory_type == MemoryType.TOOL and m.when_to_use != query) + ] + + return filtered_memory_nodes + + async def execute(self): + """Execute memory retrieval based on query texts. + + Handles single/multiple query modes. When add_memory_type_target is False, + memory_type/memory_target are from context. Outputs formatted results or error message. + """ + default_memory_type: str = self.context.get("memory_type", "") + default_memory_target: str = self.context.get("memory_target", "") + + # Normalize to list of query items + if self.enable_multiple: + query_items: list[dict] = self.context.get("query_items", []) + if not query_items: + self.output = "No query items provided for retrieval." + return + else: + query = self.context.get("query", "") + if not query: + self.output = "No query provided for retrieval." + return + + query_items = [ + { + "memory_type": default_memory_type, + "memory_target": default_memory_target, + "query": query, + }, + ] + + # Filter out items without query text + query_items = [item for item in query_items if item.get("query")] + + if not query_items: + self.output = "No valid query texts provided for retrieval." + return + + # Retrieve memories for all queries + memories: list[MemoryNode] = [] + for item in query_items: + memory_type = item.get("memory_type") or default_memory_type + memory_target = item.get("memory_target") or default_memory_target + + if not memory_type or not memory_target: + logger.warning(f"Skipping query with missing memory_type or memory_target: {item}") + continue + + retrieved = await self._retrieve_by_query( + memory_type=memory_type, + memory_target=memory_target, + query=item["query"], + ) + memories.extend(retrieved) + + # Deduplicate and format output + memories = deduplicate_memories(memories) + + if not memories: + self.output = "No memories found matching the query." + else: + self.output = "\n".join([m.format_memory() for m in memories]) + + logger.info(f"Retrieved {len(memories)} memories") diff --git a/reme_ai/tool/memory/vector/vector_retrieve_memory.yaml b/reme_ai/tool/memory/vector/vector_retrieve_memory.yaml new file mode 100644 index 00000000..c30a6efe --- /dev/null +++ b/reme_ai/tool/memory/vector/vector_retrieve_memory.yaml @@ -0,0 +1,31 @@ +tool: | + Retrieve memories from the memory store using vector similarity search. + Use this tool to find relevant memories based on semantic similarity to the query. + The search returns the most relevant memories ranked by similarity score. + +tool_multiple: | + Retrieve memories from the memory store using multiple queries with vector similarity search. + Use this tool to find relevant memories based on semantic similarity to multiple queries. + This is useful when you need to search for different types of information in a single operation. + The search returns the most relevant memories ranked by similarity score for each query. + +memory_type: | + The type of memory to search for. Must be one of: + - "identity": Information about the AI agent's identity, role, or characteristics + - "personal": Information about users, their preferences, or personal details + - "procedural": Step-by-step instructions, workflows, or how-to knowledge + - "tool": Tool usage tips, examples, and best practices + +memory_target: | + The target of the memory to search within. + - For "personal" memory: the person's name or identifier (e.g., "john", "alice") + - For "procedural" memory: the process or task name (e.g., "deployment", "authentication") + - For "tool" memory: the tool name (e.g., "search_tool", "calculator") + - For "identity" memory: typically "self" or the agent's identifier + +query: | + The query text for vector similarity search. + Use descriptive queries that capture the semantic meaning of what you're looking for. + +query_items: | + A list of query items for vector similarity search. diff --git a/reme_ai/core/tool/search/__init__.py b/reme_ai/tool/search/__init__.py similarity index 100% rename from reme_ai/core/tool/search/__init__.py rename to reme_ai/tool/search/__init__.py index d213ee70..5a73dc2e 100644 --- a/reme_ai/core/tool/search/__init__.py +++ b/reme_ai/tool/search/__init__.py @@ -6,6 +6,6 @@ from .tavily_search import TavilySearch __all__ = [ "DashscopeSearch", - "TavilySearch", "MockSearch", + "TavilySearch", ] diff --git a/reme_ai/core/tool/search/dashscope_search.py b/reme_ai/tool/search/dashscope_search.py similarity index 97% rename from reme_ai/core/tool/search/dashscope_search.py rename to reme_ai/tool/search/dashscope_search.py index ec27e60c..19bd8104 100644 --- a/reme_ai/core/tool/search/dashscope_search.py +++ b/reme_ai/tool/search/dashscope_search.py @@ -9,9 +9,9 @@ from typing import Literal from loguru import logger -from ...context import C -from ...op import BaseOp -from ...schema import ToolCall +from ...core.context import C +from ...core.op import BaseOp +from ...core.schema import ToolCall @C.register_op() diff --git a/reme_ai/core/tool/search/dashscope_search.yaml b/reme_ai/tool/search/dashscope_search.yaml similarity index 100% rename from reme_ai/core/tool/search/dashscope_search.yaml rename to reme_ai/tool/search/dashscope_search.yaml diff --git a/reme_ai/core/tool/search/mock_search.py b/reme_ai/tool/search/mock_search.py similarity index 90% rename from reme_ai/core/tool/search/mock_search.py rename to reme_ai/tool/search/mock_search.py index b08956c0..187463dc 100644 --- a/reme_ai/core/tool/search/mock_search.py +++ b/reme_ai/tool/search/mock_search.py @@ -9,11 +9,11 @@ import random from loguru import logger -from ...context import C -from ...enumeration import Role -from ...op import BaseOp -from ...schema import ToolCall, Message -from ...utils import extract_content +from ...core.context import C +from ...core.enumeration import Role +from ...core.op import BaseOp +from ...core.schema import ToolCall, Message +from ...core.utils import extract_content @C.register_op() diff --git a/reme_ai/core/tool/search/mock_search.yaml b/reme_ai/tool/search/mock_search.yaml similarity index 100% rename from reme_ai/core/tool/search/mock_search.yaml rename to reme_ai/tool/search/mock_search.yaml diff --git a/reme_ai/core/tool/search/tavily_search.py b/reme_ai/tool/search/tavily_search.py similarity index 97% rename from reme_ai/core/tool/search/tavily_search.py rename to reme_ai/tool/search/tavily_search.py index 8feda12e..5c194bdc 100644 --- a/reme_ai/core/tool/search/tavily_search.py +++ b/reme_ai/tool/search/tavily_search.py @@ -9,9 +9,9 @@ import os from loguru import logger -from ...context import C -from ...op import BaseOp -from ...schema import ToolCall +from ...core.context import C +from ...core.op import BaseOp +from ...core.schema import ToolCall @C.register_op() diff --git a/reme_ai/core/tool/search/tavily_search.yaml b/reme_ai/tool/search/tavily_search.yaml similarity index 100% rename from reme_ai/core/tool/search/tavily_search.yaml rename to reme_ai/tool/search/tavily_search.yaml diff --git a/reme_ai/tool/think_tool.py b/reme_ai/tool/think_tool.py new file mode 100644 index 00000000..2e83b3ad --- /dev/null +++ b/reme_ai/tool/think_tool.py @@ -0,0 +1,56 @@ +"""Think tool for agent reflection and planning. + +This module provides a tool that prompts the model for explicit reflection +before taking actions, helping agents reason about their next steps. +""" + +from ..core.context import C +from ..core.op import BaseOp +from ..core.schema import ToolCall + + +@C.register_op() +class ThinkTool(BaseOp): + """Utility that prompts the model for explicit reflection text. + + This tool provides a thinking mechanism for agents to reflect on: + 1. Whether current context is sufficient to answer + 2. What information is missing + 3. Which tool and parameters to use next + """ + + def __init__(self, add_output_reflection: bool = False, **kwargs): + """Initialize the think tool tool. + + Args: + add_output_reflection: If True, outputs the reflection content; + if False, outputs a confirmation message + **kwargs: Additional arguments passed to BaseOp + """ + super().__init__(**kwargs) + self.add_output_reflection: bool = add_output_reflection + + def _build_tool_call(self) -> ToolCall: + """Build the tool call schema for think tool.""" + return ToolCall( + **{ + "description": self.get_prompt("tool"), + "parameters": { + "type": "object", + "properties": { + "reflection": { + "type": "string", + "description": self.get_prompt("reflection"), + }, + }, + "required": ["reflection"], + }, + }, + ) + + async def execute(self): + """Execute the think tool by processing reflection input.""" + if self.add_output_reflection: + self.output = self.context["reflection"] + else: + self.output = self.get_prompt("reflection_output") diff --git a/reme_ai/tool/think_tool.yaml b/reme_ai/tool/think_tool.yaml new file mode 100644 index 00000000..75903557 --- /dev/null +++ b/reme_ai/tool/think_tool.yaml @@ -0,0 +1,32 @@ +tool: | + Before calling any external tool or when rethinking and planning is needed, you must invoke this tool for brief reflection. + The output must cover: + 1. Whether the current context is enough to answer the user directly, plus reasoning. + 2. If not, what information or validation is missing. + 3. A strategy to close the gap: which tool to call next, why, and key parameters or query terms. + Keep the reasoning tightly scoped to the current turn, avoid unrelated background, + and do not execute tools from here—only produce clear, actionable thoughts. + +reflection: | + 1) Can I answer now? Why? + 2) What is missing? + 3) Which tool + params next? + +reflection_output: | + Reflection has been recorded. + +tool_zh: | + 每次准备调用任何外部工具之前或者需要重新思考规划,都必须先调用本工具进行简短思考。 + 输出需覆盖以下要点: + 1. 评估当前上下文是否足以直接回答用户问题,并解释理由。 + 2. 若不能回答,明确缺失的信息或验证步骤。 + 3. 针对缺口设计下一步策略:列出计划使用的工具、调用目的、关键参数或查询关键词。 + 思考要紧扣当前轮对话内容,避免复述无关背景,不要直接执行工具,只输出清晰推理。 + +reflection_zh: | + 1) 能直接回答吗?为什么? + 2) 缺什么信息? + 3) 下一步用哪个工具+参数? + +reflection_output_zh: | + 已经记录反思 \ No newline at end of file diff --git a/tests/test_tool.py b/tests/test_tool.py index e35caef8..7f6c7afb 100644 --- a/tests/test_tool.py +++ b/tests/test_tool.py @@ -19,7 +19,7 @@ def test_search(): Tests DashscopeSearch, MockSearch, and TavilySearch operations with a sample query to verify they work correctly. """ - from reme_ai.core.tool.search import DashscopeSearch, MockSearch, TavilySearch + from reme_ai.tool.search import DashscopeSearch, MockSearch, TavilySearch query = "今天杭州的天气如何?" @@ -43,7 +43,7 @@ def test_execute(): including successful execution, syntax errors, runtime errors, and invalid commands to verify error handling. """ - from reme_ai.core.tool.execute import ExecuteCode, ExecuteShell + from reme_ai.tool.execute import ExecuteCode, ExecuteShell # Test ExecuteCode print("\n" + "=" * 60) @@ -155,7 +155,7 @@ def test_simple_chat(): Tests the SimpleChat agent with a basic query to verify it can process and respond to user input. """ - from reme_ai.core.agent import SimpleChat + from reme_ai.mem_agent import SimpleChat op = SimpleChat() asyncio.run(op.call(query="你好")) @@ -168,7 +168,7 @@ async def test_stream_chat(): Tests the StreamChat agent with a query to verify it can process and stream responses in real-time using async operations. """ - from reme_ai.core.agent import StreamChat + from reme_ai.mem_agent import StreamChat from reme_ai.core.utils import execute_stream_task from reme_ai.core.context import RuntimeContext from asyncio import Queue