Spaces:
Sleeping
Sleeping
Use smolagents' native RPM/retry controls, cap steps, trim memory
Browse filesCustom step_callback throttling couldn't see smolagents' own internal retries on rate-limit errors. Switch to LiteLLMModel's native requests_per_minute=25 and retry=False. Cap max_steps=7 and add MemoryTrimmer to stop unbounded context growth (the real driver of TPM failures). Prioritizes speed - a failed question is skipped, not retried.
app.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 1 |
import os
|
| 2 |
import tempfile
|
| 3 |
-
import time
|
| 4 |
-
from collections import deque
|
| 5 |
import gradio as gr
|
| 6 |
import requests
|
| 7 |
import inspect
|
|
@@ -38,50 +36,30 @@ Your final answer should be a number OR as few words as possible OR a comma sepa
|
|
| 38 |
If you are asked for a number, don't use commas to write it, and don't use units such as $ or % unless specified otherwise.
|
| 39 |
If you are asked for a string, don't use articles or abbreviations (e.g. for cities), and write digits in plain text unless specified otherwise.
|
| 40 |
If you are asked for a comma separated list, apply the above rules to each element depending on whether it's a number or a string.
|
|
|
|
| 41 |
"""
|
| 42 |
|
| 43 |
-
class
|
| 44 |
"""
|
| 45 |
-
Step_callback that
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
"""
|
| 54 |
-
def __init__(self,
|
| 55 |
-
self.
|
| 56 |
-
self.
|
| 57 |
-
self._last_call_at: float | None = None
|
| 58 |
-
self._usage_window: deque[tuple[float, int]] = deque()
|
| 59 |
|
| 60 |
def __call__(self, memory_step: ActionStep, agent: CodeAgent) -> None:
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
if wait > 0:
|
| 67 |
-
time.sleep(wait)
|
| 68 |
-
now = time.monotonic()
|
| 69 |
-
|
| 70 |
-
# TPM guard, based on actual usage from the step that just completed
|
| 71 |
-
usage = getattr(memory_step, "token_usage", None)
|
| 72 |
-
tokens = (usage.input_tokens + usage.output_tokens) if usage else 0
|
| 73 |
-
self._usage_window.append((now, tokens))
|
| 74 |
-
cutoff = now - 60
|
| 75 |
-
while self._usage_window and self._usage_window[0][0] < cutoff:
|
| 76 |
-
self._usage_window.popleft()
|
| 77 |
-
window_tokens = sum(t for _, t in self._usage_window)
|
| 78 |
-
if window_tokens > self.tokens_per_minute_budget and self._usage_window:
|
| 79 |
-
wait = 60 - (now - self._usage_window[0][0]) + 0.5
|
| 80 |
-
if wait > 0:
|
| 81 |
-
print(f"Rate limiter: {window_tokens} tokens in the last 60s (budget {self.tokens_per_minute_budget}), sleeping {wait:.1f}s")
|
| 82 |
-
time.sleep(wait)
|
| 83 |
-
|
| 84 |
-
self._last_call_at = time.monotonic()
|
| 85 |
|
| 86 |
|
| 87 |
# --- Basic Agent Definition ---
|
|
@@ -93,12 +71,27 @@ class BasicAgent:
|
|
| 93 |
if not api_key:
|
| 94 |
print("Warning: GROQ_API_KEY is not set - the agent will fail to call the model.")
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
self.agent = CodeAgent(
|
| 98 |
model=self.model,
|
| 99 |
tools=[
|
| 100 |
WebSearchTool(),
|
| 101 |
-
VisitWebpageTool(max_output_length=
|
| 102 |
WikipediaSearchTool(content_type="summary"), # "text" (default) returns the full article
|
| 103 |
],
|
| 104 |
add_base_tools=True, # adds DuckDuckGo search + Whisper audio transcriber
|
|
@@ -106,11 +99,8 @@ class BasicAgent:
|
|
| 106 |
"pandas", "numpy", "math", "re", "json", "itertools",
|
| 107 |
"collections", "statistics", "datetime", "io", "openpyxl", "PIL",
|
| 108 |
],
|
| 109 |
-
max_steps=
|
| 110 |
-
step_callbacks=[
|
| 111 |
-
min_seconds_between_calls=float(os.getenv("RATE_LIMIT_SECONDS_BETWEEN_CALLS", "2.5")),
|
| 112 |
-
tokens_per_minute_budget=int(os.getenv("RATE_LIMIT_TOKENS_PER_MINUTE", "9000")),
|
| 113 |
-
)],
|
| 114 |
)
|
| 115 |
print("BasicAgent initialized.")
|
| 116 |
|
|
|
|
| 1 |
import os
|
| 2 |
import tempfile
|
|
|
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
import requests
|
| 5 |
import inspect
|
|
|
|
| 36 |
If you are asked for a number, don't use commas to write it, and don't use units such as $ or % unless specified otherwise.
|
| 37 |
If you are asked for a string, don't use articles or abbreviations (e.g. for cities), and write digits in plain text unless specified otherwise.
|
| 38 |
If you are asked for a comma separated list, apply the above rules to each element depending on whether it's a number or a string.
|
| 39 |
+
Work efficiently: if a search or lookup doesn't find what you need after 1-2 tries, try a meaningfully different approach rather than repeating similar queries, and give your best-guess final_answer rather than exhausting all steps.
|
| 40 |
"""
|
| 41 |
|
| 42 |
+
class MemoryTrimmer:
|
| 43 |
"""
|
| 44 |
+
Step_callback that collapses old tool outputs in the agent's memory.
|
| 45 |
+
CodeAgent re-sends the *entire* step history on every call, so input
|
| 46 |
+
tokens grow every single step (2k -> 5k -> 8k -> ... -> 20k+ by step 6),
|
| 47 |
+
which both wrecks the TPM budget and slows every later step far more
|
| 48 |
+
than it needs to. Keeps the most recent `keep_recent` steps' tool
|
| 49 |
+
outputs intact (the agent still needs that detail) and truncates older
|
| 50 |
+
ones to a short placeholder, keeping per-call token cost roughly flat
|
| 51 |
+
across a run instead of growing unbounded.
|
| 52 |
"""
|
| 53 |
+
def __init__(self, keep_recent: int = 2, max_old_observation_chars: int = 300):
|
| 54 |
+
self.keep_recent = keep_recent
|
| 55 |
+
self.max_old_observation_chars = max_old_observation_chars
|
|
|
|
|
|
|
| 56 |
|
| 57 |
def __call__(self, memory_step: ActionStep, agent: CodeAgent) -> None:
|
| 58 |
+
action_steps = [step for step in agent.memory.steps if isinstance(step, ActionStep)]
|
| 59 |
+
for step in action_steps[:-self.keep_recent]:
|
| 60 |
+
observations = getattr(step, "observations", None)
|
| 61 |
+
if observations and len(observations) > self.max_old_observation_chars:
|
| 62 |
+
step.observations = observations[:self.max_old_observation_chars] + " [...older output truncated to save context]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
# --- Basic Agent Definition ---
|
|
|
|
| 71 |
if not api_key:
|
| 72 |
print("Warning: GROQ_API_KEY is not set - the agent will fail to call the model.")
|
| 73 |
|
| 74 |
+
# requests_per_minute: smolagents' own ApiModel throttles calls at this
|
| 75 |
+
# rate before ever hitting the API (a bit under Groq's 30 RPM cap).
|
| 76 |
+
# retry=False: smolagents' default retries a failed call several
|
| 77 |
+
# times internally (invisible to us - confirmed via Groq's dashboard,
|
| 78 |
+
# 429s with sub-second gaps between requests). Only ~30% of GAIA
|
| 79 |
+
# questions need to succeed here, so a call that still gets rate
|
| 80 |
+
# limited (e.g. on tokens/minute, which this doesn't track) should
|
| 81 |
+
# just fail fast - the per-question try/except below skips it and
|
| 82 |
+
# moves on, which is much cheaper than retry-storming.
|
| 83 |
+
self.model = LiteLLMModel(
|
| 84 |
+
model_id=model_id,
|
| 85 |
+
api_key=api_key,
|
| 86 |
+
temperature=0,
|
| 87 |
+
requests_per_minute=float(os.getenv("RATE_LIMIT_RPM", "25")),
|
| 88 |
+
retry=False,
|
| 89 |
+
)
|
| 90 |
self.agent = CodeAgent(
|
| 91 |
model=self.model,
|
| 92 |
tools=[
|
| 93 |
WebSearchTool(),
|
| 94 |
+
VisitWebpageTool(max_output_length=3000), # default 40000 chars blows the TPM budget in one call
|
| 95 |
WikipediaSearchTool(content_type="summary"), # "text" (default) returns the full article
|
| 96 |
],
|
| 97 |
add_base_tools=True, # adds DuckDuckGo search + Whisper audio transcriber
|
|
|
|
| 99 |
"pandas", "numpy", "math", "re", "json", "itertools",
|
| 100 |
"collections", "statistics", "datetime", "io", "openpyxl", "PIL",
|
| 101 |
],
|
| 102 |
+
max_steps=7, # keep runs short: growing history otherwise blows the TPM budget by step ~6
|
| 103 |
+
step_callbacks=[MemoryTrimmer()],
|
|
|
|
|
|
|
|
|
|
| 104 |
)
|
| 105 |
print("BasicAgent initialized.")
|
| 106 |
|