mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-10 22:41:06 +00:00
597 lines
22 KiB
Python
597 lines
22 KiB
Python
"""Memory schema definitions for ReMe.
|
|
|
|
This module defines the core memory data structures used in the ReMe system,
|
|
including base memory classes and specialized memory types for tasks, personal
|
|
information, and tool call results.
|
|
"""
|
|
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
from abc import ABC
|
|
from typing import List
|
|
from uuid import uuid4
|
|
|
|
from flowllm.core.schema import VectorNode
|
|
from mcp.types import CallToolResult, TextContent
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class BaseMemory(BaseModel, ABC):
|
|
"""Base class for all memory types in the ReMe system.
|
|
|
|
This abstract base class provides common fields and methods for all memory
|
|
types, including workspace identification, content storage, timestamps,
|
|
and conversion to/from vector nodes for storage and retrieval.
|
|
|
|
Attributes:
|
|
workspace_id: Identifier for the workspace this memory belongs to.
|
|
memory_id: Unique identifier for this memory instance.
|
|
memory_type: Type of memory (task, personal, tool, etc.).
|
|
when_to_use: Description of when this memory should be retrieved.
|
|
content: The actual content of the memory (string or bytes).
|
|
score: Relevance score for this memory (0.0 to 1.0).
|
|
time_created: Timestamp when the memory was created.
|
|
time_modified: Timestamp when the memory was last modified.
|
|
author: Identifier of the entity that created this memory.
|
|
metadata: Additional metadata dictionary for extensibility.
|
|
"""
|
|
|
|
workspace_id: str = Field(default="")
|
|
memory_id: str = Field(default_factory=lambda: uuid4().hex)
|
|
memory_type: str = Field(default=...)
|
|
|
|
when_to_use: str = Field(default="")
|
|
content: str | bytes = Field(default="")
|
|
score: float = Field(default=0)
|
|
|
|
time_created: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
time_modified: str = Field(default_factory=lambda: datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
author: str = Field(default="")
|
|
|
|
metadata: dict = Field(default_factory=dict)
|
|
|
|
def update_modified_time(self):
|
|
"""Update the time_modified field to the current timestamp."""
|
|
self.time_modified = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
def update_metadata(self, new_metadata):
|
|
"""Update the metadata dictionary with new values.
|
|
|
|
Args:
|
|
new_metadata: Dictionary containing new metadata to replace existing metadata.
|
|
"""
|
|
self.metadata = new_metadata
|
|
|
|
def to_vector_node(self) -> VectorNode:
|
|
"""Convert this memory instance to a VectorNode for storage.
|
|
|
|
Returns:
|
|
VectorNode: A vector node representation of this memory.
|
|
|
|
Raises:
|
|
NotImplementedError: Must be implemented by subclasses.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def from_vector_node(cls, node: VectorNode):
|
|
"""Create a memory instance from a VectorNode.
|
|
|
|
Args:
|
|
node: VectorNode containing memory data.
|
|
|
|
Returns:
|
|
BaseMemory: A memory instance reconstructed from the vector node.
|
|
|
|
Raises:
|
|
NotImplementedError: Must be implemented by subclasses.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
|
|
class TaskMemory(BaseMemory):
|
|
"""Memory type for storing task-related information.
|
|
|
|
TaskMemory is used to store information about tasks, including when to use
|
|
the memory and the task content itself. It extends BaseMemory with
|
|
task-specific behavior.
|
|
|
|
Attributes:
|
|
memory_type: Always set to "task" for task memories.
|
|
"""
|
|
|
|
memory_type: str = Field(default="task")
|
|
|
|
def to_vector_node(self) -> VectorNode:
|
|
"""Convert this TaskMemory to a VectorNode.
|
|
|
|
Returns:
|
|
VectorNode: Vector node representation with when_to_use as content
|
|
and all other fields stored in metadata.
|
|
"""
|
|
return VectorNode(
|
|
unique_id=self.memory_id,
|
|
workspace_id=self.workspace_id,
|
|
content=self.when_to_use,
|
|
metadata={
|
|
"memory_type": self.memory_type,
|
|
"content": self.content,
|
|
"score": self.score,
|
|
"time_created": self.time_created,
|
|
"time_modified": self.time_modified,
|
|
"author": self.author,
|
|
"metadata": json.dumps(self.metadata, ensure_ascii=False),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def from_vector_node(cls, node: VectorNode) -> "TaskMemory":
|
|
"""Create a TaskMemory instance from a VectorNode.
|
|
|
|
Args:
|
|
node: VectorNode containing task memory data.
|
|
|
|
Returns:
|
|
TaskMemory: Reconstructed TaskMemory instance.
|
|
"""
|
|
metadata = node.metadata.copy()
|
|
memory_metadata = metadata.pop("metadata", {})
|
|
if isinstance(memory_metadata, str):
|
|
memory_metadata = json.loads(memory_metadata)
|
|
|
|
return cls(
|
|
workspace_id=node.workspace_id,
|
|
memory_id=node.unique_id,
|
|
memory_type=metadata.pop("memory_type"),
|
|
when_to_use=node.content,
|
|
content=metadata.pop("content"),
|
|
score=metadata.pop("score"),
|
|
time_created=metadata.pop("time_created"),
|
|
time_modified=metadata.pop("time_modified"),
|
|
author=metadata.pop("author"),
|
|
metadata=memory_metadata,
|
|
)
|
|
|
|
|
|
class PersonalMemory(BaseMemory):
|
|
"""Memory type for storing personal information and user preferences.
|
|
|
|
PersonalMemory extends BaseMemory with fields specific to personal data,
|
|
including target information and reflection subject attributes. This is
|
|
used for storing user preferences, personal insights, and reflection data.
|
|
|
|
Attributes:
|
|
memory_type: Always set to "personal" for personal memories.
|
|
target: Target identifier or category for this personal memory.
|
|
reflection_subject: Subject of reflection for storing reflection attributes.
|
|
"""
|
|
|
|
memory_type: str = Field(default="personal")
|
|
target: str = Field(default="")
|
|
reflection_subject: str = Field(default="") # For storing reflection subject attributes
|
|
|
|
def to_vector_node(self) -> VectorNode:
|
|
"""Convert this PersonalMemory to a VectorNode.
|
|
|
|
Returns:
|
|
VectorNode: Vector node representation with when_to_use as content
|
|
and all other fields including target and reflection_subject
|
|
stored in metadata.
|
|
"""
|
|
return VectorNode(
|
|
unique_id=self.memory_id,
|
|
workspace_id=self.workspace_id,
|
|
content=self.when_to_use,
|
|
metadata={
|
|
"memory_type": self.memory_type,
|
|
"content": self.content,
|
|
"target": self.target,
|
|
"reflection_subject": self.reflection_subject,
|
|
"score": self.score,
|
|
"time_created": self.time_created,
|
|
"time_modified": self.time_modified,
|
|
"author": self.author,
|
|
"metadata": json.dumps(self.metadata, ensure_ascii=False),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def from_vector_node(cls, node: VectorNode) -> "PersonalMemory":
|
|
"""Create a PersonalMemory instance from a VectorNode.
|
|
|
|
Args:
|
|
node: VectorNode containing personal memory data.
|
|
|
|
Returns:
|
|
PersonalMemory: Reconstructed PersonalMemory instance.
|
|
"""
|
|
metadata = node.metadata.copy()
|
|
memory_metadata = metadata.pop("metadata", {})
|
|
if isinstance(memory_metadata, str):
|
|
memory_metadata = json.loads(memory_metadata)
|
|
|
|
return cls(
|
|
workspace_id=node.workspace_id,
|
|
memory_id=node.unique_id,
|
|
memory_type=metadata.pop("memory_type"),
|
|
when_to_use=node.content,
|
|
content=metadata.pop("content"),
|
|
target=metadata.pop("target", ""),
|
|
reflection_subject=metadata.pop("reflection_subject", ""),
|
|
score=metadata.pop("score"),
|
|
time_created=metadata.pop("time_created"),
|
|
time_modified=metadata.pop("time_modified"),
|
|
author=metadata.pop("author"),
|
|
metadata=memory_metadata,
|
|
)
|
|
|
|
|
|
class ToolCallResult(BaseModel):
|
|
"""Represents the result of a tool invocation.
|
|
|
|
This class stores comprehensive information about a tool call, including
|
|
inputs, outputs, performance metrics, evaluation, and deduplication hash.
|
|
|
|
Attributes:
|
|
create_time: Timestamp when the tool was invoked.
|
|
tool_name: Name of the tool that was called.
|
|
input: Input parameters passed to the tool (dict or string).
|
|
output: Output result from the tool execution.
|
|
token_cost: Number of tokens consumed by the tool call (-1 if unknown).
|
|
success: Whether the tool invocation completed successfully.
|
|
time_cost: Time taken for the tool invocation in seconds.
|
|
summary: Brief summary of the tool call result.
|
|
evaluation: Detailed evaluation of the tool invocation.
|
|
score: Quality score from 0.0 (failure) to 1.0 (complete success).
|
|
is_summarized: Whether this tool call has been included in a summary.
|
|
call_hash: MD5 hash of input and output for deduplication.
|
|
metadata: Additional metadata dictionary.
|
|
"""
|
|
|
|
create_time: str = Field(default="", description="Time of tool invocation")
|
|
tool_name: str = Field(default=..., description="Name of the tool")
|
|
input: dict | str = Field(default="", description="Tool input")
|
|
output: str = Field(default="", description="Tool output")
|
|
token_cost: int = Field(default=-1, description="Token consumption of the tool")
|
|
success: bool = Field(default=True, description="Whether the tool invocation was successful")
|
|
time_cost: float = Field(default=0, description="Time consumed by the tool invocation, in seconds")
|
|
summary: str = Field(default="", description="Brief summary of the tool call result")
|
|
evaluation: str = Field(default="", description="Detailed evaluation for the tool invocation")
|
|
score: float = Field(default=0, description="Score of the Evaluation (0.0 for failure, 1.0 for complete success)")
|
|
is_summarized: bool = Field(default=False, description="Whether this tool call has been included in a summary")
|
|
call_hash: str = Field(default="", description="Hash value of input and output combined for deduplication")
|
|
|
|
metadata: dict = Field(default_factory=dict)
|
|
|
|
def generate_hash(self) -> str:
|
|
"""Generate hash value from tool input and output for deduplication.
|
|
|
|
Creates an MD5 hash from the combined input and output strings.
|
|
This hash is used to identify duplicate tool calls.
|
|
|
|
Returns:
|
|
str: MD5 hash hexdigest of the combined input and output.
|
|
"""
|
|
# Convert input to string if it's a dict
|
|
input_str = json.dumps(self.input, sort_keys=True) if isinstance(self.input, dict) else str(self.input)
|
|
|
|
# Combine input and output
|
|
combined = f"{input_str}|{self.output}"
|
|
|
|
# Generate MD5 hash
|
|
hash_value = hashlib.md5(combined.encode("utf-8")).hexdigest()
|
|
|
|
return hash_value
|
|
|
|
def ensure_hash(self):
|
|
"""Ensure call_hash is set, generate if empty."""
|
|
if not self.call_hash:
|
|
self.call_hash = self.generate_hash()
|
|
|
|
def from_mcp_tool_result(self, tool_result: CallToolResult, max_char_len: int = None):
|
|
"""Populate this instance from an MCP CallToolResult.
|
|
|
|
Args:
|
|
tool_result: MCP CallToolResult to extract data from.
|
|
max_char_len: Optional maximum character length for output content.
|
|
If provided, output will be truncated to this length.
|
|
"""
|
|
text_list = []
|
|
for content in tool_result.content:
|
|
if isinstance(content, TextContent):
|
|
text_list.append(content.text)
|
|
|
|
else:
|
|
raise NotImplementedError(f"content.type={type(content)} not supported")
|
|
content = "\n".join(text_list)
|
|
|
|
if max_char_len:
|
|
content = content[:max_char_len]
|
|
self.output = content
|
|
|
|
self.success = not tool_result.is_error
|
|
self.metadata.update(tool_result.meta)
|
|
|
|
|
|
class ToolMemory(BaseMemory):
|
|
"""Memory type for storing tool call execution history.
|
|
|
|
ToolMemory extends BaseMemory to store a collection of tool call results,
|
|
allowing tracking of tool usage patterns, performance metrics, and
|
|
execution history for analysis and summarization.
|
|
|
|
Attributes:
|
|
memory_type: Always set to "tool" for tool memories.
|
|
tool_call_results: List of ToolCallResult instances representing
|
|
historical tool invocations.
|
|
"""
|
|
|
|
memory_type: str = Field(default="tool")
|
|
tool_call_results: List[ToolCallResult] = Field(default_factory=list)
|
|
|
|
def to_vector_node(self) -> VectorNode:
|
|
"""Convert this ToolMemory to a VectorNode.
|
|
|
|
Returns:
|
|
VectorNode: Vector node representation with when_to_use as content
|
|
and all tool_call_results serialized in metadata.
|
|
"""
|
|
return VectorNode(
|
|
unique_id=self.memory_id,
|
|
workspace_id=self.workspace_id,
|
|
content=self.when_to_use,
|
|
metadata={
|
|
"memory_type": self.memory_type,
|
|
"content": self.content,
|
|
"score": self.score,
|
|
"time_created": self.time_created,
|
|
"time_modified": self.time_modified,
|
|
"author": self.author,
|
|
"tool_call_results": [x.model_dump() for x in self.tool_call_results],
|
|
"metadata": json.dumps(self.metadata, ensure_ascii=False),
|
|
},
|
|
)
|
|
|
|
def statistic(self, recent_frequency: int = 20) -> dict:
|
|
"""Calculate statistical information for the most recent N tool calls.
|
|
|
|
Analyzes the most recent tool calls and computes average metrics including
|
|
token cost, success rate, time cost, and quality scores.
|
|
|
|
Args:
|
|
recent_frequency: Number of most recent tool calls to analyze.
|
|
Defaults to 20.
|
|
|
|
Returns:
|
|
dict: Dictionary containing:
|
|
- avg_token_cost: Average token consumption (rounded to 2 decimals)
|
|
- avg_time_cost: Average execution time in seconds (rounded to 3 decimals)
|
|
- success_rate: Ratio of successful calls (rounded to 4 decimals)
|
|
- avg_score: Average quality score (rounded to 3 decimals)
|
|
"""
|
|
if not self.tool_call_results:
|
|
return {
|
|
"total_calls": 0,
|
|
"recent_calls_analyzed": 0,
|
|
"avg_token_cost": 0.0,
|
|
"success_rate": 0.0,
|
|
"avg_time_cost": 0.0,
|
|
"avg_score": 0.0,
|
|
}
|
|
|
|
# Get the most recent N tool calls (or all if less than N)
|
|
recent_calls = self.tool_call_results[-recent_frequency:]
|
|
# total_calls = len(self.tool_call_results)
|
|
recent_calls_count = len(recent_calls)
|
|
|
|
# Calculate statistics
|
|
total_token_cost = sum(call.token_cost for call in recent_calls if call.token_cost >= 0)
|
|
valid_token_calls = [call for call in recent_calls if call.token_cost >= 0]
|
|
avg_token_cost = total_token_cost / len(valid_token_calls) if valid_token_calls else 0.0
|
|
|
|
successful_calls = sum(1 for call in recent_calls if call.success)
|
|
success_rate = successful_calls / recent_calls_count if recent_calls_count > 0 else 0.0
|
|
|
|
total_time_cost = sum(call.time_cost for call in recent_calls)
|
|
avg_time_cost = total_time_cost / recent_calls_count if recent_calls_count > 0 else 0.0
|
|
|
|
total_score = sum(call.score for call in recent_calls)
|
|
avg_score = total_score / recent_calls_count if recent_calls_count > 0 else 0.0
|
|
|
|
return {
|
|
"avg_token_cost": round(avg_token_cost, 2),
|
|
"avg_time_cost": round(avg_time_cost, 3),
|
|
"success_rate": round(success_rate, 4),
|
|
"avg_score": round(avg_score, 3),
|
|
}
|
|
|
|
@classmethod
|
|
def from_vector_node(cls, node: VectorNode) -> "ToolMemory":
|
|
"""Create a ToolMemory instance from a VectorNode.
|
|
|
|
Args:
|
|
node: VectorNode containing tool memory data.
|
|
|
|
Returns:
|
|
ToolMemory: Reconstructed ToolMemory instance with tool_call_results
|
|
deserialized from metadata.
|
|
"""
|
|
metadata = node.metadata.copy()
|
|
tool_call_results = [ToolCallResult(**result) for result in metadata.pop("tool_call_results", [])]
|
|
memory_metadata = metadata.pop("metadata", {})
|
|
if isinstance(memory_metadata, str):
|
|
memory_metadata = json.loads(memory_metadata)
|
|
|
|
return cls(
|
|
workspace_id=node.workspace_id,
|
|
memory_id=node.unique_id,
|
|
when_to_use=node.content,
|
|
memory_type=metadata.pop("memory_type"),
|
|
content=metadata.pop("content"),
|
|
score=metadata.pop("score"),
|
|
time_created=metadata.pop("time_created"),
|
|
time_modified=metadata.pop("time_modified"),
|
|
author=metadata.pop("author"),
|
|
tool_call_results=tool_call_results,
|
|
metadata=memory_metadata,
|
|
)
|
|
|
|
|
|
def vector_node_to_memory(node: VectorNode):
|
|
"""Convert a VectorNode to the appropriate memory type.
|
|
|
|
This function inspects the memory_type in the node's metadata and
|
|
reconstructs the appropriate memory subclass (TaskMemory, PersonalMemory,
|
|
or ToolMemory).
|
|
|
|
Args:
|
|
node: VectorNode containing memory data with memory_type in metadata.
|
|
|
|
Returns:
|
|
BaseMemory: Instance of the appropriate memory subclass based on
|
|
memory_type.
|
|
|
|
Raises:
|
|
RuntimeError: If memory_type is not recognized or not present.
|
|
"""
|
|
memory_type = node.metadata.get("memory_type")
|
|
if memory_type == "task":
|
|
return TaskMemory.from_vector_node(node)
|
|
|
|
elif memory_type == "personal":
|
|
return PersonalMemory.from_vector_node(node)
|
|
|
|
elif memory_type == "tool":
|
|
return ToolMemory.from_vector_node(node)
|
|
|
|
else:
|
|
raise RuntimeError(f"memory_type={memory_type} not supported!")
|
|
|
|
|
|
def dict_to_memory(memory_dict: dict):
|
|
"""Create a memory instance from a dictionary.
|
|
|
|
This function creates the appropriate memory subclass based on the
|
|
memory_type field in the dictionary. Defaults to TaskMemory if
|
|
memory_type is not specified.
|
|
|
|
Args:
|
|
memory_dict: Dictionary containing memory data with optional
|
|
memory_type field.
|
|
|
|
Returns:
|
|
BaseMemory: Instance of the appropriate memory subclass based on
|
|
memory_type.
|
|
|
|
Raises:
|
|
RuntimeError: If memory_type is not recognized.
|
|
"""
|
|
memory_type = memory_dict.get("memory_type", "task")
|
|
if memory_type == "task":
|
|
return TaskMemory(**memory_dict)
|
|
|
|
elif memory_type == "personal":
|
|
return PersonalMemory(**memory_dict)
|
|
|
|
elif memory_type == "tool":
|
|
return ToolMemory(**memory_dict)
|
|
|
|
else:
|
|
raise RuntimeError(f"memory_type={memory_type} not supported!")
|
|
|
|
|
|
def task_main():
|
|
"""Test function for TaskMemory serialization and deserialization."""
|
|
e1 = TaskMemory(
|
|
workspace_id="w_1024",
|
|
memory_id="123",
|
|
when_to_use="test case use",
|
|
content="test content",
|
|
score=0.99,
|
|
metadata={},
|
|
)
|
|
print(e1.model_dump_json(indent=2))
|
|
v1 = e1.to_vector_node()
|
|
print(v1.model_dump_json(indent=2))
|
|
e2 = vector_node_to_memory(v1)
|
|
print(e2.model_dump_json(indent=2))
|
|
|
|
|
|
def personal_main():
|
|
"""Test function for PersonalMemory serialization and deserialization."""
|
|
p1 = PersonalMemory(
|
|
workspace_id="w_2048",
|
|
memory_id="456",
|
|
when_to_use="personal memory test case",
|
|
content="personal test content",
|
|
target="user_preferences",
|
|
reflection_subject="learning_style",
|
|
score=0.85,
|
|
metadata={"category": "user_profile"},
|
|
)
|
|
print("PersonalMemory test:")
|
|
print(p1.model_dump_json(indent=2))
|
|
v1 = p1.to_vector_node()
|
|
print("VectorNode:")
|
|
print(v1.model_dump_json(indent=2))
|
|
p2 = vector_node_to_memory(v1)
|
|
print("Reconstructed PersonalMemory:")
|
|
print(p2.model_dump_json(indent=2))
|
|
|
|
|
|
def tool_main():
|
|
"""Test function for ToolMemory serialization and deserialization."""
|
|
# Create sample tool call results
|
|
tool_result1 = ToolCallResult(
|
|
create_time="2025-10-15 10:30:00",
|
|
tool_name="file_reader",
|
|
input={"file_path": "/test/file.txt"},
|
|
output="File content successfully read",
|
|
token_cost=50,
|
|
success=True,
|
|
time_cost=0.5,
|
|
evaluation="Successfully executed",
|
|
score=0.95,
|
|
)
|
|
|
|
tool_result2 = ToolCallResult(
|
|
create_time="2025-10-15 10:31:00",
|
|
tool_name="data_processor",
|
|
input={"data": "sample_data", "format": "json"},
|
|
output="Data processed successfully",
|
|
token_cost=75,
|
|
success=True,
|
|
time_cost=1.2,
|
|
evaluation="Good performance",
|
|
score=0.88,
|
|
)
|
|
|
|
t1 = ToolMemory(
|
|
workspace_id="w_4096",
|
|
memory_id="789",
|
|
memory_type="tool",
|
|
when_to_use="tool execution memory test",
|
|
content="tool execution test content",
|
|
score=0.92,
|
|
tool_call_results=[tool_result1, tool_result2],
|
|
metadata={"execution_context": "test_environment"},
|
|
)
|
|
|
|
print("ToolMemory test:")
|
|
print(t1.model_dump_json(indent=2))
|
|
v1 = t1.to_vector_node()
|
|
print("VectorNode:")
|
|
print(v1.model_dump_json(indent=2))
|
|
t2 = ToolMemory.from_vector_node(v1)
|
|
print("Reconstructed ToolMemory:")
|
|
print(t2.model_dump_json(indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=== Task Memory Test ===")
|
|
# task_main()
|
|
print("\n=== Personal Memory Test ===")
|
|
# personal_main()
|
|
print("\n=== Tool Memory Test ===")
|
|
tool_main()
|