czrrr commited on
Commit
c991e6f
·
verified ·
1 Parent(s): 82e979c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -64
app.py CHANGED
@@ -10,10 +10,10 @@ import pandas as pd
10
  import requests
11
  from litellm import completion
12
  from smolagents import (
13
- CodeAgent,
14
  DuckDuckGoSearchTool,
15
  LiteLLMModel,
16
  Tool,
 
17
  WikipediaSearchTool,
18
  )
19
 
@@ -22,7 +22,7 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
  RESULT_COLUMNS = ["Task ID", "Question", "Submitted Answer"]
23
  HTTP_TIMEOUT = 45
24
  MAX_EXTRACTED_CHARS = 35_000
25
- DEFAULT_HF_MODEL = "huggingface/openai/gpt-oss-120b"
26
  DEFAULT_GROQ_REVIEW_MODEL = "groq/qwen/qwen3.6-27b"
27
  TASK_FILE_CACHE = {}
28
 
@@ -210,7 +210,7 @@ def extract_attachment_text(data: bytes, filename: str) -> str:
210
  else:
211
  client = InferenceClient(api_key=token, provider="auto")
212
  asr_model = os.getenv(
213
- "GAIA_ASR_MODEL", "openai/whisper-large-v3"
214
  )
215
  transcript = client.automatic_speech_recognition(
216
  data, model=asr_model
@@ -401,8 +401,6 @@ class AnalyzeGaiaImageTool(Tool):
401
  output_type = "string"
402
 
403
  def forward(self, task_id: str, question: str) -> str:
404
- from openai import OpenAI
405
-
406
  token = os.getenv("HF_TOKEN")
407
  if not token:
408
  return "Image analysis failed: HF_TOKEN is not configured."
@@ -419,41 +417,48 @@ class AnalyzeGaiaImageTool(Tool):
419
  }.get(suffix, "image/png")
420
  encoded = base64.b64encode(data).decode("ascii")
421
 
422
- client = OpenAI(
423
- base_url="https://router.huggingface.co/v1",
424
- api_key=token,
425
- )
426
  vision_model = os.getenv(
427
  "GAIA_VISION_MODEL",
428
  "Qwen/Qwen3-VL-235B-A22B-Instruct:cheapest",
429
  )
430
- result = client.chat.completions.create(
431
- model=vision_model,
432
- messages=[
433
- {
434
- "role": "user",
435
- "content": [
436
- {
437
- "type": "text",
438
- "text": (
439
- "Analyze the supplied image carefully and "
440
- "answer this task. Explain visual evidence "
441
- f"briefly so another agent can verify it:\n{question}"
442
- ),
443
- },
444
- {
445
- "type": "image_url",
446
- "image_url": {
447
- "url": f"data:{mime};base64,{encoded}"
 
 
448
  },
449
- },
450
- ],
451
- }
452
- ],
453
- temperature=0,
454
- max_tokens=600,
 
 
 
 
 
 
 
455
  )
456
- return str(result.choices[0].message.content).strip()
 
 
457
  except Exception as exc:
458
  return f"Could not analyze the GAIA image: {exc}"
459
 
@@ -463,18 +468,26 @@ class BasicAgent:
463
  print("Inicializando o agente GAIA...")
464
 
465
  hf_token = os.getenv("HF_TOKEN")
 
466
  configured_model = os.getenv("GAIA_MODEL_ID")
467
 
468
- model_id = configured_model or DEFAULT_HF_MODEL
469
- if not hf_token:
 
 
 
 
 
 
 
470
  raise RuntimeError(
471
- "O secret HF_TOKEN não está configurado. "
472
  "Adicione a chave em Settings > Variables and secrets > Secrets."
473
  )
474
 
475
  self.model = LiteLLMModel(
476
  model_id=model_id,
477
- api_key=hf_token,
478
  temperature=0,
479
  max_tokens=2_000,
480
  )
@@ -511,21 +524,13 @@ class BasicAgent:
511
  AnalyzeGaiaImageTool(),
512
  ]
513
 
514
- self.agent = CodeAgent(
 
 
515
  tools=agent_tools,
516
  model=self.model,
517
  max_steps=10,
518
  planning_interval=None,
519
- additional_authorized_imports=[
520
- "collections",
521
- "datetime",
522
- "itertools",
523
- "math",
524
- "re",
525
- "statistics",
526
- "unicodedata",
527
- ],
528
- max_print_outputs_length=20_000,
529
  description=(
530
  "Agent designed to solve GAIA benchmark questions with "
531
  "exact-match answers."
@@ -555,16 +560,8 @@ can be opened. Never invent a tool, use subprocess, or use shell commands.
555
  Do not repeat nearly identical searches; change the source or method.
556
 
557
  FINAL RESPONSE POLICY:
558
- Call final_answer with only the requested value. The final action must always
559
- be valid executable code inside the required code tags, for example:
560
- <code>
561
- final_answer("Claus")
562
- </code>
563
- or:
564
- <code>
565
- final_answer(5)
566
- </code>
567
- Never write the answer as plain text outside a final_answer tool call. Never
568
  include reasoning, explanations, labels, Markdown, citations, or the words
569
  "FINAL ANSWER" inside the submitted value.
570
  - Quantity/count: return only the number, unless units or currency are requested.
@@ -617,7 +614,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
617
  "Inference Providers."
618
  ) from exc
619
  raise
620
- candidate = self.format_exact_answer(question, str(result))
621
  return self.review_answer_with_groq(
622
  question=question,
623
  candidate=candidate,
@@ -628,8 +625,20 @@ include reasoning, explanations, labels, Markdown, citations, or the words
628
  def deterministic_answer_cleanup(answer: str) -> str:
629
  """Remove embalagens comuns sem alterar o conteúdo da resposta."""
630
  text = str(answer or "").strip()
 
631
  text = re.sub(r"^```(?:text|markdown)?\s*", "", text, flags=re.I)
632
  text = re.sub(r"\s*```$", "", text)
 
 
 
 
 
 
 
 
 
 
 
633
 
634
  marker_pattern = re.compile(
635
  r"(?:final\s+answer|answer|resposta\s+final|resposta)\s*:\s*",
@@ -647,6 +656,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
647
  for pattern in prefix_patterns:
648
  text = re.sub(pattern, "", text, flags=re.I).strip()
649
 
 
650
  if (
651
  len(text) >= 2
652
  and text[0] == text[-1]
@@ -656,6 +666,119 @@ include reasoning, explanations, labels, Markdown, citations, or the words
656
 
657
  return text.replace("FINAL ANSWER", "").strip()
658
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  def format_exact_answer(self, question: str, raw_answer: str) -> str:
660
  """Limpa o resultado mecanicamente, sem pedir a outro modelo para alterá-lo."""
661
  del question
@@ -692,7 +815,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
692
  """Solicita uma segunda opinião gratuita no Groq, com fallback local."""
693
  groq_api_key = os.getenv("GROQ_API_KEY")
694
  if not groq_api_key:
695
- fallback = self.deterministic_answer_cleanup(candidate)
696
  print("Groq review status: SKIPPED — GROQ_API_KEY is missing")
697
  print(f"Primary answer preserved: {fallback}")
698
  return fallback
@@ -700,6 +823,8 @@ include reasoning, explanations, labels, Markdown, citations, or the words
700
  reviewer_model = os.getenv(
701
  "GAIA_GROQ_REVIEW_MODEL", DEFAULT_GROQ_REVIEW_MODEL
702
  )
 
 
703
  review_prompt = f"""
704
  You are the mandatory final reviewer for a GAIA exact-match answer.
705
 
@@ -772,8 +897,8 @@ Do not return JSON. Do not wrap the fields in Markdown or a code block.
772
  raise ValueError(
773
  "Groq did not return the <final_answer> field."
774
  )
775
- final_answer = self.deterministic_answer_cleanup(
776
- answer_match.group(1)
777
  )
778
  if not final_answer:
779
  raise ValueError("Groq returned an empty final_answer.")
@@ -792,7 +917,7 @@ Do not return JSON. Do not wrap the fields in Markdown or a code block.
792
  except Exception as exc:
793
  last_error = exc
794
 
795
- fallback = self.deterministic_answer_cleanup(candidate)
796
  if not fallback:
797
  raise RuntimeError(
798
  "A revisão do Groq falhou e a resposta primária estava vazia. "
 
10
  import requests
11
  from litellm import completion
12
  from smolagents import (
 
13
  DuckDuckGoSearchTool,
14
  LiteLLMModel,
15
  Tool,
16
+ ToolCallingAgent,
17
  WikipediaSearchTool,
18
  )
19
 
 
22
  RESULT_COLUMNS = ["Task ID", "Question", "Submitted Answer"]
23
  HTTP_TIMEOUT = 45
24
  MAX_EXTRACTED_CHARS = 35_000
25
+ DEFAULT_MAIN_MODEL = "groq/qwen/qwen3.6-27b"
26
  DEFAULT_GROQ_REVIEW_MODEL = "groq/qwen/qwen3.6-27b"
27
  TASK_FILE_CACHE = {}
28
 
 
210
  else:
211
  client = InferenceClient(api_key=token, provider="auto")
212
  asr_model = os.getenv(
213
+ "GAIA_ASR_MODEL", "distil-whisper/distil-large-v3"
214
  )
215
  transcript = client.automatic_speech_recognition(
216
  data, model=asr_model
 
401
  output_type = "string"
402
 
403
  def forward(self, task_id: str, question: str) -> str:
 
 
404
  token = os.getenv("HF_TOKEN")
405
  if not token:
406
  return "Image analysis failed: HF_TOKEN is not configured."
 
417
  }.get(suffix, "image/png")
418
  encoded = base64.b64encode(data).decode("ascii")
419
 
 
 
 
 
420
  vision_model = os.getenv(
421
  "GAIA_VISION_MODEL",
422
  "Qwen/Qwen3-VL-235B-A22B-Instruct:cheapest",
423
  )
424
+ response = requests.post(
425
+ "https://router.huggingface.co/v1/chat/completions",
426
+ headers={
427
+ "Authorization": f"Bearer {token}",
428
+ "Content-Type": "application/json",
429
+ },
430
+ json={
431
+ "model": vision_model,
432
+ "messages": [
433
+ {
434
+ "role": "user",
435
+ "content": [
436
+ {
437
+ "type": "text",
438
+ "text": (
439
+ "Analyze the supplied image carefully and "
440
+ "answer this task. Explain visual evidence "
441
+ "briefly so another agent can verify it:\n"
442
+ f"{question}"
443
+ ),
444
  },
445
+ {
446
+ "type": "image_url",
447
+ "image_url": {
448
+ "url": f"data:{mime};base64,{encoded}"
449
+ },
450
+ },
451
+ ],
452
+ }
453
+ ],
454
+ "temperature": 0,
455
+ "max_tokens": 600,
456
+ },
457
+ timeout=HTTP_TIMEOUT,
458
  )
459
+ response.raise_for_status()
460
+ payload = response.json()
461
+ return str(payload["choices"][0]["message"]["content"]).strip()
462
  except Exception as exc:
463
  return f"Could not analyze the GAIA image: {exc}"
464
 
 
468
  print("Inicializando o agente GAIA...")
469
 
470
  hf_token = os.getenv("HF_TOKEN")
471
+ groq_api_key = os.getenv("GROQ_API_KEY")
472
  configured_model = os.getenv("GAIA_MODEL_ID")
473
 
474
+ model_id = configured_model or DEFAULT_MAIN_MODEL
475
+ if not model_id.lower().startswith("groq/qwen/"):
476
+ print(
477
+ "GAIA_MODEL_ID não apontava para um modelo Qwen no Groq "
478
+ "e foi ignorado. "
479
+ f"Usando {DEFAULT_MAIN_MODEL}."
480
+ )
481
+ model_id = DEFAULT_MAIN_MODEL
482
+ if not groq_api_key:
483
  raise RuntimeError(
484
+ "O secret GROQ_API_KEY não está configurado. "
485
  "Adicione a chave em Settings > Variables and secrets > Secrets."
486
  )
487
 
488
  self.model = LiteLLMModel(
489
  model_id=model_id,
490
+ api_key=groq_api_key,
491
  temperature=0,
492
  max_tokens=2_000,
493
  )
 
524
  AnalyzeGaiaImageTool(),
525
  ]
526
 
527
+ # Qwen returns native tool calls. ToolCallingAgent handles that
528
+ # structured format without parsing generated Python code.
529
+ self.agent = ToolCallingAgent(
530
  tools=agent_tools,
531
  model=self.model,
532
  max_steps=10,
533
  planning_interval=None,
 
 
 
 
 
 
 
 
 
 
534
  description=(
535
  "Agent designed to solve GAIA benchmark questions with "
536
  "exact-match answers."
 
560
  Do not repeat nearly identical searches; change the source or method.
561
 
562
  FINAL RESPONSE POLICY:
563
+ Call the final_answer tool with only the requested value. Never write the
564
+ answer as plain text instead of calling final_answer. Never
 
 
 
 
 
 
 
 
565
  include reasoning, explanations, labels, Markdown, citations, or the words
566
  "FINAL ANSWER" inside the submitted value.
567
  - Quantity/count: return only the number, unless units or currency are requested.
 
614
  "Inference Providers."
615
  ) from exc
616
  raise
617
+ candidate = self.enforce_direct_answer(question, str(result))
618
  return self.review_answer_with_groq(
619
  question=question,
620
  candidate=candidate,
 
625
  def deterministic_answer_cleanup(answer: str) -> str:
626
  """Remove embalagens comuns sem alterar o conteúdo da resposta."""
627
  text = str(answer or "").strip()
628
+ text = re.sub(r"</?code>", "", text, flags=re.I).strip()
629
  text = re.sub(r"^```(?:text|markdown)?\s*", "", text, flags=re.I)
630
  text = re.sub(r"\s*```$", "", text)
631
+ text = re.sub(r"^\s*#{1,6}\s*", "", text)
632
+ text = re.sub(r"^\s*[-*•]\s+", "", text)
633
+ text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
634
+
635
+ final_call = re.search(
636
+ r"final_answer\s*\(\s*([\"']?)(.*?)\1\s*\)\s*$",
637
+ text,
638
+ flags=re.I | re.S,
639
+ )
640
+ if final_call:
641
+ text = final_call.group(2).strip()
642
 
643
  marker_pattern = re.compile(
644
  r"(?:final\s+answer|answer|resposta\s+final|resposta)\s*:\s*",
 
656
  for pattern in prefix_patterns:
657
  text = re.sub(pattern, "", text, flags=re.I).strip()
658
 
659
+ text = text.replace("**", "").replace("__", "").strip()
660
  if (
661
  len(text) >= 2
662
  and text[0] == text[-1]
 
666
 
667
  return text.replace("FINAL ANSWER", "").strip()
668
 
669
+ @classmethod
670
+ def enforce_direct_answer(cls, question: str, answer: str) -> str:
671
+ """Impõe o formato exact-match sem pedir nova interpretação a uma LLM."""
672
+ original_text = str(answer or "")
673
+ bold_values = [
674
+ value.strip()
675
+ for value in re.findall(r"\*\*(.+?)\*\*", original_text, flags=re.S)
676
+ if value.strip()
677
+ ]
678
+ text = cls.deterministic_answer_cleanup(answer)
679
+ question_lower = str(question or "").lower()
680
+
681
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
682
+ if len(lines) > 1:
683
+ # Para listas, privilegia a linha que realmente contém os itens.
684
+ if "comma" in question_lower or "vírgula" in question_lower:
685
+ comma_lines = [line for line in lines if "," in line]
686
+ if comma_lines:
687
+ text = max(comma_lines, key=lambda value: value.count(","))
688
+ else:
689
+ text = lines[-1]
690
+ else:
691
+ text = lines[-1]
692
+
693
+ text = cls.deterministic_answer_cleanup(text)
694
+
695
+ quantity_question = (
696
+ "how many" in question_lower
697
+ or "numeric output" in question_lower
698
+ or "quantos" in question_lower
699
+ or "quantas" in question_lower
700
+ )
701
+ if quantity_question:
702
+ numbers = re.findall(
703
+ r"(?<![\w.])-?\d+(?:,\d{3})*(?:\.\d+)?", text
704
+ )
705
+ if numbers:
706
+ return numbers[-1].replace(",", "")
707
+
708
+ requests_usd = (
709
+ "in usd" in question_lower
710
+ or "usd with" in question_lower
711
+ or "dollars" in question_lower
712
+ )
713
+ if requests_usd:
714
+ amounts = re.findall(
715
+ r"\$?\s*(-?\d+(?:,\d{3})*(?:\.\d+)?)", text
716
+ )
717
+ if amounts:
718
+ raw_amount = amounts[-1].replace(",", "")
719
+ try:
720
+ return f"${float(raw_amount):,.2f}"
721
+ except ValueError:
722
+ pass
723
+
724
+ person_question = (
725
+ question_lower.startswith("who ")
726
+ or " who " in f" {question_lower} "
727
+ or "first name" in question_lower
728
+ or "surname" in question_lower
729
+ or "username" in question_lower
730
+ )
731
+ if person_question:
732
+ # Explanatory answers often repeat the requested person in the
733
+ # final bold fragment. Prefer it before trying sentence patterns.
734
+ if bold_values:
735
+ emphasized = cls.deterministic_answer_cleanup(bold_values[-1])
736
+ if (
737
+ emphasized
738
+ and len(emphasized) <= 100
739
+ and not re.search(r"[.!?]\s+\w", emphasized)
740
+ ):
741
+ return emphasized.strip(" .,:;\"'")
742
+
743
+ person_patterns = [
744
+ r"\b(?:nominated|written|directed|created|founded|authored|performed)"
745
+ r"\s+by\s+([A-Z][\w'’-]*(?:\s+[A-Z][\w'’-]*){0,3})",
746
+ r"\b(?:username|first\s+name|surname|name)\s+(?:is|was)\s+"
747
+ r"([A-Z][\w'’-]*(?:\s+[A-Z][\w'’-]*){0,3})",
748
+ ]
749
+ matches = []
750
+ for pattern in person_patterns:
751
+ matches.extend(re.findall(pattern, text))
752
+ if matches:
753
+ return matches[-1].strip(" .,:;\"'")
754
+
755
+ # Remove frases introdutórias que ainda possam aparecer em uma linha.
756
+ text = re.sub(
757
+ r"^(?:therefore,\s*|thus,\s*|so,\s*)?"
758
+ r"(?:the\s+)?(?:correct\s+|final\s+)?answer\s+is\s+",
759
+ "",
760
+ text,
761
+ flags=re.I,
762
+ ).strip()
763
+ text = re.sub(
764
+ r"^(?:the\s+requested\s+)?"
765
+ r"(?:first\s+name|surname|city|country|ioc\s+code)\s+is\s+",
766
+ "",
767
+ text,
768
+ flags=re.I,
769
+ ).strip()
770
+
771
+ # Se ainda restar uma explicação seguida de dois-pontos, conserva o valor.
772
+ if ":" in text:
773
+ prefix, value = text.rsplit(":", 1)
774
+ if len(value.strip()) <= 250 and any(
775
+ cue in prefix.lower()
776
+ for cue in ("answer", "resposta", "result", "resultado")
777
+ ):
778
+ text = value.strip()
779
+
780
+ return cls.deterministic_answer_cleanup(text)
781
+
782
  def format_exact_answer(self, question: str, raw_answer: str) -> str:
783
  """Limpa o resultado mecanicamente, sem pedir a outro modelo para alterá-lo."""
784
  del question
 
815
  """Solicita uma segunda opinião gratuita no Groq, com fallback local."""
816
  groq_api_key = os.getenv("GROQ_API_KEY")
817
  if not groq_api_key:
818
+ fallback = self.enforce_direct_answer(question, candidate)
819
  print("Groq review status: SKIPPED — GROQ_API_KEY is missing")
820
  print(f"Primary answer preserved: {fallback}")
821
  return fallback
 
823
  reviewer_model = os.getenv(
824
  "GAIA_GROQ_REVIEW_MODEL", DEFAULT_GROQ_REVIEW_MODEL
825
  )
826
+ if not reviewer_model.lower().startswith("groq/qwen/"):
827
+ reviewer_model = DEFAULT_GROQ_REVIEW_MODEL
828
  review_prompt = f"""
829
  You are the mandatory final reviewer for a GAIA exact-match answer.
830
 
 
897
  raise ValueError(
898
  "Groq did not return the <final_answer> field."
899
  )
900
+ final_answer = self.enforce_direct_answer(
901
+ question, answer_match.group(1)
902
  )
903
  if not final_answer:
904
  raise ValueError("Groq returned an empty final_answer.")
 
917
  except Exception as exc:
918
  last_error = exc
919
 
920
+ fallback = self.enforce_direct_answer(question, candidate)
921
  if not fallback:
922
  raise RuntimeError(
923
  "A revisão do Groq falhou e a resposta primária estava vazia. "