Toadied's picture
2312
8b383ad verified
Raw
History Blame Contribute Delete
19.6 kB
"""NoteTool - 结构化笔记工具
为Agent提供结构化笔记能力,支持:
- 创建/读取/更新/删除笔记
- 按类型组织(任务状态、结论、阻塞项、行动计划等)
- 持久化存储(Markdown格式,带YAML前置元数据)
- 搜索与过滤
- 与MemoryTool集成(可选)
使用场景:
- 长时程任务的状态跟踪
- 关键结论与依赖记录
- 待办事项与行动计划
- 项目知识沉淀
笔记格式示例:
```markdown
---
id: note_20250118_120000_0
title: 项目进展
type: task_state
tags: [milestone, phase1]
created_at: 2025-01-18T12:00:00
updated_at: 2025-01-18T12:00:00
---
# 项目进展
已完成需求分析,下一步:设计方案
## 关键里程碑
- [x] 需求收集
- [ ] 方案设计
```
"""
from typing import Dict, Any, List
from datetime import datetime
from pathlib import Path
import json
import re
from ..base import Tool, ToolParameter, tool_action
class NoteTool(Tool):
"""笔记工具
为Agent提供结构化笔记管理能力,支持多种笔记类型:
- task_state: 任务状态
- conclusion: 关键结论
- blocker: 阻塞项
- action: 行动计划
- reference: 参考资料
- general: 通用笔记
用法示例:
```python
note_tool = NoteTool(workspace="./project_notes")
# 创建笔记
note_tool.run({
"action": "create",
"title": "项目进展",
"content": "已完成需求分析,下一步:设计方案",
"note_type": "task_state",
"tags": ["milestone", "phase1"]
})
# 读取笔记
notes = note_tool.run({"action": "list", "note_type": "task_state"})
```
"""
def __init__(
self,
workspace: str = "./notes",
auto_backup: bool = True,
max_notes: int = 1000,
expandable: bool = False
):
super().__init__(
name="note",
description="笔记工具 - 创建、读取、更新、删除结构化笔记,支持任务状态、结论、阻塞项等类型",
expandable=expandable
)
self.workspace = Path(workspace)
self.auto_backup = auto_backup
self.max_notes = max_notes
# 确保工作目录存在
self.workspace.mkdir(parents=True, exist_ok=True)
# 笔记索引文件
self.index_file = self.workspace / "notes_index.json"
self._load_index()
def _load_index(self):
"""加载笔记索引"""
if self.index_file.exists():
with open(self.index_file, 'r', encoding='utf-8') as f:
self.notes_index = json.load(f)
else:
self.notes_index = {
"notes": [],
"metadata": {
"created_at": datetime.now().isoformat(),
"total_notes": 0
}
}
self._save_index()
def _save_index(self):
"""保存笔记索引"""
with open(self.index_file, 'w', encoding='utf-8') as f:
json.dump(self.notes_index, f, ensure_ascii=False, indent=2)
def _generate_note_id(self) -> str:
"""生成笔记ID"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
count = len(self.notes_index["notes"])
return f"note_{timestamp}_{count}"
def _get_note_path(self, note_id: str) -> Path:
"""获取笔记文件路径"""
return self.workspace / f"{note_id}.md"
def _note_to_markdown(self, note: Dict[str, Any]) -> str:
"""将笔记对象转换为Markdown格式"""
# YAML前置元数据
frontmatter = "---\n"
frontmatter += f"id: {note['id']}\n"
frontmatter += f"title: {note['title']}\n"
frontmatter += f"type: {note['type']}\n"
if note.get('tags'):
tags_str = json.dumps(note['tags'])
frontmatter += f"tags: {tags_str}\n"
frontmatter += f"created_at: {note['created_at']}\n"
frontmatter += f"updated_at: {note['updated_at']}\n"
frontmatter += "---\n\n"
# Markdown内容
content = f"# {note['title']}\n\n"
content += note['content']
return frontmatter + content
def _markdown_to_note(self, markdown_text: str) -> Dict[str, Any]:
"""将Markdown文本解析为笔记对象"""
# 提取YAML前置元数据
frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', markdown_text, re.DOTALL)
if not frontmatter_match:
raise ValueError("无效的笔记格式:缺少YAML前置元数据")
frontmatter_text = frontmatter_match.group(1)
content_start = frontmatter_match.end()
# 解析YAML(简化版)
note = {}
for line in frontmatter_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
# 处理特殊字段
if key == 'tags':
try:
note[key] = json.loads(value)
except (json.JSONDecodeError, ValueError):
note[key] = []
else:
note[key] = value
# 提取内容(去掉标题行)
markdown_content = markdown_text[content_start:].strip()
# 移除第一行的 # 标题
lines = markdown_content.split('\n')
if lines and lines[0].startswith('# '):
markdown_content = '\n'.join(lines[1:]).strip()
note['content'] = markdown_content
# 添加元数据
note['metadata'] = {
'word_count': len(markdown_content),
'status': 'active'
}
return note
def run(self, parameters: Dict[str, Any]) -> str:
"""执行工具(非展开模式)"""
if not self.validate_parameters(parameters):
return "❌ 参数验证失败"
action = parameters.get("action")
# 根据action调用对应的方法,传入提取的参数
if action == "create":
return self._create_note(
title=parameters.get("title"),
content=parameters.get("content"),
note_type=parameters.get("note_type", "general"),
tags=parameters.get("tags")
)
elif action == "read":
return self._read_note(note_id=parameters.get("note_id"))
elif action == "update":
return self._update_note(
note_id=parameters.get("note_id"),
title=parameters.get("title"),
content=parameters.get("content"),
note_type=parameters.get("note_type"),
tags=parameters.get("tags")
)
elif action == "delete":
return self._delete_note(note_id=parameters.get("note_id"))
elif action == "list":
return self._list_notes(
note_type=parameters.get("note_type"),
limit=parameters.get("limit", 10)
)
elif action == "search":
return self._search_notes(
query=parameters.get("query"),
limit=parameters.get("limit", 10)
)
elif action == "summary":
return self._get_summary()
else:
return f"❌ 不支持的操作: {action}"
def get_parameters(self) -> List[ToolParameter]:
"""获取工具参数定义"""
return [
ToolParameter(
name="action",
type="string",
description=(
"操作类型: create(创建), read(读取), update(更新), "
"delete(删除), list(列表), search(搜索), summary(摘要)"
),
required=True
),
ToolParameter(
name="title",
type="string",
description="笔记标题(create/update时必需)",
required=False
),
ToolParameter(
name="content",
type="string",
description="笔记内容(create/update时必需)",
required=False
),
ToolParameter(
name="note_type",
type="string",
description=(
"笔记类型: task_state(任务状态), conclusion(结论), "
"blocker(阻塞项), action(行动计划), reference(参考), general(通用)"
),
required=False,
default="general"
),
ToolParameter(
name="tags",
type="array",
description="标签列表(可选)",
required=False
),
ToolParameter(
name="note_id",
type="string",
description="笔记ID(read/update/delete时必需)",
required=False
),
ToolParameter(
name="query",
type="string",
description="搜索关键词(search时必需)",
required=False
),
ToolParameter(
name="limit",
type="integer",
description="返回结果数量限制(默认10)",
required=False,
default=10
),
]
@tool_action("note_create", "创建一条新的结构化笔记")
def _create_note(self, title: str, content: str, note_type: str = "general", tags: List[str] = None) -> str:
"""创建笔记
Args:
title: 笔记标题
content: 笔记内容
note_type: 笔记类型 (task_state, conclusion, blocker, action, reference, general)
tags: 标签列表
Returns:
创建结果
"""
if not title or not content:
return "❌ 创建笔记需要提供 title 和 content"
# 检查笔记数量限制
if len(self.notes_index["notes"]) >= self.max_notes:
return f"❌ 笔记数量已达上限 ({self.max_notes})"
# 生成笔记ID
note_id = self._generate_note_id()
# 创建笔记对象
note = {
"id": note_id,
"title": title,
"content": content,
"type": note_type,
"tags": tags if isinstance(tags, list) else [],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"metadata": {
"word_count": len(content),
"status": "active"
}
}
# 保存笔记文件(Markdown格式)
note_path = self._get_note_path(note_id)
markdown_content = self._note_to_markdown(note)
with open(note_path, 'w', encoding='utf-8') as f:
f.write(markdown_content)
# 更新索引
self.notes_index["notes"].append({
"id": note_id,
"title": title,
"type": note_type,
"tags": tags if isinstance(tags, list) else [],
"created_at": note["created_at"]
})
self.notes_index["metadata"]["total_notes"] = len(self.notes_index["notes"])
self._save_index()
return f"✅ 笔记创建成功\nID: {note_id}\n标题: {title}\n类型: {note_type}"
@tool_action("note_read", "读取指定ID的笔记")
def _read_note(self, note_id: str) -> str:
"""读取笔记
Args:
note_id: 笔记ID
Returns:
笔记内容
"""
if not note_id:
return "❌ 读取笔记需要提供 note_id"
note_path = self._get_note_path(note_id)
if not note_path.exists():
return f"❌ 笔记不存在: {note_id}"
with open(note_path, 'r', encoding='utf-8') as f:
markdown_text = f.read()
note = self._markdown_to_note(markdown_text)
return self._format_note(note)
@tool_action("note_update", "更新已存在的笔记")
def _update_note(self, note_id: str, title: str = None, content: str = None, note_type: str = None, tags: List[str] = None) -> str:
"""更新笔记
Args:
note_id: 笔记ID
title: 新标题(可选)
content: 新内容(可选)
note_type: 新类型(可选)
tags: 新标签列表(可选)
Returns:
更新结果
"""
if not note_id:
return "❌ 更新笔记需要提供 note_id"
note_path = self._get_note_path(note_id)
if not note_path.exists():
return f"❌ 笔记不存在: {note_id}"
# 读取现有笔记
with open(note_path, 'r', encoding='utf-8') as f:
markdown_text = f.read()
note = self._markdown_to_note(markdown_text)
# 更新字段
if title:
note["title"] = title
if content:
note["content"] = content
note["metadata"]["word_count"] = len(content)
if note_type:
note["type"] = note_type
if tags is not None:
note["tags"] = tags if isinstance(tags, list) else []
note["updated_at"] = datetime.now().isoformat()
# 保存更新(Markdown格式)
markdown_content = self._note_to_markdown(note)
with open(note_path, 'w', encoding='utf-8') as f:
f.write(markdown_content)
# 更新索引
for idx_note in self.notes_index["notes"]:
if idx_note["id"] == note_id:
idx_note["title"] = note["title"]
idx_note["type"] = note["type"]
idx_note["tags"] = note["tags"]
break
self._save_index()
return f"✅ 笔记更新成功: {note_id}"
@tool_action("note_delete", "删除指定ID的笔记")
def _delete_note(self, note_id: str) -> str:
"""删除笔记
Args:
note_id: 笔记ID
Returns:
删除结果
"""
if not note_id:
return "❌ 删除笔记需要提供 note_id"
note_path = self._get_note_path(note_id)
if not note_path.exists():
return f"❌ 笔记不存在: {note_id}"
# 删除文件
note_path.unlink()
# 更新索引
self.notes_index["notes"] = [
n for n in self.notes_index["notes"] if n["id"] != note_id
]
self.notes_index["metadata"]["total_notes"] = len(self.notes_index["notes"])
self._save_index()
return f"✅ 笔记已删除: {note_id}"
@tool_action("note_list", "列出所有笔记或指定类型的笔记")
def _list_notes(self, note_type: str = None, limit: int = 10) -> str:
"""列出笔记
Args:
note_type: 笔记类型过滤(可选)
limit: 返回结果数量限制
Returns:
笔记列表
"""
# 过滤笔记
filtered_notes = self.notes_index["notes"]
if note_type:
filtered_notes = [n for n in filtered_notes if n["type"] == note_type]
# 限制数量
filtered_notes = filtered_notes[:limit]
if not filtered_notes:
return "📝 暂无笔记"
result = f"📝 笔记列表(共 {len(filtered_notes)} 条)\n\n"
for note in filtered_notes:
result += f"• [{note['type']}] {note['title']}\n"
result += f" ID: {note['id']}\n"
if note.get('tags'):
result += f" 标签: {', '.join(note['tags'])}\n"
result += f" 创建时间: {note['created_at']}\n\n"
return result
@tool_action("note_search", "搜索包含关键词的笔记")
def _search_notes(self, query: str, limit: int = 10) -> str:
"""搜索笔记
Args:
query: 搜索关键词
limit: 返回结果数量限制
Returns:
搜索结果
"""
if not query:
return "❌ 搜索需要提供 query"
query_lower = query.lower()
# 搜索匹配的笔记
matched_notes = []
for idx_note in self.notes_index["notes"]:
note_path = self._get_note_path(idx_note["id"])
if note_path.exists():
with open(note_path, 'r', encoding='utf-8') as f:
markdown_text = f.read()
try:
note = self._markdown_to_note(markdown_text)
except Exception as e:
print(f"⚠️ 解析笔记失败 {idx_note['id']}: {e}")
continue
# 检查标题、内容、标签是否匹配
if (query_lower in note["title"].lower() or
query_lower in note["content"].lower() or
any(query_lower in tag.lower() for tag in note.get("tags", []))):
matched_notes.append(note)
# 限制数量
matched_notes = matched_notes[:limit]
if not matched_notes:
return f"📝 未找到匹配 '{query}' 的笔记"
result = f"🔍 搜索结果(共 {len(matched_notes)} 条)\n\n"
for note in matched_notes:
result += self._format_note(note, compact=True) + "\n"
return result
@tool_action("note_summary", "获取笔记系统的摘要统计信息")
def _get_summary(self) -> str:
"""获取笔记摘要
Returns:
摘要信息
"""
total = len(self.notes_index["notes"])
# 按类型统计
type_counts = {}
for note in self.notes_index["notes"]:
note_type = note["type"]
type_counts[note_type] = type_counts.get(note_type, 0) + 1
result = f"📊 笔记摘要\n\n"
result += f"总笔记数: {total}\n\n"
result += "按类型统计:\n"
for note_type, count in sorted(type_counts.items()):
result += f" • {note_type}: {count}\n"
return result
def _format_note(self, note: Dict[str, Any], compact: bool = False) -> str:
"""格式化笔记输出"""
if compact:
return (
f"[{note['type']}] {note['title']}\n"
f"ID: {note['id']}\n"
f"内容: {note['content'][:100]}{'...' if len(note['content']) > 100 else ''}"
)
else:
result = f"📝 笔记详情\n\n"
result += f"ID: {note['id']}\n"
result += f"标题: {note['title']}\n"
result += f"类型: {note['type']}\n"
if note.get('tags'):
result += f"标签: {', '.join(note['tags'])}\n"
result += f"创建时间: {note['created_at']}\n"
result += f"更新时间: {note['updated_at']}\n"
result += f"\n内容:\n{note['content']}\n"
return result