Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
a16b876
1
Parent(s): a2a0291
feat: handle rate limit streak and batch abort
Browse files- src/lilith_agent/runner.py +23 -1
- tests/test_runner.py +45 -1
src/lilith_agent/runner.py
CHANGED
|
@@ -160,7 +160,13 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
|
|
| 160 |
answers: list[dict] = []
|
| 161 |
|
| 162 |
from lilith_agent.config import Config
|
| 163 |
-
from lilith_agent.models import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
cfg = Config.from_env()
|
| 165 |
cheap_model = get_cheap_model(cfg)
|
| 166 |
|
|
@@ -231,6 +237,22 @@ def run_agent_on_questions(graph: Any, questions: list[dict], checkpoint_dir: st
|
|
| 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(
|
|
|
|
| 160 |
answers: list[dict] = []
|
| 161 |
|
| 162 |
from lilith_agent.config import Config
|
| 163 |
+
from lilith_agent.models import (
|
| 164 |
+
BatchAbortRateLimitError,
|
| 165 |
+
QuestionRateLimitStreakError,
|
| 166 |
+
RateLimitCooldownError,
|
| 167 |
+
get_cheap_model,
|
| 168 |
+
rate_limit_question_scope,
|
| 169 |
+
)
|
| 170 |
cfg = Config.from_env()
|
| 171 |
cheap_model = get_cheap_model(cfg)
|
| 172 |
|
|
|
|
| 237 |
log_runner.warning("[runner] task=%s rate limited after retry: %s", task_id, exc)
|
| 238 |
answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
|
| 239 |
continue
|
| 240 |
+
except QuestionRateLimitStreakError as exc:
|
| 241 |
+
log_runner.warning("[runner] task=%s rate limit streak: %s", task_id, exc)
|
| 242 |
+
answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
|
| 243 |
+
continue
|
| 244 |
+
except BatchAbortRateLimitError as exc:
|
| 245 |
+
log_runner.warning("[runner] task=%s batch abort rate limit: %s", task_id, exc)
|
| 246 |
+
answers.append({"task_id": task_id, "submitted_answer": "AGENT ERROR: RATE LIMITED"})
|
| 247 |
+
_write_checkpoint_atomic(
|
| 248 |
+
checkpoint_root / "rate_limit_abort.json",
|
| 249 |
+
{
|
| 250 |
+
"task_id": task_id,
|
| 251 |
+
"reason": exc.reason,
|
| 252 |
+
"original_error": exc.original_error,
|
| 253 |
+
},
|
| 254 |
+
)
|
| 255 |
+
return answers
|
| 256 |
except Exception as exc:
|
| 257 |
log_runner.warning("[runner] task=%s agent error: %s", task_id, exc)
|
| 258 |
answers.append(
|
tests/test_runner.py
CHANGED
|
@@ -8,7 +8,7 @@ import pytest
|
|
| 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 |
|
|
@@ -183,3 +183,47 @@ def test_runner_uses_fresh_ephemeral_memory_for_retry(monkeypatch, tmp_path: Pat
|
|
| 183 |
)
|
| 184 |
|
| 185 |
assert events == ["enter", "exit", "enter", "exit"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from langchain_core.messages import AIMessage
|
| 9 |
|
| 10 |
from lilith_agent.config import Config
|
| 11 |
+
from lilith_agent.models import BatchAbortRateLimitError, QuestionRateLimitStreakError, RateLimitCooldownError
|
| 12 |
from lilith_agent.runner import run_agent_on_questions, _wrap_user_question, _write_checkpoint_atomic
|
| 13 |
|
| 14 |
|
|
|
|
| 183 |
)
|
| 184 |
|
| 185 |
assert events == ["enter", "exit", "enter", "exit"]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class _GraphQuestionStreak:
|
| 189 |
+
def invoke(self, state, config):
|
| 190 |
+
raise QuestionRateLimitStreakError(count=50)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class _GraphBatchAbort:
|
| 194 |
+
def invoke(self, state, config):
|
| 195 |
+
raise BatchAbortRateLimitError(reason="daily quota exhausted", original_error="429")
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def test_runner_skips_question_on_rate_limit_streak(tmp_path: Path):
|
| 199 |
+
answers = run_agent_on_questions(
|
| 200 |
+
_GraphQuestionStreak(),
|
| 201 |
+
[
|
| 202 |
+
{"task_id": "task-streak", "question": "first"},
|
| 203 |
+
{"task_id": "task-next", "question": "second"},
|
| 204 |
+
],
|
| 205 |
+
tmp_path,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
assert answers[0] == {"task_id": "task-streak", "submitted_answer": "AGENT ERROR: RATE LIMITED"}
|
| 209 |
+
assert not (tmp_path / "task-streak.json").exists()
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def test_runner_stops_batch_and_writes_abort_marker_on_daily_quota(tmp_path: Path):
|
| 213 |
+
answers = run_agent_on_questions(
|
| 214 |
+
_GraphBatchAbort(),
|
| 215 |
+
[
|
| 216 |
+
{"task_id": "task-abort", "question": "first"},
|
| 217 |
+
{"task_id": "task-never", "question": "second"},
|
| 218 |
+
],
|
| 219 |
+
tmp_path,
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
assert answers == [{"task_id": "task-abort", "submitted_answer": "AGENT ERROR: RATE LIMITED"}]
|
| 223 |
+
marker = tmp_path / "rate_limit_abort.json"
|
| 224 |
+
assert marker.exists()
|
| 225 |
+
data = json.loads(marker.read_text())
|
| 226 |
+
assert data["task_id"] == "task-abort"
|
| 227 |
+
assert data["reason"] == "daily quota exhausted"
|
| 228 |
+
assert not (tmp_path / "task-abort.json").exists()
|
| 229 |
+
assert not (tmp_path / "task-never.json").exists()
|