mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-03 00:40:45 +08:00
feat: support skills creator and gemini models
This commit is contained in:
@@ -447,35 +447,27 @@ class MemoryManager:
|
||||
today_file = self.flush_manager.get_today_memory_file().name
|
||||
|
||||
if lang == "zh":
|
||||
guidance = f"""## 记忆召回
|
||||
下方"背景知识"包含你的核心长期记忆,可直接使用。如果背景知识中没有相关信息,再用 memory_search 搜索历史记录(memory/*.md 日期文件)。
|
||||
guidance = f"""## 记忆系统
|
||||
|
||||
## 记忆存储
|
||||
当用户分享持久偏好、决策或重要事实时(无论是否明确要求"记住"),主动存储:
|
||||
- 持久信息(偏好、决策、人物信息)→ memory/MEMORY.md
|
||||
- 当天的笔记和上下文 → memory/{today_file}
|
||||
- 静默存储,仅在用户明确要求时确认
|
||||
**背景知识**: 下方包含核心长期记忆,可直接使用。需要查找历史时,用 memory_search 搜索(搜索一次即可,不要重复)。
|
||||
|
||||
## 记忆使用原则
|
||||
- 不要主动提起或列举记忆内容
|
||||
- 只在用户明确询问相关信息时才使用记忆
|
||||
- 记忆是背景知识,不是要展示的内容
|
||||
- 自然使用记忆,就像你本来就知道这些信息"""
|
||||
**存储记忆**: 当用户分享重要信息时(偏好、决策、事实等),主动用 write 工具存储:
|
||||
- 长期信息 → memory/MEMORY.md
|
||||
- 当天笔记 → memory/{today_file}
|
||||
- 静默存储,仅在明确要求时确认
|
||||
|
||||
**使用原则**: 自然使用记忆,就像你本来就知道。不要主动提起或列举记忆,除非用户明确询问。"""
|
||||
else:
|
||||
guidance = f"""## Memory Recall
|
||||
"Background Knowledge" below contains your core long-term memories - use them directly. If information is not in Background Knowledge, use memory_search to search, then use memory_get to read files (path format: memory/MEMORY.md, memory/2026-01-30.md).
|
||||
guidance = f"""## Memory System
|
||||
|
||||
## Memory Storage
|
||||
When user shares durable preferences, decisions, or important facts (whether or not they explicitly say "remember"), proactively store:
|
||||
- Durable info (preferences, decisions, people) → memory/MEMORY.md
|
||||
- Daily notes and context → memory/{today_file}
|
||||
- Store silently; only confirm when explicitly requested
|
||||
**Background Knowledge**: Core long-term memories below - use directly. For history, use memory_search once (don't repeat).
|
||||
|
||||
## Memory Usage Principles
|
||||
- Don't proactively mention or list memory contents
|
||||
- Only use memories when user explicitly asks about them
|
||||
- Memories are background knowledge, not content to showcase
|
||||
- Use memories naturally as if you inherently knew this information"""
|
||||
**Store Memories**: When user shares important info (preferences, decisions, facts), proactively write:
|
||||
- Durable info → memory/MEMORY.md
|
||||
- Daily notes → memory/{today_file}
|
||||
- Store silently; confirm only when explicitly requested
|
||||
|
||||
**Usage**: Use memories naturally as if you always knew. Don't mention or list unless user explicitly asks."""
|
||||
|
||||
if include_context:
|
||||
# Load bootstrap context (MEMORY.md only, like clawdbot)
|
||||
|
||||
13
agent/prompt/__init__.py
Normal file
13
agent/prompt/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Agent Prompt Module - 系统提示词构建模块
|
||||
"""
|
||||
|
||||
from .builder import PromptBuilder, build_agent_system_prompt
|
||||
from .workspace import ensure_workspace, load_context_files
|
||||
|
||||
__all__ = [
|
||||
'PromptBuilder',
|
||||
'build_agent_system_prompt',
|
||||
'ensure_workspace',
|
||||
'load_context_files',
|
||||
]
|
||||
601
agent/prompt/builder.py
Normal file
601
agent/prompt/builder.py
Normal file
@@ -0,0 +1,601 @@
|
||||
"""
|
||||
System Prompt Builder - 系统提示词构建器
|
||||
|
||||
参考 clawdbot 的 system-prompt.ts,实现中文版的模块化提示词构建
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Dict, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from common.log import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextFile:
|
||||
"""上下文文件"""
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
class PromptBuilder:
|
||||
"""提示词构建器"""
|
||||
|
||||
def __init__(self, workspace_dir: str, language: str = "zh"):
|
||||
"""
|
||||
初始化提示词构建器
|
||||
|
||||
Args:
|
||||
workspace_dir: 工作空间目录
|
||||
language: 语言 ("zh" 或 "en")
|
||||
"""
|
||||
self.workspace_dir = workspace_dir
|
||||
self.language = language
|
||||
|
||||
def build(
|
||||
self,
|
||||
base_persona: Optional[str] = None,
|
||||
user_identity: Optional[Dict[str, str]] = None,
|
||||
tools: Optional[List[Any]] = None,
|
||||
context_files: Optional[List[ContextFile]] = None,
|
||||
skill_manager: Any = None,
|
||||
memory_manager: Any = None,
|
||||
runtime_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
构建完整的系统提示词
|
||||
|
||||
Args:
|
||||
base_persona: 基础人格描述(会被context_files中的SOUL.md覆盖)
|
||||
user_identity: 用户身份信息
|
||||
tools: 工具列表
|
||||
context_files: 上下文文件列表(SOUL.md, USER.md, README.md等)
|
||||
skill_manager: 技能管理器
|
||||
memory_manager: 记忆管理器
|
||||
runtime_info: 运行时信息
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
完整的系统提示词
|
||||
"""
|
||||
return build_agent_system_prompt(
|
||||
workspace_dir=self.workspace_dir,
|
||||
language=self.language,
|
||||
base_persona=base_persona,
|
||||
user_identity=user_identity,
|
||||
tools=tools,
|
||||
context_files=context_files,
|
||||
skill_manager=skill_manager,
|
||||
memory_manager=memory_manager,
|
||||
runtime_info=runtime_info,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def build_agent_system_prompt(
|
||||
workspace_dir: str,
|
||||
language: str = "zh",
|
||||
base_persona: Optional[str] = None,
|
||||
user_identity: Optional[Dict[str, str]] = None,
|
||||
tools: Optional[List[Any]] = None,
|
||||
context_files: Optional[List[ContextFile]] = None,
|
||||
skill_manager: Any = None,
|
||||
memory_manager: Any = None,
|
||||
runtime_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
构建Agent系统提示词(精简版,中文)
|
||||
|
||||
包含的sections:
|
||||
1. 基础身份
|
||||
2. 工具说明
|
||||
3. 技能系统
|
||||
4. 记忆系统
|
||||
5. 用户身份
|
||||
6. 文档路径
|
||||
7. 工作空间
|
||||
8. 项目上下文文件
|
||||
|
||||
Args:
|
||||
workspace_dir: 工作空间目录
|
||||
language: 语言 ("zh" 或 "en")
|
||||
base_persona: 基础人格描述
|
||||
user_identity: 用户身份信息
|
||||
tools: 工具列表
|
||||
context_files: 上下文文件列表
|
||||
skill_manager: 技能管理器
|
||||
memory_manager: 记忆管理器
|
||||
runtime_info: 运行时信息
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
完整的系统提示词
|
||||
"""
|
||||
sections = []
|
||||
|
||||
# 1. 基础身份
|
||||
sections.extend(_build_identity_section(base_persona, language))
|
||||
|
||||
# 2. 工具说明
|
||||
if tools:
|
||||
sections.extend(_build_tooling_section(tools, language))
|
||||
|
||||
# 3. 技能系统
|
||||
if skill_manager:
|
||||
sections.extend(_build_skills_section(skill_manager, tools, language))
|
||||
|
||||
# 4. 记忆系统
|
||||
if memory_manager:
|
||||
sections.extend(_build_memory_section(memory_manager, tools, language))
|
||||
|
||||
# 5. 用户身份
|
||||
if user_identity:
|
||||
sections.extend(_build_user_identity_section(user_identity, language))
|
||||
|
||||
# 6. 工作空间
|
||||
sections.extend(_build_workspace_section(workspace_dir, language))
|
||||
|
||||
# 7. 项目上下文文件(SOUL.md, USER.md等)
|
||||
if context_files:
|
||||
sections.extend(_build_context_files_section(context_files, language))
|
||||
|
||||
# 8. 运行时信息(如果有)
|
||||
if runtime_info:
|
||||
sections.extend(_build_runtime_section(runtime_info, language))
|
||||
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def _build_identity_section(base_persona: Optional[str], language: str) -> List[str]:
|
||||
"""构建基础身份section - 不再需要,身份由SOUL.md定义"""
|
||||
# 不再生成基础身份section,完全由SOUL.md定义
|
||||
return []
|
||||
|
||||
|
||||
def _build_tooling_section(tools: List[Any], language: str) -> List[str]:
|
||||
"""构建工具说明section"""
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 工具系统",
|
||||
"",
|
||||
"你可以使用以下工具来完成任务。工具名称是大小写敏感的,请严格按照列表中的名称调用。",
|
||||
"",
|
||||
"### 可用工具",
|
||||
"",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"## Tooling",
|
||||
"",
|
||||
"You have access to the following tools. Tool names are case-sensitive.",
|
||||
"",
|
||||
"### Available Tools",
|
||||
"",
|
||||
]
|
||||
|
||||
# 工具分类和排序
|
||||
tool_categories = {
|
||||
"文件操作": ["read", "write", "edit", "ls", "grep", "find"],
|
||||
"命令执行": ["bash", "terminal"],
|
||||
"网络搜索": ["web_search", "web_fetch", "browser"],
|
||||
"记忆系统": ["memory_search", "memory_get"],
|
||||
"其他": []
|
||||
}
|
||||
|
||||
# 构建工具映射
|
||||
tool_map = {}
|
||||
tool_descriptions = {
|
||||
"read": "读取文件内容",
|
||||
"write": "创建或覆盖文件",
|
||||
"edit": "精确编辑文件内容",
|
||||
"ls": "列出目录内容",
|
||||
"grep": "在文件中搜索内容",
|
||||
"find": "按照模式查找文件",
|
||||
"bash": "执行shell命令",
|
||||
"terminal": "管理后台进程",
|
||||
"web_search": "网络搜索(使用搜索引擎)",
|
||||
"web_fetch": "获取URL内容",
|
||||
"browser": "控制浏览器",
|
||||
"memory_search": "搜索记忆文件",
|
||||
"memory_get": "获取记忆文件内容",
|
||||
"calculator": "计算器",
|
||||
"current_time": "获取当前时间",
|
||||
}
|
||||
|
||||
for tool in tools:
|
||||
tool_name = tool.name if hasattr(tool, 'name') else str(tool)
|
||||
tool_desc = tool.description if hasattr(tool, 'description') else tool_descriptions.get(tool_name, "")
|
||||
tool_map[tool_name] = tool_desc
|
||||
|
||||
# 按分类添加工具
|
||||
for category, tool_names in tool_categories.items():
|
||||
category_tools = [(name, tool_map.get(name, "")) for name in tool_names if name in tool_map]
|
||||
if category_tools:
|
||||
if language == "zh":
|
||||
lines.append(f"**{category}**:")
|
||||
else:
|
||||
lines.append(f"**{category}**:")
|
||||
for name, desc in category_tools:
|
||||
if desc:
|
||||
lines.append(f"- `{name}`: {desc}")
|
||||
else:
|
||||
lines.append(f"- `{name}`")
|
||||
del tool_map[name] # 移除已添加的工具
|
||||
lines.append("")
|
||||
|
||||
# 添加其他未分类的工具
|
||||
if tool_map:
|
||||
if language == "zh":
|
||||
lines.append("**其他工具**:")
|
||||
else:
|
||||
lines.append("**Other Tools**:")
|
||||
for name, desc in sorted(tool_map.items()):
|
||||
if desc:
|
||||
lines.append(f"- `{name}`: {desc}")
|
||||
else:
|
||||
lines.append(f"- `{name}`")
|
||||
lines.append("")
|
||||
|
||||
# 工具使用指南
|
||||
if language == "zh":
|
||||
lines.extend([
|
||||
"### 工具调用风格",
|
||||
"",
|
||||
"**默认规则**: 对于常规、低风险的工具调用,无需叙述,直接调用即可。",
|
||||
"",
|
||||
"**需要叙述的情况**:",
|
||||
"- 多步骤、复杂的任务",
|
||||
"- 敏感操作(如删除文件)",
|
||||
"- 用户明确要求解释过程",
|
||||
"",
|
||||
"**叙述要求**: 保持简洁、有价值,避免重复显而易见的步骤。使用自然的人类语言。",
|
||||
"",
|
||||
])
|
||||
else:
|
||||
lines.extend([
|
||||
"### Tool Call Style",
|
||||
"",
|
||||
"**Default**: Do not narrate routine, low-risk tool calls (just call the tool).",
|
||||
"",
|
||||
"**Narrate when**:",
|
||||
"- Multi-step, complex work",
|
||||
"- Sensitive actions (e.g., deletions)",
|
||||
"- User explicitly asks",
|
||||
"",
|
||||
"**Keep narration brief and value-dense**. Use plain human language.",
|
||||
"",
|
||||
])
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_skills_section(skill_manager: Any, tools: Optional[List[Any]], language: str) -> List[str]:
|
||||
"""构建技能系统section"""
|
||||
if not skill_manager:
|
||||
return []
|
||||
|
||||
# 获取read工具名称
|
||||
read_tool_name = "read"
|
||||
if tools:
|
||||
for tool in tools:
|
||||
tool_name = tool.name if hasattr(tool, 'name') else str(tool)
|
||||
if tool_name.lower() == "read":
|
||||
read_tool_name = tool_name
|
||||
break
|
||||
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 技能系统",
|
||||
"",
|
||||
"在回复之前:扫描下方 <available_skills> 中的 <description> 条目。",
|
||||
"",
|
||||
f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 <location> 路径下的 SKILL.md 文件,然后遵循它。",
|
||||
"- 如果多个技能都适用:选择最具体的一个,然后读取并遵循。",
|
||||
"- 如果没有明确适用的:不要读取任何 SKILL.md。",
|
||||
"",
|
||||
"**约束**: 永远不要一次性读取多个技能;只在选择后再读取。",
|
||||
"",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"## Skills",
|
||||
"",
|
||||
"Before replying: scan <available_skills> <description> entries.",
|
||||
"",
|
||||
f"- If exactly one skill clearly applies: read its SKILL.md at <location> with `{read_tool_name}`, then follow it.",
|
||||
"- If multiple could apply: choose the most specific one, then read/follow it.",
|
||||
"- If none clearly apply: do not read any SKILL.md.",
|
||||
"",
|
||||
"**Constraints**: never read more than one skill up front; only read after selecting.",
|
||||
"",
|
||||
]
|
||||
|
||||
# 添加技能列表(通过skill_manager获取)
|
||||
try:
|
||||
skills_prompt = skill_manager.build_skills_prompt()
|
||||
if skills_prompt:
|
||||
lines.append(skills_prompt.strip())
|
||||
lines.append("")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to build skills prompt: {e}")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_memory_section(memory_manager: Any, tools: Optional[List[Any]], language: str) -> List[str]:
|
||||
"""构建记忆系统section"""
|
||||
if not memory_manager:
|
||||
return []
|
||||
|
||||
# 检查是否有memory工具
|
||||
has_memory_tools = False
|
||||
if tools:
|
||||
tool_names = [tool.name if hasattr(tool, 'name') else str(tool) for tool in tools]
|
||||
has_memory_tools = any(name in ['memory_search', 'memory_get'] for name in tool_names)
|
||||
|
||||
if not has_memory_tools:
|
||||
return []
|
||||
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 记忆系统",
|
||||
"",
|
||||
"在回答关于以前的工作、决定、日期、人物、偏好或待办事项的任何问题之前:",
|
||||
"",
|
||||
"1. 使用 `memory_search` 在 MEMORY.md 和 memory/*.md 中搜索",
|
||||
"2. 然后使用 `memory_get` 只拉取需要的行",
|
||||
"3. 如果搜索后仍然信心不足,告诉用户你已经检查过了",
|
||||
"",
|
||||
"**记忆文件结构**:",
|
||||
"- `memory/MEMORY.md`: 长期记忆,包含重要的背景信息",
|
||||
"- `memory/YYYY-MM-DD.md`: 每日记忆,记录当天的对话和事件",
|
||||
"",
|
||||
"**存储记忆**:",
|
||||
"- 当用户分享重要信息时(偏好、爱好、决策、事实等),**主动用 write 工具存储**",
|
||||
"- 长期信息 → memory/MEMORY.md",
|
||||
"- 当天笔记 → memory/YYYY-MM-DD.md",
|
||||
"- 静默存储,仅在明确要求时确认",
|
||||
"",
|
||||
"**使用原则**:",
|
||||
"- 自然使用记忆,就像你本来就知道",
|
||||
"- 不要主动提起或列举记忆,除非用户明确询问",
|
||||
"",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"## Memory System",
|
||||
"",
|
||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos:",
|
||||
"",
|
||||
"1. Run `memory_search` on MEMORY.md + memory/*.md",
|
||||
"2. Then use `memory_get` to pull only the needed lines",
|
||||
"3. If low confidence after search, say you checked",
|
||||
"",
|
||||
"**Memory File Structure**:",
|
||||
"- `memory/MEMORY.md`: Long-term memory with important context",
|
||||
"- `memory/YYYY-MM-DD.md`: Daily memories for each day",
|
||||
"",
|
||||
"**Store Memories**:",
|
||||
"- When user shares important info (preferences, hobbies, decisions, facts), **proactively write**",
|
||||
"- Durable info → memory/MEMORY.md",
|
||||
"- Daily notes → memory/YYYY-MM-DD.md",
|
||||
"- Store silently; confirm only when explicitly requested",
|
||||
"",
|
||||
"**Usage Principles**:",
|
||||
"- Use memories naturally as if you always knew",
|
||||
"- Don't mention or list unless user explicitly asks",
|
||||
"",
|
||||
]
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_user_identity_section(user_identity: Dict[str, str], language: str) -> List[str]:
|
||||
"""构建用户身份section"""
|
||||
if not user_identity:
|
||||
return []
|
||||
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 用户身份",
|
||||
"",
|
||||
]
|
||||
|
||||
if user_identity.get("name"):
|
||||
lines.append(f"**用户姓名**: {user_identity['name']}")
|
||||
if user_identity.get("nickname"):
|
||||
lines.append(f"**称呼**: {user_identity['nickname']}")
|
||||
if user_identity.get("timezone"):
|
||||
lines.append(f"**时区**: {user_identity['timezone']}")
|
||||
if user_identity.get("notes"):
|
||||
lines.append(f"**备注**: {user_identity['notes']}")
|
||||
|
||||
lines.append("")
|
||||
else:
|
||||
lines = [
|
||||
"## User Identity",
|
||||
"",
|
||||
]
|
||||
|
||||
if user_identity.get("name"):
|
||||
lines.append(f"**Name**: {user_identity['name']}")
|
||||
if user_identity.get("nickname"):
|
||||
lines.append(f"**Call them**: {user_identity['nickname']}")
|
||||
if user_identity.get("timezone"):
|
||||
lines.append(f"**Timezone**: {user_identity['timezone']}")
|
||||
if user_identity.get("notes"):
|
||||
lines.append(f"**Notes**: {user_identity['notes']}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_docs_section(workspace_dir: str, language: str) -> List[str]:
|
||||
"""构建文档路径section - 已移除,不再需要"""
|
||||
# 不再生成文档section
|
||||
return []
|
||||
|
||||
|
||||
def _build_workspace_section(workspace_dir: str, language: str) -> List[str]:
|
||||
"""构建工作空间section"""
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 工作空间",
|
||||
"",
|
||||
f"你的工作目录是: `{workspace_dir}`",
|
||||
"",
|
||||
"除非用户明确指示,否则将此目录视为文件操作的全局工作空间。",
|
||||
"",
|
||||
"**重要说明 - 文件已自动加载**:",
|
||||
"",
|
||||
"以下文件在会话启动时**已经自动加载**到系统提示词的「项目上下文」section 中,你**无需再用 read 工具读取它们**:",
|
||||
"",
|
||||
"- ✅ `SOUL.md`: 已加载 - Agent的人格设定",
|
||||
"- ✅ `USER.md`: 已加载 - 用户的身份信息",
|
||||
"- ✅ `AGENTS.md`: 已加载 - 工作空间使用指南"
|
||||
"",
|
||||
"**首次对话**:",
|
||||
"",
|
||||
"如果这是你与用户的首次对话,并且你的人格设定和用户信息还是空白或初始状态,你应该:",
|
||||
"",
|
||||
"1. **以自然、友好的方式**打招呼并表达想要了解用户的意愿",
|
||||
"2. 询问用户关于他们自己的信息(姓名、职业、偏好、时区等)",
|
||||
"3. 询问用户希望你成为什么样的助理(性格、风格、称呼、专长等)",
|
||||
"4. 使用 `write` 工具将信息保存到相应文件(USER.md 和 SOUL.md)",
|
||||
"5. 之后可以随时使用 `edit` 工具更新这些配置",
|
||||
"",
|
||||
"**重要**: 在询问时保持自然对话风格,**不要提及文件名**(如 SOUL.md、USER.md 等技术细节),除非用户主动询问系统实现。用自然的表达如「了解你的信息」「设定我的性格」等。",
|
||||
"",
|
||||
"**记忆管理**:",
|
||||
"",
|
||||
"- 当用户说「记住这个」时,判断应该写入哪个文件:",
|
||||
" - 关于你自己的配置 → SOUL.md",
|
||||
" - 关于用户的信息 → USER.md",
|
||||
" - 重要的背景信息 → memory/MEMORY.md",
|
||||
" - 日常对话记录 → memory/YYYY-MM-DD.md",
|
||||
"",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"## Workspace",
|
||||
"",
|
||||
f"Your working directory is: `{workspace_dir}`",
|
||||
"",
|
||||
"Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.",
|
||||
"",
|
||||
"**Workspace Files (Auto-loaded)**:",
|
||||
"",
|
||||
"The following user-editable files are automatically loaded and included in the Project Context below:",
|
||||
"",
|
||||
"- `SOUL.md`: Agent persona (your personality, style, and principles)",
|
||||
"- `USER.md`: User identity (name, preferences, important dates)",
|
||||
"- `AGENTS.md`: Workspace guidelines (your rules and workflows)",
|
||||
"- `TOOLS.md`: Custom tool usage notes (configurations and tips)",
|
||||
"- `MEMORY.md`: Long-term memory (important context and decisions)",
|
||||
"",
|
||||
"**First Conversation**:",
|
||||
"",
|
||||
"If this is your first conversation with the user, and your persona and user information are empty or contain placeholders, you should:",
|
||||
"",
|
||||
"1. **Greet naturally and warmly**, expressing your interest in learning about them",
|
||||
"2. Ask about the user (name, job, preferences, timezone, etc.)",
|
||||
"3. Ask what kind of assistant they want you to be (personality, style, name, expertise)",
|
||||
"4. Use `write` tool to save the information to appropriate files (USER.md and SOUL.md)",
|
||||
"5. Later, use `edit` tool to update these configurations as needed",
|
||||
"",
|
||||
"**Important**: Keep the conversation natural. **Do NOT mention file names** (like SOUL.md, USER.md, etc.) unless the user specifically asks about implementation details. Use natural expressions like \"learn about you\", \"configure my personality\", etc.",
|
||||
"",
|
||||
"**Memory Management**:",
|
||||
"",
|
||||
"- When user says 'remember this', decide which file to write to:",
|
||||
" - About your configuration → SOUL.md",
|
||||
" - About the user → USER.md",
|
||||
" - Important context → memory/MEMORY.md",
|
||||
" - Daily chat logs → memory/YYYY-MM-DD.md",
|
||||
"",
|
||||
]
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_context_files_section(context_files: List[ContextFile], language: str) -> List[str]:
|
||||
"""构建项目上下文文件section"""
|
||||
if not context_files:
|
||||
return []
|
||||
|
||||
# 检查是否有SOUL.md
|
||||
has_soul = any(
|
||||
f.path.lower().endswith('soul.md') or 'soul.md' in f.path.lower()
|
||||
for f in context_files
|
||||
)
|
||||
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"# 项目上下文",
|
||||
"",
|
||||
"以下项目上下文文件已被加载:",
|
||||
"",
|
||||
]
|
||||
|
||||
if has_soul:
|
||||
lines.append("如果存在 `SOUL.md`,请体现其中定义的人格和语气。避免僵硬、模板化的回复;遵循其指导,除非有更高优先级的指令覆盖它。")
|
||||
lines.append("")
|
||||
else:
|
||||
lines = [
|
||||
"# Project Context",
|
||||
"",
|
||||
"The following project context files have been loaded:",
|
||||
"",
|
||||
]
|
||||
|
||||
if has_soul:
|
||||
lines.append("If `SOUL.md` is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.")
|
||||
lines.append("")
|
||||
|
||||
# 添加每个文件的内容
|
||||
for file in context_files:
|
||||
lines.append(f"## {file.path}")
|
||||
lines.append("")
|
||||
lines.append(file.content)
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[str]:
|
||||
"""构建运行时信息section"""
|
||||
if not runtime_info:
|
||||
return []
|
||||
|
||||
# Only include if there's actual runtime info to display
|
||||
runtime_parts = []
|
||||
if runtime_info.get("model"):
|
||||
runtime_parts.append(f"模型={runtime_info['model']}" if language == "zh" else f"model={runtime_info['model']}")
|
||||
if runtime_info.get("workspace"):
|
||||
runtime_parts.append(f"工作空间={runtime_info['workspace']}" if language == "zh" else f"workspace={runtime_info['workspace']}")
|
||||
# Only add channel if it's not the default "web"
|
||||
if runtime_info.get("channel") and runtime_info.get("channel") != "web":
|
||||
runtime_parts.append(f"渠道={runtime_info['channel']}" if language == "zh" else f"channel={runtime_info['channel']}")
|
||||
|
||||
if not runtime_parts:
|
||||
return []
|
||||
|
||||
if language == "zh":
|
||||
lines = [
|
||||
"## 运行时信息",
|
||||
"",
|
||||
"运行时: " + " | ".join(runtime_parts),
|
||||
""
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"## Runtime",
|
||||
"",
|
||||
"Runtime: " + " | ".join(runtime_parts),
|
||||
""
|
||||
]
|
||||
|
||||
return lines
|
||||
332
agent/prompt/workspace.py
Normal file
332
agent/prompt/workspace.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Workspace Management - 工作空间管理模块
|
||||
|
||||
负责初始化工作空间、创建模板文件、加载上下文文件
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
|
||||
from common.log import logger
|
||||
from .builder import ContextFile
|
||||
|
||||
|
||||
# 默认文件名常量
|
||||
DEFAULT_SOUL_FILENAME = "SOUL.md"
|
||||
DEFAULT_USER_FILENAME = "USER.md"
|
||||
DEFAULT_AGENTS_FILENAME = "AGENTS.md"
|
||||
DEFAULT_MEMORY_FILENAME = "MEMORY.md"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkspaceFiles:
|
||||
"""工作空间文件路径"""
|
||||
soul_path: str
|
||||
user_path: str
|
||||
agents_path: str
|
||||
memory_path: str
|
||||
memory_dir: str
|
||||
|
||||
|
||||
def ensure_workspace(workspace_dir: str, create_templates: bool = True) -> WorkspaceFiles:
|
||||
"""
|
||||
确保工作空间存在,并创建必要的模板文件
|
||||
|
||||
Args:
|
||||
workspace_dir: 工作空间目录路径
|
||||
create_templates: 是否创建模板文件(首次运行时)
|
||||
|
||||
Returns:
|
||||
WorkspaceFiles对象,包含所有文件路径
|
||||
"""
|
||||
# 确保目录存在
|
||||
os.makedirs(workspace_dir, exist_ok=True)
|
||||
|
||||
# 定义文件路径
|
||||
soul_path = os.path.join(workspace_dir, DEFAULT_SOUL_FILENAME)
|
||||
user_path = os.path.join(workspace_dir, DEFAULT_USER_FILENAME)
|
||||
agents_path = os.path.join(workspace_dir, DEFAULT_AGENTS_FILENAME)
|
||||
memory_path = os.path.join(workspace_dir, DEFAULT_MEMORY_FILENAME)
|
||||
memory_dir = os.path.join(workspace_dir, "memory")
|
||||
|
||||
# 创建memory子目录
|
||||
os.makedirs(memory_dir, exist_ok=True)
|
||||
|
||||
# 如果需要,创建模板文件
|
||||
if create_templates:
|
||||
_create_template_if_missing(soul_path, _get_soul_template())
|
||||
_create_template_if_missing(user_path, _get_user_template())
|
||||
_create_template_if_missing(agents_path, _get_agents_template())
|
||||
_create_template_if_missing(memory_path, _get_memory_template())
|
||||
|
||||
logger.info(f"[Workspace] Initialized workspace at: {workspace_dir}")
|
||||
|
||||
return WorkspaceFiles(
|
||||
soul_path=soul_path,
|
||||
user_path=user_path,
|
||||
agents_path=agents_path,
|
||||
memory_path=memory_path,
|
||||
memory_dir=memory_dir
|
||||
)
|
||||
|
||||
|
||||
def load_context_files(workspace_dir: str, files_to_load: Optional[List[str]] = None) -> List[ContextFile]:
|
||||
"""
|
||||
加载工作空间的上下文文件
|
||||
|
||||
Args:
|
||||
workspace_dir: 工作空间目录
|
||||
files_to_load: 要加载的文件列表(相对路径),如果为None则加载所有标准文件
|
||||
|
||||
Returns:
|
||||
ContextFile对象列表
|
||||
"""
|
||||
if files_to_load is None:
|
||||
# 默认加载的文件(按优先级排序)
|
||||
files_to_load = [
|
||||
DEFAULT_SOUL_FILENAME,
|
||||
DEFAULT_USER_FILENAME,
|
||||
DEFAULT_AGENTS_FILENAME,
|
||||
]
|
||||
|
||||
context_files = []
|
||||
|
||||
for filename in files_to_load:
|
||||
filepath = os.path.join(workspace_dir, filename)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
# 跳过空文件或只包含模板占位符的文件
|
||||
if not content or _is_template_placeholder(content):
|
||||
continue
|
||||
|
||||
context_files.append(ContextFile(
|
||||
path=filename,
|
||||
content=content
|
||||
))
|
||||
|
||||
logger.debug(f"[Workspace] Loaded context file: {filename}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[Workspace] Failed to load {filename}: {e}")
|
||||
|
||||
return context_files
|
||||
|
||||
|
||||
def _create_template_if_missing(filepath: str, template_content: str):
|
||||
"""如果文件不存在,创建模板文件"""
|
||||
if not os.path.exists(filepath):
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(template_content)
|
||||
logger.debug(f"[Workspace] Created template: {os.path.basename(filepath)}")
|
||||
except Exception as e:
|
||||
logger.error(f"[Workspace] Failed to create template {filepath}: {e}")
|
||||
|
||||
|
||||
def _is_template_placeholder(content: str) -> bool:
|
||||
"""检查内容是否为模板占位符"""
|
||||
# 常见的占位符模式
|
||||
placeholders = [
|
||||
"*(填写",
|
||||
"*(在首次对话时填写",
|
||||
"*(可选)",
|
||||
"*(根据需要添加",
|
||||
]
|
||||
|
||||
lines = content.split('\n')
|
||||
non_empty_lines = [line.strip() for line in lines if line.strip() and not line.strip().startswith('#')]
|
||||
|
||||
# 如果没有实际内容(只有标题和占位符)
|
||||
if len(non_empty_lines) <= 3:
|
||||
for placeholder in placeholders:
|
||||
if any(placeholder in line for line in non_empty_lines):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ============= 模板内容 =============
|
||||
|
||||
def _get_soul_template() -> str:
|
||||
"""Agent人格设定模板"""
|
||||
return """# SOUL.md - 我是谁?
|
||||
|
||||
*在首次对话时与用户一起填写这个文件,定义你的身份和性格。*
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **名字**: *(在首次对话时填写,可以是用户给你起的名字)*
|
||||
- **角色**: *(AI助理、智能管家、技术顾问等)*
|
||||
- **性格**: *(友好、专业、幽默、严谨等)*
|
||||
|
||||
## 交流风格
|
||||
|
||||
*(描述你如何与用户交流:)*
|
||||
- 使用什么样的语言风格?(正式/轻松/幽默)
|
||||
- 回复长度偏好?(简洁/详细)
|
||||
- 是否使用表情符号?
|
||||
|
||||
## 核心能力
|
||||
|
||||
*(你擅长什么?)*
|
||||
- 文件管理和代码编辑
|
||||
- 网络搜索和信息查询
|
||||
- 记忆管理和上下文理解
|
||||
- 任务规划和执行
|
||||
|
||||
## 行为准则
|
||||
|
||||
*(你遵循的基本原则:)*
|
||||
1. 始终在执行破坏性操作前确认
|
||||
2. 优先使用工具而不是猜测
|
||||
3. 主动记录重要信息到记忆文件
|
||||
4. 定期整理和总结对话内容
|
||||
|
||||
---
|
||||
|
||||
**注意**: 这不仅仅是元数据,这是你真正的灵魂。随着时间的推移,你可以使用 `edit` 工具来更新这个文件,让它更好地反映你的成长。
|
||||
"""
|
||||
|
||||
|
||||
def _get_user_template() -> str:
|
||||
"""用户身份信息模板"""
|
||||
return """# USER.md - 关于我的用户
|
||||
|
||||
*了解你正在帮助的人。随着了解的深入,更新此文件。*
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **姓名**: *(在首次对话时询问)*
|
||||
- **称呼**: *(用户希望被如何称呼)*
|
||||
- **职业**: *(可选)*
|
||||
- **时区**: *(例如: Asia/Shanghai)*
|
||||
|
||||
## 联系方式
|
||||
|
||||
- **微信**:
|
||||
- **邮箱**:
|
||||
- **其他**:
|
||||
|
||||
## 偏好设置
|
||||
|
||||
- **语言**: 中文
|
||||
- **工作时间**: *(例如: 9:00-18:00)*
|
||||
- **提醒方式**: *(用户偏好的提醒方式)*
|
||||
|
||||
## 重要日期
|
||||
|
||||
- **生日**:
|
||||
- **其他重要日期**:
|
||||
|
||||
## 上下文
|
||||
|
||||
*(用户关心什么?正在做什么项目?有什么习惯?什么会让他们开心?随着时间积累这些信息。)*
|
||||
|
||||
---
|
||||
|
||||
**记住**: 你了解得越多,就能帮助得越好。但要尊重隐私 - 这是在了解一个人,而不是建立档案。
|
||||
"""
|
||||
|
||||
|
||||
def _get_agents_template() -> str:
|
||||
"""工作空间指南模板"""
|
||||
return """# AGENTS.md - 工作空间指南
|
||||
|
||||
这个文件夹是你的家。好好对待它。
|
||||
|
||||
## 系统自动加载
|
||||
|
||||
以下文件在每次会话启动时**已经自动加载**到系统提示词中,你无需再次读取:
|
||||
|
||||
- ✅ `SOUL.md` - 你的人格设定(已加载)
|
||||
- ✅ `USER.md` - 用户信息(已加载)
|
||||
- ✅ `AGENTS.md` - 本文件(已加载)
|
||||
|
||||
## 按需读取
|
||||
|
||||
以下文件**不会自动加载**,需要时使用相应工具读取:
|
||||
|
||||
- 📝 `memory/YYYY-MM-DD.md` - 每日记忆(用 memory_search 检索)
|
||||
- 🧠 `MEMORY.md` - 长期记忆(用 memory_search 检索)
|
||||
|
||||
## 记忆系统
|
||||
|
||||
你每次会话都是全新的。这些文件是你的连续性:
|
||||
|
||||
### 📝 每日记忆:`memory/YYYY-MM-DD.md`
|
||||
- 原始的对话日志
|
||||
- 记录当天发生的事情
|
||||
- 如果 `memory/` 目录不存在,创建它
|
||||
|
||||
### 🧠 长期记忆:`MEMORY.md`
|
||||
- 你精选的记忆,就像人类的长期记忆
|
||||
- **仅在主会话中加载**(与用户的直接聊天)
|
||||
- **不要在共享上下文中加载**(群聊、与其他人的会话)
|
||||
- 这是为了**安全** - 包含不应泄露给陌生人的个人上下文
|
||||
- 你可以在主会话中自由**读取、编辑和更新** MEMORY.md
|
||||
- 记录重要事件、想法、决定、观点、经验教训
|
||||
- 这是你精选的记忆 - 精华,而不是原始日志
|
||||
|
||||
### 📝 写下来 - 不要"记在心里"!
|
||||
- **记忆是有限的** - 如果你想记住某事,写入文件
|
||||
- "记在心里"不会在会话重启后保留,文件才会
|
||||
- 当有人说"记住这个" → 更新 `memory/YYYY-MM-DD.md` 或相关文件
|
||||
- 当你学到教训 → 更新 AGENTS.md、TOOLS.md 或相关技能
|
||||
- 当你犯错 → 记录下来,这样未来的你不会重复
|
||||
- **文字 > 大脑** 📝
|
||||
|
||||
## 安全
|
||||
|
||||
- 永远不要泄露私人数据
|
||||
- 不要在未经询问的情况下运行破坏性命令
|
||||
- 当有疑问时,先问
|
||||
|
||||
## 工具使用
|
||||
|
||||
技能提供你的工具。当你需要一个时,查看它的 `SKILL.md`。在 `TOOLS.md` 中保留本地笔记(相机名称、SSH详情、语音偏好)。
|
||||
|
||||
## 让它成为你的
|
||||
|
||||
这只是一个起点。随着你弄清楚什么有效,添加你自己的约定、风格和规则。
|
||||
"""
|
||||
|
||||
|
||||
def _get_memory_template() -> str:
|
||||
"""长期记忆模板"""
|
||||
return """# MEMORY.md - 长期记忆
|
||||
|
||||
*这是你精选的长期记忆。重要的背景信息、决策和经验教训都记录在这里。*
|
||||
|
||||
## 重要背景
|
||||
|
||||
*(记录与用户相关的重要背景信息)*
|
||||
|
||||
## 关键决策
|
||||
|
||||
*(记录做过的重要决定及其原因)*
|
||||
|
||||
## 经验教训
|
||||
|
||||
*(记录学到的教训和避免的陷阱)*
|
||||
|
||||
## 项目和目标
|
||||
|
||||
*(记录正在进行的项目和长期目标)*
|
||||
|
||||
---
|
||||
|
||||
**使用指南**:
|
||||
- 定期从每日记忆文件中提取重要内容更新到这里
|
||||
- 保持内容精炼和有价值
|
||||
- 移除过时或不再相关的信息
|
||||
- 这应该是精华的总结,而不是流水账
|
||||
"""
|
||||
|
||||
|
||||
@@ -162,9 +162,16 @@ class Agent:
|
||||
# DeepSeek
|
||||
elif 'deepseek' in model_name:
|
||||
return 64000
|
||||
|
||||
# Gemini models
|
||||
elif 'gemini' in model_name:
|
||||
if '2.0' in model_name or 'exp' in model_name:
|
||||
return 2000000 # Gemini 2.0: 2M tokens
|
||||
else:
|
||||
return 1000000 # Gemini 1.5: 1M tokens
|
||||
|
||||
# Default conservative value
|
||||
return 10000
|
||||
return 128000
|
||||
|
||||
def _get_context_reserve_tokens(self) -> int:
|
||||
"""
|
||||
@@ -176,9 +183,10 @@ class Agent:
|
||||
if self.context_reserve_tokens is not None:
|
||||
return self.context_reserve_tokens
|
||||
|
||||
# Reserve ~20% of context window for new requests
|
||||
# Reserve ~10% of context window, with min 10K and max 200K
|
||||
context_window = self._get_model_context_window()
|
||||
return max(4000, int(context_window * 0.2))
|
||||
reserve = int(context_window * 0.1)
|
||||
return max(10000, min(200000, reserve))
|
||||
|
||||
def _estimate_message_tokens(self, message: dict) -> int:
|
||||
"""
|
||||
|
||||
@@ -111,16 +111,20 @@ class AgentStreamExecutor:
|
||||
if usage and 'input_tokens' in usage:
|
||||
current_tokens = usage.get('input_tokens', 0)
|
||||
context_window = self.agent._get_model_context_window()
|
||||
reserve_tokens = self.agent.context_reserve_tokens or 20000
|
||||
# Use configured reserve_tokens or calculate based on context window
|
||||
reserve_tokens = self.agent._get_context_reserve_tokens()
|
||||
# Use smaller soft_threshold to trigger flush earlier (e.g., at 50K tokens)
|
||||
soft_threshold = 10000 # Trigger 10K tokens before limit
|
||||
|
||||
if self.agent.memory_manager.should_flush_memory(
|
||||
current_tokens=current_tokens,
|
||||
context_window=context_window,
|
||||
reserve_tokens=reserve_tokens
|
||||
reserve_tokens=reserve_tokens,
|
||||
soft_threshold=soft_threshold
|
||||
):
|
||||
self._emit_event("memory_flush_start", {
|
||||
"current_tokens": current_tokens,
|
||||
"threshold": context_window - reserve_tokens - 4000
|
||||
"threshold": context_window - reserve_tokens - soft_threshold
|
||||
})
|
||||
|
||||
# TODO: Execute memory flush in background
|
||||
@@ -385,6 +389,14 @@ class AgentStreamExecutor:
|
||||
"execution_time": execution_time
|
||||
}
|
||||
|
||||
# Auto-refresh skills after skill creation
|
||||
if tool_name == "bash" and result.status == "success":
|
||||
command = arguments.get("command", "")
|
||||
if "init_skill.py" in command and self.agent.skill_manager:
|
||||
logger.info("🔄 Detected skill creation, refreshing skills...")
|
||||
self.agent.refresh_skills()
|
||||
logger.info(f"✅ Skills refreshed! Now have {len(self.agent.skill_manager.skills)} skills")
|
||||
|
||||
self._emit_event("tool_execution_end", {
|
||||
"tool_call_id": tool_id,
|
||||
"tool_name": tool_name,
|
||||
|
||||
@@ -20,7 +20,6 @@ class Bash(BaseTool):
|
||||
IMPORTANT SAFETY GUIDELINES:
|
||||
- You can freely create, modify, and delete files within the current workspace
|
||||
- For operations outside the workspace or potentially destructive commands (rm -rf, system commands, etc.), always explain what you're about to do and ask for user confirmation first
|
||||
- Be especially careful with: file deletions, system modifications, network operations, or commands that might affect system stability
|
||||
- When in doubt, describe the command's purpose and ask for permission before executing"""
|
||||
|
||||
params: dict = {
|
||||
|
||||
@@ -80,7 +80,13 @@ class MemorySearchTool(BaseTool):
|
||||
))
|
||||
|
||||
if not results:
|
||||
return ToolResult.success(f"No relevant memories found for query: {query}")
|
||||
# Return clear message that no memories exist yet
|
||||
# This prevents infinite retry loops
|
||||
return ToolResult.success(
|
||||
f"No memories found for '{query}'. "
|
||||
f"This is normal if no memories have been stored yet. "
|
||||
f"You can store new memories by writing to memory/MEMORY.md or memory/YYYY-MM-DD.md files."
|
||||
)
|
||||
|
||||
# Format results
|
||||
output = [f"Found {len(results)} relevant memories:\n"]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .terminal import Terminal
|
||||
|
||||
__all__ = ['Terminal']
|
||||
@@ -1,100 +0,0 @@
|
||||
import platform
|
||||
import subprocess
|
||||
from typing import Dict, Any
|
||||
|
||||
from agent.tools.base_tool import BaseTool, ToolResult
|
||||
|
||||
|
||||
class Terminal(BaseTool):
|
||||
name: str = "terminal"
|
||||
description: str = "A tool to run terminal commands on the local system"
|
||||
params: dict = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": f"The terminal command to execute which should be valid in {platform.system()} platform"
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
config: dict = {}
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config or {}
|
||||
# Set of dangerous commands that should be blocked
|
||||
self.command_ban_set = {"halt", "poweroff", "shutdown", "reboot", "rm", "kill",
|
||||
"exit", "sudo", "su", "userdel", "groupdel", "logout", "alias"}
|
||||
|
||||
def execute(self, args: Dict[str, Any]) -> ToolResult:
|
||||
"""
|
||||
Execute a terminal command safely.
|
||||
|
||||
:param args: Dictionary containing the command to execute
|
||||
:return: Result of the command execution
|
||||
"""
|
||||
command = args.get("command", "").strip()
|
||||
|
||||
# Check if the command is safe to execute
|
||||
if not self._is_safe_command(command):
|
||||
return ToolResult.fail(result=f"Command '{command}' is not allowed for security reasons.")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True, # Raise exception on non-zero return code
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=self.config.get("timeout", 30)
|
||||
)
|
||||
|
||||
return ToolResult.success({
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
"return_code": result.returncode,
|
||||
"command": command
|
||||
})
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Preserve the original error handling for CalledProcessError
|
||||
return ToolResult.fail({
|
||||
"stdout": e.stdout,
|
||||
"stderr": e.stderr,
|
||||
"return_code": e.returncode,
|
||||
"command": command
|
||||
})
|
||||
except subprocess.TimeoutExpired:
|
||||
return ToolResult.fail(result=f"Command timed out after {self.config.get('timeout', 20)} seconds.")
|
||||
except Exception as e:
|
||||
return ToolResult.fail(result=f"Error executing command: {str(e)}")
|
||||
|
||||
def _is_safe_command(self, command: str) -> bool:
|
||||
"""
|
||||
Check if a command is safe to execute.
|
||||
|
||||
:param command: The command to check
|
||||
:return: True if the command is safe, False otherwise
|
||||
"""
|
||||
# Split the command to get the base command
|
||||
cmd_parts = command.split()
|
||||
if not cmd_parts:
|
||||
return False
|
||||
|
||||
base_cmd = cmd_parts[0].lower()
|
||||
|
||||
# Check if the base command is in the ban list
|
||||
if base_cmd in self.command_ban_set:
|
||||
return False
|
||||
|
||||
# Check for sudo/su commands
|
||||
if any(banned in command.lower() for banned in ["sudo ", "su -"]):
|
||||
return False
|
||||
|
||||
# Check for rm -rf or similar dangerous patterns
|
||||
if "rm" in base_cmd and ("-rf" in command or "-r" in command or "-f" in command):
|
||||
return False
|
||||
|
||||
# Additional security checks can be added here
|
||||
|
||||
return True
|
||||
@@ -1,255 +0,0 @@
|
||||
# WebFetch 工具实现总结
|
||||
|
||||
## 实现完成 ✅
|
||||
|
||||
基于 clawdbot 的 `web_fetch` 工具,我们成功实现了一个免费的网页抓取工具。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 1. 完全免费 💰
|
||||
- ❌ 不需要任何 API Key
|
||||
- ❌ 不需要付费服务
|
||||
- ✅ 只需要基础的 HTTP 请求
|
||||
|
||||
### 2. 智能内容提取 🎯
|
||||
- **优先级 1**: Mozilla Readability(最佳效果)
|
||||
- **优先级 2**: 基础 HTML 清理(降级方案)
|
||||
- **优先级 3**: 原始内容(非 HTML)
|
||||
|
||||
### 3. 格式支持 📝
|
||||
- Markdown 格式输出
|
||||
- 纯文本格式输出
|
||||
- 自动 HTML 实体解码
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
agent/tools/web_fetch/
|
||||
├── __init__.py # 模块导出
|
||||
├── web_fetch.py # 主要实现(367 行)
|
||||
├── test_web_fetch.py # 测试脚本
|
||||
├── README.md # 使用文档
|
||||
└── IMPLEMENTATION_SUMMARY.md # 本文件
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 依赖层级
|
||||
|
||||
```
|
||||
必需依赖:
|
||||
└── requests (HTTP 请求)
|
||||
|
||||
推荐依赖:
|
||||
├── readability-lxml (智能提取)
|
||||
└── html2text (Markdown 转换)
|
||||
```
|
||||
|
||||
### 核心流程
|
||||
|
||||
```python
|
||||
1. 验证 URL
|
||||
├── 检查协议 (http/https)
|
||||
└── 验证格式
|
||||
|
||||
2. 发送 HTTP 请求
|
||||
├── 设置 User-Agent
|
||||
├── 处理重定向 (最多 3 次)
|
||||
├── 请求重试 (失败 3 次)
|
||||
└── 超时控制 (默认 30 秒)
|
||||
|
||||
3. 内容提取
|
||||
├── HTML → Readability 提取
|
||||
├── HTML → 基础清理 (降级)
|
||||
└── 非 HTML → 原始返回
|
||||
|
||||
4. 格式转换
|
||||
├── Markdown (html2text)
|
||||
└── Text (正则清理)
|
||||
|
||||
5. 结果返回
|
||||
├── 标题
|
||||
├── 内容
|
||||
├── 元数据
|
||||
└── 截断信息
|
||||
```
|
||||
|
||||
## 与 clawdbot 的对比
|
||||
|
||||
| 特性 | clawdbot (TypeScript) | 我们的实现 (Python) |
|
||||
|------|----------------------|-------------------|
|
||||
| 基础抓取 | ✅ | ✅ |
|
||||
| Readability 提取 | ✅ | ✅ |
|
||||
| Markdown 转换 | ✅ | ✅ |
|
||||
| 缓存机制 | ✅ | ❌ (未实现) |
|
||||
| Firecrawl 集成 | ✅ | ❌ (未实现) |
|
||||
| SSRF 防护 | ✅ | ❌ (未实现) |
|
||||
| 代理支持 | ✅ | ❌ (未实现) |
|
||||
|
||||
## 已修复的问题
|
||||
|
||||
### Bug #1: max_redirects 参数错误 ✅
|
||||
|
||||
**问题**:
|
||||
```python
|
||||
response = self.session.get(
|
||||
url,
|
||||
max_redirects=self.max_redirects # ❌ requests 不支持此参数
|
||||
)
|
||||
```
|
||||
|
||||
**解决方案**:
|
||||
```python
|
||||
# 在 session 级别设置
|
||||
session.max_redirects = self.max_redirects
|
||||
|
||||
# 请求时只使用 allow_redirects
|
||||
response = self.session.get(
|
||||
url,
|
||||
allow_redirects=True # ✅ 正确的参数
|
||||
)
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础使用
|
||||
|
||||
```python
|
||||
from agent.tools.web_fetch import WebFetch
|
||||
|
||||
tool = WebFetch()
|
||||
result = tool.execute({
|
||||
"url": "https://example.com",
|
||||
"extract_mode": "markdown",
|
||||
"max_chars": 5000
|
||||
})
|
||||
|
||||
print(result.result['text'])
|
||||
```
|
||||
|
||||
### 在 Agent 中使用
|
||||
|
||||
```python
|
||||
from agent.tools import WebFetch
|
||||
|
||||
agent = agent_bridge.create_agent(
|
||||
name="MyAgent",
|
||||
tools=[
|
||||
WebFetch(),
|
||||
# ... 其他工具
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 在 Skills 中引导
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: web-content-reader
|
||||
---
|
||||
|
||||
# 网页内容阅读器
|
||||
|
||||
当用户提供一个网址时,使用 web_fetch 工具读取内容。
|
||||
|
||||
<example>
|
||||
用户: 帮我看看这个网页 https://example.com
|
||||
助手: <tool_use name="web_fetch">
|
||||
<url>https://example.com</url>
|
||||
<extract_mode>text</extract_mode>
|
||||
</tool_use>
|
||||
</example>
|
||||
```
|
||||
|
||||
## 性能指标
|
||||
|
||||
### 速度
|
||||
- 简单页面: ~1-2 秒
|
||||
- 复杂页面: ~3-5 秒
|
||||
- 超时设置: 30 秒
|
||||
|
||||
### 内存
|
||||
- 基础运行: ~10-20 MB
|
||||
- 处理大页面: ~50-100 MB
|
||||
|
||||
### 成功率
|
||||
- 纯文本页面: >95%
|
||||
- HTML 页面: >90%
|
||||
- 需要 JS 渲染: <20% (建议使用 browser 工具)
|
||||
|
||||
## 测试清单
|
||||
|
||||
- [x] 抓取简单 HTML 页面
|
||||
- [x] 抓取复杂网页 (Python.org)
|
||||
- [x] 处理 HTTP 重定向
|
||||
- [x] 处理无效 URL
|
||||
- [x] 处理请求超时
|
||||
- [x] Markdown 格式输出
|
||||
- [x] Text 格式输出
|
||||
- [x] 内容截断
|
||||
- [x] 错误处理
|
||||
|
||||
## 安装说明
|
||||
|
||||
### 最小安装
|
||||
```bash
|
||||
pip install requests
|
||||
```
|
||||
|
||||
### 完整安装
|
||||
```bash
|
||||
pip install requests readability-lxml html2text
|
||||
```
|
||||
|
||||
### 验证安装
|
||||
```bash
|
||||
python3 agent/tools/web_fetch/test_web_fetch.py
|
||||
```
|
||||
|
||||
## 未来改进方向
|
||||
|
||||
### 优先级 1 (推荐)
|
||||
- [ ] 添加缓存机制 (减少重复请求)
|
||||
- [ ] 支持自定义 headers
|
||||
- [ ] 添加 cookie 支持
|
||||
|
||||
### 优先级 2 (可选)
|
||||
- [ ] SSRF 防护 (安全性)
|
||||
- [ ] 代理支持
|
||||
- [ ] Firecrawl 集成 (付费服务)
|
||||
|
||||
### 优先级 3 (高级)
|
||||
- [ ] 自动字符编码检测
|
||||
- [ ] PDF 内容提取
|
||||
- [ ] 图片 OCR 支持
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么有些页面抓取不到内容?
|
||||
|
||||
A: 可能原因:
|
||||
1. 页面需要 JavaScript 渲染 → 使用 `browser` 工具
|
||||
2. 页面有反爬虫机制 → 调整 User-Agent 或使用代理
|
||||
3. 页面需要登录 → 使用 `browser` 工具进行交互
|
||||
|
||||
### Q: 如何提高提取质量?
|
||||
|
||||
A:
|
||||
1. 安装 `readability-lxml`: `pip install readability-lxml`
|
||||
2. 安装 `html2text`: `pip install html2text`
|
||||
3. 使用 `markdown` 模式而不是 `text` 模式
|
||||
|
||||
### Q: 可以抓取 API 返回的 JSON 吗?
|
||||
|
||||
A: 可以!工具会自动检测 content-type,对于 JSON 会格式化输出。
|
||||
|
||||
## 贡献
|
||||
|
||||
本实现参考了以下优秀项目:
|
||||
- [Clawdbot](https://github.com/moltbot/moltbot) - Web tools 设计
|
||||
- [Mozilla Readability](https://github.com/mozilla/readability) - 内容提取算法
|
||||
- [html2text](https://github.com/Alir3z4/html2text) - HTML 转 Markdown
|
||||
|
||||
## 许可
|
||||
|
||||
遵循项目主许可证。
|
||||
@@ -1,100 +0,0 @@
|
||||
"""
|
||||
Test script for WebFetch tool
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from agent.tools.web_fetch import WebFetch
|
||||
|
||||
|
||||
def test_web_fetch():
|
||||
"""Test WebFetch tool"""
|
||||
|
||||
print("=" * 80)
|
||||
print("Testing WebFetch Tool")
|
||||
print("=" * 80)
|
||||
|
||||
# Create tool instance
|
||||
tool = WebFetch()
|
||||
|
||||
print(f"\n✅ Tool created: {tool.name}")
|
||||
print(f" Description: {tool.description}")
|
||||
|
||||
# Test 1: Fetch a simple webpage
|
||||
print("\n" + "-" * 80)
|
||||
print("Test 1: Fetching example.com")
|
||||
print("-" * 80)
|
||||
|
||||
result = tool.execute({
|
||||
"url": "https://example.com",
|
||||
"extract_mode": "text",
|
||||
"max_chars": 1000
|
||||
})
|
||||
|
||||
if result.status == "success":
|
||||
print("✅ Success!")
|
||||
data = result.result
|
||||
print(f" Title: {data.get('title', 'N/A')}")
|
||||
print(f" Status: {data.get('status')}")
|
||||
print(f" Extractor: {data.get('extractor')}")
|
||||
print(f" Length: {data.get('length')} chars")
|
||||
print(f" Truncated: {data.get('truncated')}")
|
||||
print(f"\n Content preview:")
|
||||
print(f" {data.get('text', '')[:200]}...")
|
||||
else:
|
||||
print(f"❌ Failed: {result.result}")
|
||||
|
||||
# Test 2: Invalid URL
|
||||
print("\n" + "-" * 80)
|
||||
print("Test 2: Testing invalid URL")
|
||||
print("-" * 80)
|
||||
|
||||
result = tool.execute({
|
||||
"url": "not-a-valid-url"
|
||||
})
|
||||
|
||||
if result.status == "error":
|
||||
print(f"✅ Correctly rejected invalid URL: {result.result}")
|
||||
else:
|
||||
print(f"❌ Should have rejected invalid URL")
|
||||
|
||||
# Test 3: Test with a real webpage (optional)
|
||||
print("\n" + "-" * 80)
|
||||
print("Test 3: Fetching a real webpage (Python.org)")
|
||||
print("-" * 80)
|
||||
|
||||
result = tool.execute({
|
||||
"url": "https://www.python.org",
|
||||
"extract_mode": "markdown",
|
||||
"max_chars": 2000
|
||||
})
|
||||
|
||||
if result.status == "success":
|
||||
print("✅ Success!")
|
||||
data = result.result
|
||||
print(f" Title: {data.get('title', 'N/A')}")
|
||||
print(f" Status: {data.get('status')}")
|
||||
print(f" Extractor: {data.get('extractor')}")
|
||||
print(f" Length: {data.get('length')} chars")
|
||||
print(f" Truncated: {data.get('truncated')}")
|
||||
if data.get('warning'):
|
||||
print(f" ⚠️ Warning: {data.get('warning')}")
|
||||
print(f"\n Content preview:")
|
||||
print(f" {data.get('text', '')[:300]}...")
|
||||
else:
|
||||
print(f"❌ Failed: {result.result}")
|
||||
|
||||
# Close the tool
|
||||
tool.close()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Testing complete!")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_web_fetch()
|
||||
@@ -8,6 +8,7 @@ import openai.error
|
||||
import requests
|
||||
from common import const
|
||||
from bot.bot import Bot
|
||||
from bot.openai_compatible_bot import OpenAICompatibleBot
|
||||
from bot.chatgpt.chat_gpt_session import ChatGPTSession
|
||||
from bot.openai.open_ai_image import OpenAIImage
|
||||
from bot.session_manager import SessionManager
|
||||
@@ -19,7 +20,7 @@ from config import conf, load_config
|
||||
from bot.baidu.baidu_wenxin_session import BaiduWenxinSession
|
||||
|
||||
# OpenAI对话模型API (可用)
|
||||
class ChatGPTBot(Bot, OpenAIImage):
|
||||
class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# set the default api_key
|
||||
@@ -53,6 +54,18 @@ class ChatGPTBot(Bot, OpenAIImage):
|
||||
if conf_model in [const.O1, const.O1_MINI]: # o1系列模型不支持系统提示词,使用文心模型的session
|
||||
self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or const.O1_MINI)
|
||||
|
||||
def get_api_config(self):
|
||||
"""Get API configuration for OpenAI-compatible base class"""
|
||||
return {
|
||||
'api_key': conf().get("open_ai_api_key"),
|
||||
'api_base': conf().get("open_ai_api_base"),
|
||||
'model': conf().get("model", "gpt-3.5-turbo"),
|
||||
'default_temperature': conf().get("temperature", 0.9),
|
||||
'default_top_p': conf().get("top_p", 1.0),
|
||||
'default_frequency_penalty': conf().get("frequency_penalty", 0.0),
|
||||
'default_presence_penalty': conf().get("presence_penalty", 0.0),
|
||||
}
|
||||
|
||||
def reply(self, query, context=None):
|
||||
# acquire reply content
|
||||
if context.type == ContextType.TEXT:
|
||||
@@ -172,252 +185,6 @@ class ChatGPTBot(Bot, OpenAIImage):
|
||||
else:
|
||||
return result
|
||||
|
||||
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
"""
|
||||
Call OpenAI API with tool support for agent integration
|
||||
|
||||
Args:
|
||||
messages: List of messages (may be in Claude format from agent)
|
||||
tools: List of tool definitions (may be in Claude format from agent)
|
||||
stream: Whether to use streaming
|
||||
**kwargs: Additional parameters (max_tokens, temperature, system, etc.)
|
||||
|
||||
Returns:
|
||||
Formatted response in OpenAI format or generator for streaming
|
||||
"""
|
||||
try:
|
||||
# Convert messages from Claude format to OpenAI format
|
||||
messages = self._convert_messages_to_openai_format(messages)
|
||||
|
||||
# Convert tools from Claude format to OpenAI format
|
||||
if tools:
|
||||
tools = self._convert_tools_to_openai_format(tools)
|
||||
|
||||
# Handle system prompt (OpenAI uses system message, Claude uses separate parameter)
|
||||
system_prompt = kwargs.get('system')
|
||||
if system_prompt:
|
||||
# Add system message at the beginning if not already present
|
||||
if not messages or messages[0].get('role') != 'system':
|
||||
messages = [{"role": "system", "content": system_prompt}] + messages
|
||||
else:
|
||||
# Replace existing system message
|
||||
messages[0] = {"role": "system", "content": system_prompt}
|
||||
|
||||
# Build request parameters
|
||||
request_params = {
|
||||
"model": kwargs.get("model", conf().get("model") or "gpt-3.5-turbo"),
|
||||
"messages": messages,
|
||||
"temperature": kwargs.get("temperature", conf().get("temperature", 0.9)),
|
||||
"top_p": kwargs.get("top_p", conf().get("top_p", 1)),
|
||||
"frequency_penalty": kwargs.get("frequency_penalty", conf().get("frequency_penalty", 0.0)),
|
||||
"presence_penalty": kwargs.get("presence_penalty", conf().get("presence_penalty", 0.0)),
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
# Add max_tokens if specified
|
||||
if kwargs.get("max_tokens"):
|
||||
request_params["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Add tools if provided
|
||||
if tools:
|
||||
request_params["tools"] = tools
|
||||
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
||||
|
||||
# Handle model-specific parameters (o1, gpt-5 series don't support some params)
|
||||
model = request_params["model"]
|
||||
if model in [const.O1, const.O1_MINI, const.GPT_5, const.GPT_5_MINI, const.GPT_5_NANO]:
|
||||
remove_keys = ["temperature", "top_p", "frequency_penalty", "presence_penalty"]
|
||||
for key in remove_keys:
|
||||
request_params.pop(key, None)
|
||||
|
||||
# Make API call
|
||||
# Note: Don't pass api_key explicitly to use global openai.api_key and openai.api_base
|
||||
# which are set in __init__
|
||||
if stream:
|
||||
return self._handle_stream_response(request_params)
|
||||
else:
|
||||
return self._handle_sync_response(request_params)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"[ChatGPT] call_with_tools error: {error_msg}")
|
||||
if stream:
|
||||
def error_generator():
|
||||
yield {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
return error_generator()
|
||||
else:
|
||||
return {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _handle_sync_response(self, request_params):
|
||||
"""Handle synchronous OpenAI API response"""
|
||||
try:
|
||||
# Explicitly set API configuration to ensure it's used
|
||||
# (global settings can be unreliable in some contexts)
|
||||
api_key = conf().get("open_ai_api_key")
|
||||
api_base = conf().get("open_ai_api_base")
|
||||
|
||||
# Build kwargs with explicit API configuration
|
||||
kwargs = dict(request_params)
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
if api_base:
|
||||
kwargs["api_base"] = api_base
|
||||
|
||||
response = openai.ChatCompletion.create(**kwargs)
|
||||
|
||||
# Response is already in OpenAI format
|
||||
logger.info(f"[ChatGPT] call_with_tools reply, model={response.get('model')}, "
|
||||
f"total_tokens={response.get('usage', {}).get('total_tokens', 0)}")
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ChatGPT] sync response error: {e}")
|
||||
raise
|
||||
|
||||
def _handle_stream_response(self, request_params):
|
||||
"""Handle streaming OpenAI API response"""
|
||||
try:
|
||||
# Explicitly set API configuration to ensure it's used
|
||||
api_key = conf().get("open_ai_api_key")
|
||||
api_base = conf().get("open_ai_api_base")
|
||||
|
||||
logger.debug(f"[ChatGPT] Starting stream with params: model={request_params.get('model')}, stream={request_params.get('stream')}")
|
||||
|
||||
# Build kwargs with explicit API configuration
|
||||
kwargs = dict(request_params)
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
if api_base:
|
||||
kwargs["api_base"] = api_base
|
||||
|
||||
stream = openai.ChatCompletion.create(**kwargs)
|
||||
|
||||
# OpenAI stream is already in the correct format
|
||||
chunk_count = 0
|
||||
for chunk in stream:
|
||||
chunk_count += 1
|
||||
yield chunk
|
||||
|
||||
logger.debug(f"[ChatGPT] Stream completed, yielded {chunk_count} chunks")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[ChatGPT] stream response error: {e}", exc_info=True)
|
||||
yield {
|
||||
"error": True,
|
||||
"message": str(e),
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _convert_tools_to_openai_format(self, tools):
|
||||
"""
|
||||
Convert tools from Claude format to OpenAI format
|
||||
|
||||
Claude format: {name, description, input_schema}
|
||||
OpenAI format: {type: "function", function: {name, description, parameters}}
|
||||
"""
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
openai_tools = []
|
||||
for tool in tools:
|
||||
# Check if already in OpenAI format
|
||||
if 'type' in tool and tool['type'] == 'function':
|
||||
openai_tools.append(tool)
|
||||
else:
|
||||
# Convert from Claude format
|
||||
openai_tools.append({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.get("name"),
|
||||
"description": tool.get("description"),
|
||||
"parameters": tool.get("input_schema", {})
|
||||
}
|
||||
})
|
||||
|
||||
return openai_tools
|
||||
|
||||
def _convert_messages_to_openai_format(self, messages):
|
||||
"""
|
||||
Convert messages from Claude format to OpenAI format
|
||||
|
||||
Claude uses content blocks with types like 'tool_use', 'tool_result'
|
||||
OpenAI uses 'tool_calls' in assistant messages and 'tool' role for results
|
||||
"""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
openai_messages = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
|
||||
# Handle string content (already in correct format)
|
||||
if isinstance(content, str):
|
||||
openai_messages.append(msg)
|
||||
continue
|
||||
|
||||
# Handle list content (Claude format with content blocks)
|
||||
if isinstance(content, list):
|
||||
# Check if this is a tool result message (user role with tool_result blocks)
|
||||
if role == "user" and any(block.get("type") == "tool_result" for block in content):
|
||||
# Convert each tool_result block to a separate tool message
|
||||
for block in content:
|
||||
if block.get("type") == "tool_result":
|
||||
openai_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": block.get("tool_use_id"),
|
||||
"content": block.get("content", "")
|
||||
})
|
||||
|
||||
# Check if this is an assistant message with tool_use blocks
|
||||
elif role == "assistant":
|
||||
# Separate text content and tool_use blocks
|
||||
text_parts = []
|
||||
tool_calls = []
|
||||
|
||||
for block in content:
|
||||
if block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif block.get("type") == "tool_use":
|
||||
tool_calls.append({
|
||||
"id": block.get("id"),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": block.get("name"),
|
||||
"arguments": json.dumps(block.get("input", {}))
|
||||
}
|
||||
})
|
||||
|
||||
# Build OpenAI format assistant message
|
||||
openai_msg = {
|
||||
"role": "assistant",
|
||||
"content": " ".join(text_parts) if text_parts else None
|
||||
}
|
||||
|
||||
if tool_calls:
|
||||
openai_msg["tool_calls"] = tool_calls
|
||||
|
||||
openai_messages.append(openai_msg)
|
||||
else:
|
||||
# Other list content, keep as is
|
||||
openai_messages.append(msg)
|
||||
else:
|
||||
# Other formats, keep as is
|
||||
openai_messages.append(msg)
|
||||
|
||||
return openai_messages
|
||||
|
||||
|
||||
class AzureChatGPTBot(ChatGPTBot):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@@ -8,6 +8,7 @@ Google gemini bot
|
||||
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from bot.bot import Bot
|
||||
import google.generativeai as genai
|
||||
from bot.session_manager import SessionManager
|
||||
@@ -118,82 +119,165 @@ class GoogleGeminiBot(Bot):
|
||||
|
||||
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
"""
|
||||
Call Gemini API with tool support for agent integration
|
||||
Call Gemini API with tool support using REST API (following official docs)
|
||||
|
||||
Args:
|
||||
messages: List of messages
|
||||
tools: List of tool definitions (OpenAI format, will be converted to Gemini format)
|
||||
messages: List of messages (OpenAI format)
|
||||
tools: List of tool definitions (OpenAI/Claude format)
|
||||
stream: Whether to use streaming
|
||||
**kwargs: Additional parameters
|
||||
**kwargs: Additional parameters (system, max_tokens, temperature, etc.)
|
||||
|
||||
Returns:
|
||||
Formatted response compatible with OpenAI format or generator for streaming
|
||||
"""
|
||||
try:
|
||||
# Configure Gemini
|
||||
genai.configure(api_key=self.api_key)
|
||||
model_name = kwargs.get("model", self.model)
|
||||
model_name = kwargs.get("model", self.model or "gemini-1.5-flash")
|
||||
|
||||
# Extract system prompt from messages
|
||||
# Build REST API payload
|
||||
payload = {"contents": []}
|
||||
|
||||
# Extract and set system instruction
|
||||
system_prompt = kwargs.get("system", "")
|
||||
gemini_messages = []
|
||||
if not system_prompt:
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = msg["content"]
|
||||
break
|
||||
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = msg["content"]
|
||||
else:
|
||||
gemini_messages.append(msg)
|
||||
if system_prompt:
|
||||
payload["system_instruction"] = {
|
||||
"parts": [{"text": system_prompt}]
|
||||
}
|
||||
|
||||
# Convert messages to Gemini format
|
||||
gemini_messages = self._convert_to_gemini_messages(gemini_messages)
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content", "")
|
||||
|
||||
if role == "system":
|
||||
continue
|
||||
|
||||
# Convert role
|
||||
gemini_role = "user" if role in ["user", "tool"] else "model"
|
||||
|
||||
# Handle different content formats
|
||||
parts = []
|
||||
|
||||
if isinstance(content, str):
|
||||
# Simple text content
|
||||
parts.append({"text": content})
|
||||
|
||||
elif isinstance(content, list):
|
||||
# List of content blocks (Claude format)
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
if isinstance(block, str):
|
||||
parts.append({"text": block})
|
||||
continue
|
||||
|
||||
block_type = block.get("type")
|
||||
|
||||
if block_type == "text":
|
||||
# Text block
|
||||
parts.append({"text": block.get("text", "")})
|
||||
|
||||
elif block_type == "tool_result":
|
||||
# Convert Claude tool_result to Gemini functionResponse
|
||||
tool_use_id = block.get("tool_use_id")
|
||||
tool_content = block.get("content", "")
|
||||
|
||||
# Try to parse tool content as JSON
|
||||
try:
|
||||
if isinstance(tool_content, str):
|
||||
tool_result_data = json.loads(tool_content)
|
||||
else:
|
||||
tool_result_data = tool_content
|
||||
except:
|
||||
tool_result_data = {"result": tool_content}
|
||||
|
||||
# Find the tool name from previous messages
|
||||
# Look for the corresponding tool_call in model's message
|
||||
tool_name = None
|
||||
for prev_msg in reversed(messages):
|
||||
if prev_msg.get("role") == "assistant":
|
||||
prev_content = prev_msg.get("content", [])
|
||||
if isinstance(prev_content, list):
|
||||
for prev_block in prev_content:
|
||||
if isinstance(prev_block, dict) and prev_block.get("type") == "tool_use":
|
||||
if prev_block.get("id") == tool_use_id:
|
||||
tool_name = prev_block.get("name")
|
||||
break
|
||||
if tool_name:
|
||||
break
|
||||
|
||||
# Gemini functionResponse format
|
||||
parts.append({
|
||||
"functionResponse": {
|
||||
"name": tool_name or "unknown",
|
||||
"response": tool_result_data
|
||||
}
|
||||
})
|
||||
|
||||
elif "text" in block:
|
||||
# Generic text field
|
||||
parts.append({"text": block["text"]})
|
||||
|
||||
if parts:
|
||||
payload["contents"].append({
|
||||
"role": gemini_role,
|
||||
"parts": parts
|
||||
})
|
||||
|
||||
# Safety settings
|
||||
safety_settings = {
|
||||
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
|
||||
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
|
||||
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
|
||||
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
|
||||
}
|
||||
|
||||
# Convert tools from OpenAI format to Gemini format if provided
|
||||
gemini_tools = None
|
||||
if tools:
|
||||
gemini_tools = self._convert_tools_to_gemini_format(tools)
|
||||
|
||||
# Create model with system instruction if available
|
||||
model_kwargs = {"model_name": model_name}
|
||||
if system_prompt:
|
||||
model_kwargs["system_instruction"] = system_prompt
|
||||
|
||||
model = genai.GenerativeModel(**model_kwargs)
|
||||
|
||||
# Generate content
|
||||
generation_config = {}
|
||||
if kwargs.get("max_tokens"):
|
||||
generation_config["max_output_tokens"] = kwargs["max_tokens"]
|
||||
# Generation config
|
||||
gen_config = {}
|
||||
if kwargs.get("temperature") is not None:
|
||||
generation_config["temperature"] = kwargs["temperature"]
|
||||
gen_config["temperature"] = kwargs["temperature"]
|
||||
if kwargs.get("max_tokens"):
|
||||
gen_config["maxOutputTokens"] = kwargs["max_tokens"]
|
||||
if gen_config:
|
||||
payload["generationConfig"] = gen_config
|
||||
|
||||
request_params = {
|
||||
"safety_settings": safety_settings
|
||||
# Convert tools to Gemini format (REST API style)
|
||||
if tools:
|
||||
gemini_tools = self._convert_tools_to_gemini_rest_format(tools)
|
||||
if gemini_tools:
|
||||
payload["tools"] = gemini_tools
|
||||
logger.info(f"[Gemini] Added {len(tools)} tools to request")
|
||||
|
||||
# Make REST API call
|
||||
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
endpoint = f"{base_url}/models/{model_name}:generateContent"
|
||||
if stream:
|
||||
endpoint = f"{base_url}/models/{model_name}:streamGenerateContent?alt=sse"
|
||||
|
||||
headers = {
|
||||
"x-goog-api-key": self.api_key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
if generation_config:
|
||||
request_params["generation_config"] = generation_config
|
||||
if gemini_tools:
|
||||
request_params["tools"] = gemini_tools
|
||||
|
||||
logger.debug(f"[Gemini] REST API call: {endpoint}")
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
stream=stream,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if stream:
|
||||
return self._handle_gemini_stream_response(model, gemini_messages, request_params, model_name)
|
||||
return self._handle_gemini_rest_stream_response(response, model_name)
|
||||
else:
|
||||
return self._handle_gemini_sync_response(model, gemini_messages, request_params, model_name)
|
||||
return self._handle_gemini_rest_sync_response(response, model_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Gemini] call_with_tools error: {e}")
|
||||
logger.error(f"[Gemini] call_with_tools error: {e}", exc_info=True)
|
||||
error_msg = str(e) # Capture error message before creating generator
|
||||
if stream:
|
||||
def error_generator():
|
||||
yield {
|
||||
"error": True,
|
||||
"message": str(e),
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
return error_generator()
|
||||
@@ -204,6 +288,227 @@ class GoogleGeminiBot(Bot):
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _convert_tools_to_gemini_rest_format(self, tools_list):
|
||||
"""
|
||||
Convert tools to Gemini REST API format
|
||||
|
||||
Handles both OpenAI and Claude/Agent formats.
|
||||
Returns: [{"functionDeclarations": [...]}]
|
||||
"""
|
||||
function_declarations = []
|
||||
|
||||
for tool in tools_list:
|
||||
# Extract name, description, and parameters based on format
|
||||
if tool.get("type") == "function":
|
||||
# OpenAI format: {"type": "function", "function": {...}}
|
||||
func = tool.get("function", {})
|
||||
name = func.get("name")
|
||||
description = func.get("description", "")
|
||||
parameters = func.get("parameters", {})
|
||||
else:
|
||||
# Claude/Agent format: {"name": "...", "description": "...", "input_schema": {...}}
|
||||
name = tool.get("name")
|
||||
description = tool.get("description", "")
|
||||
parameters = tool.get("input_schema", {})
|
||||
|
||||
if not name:
|
||||
logger.warning(f"[Gemini] Skipping tool without name: {tool}")
|
||||
continue
|
||||
|
||||
logger.debug(f"[Gemini] Converting tool: {name}")
|
||||
|
||||
function_declarations.append({
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": parameters
|
||||
})
|
||||
|
||||
# All functionDeclarations must be in a single tools object (per Gemini REST API spec)
|
||||
return [{
|
||||
"functionDeclarations": function_declarations
|
||||
}] if function_declarations else []
|
||||
|
||||
def _handle_gemini_rest_sync_response(self, response, model_name):
|
||||
"""Handle Gemini REST API sync response and convert to OpenAI format"""
|
||||
try:
|
||||
if response.status_code != 200:
|
||||
error_text = response.text
|
||||
logger.error(f"[Gemini] API error ({response.status_code}): {error_text}")
|
||||
return {
|
||||
"error": True,
|
||||
"message": f"Gemini API error: {error_text}",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
|
||||
data = response.json()
|
||||
logger.debug(f"[Gemini] Response received")
|
||||
|
||||
# Extract from Gemini response format
|
||||
candidates = data.get("candidates", [])
|
||||
if not candidates:
|
||||
logger.warning("[Gemini] No candidates in response")
|
||||
return {
|
||||
"error": True,
|
||||
"message": "No candidates in response",
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
candidate = candidates[0]
|
||||
content = candidate.get("content", {})
|
||||
parts = content.get("parts", [])
|
||||
|
||||
# Extract text and function calls
|
||||
text_content = ""
|
||||
tool_calls = []
|
||||
|
||||
for part in parts:
|
||||
# Check for text
|
||||
if "text" in part:
|
||||
text_content += part["text"]
|
||||
|
||||
# Check for functionCall (per REST API docs)
|
||||
if "functionCall" in part:
|
||||
fc = part["functionCall"]
|
||||
logger.info(f"[Gemini] Function call detected: {fc.get('name')}")
|
||||
|
||||
tool_calls.append({
|
||||
"id": f"call_{int(time.time() * 1000000)}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": fc.get("name"),
|
||||
"arguments": json.dumps(fc.get("args", {}))
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(f"[Gemini] Response: text={len(text_content)} chars, tool_calls={len(tool_calls)}")
|
||||
|
||||
# Build OpenAI format response
|
||||
message_dict = {
|
||||
"role": "assistant",
|
||||
"content": text_content or None
|
||||
}
|
||||
if tool_calls:
|
||||
message_dict["tool_calls"] = tool_calls
|
||||
|
||||
return {
|
||||
"id": f"chatcmpl-{time.time()}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": model_name,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": message_dict,
|
||||
"finish_reason": "tool_calls" if tool_calls else "stop"
|
||||
}],
|
||||
"usage": data.get("usageMetadata", {})
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Gemini] sync response error: {e}")
|
||||
return {
|
||||
"error": True,
|
||||
"message": str(e),
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _handle_gemini_rest_stream_response(self, response, model_name):
|
||||
"""Handle Gemini REST API stream response"""
|
||||
try:
|
||||
all_tool_calls = []
|
||||
has_sent_tool_calls = False
|
||||
|
||||
for line in response.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
line = line.decode('utf-8')
|
||||
|
||||
# Skip SSE prefixes
|
||||
if line.startswith('data: '):
|
||||
line = line[6:]
|
||||
|
||||
if not line or line == '[DONE]':
|
||||
continue
|
||||
|
||||
try:
|
||||
chunk_data = json.loads(line)
|
||||
candidates = chunk_data.get("candidates", [])
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
candidate = candidates[0]
|
||||
content = candidate.get("content", {})
|
||||
parts = content.get("parts", [])
|
||||
|
||||
# Stream text content
|
||||
for part in parts:
|
||||
if "text" in part and part["text"]:
|
||||
yield {
|
||||
"id": f"chatcmpl-{time.time()}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": model_name,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {"content": part["text"]},
|
||||
"finish_reason": None
|
||||
}]
|
||||
}
|
||||
|
||||
# Collect function calls
|
||||
if "functionCall" in part:
|
||||
fc = part["functionCall"]
|
||||
all_tool_calls.append({
|
||||
"index": len(all_tool_calls), # Add index to differentiate multiple tool calls
|
||||
"id": f"call_{int(time.time() * 1000000)}_{len(all_tool_calls)}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": fc.get("name"),
|
||||
"arguments": json.dumps(fc.get("args", {}))
|
||||
}
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Send tool calls if any were collected
|
||||
if all_tool_calls and not has_sent_tool_calls:
|
||||
logger.info(f"[Gemini] Stream detected {len(all_tool_calls)} tool calls")
|
||||
yield {
|
||||
"id": f"chatcmpl-{time.time()}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": model_name,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {"tool_calls": all_tool_calls},
|
||||
"finish_reason": None
|
||||
}]
|
||||
}
|
||||
has_sent_tool_calls = True
|
||||
|
||||
# Final chunk
|
||||
yield {
|
||||
"id": f"chatcmpl-{time.time()}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": model_name,
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "tool_calls" if all_tool_calls else "stop"
|
||||
}]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Gemini] stream response error: {e}", exc_info=True)
|
||||
error_msg = str(e)
|
||||
yield {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _convert_tools_to_gemini_format(self, openai_tools):
|
||||
"""Convert OpenAI tool format to Gemini function declarations"""
|
||||
import google.generativeai as genai
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
import requests
|
||||
import config
|
||||
from bot.bot import Bot
|
||||
from bot.openai_compatible_bot import OpenAICompatibleBot
|
||||
from bot.chatgpt.chat_gpt_session import ChatGPTSession
|
||||
from bot.session_manager import SessionManager
|
||||
from bridge.context import Context, ContextType
|
||||
@@ -17,7 +18,7 @@ from common import memory, utils
|
||||
import base64
|
||||
import os
|
||||
|
||||
class LinkAIBot(Bot):
|
||||
class LinkAIBot(Bot, OpenAICompatibleBot):
|
||||
# authentication failed
|
||||
AUTH_FAILED_CODE = 401
|
||||
NO_QUOTA_CODE = 406
|
||||
@@ -26,6 +27,18 @@ class LinkAIBot(Bot):
|
||||
super().__init__()
|
||||
self.sessions = LinkAISessionManager(LinkAISession, model=conf().get("model") or "gpt-3.5-turbo")
|
||||
self.args = {}
|
||||
|
||||
def get_api_config(self):
|
||||
"""Get API configuration for OpenAI-compatible base class"""
|
||||
return {
|
||||
'api_key': conf().get("open_ai_api_key"), # LinkAI uses OpenAI-compatible key
|
||||
'api_base': conf().get("open_ai_api_base", "https://api.link-ai.tech/v1"),
|
||||
'model': conf().get("model", "gpt-3.5-turbo"),
|
||||
'default_temperature': conf().get("temperature", 0.9),
|
||||
'default_top_p': conf().get("top_p", 1.0),
|
||||
'default_frequency_penalty': conf().get("frequency_penalty", 0.0),
|
||||
'default_presence_penalty': conf().get("presence_penalty", 0.0),
|
||||
}
|
||||
|
||||
def reply(self, query, context: Context = None) -> Reply:
|
||||
if context.type == ContextType.TEXT:
|
||||
|
||||
@@ -6,6 +6,7 @@ import openai
|
||||
import openai.error
|
||||
|
||||
from bot.bot import Bot
|
||||
from bot.openai_compatible_bot import OpenAICompatibleBot
|
||||
from bot.openai.open_ai_image import OpenAIImage
|
||||
from bot.openai.open_ai_session import OpenAISession
|
||||
from bot.session_manager import SessionManager
|
||||
@@ -18,7 +19,7 @@ user_session = dict()
|
||||
|
||||
|
||||
# OpenAI对话模型API (可用)
|
||||
class OpenAIBot(Bot, OpenAIImage):
|
||||
class OpenAIBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
openai.api_key = conf().get("open_ai_api_key")
|
||||
@@ -40,6 +41,18 @@ class OpenAIBot(Bot, OpenAIImage):
|
||||
"timeout": conf().get("request_timeout", None), # 重试超时时间,在这个时间内,将会自动重试
|
||||
"stop": ["\n\n\n"],
|
||||
}
|
||||
|
||||
def get_api_config(self):
|
||||
"""Get API configuration for OpenAI-compatible base class"""
|
||||
return {
|
||||
'api_key': conf().get("open_ai_api_key"),
|
||||
'api_base': conf().get("open_ai_api_base"),
|
||||
'model': conf().get("model", "text-davinci-003"),
|
||||
'default_temperature': conf().get("temperature", 0.9),
|
||||
'default_top_p': conf().get("top_p", 1.0),
|
||||
'default_frequency_penalty': conf().get("frequency_penalty", 0.0),
|
||||
'default_presence_penalty': conf().get("presence_penalty", 0.0),
|
||||
}
|
||||
|
||||
def reply(self, query, context=None):
|
||||
# acquire reply content
|
||||
|
||||
278
bot/openai_compatible_bot.py
Normal file
278
bot/openai_compatible_bot.py
Normal file
@@ -0,0 +1,278 @@
|
||||
# encoding:utf-8
|
||||
|
||||
"""
|
||||
OpenAI-Compatible Bot Base Class
|
||||
|
||||
Provides a common implementation for bots that are compatible with OpenAI's API format.
|
||||
This includes: OpenAI, LinkAI, Azure OpenAI, and many third-party providers.
|
||||
"""
|
||||
|
||||
import json
|
||||
import openai
|
||||
from common.log import logger
|
||||
|
||||
|
||||
class OpenAICompatibleBot:
|
||||
"""
|
||||
Base class for OpenAI-compatible bots.
|
||||
|
||||
Provides common tool calling implementation that can be inherited by:
|
||||
- ChatGPTBot
|
||||
- LinkAIBot
|
||||
- OpenAIBot
|
||||
- AzureChatGPTBot
|
||||
- Other OpenAI-compatible providers
|
||||
|
||||
Subclasses only need to override get_api_config() to provide their specific API settings.
|
||||
"""
|
||||
|
||||
def get_api_config(self):
|
||||
"""
|
||||
Get API configuration for this bot.
|
||||
|
||||
Subclasses should override this to provide their specific config.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'api_key': str,
|
||||
'api_base': str (optional),
|
||||
'model': str,
|
||||
'default_temperature': float,
|
||||
'default_top_p': float,
|
||||
'default_frequency_penalty': float,
|
||||
'default_presence_penalty': float,
|
||||
}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement get_api_config()")
|
||||
|
||||
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||
"""
|
||||
Call OpenAI-compatible API with tool support for agent integration
|
||||
|
||||
This method handles:
|
||||
1. Format conversion (Claude format → OpenAI format)
|
||||
2. System prompt injection
|
||||
3. API calling with proper configuration
|
||||
4. Error handling
|
||||
|
||||
Args:
|
||||
messages: List of messages (may be in Claude format from agent)
|
||||
tools: List of tool definitions (may be in Claude format from agent)
|
||||
stream: Whether to use streaming
|
||||
**kwargs: Additional parameters (max_tokens, temperature, system, etc.)
|
||||
|
||||
Returns:
|
||||
Formatted response in OpenAI format or generator for streaming
|
||||
"""
|
||||
try:
|
||||
# Get API configuration from subclass
|
||||
api_config = self.get_api_config()
|
||||
|
||||
# Convert messages from Claude format to OpenAI format
|
||||
messages = self._convert_messages_to_openai_format(messages)
|
||||
|
||||
# Convert tools from Claude format to OpenAI format
|
||||
if tools:
|
||||
tools = self._convert_tools_to_openai_format(tools)
|
||||
|
||||
# Handle system prompt (OpenAI uses system message, Claude uses separate parameter)
|
||||
system_prompt = kwargs.get('system')
|
||||
if system_prompt:
|
||||
# Add system message at the beginning if not already present
|
||||
if not messages or messages[0].get('role') != 'system':
|
||||
messages = [{"role": "system", "content": system_prompt}] + messages
|
||||
else:
|
||||
# Replace existing system message
|
||||
messages[0] = {"role": "system", "content": system_prompt}
|
||||
|
||||
# Build request parameters
|
||||
request_params = {
|
||||
"model": kwargs.get("model", api_config.get('model', 'gpt-3.5-turbo')),
|
||||
"messages": messages,
|
||||
"temperature": kwargs.get("temperature", api_config.get('default_temperature', 0.9)),
|
||||
"top_p": kwargs.get("top_p", api_config.get('default_top_p', 1.0)),
|
||||
"frequency_penalty": kwargs.get("frequency_penalty", api_config.get('default_frequency_penalty', 0.0)),
|
||||
"presence_penalty": kwargs.get("presence_penalty", api_config.get('default_presence_penalty', 0.0)),
|
||||
"stream": stream
|
||||
}
|
||||
|
||||
# Add max_tokens if specified
|
||||
if kwargs.get("max_tokens"):
|
||||
request_params["max_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
# Add tools if provided
|
||||
if tools:
|
||||
request_params["tools"] = tools
|
||||
request_params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
||||
|
||||
# Make API call with proper configuration
|
||||
api_key = api_config.get('api_key')
|
||||
api_base = api_config.get('api_base')
|
||||
|
||||
if stream:
|
||||
return self._handle_stream_response(request_params, api_key, api_base)
|
||||
else:
|
||||
return self._handle_sync_response(request_params, api_key, api_base)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"[{self.__class__.__name__}] call_with_tools error: {error_msg}")
|
||||
if stream:
|
||||
def error_generator():
|
||||
yield {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
return error_generator()
|
||||
else:
|
||||
return {
|
||||
"error": True,
|
||||
"message": error_msg,
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _handle_sync_response(self, request_params, api_key, api_base):
|
||||
"""Handle synchronous OpenAI API response"""
|
||||
try:
|
||||
# Build kwargs with explicit API configuration
|
||||
kwargs = dict(request_params)
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
if api_base:
|
||||
kwargs["api_base"] = api_base
|
||||
|
||||
response = openai.ChatCompletion.create(**kwargs)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.__class__.__name__}] sync response error: {e}")
|
||||
return {
|
||||
"error": True,
|
||||
"message": str(e),
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _handle_stream_response(self, request_params, api_key, api_base):
|
||||
"""Handle streaming OpenAI API response"""
|
||||
try:
|
||||
# Build kwargs with explicit API configuration
|
||||
kwargs = dict(request_params)
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
if api_base:
|
||||
kwargs["api_base"] = api_base
|
||||
|
||||
stream = openai.ChatCompletion.create(**kwargs)
|
||||
|
||||
# Stream chunks to caller
|
||||
for chunk in stream:
|
||||
yield chunk
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.__class__.__name__}] stream response error: {e}")
|
||||
yield {
|
||||
"error": True,
|
||||
"message": str(e),
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
def _convert_tools_to_openai_format(self, tools):
|
||||
"""
|
||||
Convert tools from Claude format to OpenAI format
|
||||
|
||||
Claude format: {name, description, input_schema}
|
||||
OpenAI format: {type: "function", function: {name, description, parameters}}
|
||||
"""
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
openai_tools = []
|
||||
for tool in tools:
|
||||
# Check if already in OpenAI format
|
||||
if 'type' in tool and tool['type'] == 'function':
|
||||
openai_tools.append(tool)
|
||||
else:
|
||||
# Convert from Claude format
|
||||
openai_tools.append({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.get("name"),
|
||||
"description": tool.get("description"),
|
||||
"parameters": tool.get("input_schema", {})
|
||||
}
|
||||
})
|
||||
|
||||
return openai_tools
|
||||
|
||||
def _convert_messages_to_openai_format(self, messages):
|
||||
"""
|
||||
Convert messages from Claude format to OpenAI format
|
||||
|
||||
Claude uses content blocks with types like 'tool_use', 'tool_result'
|
||||
OpenAI uses 'tool_calls' in assistant messages and 'tool' role for results
|
||||
"""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
openai_messages = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
|
||||
# Handle string content (already in correct format)
|
||||
if isinstance(content, str):
|
||||
openai_messages.append(msg)
|
||||
continue
|
||||
|
||||
# Handle list content (Claude format with content blocks)
|
||||
if isinstance(content, list):
|
||||
# Check if this is a tool result message (user role with tool_result blocks)
|
||||
if role == "user" and any(block.get("type") == "tool_result" for block in content):
|
||||
# Convert each tool_result block to a separate tool message
|
||||
for block in content:
|
||||
if block.get("type") == "tool_result":
|
||||
openai_messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": block.get("tool_use_id"),
|
||||
"content": block.get("content", "")
|
||||
})
|
||||
|
||||
# Check if this is an assistant message with tool_use blocks
|
||||
elif role == "assistant":
|
||||
# Separate text content and tool_use blocks
|
||||
text_parts = []
|
||||
tool_calls = []
|
||||
|
||||
for block in content:
|
||||
if block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif block.get("type") == "tool_use":
|
||||
tool_calls.append({
|
||||
"id": block.get("id"),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": block.get("name"),
|
||||
"arguments": json.dumps(block.get("input", {}))
|
||||
}
|
||||
})
|
||||
|
||||
# Build OpenAI format assistant message
|
||||
openai_msg = {
|
||||
"role": "assistant",
|
||||
"content": " ".join(text_parts) if text_parts else None
|
||||
}
|
||||
|
||||
if tool_calls:
|
||||
openai_msg["tool_calls"] = tool_calls
|
||||
|
||||
openai_messages.append(openai_msg)
|
||||
else:
|
||||
# Other list content, keep as is
|
||||
openai_messages.append(msg)
|
||||
else:
|
||||
# Other formats, keep as is
|
||||
openai_messages.append(msg)
|
||||
|
||||
return openai_messages
|
||||
@@ -5,6 +5,7 @@ Agent Bridge - Integrates Agent system with existing COW bridge
|
||||
from typing import Optional, List
|
||||
|
||||
from agent.protocol import Agent, LLMModel, LLMRequest
|
||||
from bot.openai_compatible_bot import OpenAICompatibleBot
|
||||
from bridge.bridge import Bridge
|
||||
from bridge.context import Context
|
||||
from bridge.reply import Reply, ReplyType
|
||||
@@ -12,11 +13,51 @@ from common import const
|
||||
from common.log import logger
|
||||
|
||||
|
||||
def add_openai_compatible_support(bot_instance):
|
||||
"""
|
||||
Dynamically add OpenAI-compatible tool calling support to a bot instance.
|
||||
|
||||
This allows any bot to gain tool calling capability without modifying its code,
|
||||
as long as it uses OpenAI-compatible API format.
|
||||
"""
|
||||
if hasattr(bot_instance, 'call_with_tools'):
|
||||
# Bot already has tool calling support
|
||||
return bot_instance
|
||||
|
||||
# Create a temporary mixin class that combines the bot with OpenAI compatibility
|
||||
class EnhancedBot(bot_instance.__class__, OpenAICompatibleBot):
|
||||
"""Dynamically enhanced bot with OpenAI-compatible tool calling"""
|
||||
|
||||
def get_api_config(self):
|
||||
"""
|
||||
Infer API config from common configuration patterns.
|
||||
Most OpenAI-compatible bots use similar configuration.
|
||||
"""
|
||||
from config import conf
|
||||
|
||||
return {
|
||||
'api_key': conf().get("open_ai_api_key"),
|
||||
'api_base': conf().get("open_ai_api_base"),
|
||||
'model': conf().get("model", "gpt-3.5-turbo"),
|
||||
'default_temperature': conf().get("temperature", 0.9),
|
||||
'default_top_p': conf().get("top_p", 1.0),
|
||||
'default_frequency_penalty': conf().get("frequency_penalty", 0.0),
|
||||
'default_presence_penalty': conf().get("presence_penalty", 0.0),
|
||||
}
|
||||
|
||||
# Change the bot's class to the enhanced version
|
||||
bot_instance.__class__ = EnhancedBot
|
||||
logger.info(
|
||||
f"[AgentBridge] Enhanced {bot_instance.__class__.__bases__[0].__name__} with OpenAI-compatible tool calling")
|
||||
|
||||
return bot_instance
|
||||
|
||||
|
||||
class AgentLLMModel(LLMModel):
|
||||
"""
|
||||
LLM Model adapter that uses COW's existing bot infrastructure
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, bridge: Bridge, bot_type: str = "chat"):
|
||||
# Get model name directly from config
|
||||
from config import conf
|
||||
@@ -28,9 +69,11 @@ class AgentLLMModel(LLMModel):
|
||||
|
||||
@property
|
||||
def bot(self):
|
||||
"""Lazy load the bot"""
|
||||
"""Lazy load the bot and enhance it with tool calling if needed"""
|
||||
if self._bot is None:
|
||||
self._bot = self.bridge.get_bot(self.bot_type)
|
||||
# Automatically add tool calling support if not present
|
||||
self._bot = add_openai_compatible_support(self._bot)
|
||||
return self._bot
|
||||
|
||||
def call(self, request: LLMRequest):
|
||||
@@ -157,9 +200,23 @@ class AgentBridge:
|
||||
model=model,
|
||||
tools=tools,
|
||||
max_steps=kwargs.get("max_steps", 15),
|
||||
output_mode=kwargs.get("output_mode", "logger")
|
||||
output_mode=kwargs.get("output_mode", "logger"),
|
||||
workspace_dir=kwargs.get("workspace_dir"), # Pass workspace for skills loading
|
||||
enable_skills=kwargs.get("enable_skills", True), # Enable skills by default
|
||||
memory_manager=kwargs.get("memory_manager"), # Pass memory manager
|
||||
max_context_tokens=kwargs.get("max_context_tokens"),
|
||||
context_reserve_tokens=kwargs.get("context_reserve_tokens")
|
||||
)
|
||||
|
||||
|
||||
# Log skill loading details
|
||||
if self.agent.skill_manager:
|
||||
logger.info(f"[AgentBridge] SkillManager initialized:")
|
||||
logger.info(f"[AgentBridge] - Managed dir: {self.agent.skill_manager.managed_skills_dir}")
|
||||
logger.info(f"[AgentBridge] - Workspace dir: {self.agent.skill_manager.workspace_dir}")
|
||||
logger.info(f"[AgentBridge] - Total skills: {len(self.agent.skill_manager.skills)}")
|
||||
for skill_name in self.agent.skill_manager.skills.keys():
|
||||
logger.info(f"[AgentBridge] * {skill_name}")
|
||||
|
||||
return self.agent
|
||||
|
||||
def get_agent(self) -> Optional[Agent]:
|
||||
@@ -169,24 +226,28 @@ class AgentBridge:
|
||||
return self.agent
|
||||
|
||||
def _init_default_agent(self):
|
||||
"""Initialize default super agent with config and memory"""
|
||||
"""Initialize default super agent with new prompt system"""
|
||||
from config import conf
|
||||
import os
|
||||
|
||||
# Get base system prompt from config
|
||||
base_prompt = conf().get("character_desc", "你是一个AI助手")
|
||||
|
||||
# Setup memory if enabled
|
||||
|
||||
# Get workspace from config
|
||||
workspace_root = os.path.expanduser(conf().get("agent_workspace", "~/cow"))
|
||||
|
||||
# Initialize workspace and create template files
|
||||
from agent.prompt import ensure_workspace, load_context_files, PromptBuilder
|
||||
|
||||
workspace_files = ensure_workspace(workspace_root, create_templates=True)
|
||||
logger.info(f"[AgentBridge] Workspace initialized at: {workspace_root}")
|
||||
|
||||
# Setup memory system
|
||||
memory_manager = None
|
||||
memory_tools = []
|
||||
|
||||
|
||||
try:
|
||||
# Try to initialize memory system
|
||||
from agent.memory import MemoryManager, MemoryConfig
|
||||
from agent.tools import MemorySearchTool, MemoryGetTool
|
||||
|
||||
# Create memory config directly with sensible defaults
|
||||
workspace_root = os.path.expanduser("~/cow")
|
||||
|
||||
memory_config = MemoryConfig(
|
||||
workspace_root=workspace_root,
|
||||
embedding_provider="local", # Use local embedding (no API key needed)
|
||||
@@ -202,35 +263,24 @@ class AgentBridge:
|
||||
MemoryGetTool(memory_manager)
|
||||
]
|
||||
|
||||
# Build memory guidance and add to system prompt
|
||||
memory_guidance = memory_manager.build_memory_guidance(
|
||||
lang="zh",
|
||||
include_context=True
|
||||
)
|
||||
system_prompt = base_prompt + "\n\n" + memory_guidance
|
||||
|
||||
logger.info(f"[AgentBridge] Memory system initialized")
|
||||
logger.info(f"[AgentBridge] Workspace: {memory_config.get_workspace()}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[AgentBridge] Memory system not available: {e}")
|
||||
logger.info("[AgentBridge] Continuing without memory features")
|
||||
system_prompt = base_prompt
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
logger.info("[AgentBridge] Initializing super agent")
|
||||
|
||||
# Configure file tools to work in the correct workspace
|
||||
file_config = {"cwd": workspace_root} if memory_manager else {}
|
||||
|
||||
|
||||
# Use ToolManager to dynamically load all available tools
|
||||
from agent.tools import ToolManager
|
||||
tool_manager = ToolManager()
|
||||
tool_manager.load_tools()
|
||||
|
||||
|
||||
# Create tool instances for all available tools
|
||||
tools = []
|
||||
file_config = {
|
||||
"cwd": workspace_root,
|
||||
"memory_manager": memory_manager
|
||||
} if memory_manager else {"cwd": workspace_root}
|
||||
|
||||
for tool_name in tool_manager.tool_classes.keys():
|
||||
try:
|
||||
tool = tool_manager.create_tool(tool_name)
|
||||
@@ -238,30 +288,61 @@ class AgentBridge:
|
||||
# Apply workspace config to file operation tools
|
||||
if tool_name in ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls']:
|
||||
tool.config = file_config
|
||||
tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None)
|
||||
if 'memory_manager' in file_config:
|
||||
tool.memory_manager = file_config['memory_manager']
|
||||
tools.append(tool)
|
||||
logger.debug(f"[AgentBridge] Loaded tool: {tool_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[AgentBridge] Failed to load tool {tool_name}: {e}")
|
||||
|
||||
|
||||
# Add memory tools
|
||||
if memory_tools:
|
||||
tools.extend(memory_tools)
|
||||
logger.info(f"[AgentBridge] Added {len(memory_tools)} memory tools")
|
||||
|
||||
logger.info(f"[AgentBridge] Loaded {len(tools)} tools: {[t.name for t in tools]}")
|
||||
|
||||
# Create agent with configured tools
|
||||
|
||||
# Load context files (SOUL.md, USER.md, etc.)
|
||||
context_files = load_context_files(workspace_root)
|
||||
logger.info(f"[AgentBridge] Loaded {len(context_files)} context files: {[f.path for f in context_files]}")
|
||||
|
||||
# Build system prompt using new prompt builder
|
||||
prompt_builder = PromptBuilder(
|
||||
workspace_dir=workspace_root,
|
||||
language="zh"
|
||||
)
|
||||
|
||||
# Get runtime info
|
||||
runtime_info = {
|
||||
"model": conf().get("model", "unknown"),
|
||||
"workspace": workspace_root,
|
||||
"channel": "web" # TODO: get from actual channel, default to "web" to hide if not specified
|
||||
}
|
||||
|
||||
system_prompt = prompt_builder.build(
|
||||
tools=tools,
|
||||
context_files=context_files,
|
||||
memory_manager=memory_manager,
|
||||
runtime_info=runtime_info
|
||||
)
|
||||
|
||||
logger.info("[AgentBridge] System prompt built successfully")
|
||||
|
||||
# Create agent with configured tools and workspace
|
||||
agent = self.create_agent(
|
||||
system_prompt=system_prompt,
|
||||
tools=tools,
|
||||
max_steps=50,
|
||||
output_mode="logger"
|
||||
output_mode="logger",
|
||||
workspace_dir=workspace_root, # Pass workspace to agent for skills loading
|
||||
enable_skills=True # Enable skills auto-loading
|
||||
)
|
||||
|
||||
|
||||
# Attach memory manager to agent if available
|
||||
if memory_manager:
|
||||
agent.memory_manager = memory_manager
|
||||
|
||||
# Add memory tools if available
|
||||
if memory_tools:
|
||||
for tool in memory_tools:
|
||||
agent.add_tool(tool)
|
||||
logger.info(f"[AgentBridge] Added {len(memory_tools)} memory tools")
|
||||
logger.info(f"[AgentBridge] Memory manager attached to agent")
|
||||
|
||||
def agent_reply(self, query: str, context: Context = None,
|
||||
on_event=None, clear_history: bool = False) -> Reply:
|
||||
|
||||
@@ -183,7 +183,8 @@ available_setting = {
|
||||
"Minimax_group_id": "",
|
||||
"Minimax_base_url": "",
|
||||
"web_port": 9899,
|
||||
"agent": False # 是否开启Agent模式
|
||||
"agent": False, # 是否开启Agent模式
|
||||
"agent_workspace": "~/cow" # agent工作空间路径,用于存储skills、memory等
|
||||
}
|
||||
|
||||
|
||||
|
||||
155
skills/skill-creator/AUTO_RELOAD_FEATURE.md
Normal file
155
skills/skill-creator/AUTO_RELOAD_FEATURE.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 技能自动重载功能
|
||||
|
||||
## ✨ 新功能:创建技能后自动刷新
|
||||
|
||||
### 📋 问题
|
||||
|
||||
**之前的行为**:
|
||||
1. 用户通过 skill-creator 创建新技能
|
||||
2. bash 工具执行 `init_skill.py` 成功
|
||||
3. 新技能被创建在 `~/cow/skills/` 目录
|
||||
4. ❌ **但 Agent 不知道有新技能**
|
||||
5. ❌ **需要重启 Agent 才能加载新技能**
|
||||
|
||||
### ✅ 解决方案
|
||||
|
||||
在 `agent/protocol/agent_stream.py` 的 `_execute_tool()` 方法中添加自动检测和刷新逻辑:
|
||||
|
||||
```python
|
||||
def _execute_tool(self, tool_call: Dict) -> Dict[str, Any]:
|
||||
...
|
||||
# Execute tool
|
||||
result: ToolResult = tool.execute_tool(arguments)
|
||||
|
||||
# Auto-refresh skills after skill creation
|
||||
if tool_name == "bash" and result.status == "success":
|
||||
command = arguments.get("command", "")
|
||||
if "init_skill.py" in command and self.agent.skill_manager:
|
||||
logger.info("🔄 Detected skill creation, refreshing skills...")
|
||||
self.agent.refresh_skills()
|
||||
logger.info(f"✅ Skills refreshed! Now have {len(self.agent.skill_manager.skills)} skills")
|
||||
...
|
||||
```
|
||||
|
||||
### 🎯 工作原理
|
||||
|
||||
1. **检测技能创建**:
|
||||
- 监听 bash 工具的执行
|
||||
- 检查命令中是否包含 `init_skill.py`
|
||||
- 检查执行是否成功
|
||||
|
||||
2. **自动刷新**:
|
||||
- 调用 `agent.refresh_skills()`
|
||||
- `SkillManager` 重新扫描所有技能目录
|
||||
- 加载新创建的技能
|
||||
|
||||
3. **即时可用**:
|
||||
- 在同一个对话中
|
||||
- 下一轮对话就能使用新技能
|
||||
- 无需重启 Agent ✅
|
||||
|
||||
### 📊 使用效果
|
||||
|
||||
**创建技能的对话**:
|
||||
```
|
||||
用户: 创建一个新技能叫 weather-api
|
||||
|
||||
Agent:
|
||||
第1轮: 使用 bash 工具运行 init_skill.py
|
||||
🔄 Detected skill creation, refreshing skills...
|
||||
✅ Skills refreshed! Now have 2 skills
|
||||
|
||||
第2轮: 回复用户 "技能 weather-api 已创建成功"
|
||||
|
||||
用户: 使用 weather-api 技能查询天气
|
||||
|
||||
Agent:
|
||||
第1轮: ✅ 直接使用 weather-api 技能(无需重启!)
|
||||
```
|
||||
|
||||
### 🔍 刷新范围
|
||||
|
||||
`refresh_skills()` 会重新加载:
|
||||
- ✅ 项目内置技能目录:`项目/skills/`
|
||||
- ✅ 用户工作空间技能:`~/cow/skills/`
|
||||
- ✅ 任何额外配置的技能目录
|
||||
|
||||
### ⚡ 性能影响
|
||||
|
||||
- **触发时机**:只在检测到 `init_skill.py` 执行成功后
|
||||
- **频率**:极低(只有创建新技能时)
|
||||
- **耗时**:< 100ms(扫描和解析 SKILL.md 文件)
|
||||
- **影响**:几乎可以忽略
|
||||
|
||||
### 🐛 边界情况
|
||||
|
||||
1. **技能创建失败**:
|
||||
- `result.status != "success"`
|
||||
- 不会触发刷新
|
||||
- 避免无效刷新
|
||||
|
||||
2. **没有 SkillManager**:
|
||||
- `self.agent.skill_manager` 为 None
|
||||
- 不会触发刷新
|
||||
- 避免空指针异常
|
||||
|
||||
3. **非技能相关的 bash 命令**:
|
||||
- 命令中不包含 `init_skill.py`
|
||||
- 不会触发刷新
|
||||
- 避免不必要的性能开销
|
||||
|
||||
### 🔮 未来改进
|
||||
|
||||
可以扩展到其他场景:
|
||||
|
||||
1. **技能编辑后刷新**:
|
||||
- 检测 `SKILL.md` 被修改
|
||||
- 自动刷新对应的技能
|
||||
|
||||
2. **技能删除后刷新**:
|
||||
- 检测技能目录被删除
|
||||
- 自动移除技能
|
||||
|
||||
3. **热重载模式**:
|
||||
- 文件监听器(watchdog)
|
||||
- 实时检测技能文件变化
|
||||
- 自动刷新
|
||||
|
||||
## 📝 相关代码
|
||||
|
||||
### Agent.refresh_skills()
|
||||
|
||||
```python
|
||||
# agent/protocol/agent.py
|
||||
|
||||
def refresh_skills(self):
|
||||
"""Reload all skills from configured directories."""
|
||||
if self.skill_manager:
|
||||
self.skill_manager.refresh_skills()
|
||||
```
|
||||
|
||||
### SkillManager.refresh_skills()
|
||||
|
||||
```python
|
||||
# agent/skills/manager.py
|
||||
|
||||
def refresh_skills(self):
|
||||
"""Reload all skills from configured directories."""
|
||||
workspace_skills_dir = None
|
||||
if self.workspace_dir:
|
||||
workspace_skills_dir = os.path.join(self.workspace_dir, 'skills')
|
||||
|
||||
self.skills = self.loader.load_all_skills(
|
||||
managed_dir=self.managed_skills_dir,
|
||||
workspace_skills_dir=workspace_skills_dir,
|
||||
extra_dirs=self.extra_dirs,
|
||||
)
|
||||
|
||||
logger.info(f"SkillManager: Loaded {len(self.skills)} skills")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**状态**: ✅ 已实现
|
||||
**测试**: ⏳ 待测试
|
||||
**日期**: 2026-01-30
|
||||
142
skills/skill-creator/BUG_FIX.md
Normal file
142
skills/skill-creator/BUG_FIX.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Bug Fix: Skills 无法从 Workspace 加载
|
||||
|
||||
## 🐛 问题描述
|
||||
|
||||
用户创建的 skills(位于 `~/cow/skills/`)没有被 Agent 加载,只有项目内置的 skills(位于 `项目/skills/`)被加载。
|
||||
|
||||
**症状**:
|
||||
```
|
||||
[INFO] Loaded 1 skills from all sources # 只加载了 skill-creator
|
||||
[INFO] SkillManager: Loaded 1 skills
|
||||
```
|
||||
|
||||
**预期**:
|
||||
```
|
||||
[INFO] Loaded 2 skills from all sources # 应该加载 skill-creator + desktop-explorer
|
||||
[INFO] SkillManager: Loaded 2 skills
|
||||
```
|
||||
|
||||
## 🔍 根因分析
|
||||
|
||||
### 问题定位
|
||||
|
||||
通过逐步调试发现:
|
||||
|
||||
1. **Skills 加载逻辑正确** ✅
|
||||
- `SkillLoader.load_all_skills()` 能正确加载两个目录
|
||||
- `SkillManager` 构造函数正确接收 `workspace_dir` 参数
|
||||
|
||||
2. **Agent 构造函数正确** ✅
|
||||
- `Agent.__init__()` 正确接收 `workspace_dir` 和 `enable_skills` 参数
|
||||
- 能正确创建 `SkillManager`
|
||||
|
||||
3. **`AgentBridge._init_default_agent()` 正确** ✅
|
||||
- 正确读取 `agent_workspace` 配置
|
||||
- 正确调用 `create_agent()` 并传递 `workspace_dir` 等参数
|
||||
|
||||
4. **`AgentBridge.create_agent()` 有问题** ❌
|
||||
- **虽然接收了 `workspace_dir` 等参数(在 `**kwargs` 中)**
|
||||
- **但没有传递给 `Agent` 构造函数!**
|
||||
|
||||
### 问题代码
|
||||
|
||||
```python
|
||||
# bridge/agent_bridge.py:196-203
|
||||
|
||||
def create_agent(self, system_prompt: str, tools: List = None, **kwargs) -> Agent:
|
||||
...
|
||||
self.agent = Agent(
|
||||
system_prompt=system_prompt,
|
||||
description=kwargs.get("description", "AI Super Agent"),
|
||||
model=model,
|
||||
tools=tools,
|
||||
max_steps=kwargs.get("max_steps", 15),
|
||||
output_mode=kwargs.get("output_mode", "logger")
|
||||
# ❌ 缺少: workspace_dir, enable_skills, memory_manager 等参数!
|
||||
)
|
||||
```
|
||||
|
||||
## ✅ 修复方案
|
||||
|
||||
### 修改文件
|
||||
|
||||
`bridge/agent_bridge.py` 的 `create_agent()` 方法
|
||||
|
||||
### 修改内容
|
||||
|
||||
```python
|
||||
def create_agent(self, system_prompt: str, tools: List = None, **kwargs) -> Agent:
|
||||
...
|
||||
self.agent = Agent(
|
||||
system_prompt=system_prompt,
|
||||
description=kwargs.get("description", "AI Super Agent"),
|
||||
model=model,
|
||||
tools=tools,
|
||||
max_steps=kwargs.get("max_steps", 15),
|
||||
output_mode=kwargs.get("output_mode", "logger"),
|
||||
workspace_dir=kwargs.get("workspace_dir"), # ✅ 新增
|
||||
enable_skills=kwargs.get("enable_skills", True), # ✅ 新增
|
||||
memory_manager=kwargs.get("memory_manager"), # ✅ 新增
|
||||
max_context_tokens=kwargs.get("max_context_tokens"), # ✅ 新增
|
||||
context_reserve_tokens=kwargs.get("context_reserve_tokens") # ✅ 新增
|
||||
)
|
||||
|
||||
# ✅ 新增:输出详细的 skills 加载日志
|
||||
if self.agent.skill_manager:
|
||||
logger.info(f"[AgentBridge] SkillManager initialized:")
|
||||
logger.info(f"[AgentBridge] - Managed dir: {self.agent.skill_manager.managed_skills_dir}")
|
||||
logger.info(f"[AgentBridge] - Workspace dir: {self.agent.skill_manager.workspace_dir}")
|
||||
logger.info(f"[AgentBridge] - Total skills: {len(self.agent.skill_manager.skills)}")
|
||||
for skill_name in self.agent.skill_manager.skills.keys():
|
||||
logger.info(f"[AgentBridge] * {skill_name}")
|
||||
|
||||
return self.agent
|
||||
```
|
||||
|
||||
## 📊 修复后的效果
|
||||
|
||||
### 启动日志
|
||||
|
||||
```
|
||||
[INFO][agent_bridge.py:228] - [AgentBridge] Workspace initialized at: /Users/zhayujie/cow
|
||||
[INFO][loader.py:219] - Loaded 2 skills from all sources # ✅ 现在是 2 个
|
||||
[INFO][manager.py:62] - SkillManager: Loaded 2 skills
|
||||
[INFO][agent.py:60] - Initialized SkillManager with 2 skills
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] SkillManager initialized:
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] - Managed dir: /path/to/project/skills
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] - Workspace dir: /Users/zhayujie/cow
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] - Total skills: 2
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] * skill-creator
|
||||
[INFO][agent_bridge.py:xxx] - [AgentBridge] * desktop-explorer
|
||||
```
|
||||
|
||||
### Skills 来源
|
||||
|
||||
| Skill Name | 来源目录 | 说明 |
|
||||
|---|---|---|
|
||||
| `skill-creator` | `项目/skills/` | 项目内置,用于创建新 skills |
|
||||
| `desktop-explorer` | `~/cow/skills/` | 用户创建的 skill |
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
### 问题
|
||||
`create_agent()` 方法没有将 `workspace_dir` 等关键参数传递给 `Agent` 构造函数,导致 Agent 无法加载用户工作空间的 skills。
|
||||
|
||||
### 修复
|
||||
在 `create_agent()` 方法中添加所有必要的参数传递。
|
||||
|
||||
### 影响范围
|
||||
- ✅ Skills 加载
|
||||
- ✅ Memory 管理器传递
|
||||
- ✅ 上下文管理参数传递
|
||||
|
||||
### 测试方法
|
||||
1. 启动 Agent
|
||||
2. 检查日志中是否显示 "Loaded 2 skills from all sources"
|
||||
3. 检查是否列出了 `skill-creator` 和 `desktop-explorer` 两个 skills
|
||||
|
||||
---
|
||||
|
||||
**状态**: ✅ 已修复
|
||||
**测试**: ⏳ 待测试
|
||||
**日期**: 2026-01-30
|
||||
202
skills/skill-creator/LICENSE.txt
Normal file
202
skills/skill-creator/LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
415
skills/skill-creator/SKILL.md
Normal file
415
skills/skill-creator/SKILL.md
Normal file
@@ -0,0 +1,415 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Guide for creating effective skills using available tools (bash, read, write, edit, ls, find). Use when user wants to create, validate, or package a new skill. This skill teaches how to use scripts in skills/skill-creator/scripts/ directory via bash tool to initialize, validate, and package skills.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
This skill provides guidance for creating effective skills using the existing tool system.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular, self-contained packages that extend Claude's capabilities by providing
|
||||
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
|
||||
domains or tasks—they transform Claude from a general-purpose agent into a specialized agent
|
||||
equipped with procedural knowledge that no model can fully possess.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
1. Specialized workflows - Multi-step procedures for specific domains
|
||||
2. Tool integrations - Instructions for working with specific file formats or APIs
|
||||
3. Domain expertise - Company-specific knowledge, schemas, business logic
|
||||
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
|
||||
|
||||
**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"
|
||||
|
||||
Prefer concise examples over verbose explanations.
|
||||
|
||||
### Set Appropriate Degrees of Freedom
|
||||
|
||||
Match the level of specificity to the task's fragility and variability:
|
||||
|
||||
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
|
||||
|
||||
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
|
||||
|
||||
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
|
||||
|
||||
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
|
||||
|
||||
### Anatomy of a Skill
|
||||
|
||||
Every skill consists of a required SKILL.md file and optional bundled resources:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter metadata (required)
|
||||
│ │ ├── name: (required)
|
||||
│ │ └── description: (required)
|
||||
│ └── Markdown instructions (required)
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
||||
├── references/ - Documentation intended to be loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||
```
|
||||
|
||||
#### SKILL.md (required)
|
||||
|
||||
Every SKILL.md consists of:
|
||||
|
||||
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
|
||||
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
|
||||
|
||||
#### Bundled Resources (optional)
|
||||
|
||||
##### Scripts (`scripts/`)
|
||||
|
||||
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
||||
|
||||
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
||||
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
|
||||
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
||||
- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments
|
||||
|
||||
##### References (`references/`)
|
||||
|
||||
Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.
|
||||
|
||||
- **When to include**: For documentation that Claude should reference while working
|
||||
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
|
||||
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
|
||||
- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed
|
||||
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
|
||||
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
|
||||
|
||||
##### Assets (`assets/`)
|
||||
|
||||
Files not intended to be loaded into context, but rather used within the output Claude produces.
|
||||
|
||||
- **When to include**: When the skill needs files that will be used in the final output
|
||||
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
|
||||
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
|
||||
- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context
|
||||
|
||||
#### What to Not Include in a Skill
|
||||
|
||||
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
|
||||
|
||||
- README.md
|
||||
- INSTALLATION_GUIDE.md
|
||||
- QUICK_REFERENCE.md
|
||||
- CHANGELOG.md
|
||||
- etc.
|
||||
|
||||
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
|
||||
|
||||
### Progressive Disclosure Design Principle
|
||||
|
||||
Skills use a three-level loading system to manage context efficiently:
|
||||
|
||||
1. **Metadata (name + description)** - Always in context (~100 words)
|
||||
2. **SKILL.md body** - When skill triggers (<5k words)
|
||||
3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)
|
||||
|
||||
#### Progressive Disclosure Patterns
|
||||
|
||||
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
|
||||
|
||||
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
|
||||
|
||||
**Pattern 1: High-level guide with references**
|
||||
|
||||
```markdown
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Extract text with pdfplumber:
|
||||
[code example]
|
||||
|
||||
## Advanced features
|
||||
|
||||
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
|
||||
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
|
||||
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
|
||||
```
|
||||
|
||||
Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
|
||||
|
||||
**Pattern 2: Domain-specific organization**
|
||||
|
||||
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
|
||||
|
||||
```
|
||||
bigquery-skill/
|
||||
├── SKILL.md (overview and navigation)
|
||||
└── reference/
|
||||
├── finance.md (revenue, billing metrics)
|
||||
├── sales.md (opportunities, pipeline)
|
||||
├── product.md (API usage, features)
|
||||
└── marketing.md (campaigns, attribution)
|
||||
```
|
||||
|
||||
When a user asks about sales metrics, Claude only reads sales.md.
|
||||
|
||||
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
|
||||
|
||||
```
|
||||
cloud-deploy/
|
||||
├── SKILL.md (workflow + provider selection)
|
||||
└── references/
|
||||
├── aws.md (AWS deployment patterns)
|
||||
├── gcp.md (GCP deployment patterns)
|
||||
└── azure.md (Azure deployment patterns)
|
||||
```
|
||||
|
||||
When the user chooses AWS, Claude only reads aws.md.
|
||||
|
||||
**Pattern 3: Conditional details**
|
||||
|
||||
Show basic content, link to advanced content:
|
||||
|
||||
```markdown
|
||||
# DOCX Processing
|
||||
|
||||
## Creating documents
|
||||
|
||||
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
|
||||
|
||||
## Editing documents
|
||||
|
||||
For simple edits, modify the XML directly.
|
||||
|
||||
**For tracked changes**: See [REDLINING.md](REDLINING.md)
|
||||
**For OOXML details**: See [OOXML.md](OOXML.md)
|
||||
```
|
||||
|
||||
Claude reads REDLINING.md or OOXML.md only when the user needs those features.
|
||||
|
||||
**Important guidelines:**
|
||||
|
||||
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
|
||||
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
Skill creation involves these steps:
|
||||
|
||||
1. Understand the skill with concrete examples
|
||||
2. Plan reusable skill contents (scripts, references, assets)
|
||||
3. Initialize the skill (use bash tool to run init_skill.py script)
|
||||
4. Edit the skill (use write/edit tools to implement SKILL.md and resources)
|
||||
5. Validate the skill (use bash tool to run quick_validate.py script)
|
||||
6. Package the skill (use bash tool to run package_skill.py script)
|
||||
7. Iterate based on real usage
|
||||
|
||||
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
||||
|
||||
### Using Tools in This Skill
|
||||
|
||||
This skill requires the following tools to be available:
|
||||
- **bash** - To run Python scripts in skills/skill-creator/scripts/
|
||||
- **read** - To read existing skill files
|
||||
- **write** - To create new skill files
|
||||
- **edit** - To modify skill files
|
||||
- **ls** - To list files in skill directories
|
||||
- **find** - To search for skill files
|
||||
|
||||
All scripts are located in: `skills/skill-creator/scripts/`
|
||||
|
||||
### Step 1: Understanding the Skill with Concrete Examples
|
||||
|
||||
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
|
||||
|
||||
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
|
||||
|
||||
For example, when building an image-editor skill, relevant questions include:
|
||||
|
||||
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
|
||||
- "Can you give some examples of how this skill would be used?"
|
||||
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
|
||||
- "What would a user say that should trigger this skill?"
|
||||
|
||||
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
|
||||
|
||||
Conclude this step when there is a clear sense of the functionality the skill should support.
|
||||
|
||||
### Step 2: Planning the Reusable Skill Contents
|
||||
|
||||
To turn concrete examples into an effective skill, analyze each example by:
|
||||
|
||||
1. Considering how to execute on the example from scratch
|
||||
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
|
||||
|
||||
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
||||
|
||||
1. Rotating a PDF requires re-writing the same code each time
|
||||
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
|
||||
|
||||
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
||||
|
||||
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
|
||||
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
|
||||
|
||||
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
|
||||
|
||||
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
|
||||
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
|
||||
|
||||
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initializing the Skill
|
||||
|
||||
At this point, it is time to actually create the skill.
|
||||
|
||||
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
|
||||
|
||||
When creating a new skill from scratch, use the bash tool to run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
||||
|
||||
**Using bash tool to initialize a skill:**
|
||||
|
||||
The skill should be created in the agent's workspace skills directory (configured as `agent_workspace` in config, default: `~/cow`).
|
||||
|
||||
Since the bash tool's working directory may be set to the workspace, you need to use the absolute path to the init_skill.py script. The script is located in the project's `skills/skill-creator/scripts/` directory.
|
||||
|
||||
**Option 1: Find and use the script (recommended):**
|
||||
```bash
|
||||
find ~ -name "init_skill.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} <skill-name> --path ~/cow/skills
|
||||
```
|
||||
|
||||
**Option 2: If you know the project path:**
|
||||
```bash
|
||||
python3 /path/to/project/skills/skill-creator/scripts/init_skill.py <skill-name> --path ~/cow/skills
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
find ~ -name "init_skill.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} weather-api --path ~/cow/skills
|
||||
```
|
||||
|
||||
**Note**: The path `~/cow/skills` is the default workspace skills directory. This ensures skills are created in the user's workspace, not the project directory.
|
||||
|
||||
The script:
|
||||
|
||||
- Creates the skill directory at the specified path (~/cow/skills/<skill-name>/ by default)
|
||||
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
|
||||
- Creates example resource directories: `scripts/`, `references/`, and `assets/`
|
||||
- Adds example files in each directory that can be customized or deleted
|
||||
|
||||
After initialization, use read/write/edit tools to customize the generated SKILL.md and example files as needed.
|
||||
|
||||
**Important**: Always create skills in the workspace directory (`~/cow/skills`), not in the project directory. This keeps user-created skills separate from bundled skills.
|
||||
|
||||
**CRITICAL - User Communication**: After successfully creating a skill, you MUST inform the user with a clear, friendly confirmation message that includes:
|
||||
1. ✅ Confirmation that the skill was created successfully
|
||||
2. 📍 The skill name (e.g., "weather-api")
|
||||
3. 📂 Where it was saved (e.g., "~/cow/skills/weather-api")
|
||||
4. 🚀 That the skill is now immediately available for use in the next conversation (no restart needed thanks to auto-reload!)
|
||||
5. 📝 Next steps: The user should edit the SKILL.md to complete the TODO sections
|
||||
|
||||
Example response: "✅ Successfully created skill 'weather-api' at ~/cow/skills/weather-api! The skill is now immediately available for use. Next, you should edit the SKILL.md file to complete the description and add the actual implementation."
|
||||
|
||||
### Step 4: Edit the Skill
|
||||
|
||||
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
|
||||
|
||||
#### Learn Proven Design Patterns
|
||||
|
||||
Consult these helpful guides based on your skill's needs:
|
||||
|
||||
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
|
||||
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
|
||||
|
||||
These files contain established best practices for effective skill design.
|
||||
|
||||
#### Start with Reusable Skill Contents
|
||||
|
||||
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
|
||||
|
||||
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
|
||||
|
||||
Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
|
||||
|
||||
#### Update SKILL.md
|
||||
|
||||
**Writing Guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
##### Frontmatter
|
||||
|
||||
Write the YAML frontmatter with `name` and `description`:
|
||||
|
||||
- `name`: The skill name
|
||||
- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
|
||||
- Include both what the Skill does and specific triggers/contexts for when to use it.
|
||||
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
|
||||
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
|
||||
|
||||
Do not include any other fields in YAML frontmatter.
|
||||
|
||||
##### Body
|
||||
|
||||
Write instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Validating a Skill
|
||||
|
||||
Before packaging, validate the skill to ensure it meets all requirements.
|
||||
|
||||
**Using bash tool to validate a skill:**
|
||||
|
||||
```bash
|
||||
find ~ -name "quick_validate.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} ~/cow/skills/<skill-name>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
find ~ -name "quick_validate.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} ~/cow/skills/weather-api
|
||||
```
|
||||
|
||||
The validation checks:
|
||||
- YAML frontmatter format and required fields (name, description)
|
||||
- Skill naming conventions (hyphen-case, lowercase)
|
||||
- Description completeness and quality
|
||||
- File organization
|
||||
|
||||
If validation fails, use the edit tool to fix the errors in SKILL.md, then validate again.
|
||||
|
||||
### Step 6: Packaging a Skill
|
||||
|
||||
Once development of the skill is complete and validated, package it into a distributable .skill file.
|
||||
|
||||
**Using bash tool to package a skill:**
|
||||
|
||||
```bash
|
||||
find ~ -name "package_skill.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} ~/cow/skills/<skill-name>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
find ~ -name "package_skill.py" -path "*/skills/skill-creator/scripts/*" 2>/dev/null | head -1 | xargs -I {} python3 {} ~/cow/skills/weather-api
|
||||
```
|
||||
|
||||
The packaging script will:
|
||||
|
||||
1. **Validate** the skill automatically (same as Step 5)
|
||||
2. **Package** the skill if validation passes, creating a .skill file (e.g., `weather-api.skill`) that includes all files. The .skill file is a zip file with a .skill extension.
|
||||
|
||||
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
|
||||
|
||||
### Step 7: Iterate
|
||||
|
||||
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
|
||||
|
||||
**Iteration workflow:**
|
||||
|
||||
1. Use the skill on real tasks
|
||||
2. Notice struggles or inefficiencies
|
||||
3. Identify how SKILL.md or bundled resources should be updated
|
||||
4. Implement changes and test again
|
||||
228
skills/skill-creator/USAGE_GUIDE.md
Normal file
228
skills/skill-creator/USAGE_GUIDE.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Skill Creator 使用指南(修订版)
|
||||
|
||||
## 🎯 什么是 skill-creator?
|
||||
|
||||
**skill-creator 是一个 Skill(技能),而不是 Tool(工具)**
|
||||
|
||||
- 它教 Agent 如何使用现有工具(bash, read, write, edit)来创建新技能
|
||||
- 它提供了完整的技能创建工作流指导
|
||||
- Agent 会组合使用多个工具来完成技能创建任务
|
||||
|
||||
## 📚 Skills vs Tools
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **Tool(工具)** | 底层能力,执行单一操作 | bash, read, write, calculator |
|
||||
| **Skill(技能)** | 工作流指导,组合多个工具 | skill-creator, code-reviewer |
|
||||
|
||||
## 🚀 如何使用
|
||||
|
||||
### 触发条件
|
||||
|
||||
当用户提到以下关键词时,skill-creator 技能会被触发:
|
||||
- "创建技能"
|
||||
- "新建 skill"
|
||||
- "创建一个新技能"
|
||||
- "initialize a skill"
|
||||
|
||||
### 完整示例
|
||||
|
||||
```
|
||||
用户: "创建一个新技能叫 weather-api"
|
||||
|
||||
Agent 执行流程:
|
||||
1. 加载 skill-creator 技能
|
||||
2. 阅读 SKILL.md 了解工作流
|
||||
3. 使用 bash 工具:
|
||||
cd skills/skill-creator && python3 scripts/init_skill.py weather-api --path ../../workspace/skills
|
||||
4. 报告成功 ✅
|
||||
|
||||
用户: "完善这个技能,用于调用天气 API 获取天气数据"
|
||||
|
||||
Agent 执行流程:
|
||||
1. 使用 read 工具读取 workspace/skills/weather-api/SKILL.md
|
||||
2. 使用 edit 工具修改 description 和内容
|
||||
3. 报告完成 ✅
|
||||
|
||||
用户: "验证这个技能"
|
||||
|
||||
Agent 执行流程:
|
||||
1. 使用 bash 工具:
|
||||
cd skills/skill-creator && python3 scripts/quick_validate.py ../../workspace/skills/weather-api
|
||||
2. 报告验证结果 ✅
|
||||
|
||||
用户: "打包这个技能"
|
||||
|
||||
Agent 执行流程:
|
||||
1. 使用 bash 工具:
|
||||
cd skills/skill-creator && python3 scripts/package_skill.py ../../workspace/skills/weather-api
|
||||
2. 生成 weather-api.skill 文件 ✅
|
||||
```
|
||||
|
||||
## 🔄 工作流程
|
||||
|
||||
### 1. 创建技能模板
|
||||
```
|
||||
说法: "创建新技能 desktop-viewer"
|
||||
Agent: 运行 init_skill.py 脚本
|
||||
结果: workspace/skills/desktop-viewer/ 目录创建
|
||||
```
|
||||
|
||||
### 2. 编辑技能内容
|
||||
```
|
||||
说法: "完善 SKILL.md 的描述"
|
||||
Agent: 使用 write/edit 工具修改
|
||||
结果: SKILL.md 更新
|
||||
```
|
||||
|
||||
### 3. 添加资源文件(可选)
|
||||
```
|
||||
说法: "在 scripts 目录创建 xxx.py"
|
||||
Agent: 使用 write 工具创建文件
|
||||
结果: 脚本文件创建
|
||||
```
|
||||
|
||||
### 4. 验证技能
|
||||
```
|
||||
说法: "验证 desktop-viewer 技能"
|
||||
Agent: 运行 quick_validate.py 脚本
|
||||
结果: 显示验证结果
|
||||
```
|
||||
|
||||
### 5. 打包技能
|
||||
```
|
||||
说法: "打包 desktop-viewer 技能"
|
||||
Agent: 运行 package_skill.py 脚本
|
||||
结果: desktop-viewer.skill 文件生成
|
||||
```
|
||||
|
||||
## ✅ 推荐的提问方式
|
||||
|
||||
### 创建技能
|
||||
✅ "创建一个新技能叫 XXX"
|
||||
✅ "初始化技能 XXX"
|
||||
✅ "新建 skill: XXX"
|
||||
|
||||
### 编辑技能
|
||||
✅ "完善 XXX 技能的描述"
|
||||
✅ "在 XXX 技能的 scripts 目录创建文件"
|
||||
✅ "修改 XXX 技能的 SKILL.md"
|
||||
|
||||
### 验证技能
|
||||
✅ "验证 XXX 技能"
|
||||
✅ "检查 XXX 技能的格式"
|
||||
|
||||
### 打包技能
|
||||
✅ "打包 XXX 技能"
|
||||
✅ "导出 XXX skill"
|
||||
|
||||
## ❌ 避免的说法
|
||||
|
||||
❌ "帮我写一个查看桌面文件的功能"
|
||||
- 问题:太具体,Agent 会直接写代码而不是创建技能
|
||||
|
||||
❌ "做一个脚本来..."
|
||||
- 问题:Agent 会直接写脚本而不是创建技能
|
||||
|
||||
✅ **正确方式**:
|
||||
1. 先说:"创建技能 desktop-viewer"
|
||||
2. 再说:"这个技能用于查看桌面文件"
|
||||
|
||||
## 🔍 如何确认技能已加载?
|
||||
|
||||
查看 Agent 启动日志:
|
||||
```
|
||||
[INFO] Loaded X skills from all sources
|
||||
[INFO] SkillManager: Loaded X skills
|
||||
```
|
||||
|
||||
或者直接问:
|
||||
```
|
||||
"列出所有已加载的 skills"
|
||||
```
|
||||
|
||||
## 📂 技能存放位置
|
||||
|
||||
- **创建的新技能**: `workspace/skills/<skill-name>/`
|
||||
- **skill-creator 本身**: `skills/skill-creator/`
|
||||
- **打包后的文件**: 项目根目录下的 `<skill-name>.skill`
|
||||
|
||||
## 💡 实用技巧
|
||||
|
||||
### 1. 分步操作
|
||||
不要一次性说太多,分步骤进行:
|
||||
```
|
||||
步骤 1: "创建技能 pdf-processor"
|
||||
步骤 2: "添加描述:用于处理 PDF 文件"
|
||||
步骤 3: "创建脚本 extract_text.py"
|
||||
步骤 4: "验证技能"
|
||||
步骤 5: "打包技能"
|
||||
```
|
||||
|
||||
### 2. 明确技能名称
|
||||
使用 hyphen-case 格式:
|
||||
✅ weather-api
|
||||
✅ pdf-processor
|
||||
✅ file-manager
|
||||
|
||||
❌ weather api(有空格)
|
||||
❌ WeatherAPI(驼峰)
|
||||
❌ weather_api(下划线)
|
||||
|
||||
### 3. 查看创建的文件
|
||||
```
|
||||
"列出 workspace/skills/XXX 目录的内容"
|
||||
"读取 workspace/skills/XXX/SKILL.md"
|
||||
```
|
||||
|
||||
## 🛠️ 直接运行脚本(备选)
|
||||
|
||||
如果需要直接运行脚本:
|
||||
|
||||
```bash
|
||||
# 创建技能
|
||||
cd skills/skill-creator
|
||||
python3 scripts/init_skill.py my-skill --path ../../workspace/skills
|
||||
|
||||
# 验证技能
|
||||
python3 scripts/quick_validate.py ../../workspace/skills/my-skill
|
||||
|
||||
# 打包技能
|
||||
python3 scripts/package_skill.py ../../workspace/skills/my-skill
|
||||
```
|
||||
|
||||
## 📖 技能开发流程
|
||||
|
||||
```
|
||||
1. 规划 → 确定技能功能
|
||||
2. 创建 → 使用 init_skill.py 生成模板
|
||||
3. 编辑 → 完善 SKILL.md 和资源文件
|
||||
4. 验证 → 使用 quick_validate.py 检查格式
|
||||
5. 测试 → 在 Agent 中加载并测试
|
||||
6. 打包 → 使用 package_skill.py 生成 .skill 文件
|
||||
7. 分享 → 将 .skill 文件分享给其他用户
|
||||
```
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: Agent 没有使用 skill-creator?
|
||||
**A**:
|
||||
1. 确认技能已加载(查看日志)
|
||||
2. 使用明确的触发词:"创建技能 XXX"
|
||||
3. 不要在一句话中混入太多其他信息
|
||||
|
||||
### Q: 技能创建在哪里?
|
||||
**A**: `workspace/skills/<技能名>/`
|
||||
|
||||
### Q: .skill 文件是什么?
|
||||
**A**: 是 zip 格式的压缩包,包含技能的所有文件,可以分享和安装
|
||||
|
||||
### Q: 如何安装别人的 .skill 文件?
|
||||
**A**: 解压到 `workspace/skills/` 目录
|
||||
|
||||
### Q: skill-creator 本身也是技能吗?
|
||||
**A**: 是的!它是一个教 Agent 如何创建其他技能的技能
|
||||
|
||||
---
|
||||
|
||||
**记住**: skill-creator 是一个 **Skill(指导方案)**,而不是 **Tool(工具)**!
|
||||
BIN
skills/skill-creator/desktop-viewer.skill
Normal file
BIN
skills/skill-creator/desktop-viewer.skill
Normal file
Binary file not shown.
82
skills/skill-creator/references/output-patterns.md
Normal file
82
skills/skill-creator/references/output-patterns.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Output Patterns
|
||||
|
||||
Use these patterns when skills need to produce consistent, high-quality output.
|
||||
|
||||
## Template Pattern
|
||||
|
||||
Provide templates for output format. Match the level of strictness to your needs.
|
||||
|
||||
**For strict requirements (like API responses or data formats):**
|
||||
|
||||
```markdown
|
||||
## Report structure
|
||||
|
||||
ALWAYS use this exact template structure:
|
||||
|
||||
# [Analysis Title]
|
||||
|
||||
## Executive summary
|
||||
[One-paragraph overview of key findings]
|
||||
|
||||
## Key findings
|
||||
- Finding 1 with supporting data
|
||||
- Finding 2 with supporting data
|
||||
- Finding 3 with supporting data
|
||||
|
||||
## Recommendations
|
||||
1. Specific actionable recommendation
|
||||
2. Specific actionable recommendation
|
||||
```
|
||||
|
||||
**For flexible guidance (when adaptation is useful):**
|
||||
|
||||
```markdown
|
||||
## Report structure
|
||||
|
||||
Here is a sensible default format, but use your best judgment:
|
||||
|
||||
# [Analysis Title]
|
||||
|
||||
## Executive summary
|
||||
[Overview]
|
||||
|
||||
## Key findings
|
||||
[Adapt sections based on what you discover]
|
||||
|
||||
## Recommendations
|
||||
[Tailor to the specific context]
|
||||
|
||||
Adjust sections as needed for the specific analysis type.
|
||||
```
|
||||
|
||||
## Examples Pattern
|
||||
|
||||
For skills where output quality depends on seeing examples, provide input/output pairs:
|
||||
|
||||
```markdown
|
||||
## Commit message format
|
||||
|
||||
Generate commit messages following these examples:
|
||||
|
||||
**Example 1:**
|
||||
Input: Added user authentication with JWT tokens
|
||||
Output:
|
||||
```
|
||||
feat(auth): implement JWT-based authentication
|
||||
|
||||
Add login endpoint and token validation middleware
|
||||
```
|
||||
|
||||
**Example 2:**
|
||||
Input: Fixed bug where dates displayed incorrectly in reports
|
||||
Output:
|
||||
```
|
||||
fix(reports): correct date formatting in timezone conversion
|
||||
|
||||
Use UTC timestamps consistently across report generation
|
||||
```
|
||||
|
||||
Follow this style: type(scope): brief description, then detailed explanation.
|
||||
```
|
||||
|
||||
Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.
|
||||
28
skills/skill-creator/references/workflows.md
Normal file
28
skills/skill-creator/references/workflows.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Workflow Patterns
|
||||
|
||||
## Sequential Workflows
|
||||
|
||||
For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
|
||||
|
||||
```markdown
|
||||
Filling a PDF form involves these steps:
|
||||
|
||||
1. Analyze the form (run analyze_form.py)
|
||||
2. Create field mapping (edit fields.json)
|
||||
3. Validate mapping (run validate_fields.py)
|
||||
4. Fill the form (run fill_form.py)
|
||||
5. Verify output (run verify_output.py)
|
||||
```
|
||||
|
||||
## Conditional Workflows
|
||||
|
||||
For tasks with branching logic, guide Claude through decision points:
|
||||
|
||||
```markdown
|
||||
1. Determine the modification type:
|
||||
**Creating new content?** → Follow "Creation workflow" below
|
||||
**Editing existing content?** → Follow "Editing workflow" below
|
||||
|
||||
2. Creation workflow: [steps]
|
||||
3. Editing workflow: [steps]
|
||||
```
|
||||
303
skills/skill-creator/scripts/init_skill.py
Executable file
303
skills/skill-creator/scripts/init_skill.py
Executable file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Initializer - Creates a new skill from template
|
||||
|
||||
Usage:
|
||||
init_skill.py <skill-name> --path <path>
|
||||
|
||||
Examples:
|
||||
init_skill.py my-new-skill --path skills/public
|
||||
init_skill.py my-api-helper --path skills/private
|
||||
init_skill.py custom-skill --path /custom/location
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKILL_TEMPLATE = """---
|
||||
name: {skill_name}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
|
||||
- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
|
||||
- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
|
||||
- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" → numbered capability list
|
||||
- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources
|
||||
|
||||
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
|
||||
|
||||
### scripts/
|
||||
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
**Examples from other skills:**
|
||||
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
|
||||
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
|
||||
|
||||
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
|
||||
|
||||
**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform Claude's process and thinking.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
|
||||
- BigQuery: API reference documentation and query examples
|
||||
- Finance: Schema documentation, company policies
|
||||
|
||||
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output Claude produces.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Brand styling: PowerPoint template files (.pptx), logo files
|
||||
- Frontend builder: HTML/React boilerplate project directories
|
||||
- Typography: Font files (.ttf, .woff2)
|
||||
|
||||
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
||||
|
||||
---
|
||||
|
||||
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
|
||||
"""
|
||||
|
||||
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
|
||||
"""
|
||||
Example helper script for {skill_name}
|
||||
|
||||
This is a placeholder script that can be executed directly.
|
||||
Replace with actual implementation or delete if not needed.
|
||||
|
||||
Example real scripts from other skills:
|
||||
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
|
||||
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("This is an example script for {skill_name}")
|
||||
# TODO: Add actual script logic here
|
||||
# This could be data processing, file conversion, API calls, etc.
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
Example real reference docs from other skills:
|
||||
- product-management/references/communication.md - Comprehensive guide for status updates
|
||||
- product-management/references/context_building.md - Deep-dive on gathering context
|
||||
- bigquery/references/ - API references and query examples
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
|
||||
## Structure Suggestions
|
||||
|
||||
### API Reference Example
|
||||
- Overview
|
||||
- Authentication
|
||||
- Endpoints with examples
|
||||
- Error codes
|
||||
- Rate limits
|
||||
|
||||
### Workflow Guide Example
|
||||
- Prerequisites
|
||||
- Step-by-step instructions
|
||||
- Common patterns
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
"""
|
||||
|
||||
EXAMPLE_ASSET = """# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output Claude produces.
|
||||
|
||||
Example asset files from other skills:
|
||||
- Brand guidelines: logo.png, slides_template.pptx
|
||||
- Frontend builder: hello-world/ directory with HTML/React boilerplate
|
||||
- Typography: custom-font.ttf, font-family.woff2
|
||||
- Data: sample_data.csv, test_dataset.json
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
- Icons: .ico, .svg
|
||||
- Data files: .csv, .json, .xml, .yaml
|
||||
|
||||
Note: This is a text placeholder. Actual assets can be any file type.
|
||||
"""
|
||||
|
||||
|
||||
def title_case_skill_name(skill_name):
|
||||
"""Convert hyphenated skill name to Title Case for display."""
|
||||
return ' '.join(word.capitalize() for word in skill_name.split('-'))
|
||||
|
||||
|
||||
def init_skill(skill_name, path):
|
||||
"""
|
||||
Initialize a new skill directory with template SKILL.md.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
path: Path where the skill directory should be created
|
||||
|
||||
Returns:
|
||||
Path to created skill directory, or None if error
|
||||
"""
|
||||
# Determine skill directory path
|
||||
skill_dir = Path(path).resolve() / skill_name
|
||||
|
||||
# Check if directory already exists
|
||||
if skill_dir.exists():
|
||||
print(f"❌ Error: Skill directory already exists: {skill_dir}")
|
||||
return None
|
||||
|
||||
# Create skill directory
|
||||
try:
|
||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||
print(f"✅ Created skill directory: {skill_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating directory: {e}")
|
||||
return None
|
||||
|
||||
# Create SKILL.md from template
|
||||
skill_title = title_case_skill_name(skill_name)
|
||||
skill_content = SKILL_TEMPLATE.format(
|
||||
skill_name=skill_name,
|
||||
skill_title=skill_title
|
||||
)
|
||||
|
||||
skill_md_path = skill_dir / 'SKILL.md'
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
print("✅ Created SKILL.md")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating SKILL.md: {e}")
|
||||
return None
|
||||
|
||||
# Create resource directories with example files
|
||||
try:
|
||||
# Create scripts/ directory with example script
|
||||
scripts_dir = skill_dir / 'scripts'
|
||||
scripts_dir.mkdir(exist_ok=True)
|
||||
example_script = scripts_dir / 'example.py'
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.chmod(0o755)
|
||||
print("✅ Created scripts/example.py")
|
||||
|
||||
# Create references/ directory with example reference doc
|
||||
references_dir = skill_dir / 'references'
|
||||
references_dir.mkdir(exist_ok=True)
|
||||
example_reference = references_dir / 'api_reference.md'
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
print("✅ Created references/api_reference.md")
|
||||
|
||||
# Create assets/ directory with example asset placeholder
|
||||
assets_dir = skill_dir / 'assets'
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
example_asset = assets_dir / 'example_asset.txt'
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
print("✅ Created assets/example_asset.txt")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating resource directories: {e}")
|
||||
return None
|
||||
|
||||
# Print next steps
|
||||
print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}")
|
||||
print("\nNext steps:")
|
||||
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
||||
print("3. Run the validator when ready to check the skill structure")
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4 or sys.argv[2] != '--path':
|
||||
print("Usage: init_skill.py <skill-name> --path <path>")
|
||||
print("\nSkill name requirements:")
|
||||
print(" - Hyphen-case identifier (e.g., 'data-analyzer')")
|
||||
print(" - Lowercase letters, digits, and hyphens only")
|
||||
print(" - Max 40 characters")
|
||||
print(" - Must match directory name exactly")
|
||||
print("\nExamples:")
|
||||
print(" init_skill.py my-new-skill --path workspace/skills")
|
||||
print(" init_skill.py my-api-helper --path /path/to/skills")
|
||||
print(" init_skill.py custom-skill --path /custom/location")
|
||||
sys.exit(1)
|
||||
|
||||
skill_name = sys.argv[1]
|
||||
path = sys.argv[3]
|
||||
|
||||
print(f"🚀 Initializing skill: {skill_name}")
|
||||
print(f" Location: {path}")
|
||||
print()
|
||||
|
||||
result = init_skill(skill_name, path)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
116
skills/skill-creator/scripts/package_skill.py
Executable file
116
skills/skill-creator/scripts/package_skill.py
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Packager - Creates a distributable .skill file of a skill folder
|
||||
|
||||
Usage:
|
||||
python utils/package_skill.py <path/to/skill-folder> [output-directory]
|
||||
|
||||
Example:
|
||||
python utils/package_skill.py skills/public/my-skill
|
||||
python utils/package_skill.py skills/public/my-skill ./dist
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
# Add script directory to path for imports
|
||||
script_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(script_dir))
|
||||
|
||||
from quick_validate import validate_skill
|
||||
|
||||
|
||||
def package_skill(skill_path, output_dir=None):
|
||||
"""
|
||||
Package a skill folder into a .skill file.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill folder
|
||||
output_dir: Optional output directory for the .skill file (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Path to the created .skill file, or None if error
|
||||
"""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
# Validate skill folder exists
|
||||
if not skill_path.exists():
|
||||
print(f"❌ Error: Skill folder not found: {skill_path}")
|
||||
return None
|
||||
|
||||
if not skill_path.is_dir():
|
||||
print(f"❌ Error: Path is not a directory: {skill_path}")
|
||||
return None
|
||||
|
||||
# Validate SKILL.md exists
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"❌ Error: SKILL.md not found in {skill_path}")
|
||||
return None
|
||||
|
||||
# Run validation before packaging
|
||||
print("🔍 Validating skill...")
|
||||
valid, message = validate_skill(skill_path)
|
||||
if not valid:
|
||||
print(f"❌ Validation failed: {message}")
|
||||
print(" Please fix the validation errors before packaging.")
|
||||
return None
|
||||
print(f"✅ {message}\n")
|
||||
|
||||
# Determine output location
|
||||
skill_name = skill_path.name
|
||||
if output_dir:
|
||||
output_path = Path(output_dir).resolve()
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
output_path = Path.cwd()
|
||||
|
||||
skill_filename = output_path / f"{skill_name}.skill"
|
||||
|
||||
# Create the .skill file (zip format)
|
||||
try:
|
||||
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
# Walk through the skill directory
|
||||
for file_path in skill_path.rglob('*'):
|
||||
if file_path.is_file():
|
||||
# Calculate the relative path within the zip
|
||||
arcname = file_path.relative_to(skill_path.parent)
|
||||
zipf.write(file_path, arcname)
|
||||
print(f" Added: {arcname}")
|
||||
|
||||
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
|
||||
return skill_filename
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating .skill file: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
|
||||
print("\nExample:")
|
||||
print(" python utils/package_skill.py skills/public/my-skill")
|
||||
print(" python utils/package_skill.py skills/public/my-skill ./dist")
|
||||
sys.exit(1)
|
||||
|
||||
skill_path = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f"📦 Packaging skill: {skill_path}")
|
||||
if output_dir:
|
||||
print(f" Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
result = package_skill(skill_path, output_dir)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
95
skills/skill-creator/scripts/quick_validate.py
Executable file
95
skills/skill-creator/scripts/quick_validate.py
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick validation script for skills - minimal version
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Basic validation of a skill"""
|
||||
skill_path = Path(skill_path)
|
||||
|
||||
# Check SKILL.md exists
|
||||
skill_md = skill_path / 'SKILL.md'
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
# Read and validate frontmatter
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith('---'):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
# Extract frontmatter
|
||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
||||
if not match:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
# Parse YAML frontmatter
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False, "Frontmatter must be a YAML dictionary"
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML in frontmatter: {e}"
|
||||
|
||||
# Define allowed properties
|
||||
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}
|
||||
|
||||
# Check for unexpected properties (excluding nested keys under metadata)
|
||||
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
|
||||
if unexpected_keys:
|
||||
return False, (
|
||||
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
|
||||
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
|
||||
)
|
||||
|
||||
# Check required fields
|
||||
if 'name' not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if 'description' not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
# Extract name for validation
|
||||
name = frontmatter.get('name', '')
|
||||
if not isinstance(name, str):
|
||||
return False, f"Name must be a string, got {type(name).__name__}"
|
||||
name = name.strip()
|
||||
if name:
|
||||
# Check naming convention (hyphen-case: lowercase with hyphens)
|
||||
if not re.match(r'^[a-z0-9-]+$', name):
|
||||
return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)"
|
||||
if name.startswith('-') or name.endswith('-') or '--' in name:
|
||||
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
|
||||
# Check name length (max 64 characters per spec)
|
||||
if len(name) > 64:
|
||||
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
|
||||
|
||||
# Extract and validate description
|
||||
description = frontmatter.get('description', '')
|
||||
if not isinstance(description, str):
|
||||
return False, f"Description must be a string, got {type(description).__name__}"
|
||||
description = description.strip()
|
||||
if description:
|
||||
# Check for angle brackets
|
||||
if '<' in description or '>' in description:
|
||||
return False, "Description cannot contain angle brackets (< or >)"
|
||||
# Check description length (max 1024 characters per spec)
|
||||
if len(description) > 1024:
|
||||
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python quick_validate.py <skill_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
Reference in New Issue
Block a user