| """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格式""" |
| |
| 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" |
| |
| |
| content = f"# {note['title']}\n\n" |
| content += note['content'] |
| |
| return frontmatter + content |
| |
| def _markdown_to_note(self, markdown_text: str) -> Dict[str, Any]: |
| """将Markdown文本解析为笔记对象""" |
| |
| 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() |
| |
| |
| 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") |
|
|
| |
| 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})" |
| |
| |
| 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" |
| } |
| } |
| |
| |
| 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_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 |
|
|