iitmbs24f commited on
Commit
4475f15
·
verified ·
1 Parent(s): 1a11167

Upload 9 files

Browse files
Files changed (2) hide show
  1. app/main.py +5 -1
  2. app/solver.py +43 -60
app/main.py CHANGED
@@ -67,7 +67,11 @@ class QuizRequest(BaseModel):
67
  @field_validator('url')
68
  @classmethod
69
  def validate_url(cls, v):
70
- if not v or not v.startswith(('http://', 'https://')):
 
 
 
 
71
  raise ValueError('Invalid URL format')
72
  return v
73
 
 
67
  @field_validator('url')
68
  @classmethod
69
  def validate_url(cls, v):
70
+ if not v:
71
+ raise ValueError('Invalid URL format')
72
+ # Strip whitespace (handles newlines, spaces from JSON formatting)
73
+ v = v.strip()
74
+ if not v.startswith(('http://', 'https://')):
75
  raise ValueError('Invalid URL format')
76
  return v
77
 
app/solver.py CHANGED
@@ -191,13 +191,13 @@ class BrowserHelper:
191
  try:
192
  logger.info(f"Loading page: {url}")
193
  await self.page.goto(url, wait_until='load', timeout=timeout)
194
- await asyncio.sleep(min(wait_time, 2))
195
  content = {
196
  'url': url,
197
  'title': await self.page.title(),
198
  'text': await self.page.inner_text('body'),
199
  'html': await self.page.content(),
200
- 'screenshot': await self.page.screenshot(full_page=True),
201
  }
202
  try:
203
  content['all_text'] = await self.page.evaluate("""() => {
@@ -295,18 +295,21 @@ async def ask_openrouter(prompt: str, model: Optional[str] = None, max_tokens: i
295
  "X-Title": OPENROUTER_APP_NAME,
296
  "Content-Type": "application/json",
297
  }
298
- system_content = system_prompt if system_prompt else "You are a helpful assistant that solves quiz questions accurately and concisely."
 
 
299
  payload = {
300
  "model": model,
301
  "messages": [
302
  {"role": "system", "content": system_content},
303
  {"role": "user", "content": prompt}
304
  ],
305
- "max_tokens": max_tokens,
306
- "temperature": 0.2
307
  }
308
  try:
309
- async with httpx.AsyncClient(timeout=30) as http_client:
 
310
  response = await http_client.post(url, headers=headers, json=payload)
311
  response.raise_for_status()
312
  data = response.json()
@@ -328,26 +331,12 @@ async def test_prompt_with_custom_messages(system_prompt: str, user_prompt: str,
328
 
329
  async def parse_question_with_llm(question_text: str, context: str = "") -> Optional[Dict[str, Any]]:
330
  """Use LLM to parse and understand a quiz question."""
331
- prompt = f"""Analyze this quiz question and provide a structured response:
 
332
 
333
- Question: {question_text}
334
-
335
- Context: {context}
336
-
337
- Please identify:
338
- 1. What type of question is this? (scraping, calculation, API call, data analysis, etc.)
339
- 2. What data or resources are needed?
340
- 3. What is the expected answer format? (JSON, number, text, etc.)
341
-
342
- Respond in JSON format:
343
- {{
344
- "type": "question_type",
345
- "requirements": ["requirement1", "requirement2"],
346
- "answer_format": "format_type",
347
- "reasoning": "your reasoning"
348
- }}
349
- """
350
- response = await ask_gpt(prompt)
351
  if not response:
352
  return None
353
  json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response, re.DOTALL)
@@ -389,22 +378,14 @@ async def solve_with_llm(question: str, available_data: Dict[str, Any], question
389
  # Format available_data more clearly
390
  data_str = json.dumps(available_data, indent=2) if available_data else "No additional data"
391
 
392
- prompt = f"""Solve this quiz question:
 
393
 
394
- Question: {question}
395
-
396
- Available Data:
397
- {data_str}
398
- {email_instruction}
399
- {audio_data}
400
- {format_instructions}
401
 
402
- Provide a clear, concise answer. If the answer should be in JSON format, provide valid JSON.
403
- If it's a calculation, show your work briefly.
404
- If it's a command or path, return ONLY that command or path without any explanation.
405
- If it's an audio transcription, return the spoken phrase with any codes or numbers EXACTLY as transcribed above.
406
- """
407
- return await ask_gpt(prompt, max_tokens=3000)
408
 
409
  async def ocr_image_with_llm(image_base64: str) -> Optional[str]:
410
  """Use OpenRouter vision model to extract text from an image."""
@@ -432,7 +413,8 @@ async def ocr_image_with_llm(image_base64: str) -> Optional[str]:
432
  }],
433
  "max_tokens": 1000
434
  }
435
- async with httpx.AsyncClient(timeout=60) as http_client:
 
436
  response = await http_client.post(url, headers=headers, json=payload)
437
  response.raise_for_status()
438
  data = response.json()
@@ -677,7 +659,7 @@ class MediaProcessor:
677
  """Download and transcribe audio from URL."""
678
  try:
679
  logger.info(f"Processing audio from URL: {audio_url}")
680
- response = requests.get(audio_url, timeout=30)
681
  response.raise_for_status()
682
  audio_data = response.content
683
  audio_base64 = base64.b64encode(audio_data).decode('utf-8')
@@ -696,7 +678,7 @@ class MediaProcessor:
696
  if openai_key and OPENAI_AVAILABLE:
697
  try:
698
  client = OpenAI(api_key=openai_key)
699
- response = requests.get(audio_url, timeout=30)
700
  response.raise_for_status()
701
  with tempfile.NamedTemporaryFile(suffix='.opus', delete=False) as tmp_file:
702
  tmp_file.write(response.content)
@@ -719,7 +701,7 @@ class MediaProcessor:
719
  """Process video from URL - extract frames, transcribe audio, OCR text."""
720
  try:
721
  logger.info(f"Processing video from URL: {video_url}")
722
- response = requests.get(video_url, timeout=30, stream=True)
723
  response.raise_for_status()
724
  video_info = {
725
  'url': video_url,
@@ -747,7 +729,7 @@ Provide a comprehensive description."""
747
  """Process image from URL - extract text using OCR."""
748
  try:
749
  logger.info(f"Processing image from URL: {image_url}")
750
- response = requests.get(image_url, timeout=30)
751
  response.raise_for_status()
752
  image_data = response.content
753
  image_base64 = base64.b64encode(image_data).decode('utf-8')
@@ -838,7 +820,7 @@ async def extract_image_color(image_url: str, base_url: str = '') -> Optional[st
838
  if image_url.startswith('/') and base_url:
839
  image_url = urljoin(base_url, image_url)
840
  logger.info(f"Processing image for color extraction: {image_url}")
841
- response = requests.get(image_url, timeout=30)
842
  response.raise_for_status()
843
  img = Image.open(io.BytesIO(response.content))
844
  if img.mode != 'RGB':
@@ -859,7 +841,7 @@ async def convert_csv_to_json(csv_url: str, base_url: str = '', normalize: bool
859
  if csv_url.startswith('/') and base_url:
860
  csv_url = urljoin(base_url, csv_url)
861
  logger.info(f"Converting CSV to JSON: {csv_url}")
862
- response = requests.get(csv_url, timeout=30)
863
  response.raise_for_status()
864
  df = pd.read_csv(io.StringIO(response.text))
865
  if normalize:
@@ -907,7 +889,7 @@ async def call_github_api(endpoint: str, token: Optional[str] = None) -> Optiona
907
  if token:
908
  headers['Authorization'] = f'token {token}'
909
  logger.info(f"Calling GitHub API: {url}")
910
- async with httpx.AsyncClient(timeout=30) as client:
911
  response = await client.get(url, headers=headers)
912
  response.raise_for_status()
913
  return response.json()
@@ -1061,7 +1043,7 @@ def solve_project2_png(image_url: str, base_url: str) -> str:
1061
  try:
1062
  if image_url.startswith('/'):
1063
  image_url = urljoin(base_url, image_url)
1064
- response = requests.get(image_url, timeout=30)
1065
  response.raise_for_status()
1066
  img = Image.open(io.BytesIO(response.content))
1067
  if img.mode != 'RGB':
@@ -1079,7 +1061,7 @@ def solve_project2_json(json_url: str, base_url: str) -> str:
1079
  try:
1080
  if json_url.startswith('/'):
1081
  json_url = urljoin(base_url, json_url)
1082
- response = requests.get(json_url, timeout=30)
1083
  response.raise_for_status()
1084
  data = response.json()
1085
  if isinstance(data, list):
@@ -1196,7 +1178,7 @@ def solve_project2_sql(sql_query: str, csv_url: str, base_url: str) -> str:
1196
  try:
1197
  if csv_url.startswith('/'):
1198
  csv_url = urljoin(base_url, csv_url)
1199
- response = requests.get(csv_url, timeout=30)
1200
  response.raise_for_status()
1201
  df = pd.read_csv(io.StringIO(response.text))
1202
  conn = duckdb.connect(':memory:')
@@ -1304,10 +1286,10 @@ class QuizSolver:
1304
  return {"error": "Timeout imminent - insufficient time remaining"}
1305
 
1306
  try:
1307
- # Optimize wait time based on remaining time (min 0.5s, max 1.5s) - reduced for speed
1308
- wait_time = min(1.5, max(0.5, int(remaining / 15)))
1309
  # Load the quiz page with optimized timeout - use less time for page load
1310
- page_timeout = min(12000, int(remaining * 1000 * 0.6)) # 60% of remaining time, max 12s
1311
  page_content = await self.browser.load_page(url, wait_time=wait_time, timeout=page_timeout)
1312
 
1313
  # Extract submit URL
@@ -1502,8 +1484,9 @@ class QuizSolver:
1502
  logger.info("Analyzing question type...")
1503
 
1504
  # Try to parse question with LLM first (only if we have enough time)
 
1505
  remaining = self._check_time_remaining()
1506
- if remaining >= 30.0: # Only parse with LLM if we have at least 30s remaining
1507
  parsed = await parse_question_with_llm(question, page_content.get('text', ''))
1508
  else:
1509
  parsed = None
@@ -1745,7 +1728,7 @@ class QuizSolver:
1745
  from openai import OpenAI
1746
  import tempfile
1747
  client = OpenAI(api_key=openai_key)
1748
- response = requests.get(audio_url, timeout=30)
1749
  response.raise_for_status()
1750
  with tempfile.NamedTemporaryFile(suffix='.opus', delete=False) as tmp_file:
1751
  tmp_file.write(response.content)
@@ -1926,10 +1909,10 @@ class QuizSolver:
1926
  # Use LLM more aggressively - lower thresholds to prioritize LLM solving
1927
  is_audio_question = 'transcribe' in question.lower() or 'passphrase' in question.lower() or 'spoken phrase' in question.lower()
1928
  # Very low thresholds - use LLM as primary solver whenever possible
1929
- min_time_needed = 5.0 if is_audio_question else 10.0 # Very low - use LLM whenever possible
1930
 
1931
  # Use LLM if we have enough time AND haven't found answer yet
1932
- # Reserve at least 3s for submission
1933
  if remaining >= min_time_needed:
1934
  logger.info("Attempting to solve with LLM...")
1935
  try:
@@ -1954,7 +1937,7 @@ class QuizSolver:
1954
  # Try to extract any useful information from the error
1955
  pass
1956
  else:
1957
- logger.warning(f"Skipping LLM call - insufficient time remaining ({remaining:.1f}s, need {min_time_needed}s)")
1958
 
1959
  # Strategy 8: Fallback - try to extract a simple answer from the question
1960
  # Many quiz pages have the answer in the question itself
@@ -2497,8 +2480,8 @@ class QuizSolver:
2497
  break
2498
 
2499
  logger.info(f"Downloading file: {url}")
2500
- # Use adaptive timeout based on remaining time (max 10s, min 3s) - faster
2501
- file_timeout = min(10, max(3, int(remaining * 0.4))) # Use less time for downloads
2502
  response = requests.get(url, timeout=file_timeout)
2503
  response.raise_for_status()
2504
 
 
191
  try:
192
  logger.info(f"Loading page: {url}")
193
  await self.page.goto(url, wait_until='load', timeout=timeout)
194
+ await asyncio.sleep(0.1) # Minimal wait - just enough for JS to execute
195
  content = {
196
  'url': url,
197
  'title': await self.page.title(),
198
  'text': await self.page.inner_text('body'),
199
  'html': await self.page.content(),
200
+ # Skip screenshot to save time - not needed for solving
201
  }
202
  try:
203
  content['all_text'] = await self.page.evaluate("""() => {
 
295
  "X-Title": OPENROUTER_APP_NAME,
296
  "Content-Type": "application/json",
297
  }
298
+ system_content = system_prompt if system_prompt else "You are a helpful assistant that solves quiz questions accurately and concisely. Be direct and brief."
299
+ # Optimize max_tokens - reduce for faster responses (default 1000 instead of 2000)
300
+ optimized_max_tokens = min(max_tokens, 1000) if max_tokens > 1000 else max_tokens
301
  payload = {
302
  "model": model,
303
  "messages": [
304
  {"role": "system", "content": system_content},
305
  {"role": "user", "content": prompt}
306
  ],
307
+ "max_tokens": optimized_max_tokens,
308
+ "temperature": 0.1 # Lower temperature for more deterministic, faster responses
309
  }
310
  try:
311
+ # Reduced timeout for faster responses - 15s is enough for most LLM calls
312
+ async with httpx.AsyncClient(timeout=15) as http_client:
313
  response = await http_client.post(url, headers=headers, json=payload)
314
  response.raise_for_status()
315
  data = response.json()
 
331
 
332
  async def parse_question_with_llm(question_text: str, context: str = "") -> Optional[Dict[str, Any]]:
333
  """Use LLM to parse and understand a quiz question."""
334
+ # Optimized prompt - more concise for faster processing
335
+ prompt = f"""Analyze: {question_text[:500]}
336
 
337
+ Type? Data needed? Format? JSON: {{"type":"...","requirements":[],"answer_format":"..."}}"""
338
+ # Reduced max_tokens for faster response
339
+ response = await ask_gpt(prompt, max_tokens=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  if not response:
341
  return None
342
  json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response, re.DOTALL)
 
378
  # Format available_data more clearly
379
  data_str = json.dumps(available_data, indent=2) if available_data else "No additional data"
380
 
381
+ # Optimized prompt - more concise for faster LLM processing
382
+ prompt = f"""Solve: {question}
383
 
384
+ Data: {data_str[:1000]}{email_instruction}{audio_data}{format_instructions}
 
 
 
 
 
 
385
 
386
+ Answer directly. JSON if needed. Command/path: return ONLY that. Audio: use transcription exactly."""
387
+ # Reduced max_tokens for faster response
388
+ return await ask_gpt(prompt, max_tokens=1500)
 
 
 
389
 
390
  async def ocr_image_with_llm(image_base64: str) -> Optional[str]:
391
  """Use OpenRouter vision model to extract text from an image."""
 
413
  }],
414
  "max_tokens": 1000
415
  }
416
+ # Reduced timeout for vision calls - 30s should be enough
417
+ async with httpx.AsyncClient(timeout=30) as http_client:
418
  response = await http_client.post(url, headers=headers, json=payload)
419
  response.raise_for_status()
420
  data = response.json()
 
659
  """Download and transcribe audio from URL."""
660
  try:
661
  logger.info(f"Processing audio from URL: {audio_url}")
662
+ response = requests.get(audio_url, timeout=15)
663
  response.raise_for_status()
664
  audio_data = response.content
665
  audio_base64 = base64.b64encode(audio_data).decode('utf-8')
 
678
  if openai_key and OPENAI_AVAILABLE:
679
  try:
680
  client = OpenAI(api_key=openai_key)
681
+ response = requests.get(audio_url, timeout=15)
682
  response.raise_for_status()
683
  with tempfile.NamedTemporaryFile(suffix='.opus', delete=False) as tmp_file:
684
  tmp_file.write(response.content)
 
701
  """Process video from URL - extract frames, transcribe audio, OCR text."""
702
  try:
703
  logger.info(f"Processing video from URL: {video_url}")
704
+ response = requests.get(video_url, timeout=15, stream=True)
705
  response.raise_for_status()
706
  video_info = {
707
  'url': video_url,
 
729
  """Process image from URL - extract text using OCR."""
730
  try:
731
  logger.info(f"Processing image from URL: {image_url}")
732
+ response = requests.get(image_url, timeout=15)
733
  response.raise_for_status()
734
  image_data = response.content
735
  image_base64 = base64.b64encode(image_data).decode('utf-8')
 
820
  if image_url.startswith('/') and base_url:
821
  image_url = urljoin(base_url, image_url)
822
  logger.info(f"Processing image for color extraction: {image_url}")
823
+ response = requests.get(image_url, timeout=15)
824
  response.raise_for_status()
825
  img = Image.open(io.BytesIO(response.content))
826
  if img.mode != 'RGB':
 
841
  if csv_url.startswith('/') and base_url:
842
  csv_url = urljoin(base_url, csv_url)
843
  logger.info(f"Converting CSV to JSON: {csv_url}")
844
+ response = requests.get(csv_url, timeout=15)
845
  response.raise_for_status()
846
  df = pd.read_csv(io.StringIO(response.text))
847
  if normalize:
 
889
  if token:
890
  headers['Authorization'] = f'token {token}'
891
  logger.info(f"Calling GitHub API: {url}")
892
+ async with httpx.AsyncClient(timeout=15) as client:
893
  response = await client.get(url, headers=headers)
894
  response.raise_for_status()
895
  return response.json()
 
1043
  try:
1044
  if image_url.startswith('/'):
1045
  image_url = urljoin(base_url, image_url)
1046
+ response = requests.get(image_url, timeout=15)
1047
  response.raise_for_status()
1048
  img = Image.open(io.BytesIO(response.content))
1049
  if img.mode != 'RGB':
 
1061
  try:
1062
  if json_url.startswith('/'):
1063
  json_url = urljoin(base_url, json_url)
1064
+ response = requests.get(json_url, timeout=15)
1065
  response.raise_for_status()
1066
  data = response.json()
1067
  if isinstance(data, list):
 
1178
  try:
1179
  if csv_url.startswith('/'):
1180
  csv_url = urljoin(base_url, csv_url)
1181
+ response = requests.get(csv_url, timeout=15)
1182
  response.raise_for_status()
1183
  df = pd.read_csv(io.StringIO(response.text))
1184
  conn = duckdb.connect(':memory:')
 
1286
  return {"error": "Timeout imminent - insufficient time remaining"}
1287
 
1288
  try:
1289
+ # Minimal wait time - just enough for page to load
1290
+ wait_time = 0.1 # Fixed minimal wait - no dynamic calculation needed
1291
  # Load the quiz page with optimized timeout - use less time for page load
1292
+ page_timeout = min(8000, int(remaining * 1000 * 0.4)) # 40% of remaining time, max 8s (reduced from 12s)
1293
  page_content = await self.browser.load_page(url, wait_time=wait_time, timeout=page_timeout)
1294
 
1295
  # Extract submit URL
 
1484
  logger.info("Analyzing question type...")
1485
 
1486
  # Try to parse question with LLM first (only if we have enough time)
1487
+ # Reduced threshold - parse even with less time for better adaptability
1488
  remaining = self._check_time_remaining()
1489
+ if remaining >= 10.0: # Reduced from 30s to 10s - parse faster
1490
  parsed = await parse_question_with_llm(question, page_content.get('text', ''))
1491
  else:
1492
  parsed = None
 
1728
  from openai import OpenAI
1729
  import tempfile
1730
  client = OpenAI(api_key=openai_key)
1731
+ response = requests.get(audio_url, timeout=15)
1732
  response.raise_for_status()
1733
  with tempfile.NamedTemporaryFile(suffix='.opus', delete=False) as tmp_file:
1734
  tmp_file.write(response.content)
 
1909
  # Use LLM more aggressively - lower thresholds to prioritize LLM solving
1910
  is_audio_question = 'transcribe' in question.lower() or 'passphrase' in question.lower() or 'spoken phrase' in question.lower()
1911
  # Very low thresholds - use LLM as primary solver whenever possible
1912
+ min_time_needed = 3.0 if is_audio_question else 5.0 # Reduced further - use LLM more aggressively
1913
 
1914
  # Use LLM if we have enough time AND haven't found answer yet
1915
+ # Reduced threshold - use LLM more aggressively for adaptability
1916
  if remaining >= min_time_needed:
1917
  logger.info("Attempting to solve with LLM...")
1918
  try:
 
1937
  # Try to extract any useful information from the error
1938
  pass
1939
  else:
1940
+ logger.debug(f"Skipping LLM call - insufficient time remaining ({remaining:.1f}s, need {min_time_needed}s)")
1941
 
1942
  # Strategy 8: Fallback - try to extract a simple answer from the question
1943
  # Many quiz pages have the answer in the question itself
 
2480
  break
2481
 
2482
  logger.info(f"Downloading file: {url}")
2483
+ # Use adaptive timeout based on remaining time (max 8s, min 2s) - faster
2484
+ file_timeout = min(8, max(2, int(remaining * 0.3))) # Use less time for downloads
2485
  response = requests.get(url, timeout=file_timeout)
2486
  response.raise_for_status()
2487