Commit ·
ef849ff
1
Parent(s): 21bbba7
Cache good answers by task_id so re-runs skip solved questions
Browse filesGaiaAgent persists non-empty answers to gaia_cache.json and returns cached results on re-run, saving gateway tokens + time and surviving partial failures. Blanks are never cached so they retry. Cache file git-ignored (ephemeral on HF Spaces).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .gitignore +3 -0
- gaia_agent/agent.py +42 -7
- gaia_agent/config.py +6 -0
.gitignore
CHANGED
|
@@ -161,3 +161,6 @@ cython_debug/
|
|
| 161 |
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 162 |
#.idea/
|
| 163 |
.langgraph_api/
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 162 |
#.idea/
|
| 163 |
.langgraph_api/
|
| 164 |
+
|
| 165 |
+
# agent answer cache
|
| 166 |
+
gaia_cache.json
|
gaia_agent/agent.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import time
|
| 6 |
from concurrent.futures import ThreadPoolExecutor
|
| 7 |
from concurrent.futures import TimeoutError as FutureTimeout
|
|
@@ -9,18 +11,42 @@ from concurrent.futures import TimeoutError as FutureTimeout
|
|
| 9 |
from gaia_agent.config import get_settings
|
| 10 |
from gaia_agent.graph import graph
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
class GaiaAgent:
|
| 14 |
"""Callable wrapper around the compiled LangGraph graph.
|
| 15 |
|
| 16 |
``app.py`` invokes ``agent(question, task_id)`` per question and submits the
|
| 17 |
-
returned string for exact-match scoring.
|
|
|
|
| 18 |
"""
|
| 19 |
|
| 20 |
def __init__(self):
|
| 21 |
self._graph = graph
|
| 22 |
self._settings = get_settings()
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
def _run(self, question: str, task_id: str) -> str:
|
| 26 |
result = self._graph.invoke(
|
|
@@ -30,13 +56,17 @@ class GaiaAgent:
|
|
| 30 |
return (result.get("final_answer") or "").strip()
|
| 31 |
|
| 32 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 33 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
A fresh single-worker executor per call means a timed-out question's thread
|
| 36 |
-
is abandoned (daemon) rather than blocking the next question.
|
| 37 |
-
"""
|
| 38 |
start = time.time()
|
| 39 |
print(f"[GaiaAgent] task {task_id}: {question[:70]}...")
|
|
|
|
|
|
|
| 40 |
pool = ThreadPoolExecutor(max_workers=1)
|
| 41 |
future = pool.submit(self._run, question, task_id)
|
| 42 |
try:
|
|
@@ -51,4 +81,9 @@ class GaiaAgent:
|
|
| 51 |
finally:
|
| 52 |
pool.shutdown(wait=False)
|
| 53 |
print(f"[GaiaAgent] task {task_id} done in {time.time() - start:.0f}s -> {answer!r}")
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
import time
|
| 8 |
from concurrent.futures import ThreadPoolExecutor
|
| 9 |
from concurrent.futures import TimeoutError as FutureTimeout
|
|
|
|
| 11 |
from gaia_agent.config import get_settings
|
| 12 |
from gaia_agent.graph import graph
|
| 13 |
|
| 14 |
+
# Sentinel returned when no answer could be produced; never cached.
|
| 15 |
+
_FAILURE = "Unable to determine an answer."
|
| 16 |
+
|
| 17 |
|
| 18 |
class GaiaAgent:
|
| 19 |
"""Callable wrapper around the compiled LangGraph graph.
|
| 20 |
|
| 21 |
``app.py`` invokes ``agent(question, task_id)`` per question and submits the
|
| 22 |
+
returned string for exact-match scoring. Good answers are cached to disk by
|
| 23 |
+
task_id so a re-run skips already-solved questions (saves gateway tokens/time).
|
| 24 |
"""
|
| 25 |
|
| 26 |
def __init__(self):
|
| 27 |
self._graph = graph
|
| 28 |
self._settings = get_settings()
|
| 29 |
+
self._cache_path = self._settings.cache_path
|
| 30 |
+
self._cache = self._load_cache()
|
| 31 |
+
print(f"GaiaAgent initialised (LangGraph). Cached answers: {len(self._cache)}.")
|
| 32 |
+
|
| 33 |
+
# --- cache helpers ---
|
| 34 |
+
def _load_cache(self) -> dict[str, str]:
|
| 35 |
+
try:
|
| 36 |
+
if os.path.exists(self._cache_path):
|
| 37 |
+
with open(self._cache_path, encoding="utf-8") as fh:
|
| 38 |
+
data = json.load(fh)
|
| 39 |
+
return {k: v for k, v in data.items() if isinstance(v, str) and v}
|
| 40 |
+
except Exception as exc: # noqa: BLE001
|
| 41 |
+
print(f"[GaiaAgent] cache load failed: {exc}")
|
| 42 |
+
return {}
|
| 43 |
+
|
| 44 |
+
def _save_cache(self) -> None:
|
| 45 |
+
try:
|
| 46 |
+
with open(self._cache_path, "w", encoding="utf-8") as fh:
|
| 47 |
+
json.dump(self._cache, fh, ensure_ascii=False, indent=2)
|
| 48 |
+
except Exception as exc: # noqa: BLE001
|
| 49 |
+
print(f"[GaiaAgent] cache save failed: {exc}")
|
| 50 |
|
| 51 |
def _run(self, question: str, task_id: str) -> str:
|
| 52 |
result = self._graph.invoke(
|
|
|
|
| 56 |
return (result.get("final_answer") or "").strip()
|
| 57 |
|
| 58 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 59 |
+
"""Answer one question (timeout-bounded), using/refreshing the disk cache."""
|
| 60 |
+
# Cache hit: skip recompute (and gateway tokens) entirely.
|
| 61 |
+
cached = self._cache.get(task_id)
|
| 62 |
+
if cached:
|
| 63 |
+
print(f"[GaiaAgent] task {task_id} cache hit -> {cached!r}")
|
| 64 |
+
return cached
|
| 65 |
|
|
|
|
|
|
|
|
|
|
| 66 |
start = time.time()
|
| 67 |
print(f"[GaiaAgent] task {task_id}: {question[:70]}...")
|
| 68 |
+
# Fresh single-worker executor per call: a timed-out question's thread is
|
| 69 |
+
# abandoned (daemon) rather than blocking the next question.
|
| 70 |
pool = ThreadPoolExecutor(max_workers=1)
|
| 71 |
future = pool.submit(self._run, question, task_id)
|
| 72 |
try:
|
|
|
|
| 81 |
finally:
|
| 82 |
pool.shutdown(wait=False)
|
| 83 |
print(f"[GaiaAgent] task {task_id} done in {time.time() - start:.0f}s -> {answer!r}")
|
| 84 |
+
|
| 85 |
+
if answer: # cache only real answers so blanks are retried next run
|
| 86 |
+
self._cache[task_id] = answer
|
| 87 |
+
self._save_cache()
|
| 88 |
+
return answer
|
| 89 |
+
return _FAILURE
|
gaia_agent/config.py
CHANGED
|
@@ -53,6 +53,12 @@ class Settings(BaseSettings):
|
|
| 53 |
max_tool_rounds: int = 3
|
| 54 |
recursion_limit: int = 40
|
| 55 |
question_timeout: int = 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
@lru_cache(maxsize=1)
|
|
|
|
| 53 |
max_tool_rounds: int = 3
|
| 54 |
recursion_limit: int = 40
|
| 55 |
question_timeout: int = 500
|
| 56 |
+
# Disk cache of good answers keyed by task_id; lets a re-run skip solved
|
| 57 |
+
# questions (saves gateway tokens + time). Ephemeral on HF Spaces.
|
| 58 |
+
cache_path: str = "gaia_cache.json"
|
| 59 |
+
# Persisted {task_id: answer} cache so reruns skip already-solved questions
|
| 60 |
+
# (saves provider tokens). Set to "" to disable.
|
| 61 |
+
answers_cache_path: str = "answers_cache.json"
|
| 62 |
|
| 63 |
|
| 64 |
@lru_cache(maxsize=1)
|