yc1838 commited on
Commit
dcd46a6
·
1 Parent(s): d256c19

feat: retry gaia task once after cooldown

Browse files
Files changed (2) hide show
  1. src/lilith_agent/runner.py +26 -4
  2. tests/test_runner.py +73 -1
src/lilith_agent/runner.py CHANGED
@@ -4,6 +4,7 @@ import json
4
  import logging
5
  import os
6
  import re
 
7
  from pathlib import Path
8
  from typing import Any
9
 
@@ -159,11 +160,19 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
159
  answers: list[dict] = []
160
 
161
  from lilith_agent.config import Config
162
- from lilith_agent.models import get_cheap_model
163
  cfg = Config.from_env()
164
  cheap_model = get_cheap_model(cfg)
165
 
166
  total = len(questions)
 
 
 
 
 
 
 
 
167
  for idx, question in enumerate(questions, start=1):
168
  reset_vision_state()
169
  task_id = question.get("task_id")
@@ -205,10 +214,23 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
205
  "iterations": 0
206
  }
207
 
208
- from lilith_agent.memory import ephemeral_memory
209
  try:
210
- with ephemeral_memory():
211
- result = graph.invoke(state, {"configurable": {"thread_id": task_id}})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  except Exception as exc:
213
  log_runner.warning("[runner] task=%s agent error: %s", task_id, exc)
214
  answers.append(
 
4
  import logging
5
  import os
6
  import re
7
+ import time
8
  from pathlib import Path
9
  from typing import Any
10
 
 
160
  answers: list[dict] = []
161
 
162
  from lilith_agent.config import Config
163
+ from lilith_agent.models import RateLimitCooldownError, get_cheap_model, rate_limit_question_scope
164
  cfg = Config.from_env()
165
  cheap_model = get_cheap_model(cfg)
166
 
167
  total = len(questions)
168
+
169
+ def _invoke_task_once(task_state: dict, task_id: str):
170
+ from lilith_agent.memory import ephemeral_memory
171
+
172
+ with rate_limit_question_scope():
173
+ with ephemeral_memory():
174
+ return graph.invoke(task_state, {"configurable": {"thread_id": task_id}})
175
+
176
  for idx, question in enumerate(questions, start=1):
177
  reset_vision_state()
178
  task_id = question.get("task_id")
 
214
  "iterations": 0
215
  }
216
 
 
217
  try:
218
+ try:
219
+ result = _invoke_task_once(state, task_id)
220
+ except RateLimitCooldownError as exc:
221
+ log_runner.warning(
222
+ "[runner] task=%s rate limited provider=%s model=%s cooldown=%s",
223
+ task_id,
224
+ exc.provider,
225
+ exc.model,
226
+ exc.cooldown_seconds,
227
+ )
228
+ time.sleep(exc.cooldown_seconds)
229
+ result = _invoke_task_once(state, task_id)
230
+ except RateLimitCooldownError as exc:
231
+ log_runner.warning("[runner] task=%s rate limited after retry: %s", task_id, exc)
232
+ answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
233
+ continue
234
  except Exception as exc:
235
  log_runner.warning("[runner] task=%s agent error: %s", task_id, exc)
236
  answers.append(
tests/test_runner.py CHANGED
@@ -5,7 +5,11 @@ from pathlib import Path
5
 
6
  import pytest
7
 
8
- from lilith_agent.runner import _wrap_user_question, _write_checkpoint_atomic
 
 
 
 
9
 
10
 
11
  def test_wrap_escapes_closing_tag_to_prevent_injection():
@@ -57,3 +61,71 @@ def test_atomic_write_does_not_corrupt_existing_file_on_serialization_failure(tm
57
  data = json.loads(dest.read_text())
58
  assert data["submitted_answer"] == "good"
59
  assert list(tmp_path.glob("*.tmp")) == []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  import pytest
7
 
8
+ from langchain_core.messages import AIMessage
9
+
10
+ from lilith_agent.config import Config
11
+ from lilith_agent.models import RateLimitCooldownError
12
+ from lilith_agent.runner import run_agent_on_questions, _wrap_user_question, _write_checkpoint_atomic
13
 
14
 
15
  def test_wrap_escapes_closing_tag_to_prevent_injection():
 
61
  data = json.loads(dest.read_text())
62
  assert data["submitted_answer"] == "good"
63
  assert list(tmp_path.glob("*.tmp")) == []
64
+
65
+
66
+ @pytest.fixture
67
+ def runner_test_config() -> Config:
68
+ return Config(
69
+ cheap_provider="google",
70
+ cheap_model="gemini-3-flash-preview",
71
+ strong_provider="google",
72
+ strong_model="gemini-3.1-pro",
73
+ extra_strong_provider="google",
74
+ extra_strong_model="gemini-3.1-pro",
75
+ vision_provider="fal",
76
+ vision_model="gemini-3-flash-preview",
77
+ fal_vision_api_key="",
78
+ api_url="",
79
+ checkpoint_dir="",
80
+ whisper_model="base",
81
+ anthropic_api_key="",
82
+ google_api_key="",
83
+ huggingface_api_key="",
84
+ tavily_api_key="",
85
+ lmstudio_base_url="",
86
+ max_tokens=1024,
87
+ llm_formatter_enabled=True,
88
+ )
89
+
90
+
91
+ @pytest.fixture(autouse=True)
92
+ def _isolate_runner_model_setup(monkeypatch, runner_test_config):
93
+ monkeypatch.setattr(Config, "from_env", classmethod(lambda cls: runner_test_config))
94
+ monkeypatch.setattr("lilith_agent.models.get_cheap_model", lambda cfg: object())
95
+
96
+
97
+ class _GraphFailsOnceWithCooldown:
98
+ def __init__(self):
99
+ self.calls = 0
100
+ self.thread_ids = []
101
+
102
+ def invoke(self, state, config):
103
+ self.calls += 1
104
+ self.thread_ids.append(config["configurable"]["thread_id"])
105
+ if self.calls == 1:
106
+ raise RateLimitCooldownError(
107
+ provider="google",
108
+ model="gemini-3.1-pro",
109
+ cooldown_seconds=12,
110
+ original_error="429",
111
+ )
112
+ return {"messages": [AIMessage(content="Final Answer: 42")]}
113
+
114
+
115
+ def test_runner_retries_same_question_once_after_cooldown(monkeypatch, tmp_path: Path):
116
+ monkeypatch.setattr("lilith_agent.runner._final_formatting_cleanup", lambda model, question, raw, llm_formatter_enabled=True: raw)
117
+ sleeps = []
118
+ monkeypatch.setattr("lilith_agent.runner.time.sleep", sleeps.append)
119
+ graph = _GraphFailsOnceWithCooldown()
120
+
121
+ answers = run_agent_on_questions(
122
+ graph,
123
+ [{"task_id": "task-1", "question": "What is 6*7?"}],
124
+ tmp_path,
125
+ )
126
+
127
+ assert graph.calls == 2
128
+ assert graph.thread_ids == ["task-1", "task-1"]
129
+ assert sleeps == [12]
130
+ assert answers == [{"task_id": "task-1", "submitted_answer": "Final Answer: 42"}]
131
+ assert (tmp_path / "task-1.json").exists()