diff --git a/skills/moark-agent-skeleton/SKILL.md b/skills/moark-agent-skeleton/SKILL.md new file mode 100755 index 0000000000000000000000000000000000000000..ffdc0142677ab0ff4f8e6ec4185b92f2acd4f4c4 --- /dev/null +++ b/skills/moark-agent-skeleton/SKILL.md @@ -0,0 +1,79 @@ +--- +name: moark-agent-skeleton +description: One-shot generator for a complete AI agent skeleton — identity (USER.md/SOUL.md), rules, memory, and domain ontology. Parallel generation, multi-platform export (Claude Code/OpenAI/Coze), drop-in ready. +metadata: + openclaw: + emoji: "🤖" + requires: + env: ["GITEEAI_API_KEY"] + primaryEnv: "GITEEAI_API_KEY" +--- + +# Agent Skeleton Generator 🤖 + +Stop hand-crafting agent configs. Describe what you want in one sentence — this skill generates a **complete, production-ready AI agent skeleton** with all four layers in parallel. + +## What You Get + +- 🧬 **Identity layer** — USER.md + SOUL.md with 7 personality anchors (prevents agent drift) +- 📜 **Rules layer** — RULES.md + AGENTS.md (operational guardrails) +- 💾 **Memory layer** — MEMORY.md (continuity across sessions) +- 🏗️ **Ontology layer** — domain model tailored to your industry +- 🔄 **Multi-platform export** — drop into Claude Code, OpenAI, or Coze in one command +- ⚡ **Parallel generation** — all 4 layers generated concurrently (4× faster) + +## Usage + +Set your API key once: `export GITEEAI_API_KEY=your_key` + +**Generate full skeleton for a customer service agent** +```bash +python {baseDir}/scripts/perform_skeleton_generate.py \ + --description "a helpful customer service agent for a flower shop" \ + --scope full --industry retail --role assistant --output json +``` + +**Identity only (quick personality setup)** +```bash +python {baseDir}/scripts/perform_skeleton_generate.py \ + --description "a witty coding assistant" --scope identity +``` + +**Export as Claude Code ready config** +```bash +python {baseDir}/scripts/perform_skeleton_generate.py \ + --description "a coding assistant" --scope full --export claude-code +``` + +**Save to a project directory** +```bash +python {baseDir}/scripts/perform_skeleton_generate.py \ + --description "a research analyst" --scope full --export-dir ./my-agent +``` + +## Options +- `--description` / `-t` (required): Plain-language description of the agent +- `--scope` / `-s`: `full` (all 4 layers, default), `identity`, `rules`, `ontology` +- `--industry` / `-i`: Industry context (default: general) +- `--role` / `-r`: Agent role type (default: assistant) +- `--export` / `-e`: `none` (raw, default), `claude-code`, `openai`, `coze` +- `--export-dir`: Save each layer as a separate .md file +- `--output` / `-o`: `json` (default) or `markdown` +- `--model` / `-m`: Generation model (default: DeepSeek-R1-0528) +- `--timeout`: Per-call timeout in seconds (default: 120) +- `--api-key` / `-k`: Gitee AI API key (prefer `GITEEAI_API_KEY` env var) + +## Workflow + +1. Run `perform_skeleton_generate.py` with the user's parameters. +2. Find the line starting with `SKELETON_RESULT:` in the output. +3. Extract everything from that line onwards. +4. Present to the user as: `🤖 [Agent Skeleton]` + +## Notes +- `full` scope generates all 4 layers in parallel — typically 2-3× faster than sequential +- API calls include automatic retry (up to 3 attempts with exponential backoff) +- Case-insensitive JSON extraction (` ```JSON ` and ` ```json ` both work) +- Identity layer uses 7 personality anchors — significantly reduces agent drift in long sessions +- `--export` reformats for target platform; `--export-dir` saves each layer as a standalone file +- All file writes use atomic replacement (temp file + os.replace) to prevent data corruption on crash diff --git a/skills/moark-agent-skeleton/scripts/perform_skeleton_generate.py b/skills/moark-agent-skeleton/scripts/perform_skeleton_generate.py new file mode 100755 index 0000000000000000000000000000000000000000..cd0fb30c6515e637ba5a4cb668720870b476ac52 --- /dev/null +++ b/skills/moark-agent-skeleton/scripts/perform_skeleton_generate.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "openai" +# ] +# /// + +""" +Generate an AI agent skeleton including identity, rules, memory, and domain model layers. + +Usage: + python perform_skeleton_generate.py --description "a helpful assistant" --scope full [--api-key KEY] + python perform_skeleton_generate.py --description "a coding bot" --scope full --export-dir ./output [--api-key KEY] +""" + +import argparse +import json +import os +import random +import re +import sys +import tempfile +import time +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from openai import OpenAI + +DEFAULT_MODEL = "DeepSeek-R1-0528" +API_BASE_URL = "https://ai.gitee.com/v1" +DEFAULT_TIMEOUT = 120 +MAX_RETRIES = 3 + +SYSTEM_PROMPT_PREFIX = "You are an expert AI agent architect." + +# Response format for all LLM calls (ensure JSON output) +JSON_RESPONSE_FORMAT = {"type": "json_object"} + + +def handle_error_exit(message: str, exit_code: int = 1) -> None: + """Print error message to stderr and exit with code.""" + print(f"Error: {message}", file=sys.stderr) + sys.exit(exit_code) + +# Layer configurations: each layer's system prompt and JSON keys +LAYER_CONFIGS = { + "identity": { + "label": "identity layer (USER.md + SOUL.md)", + "step": "[1/4]", + "system_prompt": ( + f"{SYSTEM_PROMPT_PREFIX} Generate the identity layer for an AI agent. " + "Return a JSON object with two keys: 'user_md' and 'soul_md'. Each value is a string " + "containing the full markdown content.\n" + "USER.md should contain: agent name, personality traits (7 anchors to prevent drift), " + "communication style, and core values.\n" + "SOUL.md should contain: the agent's purpose, emotional baseline, decision principles, " + "and boundaries." + ), + "fallback": {"user_md": "(Generation failed)", "soul_md": "(Failed to generate SOUL.md)"}, + }, + "rules": { + "label": "rules layer (RULES.md + AGENTS.md)", + "step": "[2/4]", + "system_prompt": ( + f"{SYSTEM_PROMPT_PREFIX} Generate the rules layer for an AI agent. " + "Return a JSON object with two keys: 'rules_md' and 'agents_md'. Each value is a string " + "containing the full markdown content.\n" + "RULES.md should contain: behavioral rules, constraints, safety guidelines, and " + "decision-making protocols.\n" + "AGENTS.md should contain: multi-agent coordination rules, delegation guidelines, " + "and collaboration patterns (if applicable)." + ), + "fallback": {"rules_md": "(Generation failed)", "agents_md": "(Failed to generate AGENTS.md)"}, + }, + "memory": { + "label": "memory layer (MEMORY.md)", + "step": "[3/4]", + "system_prompt": ( + f"{SYSTEM_PROMPT_PREFIX} Generate the memory layer for an AI agent. " + "Return a JSON object with one key: 'memory_md'. The value is a string containing " + "the full markdown content.\n" + "MEMORY.md should contain: memory architecture (immediate/recent/long-term layers), " + "index structure, and key facts template." + ), + "fallback": {"memory_md": "(Generation failed)"}, + }, + "ontology": { + "label": "domain model (ontology)", + "step": "[4/4]", + "system_prompt": ( + f"{SYSTEM_PROMPT_PREFIX} Generate the domain model/ontology layer " + "for an AI agent. Return a JSON object with one key: 'ontology_md'. The value is " + "a string containing the full markdown content.\n" + "The ontology should contain: key domain entities, relationships, terminology, " + "and domain-specific decision trees relevant to the agent's role." + ), + "fallback": {"ontology_md": "(Generation failed)"}, + }, +} + + +def get_api_key(provided_key: str | None) -> str | None: + """Get API key from argument or environment.""" + if provided_key: + return provided_key + return os.environ.get("GITEEAI_API_KEY") + + +def _extract_json(content: str) -> str: + """Extract JSON content from markdown code blocks.""" + match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL | re.IGNORECASE) + return match.group(1).strip() if match else content + + +def chat_with_retry( + api_key: str, + model: str, + system_prompt: str, + user_prompt: str, + timeout: int, +) -> str: + """Call chat API with exponential backoff and random jitter. + + Each call creates its own OpenAI client to ensure thread safety + when used from ThreadPoolExecutor. + """ + client = OpenAI(base_url=API_BASE_URL, api_key=api_key) + last_error = None + for attempt in range(MAX_RETRIES): + try: + response = client.chat.completions.create( + model=model, + response_format=JSON_RESPONSE_FORMAT, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + stream=False, + timeout=timeout, + ) + if response.choices: + message = response.choices[0].message + content = message.content if message else None + return content.strip() if content else "" + return "" + except Exception as e: + last_error = e + if attempt < MAX_RETRIES - 1: + backoff = (2 ** attempt) + random.uniform(0, 1) + print(f" Retry {attempt + 1}/{MAX_RETRIES} in {backoff:.1f}s after error: {e}", file=sys.stderr) + time.sleep(backoff) + raise last_error # type: ignore[misc] + + +def _generate_layer( + api_key: str, + model: str, + layer_name: str, + description: str, + industry: str, + role: str, + timeout: int, +) -> dict: + """Unified layer generation function used by all four layers. + + Creates its own OpenAI client internally to ensure thread safety + when called from ThreadPoolExecutor. + """ + config = LAYER_CONFIGS[layer_name] + user_prompt = ( + f"Generate {layer_name} layer for an AI agent with:\n" + f"- Description: {description}\n" + f"- Industry: {industry}\n" + f"- Role: {role}" + ) + content = chat_with_retry(api_key, model, config["system_prompt"], user_prompt, timeout) + content = _extract_json(content) + try: + return json.loads(content) + except json.JSONDecodeError: + return config["fallback"] + + +def format_claude_code(result: dict) -> dict: + """Export formatter for Claude Code (CLAUDE.md).""" + return { + "CLAUDE.md": "\n".join( + f"# {k}\n{v}" for k, v in result.items() if v and k.endswith("_md") + ) + } + + +def format_openai(result: dict) -> dict: + """Export formatter for OpenAI (system_prompt).""" + return { + "system_prompt": "\n".join( + v for k, v in result.items() if v and k.endswith("_md") + ) + } + + +def format_coze(result: dict) -> dict: + """Export formatter for Coze (persona + system_prompt + rules).""" + return { + "persona": result.get("user_md", ""), + "system_prompt": result.get("soul_md", ""), + "rules": result.get("rules_md", ""), + } + + +EXPORT_FORMATTERS = { + "none": lambda result: result, + "claude-code": format_claude_code, + "openai": format_openai, + "coze": format_coze, +} + + +def save_to_dir(result: dict, export_dir: str) -> None: + """Save generated files to a directory with path validation and atomic writes. + + Security: validates that the resolved export path stays within the current + working directory to prevent path traversal attacks. + Atomicity: uses tempfile + os.replace() for crash-safe writes. + """ + dir_path = Path(export_dir) + + # Validate path: must be a non-empty string + if not export_dir or not isinstance(export_dir, str): + print(f" Error: Invalid export directory: {export_dir!r}", file=sys.stderr) + return + + # Resolve and check for path traversal (must stay within cwd) + try: + dir_path = dir_path.resolve() + except (OSError, ValueError) as e: + print(f" Error: Invalid path '{export_dir}': {e}", file=sys.stderr) + return + + cwd = Path.cwd().resolve() + try: + # Python 3.9+: is_relative_to() is more robust than string startswith() + # because it handles resolved symlinks, ".." components, and case-insensitive + # filesystems correctly. + if not dir_path.is_relative_to(cwd): + print(f" Error: Export directory '{dir_path}' is outside safe boundary ({cwd})", file=sys.stderr) + return + except TypeError: + # Fallback for Python < 3.9 (should not happen given requires-python >= 3.10) + if not str(dir_path).startswith(str(cwd)): + print(f" Error: Export directory '{dir_path}' is outside safe boundary", file=sys.stderr) + return + + try: + dir_path.mkdir(parents=True, exist_ok=True) + except OSError as e: + print(f" Error: Cannot create directory '{dir_path}': {e}", file=sys.stderr) + return + + saved_count = 0 + for key, value in result.items(): + if value and key.endswith("_md"): + filename = key.replace("_md", ".md") + file_path = dir_path / filename + try: + # Atomic write: write to temp file first, then replace + fd, tmp_path = tempfile.mkstemp(dir=str(dir_path), suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(value) + os.replace(tmp_path, str(file_path)) + except Exception: + os.unlink(tmp_path) + raise + print(f" Saved: {file_path}") + saved_count += 1 + except OSError as e: + print(f" Error: Failed to write '{file_path}': {e}", file=sys.stderr) + + if saved_count == 0: + print(" Warning: No files were saved.", file=sys.stderr) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate an AI agent skeleton including identity, rules, memory, and domain model" + ) + parser.add_argument( + "--description", "-t", + required=True, + help="Description of the agent to generate", + ) + parser.add_argument( + "--scope", "-s", + choices=["full", "identity", "rules", "ontology"], + default="full", + help="Generation scope (default: full)", + ) + parser.add_argument( + "--industry", "-i", + default="general", + help="Industry/domain context (default: general)", + ) + parser.add_argument( + "--role", "-r", + default="assistant", + help="Agent role type (default: assistant)", + ) + parser.add_argument( + "--export", "-e", + choices=["none", "claude-code", "openai", "coze"], + default="none", + help="Export format (default: none - raw markdown files)", + ) + parser.add_argument( + "--export-dir", + help="Directory to save generated files (each layer as .md file)", + ) + parser.add_argument( + "--output", "-o", + choices=["json", "markdown"], + default="json", + help="Output format (default: json)", + ) + parser.add_argument( + "--model", "-m", + default=DEFAULT_MODEL, + help=f"Model to use for generation (default: {DEFAULT_MODEL})", + ) + parser.add_argument( + "--timeout", + type=int, + default=DEFAULT_TIMEOUT, + help=f"Timeout per API call in seconds (default: {DEFAULT_TIMEOUT})", + ) + parser.add_argument( + "--api-key", "-k", + help="Gitee AI API key (overrides GITEEAI_API_KEY env var)", + ) + + args = parser.parse_args() + + api_key = get_api_key(args.api_key) + if not api_key: + handle_error_exit( + "No API key provided.\n" + "Please either:\n" + " 1. Provide --api-key argument\n" + " 2. Set GITEEAI_API_KEY environment variable" + ) + + print(f"Generating agent skeleton (scope={args.scope})...") + print(f"Description: {args.description}") + print(f"Industry: {args.industry}, Role: {args.role}") + + try: + # Determine which layers to generate + layers_to_generate = [] + if args.scope in ("full", "identity"): + layers_to_generate.append("identity") + if args.scope in ("full", "rules"): + layers_to_generate.append("rules") + if args.scope == "full": + layers_to_generate.append("memory") + if args.scope in ("full", "ontology"): + layers_to_generate.append("ontology") + + result = {} + + if args.scope == "full": + # Parallel generation: all 4 layers have no dependencies on each other + print(f"\nGenerating all 4 layers in parallel...") + with ThreadPoolExecutor(max_workers=4) as executor: + futures = {} + for layer_name in layers_to_generate: + config = LAYER_CONFIGS[layer_name] + print(f" {config['step']} Starting {config['label']}...") + future = executor.submit( + _generate_layer, + api_key, args.model, layer_name, + args.description, args.industry, args.role, args.timeout, + ) + futures[future] = layer_name + + for future in futures: + layer_name = futures[future] + config = LAYER_CONFIGS[layer_name] + try: + layer_result = future.result() + result.update(layer_result) + print(f" {config['step']} Completed {config['label']}") + except Exception as e: + print(f" {config['step']} Failed {config['label']}: {e}", file=sys.stderr) + result.update(config["fallback"]) + else: + # Sequential for single-layer scopes + for layer_name in layers_to_generate: + config = LAYER_CONFIGS[layer_name] + print(f"\n {config['step']} Generating {config['label']}...") + try: + layer_result = _generate_layer( + api_key, args.model, layer_name, + args.description, args.industry, args.role, args.timeout, + ) + result.update(layer_result) + print(f" {config['step']} Completed {config['label']}") + except Exception as e: + print(f" {config['step']} Failed {config['label']}: {e}", file=sys.stderr) + result.update(config["fallback"]) + + # Save to directory if requested (before formatting, so raw _md keys are preserved) + if args.export_dir: + print(f"\nSaving files to {args.export_dir}...") + save_to_dir(result, args.export_dir) + + # Apply export format if requested + if args.export != "none": + formatter = EXPORT_FORMATTERS.get(args.export) + if formatter: + result = formatter(result) + + print("\nSKELETON_RESULT:") + if args.output == "json": + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + for key, value in result.items(): + if value: + print(f"\n## {key}\n") + print(value) + + except KeyboardInterrupt: + print("\nOperation cancelled by user.", file=sys.stderr) + sys.exit(130) + except Exception as e: + handle_error_exit(f"Error generating skeleton: {e}") + + +if __name__ == "__main__": + main()