Merge pull request #27 from modelscope/tool_memory

ToolMemory
This commit is contained in:
Jiaji 2025-10-22 12:16:00 +08:00 • committed by GitHub
commit 691eec954a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 4569 additions and 464 deletions

176
README.md
View file

@ -1,5 +1,3 @@
English | [**中文**](./README_ZH.md)
<p align="center">
<img src="docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
</p>
@ -21,16 +19,17 @@ ReMe provides AI agents with a unified memory system—enabling the ability to e
users, tasks, and agents.
```
Personal Memory + Task Memory = Agent Memory
Personal Memory + Task Memory + Tool Memory = Agent Memory
```
Personal memory helps "**understand user preferences**", while task memory helps agents "**perform better**".
Personal memory helps "**understand user preferences**", task memory helps agents "**perform better**", and tool memory enables "**smarter tool usage**".
---
## 📰 Latest Updates
- **[2025-09]** 🎉 ReMe v0.1.8 has been officially released, adding support for asynchronous operations. It has also been
- **[2025-10]** 🔧 Tool Memory support is now available! Enables data-driven tool selection and parameter optimization through historical performance tracking. Check out the [Tool Memory Guide](docs/tool_memory/tool_memory.md) and [benchmark results](docs/tool_memory/tool_bench.md).
- **[2025-09]** 🎉 ReMe v0.1.9 has been officially released, adding support for asynchronous operations. It has also been
integrated into the memory service of agentscope-runtime.
- **[2025-09]** 🎉 ReMe v0.1 officially released, integrating task memory and personal memory. If you want to use the
original memoryscope project, you can find it
@ -53,7 +52,7 @@ Personal memory helps "**understand user preferences**", while task memory helps
<img src="docs/figure/reme_structure.jpg" alt="ReMe Logo" width="100%">
</p>
ReMe integrates two complementary memory capabilities:
ReMe integrates three complementary memory capabilities:
#### 🧠 **Task Memory/Experience**
@ -77,6 +76,17 @@ Contextualized memory for specific users
Learn more about how to use personal memory from [personal memory](docs/personal_memory/personal_memory.md)
#### 🔧 **Tool Memory**
Data-driven tool selection and usage optimization
- **Historical Performance Tracking**: Success rates, execution times, and token costs from real usage
- **LLM-as-Judge Evaluation**: Qualitative insights on why tools succeed or fail
- **Parameter Optimization**: Learn optimal parameter configurations from successful calls
- **Dynamic Guidelines**: Transform static tool descriptions into living, learned manuals
Learn more about how to use tool memory from [tool memory](docs/tool_memory/tool_memory.md)
---
## 🛠️ Installation
@ -315,6 +325,140 @@ fetch("http://localhost:8002/retrieve_personal_memory", {
</details>
#### Tool Memory Management
```python
import requests
# Record tool execution results
response = requests.post("http://localhost:8002/add_tool_call_result", json={
"workspace_id": "tool_workspace",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {"query": "Python asyncio tutorial", "max_results": 10},
"output": "Found 10 relevant results...",
"token_cost": 150,
"success": True,
"time_cost": 2.3
}
]
})
# Generate usage guidelines from history
response = requests.post("http://localhost:8002/summary_tool_memory", json={
"workspace_id": "tool_workspace",
"tool_names": "web_search"
})
# Retrieve tool guidelines before use
response = requests.post("http://localhost:8002/retrieve_tool_memory", json={
"workspace_id": "tool_workspace",
"tool_names": "web_search"
})
```
<details>
<summary>curl version</summary>
```bash
# Record tool execution results
curl -X POST http://localhost:8002/add_tool_call_result \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {"query": "Python asyncio tutorial", "max_results": 10},
"output": "Found 10 relevant results...",
"token_cost": 150,
"success": true,
"time_cost": 2.3
}
]
}'
# Generate usage guidelines from history
curl -X POST http://localhost:8002/summary_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_names": "web_search"
}'
# Retrieve tool guidelines before use
curl -X POST http://localhost:8002/retrieve_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_names": "web_search"
}'
```
</details>
<details>
<summary>Node.js version</summary>
```javascript
// Record tool execution results
fetch("http://localhost:8002/add_tool_call_result", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_call_results: [
{
create_time: "2025-10-21 10:30:00",
tool_name: "web_search",
input: {query: "Python asyncio tutorial", max_results: 10},
output: "Found 10 relevant results...",
token_cost: 150,
success: true,
time_cost: 2.3
}
]
})
})
.then(response => response.json())
.then(data => console.log(data));
// Generate usage guidelines from history
fetch("http://localhost:8002/summary_tool_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_names: "web_search"
})
})
.then(response => response.json())
.then(data => console.log(data));
// Retrieve tool guidelines before use
fetch("http://localhost:8002/retrieve_tool_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_names: "web_search"
})
})
.then(response => response.json())
.then(data => console.log(data));
```
</details>
---
## 📦 Ready-to-Use Libraries
@ -386,12 +530,30 @@ We tested ReMe on BFCL-V3 multi-turn-base (randomly split 50train/150val) using
| without ReMe | 0.2472 | 0.2733 | 0.2922 |
| with ReMe | 0.3061 **(+5.89%)** | 0.3500 **(+7.67%)** | 0.3888 **(+9.66%)** |
### 🛠️ [Tool Memory Benchmark](docs/tool_memory/tool_bench.md)
We evaluated Tool Memory effectiveness using a controlled benchmark with three mock search tools using Qwen3-30B-Instruct:
| Scenario | Avg Score | Improvement |
|-----------------------|-----------|--------------------|
| Train (No Memory) | 0.650 | - |
| Test (No Memory) | 0.672 | Baseline |
| **Test (With Memory)** | **0.772** | **+14.88%** |
**Key Findings:**
- Tool Memory enables data-driven tool selection based on historical performance
- Success rates improved by ~15% with learned parameter configurations
You can find more details in [tool_bench.md](docs/tool_memory/tool_bench.md) and the implementation at [run_reme_tool_bench.py](cookbook/tool_memory/run_reme_tool_bench.py).
## 📚 Resources
- **[Quick Start](./cookbook/simple_demo)**: Get started quickly with practical examples
- [Tool Memory Demo](cookbook/simple_demo/use_tool_memory_demo.py): Complete lifecycle demonstration of tool memory
- [Tool Memory Benchmark](cookbook/tool_memory/run_reme_tool_bench.py): Evaluate tool memory effectiveness
- **[Vector Storage Setup](docs/vector_store_api_guide.md)**: Configure local/vector databases and usage
- **[MCP Guide](docs/mcp_quick_start.md)**: Create MCP services
- **[personal memory](docs/personal_memory)** & **[task memory](docs/task_memory)** : Operators used in personal memory and task memory, You can modify the config to customize the pipelines.
- **[Personal Memory](docs/personal_memory)**, **[Task Memory](docs/task_memory)** & **[Tool Memory](docs/tool_memory)**: Operators used in personal memory, task memory and tool memory. You can modify the config to customize the pipelines.
- **[Example Collection](./cookbook)**: Real use cases and best practices
---

View file

@ -1,412 +0,0 @@
中文 | [**English**](./README.md)
<p align="center">
<img src="docs/figure/reme_logo.png" alt="ReMe Logo" width="50%">
</p>
<p align="center">
<a href="https://pypi.org/project/reme-ai/"><img src="https://img.shields.io/badge/python-3.12+-blue" alt="Python Version"></a>
<a href="https://pypi.org/project/reme-ai/"><img src="https://img.shields.io/badge/pypi-v0.1-blue?logo=pypi" alt="PyPI Version"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-black" alt="License"></a>
<a href="https://github.com/modelscope/ReMe"><img src="https://img.shields.io/github/stars/modelscope/ReMe?style=social" alt="GitHub Stars"></a>
</p>
<p align="center">
<strong>ReMe (formerly MemoryScope):为Agent设计的记忆管理框架</strong><br>
<em>Remember Me, Refine Me.</em>
</p>
---
ReMe为AI智能体提供了统一的记忆与经验系统——在跨用户、跨任务、跨智能体下抽取、复用和分享记忆的能力。
```
个性化记忆 (Personal Memory) + 任务经验 (Task Memory)= agent记忆
```
个性化记忆能够"**理解用户偏好**",任务记忆让agent"**做得更好**",
---
## 📰 最新动态
- **[2025-09]** 🎉 ReMe v0.1
正式发布,整合任务记忆与个人记忆。如果想使用原始的memoryscope项目,你可以在[MemoryScope](https://github.com/modelscope/Reme/tree/memoryscope_branch)
中找到。
- **[2025-09]** 🧪 我们在appworld, bfcl(v3)
以及frozenlake环境验证了任务记忆抽取与复用在Agent中的效果,更多信息请查看 [appworld exp](docs/cookbook/appworld/quickstart.md), [bfcl exp](docs/cookbook/bfcl/quickstart.md)
和 [frozenlake exp](docs/cookbook/frozenlake/quickstart.md)。
- **[2025-08]** 🚀 MCP协议支持已上线-> [MCP指南](docs/mcp_quick_start.md)。
- **[2025-06]** 🚀 多后端向量存储支持 (Elasticsearch & ChromaDB) -> [向量数据库指南](docs/vector_store_api_guide.md)。
- **[2024-09]** 🧠 [MemoryScope](https://github.com/modelscope/Reme/tree/memoryscope_branch) v0.1 发布,个性化和时间感知的记忆存储与使用。
---
## ✨ 功能设计
<p align="center">
<img src="docs/figure/reme_structure.jpg" alt="ReMe Logo" width="100%">
</p>
ReMe整合两种互补的记忆能力:
#### 🧠 **任务经验 (Task Memory/Experience)**
跨智能体复用的程序性知识
- **成功模式识别**:识别有效策略并理解其根本原理
- **失败分析学习**:从错误中学习,避免重复同样的问题
- **对比模式**:不同采样轨迹通过对比得到更有价值的经验
- **验证模式**:经过验证模块确认抽取记忆的有效性
你可以从[task memory](docs/task_memory/task_memory.md)了解更多如何使用task memory的方法
#### 👤 **个人记忆 (Personal Memory)**
特定用户的情境化记忆
- **个体偏好**:用户的习惯、偏好和交互风格
- **情境适应**:基于时间和上下文的智能记忆管理
- **渐进学习**:通过长期交互逐步建立深度理解
- **时间感知**:检索和整合时都具备时间敏感性
你可以从[personal memory](docs/personal_memory/personal_memory.md)了解更多如何使用personal memory的方法
---
## 🛠️ 安装
### 从PyPI安装(推荐)
```bash
pip install reme-ai
```
### 从源码安装
```bash
git clone https://github.com/modelscope/ReMe.git
cd ReMe
pip install .
```
### 环境配置
复制 `example.env` 为 .env并修改其中对应参数:
```bash
FLOW_APP_NAME=ReMe
FLOW_LLM_API_KEY=sk-xxxx
FLOW_LLM_BASE_URL=https://xxxx/v1
FLOW_EMBEDDING_API_KEY=sk-xxxx
FLOW_EMBEDDING_BASE_URL=https://xxxx/v1
```
---
## 🚀 快速开始
### HTTP服务启动
```bash
reme \
backend=http \
http.port=8002 \
llm.default.model_name=qwen3-30b-a3b-thinking-2507 \
embedding_model.default.model_name=text-embedding-v4 \
vector_store.default.backend=local
```
### MCP服务器支持
```bash
reme \
backend=mcp \
mcp.transport=stdio \
llm.default.model_name=qwen3-30b-a3b-thinking-2507 \
embedding_model.default.model_name=text-embedding-v4 \
vector_store.default.backend=local
```
### 核心API使用
#### 任务记忆管理
```python
import requests
# 经验总结器:从执行轨迹学习
response = requests.post("http://localhost:8002/summary_task_memory", json={
"workspace_id": "task_workspace",
"trajectories": [
{"messages": [{"role": "user", "content": "帮我制定项目计划"}], "score": 1.0}
]
})
# 经验检索器:获取相关经验
response = requests.post("http://localhost:8002/retrieve_task_memory", json={
"workspace_id": "task_workspace",
"query": "如何高效管理项目进度?",
"top_k": 1
})
```
<details>
<summary>curl 版本</summary>
```bash
# 经验总结器:从执行轨迹学习
curl -X POST http://localhost:8002/summary_task_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "task_workspace",
"trajectories": [
{"messages": [{"role": "user", "content": "帮我制定项目计划"}], "score": 1.0}
]
}'
# 经验检索器:获取相关经验
curl -X POST http://localhost:8002/retrieve_task_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "task_workspace",
"query": "如何高效管理项目进度?",
"top_k": 1
}'
```
</details>
<details>
<summary>Node.js 版本</summary>
```javascript
// 经验总结器:从执行轨迹学习
fetch("http://localhost:8002/summary_task_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "task_workspace",
trajectories: [
{messages: [{role: "user", content: "帮我制定项目计划"}], score: 1.0}
]
})
})
.then(response => response.json())
.then(data => console.log(data));
// 经验检索器:获取相关经验
fetch("http://localhost:8002/retrieve_task_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "task_workspace",
query: "如何高效管理项目进度?",
top_k: 1
})
})
.then(response => response.json())
.then(data => console.log(data));
```
</details>
#### 个人记忆管理
```python
# 记忆整合:从用户交互中学习
response = requests.post("http://localhost:8002/summary_personal_memory", json={
"workspace_id": "task_workspace",
"trajectories": [
{"messages":
[
{"role": "user", "content": "我喜欢早上喝咖啡工作"},
{"role": "assistant", "content": "了解,您习惯早上用咖啡提神来开始工作"}
]
}
]
})
# 记忆检索:获取个人记忆片段
response = requests.post("http://localhost:8002/retrieve_personal_memory", json={
"workspace_id": "task_workspace",
"query": "用户的工作习惯是什么?",
"top_k": 5
})
```
<details>
<summary>curl 版本</summary>
```bash
# 记忆整合:从用户交互中学习
curl -X POST http://localhost:8002/summary_personal_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "task_workspace",
"trajectories": [
{"messages": [
{"role": "user", "content": "我喜欢早上喝咖啡工作"},
{"role": "assistant", "content": "了解,您习惯早上用咖啡提神来开始工作"}
]}
]
}'
# 记忆检索:获取个人记忆片段
curl -X POST http://localhost:8002/retrieve_personal_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "task_workspace",
"query": "用户的工作习惯是什么?",
"top_k": 5
}'
```
</details>
<details>
<summary>Node.js 版本</summary>
```javascript
// 记忆整合:从用户交互中学习
fetch("http://localhost:8002/summary_personal_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "task_workspace",
trajectories: [
{messages: [
{role: "user", content: "我喜欢早上喝咖啡工作"},
{role: "assistant", content: "了解,您习惯早上用咖啡提神来开始工作"}
]}
]
})
})
.then(response => response.json())
.then(data => console.log(data));
// 记忆检索:获取个人记忆片段
fetch("http://localhost:8002/retrieve_personal_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "task_workspace",
query: "用户的工作习惯是什么?",
top_k: 5
})
})
.then(response => response.json())
.then(data => console.log(data));
```
</details>
---
## 📦 即用型经验库
ReMe提供预构建的经验库,智能体可以立即使用经过验证的最佳实践:
### 可用经验库
- **`appworld.jsonl`**:Appworld智能体交互的记忆库,涵盖复杂任务规划和执行模式
- **`bfcl_v3.jsonl`**:BFCL工具调用的工作记忆库
### 快速使用
```python
# 加载预构建经验
response = requests.post("http://localhost:8002/vector_store", json={
"workspace_id": "appworld",
"action": "load",
"path": "./docs/library/"
})
# 查询相关经验
response = requests.post("http://localhost:8002/retrieve_task_memory", json={
"workspace_id": "appworld",
"query": "如何导航到设置并更新用户资料?",
"top_k": 1
})
```
## 🧪 实验
### 🌍 [Appworld 实验](docs/cookbook/appworld/quickstart.md)
我们在 Appworld 上使用 qwen3-8b 测试 ReMe:
| 方法 | pass@1 | pass@2 | pass@4 |
|--------------|-------------------|-------------------|-------------------|
| without ReMe | 0.083 | 0.140 | 0.228 |
| with ReMe | 0.109 **(+2.6%)** | 0.175 **(+3.5%)** | 0.281 **(+5.3%)** |
Pass@K 衡量的是在生成的 K 个样本中,至少有一个成功完成任务(score=1)的概率。
当前实验使用的是一个内部的 AppWorld 环境,可能存在轻微差异。
你可以在 [quickstart.md](docs/cookbook/appworld/quickstart.md) 中找到复现实验的更多细节。
### 🧊 [Frozenlake 实验](docs/cookbook/frozenlake/quickstart.md)
| 不使用ReMe | 使用ReMe |
|:--------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------:|
| <p align="center"><img src="docs/figure/frozenlake_failure.gif" alt="GIF 1" width="30%"></p> | <p align="center"><img src="docs/figure/frozenlake_success.gif" alt="GIF 2" width="30%"></p> |
我们在 100 个随机 frozenlake 地图上使用 qwen3-8b 进行测试:
| 方法 | pass rate |
|--------------|------------------|
| without ReMe | 0.66 |
| with ReMe | 0.72 **(+6.0%)** |
你可以在 [quickstart.md](docs/cookbook/frozenlake/quickstart.md) 中找到复现实验的更多细节。
### 🔧 [BFCL-V3 实验](docs/cookbook/bfcl/quickstart.md)
我们在 BFCL-V3 multi-turn-base (随机划分50train/150val) 上使用 qwen3-8b 测试 ReMe:
| 方法 | pass@1 | pass@2 | pass@4 |
|--------------|---------------------|---------------------|---------------------|
| without ReMe | 0.2472 | 0.2733 | 0.2922 |
| with ReMe | 0.3061 **(+5.89%)** | 0.3500 **(+7.67%)** | 0.3888 **(+9.66%)** |
## 📚 相关资源
- **[快速开始](./cookbook/simple_demo)**:通过实际示例快速上手
- **[向量存储设置](docs/vector_store_api_guide.md)**:配置本地/向量数据库以及使用
- **[mcp指南](docs/mcp_quick_start.md)**:创建mcp服务
- **[个性化记忆](docs/personal_memory)** 与 [任务记忆](docs/task_memory): 个性化记忆与任务记忆中分别使用的算子及其含义,你可以修改config以自定义链路
- **[示例集合](./cookbook)**:实际用例和最佳实践
---
## 🤝 贡献
我们相信最好的记忆系统来自集体智慧。欢迎贡献👉[指南](docs/contribution.md):
### 代码贡献
- 新操作和工具开发
- 后端实现和优化
- API增强和新端点
### 文档改进
- 使用示例和教程
- 最佳实践指南
---
## 📄 引用
```bibtex
@software{ReMe2025,
title = {ReMe: Memory Management Framework for Agents},
author = {Li Yu, Jiaji Deng, Zouying Cao},
url = {https://github.com/modelscope/ReMe},
year = {2025}
}
```
---
## ⚖️ 许可证
本项目采用Apache License 2.0许可证 - 详情请参阅[LICENSE](./LICENSE)文件。
---
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=modelscope/ReMe&type=Date)](https://www.star-history.com/#modelscope/ReMe&Date)

View file

@ -0,0 +1,238 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Tool Memory Demo - 展示工具记忆的完整生命周期"""
import json
import time
from typing import List, Dict, Any, Optional
import requests
from dotenv import load_dotenv
from reme_ai.utils.tool_memory_utils import create_mock_tool_call_results
load_dotenv()
BASE_URL = "http://0.0.0.0:8002/"
WORKSPACE_ID = "test_tool_memory_workspace"
def api_call(endpoint: str, data: dict) -> Optional[Dict[str, Any]]:
"""统一的API调用处理"""
response = requests.post(f"{BASE_URL}{endpoint}", json=data)
if response.status_code != 200:
print(f"Error: {response.status_code} - {response.text}")
return None
return response.json()
def delete_workspace() -> None:
"""删除工作空间
curl example:
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"action": "delete"
}'
"""
result = api_call("vector_store", {"workspace_id": WORKSPACE_ID, "action": "delete"})
if result:
print(f"✓ Workspace '{WORKSPACE_ID}' deleted")
def add_tool_call_results(tool_call_results: List[Dict[str, Any]]) -> bool:
"""添加工具调用结果到记忆库
curl example:
curl -X POST http://0.0.0.0:8002/add_tool_call_result \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"tool_call_results": [
{
"tool_name": "web_search",
"tool_input": "Python tutorials",
"tool_output": "Found 100 results about Python",
"execution_time": 0.5
}
]
}'
"""
# 统计不同的工具
tool_names = set(r.get("tool_name") for r in tool_call_results)
print(f"\n[ADD] {len(tool_call_results)} results for {len(tool_names)} tools: {', '.join(sorted(tool_names))}")
result = api_call("add_tool_call_result", {
"workspace_id": WORKSPACE_ID,
"tool_call_results": tool_call_results
})
if result:
memory_list = result.get("metadata", {}).get("memory_list", [])
print(f"✓ Added successfully, created/updated {len(memory_list)} tool memories")
return True
return False
def summarize_tool_memory(tool_names: str) -> Optional[Dict[str, Any]]:
"""总结工具使用模式
curl example:
curl -X POST http://0.0.0.0:8002/summary_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"tool_names": "web_search,database_query"
}'
"""
print(f"\n[SUMMARIZE] {tool_names}")
result = api_call("summary_tool_memory", {
"workspace_id": WORKSPACE_ID,
"tool_names": tool_names
})
if result:
memory_list = result.get("metadata", {}).get("memory_list", [])
print(f"✓ Summarized {len(memory_list)} tool memories")
for memory in memory_list:
print(f"\n{'=' * 60}")
print(f"Tool: {memory.get('when_to_use', 'N/A')}")
print(f"{'=' * 60}")
print(memory.get('content', 'No content'))
return result
def retrieve_tool_memory(tool_names: str, save_to_file: bool = False) -> str:
"""检索工具记忆
curl example:
curl -X POST http://0.0.0.0:8002/retrieve_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"tool_names": "web_search"
}'
"""
print(f"\n[RETRIEVE] {tool_names}")
result = api_call("retrieve_tool_memory", {
"workspace_id": WORKSPACE_ID,
"tool_names": tool_names
})
if not result:
return ""
memory_list = result.get("metadata", {}).get("memory_list", [])
if not memory_list:
print("No memories found")
return ""
print(f"✓ Retrieved {len(memory_list)} memories")
formatted_memories = []
for memory in memory_list:
content = f"\nTool: {memory.get('when_to_use', 'N/A')}\n" \
f"Calls: {len(memory.get('tool_call_results', []))}\n" \
f"{'-' * 60}\n{memory.get('content', 'No content')}\n"
formatted_memories.append(content)
print(content)
if save_to_file:
with open("tool_memory.json", "w") as f:
json.dump(memory_list, f, indent=2, ensure_ascii=False)
print("✓ Saved to tool_memory.json")
return "\n".join(formatted_memories)
def dump_memory(path: str = "./") -> None:
"""导出记忆到磁盘
curl example:
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"action": "dump",
"path": "./"
}'
"""
result = api_call("vector_store", {
"workspace_id": WORKSPACE_ID,
"action": "dump",
"path": path
})
if result:
print(f"✓ Memory dumped to {path}")
def load_memory(path: str = "./") -> None:
"""从磁盘加载记忆
curl example:
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "test_tool_memory_workspace",
"action": "load",
"path": "./"
}'
"""
result = api_call("vector_store", {
"workspace_id": WORKSPACE_ID,
"action": "load",
"path": path
})
if result:
print(f"✓ Memory loaded from {path}")
def main() -> None:
# 1. 清理工作空间
print("\n[1] Cleaning workspace...")
delete_workspace()
time.sleep(1)
# 2. 创建和添加模拟工具调用结果
print("\n[2] Adding mock tool call results...")
tools_to_test = [
("web_search", 30),
("database_query", 22),
("file_processor", 18)
]
# 收集所有工具的结果,然后一次性添加
all_mock_results = []
for tool_name, count in tools_to_test:
mock_results = create_mock_tool_call_results(tool_name, count)
all_mock_results.extend(mock_results)
if not add_tool_call_results(all_mock_results):
print("✗ Failed to add results")
else:
time.sleep(1)
# 3. 总结工具记忆
print("\n[3] Summarizing tool memories...")
all_tool_names = ",".join([tool[0] for tool in tools_to_test])
summarize_tool_memory(all_tool_names)
time.sleep(1)
# 4. 检索工具记忆
print("\n[4] Retrieving tool memories...")
for tool_name, _ in tools_to_test:
retrieve_tool_memory(tool_name, save_to_file=True)
time.sleep(0.5)
# 5. 测试记忆持久化
print("\n[5] Testing memory persistence...")
dump_memory()
load_memory()
print("\n" + "=" * 60)
print("DEMO COMPLETE ✓")
print("=" * 60)
if __name__ == "__main__":
main()

View file

View file

@ -0,0 +1,139 @@
{
"train": {
"simple": [
"法国的首都是什么?",
"Python 是什么时候首次发布的?",
"地球上有几个大洲?",
"水的沸点是多少度?",
"谁发明了电话?",
"一年有多少天?",
"光速是多少?",
"氧气的化学符号是什么?",
"世界上最高的山峰是哪座?",
"太阳系有几颗行星?",
"地球的半径是多少公里?",
"元素周期表有多少个元素?",
"蒙娜丽莎是谁画的?",
"人类DNA有多少条染色体?",
"HTTP的默认端口号是多少?",
"世界上人口最多的国家是哪个?",
"一公里等于多少米?",
"谁提出了相对论?",
"圆周率π的前两位小数是多少?",
"中国的货币单位是什么?"
],
"moderate": [
"列举Python 3.10的主要特性",
"微服务架构有哪些好处?",
"解释区块链技术的工作原理",
"描述SQL和NoSQL数据库的主要区别",
"软件工程中常见的设计模式有哪些?",
"什么是RESTful API的设计原则?",
"解释Docker容器化技术的优势",
"云计算的三种服务模型是什么?",
"什么是机器学习中的过拟合问题?",
"解释Git中merge和rebase的区别",
"什么是持续集成和持续部署(CI/CD)?",
"描述OSI七层网络模型的作用",
"什么是JWT令牌认证机制?",
"解释虚拟内存的工作原理",
"Kubernetes的主要组件有哪些?",
"什么是函数式编程的核心概念?",
"解释负载均衡的常见策略",
"描述SOLID设计原则的含义",
"什么是异步编程的优势和挑战?",
"解释数据库索引的作用和类型"
],
"complex": [
"比较凯恩斯主义和奥地利学派的经济政策",
"分析可再生能源采用对环境的影响",
"解释量子力学和广义相对论之间的关系",
"讨论人工智能的历史演变过程",
"评估不同机器学习算法在NLP中的有效性",
"分析全球化对发展中国家经济的长期影响",
"比较不同民主制度模型的优缺点",
"探讨气候变化对生物多样性的影响机制",
"研究基因编辑技术的伦理问题和社会影响",
"分析区块链技术在金融领域的应用前景和挑战",
"评估不同神经网络架构在计算机视觉中的表现",
"探讨量子计算对现代密码学的威胁和机遇",
"分析人口老龄化对社会保障体系的影响",
"比较不同哲学流派对人工智能意识的观点",
"研究微生物组与人类健康之间的复杂关系",
"评估碳捕获技术在应对气候变化中的作用",
"分析5G技术对物联网发展的推动作用",
"探讨认知科学与人工智能的交叉研究领域",
"比较不同经济体制下的创新能力差异",
"研究纳米技术在医疗领域的应用和风险"
]
},
"test": {
"simple": [
"日本的首都是什么?",
"Java 语言是哪一年发布的?",
"大西洋有多宽?",
"铁的熔点是多少度?",
"谁发明了汽车?",
"一周有多少小时?",
"声音在空气中的速度是多少?",
"氢气的化学符号是什么?",
"世界上最长的河流是哪条?",
"月球绕地球一周需要多少天?",
"标准大气压是多少帕?",
"人体有多少块骨头?",
"《星空》是谁画的?",
"成年人有多少颗牙齿?",
"HTTPS的默认端口号是多少?",
"世界上面积最大的国家是哪个?",
"一英里等于多少公里?",
"谁发现了万有引力定律?",
"黄金的化学元素符号是什么?",
"美国的货币单位是什么?"
],
"moderate": [
"列举TypeScript 5.0的主要新功能",
"单体架构和微服务架构的区别是什么?",
"解释分布式系统的CAP定理",
"描述关系型数据库的ACID特性",
"前端开发中常用的状态管理方案有哪些?",
"什么是GraphQL的核心优势?",
"解释Kubernetes容器编排的基本概念",
"边缘计算和雾计算有什么区别?",
"什么是神经网络中的梯度消失问题?",
"解释Git中revert和reset的区别",
"什么是DevOps的核心理念?",
"描述TCP/IP协议栈的层次结构",
"什么是OAuth 2.0授权框架?",
"解释进程和线程的本质区别",
"Service Mesh的主要功能是什么?",
"什么是响应式编程的基本原理?",
"解释缓存一致性的常见问题",
"描述DDD领域驱动设计的核心思想",
"什么是并发编程中的死锁问题?",
"解释数据库事务隔离级别的分类"
],
"complex": [
"比较货币主义和供给学派的经济理论差异",
"分析核能发展对能源转型的作用",
"解释弦理论与标准模型之间的联系",
"讨论云计算技术的发展历程和趋势",
"评估深度学习在计算机视觉中的应用效果",
"分析数字化转型对传统产业的影响",
"比较总统制和议会制的政治体制特点",
"探讨海洋酸化对海洋生态系统的影响",
"研究人工智能在医疗诊断中的伦理挑战",
"分析去中心化金融(DeFi)的发展机遇与风险",
"评估Transformer架构在自然语言处理中的优势",
"探讨后量子密码学的研究方向和应用",
"分析城市化进程对资源配置的影响",
"比较实用主义和理想主义对技术伦理的看法",
"研究肠道菌群与神经系统疾病的关联",
"评估直接空气捕获技术的经济可行性",
"分析边缘计算对云计算架构的影响",
"探讨神经语言学与自然语言处理的交叉应用",
"比较计划经济和市场经济的资源配置效率",
"研究量子传感技术在精密测量中的突破"
]
}
}

View file

@ -0,0 +1,633 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Tool Memory Benchmark Script
评估工具记忆在不同场景下的效果,包括有记忆和无记忆的对比
Dependencies:
pip install requests python-dotenv loguru tabulate
"""
import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import List, Dict, Any, Optional
import requests
from dotenv import load_dotenv
from loguru import logger
from tabulate import tabulate
from reme_ai.schema.memory import ToolCallResult, ToolMemory
load_dotenv()
BASE_URL = "http://0.0.0.0:8002/"
TRAIN_WORKSPACE = "train_tool_workspace"
TEST_WORKSPACE = "test_tool_workspace"
class BenchmarkStats:
"""统计数据收集器"""
def __init__(self, name: str):
self.name = name
self.total_count = 0
self.scores = []
def add_result(self, result: Dict[str, Any]):
"""添加一个工具调用结果
Note:
- score: Quality/relevance of the result (0.0 or 1.0)
"""
self.total_count += 1
# Collect score
score = result.get("score", 0)
self.scores.append(score)
def get_summary(self) -> Dict[str, Any]:
"""获取统计摘要"""
if self.total_count == 0:
return {
"name": self.name,
"total_calls": 0,
"avg_score": 0.0
}
return {
"name": self.name,
"total_calls": self.total_count,
"avg_score": round(sum(self.scores) / len(self.scores), 3)
}
def api_call(endpoint: str, data: dict) -> Optional[Dict[str, Any]]:
"""统一的API调用处理"""
try:
response = requests.post(f"{BASE_URL}{endpoint}", json=data, timeout=120)
if response.status_code != 200:
logger.error(f"API Error: {response.status_code} - {response.text}")
return None
return response.json()
except Exception as e:
logger.error(f"API call failed: {e}")
return None
def delete_workspace(workspace_id: str) -> bool:
"""删除工作空间"""
logger.info(f"Deleting workspace: {workspace_id}")
result = api_call("vector_store", {"workspace_id": workspace_id, "action": "delete"})
return result is not None
def load_queries(query_file: str = "query.json") -> Dict[str, Any]:
"""加载查询数据"""
query_path = Path(__file__).parent / query_file
with open(query_path, 'r', encoding='utf-8') as f:
return json.load(f)
def run_use_mock_search(workspace_id: str, queries: List[str], prompt_template: str = "") -> List[ToolCallResult]:
"""运行use_mock_search并收集结果(支持并发)
Args:
workspace_id: 工作空间ID
queries: 查询列表
prompt_template: 提示模板
Returns:
工具调用结果列表
"""
logger.info(f"Running use_mock_search on {workspace_id} with {len(queries)} queries (max concurrency: 4)")
results: List[ToolCallResult] = []
def process_single_query(idx: int, query: str) -> Optional[ToolCallResult]:
"""处理单个查询"""
logger.info(f"[{idx + 1}/{len(queries)}] Processing: {query}")
# 提交之前sleep 1秒
time.sleep(1)
result = api_call("use_mock_search", {
"workspace_id": workspace_id,
"query": prompt_template.format(query=query),
})
if result:
tool_call_result = result.get("answer")
return ToolCallResult(**json.loads(tool_call_result))
else:
logger.warning(f"No result for query: {query}")
return None
# 使用线程池并发处理,最大并发数为4
with ThreadPoolExecutor(max_workers=4) as executor:
# 提交所有任务
future_to_query = {
executor.submit(process_single_query, idx, query): (idx, query)
for idx, query in enumerate(queries)
}
# 收集结果(按完成顺序)
completed_results = []
for future in as_completed(future_to_query):
idx, query = future_to_query[future]
try:
result = future.result()
if result:
completed_results.append((idx, result))
except Exception as e:
logger.error(f"Error processing query [{idx + 1}]: {query}, error: {e}")
# 按原始顺序排序结果
completed_results.sort(key=lambda x: x[0])
results = [result for _, result in completed_results]
logger.info(f"Collected {len(results)} results out of {len(queries)} queries")
return results
def add_tool_call_results(workspace_id: str, results: List[ToolCallResult]) -> List[ToolCallResult]:
"""批量添加工具调用结果到记忆库,并返回带评分的结果
Args:
workspace_id: 工作空间ID
results: 工具调用结果列表
Returns:
从API返回的memory_list中提取的带评分的ToolCallResult列表
"""
if not results:
logger.warning("No results to add")
return []
# 转换为字典用于API调用
tool_call_results = [result.model_dump() for result in results]
logger.info(f"Adding tool call results to {workspace_id}: {len(tool_call_results)} results")
# 统一调用API,让后端自动按tool_name分组处理
api_result = api_call("add_tool_call_result", {
"workspace_id": workspace_id,
"tool_call_results": tool_call_results
})
if not api_result:
logger.error("Failed to add results")
return []
# 收集所有带评分的结果
all_scored_results: List[ToolCallResult] = []
# 解析返回的memory_list(可能包含多个工具的记忆)
memory_list = api_result.get("metadata", {}).get("memory_list", [])
logger.info(f"Received {len(memory_list)} tool memories from API")
for memory_dict in memory_list:
tool_memory = ToolMemory(**memory_dict)
tool_name = tool_memory.when_to_use
scored_results = tool_memory.tool_call_results
all_scored_results.extend(scored_results)
logger.info(f"Extracted {len(scored_results)} scored results from {tool_name}")
# 打印一些评分示例
for idx, result in enumerate(scored_results[:3]):
logger.info(f" Result #{idx + 1}: score={result.score}, success={result.success}")
logger.info(f"Total scored results collected: {len(all_scored_results)}")
return all_scored_results
def summarize_tool_memory(workspace_id: str, tool_names: str) -> bool:
"""总结工具记忆"""
logger.info(f"Summarizing tool memory for {workspace_id}: {tool_names}")
result = api_call("summary_tool_memory", {
"workspace_id": workspace_id,
"tool_names": tool_names
})
if result:
memory_list = result.get("metadata", {}).get("memory_list", [])
logger.info(f"Summarized {len(memory_list)} tool memories")
return True
return False
def retrieve_tool_memory(workspace_id: str, tool_names: str) -> str:
"""检索工具记忆并返回格式化的内容
Args:
workspace_id: 工作空间ID
tool_names: 逗号分隔的工具名称
Returns:
格式化的工具记忆内容,每个工具名称作为一级markdown标题
"""
logger.info(f"Retrieving tool memory for {workspace_id}: {tool_names}")
result = api_call("retrieve_tool_memory", {
"workspace_id": workspace_id,
"tool_names": tool_names
})
if not result:
logger.error("Failed to retrieve tool memory")
return ""
memory_list = result.get("metadata", {}).get("memory_list", [])
logger.info(f"Retrieved {len(memory_list)} tool memories")
# 提取每个工具记忆的content字段,并格式化为markdown
formatted_contents = []
for memory_dict in memory_list:
tool_memory = ToolMemory(**memory_dict)
if tool_memory.content:
# 使用工具名称作为一级markdown标题
tool_name = tool_memory.when_to_use or "Unknown Tool"
formatted_section = f"# {tool_name}\n\n{tool_memory.content}"
formatted_contents.append(formatted_section)
logger.info(f"Retrieved content for tool: {tool_name}, "
f"content_length={len(tool_memory.content)}")
# 用两个换行符分隔不同工具的记忆
joined_content = "\n\n".join(formatted_contents)
logger.info(f"Total content length: {len(joined_content)}")
return joined_content
def collect_statistics(results: List[ToolCallResult], stats: BenchmarkStats) -> None:
"""从结果列表中收集统计数据"""
for result in results:
# 转换为字典用于统计
result_dict = result.model_dump() if hasattr(result, 'model_dump') else result
stats.add_result(result_dict)
def print_comparison_table(stats_list: List[BenchmarkStats]) -> None:
"""打印对比表格"""
headers = ["Scenario", "Total Calls", "Avg Score"]
rows = []
for stats in stats_list:
summary = stats.get_summary()
rows.append([
summary["name"],
summary["total_calls"],
summary["avg_score"]
])
print("\n" + "=" * 100)
print("BENCHMARK RESULTS COMPARISON")
print("=" * 100)
print("Note: Avg Score = average quality score")
print(tabulate(rows, headers=headers, tablefmt="grid"))
print("=" * 100)
def calculate_improvements(baseline_stats: BenchmarkStats, improved_stats: BenchmarkStats) -> Dict[str, float]:
"""计算改进百分比"""
baseline = baseline_stats.get_summary()
improved = improved_stats.get_summary()
improvements = {}
# 平均分数改进(相对提升百分比)
if baseline["avg_score"] > 0:
improvements["avg_score"] = ((improved["avg_score"] - baseline["avg_score"])
/ baseline["avg_score"] * 100)
else:
improvements["avg_score"] = 0.0
return improvements
def print_improvements(improvements: Dict[str, float]) -> None:
"""打印改进情况"""
print("\n" + "=" * 100)
print("IMPROVEMENTS WITH TOOL MEMORY (Baseline: Test without memory)")
print("=" * 100)
metric_labels = {
"avg_score": "Average Score"
}
for metric, improvement in improvements.items():
label = metric_labels.get(metric, metric)
direction = "↑" if improvement > 0 else "↓"
print(f"{label:25s}: {improvement:+7.2f}% {direction}")
print("=" * 100)
def save_results(results: Dict[str, Any], filename: str = "benchmark_results.json") -> None:
"""保存结果到文件"""
output_path = Path(__file__).parent / filename
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
logger.info(f"Results saved to {output_path}")
def run_single_epoch(epoch_num: int, train_queries: List[str], test_queries: List[str]) -> Dict[str, Any]:
"""运行单个epoch的benchmark
Args:
epoch_num: epoch编号(从1开始)
train_queries: 训练查询列表
test_queries: 测试查询列表
Returns:
包含该epoch统计结果的字典
"""
logger.info("\n" + "=" * 100)
logger.info(f"EPOCH {epoch_num} - START")
logger.info("=" * 100)
# 初始化统计收集器
train_no_memory_stats = BenchmarkStats(f"Epoch{epoch_num} - Train (No Memory)")
test_no_memory_stats = BenchmarkStats(f"Epoch{epoch_num} - Test (No Memory)")
test_with_memory_stats = BenchmarkStats(f"Epoch{epoch_num} - Test (With Memory)")
all_results = {}
# ==================== 步骤1: 无记忆在train上的效果 ====================
print("\n" + "=" * 100)
print(f"[EPOCH {epoch_num}] [STEP 1/5] Running on TRAIN without memory...")
print("=" * 100)
logger.info("Deleting workspace and starting fresh...")
delete_workspace(TRAIN_WORKSPACE)
time.sleep(2)
prompt_template = "必须选择一个工具来回答问题\n 问题\n{query}"
train_results_no_memory = run_use_mock_search(TRAIN_WORKSPACE, train_queries, prompt_template)
# 添加结果到记忆库并获取带评分的结果
train_scored_results = add_tool_call_results(TRAIN_WORKSPACE, train_results_no_memory)
time.sleep(2)
# 使用带评分的结果进行统计(如果有的话)
if train_scored_results:
logger.info(f"Using {len(train_scored_results)} scored results for statistics")
all_results["train_no_memory"] = train_scored_results
collect_statistics(train_scored_results, train_no_memory_stats)
else:
logger.warning("No scored results returned, using original results")
all_results["train_no_memory"] = train_results_no_memory
collect_statistics(train_results_no_memory, train_no_memory_stats)
print(f"✓ Train (no memory) completed: {len(train_results_no_memory)}/{len(train_queries)} results collected")
summary = train_no_memory_stats.get_summary()
print(f" Avg Score: {summary['avg_score']:.3f}")
# ==================== 步骤2: 无记忆在test上的效果 ====================
print("\n" + "=" * 100)
print(f"[EPOCH {epoch_num}] [STEP 2/5] Running on TEST without memory...")
print("=" * 100)
logger.info("Deleting workspace and starting fresh...")
delete_workspace(TEST_WORKSPACE)
time.sleep(2)
prompt_template = "必须选择一个工具来回答问题\n 问题\n{query}"
test_results_no_memory = run_use_mock_search(TEST_WORKSPACE, test_queries, prompt_template)
# 添加结果到记忆库并获取带评分的结果
# 注意:这些结果会作为TEST_WORKSPACE的初始记忆,在步骤4会被复用
test_scored_results_no_memory = add_tool_call_results(TEST_WORKSPACE, test_results_no_memory)
time.sleep(2)
# 使用带评分的结果进行统计(如果有的话)
if test_scored_results_no_memory:
logger.info(f"Using {len(test_scored_results_no_memory)} scored results for statistics")
all_results["test_no_memory"] = test_scored_results_no_memory
collect_statistics(test_scored_results_no_memory, test_no_memory_stats)
else:
logger.warning("No scored results returned, using original results")
all_results["test_no_memory"] = test_results_no_memory
collect_statistics(test_results_no_memory, test_no_memory_stats)
print(f"✓ Test (no memory) completed: {len(test_results_no_memory)}/{len(test_queries)} results collected")
summary = test_no_memory_stats.get_summary()
print(f" Avg Score: {summary['avg_score']:.3f}")
# ==================== 步骤3: 总结train的工具记忆 ====================
print("\n" + "=" * 100)
print(f"[EPOCH {epoch_num}] [STEP 3/5] Summarizing tool memory from TRAIN...")
print("=" * 100)
# 获取所有工具名称(使用带评分的结果)
tool_names_set = set()
results_to_use = train_scored_results if train_scored_results else train_results_no_memory
for result in results_to_use:
tool_name = result.tool_name if hasattr(result, 'tool_name') else None
if tool_name:
tool_names_set.add(tool_name)
tool_names_str = ",".join(sorted(tool_names_set))
print(f"Tools to summarize: {tool_names_str}")
success = summarize_tool_memory(TRAIN_WORKSPACE, tool_names_str)
if not success:
logger.error("Failed to summarize tool memory")
return {}
time.sleep(3)
print("✓ Tool memory summarized successfully")
# 检索工具记忆内容
memories = retrieve_tool_memory(TRAIN_WORKSPACE, tool_names_str)
if not memories:
logger.error("Failed to retrieve tool memory content")
return {}
logger.info(f"Retrieved tool memory content, total length: {len(memories)}")
print("\n" + "-" * 100)
print("Retrieved Tool Memory Content:")
print("-" * 100)
print(memories)
print("-" * 100)
# ==================== 步骤4: 有记忆在test上的效果 ====================
print("\n" + "=" * 100)
print(f"[EPOCH {epoch_num}] [STEP 4/5] Running on TEST with memory (after clearing existing memory)...")
print("=" * 100)
# 先清理TEST_WORKSPACE中已有的记忆记录(Step 2的60条结果)
print("Deleting existing memory records from TEST workspace...")
delete_workspace(TEST_WORKSPACE)
time.sleep(2) # 等待删除完成
print("✓ TEST workspace memory cleared")
# 通过prompt注入train阶段总结的记忆,测量记忆增强的效果
# 注意:此时workspace是空的,只通过prompt提供记忆信息
prompt_template = f"工具信息\n{memories}\n必须选择一个工具来回答问题\n 问题\n" + "{query}"
test_results_with_memory = run_use_mock_search(TEST_WORKSPACE, test_queries, prompt_template)
# 添加这些结果到记忆库并获取带评分的结果
# 此时workspace已清空,返回的就是本次新增的60条结果
test_all_results_with_memory = add_tool_call_results(TEST_WORKSPACE, test_results_with_memory)
time.sleep(2)
# workspace已清空,所有返回的结果都是本次新增的
if test_all_results_with_memory:
test_scored_results_with_memory = test_all_results_with_memory
logger.info(f"Using {len(test_scored_results_with_memory)} scored results for statistics")
all_results["test_with_memory"] = test_scored_results_with_memory
collect_statistics(test_scored_results_with_memory, test_with_memory_stats)
else:
logger.warning("No scored results returned, using original results")
all_results["test_with_memory"] = test_results_with_memory
collect_statistics(test_results_with_memory, test_with_memory_stats)
print(f"✓ Test (with memory) completed: {len(test_results_with_memory)}/{len(test_queries)} results collected")
summary = test_with_memory_stats.get_summary()
print(f" Avg Score: {summary['avg_score']:.3f}")
# ==================== 步骤5: 打印对比结果 ====================
print("\n" + "=" * 100)
print(f"[EPOCH {epoch_num}] [STEP 5/5] Generating comparison report and analysis...")
print("=" * 100)
# 打印统计表格
print_comparison_table([train_no_memory_stats, test_no_memory_stats, test_with_memory_stats])
# 计算并打印改进情况
improvements = calculate_improvements(test_no_memory_stats, test_with_memory_stats)
print_improvements(improvements)
logger.info(f"EPOCH {epoch_num} - COMPLETE")
return {
"epoch": epoch_num,
"statistics": {
"train_no_memory": train_no_memory_stats.get_summary(),
"test_no_memory": test_no_memory_stats.get_summary(),
"test_with_memory": test_with_memory_stats.get_summary()
},
"improvements": improvements
}
def main(test_mode: bool = False, run_epoch: int = 3):
"""主函数:运行完整的benchmark流程
Args:
test_mode: 如果为True,只使用每个难度级别的前3个查询进行快速测试
run_epoch: 运行的epoch数量,默认为3
"""
logger.info("=" * 100)
logger.info("TOOL MEMORY BENCHMARK - START")
if test_mode:
logger.info("Running in TEST MODE (limited queries)")
logger.info(f"Total Epochs: {run_epoch}")
logger.info("=" * 100)
# 加载查询数据
queries_data = load_queries()
train_queries = []
test_queries = []
# 合并所有难度级别的查询
for difficulty in ["simple", "moderate", "complex"]:
train_data = queries_data["train"].get(difficulty, [])
test_data = queries_data["test"].get(difficulty, [])
if test_mode:
train_queries.extend(train_data[:5])
test_queries.extend(test_data[:5])
else:
train_queries.extend(train_data)
test_queries.extend(test_data)
logger.info(f"Loaded {len(train_queries)} train queries and {len(test_queries)} test queries")
# 运行多个epoch并收集结果
all_epoch_results = []
for epoch in range(1, run_epoch + 1):
epoch_result = run_single_epoch(epoch, train_queries, test_queries)
if epoch_result:
all_epoch_results.append(epoch_result)
else:
logger.error(f"Epoch {epoch} failed, skipping...")
# ==================== 计算多轮平均效果 ====================
if not all_epoch_results:
logger.error("No successful epochs, cannot calculate averages")
return
print("\n" + "=" * 100)
print("MULTI-EPOCH AVERAGE RESULTS")
print("=" * 100)
# 计算每个场景的平均分数
avg_train_no_memory = sum(e["statistics"]["train_no_memory"]["avg_score"] for e in all_epoch_results) / len(
all_epoch_results)
avg_test_no_memory = sum(e["statistics"]["test_no_memory"]["avg_score"] for e in all_epoch_results) / len(
all_epoch_results)
avg_test_with_memory = sum(e["statistics"]["test_with_memory"]["avg_score"] for e in all_epoch_results) / len(
all_epoch_results)
# 计算平均改进
avg_improvement = sum(e["improvements"]["avg_score"] for e in all_epoch_results) / len(all_epoch_results)
# 打印汇总表格
headers = ["Scenario", "Avg Score (across epochs)"]
rows = [
["Train (No Memory)", f"{avg_train_no_memory:.3f}"],
["Test (No Memory)", f"{avg_test_no_memory:.3f}"],
["Test (With Memory)", f"{avg_test_with_memory:.3f}"]
]
print(tabulate(rows, headers=headers, tablefmt="grid"))
print("\n" + "-" * 100)
print(f"Average Improvement (Test with memory vs without): {avg_improvement:+.2f}%")
print("-" * 100)
# 打印每个epoch的详细结果
print("\n" + "=" * 100)
print("PER-EPOCH BREAKDOWN")
print("=" * 100)
headers = ["Epoch", "Train (No Mem)", "Test (No Mem)", "Test (With Mem)", "Improvement %"]
rows = []
for e in all_epoch_results:
rows.append([
f"Epoch {e['epoch']}",
f"{e['statistics']['train_no_memory']['avg_score']:.3f}",
f"{e['statistics']['test_no_memory']['avg_score']:.3f}",
f"{e['statistics']['test_with_memory']['avg_score']:.3f}",
f"{e['improvements']['avg_score']:+.2f}%"
])
print(tabulate(rows, headers=headers, tablefmt="grid"))
# 保存最终结果
benchmark_summary = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"total_epochs": run_epoch,
"successful_epochs": len(all_epoch_results),
"average_results": {
"train_no_memory": avg_train_no_memory,
"test_no_memory": avg_test_no_memory,
"test_with_memory": avg_test_with_memory,
"improvement": avg_improvement
},
"per_epoch_results": all_epoch_results
}
save_results(benchmark_summary, "tool_memory_benchmark_results.json")
logger.info("\n" + "=" * 100)
logger.info("TOOL MEMORY BENCHMARK - COMPLETE")
logger.info(f"Successfully completed {len(all_epoch_results)}/{run_epoch} epochs")
logger.info("=" * 100)
if __name__ == "__main__":
main(test_mode=False, run_epoch=3)

View file

@ -26,11 +26,11 @@ show_datetime: true
ReMe provides AI agents with a unified memory system—enabling the ability to extract, reuse, and share memories across
users, tasks, and agents.
!!! info "Personal Memory + Task Memory = Agent Memory"
Personal memory helps "**understand user preferences**", while task memory helps agents "**perform better**".
```
Personal Memory + Task Memory + Tool Memory = Agent Memory
```
Personal memory helps "**understand user preferences**", task memory helps agents "**perform better**", and tool memory enables "**smarter tool usage**".
## Architecture Design
@ -38,7 +38,7 @@ Personal memory helps "**understand user preferences**", while task memory helps
<img src="figure/reme_structure.jpg" alt="ReMe Logo" width="100%">
</p>
ReMe integrates two complementary memory capabilities:
ReMe integrates three complementary memory capabilities:
!!! note "Task Memory/Experience"
@ -59,10 +59,18 @@ ReMe integrates two complementary memory capabilities:
Contextualized memory for specific users
- **Individual Preferences**: User habits, preferences, and interaction styles
- **Contextual Adaptation**: Intelligent memory management based on time and context
- **Progressive Learning**: Gradually build deep understanding through long-term interaction
- **Time Awareness**: Time sensitivity in both retrieval and integration
#### 🔧 **Tool Memory**
Data-driven tool selection and usage optimization
- **Historical Performance Tracking**: Success rates, execution times, and token costs from real usage
- **LLM-as-Judge Evaluation**: Qualitative insights on why tools succeed or fail
- **Parameter Optimization**: Learn optimal parameter configurations from successful calls
- **Dynamic Guidelines**: Transform static tool descriptions into living, learned manuals
Learn more about how to use tool memory from [tool memory](tool_memory/tool_memory.md)
---
Learn more about how to use personal memory from [personal memory](personal_memory/personal_memory.md)
@ -303,15 +311,236 @@ fetch("http://localhost:8002/retrieve_personal_memory", {
</details>
#### Tool Memory Management
```python
import requests
# Record tool execution results
response = requests.post("http://localhost:8002/add_tool_call_result", json={
"workspace_id": "tool_workspace",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {"query": "Python asyncio tutorial", "max_results": 10},
"output": "Found 10 relevant results...",
"token_cost": 150,
"success": True,
"time_cost": 2.3
}
]
})
# Generate usage guidelines from history
response = requests.post("http://localhost:8002/summary_tool_memory", json={
"workspace_id": "tool_workspace",
"tool_names": "web_search"
})
# Retrieve tool guidelines before use
response = requests.post("http://localhost:8002/retrieve_tool_memory", json={
"workspace_id": "tool_workspace",
"tool_names": "web_search"
})
```
<details>
<summary>curl version</summary>
```bash
# Record tool execution results
curl -X POST http://localhost:8002/add_tool_call_result \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {"query": "Python asyncio tutorial", "max_results": 10},
"output": "Found 10 relevant results...",
"token_cost": 150,
"success": true,
"time_cost": 2.3
}
]
}'
# Generate usage guidelines from history
curl -X POST http://localhost:8002/summary_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_names": "web_search"
}'
# Retrieve tool guidelines before use
curl -X POST http://localhost:8002/retrieve_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "tool_workspace",
"tool_names": "web_search"
}'
```
</details>
<details>
<summary>Node.js version</summary>
```javascript
// Record tool execution results
fetch("http://localhost:8002/add_tool_call_result", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_call_results: [
{
create_time: "2025-10-21 10:30:00",
tool_name: "web_search",
input: {query: "Python asyncio tutorial", max_results: 10},
output: "Found 10 relevant results...",
token_cost: 150,
success: true,
time_cost: 2.3
}
]
})
})
.then(response => response.json())
.then(data => console.log(data));
// Generate usage guidelines from history
fetch("http://localhost:8002/summary_tool_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_names: "web_search"
})
})
.then(response => response.json())
.then(data => console.log(data));
// Retrieve tool guidelines before use
fetch("http://localhost:8002/retrieve_tool_memory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
workspace_id: "tool_workspace",
tool_names: "web_search"
})
})
.then(response => response.json())
.then(data => console.log(data));
```
</details>
---
## Resources
- **[Personal memory](personal_memory/personal_memory.md)** & **[Task memory](task_memory/task_memory.md)** : Operators used in personal memory and task memory, You can modify the config to customize the pipelines.
- **[Example Collection](cookbook/experiment_overview.md)**: Real use cases and best practices
- **[Library](./library/library.md)**: Directly use existing task memory/experience for your tasks, and you can also contribute more task memory/experience to us.
- **[Contribution](contribution.md)**: welcome to your contributions!
## 📦 Ready-to-Use Libraries
ReMe provides pre-built memory libraries that agents can immediately use with verified best practices:
### Available Libraries
- **`appworld.jsonl`**: Memory library for Appworld agent interactions, covering complex task planning and execution
patterns
- **`bfcl_v3.jsonl`**: Working memory library for BFCL tool calls
### Quick Usage
```python
# Load pre-built memories
response = requests.post("http://localhost:8002/vector_store", json={
"workspace_id": "appworld",
"action": "load",
"path": "./docs/library/"
})
# Query relevant memories
response = requests.post("http://localhost:8002/retrieve_task_memory", json={
"workspace_id": "appworld",
"query": "How to navigate to settings and update user profile?",
"top_k": 1
})
```
## 🧪 Experiments
### 🌍 [Appworld Experiment](cookbook/appworld/quickstart.md)
We tested ReMe on Appworld using qwen3-8b:
| Method | pass@1 | pass@2 | pass@4 |
|--------------|-------------------|-------------------|-------------------|
| without ReMe | 0.083 | 0.140 | 0.228 |
| with ReMe | 0.109 **(+2.6%)** | 0.175 **(+3.5%)** | 0.281 **(+5.3%)** |
Pass@K measures the probability that at least one of the K generated samples successfully completes the task (
score=1).
The current experiment uses an internal AppWorld environment, which may have slight differences.
You can find more details on reproducing the experiment in [quickstart.md](cookbook/appworld/quickstart.md).
### 🧊 [Frozenlake Experiment](./cookbook/frozenlake/quickstart.md)
| without ReMe | with ReMe |
|:-------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:|
| <p align="center"><img src="figure/frozenlake_failure.gif" alt="GIF 1" width="30%"></p> | <p align="center"><img src="figure/frozenlake_success.gif" alt="GIF 2" width="30%"></p> |
We tested on 100 random frozenlake maps using qwen3-8b:
| Method | pass rate |
|--------------|------------------|
| without ReMe | 0.66 |
| with ReMe | 0.72 **(+6.0%)** |
You can find more details on reproducing the experiment in [quickstart.md](cookbook/frozenlake/quickstart.md).
### 🔧 [BFCL-V3 Experiment](./cookbook/bfcl/quickstart.md)
We tested ReMe on BFCL-V3 multi-turn-base (randomly split 50train/150val) using qwen3-8b:
| Method | pass@1 | pass@2 | pass@4 |
|--------------|---------------------|---------------------|---------------------|
| without ReMe | 0.2472 | 0.2733 | 0.2922 |
| with ReMe | 0.3061 **(+5.89%)** | 0.3500 **(+7.67%)** | 0.3888 **(+9.66%)** |
### 🛠️ [Tool Memory Benchmark](tool_memory/tool_bench.md)
We evaluated Tool Memory effectiveness using a controlled benchmark with three mock search tools using Qwen3-30B-Instruct:
| Scenario | Avg Score | Improvement |
|-----------------------|-----------|--------------------|
| Train (No Memory) | 0.650 | - |
| Test (No Memory) | 0.672 | Baseline |
| **Test (With Memory)** | **0.772** | **+14.88%** |
**Key Findings:**
- Tool Memory enables data-driven tool selection based on historical performance
- Success rates improved by ~15% with learned parameter configurations
You can find more details in [tool_bench.md](tool_memory/tool_bench.md) and the implementation at [run_reme_tool_bench.py](https://github.com/modelscope/ReMe/tree/main/cookbook/tool_memory/run_reme_tool_bench.py).
## 📚 Resources
- **[Quick Start](https://github.com/modelscope/ReMe/tree/main/cookbook/simple_demo)**: Get started quickly with practical examples
- [Tool Memory Demo](https://github.com/modelscope/ReMe/tree/main/cookbook/simple_demo/use_tool_memory_demo.py): Complete lifecycle demonstration of tool memory
- [Tool Memory Benchmark](https://github.com/modelscope/ReMe/tree/main/cookbook/tool_memory/run_reme_tool_bench.py): Evaluate tool memory effectiveness
- **[Vector Storage Setup](vector_store_api_guide.md)**: Configure local/vector databases and usage
- **[MCP Guide](mcp_quick_start.md)**: Create MCP services
- **[Personal Memory](personal_memory/personal_memory.md)**, **[Task Memory](task_memory/task_memory.md)** & **[Tool Memory](tool_memory/tool_memory.md)**: Operators used in personal memory, task memory and tool memory. You can modify the config to customize the pipelines.
- **[Example Collection](./cookbook/appworld/quickstart.md)**: Real use cases and best practices
---

View file

@ -199,4 +199,4 @@ ReMe also provides additional task memory operations:
- `record_task_memory`: Update frequency and utility attributes of retrieved memories
- `delete_task_memory`: Delete memories based on utility/frequency thresholds
For more detailed examples, see the `use_task_memory_demo.py` file in the cookbook directory of the ReMe project.
For more detailed examples, see the `use_task_memory_demo.py` file in the cookbook directory of the ReMe project.

View file

@ -70,4 +70,4 @@ An alternative to RewriteMemoryOp that merges multiple memories into a single re
- Collects the content from all memories in the memory list
- Formats them into a single response with a standard structure
- Adds a prompt to consider the helpful parts when answering the question
- Adds a prompt to consider the helpful parts when answering the question

View file

@ -160,4 +160,4 @@ A simplified version of comparative memory extraction.
### Parameters
No specific parameters beyond the LLM configuration.
No specific parameters beyond the LLM configuration.

25
docs/todo.md Normal file
View file

@ -0,0 +1,25 @@
# TODO
## Planned Features
### 1. Automatic Tool Exploration Mode
Add an automatic tool exploration mode that generates tool memory by:
- Automatically discovering and testing available tools
- Learning tool usage patterns and best practices
- Building a comprehensive tool memory database from exploration results
### 2. Desktop Pet Personal Assistant
Build a desktop pet personal assistant with:
- Interactive desktop companion interface
- Personalized assistance capabilities
- Integration with ReMe's memory system
### 3. Task Memory Research Implementation
We are currently working on implementing features based on task memory research papers. Coming soon.
### 4. Mem-Agent Exploration
We are exploring mem-agent to implement agentic memory pathways:
- Investigating agent-driven memory management
- Developing autonomous memory retrieval and storage mechanisms
- Building more intelligent memory update strategies

View file

@ -0,0 +1,256 @@
# Tool Memory Benchmark
## Overview
This benchmark evaluates Tool Memory effectiveness by comparing agent performance with and without tool memory across multiple epochs. The experiment uses mock search tools with varying performance characteristics for different query complexities.
## Experimental Setup
### Mock Search Tools
Three LLM-based mock search tools with different performance profiles:
| Tool | Simple Queries | Medium Queries | Complex Queries |
|------|---------------|----------------|-----------------|
| **SearchToolA** | ⭐⭐⭐ Fast, high success (90%) | ❌ Poor (20% success) | ⚠️ Weak (50% success) |
| **SearchToolB** | ⚠️ Over-engineered (30%) | ⭐⭐⭐ Optimal (90% success) | ⚠️ Limited (50% success) |
| **SearchToolC** | ⚠️ Overkill (30%) | ⚠️ Excessive (40%) | ⭐⭐⭐ Best (90% success) |
**Performance Characteristics:**
- `success_rate`: Probability of successful execution (vs "Service busy" error)
- `relevance_ratio`: Probability of returning relevant results (vs random content)
- `extra_time`: Simulated latency (currently 0 in implementation)
Each tool uses LLM to classify query complexity and generate appropriate responses.
### Query Dataset
**Source:** `cookbook/tool_memory/query.json`
- **Train Set**: 20 queries per complexity × 3 levels = 60 queries
- **Test Set**: 20 queries per complexity × 3 levels = 60 queries
- **Complexity Levels**: simple, moderate, complex
## Benchmark Workflow
### Single Epoch Process
Each epoch consists of 5 steps:
#### Step 1: Train without Memory
```python
# Execute all train queries on TRAIN_WORKSPACE
# Agent selects tools without historical guidance
run_use_mock_search(TRAIN_WORKSPACE, train_queries, prompt_template)
# Add results to memory and get scored results
train_scored_results = add_tool_call_results(TRAIN_WORKSPACE, train_results)
```
#### Step 2: Test without Memory
```python
# Execute all test queries on TEST_WORKSPACE (fresh workspace)
# Baseline performance without tool memory
run_use_mock_search(TEST_WORKSPACE, test_queries, prompt_template)
# Add results to memory (will be cleared in Step 4)
test_scored_results = add_tool_call_results(TEST_WORKSPACE, test_results)
```
#### Step 3: Summarize Tool Memory
```python
# Summarize tool performance from TRAIN_WORKSPACE
summarize_tool_memory(TRAIN_WORKSPACE, "SearchToolA,SearchToolB,SearchToolC")
# Retrieve formatted tool memory content
memories = retrieve_tool_memory(TRAIN_WORKSPACE, tool_names)
```
The summarization produces memory content including:
- Best/worst use cases per tool
- Statistical metrics (avg score, success rate, token cost, time cost)
- Usage recommendations
#### Step 4: Test with Memory
```python
# Clear TEST_WORKSPACE to start fresh
delete_workspace(TEST_WORKSPACE)
# Inject tool memory into prompt
prompt_with_memory = f"Tool Information\n{memories}\nMust select one tool to answer\nQuery\n{query}"
# Execute test queries with memory guidance
run_use_mock_search(TEST_WORKSPACE, test_queries, prompt_with_memory)
# Add results and get scored results
test_scored_results_with_memory = add_tool_call_results(TEST_WORKSPACE, test_results)
```
#### Step 5: Compare Results
```python
# Generate comparison table
print_comparison_table([train_no_memory_stats, test_no_memory_stats, test_with_memory_stats])
# Calculate improvements (baseline: test without memory)
improvements = calculate_improvements(test_no_memory_stats, test_with_memory_stats)
print_improvements(improvements)
```
### Multi-Epoch Execution
```bash
# Run benchmark with 3 epochs
python cookbook/tool_memory/run_reme_tool_bench.py
# Test mode (5 queries per complexity level)
main(test_mode=True, run_epoch=3)
# Full mode (20 queries per complexity level)
main(test_mode=False, run_epoch=3)
```
## Key Components
### 1. Tool Selection: UseMockSearchOp
```python
# Agent uses LLM to select appropriate tool
tool_call = await self.select_tool(query, [SearchToolA(), SearchToolB(), SearchToolC()])
# Execute selected tool and record results
result = ToolCallResult(
create_time=timestamp,
tool_name=tool_call.name,
input={"query": query},
output=content,
token_cost=token_cost,
success=success,
time_cost=time_cost
)
```
### 2. Tool Call Result Evaluation
Results are automatically evaluated and scored:
- `score`: 0.0 (failure/irrelevant) or 1.0 (complete success)
- `success`: Tool execution status
- `summary`: Brief description
- `evaluation`: Detailed assessment
### 3. Tool Memory Schema
```python
ToolMemory(
workspace_id="workspace_id",
memory_type="tool",
when_to_use="Brief usage scenario description",
content="Detailed performance analysis and recommendations",
score=0.85,
tool_call_results=[list of ToolCallResult],
metadata={"tool_name": "SearchToolA"}
)
```
## Evaluation Metrics
### Per-Scenario Metrics
- **Avg Score**: Average quality score (0.0-1.0)
- **Total Calls**: Number of tool invocations
- **Success Rate**: Percentage of successful executions
### Improvement Calculation
```python
improvement_percentage = ((with_memory_score - without_memory_score) / without_memory_score) * 100
```
## Expected Results
### Hypothesis
Tool Memory should enable the agent to:
1. **Select optimal tools** based on query complexity
2. **Improve average score** by 10-30% on test set
3. **Increase consistency** across multiple epochs
### Sample Output
```
==================================================================================================
BENCHMARK RESULTS COMPARISON
==================================================================================================
Note: Avg Score = average quality score
+---------------------------+--------------+-----------+
| Scenario | Total Calls | Avg Score |
+===========================+==============+===========+
| Epoch1 - Train (No Memory)| 60 | 0.650 |
+---------------------------+--------------+-----------+
| Epoch1 - Test (No Memory) | 60 | 0.633 |
+---------------------------+--------------+-----------+
| Epoch1 - Test (With Memory)| 60 | 0.817 |
+---------------------------+--------------+-----------+
==================================================================================================
IMPROVEMENTS WITH TOOL MEMORY (Baseline: Test without memory)
==================================================================================================
Average Score : +29.07% ↑
==================================================================================================
```
## Running the Benchmark
### Prerequisites
```bash
pip install requests python-dotenv loguru tabulate
```
### Start API Server
```bash
# Start ReMe API server
python reme_ai/app.py --port 8002
```
### Execute Benchmark
```bash
# Full benchmark (3 epochs, 60+60 queries per epoch)
python cookbook/tool_memory/run_reme_tool_bench.py
# Quick test (3 epochs, 15+15 queries per epoch)
# Modify main() call: main(test_mode=True, run_epoch=3)
```
### Output Files
- `tool_memory_benchmark_results.json`: Complete benchmark results
- Console output: Real-time progress and comparison tables
## API Endpoints Used
1. **`/use_mock_search`**: Execute tool selection and search
- Input: `workspace_id`, `query`
- Output: `ToolCallResult` JSON
2. **`/add_tool_call_result`**: Add results to memory and get evaluation scores
- Input: `workspace_id`, `tool_call_results` (list)
- Output: `memory_list` with scored results
3. **`/summary_tool_memory`**: Summarize tool performance
- Input: `workspace_id`, `tool_names` (comma-separated)
- Output: Updated `ToolMemory` with content
4. **`/retrieve_tool_memory`**: Retrieve formatted tool memory
- Input: `workspace_id`, `tool_names`
- Output: Markdown-formatted memory content
5. **`/vector_store`**: Delete workspace
- Input: `workspace_id`, `action: "delete"`
## Concurrency Control
- **Max workers**: 4 parallel queries
- **Rate limiting**: 1 second delay between submissions
- **Timeout**: 120 seconds per API call
## References
- Tool Memory Schema: `reme_ai/schema/memory.py`
- Mock Tools Implementation: `reme_ai/agent/tools/mock_search_tools.py`
- LLM-based Search Op: `reme_ai/agent/tools/llm_mock_search_op.py`
- Tool Selection Op: `reme_ai/agent/tools/use_mock_search_op.py`

View file

@ -0,0 +1,821 @@
# Tool Memory in ReMe
## 1. Background: Why Tool Memory?
### The MCP Tool Selection Challenge
In modern AI agent systems, LLMs face a rapidly expanding ecosystem of MCP (Model Context Protocol) tools. With hundreds or thousands of available tools, a critical problem emerges:
**The Core Problem: Tool Description is Not Enough**
When an LLM faces numerous MCP tools, it relies heavily on tool descriptions to decide which tool to use and how to use it. However:
- **Ambiguous Descriptions**: Many tools have similar descriptions but different performance characteristics
- **Hidden Complexity**: Static descriptions can't capture runtime behaviors, edge cases, or failure patterns
- **Parameter Confusion**: Tools may accept similar parameters with different optimal values
- **No Quality Signal**: Descriptions don't tell you which tools are reliable, fast, or cost-effective
**Example: Web Search Tools**
Imagine an LLM choosing between three search tools:
```
Tool A: "Search the web for information"
Tool B: "Perform web searches with customizable parameters"
Tool C: "Query search engines and return results"
```
The descriptions are nearly identical, but in reality:
- Tool A: 95% success rate, avg 2.3s, best for technical queries
- Tool B: 70% success rate, avg 5.8s, often times out with >20 results
- Tool C: 85% success rate, avg 3.1s, good for general queries
**Without historical data, the LLM can't make informed decisions.**
### The Solution: Tool Memory as Context Enhancement
Tool Memory solves this by providing **learned context from historical usage**, transforming static tool descriptions into dynamic, data-driven guidance:
**1. Rule-Based Statistics** (Objective Metrics)
- **Success Rate**: "This tool succeeds 92% of the time"
- **Performance**: "Average execution time: 2.3s, token cost: 150"
- **Usage Patterns**: "Most successful calls use max_results=10-20"
**2. LLM-as-Judge Evaluation** (Qualitative Insights)
- **Quality Assessment**: LLM evaluates each call's effectiveness
- **Pattern Recognition**: Identifies why some calls succeed and others fail
- **Actionable Recommendations**: Synthesizes guidelines from patterns
**3. Enhanced Context for LLM Decision-Making**
Instead of just a tool description, the LLM now receives:
```
Tool: web_search
Static Description:
"Search the web for information"
+ Tool Memory Context:
"Based on 150 historical calls:
- Success rate: 92% (138 successful, 12 failed)
- Avg time: 2.3s, Avg tokens: 150
- Best for: Technical documentation, tutorials (95% success)
- Optimal params: max_results=5-20, language='en'
- Common failures: Generic queries timeout, max_results>50 unreliable
- Recommendation: Use specific multi-word queries with filter_type='technical_docs'"
```
This enriched context enables the LLM to:
- **Choose the right tool** based on task requirements and reliability
- **Use optimal parameters** learned from successful historical calls
- **Avoid known pitfalls** that caused previous failures
- **Estimate costs** (time and tokens) before execution
### The Impact: From Static Descriptions to Dynamic Intelligence
**Traditional Approach (Static Descriptions Only):**
```
LLM: "I have 50 search tools, all with similar descriptions"
→ Random choice or first match
→ Trial-and-error parameter selection
→ 75% success rate, repeated failures
```
**Tool Memory Approach (Description + Historical Context):**
```
LLM: "I have 50 search tools, but Tool A has 95% success for technical queries"
→ Informed choice based on data
→ Use proven parameter configurations
→ 92% success rate, optimized performance
```
**Real-World Impact:**
```
Before Tool Memory:
- Success rate: 75%
- Average time cost: 5.2s
- Token cost: 200+ per call
- Repeated parameter errors
- Random tool selection
After Tool Memory:
- Success rate: 92% (+17%)
- Average time cost: 2.8s (-46%)
- Token cost: 150 per call (-25%)
- Consistent best practices
- Data-driven tool selection
```
### Why This Matters for MCP Ecosystem
As the MCP ecosystem grows, Tool Memory becomes essential:
1. **Scalability**: LLMs can navigate thousands of tools with confidence
2. **Quality Control**: Tools with poor performance get flagged automatically
3. **Continuous Improvement**: Every call improves the knowledge base
4. **Transfer Learning**: Insights from one agent benefit all agents in the workspace
**Tool Memory transforms tool descriptions from static documentation into living, learned manuals that improve with every use.**
## 2. What is Tool Memory?
Tool Memory is a structured knowledge base that captures insights from tool usage history. Each Tool Memory represents accumulated wisdom about a specific tool.
### Data Structure
#### ToolMemory
`ToolMemory` is the core data structure that stores comprehensive information about a tool's usage patterns:
```python
class ToolMemory(BaseMemory):
memory_type: str = "tool" # Type identifier
workspace_id: str # Workspace identifier
memory_id: str # Unique memory ID
when_to_use: str # Tool name (serves as unique identifier)
content: str # Synthesized usage guidelines
score: float # Overall quality score
time_created: str # Creation timestamp
time_modified: str # Last modification timestamp
author: str # Creator (typically LLM model name)
tool_call_results: List[ToolCallResult] # Historical invocation records
metadata: dict # Additional metadata
```
**Key Fields:**
- **`when_to_use`**: The tool name, used as the unique identifier for retrieval
- **`content`**: Human-readable usage guidelines synthesized from historical data
- **`tool_call_results`**: Complete history of tool invocations with evaluations
- **`score`**: Overall quality metric for the tool's performance
#### ToolCallResult
Each tool invocation is captured as a `ToolCallResult`:
```python
class ToolCallResult(BaseModel):
create_time: str # Invocation timestamp
tool_name: str # Name of the tool
input: dict | str # Input parameters
output: str # Tool output
token_cost: int # Token consumption
success: bool # Whether invocation succeeded
time_cost: float # Time consumed (seconds)
summary: str # Brief summary of the result
evaluation: str # Detailed evaluation (generated by LLM)
score: float # Evaluation score (0.0 for failure, 1.0 for success)
metadata: dict # Additional metadata
```
**Key Fields:**
- **`input`/`output`**: The complete I/O data for analysis
- **`summary`**: LLM-generated brief summary of what happened
- **`evaluation`**: LLM-generated detailed analysis of the call quality
- **`score`**: Binary evaluation (0.0 = failure, 1.0 = success)
- **Performance metrics**: `time_cost`, `token_cost`, `success` for statistical analysis
### Tool Memory Lifecycle
```mermaid
graph LR
A[Tool Call] --> B[Evaluate]
B --> C[Store Memory]
C --> D[(Vector Store)]
D --> E[Agent Retrieves]
E --> A
C -.Periodic.-> F[Summarize]
F --> C
```
## 3. How Tool Memory Works: The Complete Flow
Tool Memory operates through three complementary operations that work together to create a learning loop:
```mermaid
graph LR
A[Agent] -->|1 retrieve_tool_memory| B[(Vector Store)]
B -->|Guidelines| A
A -->|2 Execute Tool| C[Tool]
C -->|Result| A
A -->|3 add_tool_call_result| D[LLM Evaluate]
D -->|Store| B
B -->|Periodic| E[summary_tool_memory]
E -->|4 Update Guidelines| B
```
### Operation Flow
**1. retrieve_tool_memory** (Before Execution)
- Agent queries: "How should I use `web_search` tool?"
- Retrieves stored guidelines and historical patterns
- Returns: Usage recommendations, parameter suggestions, common pitfalls
**2. Tool Execution**
- Agent executes tool with informed parameters
- Collects: input, output, time_cost, token_cost, success status
**3. add_tool_call_result** (After Execution)
- Submits execution data for evaluation
- LLM analyzes: Was it successful? What could be improved?
- Generates: summary, evaluation, score (0.0 or 1.0)
- Appends to tool's historical record in Vector Store
**4. summary_tool_memory** (Periodic)
- Analyzes recent N tool calls (e.g., last 20-30)
- Calculates statistics: success rate, avg costs, avg score
- LLM synthesizes: Actionable usage guidelines
- Updates the `content` field with comprehensive guidance
### Example Flow from Demo
Based on `use_tool_memory_demo.py`, here's a typical workflow:
```python
# Step 1: Add tool call results (accumulate history)
add_tool_call_results([
{"tool_name": "web_search", "input": {...}, "output": "...", "success": True},
{"tool_name": "web_search", "input": {...}, "output": "...", "success": False},
# ... more results
])
# Step 2: Generate usage guidelines (periodic)
summarize_tool_memory("web_search")
# Step 3: Retrieve guidelines before next use
memory = retrieve_tool_memory("web_search")
# Returns:
# "For web_search tool:
# - Use max_results=5-20 for optimal performance
# - Avoid generic queries, be specific
# - Language parameter 'en' has 95% success rate
# Statistics: 83% success, avg 2.3s, avg 150 tokens"
# Step 4: Agent uses guidelines for better execution
execute_with_recommended_parameters()
```
## 4. Operation Details: How to Use Each Component
### 4.1 `add_tool_call_result`
**Purpose**: Evaluate and store tool call results into Tool Memory.
**Flow**:
```yaml
add_tool_call_result:
flow_content: parse_tool_call_result_op >> update_vector_store_op
description: "Evaluates and adds tool call results to the tool memory database"
```
**Process**:
1. Receives raw tool call results
2. Uses LLM to evaluate each call (generates summary, evaluation, score)
3. Groups results by tool name
4. Creates or updates ToolMemory objects
5. Stores in Vector Store
**Configuration** (`default.yaml`):
```yaml
op:
parse_tool_call_result_op:
backend: parse_tool_call_result_op
llm: default
params:
max_history_tool_call_cnt: 100 # Max calls to retain per tool
evaluation_sleep_interval: 1.0 # Delay between evaluations (seconds)
```
#### Usage with curl
```bash
curl -X POST http://0.0.0.0:8002/add_tool_call_result \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {
"query": "Python asyncio tutorial",
"max_results": 10,
"language": "en"
},
"output": "Found 10 relevant results including official docs and tutorials",
"token_cost": 150,
"success": true,
"time_cost": 2.3
},
{
"create_time": "2025-10-21 10:32:00",
"tool_name": "web_search",
"input": {
"query": "test",
"max_results": 100,
"language": "unknown"
},
"output": "Error: Invalid language parameter",
"token_cost": 50,
"success": false,
"time_cost": 0.5
}
]
}'
```
**Response**:
```json
{
"success": true,
"answer": "Successfully evaluated and stored 2 tool call results",
"metadata": {
"memory_list": [
{
"when_to_use": "web_search",
"memory_id": "abc123...",
"tool_call_results": [
{
"tool_name": "web_search",
"summary": "Successfully retrieved relevant Python asyncio documentation",
"evaluation": "Good parameter choices with appropriate max_results and language settings",
"score": 1.0,
...
},
{
"tool_name": "web_search",
"summary": "Failed due to invalid language parameter",
"evaluation": "Query too generic and language parameter not supported",
"score": 0.0,
...
}
]
}
]
}
}
```
#### Usage with Python
```python
import requests
from datetime import datetime
def add_tool_call_results(tool_call_results: list) -> dict:
"""Add tool call results to Tool Memory"""
response = requests.post(
url=f"{BASE_URL}add_tool_call_result",
json={
"workspace_id": WORKSPACE_ID,
"tool_call_results": tool_call_results
}
)
return response.json()
# Example: Record a tool invocation
result = add_tool_call_results([{
"create_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": "web_search",
"input": {"query": "Python asyncio", "max_results": 10},
"output": "Found 10 relevant results...",
"token_cost": 150,
"success": True,
"time_cost": 2.3
}])
```
**Complete examples**: See `cookbook/simple_demo/use_tool_memory_demo.py` for full working code.
---
### 4.2 `retrieve_tool_memory`
**Purpose**: Retrieve usage guidelines and historical data for specific tools.
**Flow**:
```yaml
retrieve_tool_memory:
flow_content: retrieve_tool_memory_op
description: "Retrieves tool memories from the vector database based on tool names"
```
**Process**:
1. Takes comma-separated tool names as input
2. Searches Vector Store for exact matches (by `when_to_use` field)
3. Returns complete ToolMemory objects with:
- Usage guidelines (`content`)
- Historical call records (`tool_call_results`)
- Statistics and metadata
#### Usage with curl
```bash
# Retrieve single tool
curl -X POST http://0.0.0.0:8002/retrieve_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"tool_names": "web_search"
}'
# Retrieve multiple tools (comma-separated)
curl -X POST http://0.0.0.0:8002/retrieve_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"tool_names": "web_search,database_query,file_processor"
}'
```
**Response**:
```json
{
"success": true,
"answer": "Successfully retrieved 1 tool memories",
"metadata": {
"memory_list": [
{
"memory_type": "tool",
"workspace_id": "my_workspace",
"memory_id": "abc123...",
"when_to_use": "web_search",
"content": "## Usage Guidelines\n\n**Best Practices:**\n- Use max_results between 5-20 for optimal performance\n- Always specify language parameter (en has 95% success rate)\n- Avoid generic single-word queries\n\n**Common Pitfalls:**\n- max_results > 50 often causes timeouts\n- Unknown language values default to 'en' with warning\n\n## Statistics\n- **Success Rate**: 83.33%\n- **Average Score**: 0.833\n- **Average Time Cost**: 2.345s\n- **Average Token Cost**: 156.7",
"score": 0.85,
"time_created": "2025-10-20 10:00:00",
"time_modified": "2025-10-21 10:35:00",
"author": "gpt-4",
"tool_call_results": [
{
"create_time": "2025-10-21 10:30:00",
"tool_name": "web_search",
"input": {"query": "Python asyncio", "max_results": 10},
"output": "Found 10 results...",
"summary": "Successfully retrieved relevant documentation",
"evaluation": "Good parameter choices...",
"score": 1.0,
"token_cost": 150,
"success": true,
"time_cost": 2.3
}
// ... more historical calls
]
}
]
}
}
```
#### Usage with Python
```python
import requests
def retrieve_tool_memory(tool_names: str) -> dict:
"""Retrieve tool memories by tool names"""
response = requests.post(
url=f"{BASE_URL}retrieve_tool_memory",
json={
"workspace_id": WORKSPACE_ID,
"tool_names": tool_names
}
)
return response.json()
# Example: Retrieve and use guidelines
result = retrieve_tool_memory("web_search")
if result['success']:
memory = result['metadata']['memory_list'][0]
print(f"Tool: {memory['when_to_use']}")
print(f"Guidelines:\n{memory['content']}")
```
**Complete examples**: See `cookbook/simple_demo/use_tool_memory_demo.py` for full working code.
---
### 4.3 `summary_tool_memory`
**Purpose**: Analyze historical tool calls and generate comprehensive usage guidelines.
**Flow**:
```yaml
summary_tool_memory:
flow_content: summary_tool_memory_op >> update_vector_store_op
description: "Analyzes tool call history and generates comprehensive usage patterns"
```
**Process**:
1. Retrieves existing ToolMemory by tool name
2. Analyzes recent N tool calls (default: 30)
3. Calculates statistics:
- Success rate
- Average score
- Average time cost
- Average token cost
4. Uses LLM to synthesize actionable guidelines from call summaries
5. Appends statistics to guidelines
6. Updates ToolMemory content in Vector Store
**Configuration** (`default.yaml`):
```yaml
op:
summary_tool_memory_op:
backend: summary_tool_memory_op
llm: default
params:
recent_call_count: 30 # Number of recent calls to analyze
summary_sleep_interval: 1.0 # Delay between summaries (seconds)
```
#### Usage with curl
```bash
# Summarize single tool
curl -X POST http://0.0.0.0:8002/summary_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"tool_names": "web_search"
}'
# Summarize multiple tools (comma-separated)
curl -X POST http://0.0.0.0:8002/summary_tool_memory \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"tool_names": "web_search,database_query,file_processor"
}'
```
**Response**:
```json
{
"success": true,
"answer": "Successfully summarized 1 tool memories",
"metadata": {
"memory_list": [
{
"memory_type": "tool",
"when_to_use": "web_search",
"content": "## Usage Guidelines\n\n**Optimal Parameters:**\n- Set max_results between 5-20 for best balance of coverage and speed\n- Always specify language='en' for technical queries (95% success rate)\n- Use filter_type='technical_docs' for development-related searches\n\n**Success Patterns:**\n- Specific, multi-word queries perform significantly better than generic terms\n- Queries with clear intent (e.g., 'Python asyncio tutorial') return high-quality results\n- Technical terms and version numbers improve result relevance\n\n**Common Failures:**\n- Generic single-word queries (e.g., 'test') return poor results\n- max_results > 50 increases timeout risk (5 failures observed)\n- Invalid language codes cause fallback to default with warnings\n\n**Performance Insights:**\n- Typical response time: 1.5-3.5s for successful queries\n- Timeout threshold: 10s (consider simplifying complex queries)\n- Token cost scales with result count: ~150 tokens for 10 results\n\n**Recommendations:**\n1. Always validate language parameter before calling\n2. Start with max_results=10, adjust based on needs\n3. For time-sensitive operations, set timeout < 5s\n4. Monitor token costs for high-frequency usage\n\n## Statistics\n- **Success Rate**: 83.33%\n- **Average Score**: 0.833\n- **Average Time Cost**: 2.345s\n- **Average Token Cost**: 156.7",
"memory_id": "abc123...",
"time_modified": "2025-10-21 10:40:00",
...
}
]
}
}
```
#### Usage with Python
```python
import requests
def summarize_tool_memory(tool_names: str) -> dict:
"""Generate comprehensive usage guidelines for tools"""
response = requests.post(
url=f"{BASE_URL}summary_tool_memory",
json={
"workspace_id": WORKSPACE_ID,
"tool_names": tool_names
}
)
return response.json()
# Example: Generate guidelines
result = summarize_tool_memory("web_search")
if result['success']:
memory = result['metadata']['memory_list'][0]
print(f"Tool: {memory['when_to_use']}")
print(f"Guidelines:\n{memory['content']}")
```
**Complete examples**: See `cookbook/simple_demo/use_tool_memory_demo.py` for full working code.
---
## 5. Best Practices
### When to Record Tool Calls
- **Always**: Record every tool invocation, including failures
- **Include**: Complete input parameters, output, and performance metrics
- **Timing**: Record immediately after tool execution completes
### When to Generate Summaries
- **Initial**: After accumulating 20-30 tool calls for meaningful patterns
- **Periodic**: Re-summarize every 50-100 new calls or weekly
- **Trigger-based**: When success rate drops or patterns change significantly
### When to Retrieve Guidelines
- **Before first use**: Always retrieve before using an unfamiliar tool
- **Before critical operations**: Check latest guidelines for important tasks
- **After updates**: Re-retrieve when tool memory has been updated
### Performance Tuning
**For High-Volume Tools** (>100 calls/day):
```yaml
op:
parse_tool_call_result_op:
params:
max_history_tool_call_cnt: 200 # Keep more history
evaluation_sleep_interval: 0.5 # Faster evaluation
summary_tool_memory_op:
params:
recent_call_count: 50 # Analyze more calls
```
**For Low-Volume Tools** (<20 calls/day):
```yaml
op:
parse_tool_call_result_op:
params:
max_history_tool_call_cnt: 50 # Less history needed
evaluation_sleep_interval: 1.0 # Standard rate
summary_tool_memory_op:
params:
recent_call_count: 20 # Analyze fewer calls
```
### Quality Maintenance
1. **Monitor Metrics**:
```python
memory = retrieve_tool_memory("web_search")['metadata']['memory_list'][0]
stats = ToolMemory(**memory).statistic(recent_frequency=30)
print(f"Success Rate: {stats['success_rate']:.2%}")
print(f"Avg Score: {stats['avg_score']:.2f}")
if stats['success_rate'] < 0.7:
print("⚠️ Low success rate - investigate tool issues")
```
2. **Clean Old Memories**:
- Delete tool memories for deprecated tools
- Reset memories when tool behavior changes significantly
3. **Validate Guidelines**:
- Periodically review generated guidelines for accuracy
- Test recommended parameters in production scenarios
## 6. Memory Management
### Delete Workspace
```bash
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"action": "delete"
}'
```
```python
def delete_workspace(workspace_id: str):
response = requests.post(
url=f"{BASE_URL}vector_store",
json={"workspace_id": workspace_id, "action": "delete"}
)
return response.json()
```
### Dump Memories to Disk
```bash
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"action": "dump",
"path": "./memory_backup/"
}'
```
```python
def dump_memory(workspace_id: str, path: str = "./"):
response = requests.post(
url=f"{BASE_URL}vector_store",
json={"workspace_id": workspace_id, "action": "dump", "path": path}
)
return response.json()
```
### Load Memories from Disk
```bash
curl -X POST http://0.0.0.0:8002/vector_store \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "my_workspace",
"action": "load",
"path": "./memory_backup/"
}'
```
```python
def load_memory(workspace_id: str, path: str = "./"):
response = requests.post(
url=f"{BASE_URL}vector_store",
json={"workspace_id": workspace_id, "action": "load", "path": path}
)
return response.json()
```
## 7. Complete Working Example
For a complete, runnable example demonstrating the full Tool Memory lifecycle, see:
**`cookbook/simple_demo/use_tool_memory_demo.py`**
This demo includes:
- **Workspace management**: Clean, delete, dump, and load operations
- **Tool call recording**: Adding 30+ mock tool invocations with various scenarios
- **Summarization**: Generating usage guidelines from historical data
- **Retrieval**: Fetching and displaying tool memories
- **Statistics**: Analyzing success rates, costs, and performance
Run the demo:
```bash
cd cookbook/simple_demo
python use_tool_memory_demo.py
```
**Key Workflow Steps:**
1. **Clean workspace**: Remove existing data
2. **Add tool calls**: Record 30+ invocations (success/failure scenarios)
3. **Generate guidelines**: LLM analyzes patterns and creates recommendations
4. **Retrieve memory**: Get usage guidelines for agent consumption
5. **Persistence**: Test dump/load operations
## 8. Advanced Use Cases
### Use Case 1: Adaptive Parameter Tuning
Retrieve tool memory statistics and adapt parameters based on historical performance:
- If `avg_time_cost > 5s`: Increase timeout
- If `success_rate < 80%`: Enable retry logic
- If `avg_token_cost` high: Reduce result limits
### Use Case 2: Multi-Tool Workflow Optimization
Retrieve memories for multiple tools at once and optimize workflow order based on:
- Success rates: Execute reliable tools first
- Time costs: Parallelize slow operations
- Token costs: Budget-aware tool selection
### Use Case 3: Automated Quality Monitoring
Periodically check tool memory statistics and alert on:
- Success rate degradation
- Increasing time/token costs
- Unusual failure patterns
**Implementation examples**: See `cookbook/simple_demo/use_tool_memory_demo.py` and the ToolBench evaluation scripts.
## 9. Benchmark Results
### Tool Memory Performance Evaluation
We evaluated Tool Memory effectiveness using a controlled benchmark with three mock search tools, each optimized for different query complexity levels (simple, moderate, complex). The benchmark compares agent performance with and without tool memory guidance across multiple epochs.
**Experimental Settings:**
- **Model**: Qwen3-30B-Instruct with default parameters
- **Task**: Single-turn tool selection and invocation
- **Dataset**: 60 training queries + 60 test queries per epoch
- **Tools**: 3 mock search tools with varying performance profiles
- **Metrics**: Average quality score (0.0-1.0) based on LLM evaluation
- **Baseline**: Test set performance without tool memory
- **Replication**: Results averaged across 3 independent experimental runs
**Results (averaged across 3 epochs):**
| Scenario | Avg Score | Improvement |
|----------|-----------|-------------|
| Train (No Memory) | 0.650 | - |
| Test (No Memory) | 0.672 | Baseline |
| **Test (With Memory)** | **0.772** | **+14.88%** |
**Key Findings:**
- **Consistent improvement**: Tool Memory boosted test performance by ~15% on average
- **Knowledge transfer**: Training data successfully informed test-time tool selection
- **Stability**: Improvement remained consistent across all 3 epochs (9.90% → 17.39% → 17.13%)
The benchmark demonstrates that Tool Memory enables agents to make data-driven tool selection decisions, significantly improving task success rates compared to relying solely on static tool descriptions.
**Benchmark Resources:**
- **Design Documentation**: [`docs/tool_memory/tool_bench.md`](tool_bench.md) - Complete benchmark methodology and workflow
- **Implementation**: [`cookbook/tool_memory/run_reme_tool_bench.py`](../../cookbook/tool_memory/run_reme_tool_bench.py) - Full benchmark script
- **Query Dataset**: [`cookbook/tool_memory/query.json`](../../cookbook/tool_memory/query.json) - 60 train + 60 test queries across 3 complexity levels
---
## 10. References
- **Implementation**: See `reme_ai/summary/tool/` and `reme_ai/retrieve/tool/`
- **Demo**: `cookbook/simple_demo/use_tool_memory_demo.py`
- **Benchmark**: `cookbook/tool_memory/run_reme_tool_bench.py`
- **Schema**: `reme_ai/schema/memory.py`
- **Utilities**: `reme_ai/utils/tool_memory_utils.py`

View file

@ -0,0 +1,19 @@
# Tool Memory Retrieval Operations
## RetrieveToolMemoryOp
### Purpose
Retrieves tool memories from the vector database based on tool names, providing usage patterns, best practices, and historical call data.
### Functionality
- Accepts comma-separated tool names as input
- Searches the vector store for exact tool name matches
- Validates that retrieved memories are of type "tool"
- Returns complete tool memories including usage guidelines and call history
### Parameters
This operation has no configurable parameters. It uses the default vector store configuration.

View file

@ -0,0 +1,82 @@
# Tool Summary Operations
## ParseToolCallResultOp
### Purpose
Evaluates individual tool invocations and adds them to the tool memory database with comprehensive assessments.
### Functionality
- Receives tool call results with input parameters, output, and metadata
- Uses LLM to evaluate each tool call based on success and parameter alignment
- Generates summary, evaluation, and score (0.0 or 1.0) for each call
- Appends evaluated results to existing tool memory or creates new memory
- Maintains a sliding window of recent tool calls (configurable limit)
### Parameters
- `op.parse_tool_call_result_op.params.max_history_tool_call_cnt` (integer, default: `100`):
- Maximum number of historical tool call results to retain per tool
- When exceeded, oldest results are removed (FIFO)
- `op.parse_tool_call_result_op.params.evaluation_sleep_interval` (float, default: `1.0`):
- Delay in seconds between concurrent evaluations
- Prevents rate limiting when evaluating multiple calls
## SummaryToolMemoryOp
### Purpose
Analyzes accumulated tool call history and generates comprehensive usage patterns, best practices, and recommendations.
### Functionality
- Retrieves existing tool memories from the vector store
- **Intelligently skips tools** where all recent calls have already been summarized (using `is_summarized` flag)
- Analyzes the most recent N tool calls (configurable)
- Calculates statistical metrics (success rate, average scores, costs)
- Uses LLM to synthesize actionable usage guidelines
- Updates tool memory content with generated insights
- **Marks processed calls** as summarized to avoid redundant processing in future runs
### Smart Skip Logic
To optimize costs and performance, `SummaryToolMemoryOp` tracks which tool call results have been included in a summary:
- **Skip Condition**: If all recent N calls are already summarized (`is_summarized=True`), the tool is skipped entirely
- **Trigger Condition**: If at least 1 recent call is new (`is_summarized=False`), re-summarization is triggered
- **Automatic Marking**: After successful summarization, all processed calls are marked with `is_summarized=True`
**Example Behavior**:
```
Run 1: 30 new calls → Summarize all 30, mark as summarized
Run 2: Same 30 calls → Skip (all already summarized) ✓ Cost savings
Run 3: 30 old + 1 new → Re-summarize all 31, mark new call as summarized
```
This ensures summaries stay fresh while avoiding unnecessary LLM calls.
### Parameters
- `op.summary_tool_memory_op.params.recent_call_count` (integer, default: `30`):
- Number of most recent tool calls to analyze
- Also determines the window for checking summarization status
- Focuses on recent usage patterns
- `op.summary_tool_memory_op.params.summary_sleep_interval` (float, default: `1.0`):
- Delay in seconds between concurrent summarizations
- Prevents rate limiting when summarizing multiple tools
### Return Value
The operation returns a response message indicating:
- Number of tools summarized (had new unsummarized calls)
- Number of tools skipped (all recent calls already summarized)
Example: `"Successfully processed 5 tool memories: 2 summarized, 3 skipped (already up-to-date)"`
### See Also
For detailed implementation and examples, see [is_summarized Feature Documentation](is_summarized_feature.md).

View file

@ -26,6 +26,12 @@ nav:
- Retrieve Ops: task_memory/task_retrieve_ops.md
- Summary Ops: task_memory/task_summary_ops.md
- Tool Memory:
- Overview: tool_memory/tool_memory.md
- Retrieve Ops: tool_memory/tool_retrieve_ops.md
- Summary Ops: tool_memory/tool_summary_ops.md
- Benchmark: tool_memory/tool_bench.md
- SOP Memory:
- Making SOP Memories: sop_memory/making_sop_memories.md
@ -39,6 +45,7 @@ nav:
- BFCL: cookbook/bfcl/quickstart.md
- FrozenLake: cookbook/frozenlake/quickstart.md
- TODO: todo.md
- Contribution Guide: contribution.md
@ -64,3 +71,9 @@ markdown_extensions:
shadcn.extensions.echarts.alpha:
shadcn.extensions.codexec:
shadcn.extensions.iconify:
# pip install mkdocs-shadcn
# mkdocs build
# mkdocs serve
# mkdocs gh-deploy --force

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "reme_ai"
version = "0.1.9"
version = "0.1.10"
description = "Remember me"
authors = [
{ name = "jinli.yl", email = "jinli.yl@alibaba-inc.com" },
@ -24,7 +24,7 @@ classifiers = [
keywords = ["llm", "memory", "experience", "memoryscope", "ai", "mcp", "http"]
dependencies = [
"flowllm==0.1.9",
"flowllm>=0.1.11.1",
]
[tool.setuptools.packages.find]

View file

@ -1,6 +1,14 @@
from reme_ai import react
from reme_ai import retrieve
from reme_ai import summary
from reme_ai import vector_store
import warnings
__version__ = "0.1.9"
from pydantic.warnings import PydanticDeprecatedSince20
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="uvicorn")
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
from . import agent
from . import retrieve
from . import summary
from . import vector_store
__version__ = "0.1.10"

View file

@ -0,0 +1,2 @@
from . import react
from . import tools

View file

@ -0,0 +1,3 @@
from .llm_mock_search_op import LLMMockSearchOp
from .mock_search_tools import SearchToolA, SearchToolB, SearchToolC
from .use_mock_search_op import UseMockSearchOp

View file

@ -0,0 +1,307 @@
import asyncio
import json
import random
from typing import Dict, Any
from loguru import logger
from flowllm.context import FlowContext, C
from flowllm.enumeration.role import Role
from flowllm.op.base_async_tool_op import BaseAsyncToolOp
from flowllm.schema.message import Message
from flowllm.schema.tool_call import ToolCall
@C.register_op()
class LLMMockSearchOp(BaseAsyncToolOp):
"""
Mock search operation that uses LLM to classify queries and simulate different scenarios.
Supports three query complexity levels:
- simple: Simple factual queries with short, direct answers
- medium: Medium complexity queries requiring balanced performance
- complex: Complex research queries requiring comprehensive, in-depth results
Each scenario can be configured with:
- success_rate: Probability of successful response (vs "Service busy" error)
- extra_time: Extra sleep time in seconds to simulate latency
- relevance_ratio: Probability of returning relevant results (vs random query results)
"""
file_path: str = __file__
def __init__(self,
llm: str = "qwen3_30b_instruct",
simple_config: Dict[str, Any] = None,
medium_config: Dict[str, Any] = None,
complex_config: Dict[str, Any] = None,
seed: int = 0,
**kwargs):
"""
Initialize the LLM Mock Search Op.
Args:
llm: LLM model name to use for classification and content generation
simple_config: Configuration for simple queries
- success_rate: float (0-1), default 0.95
- extra_time: float (seconds), default 0.5
- relevance_ratio: float (0-1), default 0.98
medium_config: Configuration for medium complexity queries
- success_rate: float (0-1), default 0.85
- extra_time: float (seconds), default 1.0
- relevance_ratio: float (0-1), default 0.90
complex_config: Configuration for complex queries
- success_rate: float (0-1), default 0.70
- extra_time: float (seconds), default 1.5
- relevance_ratio: float (0-1), default 0.80
seed: Random seed for deterministic behavior, default 0
"""
super().__init__(llm=llm, **kwargs)
# Set random seed for deterministic behavior
self.seed = seed
random.seed(self.seed)
# Default configurations for each scenario
self.simple_config = {
"success_rate": 0.95,
"extra_time": 0.5,
"relevance_ratio": 0.98,
"content_length": "short"
}
if simple_config:
self.simple_config.update(simple_config)
self.medium_config = {
"success_rate": 0.85,
"extra_time": 1.0,
"relevance_ratio": 0.90,
"content_length": "medium"
}
if medium_config:
self.medium_config.update(medium_config)
self.complex_config = {
"success_rate": 0.70,
"extra_time": 1.5,
"relevance_ratio": 0.80,
"content_length": "long"
}
if complex_config:
self.complex_config.update(complex_config)
def build_tool_call(self) -> ToolCall:
return ToolCall(**{
"description": "Use search keywords to retrieve relevant information from the internet.",
"input_schema": {
"query": {
"type": "string",
"description": "search keyword or query",
"required": True
}
}
})
async def classify_query(self, query: str) -> str:
"""
Classify the query into simple, medium, or complex using LLM.
Args:
query: The search query to classify
Returns:
Classification result: "simple", "medium", or "complex"
"""
classification_prompt = self.prompt_format(
prompt_name="classification_prompt",
query=query
)
messages = [Message(role=Role.USER, content=classification_prompt)]
response = await self.llm.achat(messages=messages)
classification = response.content.strip().lower()
# Extract classification from response
if "simple" in classification:
return "simple"
elif "complex" in classification:
return "complex"
else:
return "medium"
async def generate_search_result(self, query: str, complexity: str, config: Dict[str, Any]) -> str:
"""
Generate mock search results using LLM based on query complexity.
Args:
query: The search query
complexity: Query complexity level
config: Configuration for this complexity level
Returns:
Generated search result content
"""
content_length = config["content_length"]
generation_prompt = self.prompt_format(
prompt_name="generation_prompt",
query=query,
complexity=complexity,
content_length=content_length
)
messages = [Message(role=Role.USER, content=generation_prompt)]
response = await self.llm.achat(messages=messages)
return response.content
async def generate_random_result(self) -> str:
"""
Generate a random/irrelevant search result to simulate low relevance.
Returns:
Random search result content
"""
random_topics = [
"the history of ancient civilizations",
"modern technology trends",
"climate change impacts",
"space exploration achievements",
"culinary traditions around the world",
"evolution of music genres",
"breakthroughs in medical science",
"architectural wonders",
"wildlife conservation efforts",
"developments in artificial intelligence"
]
random_query = random.choice(random_topics)
generation_prompt = self.prompt_format(
prompt_name="generation_prompt",
query=random_query,
complexity="simple",
content_length="short"
)
messages = [Message(role=Role.USER, content=generation_prompt)]
response = await self.llm.achat(messages=messages)
return f"[Low Relevance Result]\n{response.content}"
async def async_execute(self):
query: str = self.input_dict["query"]
logger.info(f"LLMMockSearchOp processing query: {query}")
# Step 1: Classify the query
complexity = await self.classify_query(query)
logger.info(f"Query classified as: {complexity}")
# Step 2: Get configuration for this complexity
if complexity == "simple":
config = self.simple_config
elif complexity == "medium":
config = self.medium_config
else: # complex
config = self.complex_config
logger.info(f"Using config: {config}")
# Step 3: Simulate extra time delay
extra_time = config["extra_time"]
await asyncio.sleep(extra_time)
logger.info(f"Simulated extra delay: {extra_time:.2f}s")
# Step 4: Check success rate
if random.random() > config["success_rate"]:
error_message = "Search service is currently busy. Please try again later."
logger.warning(f"Simulated failure: {error_message}")
result_dict = {
"success": False,
"content": error_message,
"query": query,
"complexity": complexity
}
self.set_result(json.dumps(result_dict, ensure_ascii=False))
return
# Step 5: Check relevance ratio
if random.random() > config["relevance_ratio"]:
# Generate random/irrelevant result
# NOTE: success=True because technically the tool executed without errors,
# but the content is irrelevant (low quality), which should result in score=0.0 during evaluation
logger.info("Generating low relevance result (success=True but low quality)")
content = await self.generate_random_result()
result_dict = {
"success": True, # Technical execution succeeded
"content": content,
"query": query,
"complexity": complexity,
"is_relevant": False # Mark as irrelevant for debugging
}
else:
# Generate relevant result
logger.info("Generating relevant result")
content = await self.generate_search_result(query, complexity, config)
result_dict = {
"success": True,
"content": content,
"query": query,
"complexity": complexity,
"is_relevant": True # Mark as relevant for debugging
}
self.set_result(json.dumps(result_dict, ensure_ascii=False))
async def async_main():
from flowllm.app import FlowLLMApp
async with FlowLLMApp(load_default_config=True):
# Test with different query types
test_queries = [
"What is the capital of France?", # Simple
"How does quantum computing work?", # Medium
"Analyze the impact of artificial intelligence on global economy, employment, and society", # Complex
]
# Custom configurations for testing
custom_simple = {
"success_rate": 1,
"extra_time": 0,
"relevance_ratio": 1
}
custom_medium = {
"success_rate": 1,
"extra_time": 0,
"relevance_ratio": 1
}
custom_complex = {
"success_rate": 1,
"extra_time": 0,
"relevance_ratio": 1
}
op = LLMMockSearchOp(
simple_config=custom_simple,
medium_config=custom_medium,
complex_config=custom_complex
)
for query in test_queries:
print(f"\n{'=' * 80}")
print(f"Testing query: {query}")
print(f"{'=' * 80}")
context = FlowContext(query=query)
await op.async_call(context=context)
result = json.loads(context.llm_mock_search_result)
print(f"Success: {result['success']}")
print(f"Query: {result['query']}")
print(f"Complexity: {result['complexity']}")
print(f"Content:\n{result['content']}")
if __name__ == "__main__":
asyncio.run(async_main())

View file

@ -0,0 +1,76 @@
classification_prompt: |
You are a query complexity classifier. Analyze the following search query and classify it into one of three categories:
1. **simple** - Simple factual queries that:
- Ask for a single, direct fact
- Have a clear, unambiguous answer
- Require minimal context or explanation
- Examples: "What is the capital of France?", "Who invented the telephone?", "When did World War 2 end?"
2. **medium** - Medium complexity queries that:
- Require some explanation or context
- May involve multiple related facts
- Need balanced depth without being exhaustive
- Examples: "How does photosynthesis work?", "What are the main causes of climate change?", "Explain blockchain technology"
3. **complex** - Complex research queries that:
- Require comprehensive, multi-dimensional analysis
- Involve multiple subtopics or perspectives
- Need in-depth exploration and connections
- Examples: "Analyze the impact of AI on the global economy", "Compare different renewable energy solutions", "What are the geopolitical implications of space exploration?"
Query to classify: {query}
Respond with ONLY one word: simple, medium, or complex.
classification_prompt_zh: |
你是一个查询复杂度分类器。分析以下搜索查询并将其分类为以下三类之一:
1. **simple** - 简单事实查询:
- 询问单一、直接的事实
- 有明确、无歧义的答案
- 需要最少的上下文或解释
- 示例:"法国的首都是什么?"、"谁发明了电话?"、"第二次世界大战何时结束?"
2. **medium** - 中等复杂度查询:
- 需要一些解释或上下文
- 可能涉及多个相关事实
- 需要平衡的深度但不需要详尽无遗
- 示例:"光合作用如何工作?"、"气候变化的主要原因是什么?"、"解释区块链技术"
3. **complex** - 复杂研究查询:
- 需要全面、多维度的分析
- 涉及多个子主题或观点
- 需要深入探索和联系
- 示例:"分析人工智能对全球经济的影响"、"比较不同的可再生能源解决方案"、"太空探索的地缘政治影响是什么?"
要分类的查询:{query}
只用一个词回答:simple、medium 或 complex。
generation_prompt: |
You are a search engine generating mock search results. Generate a {content_length} response for the following query.
Query: {query}
Complexity Level: {complexity}
Instructions based on content length:
- **short**: Provide a concise answer in 1-3 sentences. Be direct and factual.
- **medium**: Provide a balanced answer in 2-4 paragraphs. Include key details and some context.
- **long**: Provide a comprehensive answer in 4-6 paragraphs. Include multiple perspectives, detailed explanations, and relevant context.
Generate the search result content now:
generation_prompt_zh: |
你是一个搜索引擎,正在生成模拟搜索结果。为以下查询生成一个 {content_length} 的响应。
查询:{query}
复杂度级别:{complexity}
根据内容长度的指示:
- **short**(短):提供 1-3 句话的简洁答案。要直接和事实性。
- **medium**(中):提供 2-4 段的平衡答案。包括关键细节和一些上下文。
- **long**(长):提供 4-6 段的全面答案。包括多个角度、详细解释和相关上下文。
现在生成搜索结果内容:

View file

@ -0,0 +1,115 @@
from flowllm.context import C
from flowllm.schema.tool_call import ToolCall
from reme_ai.agent.tools.llm_mock_search_op import LLMMockSearchOp
@C.register_op()
class SearchToolA(LLMMockSearchOp):
def __init__(self, llm: str = "qwen3_30b_instruct", **kwargs):
# Configure for fast but shallow performance
simple_config = {
"success_rate": 0.9, # High success rate for simple queries
"extra_time": 0, # Very fast (0.2-0.5s range)
"relevance_ratio": 0.9, # High relevance
"content_length": "short" # Concise answers
}
medium_config = {
"success_rate": 0.2, # Lower success for medium queries
"extra_time": 0, # Still fast
"relevance_ratio": 0.2, # Moderate relevance
"content_length": "short" # Limited depth
}
complex_config = {
"success_rate": 0.5, # Poor success rate for complex queries
"extra_time": 0, # Fast but insufficient
"relevance_ratio": 0.5, # Low relevance (often misses key aspects)
"content_length": "short" # Too shallow for complex topics
}
super().__init__(llm=llm,
simple_config=simple_config,
medium_config=medium_config,
complex_config=complex_config,
**kwargs)
def build_tool_call(self) -> ToolCall:
tool_call = super().build_tool_call()
tool_call.description += " Best suited for simple queries."
return tool_call
@C.register_op()
class SearchToolB(LLMMockSearchOp):
def __init__(self, llm: str = "qwen3_30b_instruct", **kwargs):
# Configure for balanced performance
simple_config = {
"success_rate": 0.3, # Very high success rate
"extra_time": 0, # Moderate speed (1.0-1.5s range)
"relevance_ratio": 0.3, # High relevance
"content_length": "medium" # More detailed than needed for simple
}
medium_config = {
"success_rate": 0.9, # Excellent success rate
"extra_time": 0, # Balanced speed
"relevance_ratio": 0.9, # High relevance
"content_length": "medium" # Perfect depth for medium queries
}
complex_config = {
"success_rate": 0.5, # Good success rate
"extra_time": 0, # Still reasonable speed
"relevance_ratio": 0.5, # Decent relevance but not exhaustive
"content_length": "medium" # Covers main points but lacks depth
}
super().__init__(llm=llm,
simple_config=simple_config,
medium_config=medium_config,
complex_config=complex_config,
**kwargs)
def build_tool_call(self) -> ToolCall:
tool_call = super().build_tool_call()
tool_call.description += " Best suited for medium complexity queries."
return tool_call
@C.register_op()
class SearchToolC(LLMMockSearchOp):
def __init__(self, llm: str = "qwen3_30b_instruct", **kwargs):
# Configure for comprehensive but costly performance
simple_config = {
"success_rate": 0.3, # Good but not optimal (over-processing)
"extra_time": 0, # Slow (3.0-4.0s range)
"relevance_ratio": 0.3, # High relevance but unnecessary depth
"content_length": "long" # Too detailed for simple queries
}
medium_config = {
"success_rate": 0.4, # High success rate
"extra_time": 0, # Slow but thorough
"relevance_ratio": 0.4, # High relevance with extra context
"content_length": "long" # More depth than needed
}
complex_config = {
"success_rate": 0.9, # Excellent success rate
"extra_time": 0, # Slow but comprehensive (3.5-5.0s range)
"relevance_ratio": 0.9, # Very high relevance
"content_length": "long" # Perfect depth for complex queries
}
super().__init__(llm=llm,
simple_config=simple_config,
medium_config=medium_config,
complex_config=complex_config,
**kwargs)
def build_tool_call(self) -> ToolCall:
tool_call = super().build_tool_call()
tool_call.description += " Best suited for complex queries."
return tool_call

View file

@ -0,0 +1,140 @@
import asyncio
import datetime
import json
from flowllm.context import C
from flowllm.enumeration.role import Role
from flowllm.op.base_async_tool_op import BaseAsyncToolOp
from flowllm.schema.message import Message
from flowllm.schema.tool_call import ToolCall
from flowllm.utils.timer import Timer
from flowllm.utils.token_utils import TokenCounter
from loguru import logger
from reme_ai.agent.tools.mock_search_tools import SearchToolA, SearchToolB, SearchToolC
from reme_ai.schema.memory import ToolCallResult
@C.register_op()
class UseMockSearchOp(BaseAsyncToolOp):
file_path: str = __file__
def __init__(self, llm: str = "qwen3_30b_instruct", **kwargs):
super().__init__(llm=llm, save_answer=True, **kwargs)
def build_tool_call(self) -> ToolCall:
return ToolCall(**{
"description": "Intelligently selects and executes the most appropriate search tool based on query complexity. "
"Automatically tracks performance metrics and records tool usage for optimization.",
"input_schema": {
"query": {
"type": "string",
"description": "query",
"required": True
}
}
})
async def select_tool(self, query: str, tool_ops: list[BaseAsyncToolOp]) -> ToolCall | None:
assistant_message = await self.llm.achat(messages=[Message(role=Role.USER, content=query)],
tools=[x.tool_call for x in tool_ops])
logger.info(f"assistant_message={assistant_message.model_dump_json()}")
if assistant_message.tool_calls:
return assistant_message.tool_calls[0]
return None
async def async_execute(self):
query: str = self.input_dict["query"]
logger.info(f"query={query}")
tool_ops = [
SearchToolA(),
SearchToolB(),
SearchToolC(),
]
# Step 1: Select the appropriate tool using LLM
tool_call = await self.select_tool(query, tool_ops)
if tool_call is None:
# No tool selected
error_result = ToolCallResult(
create_time=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
tool_name="None",
input={"query": query},
output="No appropriate tool was selected for the query",
token_cost=0,
success=False,
time_cost=0.0
)
self.set_result(error_result.model_dump_json())
return
# Step 2: Execute the selected tool
selected_op = None
for op in tool_ops:
if op.tool_call.name == tool_call.name:
selected_op = op
break
if selected_op is None:
# Tool not found (should not happen)
error_result = ToolCallResult(
create_time=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
tool_name=tool_call.name,
input=tool_call.arguments,
output=f"Tool {tool_call.name} not found in available tools",
token_cost=0,
success=False,
time_cost=0.0
)
self.set_result(error_result.model_dump_json())
return
# Step 3: Execute the tool with timer
timer = Timer("tool execute")
with timer:
await selected_op.async_call(query=query)
selected_op_output = json.loads(selected_op.output)
content = selected_op_output["content"]
success = selected_op_output["success"]
token_cost = TokenCounter().count(content)
time_cost = timer.time_cost
# Create ToolCallResult
tool_call_result = ToolCallResult(
create_time=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
tool_name=tool_call.name,
input={"query": query},
output=content,
token_cost=token_cost,
success=success,
time_cost=round(time_cost, 3)
)
self.set_result(tool_call_result.model_dump_json())
async def async_main():
from flowllm.app import FlowLLMApp
async with FlowLLMApp(load_default_config=True):
test_queries = [
"What is the capital of France?",
"How does quantum computing work?",
"Analyze the impact of artificial intelligence on global economy, employment, and society",
"When was Python programming language created?",
"Compare different types of renewable energy sources",
]
for query in test_queries:
op = UseMockSearchOp()
await op.async_call(query=query)
print(op.output)
if __name__ == "__main__":
asyncio.run(async_main())

View file

@ -1,8 +1,4 @@
import sys
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets")
warnings.filterwarnings("ignore", category=DeprecationWarning, module="uvicorn")
from flowllm.app import FlowLLMApp

View file

@ -49,14 +49,41 @@ 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
description: "Retrieves the most relevant top-k memory experiences from historical data based on the current query with simplified processing"
retrieve_tool_memory:
flow_content: retrieve_tool_memory_op
description: "Retrieves tool memories from the vector database based on tool names to provide tool usage patterns and best practices"
input_schema:
tool_names:
type: string
description: "Comma-separated tool names (e.g., 'tool_name1,tool_name2')"
required: true
add_tool_call_result:
flow_content: parse_tool_call_result_op >> update_vector_store_op
description: "Evaluates and adds tool call results to the tool memory database, creating new memory or updating existing memory for the specified tool"
input_schema:
tool_call_results:
type: array
description: "List of tool call result objects, each containing: tool_name, input, output, success, time_cost, token_cost, create_time"
required: true
summary_tool_memory:
flow_content: summary_tool_memory_op >> update_vector_store_op
description: "Analyzes tool call history and generates comprehensive usage patterns, best practices, and recommendations for the specified tools"
input_schema:
tool_names:
type: string
description: "Comma-separated tool names to summarize (e.g., 'tool_name1,tool_name2')"
required: true
use_mock_search:
flow_content: use_mock_search_op
description: "Simulates intelligent search tool selection and execution based on query complexity, with automatic tool memory recording"
input_schema:
query:
type: string
description: "current query"
required: true
description: "User search query to process"
required: true
summary_task_memory_simple:
flow_content: simple_summary_op >> update_vector_store_op
@ -136,6 +163,21 @@ op:
params:
enable_llm_rewrite: true
# tool memory ops
parse_tool_call_result_op:
backend: parse_tool_call_result_op
llm: default
params:
max_history_tool_call_cnt: 100
evaluation_sleep_interval: 1.0
summary_tool_memory_op:
backend: summary_tool_memory_op
llm: default
params:
recent_call_count: 20
summary_sleep_interval: 1.0
llm:
default:
backend: openai_compatible
@ -143,6 +185,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

View file

@ -1,2 +1,3 @@
from . import personal
from . import task
from . import tool

View file

@ -0,0 +1 @@
from .retrieve_tool_memory_op import RetrieveToolMemoryOp

View file

@ -0,0 +1,71 @@
from typing import List
from flowllm import C, BaseAsyncOp
from flowllm.schema.vector_node import VectorNode
from loguru import logger
from reme_ai.schema.memory import ToolMemory, vector_node_to_memory
@C.register_op()
class RetrieveToolMemoryOp(BaseAsyncOp):
file_path: str = __file__
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def async_execute(self):
tool_names: str = self.context.get("tool_names", "")
workspace_id: str = self.context.workspace_id
if not tool_names:
logger.warning("tool_names is empty, skipping processing")
self.context.response.answer = "tool_names is required"
self.context.response.success = False
return
# Split tool names by comma
tool_name_list = [name.strip() for name in tool_names.split(",") if name.strip()]
logger.info(f"workspace_id={workspace_id} retrieving {len(tool_name_list)} tools: {tool_name_list}")
# Search for each tool in the vector store
matched_tool_memories: List[ToolMemory] = []
for tool_name in tool_name_list:
nodes: List[VectorNode] = await self.vector_store.async_search(
query=tool_name,
workspace_id=workspace_id,
top_k=1
)
if nodes:
top_node = nodes[0]
memory = vector_node_to_memory(top_node)
# Ensure it's a ToolMemory and when_to_use matches
if isinstance(memory, ToolMemory) and memory.when_to_use == tool_name:
matched_tool_memories.append(memory)
logger.info(f"Found tool_memory for tool_name={tool_name}, "
f"memory_id={memory.memory_id}, "
f"total_calls={len(memory.tool_call_results)}")
else:
logger.warning(f"No exact match found for tool_name={tool_name}")
else:
logger.warning(f"No memory found for tool_name={tool_name}")
if not matched_tool_memories:
logger.info("No matching tool memories found")
self.context.response.answer = "No matching tool memories found"
self.context.response.success = False
return
# Set response
self.context.response.answer = f"Successfully retrieved {len(matched_tool_memories)} tool memories"
self.context.response.success = True
self.context.response.metadata["memory_list"] = matched_tool_memories
# Log retrieval results
for memory in matched_tool_memories:
logger.info(f"Retrieved tool: {memory.when_to_use}, "
f"total_calls={len(memory.tool_call_results)}, "
f"content_length={len(memory.content)}")

View 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,7 +107,118 @@ class PersonalMemory(BaseMemory):
metadata=metadata.pop("metadata", {}))
def vector_node_to_memory(node: VectorNode) -> BaseMemory:
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")
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")
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 = not 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 N 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 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":
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):
memory_type = node.metadata.get("memory_type")
if memory_type == "task":
return TaskMemory.from_vector_node(node)
@ -113,6 +226,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 +241,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 +261,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()

View file

@ -1,2 +1,3 @@
from . import personal
from . import task
from . import tool

View file

@ -0,0 +1,2 @@
from .parse_tool_call_result_op import ParseToolCallResultOp
from .summary_tool_memory_op import SummaryToolMemoryOp

View file

@ -0,0 +1,167 @@
import asyncio
from collections import defaultdict
from typing import List
from flowllm import C, BaseAsyncOp
from flowllm.enumeration.role import Role
from flowllm.schema.message import Message
from flowllm.schema.vector_node import VectorNode
from flowllm.utils.common_utils import extract_content
from loguru import logger
from reme_ai.schema.memory import ToolMemory, ToolCallResult, vector_node_to_memory
@C.register_op()
class ParseToolCallResultOp(BaseAsyncOp):
file_path: str = __file__
def __init__(self,
max_history_tool_call_cnt: int = 100,
evaluation_sleep_interval: float = 1.0,
**kwargs):
super().__init__(**kwargs)
self.max_history_tool_call_cnt: int = max_history_tool_call_cnt
self.evaluation_sleep_interval: float = evaluation_sleep_interval
async def _evaluate_single_tool_call(self, tool_call_result: ToolCallResult, index: int) -> ToolCallResult:
await asyncio.sleep(self.evaluation_sleep_interval * index)
prompt = self.prompt_format(
prompt_name="evaluate_tool_call_prompt",
tool_name=tool_call_result.tool_name,
input_params=str(tool_call_result.input),
output=tool_call_result.output,
success_flag=str(tool_call_result.success),
time_cost=tool_call_result.time_cost,
token_cost=tool_call_result.token_cost)
def parse_evaluation(message: Message) -> ToolCallResult:
content = message.content.strip()
eval_data = extract_content(content, "json")
# 更新 tool_call_result - 包含 summary, evaluation 和 score
tool_call_result.summary = eval_data.get("summary", "")
tool_call_result.evaluation = eval_data.get("evaluation", "")
tool_call_result.score = float(eval_data.get("score", 0.0))
# 验证 score 是否符合 2 档要求 (0.0, 1.0)
if tool_call_result.score not in [0.0, 1.0]:
if tool_call_result.score < 0.5:
tool_call_result.score = 0.0
else:
tool_call_result.score = 1.0
# 打印完整的prompt和result
logger.info(f"\n{'='*80}\nLLM Evaluation [Index {index}]\n{'='*80}\n"
f"PROMPT:\n{prompt}\n\n"
f"RESULT:\n{content}\n"
f"{'='*80}\n")
return tool_call_result
# 调用 LLM 进行评估
result = await self.llm.achat(messages=[Message(role=Role.USER, content=prompt)], callback_fn=parse_evaluation)
return result
async def async_execute(self):
tool_call_results: list = self.context.get("tool_call_results", [])
tool_call_results = [ToolCallResult(**x) if isinstance(x, dict) else x for x in tool_call_results]
workspace_id: str = self.context.workspace_id
if not tool_call_results:
self.context.response.answer = "No valid tool_call_results"
self.context.response.success = False
return
# 使用基类的 submit_async_task 提交所有评估任务
for index, tool_call_result in enumerate(tool_call_results):
self.submit_async_task(self._evaluate_single_tool_call, tool_call_result, index)
# 使用基类的 join_async_task 等待所有任务完成
# 注意: 基类已经过滤掉异常,返回的只包含成功的结果
evaluated_results = await self.join_async_task(return_exceptions=True)
tool_results_by_name = defaultdict(list)
for result in evaluated_results:
tool_results_by_name[result.tool_name].append(result)
# 处理每个 tool_name 的结果
all_memory_list = []
all_deleted_memory_ids = []
for tool_name, tool_call_results in tool_results_by_name.items():
nodes: List[VectorNode] = await self.vector_store.async_search(query=tool_name,
workspace_id=workspace_id,
top_k=1)
tool_memory: ToolMemory | None = None
exist_node: bool = False
if nodes:
top_node = nodes[0]
memory: ToolMemory = vector_node_to_memory(top_node)
# 确保是 ToolMemory 类型且 when_to_use 与 tool_name 匹配
if isinstance(memory, ToolMemory) and memory.when_to_use == tool_name:
tool_memory = memory
exist_node = True
# 如果没有找到匹配的 memory,创建新的
if tool_memory is None:
tool_memory = ToolMemory(workspace_id=workspace_id, when_to_use=tool_name)
tool_memory.tool_call_results.extend(tool_call_results)
# 保留最近的 n 个
if len(tool_memory.tool_call_results) > self.max_history_tool_call_cnt:
tool_memory.tool_call_results = tool_memory.tool_call_results[-self.max_history_tool_call_cnt:]
# 更新修改时间
tool_memory.update_modified_time()
# 如果是更新现有的 memory,需要先删除旧的
if exist_node:
all_deleted_memory_ids.append(tool_memory.memory_id)
all_memory_list.append(tool_memory)
# 设置返回结果
self.context.response.metadata["deleted_memory_ids"] = all_deleted_memory_ids
self.context.response.metadata["memory_list"] = all_memory_list
async def main():
"""Simple test for ParseToolCallResultOp"""
from flowllm.app import FlowLLMApp
from datetime import datetime
async with FlowLLMApp(load_default_config=True):
op = ParseToolCallResultOp()
# Create simple test data
tool_call_results = [
{
"create_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": "test_tool",
"input": {
"query": "search for python asyncio documentation",
"max_results": 10,
"filter_type": "official_docs",
"language": "en"
},
"output": "Found 10 relevant documentation pages for Python asyncio. Top results include: 1) Official Python docs for asyncio module, 2) Real Python asyncio tutorial, 3) Stack Overflow asyncio examples. All results are from official sources as requested.",
"token_cost": 150,
"success": True,
"time_cost": 2.3
}
]
workspace_id = "test_workspace1"
await op.async_call(tool_call_results=tool_call_results, workspace_id=workspace_id)
logger.info(f"Response: {op.context.response.model_dump_json()}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,57 @@
evaluate_tool_call_prompt: |
You are an expert in evaluating tool invocation results. Please evaluate the following tool call based on two criteria:
1. **Success**: Whether the tool invocation was successful
2. **Parameter Alignment**: Whether the result aligns with the expected behavior given the input parameters
## Tool Call Information:
- Tool Name: {tool_name}
- Success Flag: {success_flag}
- Time Cost: {time_cost}s
- Token Cost: {token_cost} tokens
- Input Parameters: {input_params}
- Output: {output}
## Evaluation Criteria:
### Important: Score independently from the success flag
The `success_flag` indicates whether the tool executed without technical errors.
The `score` should evaluate the QUALITY and RELEVANCE of the result.
A tool can execute successfully (success=True) but still produce low-quality or irrelevant results (score=0.0).
### Evaluation Dimensions:
1. **Technical Execution** (reflected in success_flag):
- Did the tool run without errors?
- Are there error messages or exceptions in the output?
2. **Result Quality** (what you should score):
- Is the output relevant to the input query/parameters?
- Does the output provide meaningful and useful information?
- Is the result appropriate given the input parameters?
- Does the output match the tool's intended purpose?
## Response Format:
Please provide your evaluation in the following JSON format:
```json
{{
"summary": "A brief one-sentence summary of the tool call result",
"evaluation": "A brief evaluation (2-3 sentences) explaining the result quality and relevance, NOT just repeating the success flag.",
"score": 1.0
}}
```
## Scoring Guidelines (Focus on Result Quality):
- **1.0**: High Quality - The output is relevant, useful, and appropriate for the given input parameters
- **0.0**: Low Quality - The output is irrelevant, incorrect, unhelpful, or inappropriate for the input
## Examples:
- success=True, but output is "No results found" for a reasonable query → score=0.0 (technically succeeded but unhelpful)
- success=True, but output contains generic/irrelevant information → score=0.0 (poor quality)
- success=True, and output provides relevant, useful information → score=1.0 (good quality)
- success=False, with error messages → score=0.0 (failed execution)
Please evaluate carefully and provide a score of exactly 0.0 or 1.0 based on OUTPUT QUALITY, not just the success flag.

View file

@ -0,0 +1,462 @@
import asyncio
from typing import List
from flowllm import C, BaseAsyncOp
from flowllm.enumeration.role import Role
from flowllm.schema.message import Message
from flowllm.schema.vector_node import VectorNode
from flowllm.utils.common_utils import extract_content
from loguru import logger
from reme_ai.schema.memory import ToolMemory, vector_node_to_memory
@C.register_op()
class SummaryToolMemoryOp(BaseAsyncOp):
file_path: str = __file__
def __init__(self,
recent_call_count: int = 30,
summary_sleep_interval: float = 1.0,
**kwargs):
super().__init__(**kwargs)
self.recent_call_count: int = recent_call_count
self.summary_sleep_interval: float = summary_sleep_interval
@staticmethod
def _format_call_summaries_markdown(recent_calls: List) -> str:
"""Format tool call summaries as markdown."""
if not recent_calls:
return "No recent calls available."
lines = []
for i, call in enumerate(recent_calls, 1):
lines.append(f"### Call #{i}")
lines.append(f"- **Summary**: {call.summary}")
lines.append(f"- **Evaluation**: {call.evaluation}")
lines.append(f"- **Score**: {call.score}")
lines.append(f"- **Success**: {call.success}")
lines.append(f"- **Time Cost**: {call.time_cost}s")
lines.append(f"- **Token Cost**: {call.token_cost}")
lines.append("")
return "\n".join(lines)
@staticmethod
def _format_statistics_markdown(statistics: dict) -> str:
"""Format statistics as markdown."""
lines = [f"- **Success Rate**: {statistics.get('success_rate', 0):.2%}",
f"- **Average Score**: {statistics.get('avg_score', 0):.3f}",
f"- **Average Time Cost**: {statistics.get('avg_time_cost', 0):.3f}s",
f"- **Average Token Cost**: {statistics.get('avg_token_cost', 0):.1f}"]
return "\n".join(lines)
async def _summarize_single_tool(self, tool_memory: ToolMemory, index: int) -> ToolMemory:
await asyncio.sleep(self.summary_sleep_interval * index)
# Get the most recent N tool calls
recent_calls = tool_memory.tool_call_results[-self.recent_call_count:]
if not recent_calls:
logger.warning(f"No tool call results found for tool: {tool_memory.when_to_use}")
return tool_memory
# Log how many unsummarized calls we're processing
unsummarized_count = sum(1 for call in recent_calls if not call.is_summarized)
logger.info(f"Summarizing tool {tool_memory.when_to_use}: "
f"{unsummarized_count}/{len(recent_calls)} unsummarized calls")
# Get statistics
statistics = tool_memory.statistic(recent_frequency=self.recent_call_count)
# Format data as markdown
call_summaries_md = self._format_call_summaries_markdown(recent_calls)
statistics_md = self._format_statistics_markdown(statistics)
# Don't include statistics in prompt - only call summaries
prompt = self.prompt_format(prompt_name="summarize_tool_usage_prompt",
tool_name=tool_memory.when_to_use,
call_summaries=call_summaries_md)
def parse_summary(message: Message) -> ToolMemory:
content = message.content.strip()
# Extract content from txt code block
llm_summary = extract_content(content, "txt")
# Append statistics markdown to LLM result
tool_memory.content = f"{llm_summary}\n\n## Statistics\n{statistics_md}"
# Mark all recent calls as summarized
for call in recent_calls:
call.is_summarized = True
# Update modified time
tool_memory.update_modified_time()
logger.info(f"Summarized tool {index}: tool_name={tool_memory.when_to_use}, "
f"content_length={len(tool_memory.content)}, "
f"marked {len(recent_calls)} calls as summarized")
return tool_memory
# Call LLM to generate summary
result = await self.llm.achat(messages=[Message(role=Role.USER, content=prompt)], callback_fn=parse_summary)
return result
async def async_execute(self):
tool_names: str = self.context.get("tool_names", "")
workspace_id: str = self.context.workspace_id
if not tool_names:
logger.warning("tool_names is empty, skipping processing")
self.context.response.answer = "tool_names is required"
self.context.response.success = False
return
# Split tool names by comma
tool_name_list = [name.strip() for name in tool_names.split(",") if name.strip()]
logger.info(f"workspace_id={workspace_id} processing {len(tool_name_list)} tools: {tool_name_list}")
# Search for each tool in the vector store
matched_tool_memories: List[ToolMemory] = []
for tool_name in tool_name_list:
nodes: List[VectorNode] = await self.vector_store.async_search(query=tool_name,
workspace_id=workspace_id,
top_k=1)
if nodes:
top_node = nodes[0]
memory = vector_node_to_memory(top_node)
# Ensure it's a ToolMemory and when_to_use matches
if isinstance(memory, ToolMemory) and memory.when_to_use == tool_name:
matched_tool_memories.append(memory)
logger.info(f"Found tool_memory for tool_name={tool_name}, "
f"memory_id={memory.memory_id}, "
f"total_calls={len(memory.tool_call_results)}")
else:
logger.warning(f"No exact match found for tool_name={tool_name}")
else:
logger.warning(f"No memory found for tool_name={tool_name}")
if not matched_tool_memories:
logger.info("No matching tool memories found")
self.context.response.answer = "No matching tool memories found"
self.context.response.success = False
return
# Check which tools need summarization
tools_need_summary = []
tools_skipped = []
for tool_memory in matched_tool_memories:
recent_calls = tool_memory.tool_call_results[-self.recent_call_count:]
unsummarized_count = sum(1 for call in recent_calls if not call.is_summarized)
if unsummarized_count > 0:
tools_need_summary.append(tool_memory)
else:
tools_skipped.append(tool_memory)
logger.info(f"Skipping tool {tool_memory.when_to_use}: all recent {len(recent_calls)} calls already summarized")
# Concurrently summarize tool memories that need it
if tools_need_summary:
logger.info(f"Starting concurrent summarization of {len(tools_need_summary)} tool memories")
# 使用基类的 submit_async_task 提交所有总结任务
for index, tool_memory in enumerate(tools_need_summary):
self.submit_async_task(self._summarize_single_tool, tool_memory, index)
# 使用基类的 join_async_task 等待所有任务完成
# 注意: 基类已经过滤掉异常,返回的只包含成功的结果
valid_summarized_memories = await self.join_async_task(return_exceptions=True)
logger.info(f"Completed summarization of {len(valid_summarized_memories)} tool memories")
else:
valid_summarized_memories = []
logger.info("All tool memories are up-to-date, no summarization needed")
# Combine summarized and skipped memories
all_memories = valid_summarized_memories + tools_skipped
# Set response
self.context.response.answer = (f"Successfully processed {len(all_memories)} tool memories: "
f"{len(valid_summarized_memories)} summarized, "
f"{len(tools_skipped)} skipped (already up-to-date)")
self.context.response.success = True
self.context.response.metadata["memory_list"] = all_memories
self.context.response.metadata["deleted_memory_ids"] = [m.memory_id for m in all_memories]
# Log summary for each tool
for memory in valid_summarized_memories:
logger.info(f"Tool: {memory.when_to_use}, "
f"Content: {memory.content[:100]}...")
async def main():
from flowllm.app import FlowLLMApp
from reme_ai.summary.tool.parse_tool_call_result_op import ParseToolCallResultOp
from reme_ai.vector_store.update_vector_store_op import UpdateVectorStoreOp
from datetime import datetime, timedelta
import random
async with FlowLLMApp(load_default_config=True):
workspace_id = "test_workspace_complex"
tool_name = "web_search_tool"
# ===== 第一步: 准备 30 条模拟的工具调用记录 =====
# 模拟不同场景的调用记录:成功、参数错误、超时、返回空结果等
logger.info("=" * 80)
logger.info("步骤1: 准备 30 条工具调用记录,模拟真实使用场景")
logger.info("=" * 80)
base_time = datetime.now() - timedelta(days=7)
tool_call_results = []
# 场景1: 成功的搜索 (15条)
success_queries = [
"Python asyncio tutorial", "machine learning basics", "React hooks guide",
"Docker best practices", "SQL optimization tips", "Git workflow strategies",
"RESTful API design", "microservices architecture", "Redis caching patterns",
"Kubernetes deployment", "GraphQL advantages", "MongoDB schema design",
"JWT authentication", "OAuth2 flow", "WebSocket real-time"
]
for i, query in enumerate(success_queries):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=i * 2)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": query,
"max_results": random.randint(5, 20),
"language": "en",
"filter_type": "technical_docs"
},
"output": f"Found {random.randint(8, 20)} relevant results for '{query}'. Top results include official documentation, tutorials, and best practice guides.",
"token_cost": random.randint(100, 300),
"success": True,
"time_cost": round(random.uniform(1.5, 3.5), 2)
})
# 场景2: 参数不合理导致的部分成功 (8条)
for i in range(8):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=30 + i * 3)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"test query {i}", # 查询词过于简单
"max_results": 100, # 请求过多结果
"language": "unknown", # 语言参数错误
},
"output": f"Warning: language 'unknown' not supported, using default. Query too generic, returning limited results. Found {random.randint(2, 5)} results.",
"token_cost": random.randint(50, 150),
"success": True,
"time_cost": round(random.uniform(2.0, 4.0), 2)
})
# 场景3: 超时或失败 (5条)
for i in range(5):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=54 + i * 4)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"extremely complex query with many conditions {i}",
"max_results": 50,
"language": "en",
"filter_type": "all"
},
"output": "Error: Request timeout after 10 seconds. Try simplifying the query or reducing max_results.",
"token_cost": 20,
"success": False,
"time_cost": 10.0
})
# 场景4: 空结果 (2条)
for i in range(2):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=74 + i * 5)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"xyzabc123nonexistent{i}", # 不存在的内容
"max_results": 10,
"language": "en",
},
"output": "No results found for the given query. Please try different keywords.",
"token_cost": 30,
"success": True,
"time_cost": 1.2
})
logger.info(f"准备了 {len(tool_call_results)} 条工具调用记录")
logger.info(f"- 成功调用: 15 条")
logger.info(f"- 参数不合理: 8 条")
logger.info(f"- 超时失败: 5 条")
logger.info(f"- 空结果: 2 条")
# ===== 第二步: 使用 ParseToolCallResultOp >> UpdateVectorStoreOp 串联运行 =====
logger.info("\n" + "=" * 80)
logger.info("步骤2: ParseToolCallResultOp >> UpdateVectorStoreOp 串联评估并保存")
logger.info("=" * 80)
# 使用 >> 操作符串联两个Op,自动传递context和metadata
pipeline = ParseToolCallResultOp(evaluation_sleep_interval=0.1) >> UpdateVectorStoreOp()
await pipeline.async_call(
tool_call_results=tool_call_results,
tool_name=tool_name,
workspace_id=workspace_id
)
if not pipeline.context.response.success:
logger.error(f"Pipeline failed: {pipeline.context.response.answer}")
return
logger.info(f"✓ Pipeline 完成")
logger.info(f" 评估了 {len(tool_call_results)} 条记录")
logger.info(f" 每条记录包含: summary, evaluation, score (0.0/0.5/1.0)")
# 显示一些评估结果示例
memory_list = pipeline.context.response.metadata.get("memory_list", [])
if memory_list:
tool_memory = memory_list[0]
logger.info(f"\n评估结果示例 (前3条):")
for i, result in enumerate(tool_memory.tool_call_results[:3], 1):
logger.info(f" 调用 #{i}:")
logger.info(f" 查询: {result.input.get('query', 'N/A')}")
logger.info(f" 评分: {result.score}")
logger.info(f" 总结: {result.summary[:80]}...")
logger.info(f" 评价: {result.evaluation[:80]}...")
# 显示向量数据库更新结果
update_result = pipeline.context.response.metadata.get("update_result", {})
logger.info(f"\n✓ 向量数据库更新完成:")
logger.info(f" 删除记录数: {update_result.get('deleted_count', 0)}")
logger.info(f" 插入记录数: {update_result.get('inserted_count', 0)}")
# ===== 第三步: 使用 SummaryToolMemoryOp 总结工具使用模式 =====
logger.info("\n" + "=" * 80)
logger.info("步骤3: 使用 SummaryToolMemoryOp 从 30 条记录中提取使用模式和建议")
logger.info("=" * 80)
summary_op = SummaryToolMemoryOp(
recent_call_count=30, # 分析最近30条记录
summary_sleep_interval=0.5
)
await summary_op.async_call(
tool_names=tool_name,
workspace_id=workspace_id
)
if not summary_op.context.response.success:
logger.error(f"SummaryToolMemoryOp failed: {summary_op.context.response.answer}")
return
logger.info(f"✓ SummaryToolMemoryOp 完成")
# ===== 第四步: 展示 summary 的价值 =====
logger.info("\n" + "=" * 80)
logger.info("步骤4: 展示 Summary 如何将分散的调用记录转化为有价值的使用指南")
logger.info("=" * 80)
summarized_memories = summary_op.context.response.metadata.get("memory_list", [])
if summarized_memories:
summarized_memory = summarized_memories[0]
logger.info(f"\n工具名称: {summarized_memory.when_to_use}")
logger.info(f"\n统计信息:")
stats = summarized_memory.statistic(recent_frequency=30)
logger.info(f" 总调用次数: {len(summarized_memory.tool_call_results)}")
logger.info(f" 成功率: {stats['success_rate']:.1%}")
logger.info(f" 平均评分: {stats['avg_score']:.2f}")
logger.info(f" 平均耗时: {stats['avg_time_cost']:.2f}s")
logger.info(f" 平均Token消耗: {stats['avg_token_cost']:.1f}")
logger.info(f"\n" + "=" * 60)
logger.info("Summary 生成的使用指南 (从30条分散记录中提取):")
logger.info("=" * 60)
logger.info(summarized_memory.content)
logger.info("=" * 60)
# ===== 第五步: 验证跳过逻辑 - 再次运行应该跳过 =====
logger.info("\n" + "=" * 80)
logger.info("步骤5: 验证跳过逻辑 - 再次运行 SummaryToolMemoryOp 应该跳过已总结的记录")
logger.info("=" * 80)
# 需要先保存更新后的 memory (带有 is_summarized=True 标记)
from reme_ai.vector_store.update_vector_store_op import UpdateVectorStoreOp
update_op = UpdateVectorStoreOp()
await update_op.async_call(
memory_list=summarized_memories,
workspace_id=workspace_id
)
logger.info("✓ 已保存带有 is_summarized 标记的 tool memory")
# 再次运行总结
summary_op2 = SummaryToolMemoryOp(
recent_call_count=30,
summary_sleep_interval=0.5
)
await summary_op2.async_call(
tool_names=tool_name,
workspace_id=workspace_id
)
if summary_op2.context.response.success:
logger.info("✓ 第二次总结完成 (预期应该跳过)")
else:
logger.info(f"第二次总结失败: {summary_op2.context.response.answer}")
# ===== 第六步: 添加新记录并验证增量总结 =====
logger.info("\n" + "=" * 80)
logger.info("步骤6: 添加 1 条新记录,验证会触发重新总结")
logger.info("=" * 80)
# 添加一条新的工具调用记录
new_tool_call = {
"create_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": "FastAPI async best practices",
"max_results": 15,
"language": "en",
"filter_type": "technical_docs"
},
"output": "Found 12 excellent resources on FastAPI async patterns including official docs, real-world examples, and performance tips.",
"token_cost": 180,
"success": True,
"time_cost": 2.1
}
# 使用 ParseToolCallResultOp 评估新记录
new_pipeline = ParseToolCallResultOp(evaluation_sleep_interval=0.1) >> UpdateVectorStoreOp()
await new_pipeline.async_call(
tool_call_results=[new_tool_call],
tool_name=tool_name,
workspace_id=workspace_id
)
logger.info("✓ 添加并评估了 1 条新记录")
# 第三次运行总结 - 这次应该会执行
summary_op3 = SummaryToolMemoryOp(
recent_call_count=30,
summary_sleep_interval=0.5
)
await summary_op3.async_call(
tool_names=tool_name,
workspace_id=workspace_id
)
if summary_op3.context.response.success:
logger.info("✓ 第三次总结完成 (因为有新记录)")
summarized_memories3 = summary_op3.context.response.metadata.get("memory_list", [])
if summarized_memories3:
summarized_memory3 = summarized_memories3[0]
unsummarized = sum(1 for call in summarized_memory3.tool_call_results[-30:] if not call.is_summarized)
logger.info(f" 最近30条中未总结的记录数: {unsummarized} (应该为 0)")
else:
logger.info(f"第三次总结失败: {summary_op3.context.response.answer}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -0,0 +1,24 @@
summarize_tool_usage_prompt: |
You are an expert in analyzing tool usage patterns and generating practical usage guidelines.
## Tool Information:
- Tool Name: {tool_name}
## Recent Tool Call History:
{call_summaries}
## Your Task:
Based on the tool call history, generate a concise and logical tool usage description following this structure:
1. **Core Function**: What this tool does and when to use it
2. **Success Patterns**: Parameter patterns and usage scenarios that work well
3. **Common Issues**: Main pitfalls to avoid and why they fail
4. **Best Practices**: 2-3 actionable recommendations
## Response Format:
Provide a structured, concise description (max 200 words). Focus on actionable insights derived from actual usage data. Avoid generic advice.
```txt
[Your concise, data-driven tool usage description]
```

View file

@ -0,0 +1,195 @@
import random
from datetime import datetime, timedelta
from typing import List, Dict, Any
def create_mock_tool_call_results(tool_name: str, count: int = 30) -> List[Dict[str, Any]]:
"""
Create mock tool call results for testing
Args:
tool_name: Name of the tool to create results for
count: Number of mock results to create
Returns:
List of tool call result dictionaries
"""
base_time = datetime.now() - timedelta(days=7)
tool_call_results = []
if tool_name == "web_search":
# Scenario 1: Successful searches (15 calls)
success_queries = [
"Python asyncio tutorial", "machine learning basics", "React hooks guide",
"Docker best practices", "SQL optimization tips", "Git workflow strategies",
"RESTful API design", "microservices architecture", "Redis caching patterns",
"Kubernetes deployment", "GraphQL advantages", "MongoDB schema design",
"JWT authentication", "OAuth2 flow", "WebSocket real-time"
]
for i, query in enumerate(success_queries):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=i * 2)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": query,
"max_results": random.randint(5, 20),
"language": "en",
"filter_type": "technical_docs"
},
"output": f"Found {random.randint(8, 20)} relevant results for '{query}'. Top results include official documentation, tutorials, and best practice guides.",
"token_cost": random.randint(100, 300),
"success": True,
"time_cost": round(random.uniform(1.5, 3.5), 2)
})
# Scenario 2: Poor parameters (8 calls)
for i in range(8):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=30 + i * 3)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"test query {i}",
"max_results": 100,
"language": "unknown",
},
"output": f"Warning: language 'unknown' not supported, using default. Query too generic, returning limited results. Found {random.randint(2, 5)} results.",
"token_cost": random.randint(50, 150),
"success": True,
"time_cost": round(random.uniform(2.0, 4.0), 2)
})
# Scenario 3: Timeouts or failures (5 calls)
for i in range(5):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=54 + i * 4)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"extremely complex query with many conditions {i}",
"max_results": 50,
"language": "en",
"filter_type": "all"
},
"output": "Error: Request timeout after 10 seconds. Try simplifying the query or reducing max_results.",
"token_cost": 20,
"success": False,
"time_cost": 10.0
})
# Scenario 4: Empty results (2 calls)
for i in range(2):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=74 + i * 5)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"query": f"xyzabc123nonexistent{i}",
"max_results": 10,
"language": "en",
},
"output": "No results found for the given query. Please try different keywords.",
"token_cost": 30,
"success": True,
"time_cost": 1.2
})
elif tool_name == "database_query":
# Scenario 1: Successful queries (12 calls)
for i in range(12):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=i * 3)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"table": f"users_{i % 3}",
"query": f"SELECT * FROM table WHERE id > {i * 10}",
"limit": random.randint(10, 100)
},
"output": f"Query executed successfully. Returned {random.randint(5, 50)} rows in {round(random.uniform(0.1, 0.5), 3)}s.",
"token_cost": random.randint(20, 80),
"success": True,
"time_cost": round(random.uniform(0.1, 0.5), 3)
})
# Scenario 2: Slow queries (6 calls)
for i in range(6):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=36 + i * 4)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"table": "large_table",
"query": f"SELECT * FROM large_table WHERE name LIKE '%pattern%' ORDER BY created_at",
"limit": 1000
},
"output": f"Query executed but took longer than expected. Returned {random.randint(100, 1000)} rows. Consider adding indexes.",
"token_cost": random.randint(50, 150),
"success": True,
"time_cost": round(random.uniform(5.0, 10.0), 2)
})
# Scenario 3: Query errors (4 calls)
for i in range(4):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=60 + i * 5)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"table": "invalid_table",
"query": f"SELECT * FROM invalid_table WHERE bad_column = {i}",
"limit": 10
},
"output": f"Error: Table 'invalid_table' does not exist or column 'bad_column' not found.",
"token_cost": 10,
"success": False,
"time_cost": 0.05
})
elif tool_name == "file_processor":
# Scenario 1: Successful file processing (10 calls)
file_types = ["csv", "json", "xml", "txt", "pdf"]
for i in range(10):
file_type = file_types[i % len(file_types)]
tool_call_results.append({
"create_time": (base_time + timedelta(hours=i * 4)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"file_path": f"/data/file_{i}.{file_type}",
"operation": "read",
"encoding": "utf-8"
},
"output": f"Successfully processed {file_type.upper()} file. Size: {random.randint(100, 5000)}KB, Records: {random.randint(100, 10000)}",
"token_cost": random.randint(50, 200),
"success": True,
"time_cost": round(random.uniform(1.0, 3.0), 2)
})
# Scenario 2: Large file warnings (5 calls)
for i in range(5):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=40 + i * 6)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"file_path": f"/data/large_file_{i}.csv",
"operation": "read",
"encoding": "utf-8"
},
"output": f"Warning: Large file detected ({random.randint(50, 200)}MB). Processing may take longer. Consider using streaming mode.",
"token_cost": random.randint(200, 500),
"success": True,
"time_cost": round(random.uniform(10.0, 30.0), 2)
})
# Scenario 3: File not found (3 calls)
for i in range(3):
tool_call_results.append({
"create_time": (base_time + timedelta(hours=70 + i * 8)).strftime("%Y-%m-%d %H:%M:%S"),
"tool_name": tool_name,
"input": {
"file_path": f"/invalid/path/file_{i}.txt",
"operation": "read",
"encoding": "utf-8"
},
"output": "Error: File not found. Please check the file path and ensure the file exists.",
"token_cost": 10,
"success": False,
"time_cost": 0.01
})
return tool_call_results[:count]

View file

@ -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
View 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
View 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)