convert_to_qoder.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. #!/usr/bin/env python3
  2. """
  3. Convert Agent Skills to Qoder Agent format
  4. This script converts Agent Skills to Qoder agent module format.
  5. Qoder is an agentic coding platform that supports agent-based automation.
  6. Usage:
  7. python convert_to_qoder.py <skill_path> [output_path]
  8. Example:
  9. python convert_to_qoder.py ../../skills/course-designer qoder-agents/
  10. """
  11. import sys
  12. import json
  13. import re
  14. import shutil
  15. from pathlib import Path
  16. try:
  17. import yaml
  18. HAS_YAML = True
  19. except ImportError:
  20. HAS_YAML = False
  21. def parse_yaml_simple(text):
  22. """Simple YAML parser for frontmatter (handles basic key: value pairs)"""
  23. result = {}
  24. for line in text.split('\n'):
  25. line = line.strip()
  26. if ':' in line and not line.startswith('#'):
  27. key, value = line.split(':', 1)
  28. key = key.strip()
  29. value = value.strip().strip('"').strip("'")
  30. result[key] = value
  31. return result
  32. def extract_frontmatter(content):
  33. """Extract YAML frontmatter from SKILL.md"""
  34. frontmatter_pattern = r'^---\n(.*?)\n---\n'
  35. match = re.match(frontmatter_pattern, content, re.DOTALL)
  36. if match:
  37. frontmatter_text = match.group(1)
  38. if HAS_YAML:
  39. frontmatter = yaml.safe_load(frontmatter_text)
  40. else:
  41. frontmatter = parse_yaml_simple(frontmatter_text)
  42. body_start = match.end()
  43. body = content[body_start:].strip()
  44. return frontmatter, body
  45. return {}, content.strip()
  46. def rewrite_markdown_links(content, skill_name, resource_dirs):
  47. """
  48. Rewrite markdown links to point to assets directory
  49. Args:
  50. content: Markdown content
  51. skill_name: Name of the skill
  52. resource_dirs: List of resource directories that were copied
  53. Returns:
  54. Updated content
  55. """
  56. def replace_link(match):
  57. text = match.group(1)
  58. url = match.group(2)
  59. # Skip absolute links, anchors, or existing asset links
  60. if url.startswith(('http://', 'https://', '#', '/')):
  61. return match.group(0)
  62. # Check if link points to a resource directory
  63. for res_dir in resource_dirs:
  64. if url.startswith(res_dir + '/') or url == res_dir:
  65. return f"[{text}](assets/{skill_name}/{url})"
  66. return match.group(0)
  67. # Replace [text](url) links
  68. content = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', replace_link, content)
  69. # Replace `path` references (common in our skills)
  70. def replace_code_path(match):
  71. path = match.group(1)
  72. # Check if path points to a resource directory
  73. for res_dir in resource_dirs:
  74. if path.startswith(res_dir + '/') or path == res_dir:
  75. return f"`assets/{skill_name}/{path}`"
  76. return match.group(0)
  77. content = re.sub(r'`([^`\n]+)`', replace_code_path, content)
  78. return content
  79. def extract_workflow_steps(body):
  80. """Extract workflow steps from skill body"""
  81. steps = []
  82. # Look for numbered lists or workflow sections
  83. lines = body.split('\n')
  84. current_step = None
  85. for line in lines:
  86. # Match numbered list items
  87. match = re.match(r'^(\d+)\.\s+(.+)$', line.strip())
  88. if match:
  89. step_num = match.group(1)
  90. step_desc = match.group(2)
  91. steps.append({
  92. "step": f"step-{step_num}",
  93. "description": step_desc
  94. })
  95. # Match ## sections as workflow steps
  96. elif line.startswith('## ') and '流程' in line or 'workflow' in line.lower():
  97. current_step = line.replace('##', '').strip()
  98. return steps if steps else [
  99. {"step": "analyze", "description": "Analyze requirements"},
  100. {"step": "execute", "description": "Execute skill task"},
  101. {"step": "output", "description": "Generate output"}
  102. ]
  103. def convert_to_qoder_agent(skill_path, output_dir=None):
  104. """
  105. Convert Agent Skill to Qoder agent format
  106. Args:
  107. skill_path: Path to skill directory containing SKILL.md
  108. output_dir: Output directory for agent (optional)
  109. """
  110. skill_path = Path(skill_path)
  111. skill_file = skill_path / "SKILL.md"
  112. if not skill_file.exists():
  113. print(f"Error: SKILL.md not found in {skill_path}")
  114. return None
  115. # Read skill file
  116. with open(skill_file, "r", encoding="utf-8") as f:
  117. content = f.read()
  118. # Extract frontmatter and body
  119. frontmatter, body = extract_frontmatter(content)
  120. if not frontmatter:
  121. print(f"Warning: No frontmatter found in {skill_file}")
  122. frontmatter = {"name": skill_path.name, "description": ""}
  123. # Extract workflow steps
  124. workflow_steps = extract_workflow_steps(body)
  125. # Extract capabilities from description
  126. capabilities = []
  127. description = frontmatter.get("description", "")
  128. if "课程设计" in description or "course" in description.lower():
  129. capabilities.append("course-design")
  130. if "代码" in description or "code" in description.lower():
  131. capabilities.append("code-generation")
  132. if "测试" in description or "test" in description.lower():
  133. capabilities.append("testing")
  134. if not capabilities:
  135. capabilities = [frontmatter.get("name", skill_path.name)]
  136. # Create Qoder agent configuration
  137. agent_config = {
  138. "name": f"{frontmatter.get('name', skill_path.name)}-agent",
  139. "description": frontmatter.get("description", ""),
  140. "version": "1.0.0",
  141. "type": "skill-agent",
  142. "capabilities": capabilities,
  143. "workflow": workflow_steps,
  144. "instructions": "SKILL.md",
  145. "metadata": {
  146. "source": "partme-ai-skills",
  147. "original_skill": frontmatter.get("name", skill_path.name),
  148. "repo": "https://github.com/partme-ai/partme-cli"
  149. }
  150. }
  151. # Write output
  152. if output_dir:
  153. output_dir = Path(output_dir)
  154. output_dir.mkdir(parents=True, exist_ok=True)
  155. # Create agent directory
  156. agent_dir = output_dir / agent_config["name"]
  157. agent_dir.mkdir(parents=True, exist_ok=True)
  158. # Write agent config
  159. config_file = agent_dir / "qoder-agent-config.json"
  160. with open(config_file, "w", encoding="utf-8") as f:
  161. json.dump(agent_config, f, indent=2, ensure_ascii=False)
  162. # Copy SKILL.md
  163. shutil.copy2(skill_file, agent_dir / "SKILL.md")
  164. # Copy resources to agent dir
  165. resource_dirs = ["scripts", "references", "assets", "api", "templates", "examples"]
  166. for res_dir in resource_dirs:
  167. src_dir = skill_path / res_dir
  168. if src_dir.exists() and src_dir.is_dir():
  169. dest_dir = agent_dir / res_dir
  170. if dest_dir.exists():
  171. shutil.rmtree(dest_dir)
  172. shutil.copytree(src_dir, dest_dir)
  173. # Create Python agent module
  174. python_module = agent_dir / f"{frontmatter.get('name', skill_path.name).replace('-', '_')}_agent.py"
  175. create_python_agent_module(frontmatter, body, python_module)
  176. # Create Qoder rules directory if requested
  177. # Qoder supports project-specific rules in .qoder/rules/
  178. # We force creation of this directory to ensure rules are generated
  179. rules_dir = output_dir.parent / ".qoder" / "rules"
  180. rules_dir.mkdir(parents=True, exist_ok=True)
  181. # Copy resources to rules assets dir
  182. rule_assets_dir = rules_dir / "assets" / frontmatter.get('name', skill_path.name)
  183. copied_rule_resources = []
  184. for res_dir in resource_dirs:
  185. src_dir = skill_path / res_dir
  186. if src_dir.exists() and src_dir.is_dir():
  187. dest_dir = rule_assets_dir / res_dir
  188. if dest_dir.exists():
  189. shutil.rmtree(dest_dir)
  190. shutil.copytree(src_dir, dest_dir)
  191. copied_rule_resources.append(res_dir)
  192. # Prepare rule content with rewritten links
  193. rule_body = body
  194. if copied_rule_resources:
  195. rule_body = rewrite_markdown_links(body, frontmatter.get('name', skill_path.name), copied_rule_resources)
  196. # Create rule file in .qoder/rules/
  197. rule_content = f"""# {frontmatter.get('name', skill_path.name)}
  198. {frontmatter.get('description', '')}
  199. ## Instructions
  200. {rule_body}
  201. """
  202. rule_file = rules_dir / f"{frontmatter.get('name', skill_path.name)}.md"
  203. with open(rule_file, "w", encoding="utf-8") as f:
  204. f.write(rule_content)
  205. # Also create a local rule file in the agent directory for reference/usage
  206. # For the local agent copy, we use the original body since resources are local
  207. local_rule_content = f"""# {frontmatter.get('name', skill_path.name)}
  208. {frontmatter.get('description', '')}
  209. ## Instructions
  210. {body}
  211. """
  212. local_rule_file = agent_dir / f"{frontmatter.get('name', skill_path.name)}.md"
  213. with open(local_rule_file, "w", encoding="utf-8") as f:
  214. f.write(local_rule_content)
  215. log_msg = f"✅ Converted to Qoder agent: {agent_dir}"
  216. if copied_rule_resources:
  217. log_msg += f" (Resources copied to rules: {', '.join(copied_rule_resources)})"
  218. print(log_msg)
  219. return agent_dir
  220. else:
  221. return agent_config
  222. def create_python_agent_module(frontmatter, body, output_file):
  223. """Create Python agent module for Qoder"""
  224. skill_name = frontmatter.get("name", "skill")
  225. class_name = "".join(word.capitalize() for word in skill_name.split("-")) + "Agent"
  226. module_content = f'''"""
  227. {frontmatter.get('description', 'Agent module')} for Qoder
  228. This agent module was automatically generated from Agent Skill.
  229. """
  230. class {class_name}:
  231. """
  232. Agent for {frontmatter.get('description', 'executing tasks')}
  233. """
  234. def __init__(self):
  235. self.name = "{skill_name}"
  236. self.description = "{frontmatter.get('description', '')}"
  237. def execute(self, task, context=None):
  238. """
  239. Execute the agent task
  240. Args:
  241. task: Task description or parameters
  242. context: Optional context information
  243. Returns:
  244. Task execution result
  245. """
  246. # Read instructions from SKILL.md
  247. # Execute skill workflow
  248. # Return result
  249. pass
  250. def get_capabilities(self):
  251. """Get agent capabilities"""
  252. return {{
  253. "name": self.name,
  254. "description": self.description,
  255. "capabilities": ["task-execution"]
  256. }}
  257. '''
  258. with open(output_file, "w", encoding="utf-8") as f:
  259. f.write(module_content)
  260. def convert_all_skills(skills_dir, output_dir):
  261. """Convert all skills in a directory"""
  262. skills_dir = Path(skills_dir)
  263. output_dir = Path(output_dir)
  264. output_dir.mkdir(parents=True, exist_ok=True)
  265. converted = []
  266. for skill_dir in skills_dir.iterdir():
  267. if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
  268. try:
  269. result = convert_to_qoder_agent(skill_dir, output_dir)
  270. if result:
  271. converted.append(skill_dir.name)
  272. except Exception as e:
  273. print(f"Error converting {skill_dir.name}: {e}")
  274. print(f"\n✅ Converted {len(converted)} skills: {', '.join(converted)}")
  275. return converted
  276. if __name__ == "__main__":
  277. if len(sys.argv) < 2:
  278. print("Usage: python convert_to_qoder.py <skill_path> [output_path]")
  279. print(" or: python convert_to_qoder.py --all <skills_dir> <output_dir>")
  280. sys.exit(1)
  281. if sys.argv[1] == "--all":
  282. if len(sys.argv) < 4:
  283. print("Usage: python convert_to_qoder.py --all <skills_dir> <output_dir>")
  284. sys.exit(1)
  285. convert_all_skills(sys.argv[2], sys.argv[3])
  286. else:
  287. skill_path = sys.argv[1]
  288. output_path = sys.argv[2] if len(sys.argv) > 2 else None
  289. convert_to_qoder_agent(skill_path, output_path)