| """ |
| Task generation system for LLM Deployment System. |
| |
| This module handles creating randomized tasks based on templates, |
| with seed generation for reproducible but varied task instances. |
| """ |
|
|
| import json |
| import random |
| import hashlib |
| import base64 |
| import uuid |
| import os |
| from typing import Dict, List, Any, Optional |
| from datetime import datetime, timezone |
|
|
| |
| try: |
| from config import Config |
| from logger import setup_logger |
| logger = setup_logger('task_generator', 'task_generator.log') |
| except ImportError: |
| |
| import logging |
| logger = logging.getLogger(__name__) |
| |
| class Config: |
| API_HOST = os.getenv('API_HOST', 'http://localhost') |
| API_PORT = os.getenv('EVAL_SERVER_PORT', '5001') |
|
|
|
|
| class SeedGenerator: |
| """Generates seeds for task randomization.""" |
|
|
| @staticmethod |
| def generate_seed(email: str, date_string: str) -> str: |
| """Generate a deterministic seed based on email and date.""" |
| |
| seed_data = f"{email}:{date_string}" |
| seed_hash = hashlib.sha256(seed_data.encode()).hexdigest() |
| return seed_hash[:16] |
|
|
| @staticmethod |
| def generate_random_data(seed: str, data_type: str = "string", length: int = 10) -> Any: |
| """Generate random data based on seed.""" |
| |
| rng = random.Random(seed) |
|
|
| if data_type == "string": |
| chars = "abcdefghijklmnopqrstuvwxyz0123456789" |
| return ''.join(rng.choice(chars) for _ in range(length)) |
| |
| elif data_type == "number": |
| return rng.randint(1000, 99999) |
| |
| elif data_type == "csv_data": |
| |
| products = ["Product A", "Product B", "Product C", "Product D"] |
| rng.shuffle(products) |
| csv_data = "Product,Sales,Region\n" |
| total = 0 |
| for product in products[:rng.randint(2, 4)]: |
| sales = rng.randint(100, 1000) |
| region = rng.choice(["North", "South", "East", "West"]) |
| csv_data += f"{product},{sales},{region}\n" |
| total += sales |
| return csv_data, total |
| |
| elif data_type == "markdown": |
| |
| content = f"""# Sample Markdown Content |
| |
| This is a sample markdown file generated for task {seed[:8]}. |
| |
| ## Features |
| |
| - Random seed: {seed} |
| - Generated at: {datetime.now().isoformat()} |
| - Contains various markdown elements |
| |
| ### Code Example |
| |
| ```python |
| def hello_world(): |
| print("Hello, World!") |
| return "success" |
| ``` |
| |
| ### Lists |
| |
| 1. Item one |
| 2. Item two |
| 3. Item three |
| |
| - Bullet item A |
| - Bullet item B |
| - Bullet item C |
| |
| > This is a blockquote with some **bold** and *italic* text. |
| |
| | Column 1 | Column 2 | Column 3 | |
| |----------|----------|----------| |
| | Data 1 | Data 2 | Data 3 | |
| | Data 4 | Data 5 | Data 6 | |
| """ |
| return content |
| |
| elif data_type == "json": |
| |
| return { |
| "currencies": { |
| "USD": 1.0, |
| "EUR": 0.85, |
| "GBP": 0.73, |
| "JPY": 110.0, |
| "CAD": 1.25 |
| }, |
| "metadata": { |
| "seed": seed, |
| "generated_at": datetime.now().isoformat() |
| } |
| } |
|
|
| return None |
|
|
|
|
| class TaskTemplate: |
| """Represents a task template with configuration for rounds.""" |
|
|
| def __init__(self, template_id: str, data: Dict[str, Any]): |
| self.template_id = template_id |
| self.name = data.get('name', '') |
| self.description = data.get('description', '') |
|
|
| |
| self.brief_template = data.get('brief_template', '') |
| self.checks_template = data.get('checks_template', []) |
| self.attachments_template = data.get('attachments_template', []) |
|
|
| |
| self.round2_brief_template = data.get('round2_brief_template', '') |
| self.round2_checks_template = data.get('round2_checks_template', []) |
| self.round2_attachments_template = data.get('round2_attachments_template', []) |
|
|
| |
| self.seed_config = data.get('seed_config', {}) |
| |
| |
| self._result_cache = {} |
|
|
| def generate_task(self, seed: str, round_num: int = 1, evaluation_url: str = None) -> Dict[str, Any]: |
| """Generate a task instance from this template.""" |
| |
| self._result_cache = {} |
| |
| |
| rng = random.Random(seed) |
|
|
| if round_num == 1: |
| brief = self._process_template(self.brief_template, seed, rng) |
| checks = self._process_checks_template(self.checks_template, seed, rng) |
| attachments = self._process_attachments_template(self.attachments_template, seed, rng) |
| elif round_num == 2: |
| if not self.round2_brief_template: |
| raise ValueError(f"Round 2 not configured for template {self.template_id}") |
| brief = self._process_template(self.round2_brief_template, seed, rng) |
| checks = self._process_checks_template(self.round2_checks_template, seed, rng) |
| attachments = self._process_attachments_template( |
| self.round2_attachments_template if self.round2_attachments_template else [], |
| seed, |
| rng |
| ) |
| else: |
| raise ValueError(f"Invalid round number: {round_num}") |
|
|
| |
| task_suffix = hashlib.md5(f"{self.template_id}:{seed}".encode()).hexdigest()[:5] |
| task_id = f"{self.template_id}-{task_suffix}" |
| |
| |
| try: |
| nonce = str(uuid.uuid7()) |
| except AttributeError: |
| nonce = str(uuid.uuid4()) |
|
|
| |
| if evaluation_url is None: |
| try: |
| evaluation_url = f"{Config.API_HOST}:{Config.API_PORT}/api/evaluate" |
| except: |
| evaluation_url = "http://localhost:5001/api/evaluate" |
|
|
| return { |
| 'task_id': task_id, |
| 'round': round_num, |
| 'nonce': nonce, |
| 'brief': brief, |
| 'checks': checks, |
| 'attachments': attachments, |
| 'evaluation_url': evaluation_url |
| } |
|
|
| def _get_result_value(self, seed: str, rng: random.Random) -> int: |
| """Get or compute result value (cached for consistency).""" |
| if 'result' not in self._result_cache: |
| if 'sales' in self.brief_template.lower(): |
| _, total = SeedGenerator.generate_random_data(seed, "csv_data") |
| self._result_cache['result'] = total |
| else: |
| self._result_cache['result'] = rng.randint(1000, 9999) |
| return self._result_cache['result'] |
|
|
| def _process_template(self, template: str, seed: str, rng: random.Random) -> str: |
| """Process template string with seed data.""" |
| processed = template.replace('{seed}', seed[:8]) |
|
|
| |
| if '{result}' in processed: |
| result = self._get_result_value(seed, rng) |
| processed = processed.replace('{result}', str(result)) |
|
|
| return processed |
|
|
| def _process_checks_template(self, checks: List[str], seed: str, rng: random.Random) -> List[str]: |
| """Process checks template with seed data.""" |
| processed_checks = [] |
|
|
| for check in checks: |
| processed = check.replace('{seed}', seed[:8]) |
|
|
| |
| if '{result}' in processed: |
| result = self._get_result_value(seed, rng) |
| processed = processed.replace('{result}', str(result)) |
|
|
| processed_checks.append(processed) |
|
|
| return processed_checks |
|
|
| def _process_attachments_template(self, attachments: List[Dict], seed: str, rng: random.Random) -> List[Dict]: |
| """Process attachments template with seed data.""" |
| processed_attachments = [] |
|
|
| for attachment in attachments: |
| processed = attachment.copy() |
|
|
| if 'url' in processed and processed['url']: |
| url = processed['url'] |
|
|
| if '{seed}' in url or url.startswith('data:text/csv;base64,{seed}'): |
| |
| csv_data, _ = SeedGenerator.generate_random_data(seed, "csv_data") |
| encoded_data = base64.b64encode(csv_data.encode()).decode() |
| processed['url'] = f"data:text/csv;base64,{encoded_data}" |
|
|
| elif url.startswith('data:text/markdown;base64,{seed}'): |
| |
| markdown_data = SeedGenerator.generate_random_data(seed, "markdown") |
| encoded_data = base64.b64encode(markdown_data.encode()).decode() |
| processed['url'] = f"data:text/markdown;base64,{encoded_data}" |
|
|
| elif url.startswith('data:application/json;base64,{seed}'): |
| |
| json_data = SeedGenerator.generate_random_data(seed, "json") |
| encoded_data = base64.b64encode(json.dumps(json_data).encode()).decode() |
| processed['url'] = f"data:application/json;base64,{encoded_data}" |
|
|
| processed_attachments.append(processed) |
|
|
| return processed_attachments |
|
|
|
|
| class TaskGenerator: |
| """Main task generator class.""" |
|
|
| def __init__(self): |
| self.templates = {} |
| self._load_default_templates() |
|
|
| def _load_default_templates(self): |
| """Load default task templates.""" |
| default_templates = [ |
| { |
| 'template_id': 'sum-of-sales', |
| 'name': 'Sales Summary Application', |
| 'description': 'Create an app that processes CSV data and displays sales summaries', |
| 'brief_template': 'Publish a single-page site that fetches data.csv from attachments, sums its sales column, sets the title to "Sales Summary {seed}", displays the total inside #total-sales, and loads Bootstrap 5 from jsdelivr.', |
| 'checks_template': [ |
| 'Repo has MIT license', |
| 'README.md is professional', |
| 'js: document.title === `Sales Summary {seed}`', |
| 'js: !!document.querySelector("link[href*=\'bootstrap\']")', |
| 'js: Math.abs(parseFloat(document.querySelector("#total-sales").textContent) - {result}) < 0.01' |
| ], |
| 'attachments_template': [ |
| { |
| 'name': 'data.csv', |
| 'url': 'data:text/csv;base64,{seed}' |
| } |
| ], |
| 'round2_brief_template': 'Add a Bootstrap table #product-sales that lists each product with its total sales and keeps #total-sales accurate after render.', |
| 'round2_checks_template': [ |
| 'js: document.querySelectorAll("#product-sales tbody tr").length >= 1', |
| 'js: (() => { const rows = [...document.querySelectorAll("#product-sales tbody tr td:last-child")]; const sum = rows.reduce((acc, cell) => acc + parseFloat(cell.textContent), 0); return Math.abs(sum - {result}) < 0.01; })()' |
| ], |
| 'round2_attachments_template': [] |
| }, |
| { |
| 'template_id': 'markdown-to-html', |
| 'name': 'Markdown to HTML Converter', |
| 'description': 'Create an app that converts Markdown to HTML with syntax highlighting', |
| 'brief_template': 'Publish a static page that converts input.md from attachments to HTML with marked, renders it inside #markdown-output, and loads highlight.js for code blocks.', |
| 'checks_template': [ |
| 'Repo has MIT license', |
| 'README.md is professional', |
| 'js: !!document.querySelector("script[src*=\'marked\']")', |
| 'js: !!document.querySelector("script[src*=\'highlight.js\']") || !!document.querySelector("link[href*=\'highlight.js\']")', |
| 'js: document.querySelector("#markdown-output").innerHTML.includes("<h")' |
| ], |
| 'attachments_template': [ |
| { |
| 'name': 'input.md', |
| 'url': 'data:text/markdown;base64,{seed}' |
| } |
| ], |
| 'round2_brief_template': 'Add tabs #markdown-tabs that switch between rendered HTML in #markdown-output and the original Markdown in #markdown-source while keeping content in sync.', |
| 'round2_checks_template': [ |
| 'js: document.querySelectorAll("#markdown-tabs button").length >= 2', |
| 'js: document.querySelector("#markdown-source").textContent.trim().length > 0' |
| ], |
| 'round2_attachments_template': [] |
| }, |
| { |
| 'template_id': 'github-user-created', |
| 'name': 'GitHub User Information', |
| 'description': 'Create an app that fetches and displays GitHub user information', |
| 'brief_template': 'Publish a Bootstrap page with form id="github-user-{seed}" that fetches a GitHub username, optionally uses ?token=, and displays the account creation date in YYYY-MM-DD UTC inside #github-created-at.', |
| 'checks_template': [ |
| 'Repo has MIT license', |
| 'README.md is professional', |
| 'js: document.querySelector("#github-user-{seed}").tagName === "FORM"', |
| 'js: !!document.querySelector("script").textContent.includes("https://api.github.com/users/")' |
| ], |
| 'attachments_template': [], |
| 'round2_brief_template': 'Show an aria-live alert #github-status that reports when a lookup starts, succeeds, or fails.', |
| 'round2_checks_template': [ |
| 'js: document.querySelector("#github-status").getAttribute("aria-live") === "polite"', |
| 'js: !!document.querySelector("script").textContent.includes("github-status")' |
| ], |
| 'round2_attachments_template': [] |
| } |
| ] |
|
|
| for template_data in default_templates: |
| template = TaskTemplate(template_data['template_id'], template_data) |
| self.templates[template_data['template_id']] = template |
|
|
| logger.info(f"Loaded {len(self.templates)} task templates") |
|
|
| def add_template(self, template_id: str, template_data: Dict[str, Any]): |
| """Add a new task template.""" |
| template = TaskTemplate(template_id, template_data) |
| self.templates[template_id] = template |
| logger.info(f"Added task template: {template_id}") |
|
|
| def get_template(self, template_id: str) -> Optional[TaskTemplate]: |
| """Get a task template by ID.""" |
| return self.templates.get(template_id) |
|
|
| def list_templates(self) -> List[str]: |
| """List all available template IDs.""" |
| return list(self.templates.keys()) |
|
|
| def generate_task( |
| self, |
| email: str, |
| template_id: Optional[str] = None, |
| round_num: int = 1, |
| evaluation_url: str = None |
| ) -> Dict[str, Any]: |
| """Generate a task for the given email.""" |
| |
| date_string = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H") |
| seed = SeedGenerator.generate_seed(email, date_string) |
|
|
| |
| if template_id is None: |
| |
| template_ids = list(self.templates.keys()) |
| template_index = int(hashlib.md5(email.encode()).hexdigest(), 16) % len(template_ids) |
| template_id = template_ids[template_index] |
|
|
| template = self.templates.get(template_id) |
| if not template: |
| raise ValueError(f"Unknown template: {template_id}") |
|
|
| |
| task = template.generate_task(seed, round_num, evaluation_url) |
|
|
| logger.info(f"Generated task {task['task_id']} for {email} using template {template_id}") |
| return task |
|
|
| def generate_task_for_submission( |
| self, |
| email: str, |
| round_num: int = 1, |
| evaluation_url: str = None |
| ) -> Dict[str, Any]: |
| """Generate a task for a student submission.""" |
| return self.generate_task(email, round_num=round_num, evaluation_url=evaluation_url) |
|
|
|
|
| |
| _task_generator = None |
|
|
|
|
| def get_task_generator() -> TaskGenerator: |
| """Get the global task generator instance.""" |
| global _task_generator |
| if _task_generator is None: |
| _task_generator = TaskGenerator() |
| return _task_generator |
|
|
|
|
| def generate_sample_task( |
| email: str = "student@example.com", |
| template_id: str = "sum-of-sales" |
| ) -> Dict[str, Any]: |
| """Generate a sample task for testing.""" |
| generator = get_task_generator() |
| return generator.generate_task(email, template_id) |
|
|
|
|
| if __name__ == "__main__": |
| |
| print("Testing Task Generator...") |
| print() |
| |
| generator = get_task_generator() |
| |
| |
| print("Available templates:") |
| for template_id in generator.list_templates(): |
| template = generator.get_template(template_id) |
| print(f" - {template_id}: {template.name}") |
| print() |
| |
| |
| email = "test@example.com" |
| print(f"Generating tasks for {email}:") |
| print() |
| |
| for template_id in generator.list_templates(): |
| task = generator.generate_task(email, template_id, round_num=1) |
| print(f"Template: {template_id}") |
| print(f"Task ID: {task['task_id']}") |
| print(f"Brief: {task['brief'][:100]}...") |
| print(f"Checks: {len(task['checks'])} checks") |
| print(f"Attachments: {len(task['attachments'])} files") |
| print() |