Spaces:
Sleeping
Sleeping
Download src/scoring_api.py from Wikkkkkoooo/gaia-agent-final: direct link, hf CLI and curl.
- Browser
- Download file 5.97 kB
-
https://huggingface.co/spaces/Wikkkkkoooo/gaia-agent-final/resolve/main/src/scoring_api.py
- Command line
-
hf download hf://spaces/Wikkkkkoooo/gaia-agent-final/src/scoring_api.py
-
curl -L -o scoring_api.py https://huggingface.co/spaces/Wikkkkkoooo/gaia-agent-final/resolve/main/src/scoring_api.py
5.97 kB
| """Typed client for the Unit 4 GAIA scoring API. | |
| Four routes are exposed by the course backend: | |
| * ``GET /questions`` -> list of question objects | |
| * ``GET /random-question`` -> a single question object | |
| * ``GET /files/{task_id}`` -> the raw file attached to a task (bytes) | |
| * ``POST /submit`` -> score the submitted answers | |
| Every function here wraps one of those routes with timeouts, retry on | |
| transient failures, and clear exceptions. JSON routes return parsed Python | |
| objects; the file route returns raw ``bytes``. | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from typing import Any, Dict, List, Mapping, Sequence | |
| import requests | |
| from . import config | |
| class ScoringAPIError(RuntimeError): | |
| """Raised when the scoring API returns an error or is unreachable.""" | |
| def __init__(self, message: str, *, status_code: int | None = None) -> None: | |
| super().__init__(message) | |
| self.status_code = status_code | |
| # Status codes worth retrying: transient server / gateway errors. | |
| _RETRYABLE_STATUS = {500, 502, 503, 504} | |
| def _request( | |
| method: str, | |
| path: str, | |
| *, | |
| json_body: Any | None = None, | |
| expect_json: bool = True, | |
| ) -> Any: | |
| """Perform an HTTP request with timeout, retry and uniform error handling. | |
| Returns parsed JSON when ``expect_json`` is True, otherwise raw bytes. | |
| Raises :class:`ScoringAPIError` on any unrecoverable failure. | |
| """ | |
| url = f"{config.SCORING_API_BASE}/{path.lstrip('/')}" | |
| last_exc: Exception | None = None | |
| for attempt in range(1, config.MAX_RETRIES + 1): | |
| try: | |
| response = requests.request( | |
| method, | |
| url, | |
| json=json_body, | |
| timeout=config.DEFAULT_TIMEOUT, | |
| ) | |
| except requests.RequestException as exc: | |
| # Network-level failure (DNS, connection reset, timeout). Retry, but | |
| # don't sleep after the final attempt — we're about to give up. | |
| last_exc = exc | |
| if attempt < config.MAX_RETRIES: | |
| _backoff(attempt) | |
| continue | |
| # Retry transient server errors; surface everything else immediately. | |
| if response.status_code in _RETRYABLE_STATUS and attempt < config.MAX_RETRIES: | |
| last_exc = ScoringAPIError( | |
| f"{method} {url} returned {response.status_code}", | |
| status_code=response.status_code, | |
| ) | |
| _backoff(attempt) | |
| continue | |
| if not response.ok: | |
| raise ScoringAPIError( | |
| f"{method} {url} failed: {response.status_code} {response.text[:300]}", | |
| status_code=response.status_code, | |
| ) | |
| if expect_json: | |
| try: | |
| return response.json() | |
| except ValueError as exc: | |
| raise ScoringAPIError( | |
| f"{method} {url} returned non-JSON body: {response.text[:200]}" | |
| ) from exc | |
| return response.content | |
| # Exhausted retries. | |
| raise ScoringAPIError( | |
| f"{method} {url} failed after {config.MAX_RETRIES} attempts: {last_exc}" | |
| ) from last_exc | |
| def _backoff(attempt: int) -> None: | |
| """Exponential backoff: 1s, 2s, 4s, ... capped at 16s.""" | |
| time.sleep(min(2 ** (attempt - 1), 16)) | |
| def get_questions() -> List[Dict[str, Any]]: | |
| """Return the full list of Unit 4 questions. | |
| Each item is a dict that includes at least ``task_id`` and ``question``; | |
| tasks with attachments also carry a non-empty ``file_name``. | |
| """ | |
| return _request("GET", "/questions") | |
| def get_random_question() -> Dict[str, Any]: | |
| """Return a single random question dict (handy for quick dry runs).""" | |
| return _request("GET", "/random-question") | |
| def get_file(task_id: str) -> bytes: | |
| """Download the raw file attached to ``task_id`` and return its bytes. | |
| The caller is responsible for writing the bytes to disk and interpreting | |
| them (csv/xlsx/image/audio/...). See ``files.py`` for that logic. | |
| """ | |
| if not task_id: | |
| raise ValueError("task_id is required to fetch a file") | |
| return _request("GET", f"/files/{task_id}", expect_json=False) | |
| def submit_answers( | |
| username: str, | |
| agent_code: str, | |
| answers: Sequence[Mapping[str, str]], | |
| ) -> Dict[str, Any]: | |
| """Submit answers for scoring and return the parsed result. | |
| Parameters | |
| ---------- | |
| username: | |
| Your Hugging Face username. | |
| agent_code: | |
| Public Space code link ending in ``/tree/main``. | |
| answers: | |
| Sequence of ``{"task_id": ..., "submitted_answer": ...}`` mappings. | |
| Use :func:`build_answer` / :func:`build_answers` to construct these. | |
| The returned dict typically contains ``score``, ``correct_count``, | |
| ``total_attempted`` and a human-readable ``message``. | |
| """ | |
| if not username: | |
| raise ValueError("username is required") | |
| if not agent_code or not agent_code.rstrip("/").endswith("/tree/main"): | |
| raise ValueError("agent_code must be a public Space link ending in /tree/main") | |
| if not answers: | |
| raise ValueError("answers must contain at least one item") | |
| payload = { | |
| "username": username.strip(), | |
| "agent_code": agent_code.strip(), | |
| "answers": list(answers), | |
| } | |
| return _request("POST", "/submit", json_body=payload) | |
| def build_answer(task_id: str, submitted_answer: Any) -> Dict[str, str]: | |
| """Build a single answer entry in the exact format the API expects. | |
| The submitted answer is coerced to ``str`` because scoring is exact-match | |
| over text; normalization of the *content* belongs in ``normalize.py``. | |
| """ | |
| return {"task_id": str(task_id), "submitted_answer": str(submitted_answer)} | |
| def build_answers(pairs: Mapping[str, Any]) -> List[Dict[str, str]]: | |
| """Build the full answers list from a ``{task_id: answer}`` mapping.""" | |
| return [build_answer(task_id, answer) for task_id, answer in pairs.items()] | |