""" 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 # Simplified imports - adjust based on your project structure try: from config import Config from logger import setup_logger logger = setup_logger('task_generator', 'task_generator.log') except ImportError: # Fallback for standalone usage 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.""" # Create a hash from email and date for deterministic but unique seeds seed_data = f"{email}:{date_string}" seed_hash = hashlib.sha256(seed_data.encode()).hexdigest() return seed_hash[:16] # Use first 16 characters @staticmethod def generate_random_data(seed: str, data_type: str = "string", length: int = 10) -> Any: """Generate random data based on seed.""" # Create a new Random instance to avoid affecting global state 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": # Generate sample CSV data for sales example 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": # Generate sample markdown content 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": # Generate sample JSON data 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', '') # Round 1 configuration self.brief_template = data.get('brief_template', '') self.checks_template = data.get('checks_template', []) self.attachments_template = data.get('attachments_template', []) # Round 2 configuration 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', []) # Seed configuration self.seed_config = data.get('seed_config', {}) # Cache for computed results (to ensure consistency within a task) 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.""" # Clear cache for new task generation self._result_cache = {} # Create RNG instance 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}") # Generate task ID task_suffix = hashlib.md5(f"{self.template_id}:{seed}".encode()).hexdigest()[:5] task_id = f"{self.template_id}-{task_suffix}" # Generate nonce try: nonce = str(uuid.uuid7()) except AttributeError: nonce = str(uuid.uuid4()) # Set evaluation URL 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]) # Handle result replacement 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]) # Handle result replacement in checks 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}'): # Generate CSV data 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}'): # Generate markdown data 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}'): # Generate JSON data 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("= 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.""" # Generate seed based on email and current date/hour for hourly expiration date_string = datetime.now(timezone.utc).strftime("%Y-%m-%d-%H") seed = SeedGenerator.generate_seed(email, date_string) # Select random template if not specified if template_id is None: # Use deterministic selection based on email 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}") # Generate task 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) # Global task generator instance _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__": # Test the task generator print("Testing Task Generator...") print() generator = get_task_generator() # List templates print("Available templates:") for template_id in generator.list_templates(): template = generator.get_template(template_id) print(f" - {template_id}: {template.name}") print() # Generate sample tasks 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()