Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -24,18 +24,20 @@ GROQ_MODELS = [
|
|
| 24 |
m.strip()
|
| 25 |
for m in os.getenv(
|
| 26 |
"GROQ_MODELS",
|
| 27 |
-
|
|
|
|
| 28 |
).split(",")
|
| 29 |
if m.strip()
|
| 30 |
]
|
| 31 |
GROQ_VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
|
| 32 |
GROQ_WHISPER_MODEL = os.getenv("GROQ_WHISPER_MODEL", "whisper-large-v3-turbo")
|
| 33 |
|
| 34 |
-
MAX_TOOL_ITERATIONS =
|
| 35 |
-
TOOL_RESULT_MAX_CHARS =
|
| 36 |
-
HISTORY_TRIM_AFTER =
|
| 37 |
-
ANSWER_CACHE_PATH = os.getenv("ANSWER_CACHE_PATH", "answers_cache.json")
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
# Track downloaded task files so vision/audio tools can re-use them by task_id.
|
| 41 |
_TASK_FILE_CACHE: dict[str, dict] = {}
|
|
@@ -458,30 +460,59 @@ SYSTEM_PROMPT = """You are a careful research agent answering GAIA benchmark que
|
|
| 458 |
|
| 459 |
Tools: web_search, fetch_url, wikipedia, python, get_task_file, transcribe_audio, view_image, youtube_transcript.
|
| 460 |
|
| 461 |
-
|
| 462 |
-
-
|
| 463 |
-
-
|
| 464 |
-
-
|
| 465 |
-
-
|
| 466 |
-
-
|
| 467 |
-
- For
|
| 468 |
-
- Use python for ALL arithmetic, sums, date math, sorting, alphabetizing. Never compute by hand.
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
-
|
| 477 |
-
-
|
| 478 |
-
-
|
| 479 |
-
-
|
| 480 |
-
-
|
| 481 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
"""
|
| 483 |
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
# ---------------------------------------------------------------------------
|
| 486 |
# Agent
|
| 487 |
# ---------------------------------------------------------------------------
|
|
@@ -570,6 +601,13 @@ class GroqAgent:
|
|
| 570 |
return head + [summary] + tail
|
| 571 |
|
| 572 |
def __call__(self, question: str, task_id: str | None = None) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
user_content = question
|
| 574 |
if task_id:
|
| 575 |
user_content = f"task_id: {task_id}\n\nQuestion: {question}"
|
|
@@ -599,7 +637,7 @@ class GroqAgent:
|
|
| 599 |
|
| 600 |
if not tool_calls:
|
| 601 |
answer = (msg.content or "").strip()
|
| 602 |
-
return self.
|
| 603 |
|
| 604 |
messages.append(
|
| 605 |
{
|
|
@@ -658,12 +696,51 @@ class GroqAgent:
|
|
| 658 |
)
|
| 659 |
try:
|
| 660 |
resp = self._chat(self._trim_messages(messages), use_tools=False, max_tokens=200)
|
| 661 |
-
return self.
|
| 662 |
(resp.choices[0].message.content or "").strip(), question
|
| 663 |
)
|
| 664 |
except Exception as e:
|
| 665 |
return f"AGENT ERROR: {e}"
|
| 666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
@staticmethod
|
| 668 |
def _postprocess_answer(text: str, question: str = "") -> str:
|
| 669 |
if not text:
|
|
@@ -726,7 +803,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 726 |
username = f"{profile.username}"
|
| 727 |
print(f"User logged in: {username}")
|
| 728 |
else:
|
| 729 |
-
return "Please Login to Hugging Face with the button.", None
|
| 730 |
|
| 731 |
api_url = DEFAULT_API_URL
|
| 732 |
questions_url = f"{api_url}/questions"
|
|
@@ -736,7 +813,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 736 |
agent = GroqAgent()
|
| 737 |
except Exception as e:
|
| 738 |
print(f"Error instantiating agent: {e}")
|
| 739 |
-
return f"Error initializing agent: {e}", None
|
| 740 |
|
| 741 |
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
|
| 742 |
print(agent_code)
|
|
@@ -747,12 +824,12 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 747 |
response.raise_for_status()
|
| 748 |
questions_data = response.json()
|
| 749 |
if not questions_data:
|
| 750 |
-
return "Fetched questions list is empty or invalid format.", None
|
| 751 |
print(f"Fetched {len(questions_data)} questions.")
|
| 752 |
except requests.exceptions.RequestException as e:
|
| 753 |
-
return f"Error fetching questions: {e}", None
|
| 754 |
except Exception as e:
|
| 755 |
-
return f"An unexpected error occurred fetching questions: {e}", None
|
| 756 |
|
| 757 |
results_log = []
|
| 758 |
answers_payload = []
|
|
@@ -788,7 +865,14 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 788 |
time.sleep(INTER_QUESTION_SLEEP)
|
| 789 |
|
| 790 |
if not answers_payload:
|
| 791 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
|
| 793 |
submission_data = {
|
| 794 |
"username": username.strip(),
|
|
@@ -810,7 +894,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 810 |
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 811 |
f"Message: {result_data.get('message', 'No message received.')}"
|
| 812 |
)
|
| 813 |
-
return final_status,
|
| 814 |
except requests.exceptions.HTTPError as e:
|
| 815 |
status = e.response.status_code if e.response is not None else "?"
|
| 816 |
last_error = e
|
|
@@ -823,7 +907,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 823 |
error_detail += f" Detail: {e.response.json().get('detail', e.response.text)}"
|
| 824 |
except Exception:
|
| 825 |
error_detail += f" Response: {e.response.text[:500] if e.response is not None else ''}"
|
| 826 |
-
return f"Submission Failed: {error_detail}",
|
| 827 |
except requests.exceptions.Timeout as e:
|
| 828 |
last_error = e
|
| 829 |
print(f"Submission attempt {attempt + 1} timed out.")
|
|
@@ -838,7 +922,8 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 838 |
return (
|
| 839 |
f"Submission Failed after retries: {last_error}. Answers are cached at "
|
| 840 |
f"{ANSWER_CACHE_PATH} — re-run to retry without re-querying the model.",
|
| 841 |
-
|
|
|
|
| 842 |
)
|
| 843 |
|
| 844 |
|
|
@@ -865,8 +950,9 @@ with gr.Blocks() as demo:
|
|
| 865 |
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
| 866 |
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 867 |
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
|
|
|
|
| 868 |
|
| 869 |
-
run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
|
| 870 |
|
| 871 |
|
| 872 |
if __name__ == "__main__":
|
|
|
|
| 24 |
m.strip()
|
| 25 |
for m in os.getenv(
|
| 26 |
"GROQ_MODELS",
|
| 27 |
+
# 70b first — much better reasoning. Trim history aggressively to fit 6K TPM.
|
| 28 |
+
"llama-3.3-70b-versatile,llama-3.1-8b-instant",
|
| 29 |
).split(",")
|
| 30 |
if m.strip()
|
| 31 |
]
|
| 32 |
GROQ_VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
|
| 33 |
GROQ_WHISPER_MODEL = os.getenv("GROQ_WHISPER_MODEL", "whisper-large-v3-turbo")
|
| 34 |
|
| 35 |
+
MAX_TOOL_ITERATIONS = 6
|
| 36 |
+
TOOL_RESULT_MAX_CHARS = 1400 # tighter -> stays under 6K TPM for 70b
|
| 37 |
+
HISTORY_TRIM_AFTER = 5 # trim aggressively
|
| 38 |
+
ANSWER_CACHE_PATH = os.getenv("ANSWER_CACHE_PATH", "/tmp/answers_cache.json")
|
| 39 |
+
RESULTS_CSV_PATH = "/tmp/gaia_results.csv"
|
| 40 |
+
INTER_QUESTION_SLEEP = float(os.getenv("INTER_QUESTION_SLEEP", "3"))
|
| 41 |
|
| 42 |
# Track downloaded task files so vision/audio tools can re-use them by task_id.
|
| 43 |
_TASK_FILE_CACHE: dict[str, dict] = {}
|
|
|
|
| 460 |
|
| 461 |
Tools: web_search, fetch_url, wikipedia, python, get_task_file, transcribe_audio, view_image, youtube_transcript.
|
| 462 |
|
| 463 |
+
Decision rules:
|
| 464 |
+
- If the question literally references "attached file/image/audio/Excel/PDF/.mp3/.xlsx/.py/code/recording", call get_task_file FIRST. If it returns NO_FILE, do NOT call it again.
|
| 465 |
+
- Audio file -> transcribe_audio(task_id).
|
| 466 |
+
- Image file -> view_image(task_id, question="<focused question>").
|
| 467 |
+
- YouTube URL -> youtube_transcript(url) directly (no get_task_file needed).
|
| 468 |
+
- For factual lookups about people / places / artists / albums / animals / Wikipedia featured articles, START with wikipedia. Then fetch_url the relevant page if more detail needed.
|
| 469 |
+
- For everything else research-y, web_search then fetch_url.
|
| 470 |
+
- Use python for ALL arithmetic, sums (e.g. summing Excel rows), date math, sorting, alphabetizing, set/group operations, string reversal. Never compute by hand.
|
| 471 |
+
- For Excel/CSV totals, after get_task_file shows you the data, ALWAYS use python to compute the sum precisely.
|
| 472 |
+
|
| 473 |
+
You have 6 tool turns. Be decisive. Do not loop on the same query.
|
| 474 |
+
|
| 475 |
+
ANSWER FORMATTING (the grader does an exact-match comparison; sentence answers ALWAYS lose):
|
| 476 |
+
|
| 477 |
+
Worked examples of correct GAIA format:
|
| 478 |
+
- Q: "How many albums..." -> "3" (NOT "3 albums" or "There were 3 albums")
|
| 479 |
+
- Q: "Express your answer in USD with two decimal places" -> "89706.00" (NOT "$89,706" or "89706")
|
| 480 |
+
- Q: "Give the IOC country code" -> "MLT" (NOT "Malta" or "Malta (MLT)")
|
| 481 |
+
- Q: "Just the city name without abbreviations" -> "Saint Petersburg"
|
| 482 |
+
- Q: "Give only the first name" -> "Bartek" (NOT "Bartlomiej" or "Bartek Kasprzykowski")
|
| 483 |
+
- Q: "Comma separated list ... in alphabetical order" -> "broccoli, celery, fresh basil, lettuce, sweet potatoes, zucchini"
|
| 484 |
+
- Q: "Under what NASA award number..." -> "80NSSC21K1130" (just the code, NO surrounding sentence)
|
| 485 |
+
- Q: "Final numeric output from the attached Python code" -> "0" (just the number)
|
| 486 |
+
- Q: opposite of "left" -> "right" (one word)
|
| 487 |
+
|
| 488 |
+
Strict rules:
|
| 489 |
+
- Reply with ONLY the answer. No preamble. No explanation. No quotes. No trailing period.
|
| 490 |
+
- Do NOT include "FINAL ANSWER", "Answer:", or any label.
|
| 491 |
+
- Numbers: digits only, no commas, no units, no $ — UNLESS the question asks for the unit.
|
| 492 |
+
- Currency "two decimal places": e.g. "89706.00".
|
| 493 |
+
- Strings: no leading articles ("the", "a") unless required; no abbreviations (write "Saint" not "St."); digits as digits.
|
| 494 |
+
- Names: read the question carefully. "First name only" / "last name only" / "surname" / "full name". Match exactly.
|
| 495 |
+
- Lists: comma-separated, ONE space after each comma. Apply formatting rules to each element. Sort if asked.
|
| 496 |
"""
|
| 497 |
|
| 498 |
|
| 499 |
+
def _maybe_reverse_text(question: str) -> str:
|
| 500 |
+
"""If the question text looks reversed, flip it. Returns possibly-modified question."""
|
| 501 |
+
# Heuristic: a normal English sentence has many word-frequencies like 'the', 'a', 'of'.
|
| 502 |
+
# A reversed one has 'eht', 'fo', 'sa', etc., and often starts with punctuation like '.'.
|
| 503 |
+
q = question.strip()
|
| 504 |
+
if not q:
|
| 505 |
+
return question
|
| 506 |
+
starts_with_punct = q[0] in ".,;:!?"
|
| 507 |
+
reversed_text = q[::-1]
|
| 508 |
+
# Look for common English words in the reversed version.
|
| 509 |
+
common = (" the ", " of ", " and ", " to ", " is ", " a ", " in ", " for ")
|
| 510 |
+
hits = sum(1 for w in common if w in (" " + reversed_text.lower() + " "))
|
| 511 |
+
if starts_with_punct and hits >= 2:
|
| 512 |
+
return reversed_text
|
| 513 |
+
return question
|
| 514 |
+
|
| 515 |
+
|
| 516 |
# ---------------------------------------------------------------------------
|
| 517 |
# Agent
|
| 518 |
# ---------------------------------------------------------------------------
|
|
|
|
| 601 |
return head + [summary] + tail
|
| 602 |
|
| 603 |
def __call__(self, question: str, task_id: str | None = None) -> str:
|
| 604 |
+
# Deterministic preprocess: detect & flip reversed-text trick questions.
|
| 605 |
+
original_q = question
|
| 606 |
+
flipped = _maybe_reverse_text(question)
|
| 607 |
+
if flipped != question:
|
| 608 |
+
print("[reversed-text detected, flipping question]")
|
| 609 |
+
question = flipped
|
| 610 |
+
|
| 611 |
user_content = question
|
| 612 |
if task_id:
|
| 613 |
user_content = f"task_id: {task_id}\n\nQuestion: {question}"
|
|
|
|
| 637 |
|
| 638 |
if not tool_calls:
|
| 639 |
answer = (msg.content or "").strip()
|
| 640 |
+
return self._finalize(answer, question)
|
| 641 |
|
| 642 |
messages.append(
|
| 643 |
{
|
|
|
|
| 696 |
)
|
| 697 |
try:
|
| 698 |
resp = self._chat(self._trim_messages(messages), use_tools=False, max_tokens=200)
|
| 699 |
+
return self._finalize(
|
| 700 |
(resp.choices[0].message.content or "").strip(), question
|
| 701 |
)
|
| 702 |
except Exception as e:
|
| 703 |
return f"AGENT ERROR: {e}"
|
| 704 |
|
| 705 |
+
def _finalize(self, raw: str, question: str) -> str:
|
| 706 |
+
"""Post-process and, if the answer still looks like a sentence, ask the model to reformat."""
|
| 707 |
+
cleaned = self._postprocess_answer(raw, question)
|
| 708 |
+
if not cleaned:
|
| 709 |
+
return cleaned
|
| 710 |
+
# If the cleaned answer is suspiciously long or contains explanation-y patterns,
|
| 711 |
+
# do a single tiny reformat pass.
|
| 712 |
+
looks_sentence = (
|
| 713 |
+
len(cleaned.split()) > 12
|
| 714 |
+
or re.search(r"\b(because|received|grant|seems|unable|sorry|cannot|provides|indicating)\b", cleaned, re.IGNORECASE)
|
| 715 |
+
)
|
| 716 |
+
if looks_sentence:
|
| 717 |
+
try:
|
| 718 |
+
resp = self._chat(
|
| 719 |
+
[
|
| 720 |
+
{
|
| 721 |
+
"role": "system",
|
| 722 |
+
"content": (
|
| 723 |
+
"Extract ONLY the final answer from the assistant text below, "
|
| 724 |
+
"matching the question's required format exactly. No preamble, "
|
| 725 |
+
"no explanation, no quotes, no trailing period, no labels."
|
| 726 |
+
),
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"role": "user",
|
| 730 |
+
"content": f"Question: {question}\n\nAssistant text: {cleaned}\n\nFinal answer:",
|
| 731 |
+
},
|
| 732 |
+
],
|
| 733 |
+
use_tools=False,
|
| 734 |
+
max_tokens=80,
|
| 735 |
+
)
|
| 736 |
+
reformat = (resp.choices[0].message.content or "").strip()
|
| 737 |
+
reformat = self._postprocess_answer(reformat, question)
|
| 738 |
+
if reformat:
|
| 739 |
+
return reformat
|
| 740 |
+
except Exception as e:
|
| 741 |
+
print(f"reformat pass failed: {e}")
|
| 742 |
+
return cleaned
|
| 743 |
+
|
| 744 |
@staticmethod
|
| 745 |
def _postprocess_answer(text: str, question: str = "") -> str:
|
| 746 |
if not text:
|
|
|
|
| 803 |
username = f"{profile.username}"
|
| 804 |
print(f"User logged in: {username}")
|
| 805 |
else:
|
| 806 |
+
return "Please Login to Hugging Face with the button.", None, None
|
| 807 |
|
| 808 |
api_url = DEFAULT_API_URL
|
| 809 |
questions_url = f"{api_url}/questions"
|
|
|
|
| 813 |
agent = GroqAgent()
|
| 814 |
except Exception as e:
|
| 815 |
print(f"Error instantiating agent: {e}")
|
| 816 |
+
return f"Error initializing agent: {e}", None, None
|
| 817 |
|
| 818 |
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
|
| 819 |
print(agent_code)
|
|
|
|
| 824 |
response.raise_for_status()
|
| 825 |
questions_data = response.json()
|
| 826 |
if not questions_data:
|
| 827 |
+
return "Fetched questions list is empty or invalid format.", None, None
|
| 828 |
print(f"Fetched {len(questions_data)} questions.")
|
| 829 |
except requests.exceptions.RequestException as e:
|
| 830 |
+
return f"Error fetching questions: {e}", None, None
|
| 831 |
except Exception as e:
|
| 832 |
+
return f"An unexpected error occurred fetching questions: {e}", None, None
|
| 833 |
|
| 834 |
results_log = []
|
| 835 |
answers_payload = []
|
|
|
|
| 865 |
time.sleep(INTER_QUESTION_SLEEP)
|
| 866 |
|
| 867 |
if not answers_payload:
|
| 868 |
+
df = pd.DataFrame(results_log)
|
| 869 |
+
df.to_csv(RESULTS_CSV_PATH, index=False)
|
| 870 |
+
return "Agent did not produce any answers to submit.", df, RESULTS_CSV_PATH
|
| 871 |
+
|
| 872 |
+
# Save results CSV before submission so the user can download even if submit fails.
|
| 873 |
+
df = pd.DataFrame(results_log)
|
| 874 |
+
df.to_csv(RESULTS_CSV_PATH, index=False)
|
| 875 |
+
print(f"Results CSV written to {RESULTS_CSV_PATH}")
|
| 876 |
|
| 877 |
submission_data = {
|
| 878 |
"username": username.strip(),
|
|
|
|
| 894 |
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 895 |
f"Message: {result_data.get('message', 'No message received.')}"
|
| 896 |
)
|
| 897 |
+
return final_status, df, RESULTS_CSV_PATH
|
| 898 |
except requests.exceptions.HTTPError as e:
|
| 899 |
status = e.response.status_code if e.response is not None else "?"
|
| 900 |
last_error = e
|
|
|
|
| 907 |
error_detail += f" Detail: {e.response.json().get('detail', e.response.text)}"
|
| 908 |
except Exception:
|
| 909 |
error_detail += f" Response: {e.response.text[:500] if e.response is not None else ''}"
|
| 910 |
+
return f"Submission Failed: {error_detail}", df, RESULTS_CSV_PATH
|
| 911 |
except requests.exceptions.Timeout as e:
|
| 912 |
last_error = e
|
| 913 |
print(f"Submission attempt {attempt + 1} timed out.")
|
|
|
|
| 922 |
return (
|
| 923 |
f"Submission Failed after retries: {last_error}. Answers are cached at "
|
| 924 |
f"{ANSWER_CACHE_PATH} — re-run to retry without re-querying the model.",
|
| 925 |
+
df,
|
| 926 |
+
RESULTS_CSV_PATH,
|
| 927 |
)
|
| 928 |
|
| 929 |
|
|
|
|
| 950 |
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
| 951 |
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 952 |
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
|
| 953 |
+
results_csv = gr.File(label="Download Results CSV (paste this back to me for tuning)")
|
| 954 |
|
| 955 |
+
run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table, results_csv])
|
| 956 |
|
| 957 |
|
| 958 |
if __name__ == "__main__":
|