maodd commited on
Commit
8be6e91
·
verified ·
1 Parent(s): eebc9ca

Make rate limiter token-aware; trim verbose tool outputs

Browse files

Groq's 429s were a TPM (12k/min) limit, not RPM. RateLimiter now tracks actual token usage per step in a trailing 60s window; VisitWebpageTool and WikipediaSearchTool outputs trimmed to reduce per-call token volume.

Files changed (1) hide show
  1. app.py +39 -11
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import tempfile
3
  import time
 
4
  import gradio as gr
5
  import requests
6
  import inspect
@@ -41,25 +42,45 @@ If you are asked for a comma separated list, apply the above rules to each eleme
41
 
42
  class RateLimiter:
43
  """
44
- Enforces a minimum delay between successive agent LLM calls (one call per
45
- step) as a step_callback, to stay under the model provider's
46
- requests-per-minute limit. Groq's free tier for llama-3.3-70b-versatile
47
- caps at 30 RPM, so the default here (2.5s) targets that with a small
48
- margin; override via RATE_LIMIT_SECONDS_BETWEEN_CALLS for other tiers.
49
- Note this only protects against per-minute limits - free tiers also
50
- often cap total requests/day, which this can't work around.
 
51
  """
52
- def __init__(self, min_seconds_between_calls: float = 2.5):
53
  self.min_seconds_between_calls = min_seconds_between_calls
 
54
  self._last_call_at: float | None = None
 
55
 
56
  def __call__(self, memory_step: ActionStep, agent: CodeAgent) -> None:
57
  now = time.monotonic()
 
 
58
  if self._last_call_at is not None:
59
  wait = self.min_seconds_between_calls - (now - self._last_call_at)
60
  if wait > 0:
61
- print(f"Rate limiter: sleeping {wait:.1f}s to stay under the RPM limit")
62
  time.sleep(wait)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  self._last_call_at = time.monotonic()
64
 
65
 
@@ -75,14 +96,21 @@ class BasicAgent:
75
  self.model = LiteLLMModel(model_id=model_id, api_key=api_key, temperature=0)
76
  self.agent = CodeAgent(
77
  model=self.model,
78
- tools=[WebSearchTool(), VisitWebpageTool(), WikipediaSearchTool()],
 
 
 
 
79
  add_base_tools=True, # adds DuckDuckGo search + Whisper audio transcriber
80
  additional_authorized_imports=[
81
  "pandas", "numpy", "math", "re", "json", "itertools",
82
  "collections", "statistics", "datetime", "io", "openpyxl", "PIL",
83
  ],
84
  max_steps=12,
85
- step_callbacks=[RateLimiter(float(os.getenv("RATE_LIMIT_SECONDS_BETWEEN_CALLS", "2.5")))],
 
 
 
86
  )
87
  print("BasicAgent initialized.")
88
 
 
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
 
42
 
43
  class RateLimiter:
44
  """
45
+ Step_callback that throttles both request frequency (RPM) and actual
46
+ token volume (TPM) against the model provider's free-tier limits. Groq's
47
+ free tier for llama-3.3-70b-versatile caps at 30 RPM and 12,000 TPM - and
48
+ TPM is the tighter constraint here, since a CodeAgent's context (tool
49
+ outputs, growing conversation history) can easily run several thousand
50
+ tokens per call. Tracks actual usage reported per step in a trailing
51
+ 60s window and sleeps as needed to stay under a token budget (with
52
+ margin below the real cap).
53
  """
54
+ def __init__(self, min_seconds_between_calls: float = 2.5, tokens_per_minute_budget: int = 9000):
55
  self.min_seconds_between_calls = min_seconds_between_calls
56
+ self.tokens_per_minute_budget = tokens_per_minute_budget
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
  now = time.monotonic()
62
+
63
+ # RPM guard
64
  if self._last_call_at is not None:
65
  wait = self.min_seconds_between_calls - (now - self._last_call_at)
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
 
 
96
  self.model = LiteLLMModel(model_id=model_id, api_key=api_key, temperature=0)
97
  self.agent = CodeAgent(
98
  model=self.model,
99
+ tools=[
100
+ WebSearchTool(),
101
+ VisitWebpageTool(max_output_length=4000), # default 40000 chars blows the TPM budget in one call
102
+ WikipediaSearchTool(content_type="summary"), # "text" (default) returns the full article
103
+ ],
104
  add_base_tools=True, # adds DuckDuckGo search + Whisper audio transcriber
105
  additional_authorized_imports=[
106
  "pandas", "numpy", "math", "re", "json", "itertools",
107
  "collections", "statistics", "datetime", "io", "openpyxl", "PIL",
108
  ],
109
  max_steps=12,
110
+ step_callbacks=[RateLimiter(
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