File size: 9,705 Bytes
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0915e9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0915e9a
c5292d8
 
 
 
0915e9a
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8eec7d
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8eec7d
c5292d8
 
 
 
0915e9a
c5292d8
 
 
 
 
 
 
 
 
 
0915e9a
c5292d8
 
 
0915e9a
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8eec7d
c5292d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Student API endpoint for receiving build/update requests."""
import asyncio
from datetime import datetime
from pathlib import Path

from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse

from shared.config import settings
from shared.logger import setup_logger
from shared.models import RepoSubmission, TaskRequest
from student.code_generator import CodeGenerator
from student.github_manager import GitHubManager
from student.notification_client import NotificationClient

logger = setup_logger(__name__)

app = FastAPI(
    title="LLM Code Deployment - Student API",
    description="API endpoint for receiving build and update requests",
    version="1.0.0",
)

# Track ongoing tasks
active_tasks: dict[str, dict] = {}

# Lazy initialization of services
_code_generator = None
_github_manager = None
_notification_client = None


def get_code_generator():
    """Lazy initialization of code generator."""
    global _code_generator
    if _code_generator is None:
        _code_generator = CodeGenerator()
    return _code_generator


def get_github_manager():
    """Lazy initialization of GitHub manager."""
    global _github_manager
    if _github_manager is None:
        _github_manager = GitHubManager()
    return _github_manager


def get_notification_client():
    """Lazy initialization of notification client."""
    global _notification_client
    if _notification_client is None:
        _notification_client = NotificationClient()
    return _notification_client


@app.post("/api/build")
async def build_app(request: TaskRequest, background_tasks: BackgroundTasks) -> JSONResponse:
    """Receive build request and process in background.

    Args:
        request: Task request with app brief and requirements
        background_tasks: FastAPI background tasks

    Returns:
        HTTP 200 JSON response
    """
    logger.info(f"Received build request for task {request.task}, round {request.round}")

    # Verify secret
    if request.secret != settings.student_secret:
        logger.error(f"Invalid secret for task {request.task}")
        raise HTTPException(status_code=401, detail="Invalid secret")

    # Verify email
    if request.email != settings.student_email:
        logger.error(f"Email mismatch: expected {settings.student_email}, got {request.email}")
        raise HTTPException(status_code=401, detail="Email mismatch")

    # Check if task is already in progress
    task_key = f"{request.task}-{request.round}"
    if task_key in active_tasks:
        logger.warning(f"Task {task_key} already in progress")
        return JSONResponse(
            status_code=200,
            content={
                "status": "in_progress",
                "message": f"Task {task_key} is already being processed",
            },
        )

    # Mark task as active
    active_tasks[task_key] = {
        "status": "accepted",
        "started_at": datetime.utcnow().isoformat(),
    }

    # Process in background
    if request.round == 1:
        background_tasks.add_task(process_build_request, request)
    else:
        background_tasks.add_task(process_update_request, request)

    logger.info(f"Accepted task {task_key} for background processing")

    return JSONResponse(
        status_code=200,
        content={
            "status": "accepted",
            "message": f"Task {task_key} accepted for processing",
            "task": request.task,
            "round": request.round,
        },
    )


async def process_build_request(request: TaskRequest) -> None:
    """Process build request in background.

    Args:
        request: Task request
    """
    task_key = f"{request.task}-{request.round}"

    try:
        logger.info(f"Processing build request for {task_key}")

        # Create output directory
        output_dir = settings.generated_repos_dir / request.task
        output_dir.mkdir(parents=True, exist_ok=True)

        # Generate code
        logger.info(f"Generating code for {task_key}")
        active_tasks[task_key]["status"] = "generating"
        get_code_generator().generate_app(request, output_dir)

        # Create repo and deploy
        logger.info(f"Deploying to GitHub for {task_key}")
        active_tasks[task_key]["status"] = "deploying"
        repo_url, commit_sha, pages_url = get_github_manager().create_and_deploy(
            request.task, output_dir
        )

        # Prepare submission
        submission = RepoSubmission(
            email=request.email,
            task=request.task,
            round=request.round,
            nonce=request.nonce,
            repo_url=repo_url,
            commit_sha=commit_sha,
            pages_url=pages_url,
        )

        # Notify evaluation endpoint
        logger.info(f"Notifying evaluation endpoint for {task_key}")
        active_tasks[task_key]["status"] = "notifying"
        success = await get_notification_client().notify_with_timeout(
            request.evaluation_url,
            submission,
            timeout_minutes=settings.task_timeout_minutes,
        )

        if success:
            logger.info(f"Successfully completed task {task_key}")
            active_tasks[task_key]["status"] = "completed"
            active_tasks[task_key]["completed_at"] = datetime.utcnow().isoformat()
        else:
            logger.error(f"Failed to notify evaluation endpoint for {task_key}")
            active_tasks[task_key]["status"] = "notification_failed"

    except Exception as e:
        logger.error(f"Error processing build request {task_key}: {e}", exc_info=True)
        active_tasks[task_key]["status"] = "failed"
        active_tasks[task_key]["error"] = str(e)


async def process_update_request(request: TaskRequest) -> None:
    """Process update request in background.

    Args:
        request: Task request
    """
    task_key = f"{request.task}-{request.round}"

    try:
        logger.info(f"Processing update request for {task_key}")

        # Use existing output directory
        output_dir = settings.generated_repos_dir / request.task

        if not output_dir.exists():
            # If directory doesn't exist, treat as new build
            logger.warning(f"No existing code for {request.task}, creating new")
            output_dir.mkdir(parents=True, exist_ok=True)
            get_code_generator().generate_app(request, output_dir)
        else:
            # Generate updated code
            logger.info(f"Updating code for {task_key}")
            active_tasks[task_key]["status"] = "generating"
            get_code_generator().generate_app(request, output_dir)

        # Update repo and redeploy
        logger.info(f"Redeploying to GitHub for {task_key}")
        active_tasks[task_key]["status"] = "deploying"

        from shared.utils import sanitize_repo_name

        repo_name = sanitize_repo_name(request.task)

        try:
            repo_url, commit_sha = get_github_manager().update_and_redeploy(repo_name, output_dir)
            pages_url = f"https://{settings.github_username}.github.io/{repo_name}/"
        except Exception as e:
            logger.warning(f"Update failed, creating new repo: {e}")
            repo_url, commit_sha, pages_url = get_github_manager().create_and_deploy(
                request.task, output_dir
            )

        # Prepare submission
        submission = RepoSubmission(
            email=request.email,
            task=request.task,
            round=request.round,
            nonce=request.nonce,
            repo_url=repo_url,
            commit_sha=commit_sha,
            pages_url=pages_url,
        )

        # Notify evaluation endpoint
        logger.info(f"Notifying evaluation endpoint for {task_key}")
        active_tasks[task_key]["status"] = "notifying"
        success = await get_notification_client().notify_with_timeout(
            request.evaluation_url,
            submission,
            timeout_minutes=settings.task_timeout_minutes,
        )

        if success:
            logger.info(f"Successfully completed task {task_key}")
            active_tasks[task_key]["status"] = "completed"
            active_tasks[task_key]["completed_at"] = datetime.utcnow().isoformat()
        else:
            logger.error(f"Failed to notify evaluation endpoint for {task_key}")
            active_tasks[task_key]["status"] = "notification_failed"

    except Exception as e:
        logger.error(f"Error processing update request {task_key}: {e}", exc_info=True)
        active_tasks[task_key]["status"] = "failed"
        active_tasks[task_key]["error"] = str(e)


@app.get("/api/status/{task_id}")
async def get_task_status(task_id: str) -> JSONResponse:
    """Get status of a task.

    Args:
        task_id: Task identifier

    Returns:
        Task status information
    """
    matching_tasks = {k: v for k, v in active_tasks.items() if k.startswith(task_id)}

    if not matching_tasks:
        raise HTTPException(status_code=404, detail="Task not found")

    return JSONResponse(content=matching_tasks)


@app.get("/health")
async def health_check() -> JSONResponse:
    """Health check endpoint.

    Returns:
        Health status
    """
    return JSONResponse(
        content={
            "status": "healthy",
            "active_tasks": len(active_tasks),
            "timestamp": datetime.utcnow().isoformat(),
        }
    )


if __name__ == "__main__":
    import uvicorn

    # Ensure directories exist
    settings.ensure_directories()

    logger.info(f"Starting Student API on port {settings.student_api_port}")
    uvicorn.run(
        "student.api:app",
        host="0.0.0.0",
        port=settings.student_api_port,
        reload=True,
    )