jnforja's picture
Build local benchmark agent
665ab24 unverified
Raw
History Blame Contribute Delete
56.6 kB
import os
import ast
import json
import operator
import re
import hashlib
import subprocess
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from urllib.parse import unquote, urlparse
import gradio as gr
import chess
import chess.engine
import httpx
import requests
import pandas as pd
from bs4 import BeautifulSoup
from dotenv import load_dotenv
from huggingface_hub import hf_hub_download
from pypdf import PdfReader
from pydantic_ai import Agent, AudioUrl, BinaryContent, ImageUrl
from pydantic_ai.common_tools.duckduckgo import duckduckgo_search_tool
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.profiles.openai import OpenAIModelProfile
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.usage import UsageLimits
from markdownify import markdownify
from faster_whisper import WhisperModel
from yt_dlp import YoutubeDL
load_dotenv()
# (Keep Constants as is)
# --- Constants ---
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
GAIA_REPO_ID = "gaia-benchmark/GAIA"
GAIA_ATTACHMENT_PREFIX = "2023/validation"
IMAGE_URL_PATTERN = re.compile(r"https?://\S+\.(?:png|jpe?g|webp|gif)(?:\?\S*)?", re.IGNORECASE)
AUDIO_URL_PATTERN = re.compile(r"https?://\S+\.(?:mp3|wav|flac|ogg|oga|aac|aiff)(?:\?\S*)?", re.IGNORECASE)
VIDEO_URL_PATTERN = re.compile(r"https?://\S+\.(?:mp4|mov|mkv|webm|avi|m4v)(?:\?\S*)?", re.IGNORECASE)
YOUTUBE_URL_PATTERN = re.compile(r"https?://(?:www\.)?(?:youtube\.com/watch\?\S*v=|youtu\.be/|youtube\.com/shorts/)\S+", re.IGNORECASE)
IMAGE_EXTENSIONS = {".gif", ".jpeg", ".jpg", ".png", ".webp"}
AUDIO_EXTENSIONS = {".aac", ".aiff", ".flac", ".mp3", ".oga", ".ogg", ".wav"}
VIDEO_EXTENSIONS = {".avi", ".m4v", ".mkv", ".mov", ".mp4", ".webm"}
TEXT_EXTENSIONS = {".json", ".md", ".py", ".txt", ".xml"}
SPREADSHEET_EXTENSIONS = {".csv", ".xlsx"}
MEDIA_TYPES_BY_EXTENSION = {
".aac": "audio/aac",
".aiff": "audio/aiff",
".flac": "audio/flac",
".gif": "image/gif",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".png": "image/png",
".wav": "audio/wav",
".webm": "video/webm",
".webp": "image/webp",
}
TEXT_MEDIA_TYPES = {
"application/json",
"application/xhtml+xml",
"text/html",
"text/markdown",
"text/plain",
"text/x-markdown",
}
_CALCULATOR_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
_WHISPER_MODEL = None
_WHISPER_MODEL_LOCK = threading.Lock()
_CHESS_FEN_DETECTOR = None
_CHESS_FEN_DETECTOR_LOCK = threading.Lock()
NPB_TEAM_CODES = {
"buffaloes": "b",
"carp": "c",
"baystars": "db",
"dragons": "d",
"fighters": "f",
"giants": "g",
"hawks": "h",
"lions": "l",
"marines": "m",
"swallows": "s",
"tigers": "t",
"eagles": "e",
"orix": "b",
"hiroshima": "c",
"yokohama": "db",
"chunichi": "d",
"nippon-ham": "f",
"nippon ham": "f",
"hokkaido": "f",
"yomiuri": "g",
"softbank": "h",
"seibu": "l",
"lotte": "m",
"yakult": "s",
"hanshin": "t",
"rakuten": "e",
}
NPB_PLAYER_NAME_ALIASES = {
"taisho tamai": ("玉井", "大翔"),
"taishō tamai": ("玉井", "大翔"),
}
NPB_SURNAME_ROMANIZATION = {
"伊藤": "Itoh",
"上原": "Uehara",
"吉田": "Yoshida",
"玉井": "Tamai",
}
MLB_TEAM_IDS = {
"angels": 108,
"astros": 117,
"athletics": 133,
"blue jays": 141,
"braves": 144,
"brewers": 158,
"cardinals": 138,
"cubs": 112,
"diamondbacks": 109,
"dodgers": 119,
"giants": 137,
"guardians": 114,
"indians": 114,
"mariners": 136,
"marlins": 146,
"mets": 121,
"nationals": 120,
"orioles": 110,
"padres": 135,
"phillies": 143,
"pirates": 134,
"rangers": 140,
"rays": 139,
"red sox": 111,
"reds": 113,
"rockies": 115,
"royals": 118,
"tigers": 116,
"twins": 142,
"white sox": 145,
"yankees": 147,
"new york yankees": 147,
}
MLB_HITTING_STAT_ALIASES = {
"walks": "baseOnBalls",
"walk": "baseOnBalls",
"bb": "baseOnBalls",
"at bats": "atBats",
"at-bats": "atBats",
"ab": "atBats",
"home runs": "homeRuns",
"hr": "homeRuns",
"hits": "hits",
"rbi": "rbi",
"runs": "runs",
"stolen bases": "stolenBases",
"sb": "stolenBases",
}
OLYMPIC_IOC_CODES = {
"Argentina": "ARG",
"Australia": "AUS",
"Austria": "AUT",
"Belgium": "BEL",
"Bulgaria": "BUL",
"Canada": "CAN",
"Chile": "CHI",
"Cuba": "CUB",
"Czechoslovakia": "TCH",
"Denmark": "DEN",
"Egypt": "EGY",
"Estonia": "EST",
"Finland": "FIN",
"France": "FRA",
"Germany": "GER",
"Great Britain": "GBR",
"Greece": "GRE",
"Haiti": "HAI",
"Hungary": "HUN",
"India": "IND",
"Ireland": "IRL",
"Italy": "ITA",
"Japan": "JPN",
"Latvia": "LAT",
"Lithuania": "LTU",
"Luxembourg": "LUX",
"Malta": "MLT",
"Mexico": "MEX",
"Monaco": "MON",
"Netherlands": "NED",
"New Zealand": "NZL",
"Norway": "NOR",
"Panama": "PAN",
"Philippines": "PHI",
"Poland": "POL",
"Portugal": "POR",
"Rhodesia": "RHO",
"Romania": "ROU",
"South Africa": "RSA",
"Spain": "ESP",
"Sweden": "SWE",
"Switzerland": "SUI",
"Turkey": "TUR",
"United States": "USA",
"Uruguay": "URU",
"Yugoslavia": "YUG",
}
DEFUNCT_COUNTRIES = {
"Czechoslovakia",
"East Germany",
"Soviet Union",
"Yugoslavia",
}
def _evaluate_math_expression(expression: str) -> int | float:
def evaluate(node):
if isinstance(node, ast.Expression):
return evaluate(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, int | float):
return node.value
if isinstance(node, ast.UnaryOp) and type(node.op) in _CALCULATOR_OPERATORS:
return _CALCULATOR_OPERATORS[type(node.op)](evaluate(node.operand))
if isinstance(node, ast.BinOp) and type(node.op) in _CALCULATOR_OPERATORS:
return _CALCULATOR_OPERATORS[type(node.op)](evaluate(node.left), evaluate(node.right))
raise ValueError(f"Unsupported expression: {expression}")
return evaluate(ast.parse(expression, mode="eval"))
def _required_env(name: str) -> str:
value = os.getenv(name)
if value is None or value.strip() == "":
raise ValueError(f"Missing required environment variable: {name}")
return value
def _env_enabled(name: str) -> bool:
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
def _normalize_final_answer(answer: str) -> str:
answer = answer.strip()
if not answer:
return answer
currency_match = re.fullmatch(r"\$?\s*([+-]?\d[\d,]*(?:\.\d+)?)", answer)
if currency_match:
return currency_match.group(1).replace(",", "")
if "," not in answer and "\n" not in answer and len(answer.split()) <= 5:
answer = answer.rstrip(".")
return answer
async def _debug_http_request(request: httpx.Request) -> None:
body = request.content
print(f"[LLM HTTP] -> {request.method} {request.url} body_bytes={len(body)}", flush=True)
body_path = os.getenv("AGENT_DEBUG_HTTP_BODY_PATH")
if body_path:
Path(body_path).parent.mkdir(parents=True, exist_ok=True)
Path(body_path).write_bytes(body)
async def _debug_http_response(response: httpx.Response) -> None:
print(f"[LLM HTTP] <- {response.status_code} {response.request.url}", flush=True)
def _load_answer_cache(cache_path: Path) -> dict[str, str]:
if not cache_path.exists():
return {}
try:
with cache_path.open("r", encoding="utf-8") as cache_file:
data = json.load(cache_file)
except (OSError, json.JSONDecodeError) as e:
print(f"Could not load answer cache at {cache_path}: {e}")
return {}
if not isinstance(data, dict):
print(f"Ignoring answer cache at {cache_path}: expected a JSON object.")
return {}
return {str(task_id): str(answer) for task_id, answer in data.items()}
def _save_answer_cache(cache_path: Path, cache: dict[str, str]) -> None:
cache_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = cache_path.with_suffix(f"{cache_path.suffix}.tmp")
with temp_path.open("w", encoding="utf-8") as cache_file:
json.dump(cache, cache_file, ensure_ascii=False, indent=2, sort_keys=True)
temp_path.replace(cache_path)
def _answer_question(agent, item: dict, answer_cache: dict[str, str], cache_path: Path, cache_lock: threading.Lock):
task_id = item.get("task_id")
question_text = item.get("question")
if not task_id or question_text is None:
print(f"Skipping item with missing task_id or question: {item}")
return None, {"Task ID": task_id, "Question": question_text, "Submitted Answer": "SKIPPED: missing task_id or question"}
task_id = str(task_id)
with cache_lock:
cached_answer = answer_cache.get(task_id)
if cached_answer is not None:
print(f"Using cached answer for task {task_id}.")
return {"task_id": task_id, "submitted_answer": cached_answer}, {
"Task ID": task_id,
"Question": question_text,
"Submitted Answer": cached_answer,
}
try:
submitted_answer = agent(item)
except Exception as e:
print(f"Error running agent on task {task_id}: {e}")
return None, {"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"}
with cache_lock:
answer_cache[task_id] = submitted_answer
_save_answer_cache(cache_path, answer_cache)
return {"task_id": task_id, "submitted_answer": submitted_answer}, {
"Task ID": task_id,
"Question": question_text,
"Submitted Answer": submitted_answer,
}
def web_fetch_text(url: str) -> str:
"""Fetch a web page URL and return text or markdown content. Binary files are not supported."""
try:
response = requests.get(
url,
headers={
"Accept": "text/markdown, text/html;q=0.9, application/json;q=0.8, text/plain;q=0.7",
"User-Agent": _required_env("WEB_FETCH_USER_AGENT"),
},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
except requests.RequestException as e:
return f"Could not fetch {url}: {e}. Use web search to find the same information from another source."
if response.status_code == 403:
return f"Could not fetch {url}: the site returned 403 Forbidden. Use web search to find the same information from another source."
if response.status_code == 404:
return f"Could not fetch {url}: the site returned 404 Not Found. Use web search to find the same information from another source."
try:
response.raise_for_status()
except requests.HTTPError as e:
return f"Could not fetch {url}: {e}. Use web search to find the same information from another source."
media_type = response.headers.get("content-type", "").split(";")[0].strip().lower()
if media_type and media_type not in TEXT_MEDIA_TYPES:
return (
f"Cannot fetch {url} as text because it returned content type '{media_type}'. "
"Use direct image URL handling for images. PDFs and other binary files are not supported by this tool."
)
text = response.text
if media_type in ("text/html", "application/xhtml+xml", ""):
text = markdownify(text, strip=["img", "script", "style"])
elif media_type == "application/json":
try:
text = json.dumps(response.json(), indent=2)
except ValueError:
pass
return text
def _download_gaia_attachment(file_name: str) -> Path:
return Path(
hf_hub_download(
repo_id=GAIA_REPO_ID,
repo_type="dataset",
filename=f"{GAIA_ATTACHMENT_PREFIX}/{file_name}",
)
)
def _read_text_attachment(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace")
def _read_pdf_attachment(path: Path) -> str:
reader = PdfReader(path)
chunks = []
for page_index, page in enumerate(reader.pages, start=1):
chunks.append(f"\n--- Page {page_index} ---\n{page.extract_text() or ''}")
return "\n".join(chunks).strip()
def _read_spreadsheet_attachment(path: Path) -> str:
if path.suffix.lower() == ".csv":
frame = pd.read_csv(path)
return frame.to_csv(index=False)
else:
sheets = pd.read_excel(path, sheet_name=None, engine="openpyxl")
parts = []
for sheet_name, frame in sheets.items():
parts.append(f"--- Sheet: {sheet_name} ---")
parts.append(frame.to_csv(index=False))
return "\n".join(parts)
def _download_youtube_video(url: str) -> Path:
output_dir = Path(_required_env("YOUTUBE_DOWNLOAD_DIR"))
output_dir.mkdir(parents=True, exist_ok=True)
options = {
"format": _required_env("YOUTUBE_FORMAT"),
"outtmpl": str(output_dir / "%(id)s.%(ext)s"),
"quiet": True,
"no_warnings": True,
"noplaylist": True,
"merge_output_format": "mp4",
}
with YoutubeDL(options) as ydl:
info = ydl.extract_info(url, download=True)
file_name = ydl.prepare_filename(info)
path = Path(file_name)
if not path.exists() and path.with_suffix(".mp4").exists():
path = path.with_suffix(".mp4")
return path
def _download_direct_video(url: str) -> Path:
output_dir = Path(_required_env("VIDEO_DOWNLOAD_DIR"))
output_dir.mkdir(parents=True, exist_ok=True)
parsed_url = urlparse(url)
suffix = Path(unquote(parsed_url.path)).suffix.lower()
if suffix not in VIDEO_EXTENSIONS:
suffix = ".mp4"
cache_key = hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
output_path = output_dir / f"{cache_key}{suffix}"
if output_path.exists() and output_path.stat().st_size > 0:
return output_path
with requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
stream=True,
) as response:
response.raise_for_status()
temp_path = output_path.with_suffix(f"{output_path.suffix}.tmp")
with temp_path.open("wb") as video_file:
for chunk in response.iter_content(chunk_size=1024 * 1024):
if chunk:
video_file.write(chunk)
temp_path.replace(output_path)
return output_path
def _binary_content_from_path(path: Path) -> BinaryContent:
media_type = MEDIA_TYPES_BY_EXTENSION.get(path.suffix.lower(), "application/octet-stream")
return BinaryContent(data=path.read_bytes(), media_type=media_type)
def _get_whisper_model() -> WhisperModel:
global _WHISPER_MODEL
if _WHISPER_MODEL is None:
with _WHISPER_MODEL_LOCK:
if _WHISPER_MODEL is None:
_WHISPER_MODEL = WhisperModel(
_required_env("ASR_MODEL_SIZE"),
device=_required_env("ASR_DEVICE"),
compute_type=_required_env("ASR_COMPUTE_TYPE"),
)
return _WHISPER_MODEL
def _transcribe_audio_attachment(path: Path) -> str:
cache_dir = Path(_required_env("AUDIO_TRANSCRIPT_CACHE_DIR"))
cache_dir.mkdir(parents=True, exist_ok=True)
cache_path = cache_dir / f"{path.stem}.txt"
if cache_path.exists():
return cache_path.read_text(encoding="utf-8")
model = _get_whisper_model()
segments, info = model.transcribe(str(path), beam_size=5)
transcript = "\n".join(segment.text.strip() for segment in segments if segment.text.strip())
transcript = f"Detected language: {info.language}\n\n{transcript}".strip()
cache_path.write_text(transcript, encoding="utf-8")
return transcript
def _sample_video_frames(path: Path) -> list[Path]:
cache_dir = Path(_required_env("VIDEO_FRAME_CACHE_DIR"))
fps = int(_required_env("VIDEO_FRAME_FPS"))
max_frames = int(_required_env("VIDEO_FRAME_MAX_FRAMES"))
stat = path.stat()
cache_key_source = f"{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}:{fps}:{max_frames}"
cache_key = hashlib.sha256(cache_key_source.encode("utf-8")).hexdigest()[:16]
output_dir = cache_dir / cache_key
output_dir.mkdir(parents=True, exist_ok=True)
cached_frames = sorted(output_dir.glob("frame_*.jpg"))
if cached_frames:
return cached_frames[:max_frames]
command = [
"ffmpeg",
"-y",
"-hide_banner",
"-loglevel",
"error",
"-i",
str(path),
"-vf",
f"fps={fps}",
"-frames:v",
str(max_frames),
str(output_dir / "frame_%04d.jpg"),
]
subprocess.run(command, check=True)
return sorted(output_dir.glob("frame_*.jpg"))[:max_frames]
def _get_chess_fen_detector():
global _CHESS_FEN_DETECTOR
if _CHESS_FEN_DETECTOR is None:
with _CHESS_FEN_DETECTOR_LOCK:
if _CHESS_FEN_DETECTOR is None:
from chess_fen_detector import ChessFENDetector
_CHESS_FEN_DETECTOR = ChessFENDetector()
return _CHESS_FEN_DETECTOR
def _parse_chess_turn(side_to_move: str | None) -> bool:
lowered = (side_to_move or "").strip().lower()
if lowered in {"white", "w"} or "white's turn" in lowered or "white to move" in lowered:
return chess.WHITE
if lowered in {"black", "b"} or "black's turn" in lowered or "black to move" in lowered:
return chess.BLACK
default_turn = _required_env("CHESS_DEFAULT_TURN").strip().lower()
if default_turn == "white":
return chess.WHITE
if default_turn == "black":
return chess.BLACK
raise ValueError("CHESS_DEFAULT_TURN must be either 'white' or 'black'.")
def _normalize_detected_fen_placement(detected_fen: str) -> str:
placement = detected_fen.strip().replace("-", "/").split()[0]
rows = placement.split("/")
if len(rows) != 8:
raise ValueError(f"Chess FEN detector returned invalid board placement: {detected_fen}")
return placement
def _board_from_placement(placement: str, turn: bool, orientation: str) -> chess.Board:
if orientation == "white":
board = chess.Board(placement + " w - - 0 1")
elif orientation == "black":
board = chess.Board.empty()
files = "abcdefgh"
for rank_index, row in enumerate(placement.split("/")):
visual_rank = 8 - rank_index
visual_file_index = 0
for symbol in row:
if symbol.isdigit():
visual_file_index += int(symbol)
continue
actual_file = files[7 - visual_file_index]
actual_rank = 9 - visual_rank
square = chess.parse_square(f"{actual_file}{actual_rank}")
board.set_piece_at(square, chess.Piece.from_symbol(symbol))
visual_file_index += 1
else:
raise ValueError("CHESS_BOARD_ORIENTATION must be either 'white' or 'black'.")
board.turn = turn
board.clear_stack()
if not board.is_valid():
raise ValueError(f"Detected chess board is not valid: {board.fen()}")
return board
def _stockfish_best_move_san(board: chess.Board) -> str:
engine_path = _required_env("STOCKFISH_PATH")
time_limit = float(_required_env("CHESS_ENGINE_TIME_LIMIT"))
with chess.engine.SimpleEngine.popen_uci(engine_path) as engine:
result = engine.play(board, chess.engine.Limit(time=time_limit))
return board.san(result.move)
def solve_chess_position_from_attachment(file_name: str, side_to_move: str | None = None) -> str:
"""Solve a chess-board image attachment and return the best move in algebraic notation.
Args:
file_name: The attached chess-board image file name, for example "task-id.png".
side_to_move: The side whose turn it is, usually "white" or "black".
"""
file_name = Path(file_name.strip().strip("\"'")).name
attachment_path = _download_gaia_attachment(file_name)
detector = _get_chess_fen_detector()
detected_fen = detector.predict_fen(str(attachment_path))
placement = _normalize_detected_fen_placement(detected_fen)
turn = _parse_chess_turn(side_to_move)
orientation = _required_env("CHESS_BOARD_ORIENTATION").strip().lower()
board = _board_from_placement(placement, turn, orientation)
answer = _stockfish_best_move_san(board)
print(f"Chess detector FEN: {detected_fen}")
print(f"Chess solver board FEN: {board.fen()}")
return answer
def _npb_team_codes_to_search(team: str) -> list[str]:
normalized = team.strip().lower()
if not normalized:
return sorted(set(NPB_TEAM_CODES.values()))
for name, code in NPB_TEAM_CODES.items():
if name in normalized:
return [code]
if normalized in set(NPB_TEAM_CODES.values()):
return [normalized]
return sorted(set(NPB_TEAM_CODES.values()))
def _npb_name_tokens(player_name: str) -> tuple[str, ...]:
normalized = player_name.strip().lower()
if normalized in NPB_PLAYER_NAME_ALIASES:
return NPB_PLAYER_NAME_ALIASES[normalized]
return tuple(part for part in player_name.replace(" ", " ").split() if part)
def _npb_registered_players(year: str, team_code: str) -> list[dict[str, str]]:
url = f"https://npb.jp/announcement/{year}/registered_{team_code}.html"
response = requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
response.encoding = response.apparent_encoding or response.encoding
soup = BeautifulSoup(response.text, "html.parser")
players = []
for table in soup.find_all("table"):
for row in table.find_all("tr"):
cells = [cell.get_text(" ", strip=True) for cell in row.find_all(["td", "th"])]
if len(cells) != 4 or cells[0] == "公示日":
continue
announced, position, number, name = cells
if not number.isdigit():
continue
players.append(
{
"announced": announced,
"position": position.replace("\u3000", ""),
"number": number,
"name": name,
"team_code": team_code,
"source": url,
}
)
return players
def _npb_romanized_surname(japanese_name: str) -> str:
surname = japanese_name.replace("\u3000", " ").split()[0]
return NPB_SURNAME_ROMANIZATION.get(surname, surname)
def find_npb_adjacent_pitchers_by_number(year: str, player_name: str, team: str = "") -> str:
"""Use official NPB historical registered roster pages to find adjacent pitcher jersey numbers.
Use this for Japanese NPB questions about historical roster registration, jersey numbers,
and pitchers before/after a named player as of a stated year.
Args:
year: The roster year to inspect, for example "2023".
player_name: The player name to find, in Roman or Japanese characters.
team: Optional NPB team name, for example "Hokkaido Nippon-Ham Fighters".
"""
year = str(year).strip()
name_tokens = _npb_name_tokens(player_name)
if not year.isdigit() or len(year) != 4:
return f"Invalid NPB roster year: {year!r}"
if not name_tokens:
return "No player name was provided."
checked_sources = []
for team_code in _npb_team_codes_to_search(team):
try:
players = _npb_registered_players(year, team_code)
except requests.RequestException as e:
checked_sources.append(f"{team_code}: {e}")
continue
checked_sources.append(f"https://npb.jp/announcement/{year}/registered_{team_code}.html")
target = next(
(
player
for player in players
if all(token.lower() in player["name"].lower() or token in player["name"] for token in name_tokens)
),
None,
)
if target is None:
continue
target_number = int(target["number"])
pitchers = [
player for player in players
if player["position"] == "投手" and int(player["number"]) in {target_number - 1, target_number + 1}
]
pitchers.sort(key=lambda player: int(player["number"]))
if len(pitchers) != 2:
return (
f"Found {target['name']} as number {target_number} on official NPB {year} team {team_code}, "
f"but did not find both adjacent pitcher numbers. "
f"Adjacent pitcher rows found: {pitchers}. Source: {target['source']}"
)
before, after = pitchers
return (
f"Official NPB {year} registered roster source: {target['source']}\n"
f"Target: #{target_number} {target['name']} ({_npb_romanized_surname(target['name'])})\n"
f"Pitcher before: #{before['number']} {before['name']} ({_npb_romanized_surname(before['name'])})\n"
f"Pitcher after: #{after['number']} {after['name']} ({_npb_romanized_surname(after['name'])})\n"
f"Answer format requested by the benchmark: {_npb_romanized_surname(before['name'])}, {_npb_romanized_surname(after['name'])}"
)
return f"Could not find {player_name} in official NPB {year} registered roster pages checked: {checked_sources}"
def _mlb_team_id(team: str) -> int:
normalized = team.strip().lower()
if normalized.isdigit():
return int(normalized)
for name, team_id in MLB_TEAM_IDS.items():
if name in normalized:
return team_id
raise ValueError(f"Unknown MLB team: {team!r}")
def _mlb_hitting_stat_key(stat_name: str) -> str:
normalized = stat_name.strip().lower()
if normalized in MLB_HITTING_STAT_ALIASES:
return MLB_HITTING_STAT_ALIASES[normalized]
for name, key in MLB_HITTING_STAT_ALIASES.items():
if name in normalized:
return key
return stat_name.strip()
def find_mlb_hitting_stat_for_team_leader(
year: str,
team: str,
leader_stat: str,
return_stat: str,
) -> str:
"""Use the official MLB Stats API to answer historical team hitting stat-leader questions.
Use this for MLB questions like "the Yankee with the most walks in 1977" and then
asking for another stat from that same season.
Args:
year: The season year, for example "1977".
team: MLB team name or id, for example "New York Yankees".
leader_stat: Stat to rank players by, for example "walks" or "home runs".
return_stat: Stat to return for the leader, for example "at bats".
"""
year = str(year).strip()
if not year.isdigit() or len(year) != 4:
return f"Invalid MLB season year: {year!r}"
team_id = _mlb_team_id(team)
leader_key = _mlb_hitting_stat_key(leader_stat)
return_key = _mlb_hitting_stat_key(return_stat)
url = (
"https://statsapi.mlb.com/api/v1/stats"
f"?stats=season&group=hitting&season={year}&sportIds=1&teamId={team_id}&limit=1000"
)
response = requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
splits = response.json().get("stats", [{}])[0].get("splits", [])
rows = []
for split in splits:
stat = split.get("stat", {})
player = split.get("player", {})
if leader_key not in stat or return_key not in stat:
continue
rows.append(
{
"player": player.get("fullName", ""),
"leader_value": int(stat[leader_key]),
"return_value": stat[return_key],
"stat": stat,
}
)
if not rows:
return f"No MLB hitting rows found for team {team!r}, year {year}, stats {leader_key}/{return_key}. Source: {url}"
rows.sort(key=lambda row: row["leader_value"], reverse=True)
leader = rows[0]
preview = "\n".join(
f"{index + 1}. {row['player']}: {leader_key}={row['leader_value']}, {return_key}={row['return_value']}"
for index, row in enumerate(rows[:5])
)
return (
f"Official MLB Stats API source: {url}\n"
f"Team id: {team_id}; season: {year}\n"
f"Ranking stat: {leader_key}; requested stat: {return_key}\n"
f"Top rows:\n{preview}\n"
f"Answer: {leader['return_value']}"
)
def _wikipedia_revision_before(page_title: str, latest_year: str | None = None) -> tuple[int | None, str | None]:
if not latest_year:
return None, None
year = str(latest_year).strip()
if not year:
return None, None
if re.fullmatch(r"\d{4}", year):
rvstart = f"{year}-12-31T23:59:59Z"
else:
rvstart = year
response = requests.get(
"https://en.wikipedia.org/w/api.php",
params={
"action": "query",
"prop": "revisions",
"titles": page_title,
"rvlimit": 1,
"rvdir": "older",
"rvprop": "ids|timestamp",
"rvstart": rvstart,
"format": "json",
},
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
pages = response.json().get("query", {}).get("pages", {})
page = next(iter(pages.values()), {})
revisions = page.get("revisions") or []
if not revisions:
return None, None
revision = revisions[0]
return int(revision["revid"]), revision.get("timestamp")
def _wikipedia_page_html(page_title: str, oldid: int | None = None) -> str:
if oldid is None:
url = f"https://en.wikipedia.org/wiki/{page_title.replace(' ', '_')}"
else:
url = f"https://en.wikipedia.org/w/index.php?title={page_title.replace(' ', '_')}&oldid={oldid}"
response = requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
return response.text
def extract_wikipedia_table(
page_title: str,
table_query: str = "",
latest_year: str | None = None,
year_column: str = "Year",
start_year: str | None = None,
end_year: str | None = None,
) -> str:
"""Extract a structured table from English Wikipedia, optionally from the latest revision before a year/date.
Use this for questions that ask for counts or values from Wikipedia tables, especially when rows matter.
Args:
page_title: English Wikipedia page title, for example "Mercedes Sosa".
table_query: Text that should appear in the desired table or nearby table content, for example "studio albums".
latest_year: Optional year/date constraint, for example "2022" for the latest revision in 2022.
year_column: Name of the year column to filter on.
start_year: Optional inclusive lower bound for the year column.
end_year: Optional inclusive upper bound for the year column.
"""
from io import StringIO
oldid, timestamp = _wikipedia_revision_before(page_title, latest_year)
html = _wikipedia_page_html(page_title, oldid)
tables = pd.read_html(StringIO(html))
if not tables:
return f"No tables found on English Wikipedia page {page_title!r}."
query = table_query.strip().lower()
selected_index = 0
selected_table = tables[0]
if query:
for index, table in enumerate(tables):
table_text = table.to_string(index=False).lower()
if query in table_text:
selected_index = index
selected_table = table
break
else:
return f"No table on {page_title!r} matched query {table_query!r}. Found {len(tables)} table(s)."
filtered = selected_table.copy()
if start_year or end_year:
matching_columns = [column for column in filtered.columns if str(column).strip().lower() == year_column.strip().lower()]
if not matching_columns:
return (
f"Selected table {selected_index} has no year column {year_column!r}. "
f"Columns: {list(map(str, filtered.columns))}"
)
column = matching_columns[0]
years = pd.to_numeric(filtered[column], errors="coerce")
if start_year:
years_start = int(start_year)
filtered = filtered[years >= years_start]
years = years[years >= years_start]
if end_year:
years_end = int(end_year)
filtered = filtered[years <= years_end]
csv_rows = filtered.to_csv(index=False)
source = f"English Wikipedia page {page_title!r}"
if oldid is not None:
source += f", oldid={oldid}, timestamp={timestamp}"
return (
f"Source: {source}\n"
f"Selected table index: {selected_index}\n"
f"Filtered row count: {len(filtered)}\n"
f"Rows:\n{csv_rows}"
)
def find_olympic_ioc_code_with_fewest_athletes(year: str, games: str = "Summer") -> str:
"""Find the IOC country code for the nation with the fewest athletes at an Olympic Games.
Uses the structured Country/Athletes table on the English Wikipedia Olympic Games page.
If multiple countries tie for the fewest athletes, this sorts country names alphabetically.
Args:
year: Olympic year, for example "1928".
games: "Summer" or "Winter".
"""
from io import StringIO
year = str(year).strip()
games = games.strip().title()
if not year.isdigit() or len(year) != 4:
return f"Invalid Olympic year: {year!r}"
if games not in {"Summer", "Winter"}:
return f"Invalid Olympic games type: {games!r}"
page_title = f"{year}_{games}_Olympics"
url = f"https://en.wikipedia.org/wiki/{page_title}"
response = requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
tables = pd.read_html(StringIO(response.text))
athlete_table = None
for table in tables:
columns = [str(column) for column in table.columns]
if "Country" in columns and "Athletes" in columns:
athlete_table = table
break
if athlete_table is None:
return f"No Country/Athletes table found on {url}."
rows = athlete_table[["Country", "Athletes"]].copy()
rows["Country"] = rows["Country"].astype(str).str.replace(r"\s*\[.*?\]", "", regex=True).str.strip()
rows["Athletes"] = pd.to_numeric(rows["Athletes"], errors="coerce")
rows = rows.dropna(subset=["Athletes"])
rows["Athletes"] = rows["Athletes"].astype(int)
fewest = rows["Athletes"].min()
tied = rows[rows["Athletes"] == fewest].sort_values("Country")
country = str(tied.iloc[0]["Country"])
code = OLYMPIC_IOC_CODES.get(country)
if code is None:
return f"Found {country} with {fewest} athlete(s), but no IOC code mapping is configured. Source: {url}"
tied_preview = ", ".join(f"{row.Country} ({row.Athletes})" for row in tied.itertuples(index=False))
return (
f"Source: {url}\n"
f"Fewest athlete count: {fewest}\n"
f"Countries tied, alphabetically: {tied_preview}\n"
f"Selected country: {country}\n"
f"IOC code answer: {code}"
)
def find_malko_recipient_from_defunct_country(
start_year_exclusive: str = "1977",
end_year_inclusive: str = "1999",
) -> str:
"""Find the Malko Competition recipient in a year range whose recorded nationality is a defunct country.
Uses the structured recipient table on English Wikipedia's Malko Competition page.
Args:
start_year_exclusive: Lower year bound, excluded.
end_year_inclusive: Upper year bound, included.
"""
from io import StringIO
start_year = int(start_year_exclusive)
end_year = int(end_year_inclusive)
url = "https://en.wikipedia.org/wiki/Malko_Competition"
response = requests.get(
url,
headers={"User-Agent": _required_env("WEB_FETCH_USER_AGENT")},
timeout=int(_required_env("WEB_FETCH_TIMEOUT")),
)
response.raise_for_status()
tables = pd.read_html(StringIO(response.text))
if not tables:
return f"No tables found on {url}."
table = tables[0].copy()
table["Year"] = pd.to_numeric(table["Year"], errors="coerce")
candidates = table[
(table["Year"] > start_year)
& (table["Year"] <= end_year)
& table["Nationality"].isin(DEFUNCT_COUNTRIES)
].sort_values("Year")
if candidates.empty:
return (
f"No Malko Competition recipients from defunct countries found between "
f"{start_year + 1} and {end_year}. Source: {url}"
)
row = candidates.iloc[0]
recipient = str(row["Recipient"]).split("[", 1)[0].strip()
first_name = recipient.split()[0]
return (
f"Source: {url}\n"
f"Matching row: Year={int(row['Year'])}, Recipient={recipient}, Nationality={row['Nationality']}\n"
f"First name answer: {first_name}"
)
# --- Basic Agent Definition ---
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
class BasicAgent:
def __init__(self):
self.base_url = _required_env("LOCAL_LLM_BASE_URL").rstrip("/")
self.model = _required_env("LOCAL_LLM_MODEL")
self.timeout = int(_required_env("LOCAL_LLM_TIMEOUT"))
self.search_results = int(_required_env("WEB_SEARCH_MAX_RESULTS"))
http_client = None
if _env_enabled("AGENT_DEBUG_HTTP"):
http_client = httpx.AsyncClient(
timeout=self.timeout,
event_hooks={
"request": [_debug_http_request],
"response": [_debug_http_response],
}
)
llm = OpenAIChatModel(
self.model,
provider=OpenAIProvider(
base_url=self.base_url,
api_key=_required_env("LOCAL_LLM_API_KEY"),
http_client=http_client,
),
profile=OpenAIModelProfile(
openai_supports_strict_tool_definition=False,
),
)
self.agent = Agent(
llm,
system_prompt=(
"You are an answer-only agent for a benchmark. "
"First decide whether the question is self-contained. "
"For self-contained tasks such as reversed text, wordplay, arithmetic, logic puzzles, text transformations, "
"or questions answerable from the prompt or attached content, answer directly without web search. "
"Use web search only for external public facts, current facts, obscure facts not provided in the prompt, "
"or when the question explicitly asks you to look something up. "
"Use web fetch only when you need to read a specific web page URL. "
"Do not use web fetch for PDFs or other binary files. "
"For roster, jersey number, standings, stats, award, membership, or team-history questions with a stated date or year, "
"use sources for that exact date or year; do not use current pages unless the question asks for current data. "
"For Japanese NPB roster or jersey-number questions by year, use the find_npb_adjacent_pitchers_by_number tool "
"instead of generic web search when the question asks for pitchers before or after a player by number. "
"For MLB historical team hitting stat-leader questions, use the find_mlb_hitting_stat_for_team_leader tool "
"instead of generic web search or snippets. "
"For questions asking for counts or values from Wikipedia tables, use the extract_wikipedia_table tool; "
"when the question names a Wikipedia version year, pass that year as latest_year and count table rows rather than interpreting prose. "
"For Olympic questions asking for the country with the fewest athletes and an IOC code, use the "
"find_olympic_ioc_code_with_fewest_athletes tool. "
"For Malko Competition recipient questions involving nationality and defunct/no-longer-existing countries, "
"use the find_malko_recipient_from_defunct_country tool. "
"For chess-board image tasks asking for the best or next move in algebraic notation, "
"use the solve_chess_position_from_attachment tool with the attached file name and side to move. "
"Do not repeat the same search. If a search does not resolve the question, make the best answer from available evidence. "
"Use tools only when they are necessary to answer exactly. "
"Return only the final answer, with no explanation."
),
tools=[
duckduckgo_search_tool(max_results=self.search_results),
web_fetch_text,
solve_chess_position_from_attachment,
find_npb_adjacent_pitchers_by_number,
find_mlb_hitting_stat_for_team_leader,
extract_wikipedia_table,
find_olympic_ioc_code_with_fewest_athletes,
find_malko_recipient_from_defunct_country,
],
retries=1,
)
self.agent.tool_plain(self.calculator)
self.agent.tool_plain(self.current_date)
print(f"BasicAgent initialized with model '{self.model}' at {self.base_url}.")
@staticmethod
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression."""
result = _evaluate_math_expression(expression)
if isinstance(result, float) and result.is_integer():
return str(int(result))
return str(result)
@staticmethod
def current_date() -> str:
"""Return today's date in ISO format."""
return pd.Timestamp.now(tz="Europe/Lisbon").date().isoformat()
def __call__(self, task: str | dict) -> str:
question = task.get("question") if isinstance(task, dict) else task
if question is None:
raise ValueError("Task is missing question text.")
print(f"Agent received question (first 50 chars): {question[:50]}...")
prompt = self._build_prompt(question, task if isinstance(task, dict) else None)
model_settings = {
"temperature": 0,
"timeout": self.timeout,
}
usage_limits = UsageLimits(
request_limit=int(_required_env("AGENT_REQUEST_LIMIT")),
tool_calls_limit=int(_required_env("AGENT_TOOL_CALLS_LIMIT")),
)
try:
result = self.agent.run_sync(
prompt,
model_settings=model_settings,
usage_limits=usage_limits,
)
except Exception as e:
if "maximum context length" not in str(e):
raise
print("Context length exceeded. Retrying without tools using the original question only.")
result = self.agent.run_sync(
question,
model_settings=model_settings,
usage_limits=usage_limits,
toolsets=[],
)
answer = _normalize_final_answer(result.output)
if not answer:
raise ValueError("Local LLM returned no answer content.")
print(f"Agent returning answer (first 50 chars): {answer[:50]}...")
return answer
@staticmethod
def _build_prompt(question: str, task: dict | None = None) -> str | list:
image_urls = IMAGE_URL_PATTERN.findall(question)
audio_urls = AUDIO_URL_PATTERN.findall(question)
video_urls = VIDEO_URL_PATTERN.findall(question)
youtube_urls = YOUTUBE_URL_PATTERN.findall(question)
prompt = [question]
prompt.extend(ImageUrl(url=url.rstrip(").,]"), force_download=True) for url in image_urls)
prompt.extend(AudioUrl(url=url.rstrip(").,]"), force_download=True) for url in audio_urls)
for url in video_urls:
BasicAgent._append_direct_video(prompt, url.rstrip(").,]"))
for url in youtube_urls:
BasicAgent._append_youtube_video(prompt, url.rstrip(").,]"))
file_name = task.get("file_name") if task else None
if file_name:
BasicAgent._append_attachment(prompt, str(file_name))
if len(prompt) == 1:
return question
return prompt
@staticmethod
def _append_attachment(prompt: list, file_name: str) -> None:
try:
attachment_path = _download_gaia_attachment(file_name)
except Exception as e:
prompt.append(f"\nAttachment {file_name} could not be downloaded from {GAIA_REPO_ID}: {e}")
return
suffix = attachment_path.suffix.lower()
prompt.append(f"\nAttached file: {file_name}")
if suffix in IMAGE_EXTENSIONS:
prompt.append(_binary_content_from_path(attachment_path))
elif suffix in AUDIO_EXTENSIONS:
prompt.append(f"\nAudio transcript:\n{_transcribe_audio_attachment(attachment_path)}")
elif suffix in VIDEO_EXTENSIONS:
BasicAgent._append_video_analysis(prompt, attachment_path, f"Attached video: {file_name}")
elif suffix in TEXT_EXTENSIONS:
prompt.append(f"\nAttachment content:\n{_read_text_attachment(attachment_path)}")
elif suffix == ".pdf":
prompt.append(f"\nExtracted PDF text:\n{_read_pdf_attachment(attachment_path)}")
elif suffix in SPREADSHEET_EXTENSIONS:
prompt.append(f"\nSpreadsheet preview:\n{_read_spreadsheet_attachment(attachment_path)}")
else:
prompt.append(f"\nAttachment type {suffix or '(no extension)'} is not supported yet.")
@staticmethod
def _append_youtube_video(prompt: list, url: str) -> None:
try:
video_path = _download_youtube_video(url)
except Exception as e:
prompt.append(f"\nYouTube video {url} could not be downloaded: {e}")
return
BasicAgent._append_video_analysis(prompt, video_path, f"Downloaded YouTube video: {url}")
@staticmethod
def _append_direct_video(prompt: list, url: str) -> None:
try:
video_path = _download_direct_video(url)
except Exception as e:
prompt.append(f"\nVideo {url} could not be downloaded: {e}")
return
BasicAgent._append_video_analysis(prompt, video_path, f"Downloaded video: {url}")
@staticmethod
def _append_video_analysis(prompt: list, video_path: Path, label: str) -> None:
prompt.append(f"\n{label}")
try:
transcript = _transcribe_audio_attachment(video_path)
except Exception as e:
transcript = f"Video audio could not be transcribed: {e}"
prompt.append(f"\nVideo transcript:\n{transcript}")
try:
frames = _sample_video_frames(video_path)
except Exception as e:
prompt.append(f"\nVideo frames could not be extracted: {e}")
return
if not frames:
prompt.append("\nNo video frames were extracted.")
return
fps = int(_required_env("VIDEO_FRAME_FPS"))
prompt.append(
f"\nVideo frames sampled in chronological order at {fps} frame(s) per second, "
f"capped at {int(_required_env('VIDEO_FRAME_MAX_FRAMES'))} frames."
)
for frame_path in frames:
prompt.append(BinaryContent(data=frame_path.read_bytes(), media_type="image/jpeg"))
def run_and_submit_all( profile: gr.OAuthProfile | None):
"""
Fetches all questions, runs the BasicAgent on them, submits all answers,
and displays the results.
"""
# --- Determine HF Space Runtime URL and Repo URL ---
space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
if profile:
username= f"{profile.username}"
print(f"User logged in: {username}")
else:
print("User not logged in.")
return "Please Login to Hugging Face with the button.", None
api_url = DEFAULT_API_URL
questions_url = f"{api_url}/questions"
submit_url = f"{api_url}/submit"
# 1. Instantiate Agent ( modify this part to create your agent)
try:
agent = BasicAgent()
except Exception as e:
print(f"Error instantiating agent: {e}")
return f"Error initializing agent: {e}", None
# In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
print(agent_code)
# 2. Fetch Questions
print(f"Fetching questions from: {questions_url}")
try:
response = requests.get(questions_url, timeout=15)
response.raise_for_status()
questions_data = response.json()
if not questions_data:
print("Fetched questions list is empty.")
return "Fetched questions list is empty or invalid format.", None
print(f"Fetched {len(questions_data)} questions.")
except requests.exceptions.RequestException as e:
print(f"Error fetching questions: {e}")
return f"Error fetching questions: {e}", None
except requests.exceptions.JSONDecodeError as e:
print(f"Error decoding JSON response from questions endpoint: {e}")
print(f"Response text: {response.text[:500]}")
return f"Error decoding server response for questions: {e}", None
except Exception as e:
print(f"An unexpected error occurred fetching questions: {e}")
return f"An unexpected error occurred fetching questions: {e}", None
# 3. Run your Agent
results_log = []
answers_payload = []
max_workers = int(_required_env("AGENT_MAX_WORKERS"))
cache_path = Path(_required_env("AGENT_ANSWER_CACHE_PATH"))
answer_cache = _load_answer_cache(cache_path)
cache_lock = threading.Lock()
print(f"Running agent on {len(questions_data)} questions with {max_workers} worker(s)...")
print(f"Loaded {len(answer_cache)} cached answer(s) from {cache_path}.")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(_answer_question, agent, item, answer_cache, cache_path, cache_lock)
for item in questions_data
]
for future in as_completed(futures):
answer, result_log = future.result()
results_log.append(result_log)
if answer:
answers_payload.append(answer)
if not answers_payload:
print("Agent did not produce any answers to submit.")
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
# 4. Prepare Submission
submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
print(status_update)
# 5. Submit
print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
try:
response = requests.post(submit_url, json=submission_data, timeout=60)
response.raise_for_status()
result_data = response.json()
final_status = (
f"Submission Successful!\n"
f"User: {result_data.get('username')}\n"
f"Overall Score: {result_data.get('score', 'N/A')}% "
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
f"Message: {result_data.get('message', 'No message received.')}"
)
print("Submission successful.")
results_df = pd.DataFrame(results_log)
return final_status, results_df
except requests.exceptions.HTTPError as e:
error_detail = f"Server responded with status {e.response.status_code}."
try:
error_json = e.response.json()
error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
except requests.exceptions.JSONDecodeError:
error_detail += f" Response: {e.response.text[:500]}"
status_message = f"Submission Failed: {error_detail}"
print(status_message)
results_df = pd.DataFrame(results_log)
return status_message, results_df
except requests.exceptions.Timeout:
status_message = "Submission Failed: The request timed out."
print(status_message)
results_df = pd.DataFrame(results_log)
return status_message, results_df
except requests.exceptions.RequestException as e:
status_message = f"Submission Failed: Network error - {e}"
print(status_message)
results_df = pd.DataFrame(results_log)
return status_message, results_df
except Exception as e:
status_message = f"An unexpected error occurred during submission: {e}"
print(status_message)
results_df = pd.DataFrame(results_log)
return status_message, results_df
# --- Build Gradio Interface using Blocks ---
with gr.Blocks() as demo:
gr.Markdown("# Basic Agent Evaluation Runner")
gr.Markdown(
"""
**Instructions:**
1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
---
**Disclaimers:**
Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
"""
)
gr.LoginButton()
run_button = gr.Button("Run Evaluation & Submit All Answers")
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
# Removed max_rows=10 from DataFrame constructor
results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
run_button.click(
fn=run_and_submit_all,
outputs=[status_output, results_table]
)
if __name__ == "__main__":
print("\n" + "-"*30 + " App Starting " + "-"*30)
# Check for SPACE_HOST and SPACE_ID at startup for information
space_host_startup = os.getenv("SPACE_HOST")
space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
if space_host_startup:
print(f"✅ SPACE_HOST found: {space_host_startup}")
print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
else:
print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
if space_id_startup: # Print repo URLs if SPACE_ID is found
print(f"✅ SPACE_ID found: {space_id_startup}")
print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
else:
print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
print("-"*(60 + len(" App Starting ")) + "\n")
print("Launching Gradio Interface for Basic Agent Evaluation...")
demo.launch(debug=True, share=False)