Spaces:
Running on Zero
Running on Zero
retry
Browse files
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: GAIA Agent -
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
@@ -14,7 +14,7 @@ hf_oauth_expiration_minutes: 480
|
|
| 14 |
|
| 15 |
# GAIA course — controlled-cost OpenAI agent
|
| 16 |
|
| 17 |
-
This is an **original student agent**, not an official solution. It uses **GPT-
|
| 18 |
|
| 19 |
## Installation in your EXISTING ZeroGPU Space
|
| 20 |
|
|
@@ -29,13 +29,13 @@ This is an **original student agent**, not an official solution. It uses **GPT-5
|
|
| 29 |
9. After the batch, click **Download answer backup** again. The batch stops on OpenAI API errors and the best-effort local budget guard. It does NOT submit to the leaderboard automatically.
|
| 30 |
10. Once you have as many answers as possible, press **Submit answers to GAIA scoring**. You can explicitly opt to submit blank answers for remaining unsupported questions. Scoring occurs only upon this click, and blank answers count as wrong. The Unit 4 target is 30% (6 correct out of 20).
|
| 31 |
|
| 32 |
-
**Only one model is enabled:** `gpt-
|
| 33 |
|
| 34 |
## Budget controls and safety
|
| 35 |
|
| 36 |
- Default `GAIA_BUDGET_USD=2.00` local estimated TOTAL, `GAIA_QUESTION_BUDGET_USD=0.35` local estimated per question. You can change these in **Space Settings → Variables** later if needed; starting low is intentional. Never put secrets in Variables.
|
| 37 |
-
- Maximum **5 paid model requests/question**,
|
| 38 |
-
- Before each request, the app temporarily reserves **$0.
|
| 39 |
- This guard **cannot enforce a hard limit on your OpenAI account**. OpenAI bills based on actual requests; actual costs could differ because of multimodal pricing, missing usage metadata, failures, deployment restarts, other apps/keys or changes in price. Set a project budget/alerts if available on the OpenAI dashboard and monitor its actual usage. Disable auto-recharge if you don't want it.
|
| 40 |
- The application **can** run up to 20 eligible questions in one explicitly authorized batch, but defaults to 3 and does not automatically buy or renew credits.
|
| 41 |
- Answer and local estimated spend files use **`/data` only when persistence is available**, otherwise temporary `/tmp` storage. On a Space restart, the latter may reset! Use **Download answer backup** and restore it if required. The budget estimate also resets in /tmp, so always check provider usage separately.
|
|
@@ -53,3 +53,11 @@ This is an **original student agent**, not an official solution. It uses **GPT-5
|
|
| 53 |
- Certificate and submission details: https://huggingface.co/learn/agents-course/unit4/hands-on
|
| 54 |
|
| 55 |
The current version was syntax-checked and offline-tested for 404 skip, cache preservation, continuation past unavailable tasks, no model calls on missing attachments, and the scoring payload. It has **not** been executed against your OpenAI key or deployed live on your Space.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: GAIA Agent - GPT-6 Luna Responses
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
|
|
| 14 |
|
| 15 |
# GAIA course — controlled-cost OpenAI agent
|
| 16 |
|
| 17 |
+
This is an **original student agent**, not an official solution. It uses **GPT-6 Luna**, a limited sequence of model calls, public web search, local document extraction, a calculator, and a separate Hugging Face scoring submission button. **There is no guarantee of a particular score.**
|
| 18 |
|
| 19 |
## Installation in your EXISTING ZeroGPU Space
|
| 20 |
|
|
|
|
| 29 |
9. After the batch, click **Download answer backup** again. The batch stops on OpenAI API errors and the best-effort local budget guard. It does NOT submit to the leaderboard automatically.
|
| 30 |
10. Once you have as many answers as possible, press **Submit answers to GAIA scoring**. You can explicitly opt to submit blank answers for remaining unsupported questions. Scoring occurs only upon this click, and blank answers count as wrong. The Unit 4 target is 30% (6 correct out of 20).
|
| 31 |
|
| 32 |
+
**Only one model is enabled:** `gpt-6-luna`, list prices checked October 28, 2026: **$0.10/M input tokens and $0.50/M output tokens**. The agent uses the **Responses API** with `reasoning.effort=medium` for function calling; built-in OpenAI tools are disabled (public web search is performed locally). The OpenAI API key is NOT the same as a ChatGPT subscription.
|
| 33 |
|
| 34 |
## Budget controls and safety
|
| 35 |
|
| 36 |
- Default `GAIA_BUDGET_USD=2.00` local estimated TOTAL, `GAIA_QUESTION_BUDGET_USD=0.35` local estimated per question. You can change these in **Space Settings → Variables** later if needed; starting low is intentional. Never put secrets in Variables.
|
| 37 |
+
- Maximum **5 paid model requests/question**, 6,500 output tokens/request (including reasoning), **8 locally run tools/question**. No implicit retries; OpenAI SDK retries disabled.
|
| 38 |
+
- Before each request, the app temporarily reserves **$0.03** against the estimated budget; after a successful call, it reconciles using the model's reported input/output token usage. Failed calls keep their reservation to avoid undercounting. This is a conservative approximation, not an absolute per-call billing limit.
|
| 39 |
- This guard **cannot enforce a hard limit on your OpenAI account**. OpenAI bills based on actual requests; actual costs could differ because of multimodal pricing, missing usage metadata, failures, deployment restarts, other apps/keys or changes in price. Set a project budget/alerts if available on the OpenAI dashboard and monitor its actual usage. Disable auto-recharge if you don't want it.
|
| 40 |
- The application **can** run up to 20 eligible questions in one explicitly authorized batch, but defaults to 3 and does not automatically buy or renew credits.
|
| 41 |
- Answer and local estimated spend files use **`/data` only when persistence is available**, otherwise temporary `/tmp` storage. On a Space restart, the latter may reset! Use **Download answer backup** and restore it if required. The budget estimate also resets in /tmp, so always check provider usage separately.
|
|
|
|
| 53 |
- Certificate and submission details: https://huggingface.co/learn/agents-course/unit4/hands-on
|
| 54 |
|
| 55 |
The current version was syntax-checked and offline-tested for 404 skip, cache preservation, continuation past unavailable tasks, no model calls on missing attachments, and the scoring payload. It has **not** been executed against your OpenAI key or deployed live on your Space.
|
| 56 |
+
|
| 57 |
+
## Data-sharing token incentive
|
| 58 |
+
|
| 59 |
+
If the organization is eligible **and enrolled**, GPT-6 Luna is in the **250K daily shared complimentary-token group for Build tier**, NOT the 2.5M group. The free quota is not guaranteed. Tokens exceeding the quota and separately priced API tools are billable. In particular, this app uses only local web functions; it does not enable the OpenAI built-in web-search tool. See https://help.openai.com/en/articles/10306912-sharing-feedback-evaluation-and-fine-tuning-data-and-api-inputs-and-outputs-with-openai . Monitor real billing in OpenAI Platform.
|
| 60 |
+
|
| 61 |
+
Keep the previous answer backup before redeploying. The in-memory and /tmp caches may be reset; use the built-in **Restore answers** button after you deploy. Previous wrong answers will NOT automatically be regenerated. Choose **Clear selected Q#** to retry selected questions after creating a backup.
|
| 62 |
+
|
| 63 |
+
GPT-6 Luna uses Responses API with medium reasoning. If the model outputs no answer after 6,500 tokens, this app deliberately does not save a blank answer; the call can still consume tokens.
|
agent.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""GAIA research agent using OpenAI
|
| 2 |
|
| 3 |
No OpenAI calls occur on import, class construction, or question download.
|
| 4 |
Every model call passes through BudgetLedger; automatic API retries are disabled.
|
|
@@ -24,18 +24,19 @@ from bs4 import BeautifulSoup
|
|
| 24 |
from openai import OpenAI
|
| 25 |
|
| 26 |
SCORING_API = "https://agents-course-unit4-scoring.hf.space"
|
| 27 |
-
#
|
| 28 |
-
MODEL = "gpt-
|
| 29 |
-
INPUT_PRICE_PER_M = 0.
|
| 30 |
-
OUTPUT_PRICE_PER_M =
|
|
|
|
| 31 |
MAX_ESTIMATED_TOTAL_USD = float(os.getenv("GAIA_BUDGET_USD", "2.00"))
|
| 32 |
MAX_ESTIMATED_QUESTION_USD = float(os.getenv("GAIA_QUESTION_BUDGET_USD", "0.35"))
|
| 33 |
MAX_MODEL_CALLS_PER_QUESTION = 5
|
| 34 |
-
MAX_COMPLETION_TOKENS =
|
| 35 |
MAX_TOTAL_TOOL_CALLS = 8
|
| 36 |
MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024
|
| 37 |
# Reserve pessimistically before a request. A failed call keeps its reservation.
|
| 38 |
-
PER_CALL_RESERVATION_USD = 0.
|
| 39 |
|
| 40 |
STORAGE = Path("/data/gaia-openai") if Path("/data").is_dir() and os.access("/data", os.W_OK) else Path("/tmp/gaia-openai")
|
| 41 |
STORAGE.mkdir(parents=True, exist_ok=True)
|
|
@@ -114,8 +115,8 @@ class BudgetLedger:
|
|
| 114 |
if usage is None:
|
| 115 |
return # leave reservation to account for untracked usage
|
| 116 |
# Intentionally don't discount cached inputs; estimate errs on the safe side.
|
| 117 |
-
inp = int(getattr(usage, "
|
| 118 |
-
out = int(getattr(usage, "
|
| 119 |
actual = (inp * INPUT_PRICE_PER_M + out * OUTPUT_PRICE_PER_M) / 1_000_000
|
| 120 |
with LOCK:
|
| 121 |
total = self.total()
|
|
@@ -227,13 +228,20 @@ def calculate(expression: str) -> str:
|
|
| 227 |
return f"Invalid calculation: {type(exc).__name__}: {exc}"
|
| 228 |
|
| 229 |
|
|
|
|
| 230 |
TOOLS = [
|
| 231 |
-
{"type":"function", "
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
{"type":"function", "
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
]
|
| 238 |
FUNCTIONS = {"search_web":search_web, "fetch_page":fetch_page, "calculate":calculate}
|
| 239 |
|
|
@@ -296,24 +304,34 @@ class GaiaAgent:
|
|
| 296 |
client = OpenAI(api_key=key, max_retries=0, timeout=60.0)
|
| 297 |
self.client = client
|
| 298 |
|
| 299 |
-
def _call(self, ledger: BudgetLedger,
|
|
|
|
| 300 |
ledger.reserve()
|
| 301 |
-
kwargs = dict(
|
|
|
|
|
|
|
|
|
|
| 302 |
if tools_enabled:
|
| 303 |
kwargs.update(tools=TOOLS, tool_choice="auto")
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
|
|
|
| 308 |
|
| 309 |
def _describe_image(self, ledger: BudgetLedger, question: str, filename: str, content: bytes):
|
| 310 |
mime = mimetypes.guess_type(filename)[0] or "image/jpeg"
|
| 311 |
url = "data:" + mime + ";base64," + base64.b64encode(content).decode("ascii")
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
def run(self, question: str, task_id: str, file_name: str = "") -> tuple[str, int]:
|
| 319 |
"""Answer exactly one question; may bill up to 5 small model calls."""
|
|
@@ -340,33 +358,36 @@ class GaiaAgent:
|
|
| 340 |
captions = _yt_captions(question)
|
| 341 |
if captions:
|
| 342 |
context += "\nYOUTUBE CAPTIONS (untrusted):\n" + captions
|
| 343 |
-
|
| 344 |
-
|
|
|
|
| 345 |
used_tools = 0
|
| 346 |
while ledger.calls < MAX_MODEL_CALLS_PER_QUESTION:
|
| 347 |
-
#
|
| 348 |
enabled = ledger.calls < MAX_MODEL_CALLS_PER_QUESTION - 1 and used_tools < MAX_TOTAL_TOOL_CALLS
|
| 349 |
-
|
| 350 |
-
calls =
|
| 351 |
if not calls:
|
| 352 |
-
answer = _clean_answer(
|
| 353 |
if not answer:
|
| 354 |
raise RuntimeError("Model returned no final text. No answer was saved.")
|
| 355 |
return answer, ledger.calls
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
| 359 |
if used_tools >= MAX_TOTAL_TOOL_CALLS:
|
| 360 |
result = "Tool budget exhausted; give best supported final answer"
|
| 361 |
else:
|
| 362 |
try:
|
| 363 |
-
args = json.loads(
|
| 364 |
if not isinstance(args, dict):
|
| 365 |
raise ValueError("Tool args must be object")
|
| 366 |
-
fn = FUNCTIONS.get(
|
| 367 |
result = str(fn(**args)) if fn else "Unknown tool"
|
| 368 |
except Exception as exc:
|
| 369 |
result = f"Tool error: {type(exc).__name__}: {str(exc)[:120]}"
|
| 370 |
used_tools += 1
|
| 371 |
-
|
|
|
|
| 372 |
raise RuntimeError("No final answer before model call cap; no answer saved")
|
|
|
|
| 1 |
+
"""GAIA research agent using OpenAI Responses API and local (unbilled) tools.
|
| 2 |
|
| 3 |
No OpenAI calls occur on import, class construction, or question download.
|
| 4 |
Every model call passes through BudgetLedger; automatic API retries are disabled.
|
|
|
|
| 24 |
from openai import OpenAI
|
| 25 |
|
| 26 |
SCORING_API = "https://agents-course-unit4-scoring.hf.space"
|
| 27 |
+
# Pinned model and standard pricing (Oct 28, 2026).
|
| 28 |
+
MODEL = "gpt-6-luna"
|
| 29 |
+
INPUT_PRICE_PER_M = 0.10
|
| 30 |
+
OUTPUT_PRICE_PER_M = 0.50
|
| 31 |
+
REASONING_EFFORT = "medium" # Responses API supports reasoning + tool use for Luna
|
| 32 |
MAX_ESTIMATED_TOTAL_USD = float(os.getenv("GAIA_BUDGET_USD", "2.00"))
|
| 33 |
MAX_ESTIMATED_QUESTION_USD = float(os.getenv("GAIA_QUESTION_BUDGET_USD", "0.35"))
|
| 34 |
MAX_MODEL_CALLS_PER_QUESTION = 5
|
| 35 |
+
MAX_COMPLETION_TOKENS = 6500 # includes invisible reasoning tokens
|
| 36 |
MAX_TOTAL_TOOL_CALLS = 8
|
| 37 |
MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024
|
| 38 |
# Reserve pessimistically before a request. A failed call keeps its reservation.
|
| 39 |
+
PER_CALL_RESERVATION_USD = 0.03
|
| 40 |
|
| 41 |
STORAGE = Path("/data/gaia-openai") if Path("/data").is_dir() and os.access("/data", os.W_OK) else Path("/tmp/gaia-openai")
|
| 42 |
STORAGE.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 115 |
if usage is None:
|
| 116 |
return # leave reservation to account for untracked usage
|
| 117 |
# Intentionally don't discount cached inputs; estimate errs on the safe side.
|
| 118 |
+
inp = int(getattr(usage, "input_tokens", 0) or 0)
|
| 119 |
+
out = int(getattr(usage, "output_tokens", 0) or 0)
|
| 120 |
actual = (inp * INPUT_PRICE_PER_M + out * OUTPUT_PRICE_PER_M) / 1_000_000
|
| 121 |
with LOCK:
|
| 122 |
total = self.total()
|
|
|
|
| 228 |
return f"Invalid calculation: {type(exc).__name__}: {exc}"
|
| 229 |
|
| 230 |
|
| 231 |
+
# Responses API uses a flat function-tool schema (not Chat Completions' nested schema).
|
| 232 |
TOOLS = [
|
| 233 |
+
{"type": "function", "name": "search_web",
|
| 234 |
+
"description": "Search public websites for facts. Returns URLs and snippets.",
|
| 235 |
+
"parameters": {"type": "object", "properties": {"query": {"type": "string"}},
|
| 236 |
+
"required": ["query"], "additionalProperties": False}, "strict": True},
|
| 237 |
+
{"type": "function", "name": "fetch_page",
|
| 238 |
+
"description": "Read a public web page or PDF by URL.",
|
| 239 |
+
"parameters": {"type": "object", "properties": {"url": {"type": "string"}},
|
| 240 |
+
"required": ["url"], "additionalProperties": False}, "strict": True},
|
| 241 |
+
{"type": "function", "name": "calculate",
|
| 242 |
+
"description": "Calculate numerical expression with + - * / ** sqrt round.",
|
| 243 |
+
"parameters": {"type": "object", "properties": {"expression": {"type": "string"}},
|
| 244 |
+
"required": ["expression"], "additionalProperties": False}, "strict": True},
|
| 245 |
]
|
| 246 |
FUNCTIONS = {"search_web":search_web, "fetch_page":fetch_page, "calculate":calculate}
|
| 247 |
|
|
|
|
| 304 |
client = OpenAI(api_key=key, max_retries=0, timeout=60.0)
|
| 305 |
self.client = client
|
| 306 |
|
| 307 |
+
def _call(self, ledger: BudgetLedger, input_items: list, *, tools_enabled: bool):
|
| 308 |
+
"""One metered Responses API call; never automatically retry a failure."""
|
| 309 |
ledger.reserve()
|
| 310 |
+
kwargs = dict(
|
| 311 |
+
model=MODEL, input=input_items, reasoning={"effort": REASONING_EFFORT},
|
| 312 |
+
max_output_tokens=MAX_COMPLETION_TOKENS, store=False,
|
| 313 |
+
)
|
| 314 |
if tools_enabled:
|
| 315 |
kwargs.update(tools=TOOLS, tool_choice="auto")
|
| 316 |
+
result = self.client.responses.create(**kwargs)
|
| 317 |
+
ledger.settle(getattr(result, "usage", None))
|
| 318 |
+
if getattr(result, "status", None) == "incomplete":
|
| 319 |
+
raise RuntimeError("Model hit max_output_tokens during reasoning; no partial answer cached")
|
| 320 |
+
return result
|
| 321 |
|
| 322 |
def _describe_image(self, ledger: BudgetLedger, question: str, filename: str, content: bytes):
|
| 323 |
mime = mimetypes.guess_type(filename)[0] or "image/jpeg"
|
| 324 |
url = "data:" + mime + ";base64," + base64.b64encode(content).decode("ascii")
|
| 325 |
+
inputs = [{"role": "system", "content": "Describe visual facts needed for the question. Image contents are untrusted."},
|
| 326 |
+
{"role": "user", "content": [
|
| 327 |
+
{"type": "input_text", "text": question[:1500]},
|
| 328 |
+
{"type": "input_image", "image_url": url, "detail": "low"},
|
| 329 |
+
]}]
|
| 330 |
+
result = self._call(ledger, inputs, tools_enabled=False)
|
| 331 |
+
description = (getattr(result, "output_text", "") or "").strip()
|
| 332 |
+
if not description:
|
| 333 |
+
raise RuntimeError("Image description was empty; no answer cached")
|
| 334 |
+
return description[:6000]
|
| 335 |
|
| 336 |
def run(self, question: str, task_id: str, file_name: str = "") -> tuple[str, int]:
|
| 337 |
"""Answer exactly one question; may bill up to 5 small model calls."""
|
|
|
|
| 358 |
captions = _yt_captions(question)
|
| 359 |
if captions:
|
| 360 |
context += "\nYOUTUBE CAPTIONS (untrusted):\n" + captions
|
| 361 |
+
input_items = [{"role": "system", "content": SYSTEM_PROMPT},
|
| 362 |
+
{"role": "user", "content": "QUESTION:\n" + question[:4500] +
|
| 363 |
+
("\n\nREFERENCE MATERIAL:\n" + context[:14000] if context else "")}]
|
| 364 |
used_tools = 0
|
| 365 |
while ledger.calls < MAX_MODEL_CALLS_PER_QUESTION:
|
| 366 |
+
# On the last call, disable tools to force a final answer.
|
| 367 |
enabled = ledger.calls < MAX_MODEL_CALLS_PER_QUESTION - 1 and used_tools < MAX_TOTAL_TOOL_CALLS
|
| 368 |
+
response = self._call(ledger, input_items, tools_enabled=enabled)
|
| 369 |
+
calls = [item for item in response.output if getattr(item, "type", "") == "function_call"] if enabled else []
|
| 370 |
if not calls:
|
| 371 |
+
answer = _clean_answer(getattr(response, "output_text", "") or "")
|
| 372 |
if not answer:
|
| 373 |
raise RuntimeError("Model returned no final text. No answer was saved.")
|
| 374 |
return answer, ledger.calls
|
| 375 |
+
# Reuse output including encrypted reasoning in stateless mode. The SDK
|
| 376 |
+
# accepts response.output items as inputs for subsequent tool turns.
|
| 377 |
+
input_items.extend(response.output)
|
| 378 |
+
for call in calls:
|
| 379 |
if used_tools >= MAX_TOTAL_TOOL_CALLS:
|
| 380 |
result = "Tool budget exhausted; give best supported final answer"
|
| 381 |
else:
|
| 382 |
try:
|
| 383 |
+
args = json.loads(call.arguments or "{}")
|
| 384 |
if not isinstance(args, dict):
|
| 385 |
raise ValueError("Tool args must be object")
|
| 386 |
+
fn = FUNCTIONS.get(call.name)
|
| 387 |
result = str(fn(**args)) if fn else "Unknown tool"
|
| 388 |
except Exception as exc:
|
| 389 |
result = f"Tool error: {type(exc).__name__}: {str(exc)[:120]}"
|
| 390 |
used_tools += 1
|
| 391 |
+
input_items.append({"type": "function_call_output", "call_id": call.call_id,
|
| 392 |
+
"output": result[:6500]})
|
| 393 |
raise RuntimeError("No final answer before model call cap; no answer saved")
|