File size: 18,543 Bytes
8afaf1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
"""
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("<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."""
        # 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()