From 1d7c344560e86069245432ab14e6e6d170285fdc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:18:16 +0000 Subject: [PATCH 1/2] Add AI agent skill for creating tutorials - Added `scripts/create_tutorial.py` to generate lesson content using OpenAI. - Added `scripts/requirements.txt` for Python dependencies. - Added `AGENTS.md` to document the new skill. - The script supports generating standalone Dockerfiles for individual lessons. Co-authored-by: mtthwcmpbll <226487+mtthwcmpbll@users.noreply.github.com> --- AGENTS.md | 61 +++++++++++++++ scripts/create_tutorial.py | 147 +++++++++++++++++++++++++++++++++++++ scripts/requirements.txt | 4 + 3 files changed, 212 insertions(+) create mode 100644 AGENTS.md create mode 100644 scripts/create_tutorial.py create mode 100644 scripts/requirements.txt diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..268b549 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# Agent Skills + +This repository includes skills designed for AI agents to assist with development and content creation. + +## Create Tutorial Skill + +The `scripts/create_tutorial.py` script allows you to generate new tutorials or add lessons to existing ones using AI. + +### Usage + +```bash +python3 scripts/create_tutorial.py --name [options] +``` + +### Arguments + +* `--name `: **Required**. The directory name for the lesson (e.g., `intro-to-python`). +* `--topic `: The topic of the tutorial. +* `--url `: URL to source documentation to scrape and use as context. +* `--objectives `: Learning objectives for the lesson. +* `--level `: Target skill level (default: "Beginner"). +* `--output-dir `: Output directory for lessons (default: `src/main/resources/lessons`). +* `--standalone`: Generate a `Dockerfile` for a standalone tutorial. +* `--api-key `: OpenAI API Key (can also be set via `OPENAI_API_KEY` env var). + +### Example + +To create a standalone tutorial on "Advanced Java" based on a URL: + +```bash +export OPENAI_API_KEY=sk-... +python3 scripts/create_tutorial.py \ + --name advanced-java \ + --topic "Advanced Java Features" \ + --url https://docs.oracle.com/en/java/ \ + --level "Advanced" \ + --standalone +``` + +This will: +1. Scrape the provided URL. +2. Generate a lesson structure (YAML metadata and MDX steps). +3. Save files to `src/main/resources/lessons/advanced-java`. +4. Create `src/main/resources/lessons/advanced-java/Dockerfile`. + +### Building Standalone Tutorial + +If `--standalone` was used: + +```bash +docker build -f src/main/resources/lessons/advanced-java/Dockerfile -t turtorial-advanced-java . +docker run -p 8080:8080 turtorial-advanced-java +``` + +### Dependencies + +Ensure dependencies are installed: + +```bash +pip install -r scripts/requirements.txt +``` diff --git a/scripts/create_tutorial.py b/scripts/create_tutorial.py new file mode 100644 index 0000000..2afc6b6 --- /dev/null +++ b/scripts/create_tutorial.py @@ -0,0 +1,147 @@ +import argparse +import os +import sys +import json +import yaml +import requests +from bs4 import BeautifulSoup +from openai import OpenAI + +def scrape_url(url): + try: + response = requests.get(url) + response.raise_for_status() + soup = BeautifulSoup(response.text, 'html.parser') + # Extract main content - simple heuristic: look for article, main, or body + content = soup.find('article') or soup.find('main') or soup.body + return content.get_text(separator='\n', strip=True)[:10000] # Limit content size + except Exception as e: + print(f"Error scraping URL: {e}") + return None + +def generate_lesson_plan(client, topic, content, objectives, level): + prompt = f""" + Create a tutorial lesson plan for the topic: "{topic}". + Target Audience Level: {level} + Learning Objectives: {objectives} + + Source Material: + {content} + + The output must be a valid JSON object with the following structure: + {{ + "title": "Lesson Title", + "description": "Brief description of the lesson.", + "steps": [ + {{ + "title": "Step Title", + "section": "Section Name", + "content": "The MDX content for this step. Use markdown formatting.", + "order": 1 + }}, + ... + ] + }} + + Ensure the content is educational, interactive, and follows a logical progression. + The "content" field should be the actual tutorial text for that step, formatted in MDX. + """ + + try: + response = client.chat.completions.create( + model="gpt-4o", # Or gpt-3.5-turbo + messages=[ + {"role": "system", "content": "You are an expert technical writer creating interactive tutorials."}, + {"role": "user", "content": prompt} + ], + response_format={"type": "json_object"} + ) + return json.loads(response.choices[0].message.content) + except Exception as e: + print(f"Error generating lesson plan: {e}") + return None + +def main(): + parser = argparse.ArgumentParser(description="Create a new tutorial lesson using AI.") + parser.add_argument("--topic", help="The topic of the tutorial") + parser.add_argument("--url", help="URL to source documentation") + parser.add_argument("--objectives", help="Learning objectives") + parser.add_argument("--level", default="Beginner", help="Target skill level") + parser.add_argument("--output-dir", default="src/main/resources/lessons", help="Output directory for lessons") + parser.add_argument("--name", required=True, help="Slug/directory name for the lesson") + parser.add_argument("--standalone", action="store_true", help="Generate a Dockerfile for a standalone tutorial") + parser.add_argument("--api-key", help="OpenAI API Key (optional, defaults to OPENAI_API_KEY env var)") + + args = parser.parse_args() + + api_key = args.api_key or os.environ.get("OPENAI_API_KEY") + if not api_key: + print("Error: OpenAI API Key is required. Set OPENAI_API_KEY env var or use --api-key.") + sys.exit(1) + + client = OpenAI(api_key=api_key) + + content = "" + if args.url: + print(f"Scraping {args.url}...") + scraped = scrape_url(args.url) + if scraped: + content += f"\nSource Content:\n{scraped}\n" + + if not args.topic and not content: + print("Error: Either --topic or --url must be provided.") + sys.exit(1) + + topic = args.topic or "Tutorial based on provided URL" + + print(f"Generating lesson plan for '{topic}'...") + lesson_data = generate_lesson_plan(client, topic, content, args.objectives, args.level) + + if not lesson_data: + print("Failed to generate lesson plan.") + sys.exit(1) + + lesson_dir = os.path.join(args.output_dir, args.name) + os.makedirs(lesson_dir, exist_ok=True) + + # Write lesson.yml + lesson_meta = { + "title": lesson_data.get("title", topic), + "description": lesson_data.get("description", "") + } + with open(os.path.join(lesson_dir, "lesson.yml"), "w") as f: + yaml.dump(lesson_meta, f) + + # Write steps + for i, step in enumerate(lesson_data.get("steps", [])): + filename = f"{i+1:02d}-{step['title'].lower().replace(' ', '-')}.mdx" + filepath = os.path.join(lesson_dir, filename) + + frontmatter = { + "title": step['title'], + "section": step.get('section', 'General'), + "order": step.get('order', i+1) + } + + with open(filepath, "w") as f: + f.write("---\n") + yaml.dump(frontmatter, f) + f.write("---\n\n") + f.write(step['content']) + + print(f"Lesson created at {lesson_dir}") + + if args.standalone: + dockerfile_content = f""" +FROM turtorial:latest + +ENV TURTORIAL_LESSONS_DIRECTORY=classpath:/lessons/{args.name} +""" + dockerfile_path = os.path.join(lesson_dir, "Dockerfile") + with open(dockerfile_path, "w") as f: + f.write(dockerfile_content.strip()) + print(f"Standalone Dockerfile created at {dockerfile_path}") + print(f"To build: docker build -f {dockerfile_path} -t turtorial-{args.name} .") + +if __name__ == "__main__": + main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..943ac85 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,4 @@ +openai +beautifulsoup4 +requests +pyyaml From 5a3c423d9cf3667ea9e65395f710e66919df24a9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:38:56 +0000 Subject: [PATCH 2/2] Add modular AI agent skills for tutorial creation - Implemented `create_turtorial`, `create_lesson`, and `update_lesson` scripts in `.agents/skills`. - Added symlinks for Claude and Copilot agents in `.agents/claude` and `.agents/copilot`. - Included documentation in `AGENTS.md`. - Added dependencies in `.agents/skills/requirements.txt`. - Added `clean_llm_response` utility to safely handle LLM output. - Updated `.gitignore` to exclude `__pycache__` and `.pyc` files. Co-authored-by: mtthwcmpbll <226487+mtthwcmpbll@users.noreply.github.com> --- .agents/claude/turtorial-create-lesson | 1 + .agents/claude/turtorial-create-turtorial | 1 + .agents/claude/turtorial-update-lesson | 1 + .agents/copilot/turtorial-create-lesson | 1 + .agents/copilot/turtorial-create-turtorial | 1 + .agents/copilot/turtorial-update-lesson | 1 + .agents/skills/create_lesson.py | 109 ++++++++++++++ .agents/skills/create_turtorial.py | 65 ++++++++ {scripts => .agents/skills}/requirements.txt | 0 .agents/skills/update_lesson.py | 48 ++++++ .agents/skills/utils.py | 51 +++++++ .gitignore | 3 +- AGENTS.md | 68 ++++----- scripts/create_tutorial.py | 147 ------------------- 14 files changed, 315 insertions(+), 182 deletions(-) create mode 120000 .agents/claude/turtorial-create-lesson create mode 120000 .agents/claude/turtorial-create-turtorial create mode 120000 .agents/claude/turtorial-update-lesson create mode 120000 .agents/copilot/turtorial-create-lesson create mode 120000 .agents/copilot/turtorial-create-turtorial create mode 120000 .agents/copilot/turtorial-update-lesson create mode 100755 .agents/skills/create_lesson.py create mode 100755 .agents/skills/create_turtorial.py rename {scripts => .agents/skills}/requirements.txt (100%) create mode 100755 .agents/skills/update_lesson.py create mode 100644 .agents/skills/utils.py delete mode 100644 scripts/create_tutorial.py diff --git a/.agents/claude/turtorial-create-lesson b/.agents/claude/turtorial-create-lesson new file mode 120000 index 0000000..964d23c --- /dev/null +++ b/.agents/claude/turtorial-create-lesson @@ -0,0 +1 @@ +../skills/create_lesson.py \ No newline at end of file diff --git a/.agents/claude/turtorial-create-turtorial b/.agents/claude/turtorial-create-turtorial new file mode 120000 index 0000000..43e6dea --- /dev/null +++ b/.agents/claude/turtorial-create-turtorial @@ -0,0 +1 @@ +../skills/create_turtorial.py \ No newline at end of file diff --git a/.agents/claude/turtorial-update-lesson b/.agents/claude/turtorial-update-lesson new file mode 120000 index 0000000..29b531b --- /dev/null +++ b/.agents/claude/turtorial-update-lesson @@ -0,0 +1 @@ +../skills/update_lesson.py \ No newline at end of file diff --git a/.agents/copilot/turtorial-create-lesson b/.agents/copilot/turtorial-create-lesson new file mode 120000 index 0000000..964d23c --- /dev/null +++ b/.agents/copilot/turtorial-create-lesson @@ -0,0 +1 @@ +../skills/create_lesson.py \ No newline at end of file diff --git a/.agents/copilot/turtorial-create-turtorial b/.agents/copilot/turtorial-create-turtorial new file mode 120000 index 0000000..43e6dea --- /dev/null +++ b/.agents/copilot/turtorial-create-turtorial @@ -0,0 +1 @@ +../skills/create_turtorial.py \ No newline at end of file diff --git a/.agents/copilot/turtorial-update-lesson b/.agents/copilot/turtorial-update-lesson new file mode 120000 index 0000000..29b531b --- /dev/null +++ b/.agents/copilot/turtorial-update-lesson @@ -0,0 +1 @@ +../skills/update_lesson.py \ No newline at end of file diff --git a/.agents/skills/create_lesson.py b/.agents/skills/create_lesson.py new file mode 100755 index 0000000..d2fcfed --- /dev/null +++ b/.agents/skills/create_lesson.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +import yaml +import json +from utils import get_openai_client, scrape_url, clean_llm_response + +def generate_lesson_plan(client, topic, objectives, level, content=""): + prompt = f""" + Create a tutorial lesson plan for the topic: "{topic}". + Target Audience Level: {level} + Learning Objectives: {objectives} + + Source Material (use this as context if provided): + {content} + + The output must be a valid JSON object with the following structure: + {{ + "title": "Lesson Title", + "description": "Brief description of the lesson.", + "steps": [ + {{ + "title": "Step Title", + "section": "Section Name", + "content": "The MDX content for this step. Use markdown formatting. Include code examples and explanation.", + "order": 1 + }}, + ... + ] + }} + + Ensure the content is educational, interactive, and follows a logical progression. + The "content" field should be the actual tutorial text for that step, formatted in MDX. + """ + + try: + response = client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": "You are an expert technical writer creating interactive tutorials."}, + {"role": "user", "content": prompt} + ], + response_format={"type": "json_object"} + ) + # Clean potential markdown wrapping before parsing + json_str = clean_llm_response(response.choices[0].message.content) + return json.loads(json_str) + except Exception as e: + print(f"Error generating lesson plan: {e}") + return None + +def create_lesson(tutorial_slug, topic, objectives, level, url=None, api_key=None): + client = get_openai_client(api_key) + + lesson_dir = f"src/main/resources/lessons/{tutorial_slug}" + if not os.path.exists(lesson_dir): + print(f"Error: Lesson directory '{lesson_dir}' does not exist. Run create-turtorial first.") + sys.exit(1) + + content = "" + if url: + print(f"Scraping {url}...") + scraped = scrape_url(url) + if scraped: + content += f"\nSource Content:\n{scraped}\n" + + print(f"Generating lesson content for '{topic}' in '{tutorial_slug}'...") + lesson_data = generate_lesson_plan(client, topic, objectives, level, content) + + if not lesson_data: + print("Failed to generate lesson content.") + sys.exit(1) + + # Determine starting order based on existing files + existing_files = [f for f in os.listdir(lesson_dir) if f.endswith(".md") or f.endswith(".mdx")] + start_order = len(existing_files) + 1 + + # Write steps + for i, step in enumerate(lesson_data.get("steps", [])): + order = start_order + i + filename = f"{order:02d}-{step['title'].lower().replace(' ', '-')}.mdx" + filepath = os.path.join(lesson_dir, filename) + + frontmatter = { + "title": step['title'], + "section": step.get('section', 'General'), + "order": order + } + + with open(filepath, "w") as f: + f.write("---\n") + yaml.dump(frontmatter, f) + f.write("---\n\n") + f.write(step['content']) + + print(f"Added {len(lesson_data.get('steps', []))} steps to {lesson_dir}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Create lesson content (MDX steps).") + parser.add_argument("--tutorial", required=True, help="Slug/directory name of the existing tutorial") + parser.add_argument("--topic", required=True, help="Topic of the new lesson content") + parser.add_argument("--objectives", required=True, help="Learning objectives") + parser.add_argument("--level", default="Beginner", help="Target skill level") + parser.add_argument("--url", help="URL to source documentation") + parser.add_argument("--api-key", help="OpenAI API Key") + + args = parser.parse_args() + create_lesson(args.tutorial, args.topic, args.objectives, args.level, args.url, args.api_key) diff --git a/.agents/skills/create_turtorial.py b/.agents/skills/create_turtorial.py new file mode 100755 index 0000000..4be0e8c --- /dev/null +++ b/.agents/skills/create_turtorial.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +import yaml +from utils import get_openai_client, generate_text, clean_llm_response + +def create_turtorial(name, description, api_key=None): + client = get_openai_client(api_key) + + print(f"Designing environment for tutorial '{name}' based on: {description}") + + prompt = f""" + Create a Dockerfile for a tutorial environment. + Tutorial Name: {name} + Description of environment needs: {description} + + Base Image: The Dockerfile MUST start with `FROM turtorial:latest`. + This base image already includes Ubuntu 24.04, Java 25, Maven, Gradle, and common tools (git, curl, vim, nano). + + Your task is to add any additional tools or configurations required by the description. + + Also, set the environment variable `TURTORIAL_LESSONS_DIRECTORY` to `classpath:/lessons/{name}`. + + Output ONLY the content of the Dockerfile. Do not include markdown code blocks. + """ + + dockerfile_content = generate_text(client, prompt, system_prompt="You are a DevOps expert writing Dockerfiles.") + if not dockerfile_content: + return + + # Clean up potential markdown code blocks + dockerfile_content = clean_llm_response(dockerfile_content) + + # Create lesson directory + lesson_dir = f"src/main/resources/lessons/{name}" + os.makedirs(lesson_dir, exist_ok=True) + + # Create lesson.yml + lesson_meta = { + "title": name.replace("-", " ").title(), + "description": description + } + with open(os.path.join(lesson_dir, "lesson.yml"), "w") as f: + yaml.dump(lesson_meta, f) + + # Write Dockerfile + dockerfile_path = f"turtorial-{name}.Dockerfile" + with open(dockerfile_path, "w") as f: + f.write(dockerfile_content) + + print(f"Tutorial scaffold created!") + print(f" - Lesson directory: {lesson_dir}") + print(f" - Lesson metadata: {lesson_dir}/lesson.yml") + print(f" - Standalone Dockerfile: {dockerfile_path}") + print(f"To build: docker build -f {dockerfile_path} -t turtorial-{name} .") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Create a tutorial scaffold (Dockerfile and lesson dir).") + parser.add_argument("--name", required=True, help="Slug/directory name for the tutorial") + parser.add_argument("--description", required=True, help="Description of the environment and tutorial goal") + parser.add_argument("--api-key", help="OpenAI API Key") + + args = parser.parse_args() + create_turtorial(args.name, args.description, args.api_key) diff --git a/scripts/requirements.txt b/.agents/skills/requirements.txt similarity index 100% rename from scripts/requirements.txt rename to .agents/skills/requirements.txt diff --git a/.agents/skills/update_lesson.py b/.agents/skills/update_lesson.py new file mode 100755 index 0000000..c0b89d0 --- /dev/null +++ b/.agents/skills/update_lesson.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +from utils import get_openai_client, generate_text, clean_llm_response + +def update_lesson(filepath, instruction, api_key=None): + if not os.path.exists(filepath): + print(f"Error: File '{filepath}' not found.") + sys.exit(1) + + with open(filepath, "r") as f: + content = f.read() + + client = get_openai_client(api_key) + + prompt = f""" + Update the following file based on the instruction provided. + + File Content: + {content} + + Instruction: + {instruction} + + Output ONLY the full updated content of the file. Do not include markdown code blocks. + """ + + updated_content = generate_text(client, prompt, system_prompt="You are a helpful coding assistant.") + + if updated_content: + # Clean up potential markdown code blocks safely + updated_content = clean_llm_response(updated_content) + + with open(filepath, "w") as f: + f.write(updated_content) + print(f"Updated {filepath}") + else: + print("Failed to update file.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update an existing lesson file.") + parser.add_argument("--file", required=True, help="Path to the lesson file (.mdx or .yml)") + parser.add_argument("--instruction", required=True, help="Instruction for the update") + parser.add_argument("--api-key", help="OpenAI API Key") + + args = parser.parse_args() + update_lesson(args.file, args.instruction, args.api_key) diff --git a/.agents/skills/utils.py b/.agents/skills/utils.py new file mode 100644 index 0000000..9aa5388 --- /dev/null +++ b/.agents/skills/utils.py @@ -0,0 +1,51 @@ +import os +import sys +import re +import requests +from bs4 import BeautifulSoup +from openai import OpenAI + +def get_openai_client(api_key=None): + key = api_key or os.environ.get("OPENAI_API_KEY") + if not key: + print("Error: OpenAI API Key is required. Set OPENAI_API_KEY env var or use --api-key.") + sys.exit(1) + return OpenAI(api_key=key) + +def generate_text(client, prompt, model="gpt-4o", system_prompt="You are a helpful assistant."): + try: + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + ) + return response.choices[0].message.content + except Exception as e: + print(f"Error generating text: {e}") + return None + +def scrape_url(url): + try: + response = requests.get(url) + response.raise_for_status() + soup = BeautifulSoup(response.text, 'html.parser') + # Extract main content - simple heuristic: look for article, main, or body + content = soup.find('article') or soup.find('main') or soup.body + return content.get_text(separator='\n', strip=True)[:10000] # Limit content size + except Exception as e: + print(f"Error scraping URL: {e}") + return None + +def clean_llm_response(text): + if not text: + return text + # Regex to find the first code block, ignoring surrounding text + # ```[a-zA-Z]*\n? matches the opening fence (potentially with language) + # ([\s\S]*?) matches the content non-greedily + # \n?``` matches the closing fence + match = re.search(r"```[a-zA-Z]*\n?([\s\S]*?)\n?```", text) + if match: + return match.group(1).strip() + return text.strip() diff --git a/.gitignore b/.gitignore index 89a5f39..2ec40d0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ build/ ### VS Code ### .vscode/ -.secrets/ \ No newline at end of file +.secrets/__pycache__/ +*.pyc diff --git a/AGENTS.md b/AGENTS.md index 268b549..b1d111b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,60 +2,60 @@ This repository includes skills designed for AI agents to assist with development and content creation. -## Create Tutorial Skill +## Turtorial Skills -The `scripts/create_tutorial.py` script allows you to generate new tutorials or add lessons to existing ones using AI. +The following skills are available for creating and managing tutorials. -### Usage +### `turtorial-create-turtorial` +Creates the container scaffold (Dockerfile and directory structure) for a new tutorial. + +**Usage:** ```bash -python3 scripts/create_tutorial.py --name [options] +.agents/claude/turtorial-create-turtorial --name --description ``` -### Arguments - -* `--name `: **Required**. The directory name for the lesson (e.g., `intro-to-python`). -* `--topic `: The topic of the tutorial. -* `--url `: URL to source documentation to scrape and use as context. -* `--objectives `: Learning objectives for the lesson. -* `--level `: Target skill level (default: "Beginner"). -* `--output-dir `: Output directory for lessons (default: `src/main/resources/lessons`). -* `--standalone`: Generate a `Dockerfile` for a standalone tutorial. -* `--api-key `: OpenAI API Key (can also be set via `OPENAI_API_KEY` env var). +**Arguments:** +* `--name`: Slug/directory name for the tutorial (e.g., `intro-to-python`). +* `--description`: Description of the environment and tutorial goal (e.g., "A Python environment with NumPy and Pandas"). -### Example +### `turtorial-create-lesson` -To create a standalone tutorial on "Advanced Java" based on a URL: +Adds a new lesson (MDX content) to an existing tutorial. +**Usage:** ```bash -export OPENAI_API_KEY=sk-... -python3 scripts/create_tutorial.py \ - --name advanced-java \ - --topic "Advanced Java Features" \ - --url https://docs.oracle.com/en/java/ \ - --level "Advanced" \ - --standalone +.agents/claude/turtorial-create-lesson --tutorial --topic --objectives --level [--url ] ``` -This will: -1. Scrape the provided URL. -2. Generate a lesson structure (YAML metadata and MDX steps). -3. Save files to `src/main/resources/lessons/advanced-java`. -4. Create `src/main/resources/lessons/advanced-java/Dockerfile`. +**Arguments:** +* `--tutorial`: Slug of the existing tutorial (e.g., `intro-to-python`). +* `--topic`: The topic of the new lesson. +* `--objectives`: Learning objectives. +* `--level`: Target skill level (default: "Beginner"). +* `--url`: URL to source documentation to scrape and use as context. -### Building Standalone Tutorial +### `turtorial-update-lesson` -If `--standalone` was used: +Enhances or modifies an existing lesson file. +**Usage:** ```bash -docker build -f src/main/resources/lessons/advanced-java/Dockerfile -t turtorial-advanced-java . -docker run -p 8080:8080 turtorial-advanced-java +.agents/claude/turtorial-update-lesson --file --instruction ``` +**Arguments:** +* `--file`: Path to the lesson file (.mdx or .yml). +* `--instruction`: Instruction for the update (e.g., "Fix typos", "Add an example"). + ### Dependencies -Ensure dependencies are installed: +Ensure Python dependencies are installed: ```bash -pip install -r scripts/requirements.txt +pip install -r .agents/skills/requirements.txt ``` + +### Environment + +Set the `OPENAI_API_KEY` environment variable to use these skills. diff --git a/scripts/create_tutorial.py b/scripts/create_tutorial.py deleted file mode 100644 index 2afc6b6..0000000 --- a/scripts/create_tutorial.py +++ /dev/null @@ -1,147 +0,0 @@ -import argparse -import os -import sys -import json -import yaml -import requests -from bs4 import BeautifulSoup -from openai import OpenAI - -def scrape_url(url): - try: - response = requests.get(url) - response.raise_for_status() - soup = BeautifulSoup(response.text, 'html.parser') - # Extract main content - simple heuristic: look for article, main, or body - content = soup.find('article') or soup.find('main') or soup.body - return content.get_text(separator='\n', strip=True)[:10000] # Limit content size - except Exception as e: - print(f"Error scraping URL: {e}") - return None - -def generate_lesson_plan(client, topic, content, objectives, level): - prompt = f""" - Create a tutorial lesson plan for the topic: "{topic}". - Target Audience Level: {level} - Learning Objectives: {objectives} - - Source Material: - {content} - - The output must be a valid JSON object with the following structure: - {{ - "title": "Lesson Title", - "description": "Brief description of the lesson.", - "steps": [ - {{ - "title": "Step Title", - "section": "Section Name", - "content": "The MDX content for this step. Use markdown formatting.", - "order": 1 - }}, - ... - ] - }} - - Ensure the content is educational, interactive, and follows a logical progression. - The "content" field should be the actual tutorial text for that step, formatted in MDX. - """ - - try: - response = client.chat.completions.create( - model="gpt-4o", # Or gpt-3.5-turbo - messages=[ - {"role": "system", "content": "You are an expert technical writer creating interactive tutorials."}, - {"role": "user", "content": prompt} - ], - response_format={"type": "json_object"} - ) - return json.loads(response.choices[0].message.content) - except Exception as e: - print(f"Error generating lesson plan: {e}") - return None - -def main(): - parser = argparse.ArgumentParser(description="Create a new tutorial lesson using AI.") - parser.add_argument("--topic", help="The topic of the tutorial") - parser.add_argument("--url", help="URL to source documentation") - parser.add_argument("--objectives", help="Learning objectives") - parser.add_argument("--level", default="Beginner", help="Target skill level") - parser.add_argument("--output-dir", default="src/main/resources/lessons", help="Output directory for lessons") - parser.add_argument("--name", required=True, help="Slug/directory name for the lesson") - parser.add_argument("--standalone", action="store_true", help="Generate a Dockerfile for a standalone tutorial") - parser.add_argument("--api-key", help="OpenAI API Key (optional, defaults to OPENAI_API_KEY env var)") - - args = parser.parse_args() - - api_key = args.api_key or os.environ.get("OPENAI_API_KEY") - if not api_key: - print("Error: OpenAI API Key is required. Set OPENAI_API_KEY env var or use --api-key.") - sys.exit(1) - - client = OpenAI(api_key=api_key) - - content = "" - if args.url: - print(f"Scraping {args.url}...") - scraped = scrape_url(args.url) - if scraped: - content += f"\nSource Content:\n{scraped}\n" - - if not args.topic and not content: - print("Error: Either --topic or --url must be provided.") - sys.exit(1) - - topic = args.topic or "Tutorial based on provided URL" - - print(f"Generating lesson plan for '{topic}'...") - lesson_data = generate_lesson_plan(client, topic, content, args.objectives, args.level) - - if not lesson_data: - print("Failed to generate lesson plan.") - sys.exit(1) - - lesson_dir = os.path.join(args.output_dir, args.name) - os.makedirs(lesson_dir, exist_ok=True) - - # Write lesson.yml - lesson_meta = { - "title": lesson_data.get("title", topic), - "description": lesson_data.get("description", "") - } - with open(os.path.join(lesson_dir, "lesson.yml"), "w") as f: - yaml.dump(lesson_meta, f) - - # Write steps - for i, step in enumerate(lesson_data.get("steps", [])): - filename = f"{i+1:02d}-{step['title'].lower().replace(' ', '-')}.mdx" - filepath = os.path.join(lesson_dir, filename) - - frontmatter = { - "title": step['title'], - "section": step.get('section', 'General'), - "order": step.get('order', i+1) - } - - with open(filepath, "w") as f: - f.write("---\n") - yaml.dump(frontmatter, f) - f.write("---\n\n") - f.write(step['content']) - - print(f"Lesson created at {lesson_dir}") - - if args.standalone: - dockerfile_content = f""" -FROM turtorial:latest - -ENV TURTORIAL_LESSONS_DIRECTORY=classpath:/lessons/{args.name} -""" - dockerfile_path = os.path.join(lesson_dir, "Dockerfile") - with open(dockerfile_path, "w") as f: - f.write(dockerfile_content.strip()) - print(f"Standalone Dockerfile created at {dockerfile_path}") - print(f"To build: docker build -f {dockerfile_path} -t turtorial-{args.name} .") - -if __name__ == "__main__": - main()