Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/claude/turtorial-create-lesson
1 change: 1 addition & 0 deletions .agents/claude/turtorial-create-turtorial
1 change: 1 addition & 0 deletions .agents/claude/turtorial-update-lesson
1 change: 1 addition & 0 deletions .agents/copilot/turtorial-create-lesson
1 change: 1 addition & 0 deletions .agents/copilot/turtorial-create-turtorial
1 change: 1 addition & 0 deletions .agents/copilot/turtorial-update-lesson
109 changes: 109 additions & 0 deletions .agents/skills/create_lesson.py
Original file line number Diff line number Diff line change
@@ -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)
65 changes: 65 additions & 0 deletions .agents/skills/create_turtorial.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions .agents/skills/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
openai
beautifulsoup4
requests
pyyaml
48 changes: 48 additions & 0 deletions .agents/skills/update_lesson.py
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions .agents/skills/utils.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ build/
### VS Code ###
.vscode/

.secrets/
.secrets/__pycache__/
*.pyc
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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 <slug> --description <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 <slug> --topic <topic> --objectives <text> --level <level> [--url <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 <path> --instruction <text>
```

**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.