Realistvik0 commited on
Commit
f9e27e9
·
verified ·
1 Parent(s): 46c0259

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +110 -8
app.py CHANGED
@@ -17,6 +17,7 @@ from tools.calculator import Calculator
17
  from tools.final_answer import FinalAnswerTool
18
  from tools.visit_webpage import VisitWebpageTool
19
  from tools.web_search import DuckDuckGoSearchTool
 
20
 
21
  from research_state import ResearchState
22
  from verifier import verify_research
@@ -40,7 +41,7 @@ MAX_TOKENS = 900
40
  TEMPERATURE = 0.2
41
 
42
  # Number of questions to skip from the start of the list (set to 0 to run all)
43
- SKIP_FIRST_N = 1
44
 
45
  # Pacing / rate-limit handling
46
  MIN_SECONDS_BETWEEN_QUESTIONS = 60
@@ -267,6 +268,66 @@ class DailyQuotaExhaustedError(Exception):
267
  pass
268
 
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  def _is_rate_limit_error(exc: Exception) -> bool:
271
  """
272
  Detect rate-limit / quota errors across providers, since exact
@@ -317,11 +378,15 @@ def _run_agent_with_backoff(agent_runner, question_text,
317
  backoff if a transient rate-limit error is hit. If the error looks
318
  like a daily/per-day quota exhaustion, raises DailyQuotaExhaustedError
319
  immediately instead of retrying, since backoff won't help.
320
- Non-rate-limit errors are raised immediately (handled by the caller).
 
 
321
  """
322
  for attempt in range(max_retries + 1):
323
  try:
324
  return agent_runner(question_text)
 
 
325
  except Exception as e:
326
  if _is_daily_quota_exhausted(e):
327
  raise DailyQuotaExhaustedError(str(e)) from e
@@ -361,6 +426,8 @@ verify_research_tool = VerifyResearchTool()
361
 
362
  final_answer = FinalAnswerTool()
363
 
 
 
364
  youtube_video_analysis = YouTubeVideoAnalysisTool(
365
  model="gemini-2.5-flash"
366
  )
@@ -371,6 +438,7 @@ tools = [
371
  calculator,
372
  verify_research_tool,
373
  youtube_transcript,
 
374
  final_answer,
375
  youtube_video_analysis,
376
  ]
@@ -445,9 +513,11 @@ class ResearchAgentWrapper:
445
  BasicAgent: agent(question) -> answer string.
446
  Resets the shared research context for every new question
447
  so state doesn't leak between questions.
 
 
448
  """
449
 
450
- def __init__(self, agent: CodeAgent):
451
  self.agent = agent
452
  print("ResearchAgentWrapper initialized.")
453
 
@@ -457,7 +527,10 @@ class ResearchAgentWrapper:
457
  research_context.start(question)
458
  answer = self.agent.run(question)
459
  print(f"Agent returning answer (first 50 chars): {str(answer)[:50]}...")
460
- return str(answer)
 
 
 
461
 
462
 
463
  # =========================================================
@@ -534,22 +607,51 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
534
 
535
  start_time = time.monotonic()
536
 
 
 
 
 
 
 
 
 
 
 
537
  try:
538
- submitted_answer = _run_agent_with_backoff(agent_runner, question_text)
539
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
540
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  except DailyQuotaExhaustedError as e:
542
  print(f"Daily quota exhausted, stopping run early: {e}")
543
  results_log.append({
544
  "Task ID": task_id,
545
  "Question": question_text,
546
- "Submitted Answer": f"SKIPPED (daily quota exhausted): {e}",
 
547
  })
548
  quota_exhausted = True
549
  break
550
  except Exception as e:
551
  print(f"Error running agent on task {task_id}: {e}")
552
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
 
 
 
 
 
553
 
554
  # Pace requests: ensure at least MIN_SECONDS_BETWEEN_QUESTIONS
555
  # seconds between question starts, accounting for however long
 
17
  from tools.final_answer import FinalAnswerTool
18
  from tools.visit_webpage import VisitWebpageTool
19
  from tools.web_search import DuckDuckGoSearchTool
20
+ from tools.attachment_reader import AttachmentReaderTool, download_attachment
21
 
22
  from research_state import ResearchState
23
  from verifier import verify_research
 
41
  TEMPERATURE = 0.2
42
 
43
  # Number of questions to skip from the start of the list (set to 0 to run all)
44
+ SKIP_FIRST_N = 0
45
 
46
  # Pacing / rate-limit handling
47
  MIN_SECONDS_BETWEEN_QUESTIONS = 60
 
268
  pass
269
 
270
 
271
+ class NonAnswerError(Exception):
272
+ """Raised when the agent returns something that is not a real answer
273
+ (a refusal, an apology, an error message, empty output, etc.)."""
274
+ pass
275
+
276
+
277
+ # Prefixes that indicate the agent is not giving an answer.
278
+ NON_ANSWER_PREFIXES = (
279
+ "i am sorry",
280
+ "i'm sorry",
281
+ "sorry,",
282
+ "i cannot",
283
+ "i can't",
284
+ "i am unable",
285
+ "i'm unable",
286
+ "unable to",
287
+ "error:",
288
+ "agent error",
289
+ "skipped",
290
+ "traceback",
291
+ "as an ai",
292
+ )
293
+
294
+ # Phrases that indicate a failure anywhere in the text.
295
+ NON_ANSWER_PHRASES = (
296
+ "i cannot directly access",
297
+ "cannot access local files",
298
+ "unable to access",
299
+ "please provide the data",
300
+ "please provide the file",
301
+ "i do not have access",
302
+ "i don't have access",
303
+ "reached max steps",
304
+ "max steps reached",
305
+ "could not find an answer",
306
+ "i was unable to",
307
+ "no answer found",
308
+ )
309
+
310
+
311
+ def _check_is_real_answer(answer) -> None:
312
+ """Raise NonAnswerError if `answer` is not a usable answer."""
313
+ if answer is None:
314
+ raise NonAnswerError("Agent returned None.")
315
+
316
+ text = str(answer).strip()
317
+ if not text:
318
+ raise NonAnswerError("Agent returned an empty answer.")
319
+
320
+ lowered = text.lower()
321
+
322
+ for prefix in NON_ANSWER_PREFIXES:
323
+ if lowered.startswith(prefix):
324
+ raise NonAnswerError(f"Answer looks like a refusal/error: {text[:200]}")
325
+
326
+ for phrase in NON_ANSWER_PHRASES:
327
+ if phrase in lowered:
328
+ raise NonAnswerError(f"Answer looks like a refusal/error: {text[:200]}")
329
+
330
+
331
  def _is_rate_limit_error(exc: Exception) -> bool:
332
  """
333
  Detect rate-limit / quota errors across providers, since exact
 
378
  backoff if a transient rate-limit error is hit. If the error looks
379
  like a daily/per-day quota exhaustion, raises DailyQuotaExhaustedError
380
  immediately instead of retrying, since backoff won't help.
381
+ NonAnswerError is re-raised untouched (a refusal that happens to
382
+ mention "quota" or "429" must not be mistaken for a rate limit).
383
+ Other errors are raised immediately (handled by the caller).
384
  """
385
  for attempt in range(max_retries + 1):
386
  try:
387
  return agent_runner(question_text)
388
+ except NonAnswerError:
389
+ raise # not a rate-limit issue, don't inspect or retry
390
  except Exception as e:
391
  if _is_daily_quota_exhausted(e):
392
  raise DailyQuotaExhaustedError(str(e)) from e
 
426
 
427
  final_answer = FinalAnswerTool()
428
 
429
+ attachment_reader = AttachmentReaderTool()
430
+
431
  youtube_video_analysis = YouTubeVideoAnalysisTool(
432
  model="gemini-2.5-flash"
433
  )
 
438
  calculator,
439
  verify_research_tool,
440
  youtube_transcript,
441
+ attachment_reader,
442
  final_answer,
443
  youtube_video_analysis,
444
  ]
 
513
  BasicAgent: agent(question) -> answer string.
514
  Resets the shared research context for every new question
515
  so state doesn't leak between questions.
516
+ Raises NonAnswerError instead of returning anything that
517
+ isn't a real answer.
518
  """
519
 
520
+ def __init__(self, agent):
521
  self.agent = agent
522
  print("ResearchAgentWrapper initialized.")
523
 
 
527
  research_context.start(question)
528
  answer = self.agent.run(question)
529
  print(f"Agent returning answer (first 50 chars): {str(answer)[:50]}...")
530
+
531
+ # Never let a non-answer leave the wrapper.
532
+ _check_is_real_answer(answer)
533
+ return str(answer).strip()
534
 
535
 
536
  # =========================================================
 
607
 
608
  start_time = time.monotonic()
609
 
610
+ # Download the attachment (if the task has one) and point the agent at it.
611
+ # download_attachment never raises; it returns None if there is no file.
612
+ attachment_path = download_attachment(api_url, task_id)
613
+ agent_question = question_text
614
+ if attachment_path:
615
+ agent_question += (
616
+ f"\n\nAn attached file for this task is saved locally at: "
617
+ f"{attachment_path}\nUse the read_attachment tool to read it."
618
+ )
619
+
620
  try:
621
+ submitted_answer = _run_agent_with_backoff(agent_runner, agent_question)
622
  answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
623
+ results_log.append({
624
+ "Task ID": task_id,
625
+ "Question": question_text,
626
+ "Submitted Answer": submitted_answer,
627
+ "Status": "OK",
628
+ })
629
+ except NonAnswerError as e:
630
+ print(f"No valid answer for task {task_id}, not submitting: {e}")
631
+ results_log.append({
632
+ "Task ID": task_id,
633
+ "Question": question_text,
634
+ "Submitted Answer": "",
635
+ "Status": f"NO ANSWER: {e}",
636
+ })
637
  except DailyQuotaExhaustedError as e:
638
  print(f"Daily quota exhausted, stopping run early: {e}")
639
  results_log.append({
640
  "Task ID": task_id,
641
  "Question": question_text,
642
+ "Submitted Answer": "",
643
+ "Status": f"SKIPPED (daily quota exhausted): {e}",
644
  })
645
  quota_exhausted = True
646
  break
647
  except Exception as e:
648
  print(f"Error running agent on task {task_id}: {e}")
649
+ results_log.append({
650
+ "Task ID": task_id,
651
+ "Question": question_text,
652
+ "Submitted Answer": "",
653
+ "Status": f"AGENT ERROR: {e}",
654
+ })
655
 
656
  # Pace requests: ensure at least MIN_SECONDS_BETWEEN_QUESTIONS
657
  # seconds between question starts, accounting for however long