DriptoBhattacharyya Claude Opus 4.8 commited on
Commit
ef849ff
·
1 Parent(s): 21bbba7

Cache good answers by task_id so re-runs skip solved questions

Browse files

GaiaAgent 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>

Files changed (3) hide show
  1. .gitignore +3 -0
  2. gaia_agent/agent.py +42 -7
  3. 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
- print("GaiaAgent initialised (LangGraph + Groq).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """Run the graph on one question, bounded by QUESTION_TIMEOUT seconds.
 
 
 
 
 
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
- return answer or "Unable to determine an answer."
 
 
 
 
 
 
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)