mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-10 22:41:06 +00:00
feat(memory): add tool call result handling and memory management
- Add new ToolCallResult and ToolMemory schemas for tracking tool executions - Implement tool memory retrieval and summarization flows in default config - Register new parse_tool_call_result_op for processing tool call results - Extend memory conversion logic to support tool memory type - Add test cases for tool memory serialization and deserialization - Include token counting utilities for text processing tasks
This commit is contained in:
parent
92f76566db
commit
b9da8d2a7f
10 changed files with 280 additions and 15 deletions
|
|
@ -49,13 +49,31 @@ flow:
|
|||
description: "A list of conversation trajectory information, including message content and score. This field does not need to be filled in, the system will complete it automatically."
|
||||
required: false
|
||||
|
||||
retrieve_task_memory_simple:
|
||||
flow_content: build_query_op >> recall_vector_store_op >> merge_memory_op
|
||||
retrieve_tool_memory:
|
||||
flow_content: parse_tool_call_result_op
|
||||
description: "Retrieve tool related memories from the memory database based on queries to improve tool recall"
|
||||
input_schema:
|
||||
tool_names:
|
||||
type: string
|
||||
description: "tool_name1,tool_name2"
|
||||
required: true
|
||||
|
||||
add_tool_call_result:
|
||||
flow_content: parse_tool_call_result_op
|
||||
description: "Retrieves the most relevant top-k memory experiences from historical data based on the current query with simplified processing"
|
||||
input_schema:
|
||||
query:
|
||||
tool_call_results:
|
||||
type: array
|
||||
description: "tool_call_result"
|
||||
required: true
|
||||
|
||||
summary_tool_call_result:
|
||||
flow_content: parse_tool_call_result_op
|
||||
description: "Retrieves the most relevant top-k memory experiences from historical data based on the current query with simplified processing"
|
||||
input_schema:
|
||||
tool_names:
|
||||
type: string
|
||||
description: "current query"
|
||||
description: "tool_name1,tool_name2"
|
||||
required: true
|
||||
|
||||
summary_task_memory_simple:
|
||||
|
|
@ -143,6 +161,12 @@ llm:
|
|||
params:
|
||||
temperature: 0.6
|
||||
|
||||
wk1:
|
||||
backend: openai_compatible
|
||||
model_name: qwen3-30b-a3b-instruct-2507
|
||||
params:
|
||||
temperature: 0.6
|
||||
|
||||
qwen3_30b_instruct:
|
||||
backend: openai_compatible
|
||||
model_name: qwen3-30b-a3b-instruct-2507
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
from . import personal
|
||||
from . import task
|
||||
from . import tool
|
||||
|
|
|
|||
0
reme_ai/retrieve/tool/__init__.py
Normal file
0
reme_ai/retrieve/tool/__init__.py
Normal file
|
|
@ -1,8 +1,10 @@
|
|||
import datetime
|
||||
from abc import ABC
|
||||
from typing import List
|
||||
from uuid import uuid4
|
||||
|
||||
from flowllm.schema.vector_node import VectorNode
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
|
|
@ -105,6 +107,118 @@ class PersonalMemory(BaseMemory):
|
|||
metadata=metadata.pop("metadata", {}))
|
||||
|
||||
|
||||
class ToolCallResult(BaseModel):
|
||||
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")
|
||||
evaluation: str = Field(default="", description="Evaluation for the tool invocation")
|
||||
score: float = Field(default=0, description="Score of the Evaluation")
|
||||
# updated: bool = Field(default=False, description="Whether tool memory has been updated by `ToolCallMetadata`")
|
||||
|
||||
metadata: dict = Field(default_factory=dict)
|
||||
|
||||
def from_mcp_tool_result(self, tool_result: CallToolResult, max_char_len: int = None):
|
||||
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 = tool_result.is_error
|
||||
self.metadata.update(tool_result.meta)
|
||||
|
||||
|
||||
class ToolMemory(BaseMemory):
|
||||
memory_type: str = Field(default="tool")
|
||||
tool_call_results: List[ToolCallResult] = Field(default_factory=list)
|
||||
|
||||
def to_vector_node(self) -> VectorNode:
|
||||
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": self.metadata,
|
||||
})
|
||||
|
||||
def statistic(self, recent_frequency: int = 20) -> dict:
|
||||
"""
|
||||
Calculate statistical information for the most recent 20 tool calls.
|
||||
Returns avg token_cost, success rate, avg time_cost, and avg score.
|
||||
"""
|
||||
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 20 tool calls (or all if less than 20)
|
||||
recent_calls = self.tool_call_results[-20:]
|
||||
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 {
|
||||
"total_calls": total_calls,
|
||||
"recent_calls_analyzed": recent_calls_count,
|
||||
"avg_token_cost": round(avg_token_cost, 2),
|
||||
"success_rate": round(success_rate, 4),
|
||||
"avg_time_cost": round(avg_time_cost, 3),
|
||||
"avg_score": round(avg_score, 3)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_vector_node(cls, node: VectorNode) -> "ToolMemory":
|
||||
metadata = node.metadata.copy()
|
||||
tool_call_results = [ToolCallResult(**result) for result in metadata.pop("tool_call_results", [])]
|
||||
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=metadata.pop("metadata", {}))
|
||||
|
||||
|
||||
|
||||
def vector_node_to_memory(node: VectorNode) -> BaseMemory:
|
||||
memory_type = node.metadata.get("memory_type")
|
||||
if memory_type == "task":
|
||||
|
|
@ -113,6 +227,9 @@ def vector_node_to_memory(node: VectorNode) -> BaseMemory:
|
|||
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!")
|
||||
|
||||
|
|
@ -125,11 +242,14 @@ def dict_to_memory(memory_dict: 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!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def task_main():
|
||||
e1 = TaskMemory(
|
||||
workspace_id="w_1024",
|
||||
memory_id="123",
|
||||
|
|
@ -142,3 +262,78 @@ if __name__ == "__main__":
|
|||
print(v1.model_dump_json(indent=2))
|
||||
e2 = vector_node_to_memory(v1)
|
||||
print(e2.model_dump_json(indent=2))
|
||||
|
||||
|
||||
def personal_main():
|
||||
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():
|
||||
# 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()
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
from . import personal
|
||||
from . import task
|
||||
from . import tool
|
||||
|
|
|
|||
1
reme_ai/summary/tool/__init__.py
Normal file
1
reme_ai/summary/tool/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .parse_tool_call_result_op import ParseToolCallResultOp
|
||||
13
reme_ai/summary/tool/parse_tool_call_result_op.py
Normal file
13
reme_ai/summary/tool/parse_tool_call_result_op.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from flowllm import C, BaseAsyncOp
|
||||
from loguru import logger
|
||||
|
||||
@C.register_op()
|
||||
class ParseToolCallResultOp(BaseAsyncOp):
|
||||
file_path: str = __file__
|
||||
|
||||
async def async_execute(self):
|
||||
tool_call_results: list = self.context.get("tool_call_results", [])
|
||||
logger.info(f"tool_call_results={tool_call_results}")
|
||||
|
||||
|
||||
self.context.response.answer = "haha"
|
||||
|
|
@ -110,23 +110,34 @@ async def run2(session):
|
|||
result = await response.json()
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
async def run3(session):
|
||||
workspace_id = "default2"
|
||||
|
||||
async with session.post(
|
||||
f"{base_url}/add_tool_call_result",
|
||||
json={
|
||||
"tool_call_results": [
|
||||
{"a": 1},
|
||||
{"a": 2},
|
||||
],
|
||||
"workspace_id": workspace_id,
|
||||
},
|
||||
headers={"Content-Type": "application/json"}
|
||||
) as response:
|
||||
result = await response.json()
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# 获取工具列表
|
||||
print("获取工具列表...")
|
||||
async with session.get(f"{base_url}/list") as response:
|
||||
if response.status == 200:
|
||||
tools = await response.json()
|
||||
print("可用工具:")
|
||||
for tool in tools:
|
||||
print(json.dumps(tool, ensure_ascii=False))
|
||||
else:
|
||||
print(f"获取工具列表失败: {response.status}")
|
||||
return
|
||||
|
||||
# await run1(session)
|
||||
await run2(session)
|
||||
# await run2(session)
|
||||
await run3(session)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
6
test/test5.py
Normal file
6
test/test5.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import tiktoken
|
||||
enc = tiktoken.get_encoding("o200k_base")
|
||||
|
||||
# r = enc.encode("我爱吃西瓜,你说啥")
|
||||
r = enc.encode("hello world aaaaaaaaaaaa")
|
||||
print(len(r))
|
||||
13
test/test6.py
Normal file
13
test/test6.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import tiktoken
|
||||
|
||||
def count_tokens(text: str) -> int:
|
||||
"""计算给定文本在指定模型下的 token 数量"""
|
||||
encoding = tiktoken.get_encoding("o200k_base")
|
||||
tokens = encoding.encode(text)
|
||||
return len(tokens)
|
||||
|
||||
# 示例使用
|
||||
text = "你好,世界!Hello, world!"
|
||||
token_count = count_tokens(text)
|
||||
print(f"Token 数量: {token_count}")
|
||||
print(len(text) / 4)
|
||||
Loading…
Add table
Reference in a new issue