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/.agents/skills/requirements.txt b/.agents/skills/requirements.txt new file mode 100644 index 0000000..943ac85 --- /dev/null +++ b/.agents/skills/requirements.txt @@ -0,0 +1,4 @@ +openai +beautifulsoup4 +requests +pyyaml 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 new file mode 100644 index 0000000..b1d111b --- /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. + +## Turtorial Skills + +The following skills are available for creating and managing tutorials. + +### `turtorial-create-turtorial` + +Creates the container scaffold (Dockerfile and directory structure) for a new tutorial. + +**Usage:** +```bash +.agents/claude/turtorial-create-turtorial --name --description +``` + +**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"). + +### `turtorial-create-lesson` + +Adds a new lesson (MDX content) to an existing tutorial. + +**Usage:** +```bash +.agents/claude/turtorial-create-lesson --tutorial --topic --objectives --level [--url ] +``` + +**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. + +### `turtorial-update-lesson` + +Enhances or modifies an existing lesson file. + +**Usage:** +```bash +.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 Python dependencies are installed: + +```bash +pip install -r .agents/skills/requirements.txt +``` + +### Environment + +Set the `OPENAI_API_KEY` environment variable to use these skills.