czrrr commited on
Commit
43960d9
·
verified ·
1 Parent(s): 9ca97b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -174
app.py CHANGED
@@ -2,8 +2,10 @@ import os
2
  import re
3
  import base64
4
  import ast
 
5
  import math
6
  import operator
 
7
  from io import BytesIO
8
  from pathlib import Path
9
  from zipfile import ZipFile
@@ -11,9 +13,7 @@ from zipfile import ZipFile
11
  import gradio as gr
12
  import pandas as pd
13
  import requests
14
- from litellm import completion
15
  from smolagents import (
16
- DuckDuckGoSearchTool,
17
  LiteLLMModel,
18
  Tool,
19
  ToolCallingAgent,
@@ -28,11 +28,13 @@ MAX_EXTRACTED_CHARS = 8_000
28
  WEBPAGE_CONNECT_TIMEOUT = 8
29
  WEBPAGE_READ_TIMEOUT = 20
30
  MAX_WEBPAGE_CHARS = 6_000
31
- DEFAULT_MAIN_MODEL = "groq/qwen/qwen3.6-27b"
32
- DEFAULT_GROQ_REVIEW_MODEL = "groq/qwen/qwen3.6-27b"
33
  TASK_FILE_CACHE = {}
34
  ATTACHMENT_CACHE = {}
35
  WEBPAGE_CACHE = {}
 
 
36
 
37
 
38
  def compact_error(exc: Exception) -> str:
@@ -336,6 +338,56 @@ def extract_attachment_text(
336
  return text
337
 
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  class OpenWebPageTool(Tool):
340
  name = "visit_webpage"
341
  description = (
@@ -528,7 +580,7 @@ class TranscribeGaiaAudioTool(Tool):
528
  description = (
529
  "Downloads and transcribes the official GAIA audio attachment. Use it "
530
  "first whenever the attachment is MP3, WAV, FLAC, M4A, OGG, or WEBM. "
531
- "Speech transcription uses the separate audio quota, not chat tokens."
532
  )
533
  inputs = {
534
  "task_id": {
@@ -551,78 +603,33 @@ class TranscribeGaiaAudioTool(Tool):
551
  "inspect_gaia_attachment."
552
  )
553
 
554
- errors = []
555
- groq_api_key = os.getenv("GROQ_API_KEY")
556
- if groq_api_key:
557
- mime_types = {
558
- ".mp3": "audio/mpeg",
559
- ".wav": "audio/wav",
560
- ".flac": "audio/flac",
561
- ".m4a": "audio/mp4",
562
- ".ogg": "audio/ogg",
563
- ".webm": "audio/webm",
564
- ".mp4": "video/mp4",
565
- }
566
- try:
567
- response = requests.post(
568
- "https://api.groq.com/openai/v1/audio/transcriptions",
569
- headers={"Authorization": f"Bearer {groq_api_key}"},
570
- files={
571
- "file": (
572
- filename,
573
- data,
574
- mime_types.get(suffix, "application/octet-stream"),
575
- )
576
- },
577
- data={
578
- "model": "whisper-large-v3-turbo",
579
- "response_format": "json",
580
- "temperature": "0",
581
- "language": "en",
582
- },
583
- timeout=(WEBPAGE_CONNECT_TIMEOUT, 90),
584
- )
585
- response.raise_for_status()
586
- transcript = str(response.json().get("text") or "").strip()
587
- if transcript:
588
- return (
589
- f"Audio transcript ({filename}):\n"
590
- f"{transcript[:6_000]}"
591
- )
592
- errors.append("Groq returned an empty transcript.")
593
- except Exception as exc:
594
- detail = compact_error(exc)
595
- if "response" in locals() and response is not None:
596
- detail += f" Response: {response.text[:500]}"
597
- errors.append(f"Groq speech-to-text: {detail}")
598
-
599
  hf_token = os.getenv("HF_TOKEN")
600
- if hf_token:
601
- try:
602
- from huggingface_hub import InferenceClient
603
-
604
- client = InferenceClient(api_key=hf_token, provider="auto")
605
- transcript_result = client.automatic_speech_recognition(
606
- data,
607
- model=os.getenv(
608
- "GAIA_ASR_MODEL", "openai/whisper-large-v3"
609
- ),
610
- )
611
- transcript = str(
612
- getattr(transcript_result, "text", transcript_result)
613
- ).strip()
614
- if transcript:
615
- return (
616
- f"Audio transcript ({filename}):\n"
617
- f"{transcript[:6_000]}"
618
- )
619
- errors.append("Hugging Face returned an empty transcript.")
620
- except Exception as exc:
621
- errors.append(
622
- f"Hugging Face speech-to-text: {compact_error(exc)}"
623
- )
624
 
625
- return "Audio transcription failed. " + " | ".join(errors)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  except Exception as exc:
627
  return (
628
  f"Could not transcribe audio for task {task_id}: "
@@ -1086,42 +1093,36 @@ class BasicAgent:
1086
  print("Inicializando o agente GAIA...")
1087
 
1088
  hf_token = os.getenv("HF_TOKEN")
1089
- groq_api_key = os.getenv("GROQ_API_KEY")
1090
  configured_model = os.getenv("GAIA_MODEL_ID")
1091
 
1092
  model_id = configured_model or DEFAULT_MAIN_MODEL
1093
- if not model_id.lower().startswith("groq/qwen/"):
1094
  print(
1095
- "GAIA_MODEL_ID não apontava para um modelo Qwen no Groq "
1096
  "e foi ignorado. "
1097
  f"Usando {DEFAULT_MAIN_MODEL}."
1098
  )
1099
  model_id = DEFAULT_MAIN_MODEL
1100
- if not groq_api_key:
1101
  raise RuntimeError(
1102
- "O secret GROQ_API_KEY não está configurado. "
1103
  "Adicione a chave em Settings > Variables and secrets > Secrets."
1104
  )
1105
 
1106
  self.model = LiteLLMModel(
1107
  model_id=model_id,
1108
- api_key=groq_api_key,
1109
  temperature=0,
1110
- max_tokens=1_200,
1111
- requests_per_minute=2,
 
1112
  )
1113
  self.hf_token = hf_token
1114
  self.model_id = model_id
1115
  print(f"Modelo principal selecionado: {model_id}")
1116
 
1117
- web_search_tool = DuckDuckGoSearchTool(
1118
- max_results=5, rate_limit=1.0
1119
- )
1120
- web_search_tool.description = (
1121
- "Searches the public web and returns result titles, URLs, and short "
1122
- "snippets. Use it to discover candidate sources. It does NOT open "
1123
- "or read the full pages; call visit_webpage on a returned URL."
1124
- )
1125
  visit_page_tool = OpenWebPageTool()
1126
  wikipedia_tool = WikipediaSearchTool(
1127
  user_agent="GAIA-Course-Agent/1.0 (educational project)",
@@ -1147,12 +1148,13 @@ class BasicAgent:
1147
  CalculatorTool(),
1148
  ]
1149
 
1150
- # Qwen returns native tool calls. ToolCallingAgent handles that
1151
  # structured format without parsing generated Python code.
1152
  self.agent = ToolCallingAgent(
1153
  tools=agent_tools,
1154
  model=self.model,
1155
- max_steps=7,
 
1156
  planning_interval=None,
1157
  description=(
1158
  "Agent designed to solve GAIA benchmark questions with "
@@ -1207,6 +1209,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1207
  question = (question or "").strip()
1208
  if not question:
1209
  raise ValueError("Digite uma pergunta para testar o agente.")
 
1210
 
1211
  if task_id:
1212
  attachment_name = get_task_file_name(task_id)
@@ -1262,7 +1265,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1262
  ) from exc
1263
  raise
1264
  candidate = self.enforce_direct_answer(question, str(result))
1265
- return self.review_answer_with_groq(
1266
  question=question,
1267
  candidate=candidate,
1268
  task_id=task_id,
@@ -1390,7 +1393,39 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1390
  if identifiers:
1391
  return identifiers[-1].strip(" .,:;\"'")
1392
 
1393
- person_question = (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  question_lower.startswith("who ")
1395
  or " who " in f" {question_lower} "
1396
  or "first name" in question_lower
@@ -1478,103 +1513,99 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1478
 
1479
  return self.deterministic_answer_cleanup(cleaned)
1480
 
1481
- def review_answer_with_groq(
1482
  self, question: str, candidate: str, task_id: str | None = None
1483
  ) -> str:
1484
- """Solicita uma segunda opinião gratuita no Groq, com fallback local."""
1485
- groq_api_key = os.getenv("GROQ_API_KEY")
1486
- if not groq_api_key:
1487
  fallback = self.enforce_direct_answer(question, candidate)
1488
- print("Groq review status: SKIPPED — GROQ_API_KEY is missing")
1489
  print(f"Primary answer preserved: {fallback}")
1490
  return fallback
1491
 
1492
  reviewer_model = os.getenv(
1493
- "GAIA_GROQ_REVIEW_MODEL", DEFAULT_GROQ_REVIEW_MODEL
1494
  )
1495
- if not reviewer_model.lower().startswith("groq/qwen/"):
1496
- reviewer_model = DEFAULT_GROQ_REVIEW_MODEL
1497
  review_prompt = f"""
1498
- Review this GAIA exact-match candidate. Preserve it unless clearly wrong.
1499
- Return only the requested value: no explanation, label, Markdown, or citation.
1500
- Respect requested number, currency, name, list separator/order, quote, or chess
1501
- notation format.
 
1502
  Question: {question}
1503
  Candidate: {candidate}
1504
- Return:
1505
- <final_answer>exact value to submit</final_answer>
1506
  """.strip()
1507
 
1508
- last_error = None
1509
- for attempt in range(1):
1510
- try:
1511
- retry_instruction = (
1512
- ""
1513
- if attempt == 0
1514
- else "\nIMPORTANT: Return both XML fields exactly as specified."
1515
- )
1516
- response = completion(
1517
- model=reviewer_model,
1518
- api_key=groq_api_key,
1519
- messages=[
1520
  {
1521
  "role": "user",
1522
- "content": review_prompt + retry_instruction,
1523
  }
1524
  ],
1525
- temperature=0,
1526
- max_tokens=120,
1527
- reasoning_effort="none",
1528
- )
1529
- content = str(response.choices[0].message.content).strip()
1530
- content = re.sub(
1531
- r"^```(?:xml|text)?\s*|\s*```$", "", content, flags=re.I
1532
- )
1533
- answer_match = re.search(
1534
- r"<final_answer>\s*(.*?)\s*</final_answer>",
1535
- content,
1536
- flags=re.I | re.S,
1537
- )
1538
- note_match = re.search(
1539
- r"<review_note>\s*(.*?)\s*</review_note>",
1540
- content,
1541
- flags=re.I | re.S,
1542
- )
1543
- if not answer_match:
1544
- raise ValueError(
1545
- "Groq did not return the <final_answer> field."
1546
- )
1547
- final_answer = self.enforce_direct_answer(
1548
- question, answer_match.group(1)
1549
- )
1550
- if not final_answer:
1551
- raise ValueError("Groq returned an empty final_answer.")
1552
-
1553
- changed = final_answer != candidate
1554
- note = (
1555
- note_match.group(1).strip()
1556
- if note_match
1557
- else "Review completed without a note."
1558
- )
1559
- print(f"Candidate answer: {candidate}")
1560
- print(f"Groq reviewed answer: {final_answer}")
1561
- print(f"Groq changed answer: {changed}")
1562
- print(f"Groq review note: {note}")
1563
- return final_answer
1564
- except Exception as exc:
1565
- last_error = exc
1566
-
1567
- fallback = self.enforce_direct_answer(question, candidate)
1568
- if not fallback:
1569
- raise RuntimeError(
1570
- "A revisão do Groq falhou e a resposta primária estava vazia. "
1571
- f"Detalhe: {last_error}"
1572
  )
1573
-
1574
- print(f"Candidate answer: {fallback}")
1575
- print("Groq review status: FAILED — primary answer preserved")
1576
- print(f"Groq review error: {last_error}")
1577
- return fallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1578
 
1579
 
1580
  def empty_results() -> pd.DataFrame:
 
2
  import re
3
  import base64
4
  import ast
5
+ import json
6
  import math
7
  import operator
8
+ import threading
9
  from io import BytesIO
10
  from pathlib import Path
11
  from zipfile import ZipFile
 
13
  import gradio as gr
14
  import pandas as pd
15
  import requests
 
16
  from smolagents import (
 
17
  LiteLLMModel,
18
  Tool,
19
  ToolCallingAgent,
 
28
  WEBPAGE_CONNECT_TIMEOUT = 8
29
  WEBPAGE_READ_TIMEOUT = 20
30
  MAX_WEBPAGE_CHARS = 6_000
31
+ DEFAULT_MAIN_MODEL = "cerebras/zai-glm-4.7"
32
+ DEFAULT_GEMINI_REVIEW_MODEL = "gemini-3.5-flash"
33
  TASK_FILE_CACHE = {}
34
  ATTACHMENT_CACHE = {}
35
  WEBPAGE_CACHE = {}
36
+ SEARCH_CACHE = {}
37
+ SEARCH_LOCK = threading.Lock()
38
 
39
 
40
  def compact_error(exc: Exception) -> str:
 
338
  return text
339
 
340
 
341
+ class ConciseWebSearchTool(Tool):
342
+ name = "web_search"
343
+ description = (
344
+ "Searches the public web without an API key. Returns at most five "
345
+ "compact results. Use one precise query, then open the best source. "
346
+ "Identical repeated searches are skipped automatically."
347
+ )
348
+ inputs = {
349
+ "query": {
350
+ "type": "string",
351
+ "description": "One concise web search query.",
352
+ }
353
+ }
354
+ output_type = "string"
355
+
356
+ def forward(self, query: str) -> str:
357
+ from ddgs import DDGS
358
+
359
+ query = " ".join(str(query or "").split())
360
+ if not query:
361
+ return "Search query is empty."
362
+ cache_key = query.casefold()
363
+
364
+ # The lock also prevents parallel duplicate searches from both sending
365
+ # the same network request and duplicating a large observation.
366
+ with SEARCH_LOCK:
367
+ if cache_key in SEARCH_CACHE:
368
+ return (
369
+ "Duplicate search skipped; the identical results are "
370
+ "already present in this run. Open one of those URLs."
371
+ )
372
+ try:
373
+ raw_results = list(DDGS().text(query, max_results=5))
374
+ lines = []
375
+ for index, result in enumerate(raw_results[:5], start=1):
376
+ title = " ".join(str(result.get("title") or "").split())
377
+ url = str(result.get("href") or result.get("url") or "").strip()
378
+ snippet = " ".join(str(result.get("body") or "").split())
379
+ lines.append(
380
+ f"{index}. {title[:180]}\n"
381
+ f"URL: {url}\n"
382
+ f"Snippet: {snippet[:320]}"
383
+ )
384
+ output = "\n\n".join(lines) or "No search results found."
385
+ SEARCH_CACHE[cache_key] = output
386
+ return output
387
+ except Exception as exc:
388
+ return f"Web search failed: {compact_error(exc)}"
389
+
390
+
391
  class OpenWebPageTool(Tool):
392
  name = "visit_webpage"
393
  description = (
 
580
  description = (
581
  "Downloads and transcribes the official GAIA audio attachment. Use it "
582
  "first whenever the attachment is MP3, WAV, FLAC, M4A, OGG, or WEBM. "
583
+ "It uses Hugging Face speech recognition, not the chat model."
584
  )
585
  inputs = {
586
  "task_id": {
 
603
  "inspect_gaia_attachment."
604
  )
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  hf_token = os.getenv("HF_TOKEN")
607
+ if not hf_token:
608
+ return "Audio transcription failed: HF_TOKEN is not configured."
609
+ try:
610
+ from huggingface_hub import InferenceClient
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
+ client = InferenceClient(api_key=hf_token, provider="auto")
613
+ transcript_result = client.automatic_speech_recognition(
614
+ data,
615
+ model=os.getenv(
616
+ "GAIA_ASR_MODEL", "openai/whisper-large-v3"
617
+ ),
618
+ )
619
+ transcript = str(
620
+ getattr(transcript_result, "text", transcript_result)
621
+ ).strip()
622
+ if not transcript:
623
+ return "Hugging Face returned an empty audio transcript."
624
+ return (
625
+ f"Audio transcript ({filename}):\n"
626
+ f"{transcript[:6_000]}"
627
+ )
628
+ except Exception as exc:
629
+ return (
630
+ "Hugging Face speech-to-text failed: "
631
+ f"{compact_error(exc)}"
632
+ )
633
  except Exception as exc:
634
  return (
635
  f"Could not transcribe audio for task {task_id}: "
 
1093
  print("Inicializando o agente GAIA...")
1094
 
1095
  hf_token = os.getenv("HF_TOKEN")
1096
+ cerebras_api_key = os.getenv("CEREBRAS_API_KEY")
1097
  configured_model = os.getenv("GAIA_MODEL_ID")
1098
 
1099
  model_id = configured_model or DEFAULT_MAIN_MODEL
1100
+ if not model_id.lower().startswith("cerebras/"):
1101
  print(
1102
+ "GAIA_MODEL_ID não apontava para um modelo Cerebras "
1103
  "e foi ignorado. "
1104
  f"Usando {DEFAULT_MAIN_MODEL}."
1105
  )
1106
  model_id = DEFAULT_MAIN_MODEL
1107
+ if not cerebras_api_key:
1108
  raise RuntimeError(
1109
+ "O secret CEREBRAS_API_KEY não está configurado. "
1110
  "Adicione a chave em Settings > Variables and secrets > Secrets."
1111
  )
1112
 
1113
  self.model = LiteLLMModel(
1114
  model_id=model_id,
1115
+ api_key=cerebras_api_key,
1116
  temperature=0,
1117
+ max_tokens=1_000,
1118
+ requests_per_minute=8,
1119
+ parallel_tool_calls=False,
1120
  )
1121
  self.hf_token = hf_token
1122
  self.model_id = model_id
1123
  print(f"Modelo principal selecionado: {model_id}")
1124
 
1125
+ web_search_tool = ConciseWebSearchTool()
 
 
 
 
 
 
 
1126
  visit_page_tool = OpenWebPageTool()
1127
  wikipedia_tool = WikipediaSearchTool(
1128
  user_agent="GAIA-Course-Agent/1.0 (educational project)",
 
1148
  CalculatorTool(),
1149
  ]
1150
 
1151
+ # GLM returns native tool calls. ToolCallingAgent handles that
1152
  # structured format without parsing generated Python code.
1153
  self.agent = ToolCallingAgent(
1154
  tools=agent_tools,
1155
  model=self.model,
1156
+ max_steps=4,
1157
+ max_tool_threads=1,
1158
  planning_interval=None,
1159
  description=(
1160
  "Agent designed to solve GAIA benchmark questions with "
 
1209
  question = (question or "").strip()
1210
  if not question:
1211
  raise ValueError("Digite uma pergunta para testar o agente.")
1212
+ SEARCH_CACHE.clear()
1213
 
1214
  if task_id:
1215
  attachment_name = get_task_file_name(task_id)
 
1265
  ) from exc
1266
  raise
1267
  candidate = self.enforce_direct_answer(question, str(result))
1268
+ return self.review_answer_with_gemini(
1269
  question=question,
1270
  candidate=candidate,
1271
  task_id=task_id,
 
1393
  if identifiers:
1394
  return identifiers[-1].strip(" .,:;\"'")
1395
 
1396
+ list_question = (
1397
+ "comma" in question_lower
1398
+ or "list" in question_lower
1399
+ or "separated" in question_lower
1400
+ or "delimited" in question_lower
1401
+ )
1402
+ if (
1403
+ list_question
1404
+ and "last name" in question_lower
1405
+ and "before" in question_lower
1406
+ and "after" in question_lower
1407
+ ):
1408
+ emphasized_names = [
1409
+ cls.deterministic_answer_cleanup(value).strip(" .,:;\"'")
1410
+ for value in bold_values
1411
+ ]
1412
+ emphasized_names = [
1413
+ value
1414
+ for value in emphasized_names
1415
+ if value and len(value) <= 60 and " " not in value
1416
+ ]
1417
+ if len(emphasized_names) >= 2:
1418
+ return ", ".join(emphasized_names[-2:])
1419
+
1420
+ name_pair = re.search(
1421
+ r"\b(?:are|were)\s+([A-Z][A-Za-z'’-]+)\s*"
1422
+ r"(?:,|and)\s*([A-Z][A-Za-z'’-]+)",
1423
+ text,
1424
+ )
1425
+ if name_pair:
1426
+ return f"{name_pair.group(1)}, {name_pair.group(2)}"
1427
+
1428
+ person_question = not list_question and (
1429
  question_lower.startswith("who ")
1430
  or " who " in f" {question_lower} "
1431
  or "first name" in question_lower
 
1513
 
1514
  return self.deterministic_answer_cleanup(cleaned)
1515
 
1516
+ def review_answer_with_gemini(
1517
  self, question: str, candidate: str, task_id: str | None = None
1518
  ) -> str:
1519
+ """Revisa com Gemini e sempre preserva a resposta primária se falhar."""
1520
+ gemini_api_key = os.getenv("GEMINI_API_KEY")
1521
+ if not gemini_api_key:
1522
  fallback = self.enforce_direct_answer(question, candidate)
1523
+ print("Gemini review status: SKIPPED — GEMINI_API_KEY is missing")
1524
  print(f"Primary answer preserved: {fallback}")
1525
  return fallback
1526
 
1527
  reviewer_model = os.getenv(
1528
+ "GAIA_GEMINI_REVIEW_MODEL", DEFAULT_GEMINI_REVIEW_MODEL
1529
  )
1530
+ if not reviewer_model.lower().startswith("gemini-"):
1531
+ reviewer_model = DEFAULT_GEMINI_REVIEW_MODEL
1532
  review_prompt = f"""
1533
+ Review this GAIA exact-match candidate. Preserve it unless a correction is
1534
+ clearly necessary. The final_answer must contain only the requested value,
1535
+ without explanation, label, Markdown, or citation. Respect requested numeric,
1536
+ currency, name, list separator/order, quote, or chess notation formats.
1537
+ Task ID: {task_id or "test"}
1538
  Question: {question}
1539
  Candidate: {candidate}
 
 
1540
  """.strip()
1541
 
1542
+ try:
1543
+ response = requests.post(
1544
+ (
1545
+ "https://generativelanguage.googleapis.com/v1beta/models/"
1546
+ f"{reviewer_model}:generateContent"
1547
+ ),
1548
+ headers={
1549
+ "x-goog-api-key": gemini_api_key,
1550
+ "Content-Type": "application/json",
1551
+ },
1552
+ json={
1553
+ "contents": [
1554
  {
1555
  "role": "user",
1556
+ "parts": [{"text": review_prompt}],
1557
  }
1558
  ],
1559
+ "generationConfig": {
1560
+ "temperature": 0,
1561
+ "maxOutputTokens": 120,
1562
+ "responseMimeType": "application/json",
1563
+ "responseJsonSchema": {
1564
+ "type": "object",
1565
+ "properties": {
1566
+ "final_answer": {"type": "string"},
1567
+ "review_note": {"type": "string"},
1568
+ },
1569
+ "required": ["final_answer", "review_note"],
1570
+ "additionalProperties": False,
1571
+ },
1572
+ },
1573
+ },
1574
+ timeout=(WEBPAGE_CONNECT_TIMEOUT, 45),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1575
  )
1576
+ response.raise_for_status()
1577
+ payload = response.json()
1578
+ parts = payload["candidates"][0]["content"]["parts"]
1579
+ content = "".join(
1580
+ str(part.get("text") or "") for part in parts
1581
+ ).strip()
1582
+ review_data = json.loads(content)
1583
+ final_answer = self.enforce_direct_answer(
1584
+ question, str(review_data.get("final_answer") or "")
1585
+ )
1586
+ if not final_answer:
1587
+ raise ValueError("Gemini returned an empty final_answer.")
1588
+
1589
+ note = str(review_data.get("review_note") or "").strip()
1590
+ print(f"Candidate answer: {candidate}")
1591
+ print(f"Gemini reviewed answer: {final_answer}")
1592
+ print(f"Gemini changed answer: {final_answer != candidate}")
1593
+ print(f"Gemini review note: {note}")
1594
+ return final_answer
1595
+ except Exception as exc:
1596
+ fallback = self.enforce_direct_answer(question, candidate)
1597
+ if not fallback:
1598
+ raise RuntimeError(
1599
+ "Gemini review failed and the primary answer was empty. "
1600
+ f"Detail: {compact_error(exc)}"
1601
+ ) from exc
1602
+ detail = compact_error(exc)
1603
+ if "response" in locals() and response is not None:
1604
+ detail += f" Response: {response.text[:500]}"
1605
+ print(f"Candidate answer: {fallback}")
1606
+ print("Gemini review status: FAILED — primary answer preserved")
1607
+ print(f"Gemini review error: {detail}")
1608
+ return fallback
1609
 
1610
 
1611
  def empty_results() -> pd.DataFrame: