maya-ur commited on
Commit
ea0e78b
·
verified ·
1 Parent(s): 9495aa1

Create solver.py

Browse files
Files changed (1) hide show
  1. app/solver.py +470 -0
app/solver.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/solver.py
2
+
3
+ import time
4
+ import requests
5
+ import tempfile
6
+ import subprocess
7
+ import httpx
8
+ import sys
9
+ import json
10
+ import re
11
+ import os
12
+ from playwright.sync_api import sync_playwright
13
+ from typing import Any, Dict, Optional
14
+ from datetime import datetime, timedelta
15
+ from urllib.parse import urlparse, urlunparse
16
+
17
+ # LLM Integration (Assuming Google GenAI for demonstration)
18
+ # from google import genai
19
+ # from google.genai.errors import APIError
20
+
21
+ from .models import QuizSubmitPayload, QuizSubmitResponse, QuizRequest
22
+ from .config import STUDENT_SECRET, LLM_API_KEY
23
+
24
+ TIMEOUT_SECONDS = 180
25
+ # TEMP_DIR = "quiz_temp"
26
+
27
+ # Initialize the LLM client
28
+ # try:
29
+ # llm_client = genai.Client(api_key=LLM_API_KEY)
30
+ # except Exception as e:
31
+ # print(f"LLM Client initialization error: {e}")
32
+ # llm_client = None
33
+
34
+ class QuizSolver:
35
+ def __init__(self, request_payload: QuizRequest, start_time: datetime):
36
+ self.email = request_payload.email
37
+ self.secret = request_payload.secret
38
+ self.current_url = request_payload.url
39
+ self.start_time = start_time
40
+ # Ensure temporary directory exists
41
+ self.temp_dir = tempfile.TemporaryDirectory()
42
+
43
+ def _get_time_remaining(self) -> float:
44
+ """Calculates the time remaining until the 3-minute deadline."""
45
+ elapsed = (datetime.now() - self.start_time).total_seconds()
46
+ remaining = TIMEOUT_SECONDS - elapsed
47
+ return max(0.0, remaining)
48
+
49
+ def _llm_analyze_and_generate_code(self, quiz_content_text: str, link_data_str: str) -> Optional[Dict[str, Any]]:
50
+ """
51
+ Uses an LLM to analyze the question and generate a structured JSON
52
+ containing the necessary Python code.
53
+ """
54
+ print("-> LLM: Analyzing question and generating code...")
55
+
56
+ system_prompt="""
57
+ You are a quiz-solving assistant.
58
+
59
+ Your task: Given the scraped text of a quiz page, you must extract:
60
+ 1. The submission URL where the final answer must be Posted.
61
+ 2. Accurate and executable Python code that computes ONLY the answer (a single value) and stores it in a variable that MUST be called answer.
62
+ 3. All Python dependencies that the code requires as a list.
63
+
64
+ Return your output ONLY as JSON with this exact structure:
65
+
66
+ {
67
+ "submit_url": "string",
68
+ "python_code": ["line1", "line2", "..."],
69
+ "dependencies": ["package1", "package2"]
70
+ }
71
+
72
+ Rules:
73
+ - The submission URL ALWAYS appears in the text. It typically looks like: "Post your answer to https://....".
74
+ - You MUST extract the submission URL.
75
+ - DO NOT guess. Extract it literally from the text.
76
+ - Do NOT include any explanation or commentary.
77
+ - Only produce valid JSON output.
78
+
79
+
80
+
81
+ Python rules:
82
+ - The python_code section MUST contain only the code needed to compute the answer and store it in a variable called "answer".
83
+ - Ignore any submission JSON shown in the page.
84
+ - DO NOT include code to send the submission.
85
+ - The backend will run your code inside a Python subprocess.
86
+ - Store the final answer in the variable named "answer".
87
+ - Ensure that the final answer is a Python-native type (int, float, str, bool) and not a NumPy or pandas dtype. If the result is a NumPy scalar, call .item() to convert it.
88
+ - Always ensure the variable "answer" is JSON-serialisable.
89
+ - If your code imports libraries other than the standard library modules, then those libraries MUST be listed in "dependencies".
90
+ - DO NOT include standard library modules in "dependencies".
91
+
92
+ """
93
+
94
+ # --- CRITICAL LLM PROMPT ---
95
+ user_prompt = f"""
96
+ You are a highly skilled Question Solver. Your task is to analyze the quiz question
97
+ provided below and generate the executable Python code to solve it.
98
+
99
+ 1. **Quiz Content:** (The main question and submission text scraped from the website)
100
+ ---
101
+ {quiz_content_text}
102
+ ---
103
+
104
+ 2. **Files Available:** (A list of paths of available files)
105
+ ---
106
+ {link_data_str}
107
+ ---
108
+
109
+ The code must adhere to these rules:
110
+ 1. All dependencies that your generated code requires must appear in "dependencies" key of json output structure.
111
+ 2. ONLY include dependencies that are actually imported.
112
+ 3. If no extra libraries are required, return an empty list.
113
+ 4. Do NOT send or execute the submission request. Only generate the code that computes the answer.
114
+ 5. The final answer must be assigned to a variable named **'answer'**.
115
+
116
+ You must also extract the submission url (link to which the answer should be sent to) from the question content.
117
+
118
+ Respond ONLY with a single JSON object that strictly adheres to the schema below.
119
+
120
+ JSON Schema:
121
+ ```json
122
+ {{
123
+ "submit_url": "The submission URL extracted from the content (e.g., [https://example.com/submit](https://example.com/submit)).",
124
+ "python_code": [
125
+ "import ...",
126
+ "# ... generated code to process data and set final_answer ...",
127
+ "answer = ..."
128
+ ],
129
+ "dependencies":[...] (eg. ["pandas","numpy",...])
130
+ }}
131
+ ```
132
+ """
133
+
134
+ # print('System prompt: ', system_prompt)
135
+ print('User prompt: ', user_prompt)
136
+
137
+ # --- END CRITICAL LLM PROMPT ---
138
+
139
+ # if not llm_client:
140
+ # print("LLM client not initialized. Cannot generate code.")
141
+ # return None
142
+
143
+ try:
144
+ # In a real scenario, you would call the LLM API here.
145
+ # response = llm_client.models.generate_content(model='gemini-2.5-flash', contents=prompt)
146
+ # llm_output = response.text.strip()
147
+ api_url='https://aipipe.org/openai/v1/chat/completions'
148
+
149
+ headers={
150
+ "Content-Type": "application/json",
151
+ "Authorization": f"Bearer{LLM_API_KEY}"
152
+ }
153
+
154
+
155
+ data = {
156
+ "model": "gpt-5-mini",
157
+ "messages": [
158
+ {
159
+ "role": "system",
160
+ "content": system_prompt
161
+ },
162
+ {
163
+ "role": "user",
164
+ "content": user_prompt
165
+ }
166
+ ],
167
+ }
168
+
169
+ response=requests.post(api_url, headers=headers, json=data)
170
+
171
+ llm_output=response.json()["choices"][0]["message"]["content"]
172
+
173
+ llm_plan = json.loads(llm_output)
174
+
175
+ print(llm_plan)
176
+
177
+ # llm_output='''{
178
+ # "submit_url":"https://submit",
179
+ # "python_code":["import pandas as pd","df=pd.DataFrame({'a':[1,2,3,4,5]})","answer=df.a.sum().item()"],
180
+ # "dependencies":["pandas"]
181
+ # }'''
182
+
183
+
184
+
185
+ except :
186
+ print(f"LLM or JSON parsing failed")
187
+ return None
188
+
189
+ return llm_plan
190
+
191
+ def _download_file(self,file_list):
192
+ """
193
+ Downloads files one after another using requests.
194
+
195
+ Parameters:
196
+ file_list (list): List of dicts with {"text": ..., "href": ...}
197
+ save_dir (str): Directory to save the downloaded files.
198
+
199
+ Returns:
200
+ List of file paths to the downloaded files.
201
+ """
202
+
203
+ # Create folder if missing
204
+
205
+
206
+ downloaded_paths = []
207
+
208
+ for item in file_list:
209
+ name = item.get("text", "file").replace(" ", "_")
210
+ url = item["href"]
211
+
212
+ # Guess filename from URL
213
+ filename = os.path.basename(url)
214
+ if not filename:
215
+ filename = f"{name}.html" # fallback
216
+
217
+ save_path = os.path.join(self.temp_dir.name, filename)
218
+
219
+ try:
220
+ print(f"Downloading: {url} -> {save_path}")
221
+ response = requests.get(url, timeout=20)
222
+ response.raise_for_status()
223
+
224
+ # Save file
225
+ with open(save_path, "wb") as f:
226
+ f.write(response.content)
227
+
228
+ downloaded_paths.append(save_path)
229
+
230
+ except Exception as e:
231
+ print(f"Failed to download {url}: {e}")
232
+ continue
233
+ print(downloaded_paths)
234
+ return downloaded_paths
235
+
236
+ def _execute_generated_code(self, code_lines: list,dependencies: list) -> Any:
237
+ """
238
+ Safely executes the LLM-generated Python code in an isolated environment.
239
+ """
240
+ print("-> Executing generated code locally...")
241
+
242
+ code_block = "\n".join(code_lines)
243
+
244
+ if dependencies!=[]:
245
+
246
+ to_install=[sys.executable, "-m", "pip", "install"] + dependencies
247
+
248
+ print('Starting package installation')
249
+
250
+ result = subprocess.run(
251
+ to_install,
252
+ check=True,
253
+ text=True,
254
+ capture_output=True
255
+ )
256
+
257
+ if result.returncode!=0:
258
+ return 'Error in installing packages'
259
+
260
+ file_path = os.path.join(self.temp_dir.name, "script.py")
261
+
262
+ print('Python file path: ',file_path)
263
+
264
+ with open(file_path, "w", encoding="utf-8") as f:
265
+ f.write(code_block)
266
+ script_path = f.name
267
+
268
+ print('Starting python execution')
269
+ try:
270
+ namespace = {}
271
+ with open(file_path, "r", encoding="utf-8") as f:
272
+ code = f.read()
273
+
274
+ exec(code, namespace)
275
+
276
+ # Step 3: extract the answer
277
+ answer = namespace["answer"]
278
+
279
+ return answer
280
+
281
+ except:
282
+ return 0
283
+
284
+ # (The _solve_and_submit method remains the same)
285
+ def _solve_and_submit(self, submission_url: str, answer: Any) -> QuizSubmitResponse:
286
+ # ... (implementation from previous response)
287
+ # time_remaining = self._get_time_remaining()
288
+ # if time_remaining <= 5: # Keep a 5s buffer
289
+ # print("Submission cancelled: Deadline is approaching.")
290
+ # return QuizSubmitResponse(correct=False, reason="Deadline exceeded.")
291
+
292
+ # submission_url="https://tds-llm-analysis.s-anand.net/submit"
293
+
294
+ payload = QuizSubmitPayload(
295
+ email=self.email,
296
+ secret=self.secret,
297
+ url=self.current_url,
298
+ answer=answer
299
+ ).model_dump()
300
+
301
+ print('Request to submit: ', payload)
302
+
303
+ try:
304
+
305
+ headers = {"Content-Type": "application/json"}
306
+ response = requests.post(submission_url, json=payload)
307
+ response.raise_for_status() # Raise exception for bad status codes
308
+
309
+ print(response)
310
+
311
+ submit_response = QuizSubmitResponse(**response.json())
312
+ print(submit_response)
313
+ return submit_response
314
+ except requests.exceptions.RequestException as e:
315
+ print(f"Submission failed for {submission_url}: {e}")
316
+ return QuizSubmitResponse(correct=False, reason=f"Submission failed: {e}")
317
+
318
+ # (The run_quiz_loop method is updated to use the new LLM and execution steps)
319
+ def run_quiz_loop(self) -> str:
320
+ """
321
+ Main loop to solve the quiz and follow new URLs until completion or timeout.
322
+ """
323
+ repeats=0
324
+
325
+ while self.current_url:
326
+
327
+ with sync_playwright() as p:
328
+ # Running headless to save resources
329
+ browser = p.chromium.launch(headless=True)
330
+ page = browser.new_page()
331
+
332
+
333
+ print(f"\n--- Solving Quiz: {self.current_url} ---")
334
+
335
+ # 1. Visit URL and get content
336
+ try:
337
+ page.goto(self.current_url, wait_until="networkidle")
338
+
339
+ # 1. Get entire rendered text content from the <body>
340
+ quiz_content_text = page.content()
341
+ final_quiz_url = page.url
342
+
343
+ # 2. Get ALL links on the page for the LLM to analyze
344
+ # We capture the text and the URL for every anchor tag (<a>)
345
+ all_links = page.evaluate('''() => {
346
+ const links = Array.from(document.querySelectorAll('a'));
347
+ const fileExtensions = [
348
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.zip', '.rar',
349
+ '.tar', '.gz', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.mp3',
350
+ '.mp4', '.avi', '.mov', '.json', '.xml', '.txt'
351
+ ];
352
+ return links.map(link => ({
353
+ text: link.innerText.trim(),
354
+ href: link.href
355
+ }))
356
+ .filter(link => {
357
+ const href = link.href.toLowerCase();
358
+ return fileExtensions.some(ext => href.endsWith(ext));
359
+ });
360
+ }''')
361
+ # link_data_str = json.dumps(all_links)
362
+ print('quiz content:',quiz_content_text)
363
+ print('link data:',all_links)
364
+
365
+ except Exception as e:
366
+ print(f"Navigation/Scraping failed: {e}")
367
+
368
+
369
+
370
+
371
+ # Download files
372
+ print('all_links: ',all_links)
373
+ if all_links !=[]:
374
+ print('calling download')
375
+ download= self._download_file(all_links)
376
+ else:
377
+ download=None
378
+
379
+ print('download finished')
380
+
381
+ print('Starting generate code function')
382
+
383
+ # 2. LLM Analysis and Code Generation
384
+ llm_plan = self._llm_analyze_and_generate_code(quiz_content_text,download)
385
+ if not llm_plan:
386
+ print("Could not generate a valid execution plan from LLM.")
387
+
388
+ submission_url = llm_plan['submit_url']
389
+
390
+ final_answer = self._execute_generated_code(
391
+ llm_plan['python_code'],llm_plan['dependencies']
392
+ )
393
+
394
+ if final_answer is None:
395
+ print("Failed to calculate a final answer from code execution.")
396
+ break
397
+
398
+ print('Received result of python code: ', final_answer)
399
+
400
+ print('Sending answer to submit function')
401
+
402
+ # 5. Submit the Answer
403
+
404
+ if submission_url.find("https://")==-1:
405
+ parsed = urlparse(self.current_url)
406
+ base_url = urlunparse((parsed.scheme, parsed.netloc, "", "", "", ""))
407
+ submission_url=base_url+submission_url
408
+
409
+ submit_response = self._solve_and_submit(submission_url, final_answer)
410
+
411
+ self.temp_dir.cleanup()
412
+
413
+ self.temp_dir = tempfile.TemporaryDirectory()
414
+
415
+ if submit_response.correct:
416
+ if submit_response.url:
417
+ self.current_url = submit_response.url
418
+ self.start_time= datetime.now()
419
+ print(f"Correct! Proceeding to new quiz: {self.current_url}")
420
+ repeats=0
421
+
422
+ else:
423
+ print("Quiz sequence complete!")
424
+ return "Success: Quiz sequence completed."
425
+ else:
426
+ print(f"Incorrect. Reason: {submit_response.reason}")
427
+ if submit_response.url:
428
+ if self._get_time_remaining() <= 10:
429
+ self.current_url = submit_response.url
430
+ self.start_time= datetime.now()
431
+ print(f"Skipping to new quiz because no time to redo: {self.current_url}")
432
+ repeats=0
433
+ else:
434
+ repeats+=1
435
+ if repeats<=2:
436
+ print("Repeating question")
437
+ continue
438
+ else:
439
+ if submit_response.url:
440
+ self.current_url = submit_response.url
441
+ self.start_time= datetime.now()
442
+ print(f"Skipping to new quiz after repeating: {self.current_url}")
443
+ repeats=0
444
+ else:
445
+ return "Done"
446
+
447
+
448
+ else:
449
+ repeats+=1
450
+ if repeats<=2:
451
+ print("Repeating question")
452
+ continue
453
+ else:
454
+ if submit_response.url:
455
+ self.current_url = submit_response.url
456
+ self.start_time= datetime.now()
457
+ print(f"Skipping to new quiz after repeating: {self.current_url}")
458
+ repeats=0
459
+ else:
460
+ return "Done"
461
+
462
+
463
+ # Else: Loop continues for re-submission on the same URL if time remains.
464
+
465
+ browser.close()
466
+
467
+ # if self._get_time_remaining() <= 10:
468
+ # return "Timeout: Did not complete quiz within 3 minutes."
469
+
470
+ # return "Failed to complete quiz sequence."