TomLii commited on
Commit
3fd8fc1
·
1 Parent(s): 34518d3

Make DuckDuckGo rate-limit non-fatal so the agent can still answer

Browse files

Before this change, a 202 Ratelimit from html.duckduckgo.com (common
when many HF Spaces share the same outbound IP) propagated all the way
up to run_ui and replaced the result with a raw "Error: ...
202 Ratelimit" string — no table, no answer, nothing.

Catch all exceptions in _run_search_single, retry once with a short
backoff to ride out transient 202s, and on persistent failure return a
structured tool error with an "answer from your own knowledge" hint so
the agent loop can still produce a final <answer> for questions the
model already knows (e.g. iPhone 15 vs 16 comparisons).

Made-with: Cursor

Files changed (1) hide show
  1. app.py +52 -13
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import json
2
  import os
3
  import re
 
4
  from dataclasses import dataclass, field
5
  from datetime import date
6
  from pathlib import Path
@@ -950,26 +951,64 @@ def parse_tool_call(text: str) -> Tuple[Optional[str], Optional[Dict[str, Any]],
950
  return name, arguments, None
951
 
952
 
 
 
 
 
 
 
 
 
 
 
953
  def _run_search_single(query: str, max_results: int) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
954
  if not query.strip():
955
  return {"ok": False, "error": "Search query cannot be empty."}
956
  cache_key = f"{query.strip().lower()}::{max_results}"
957
  if cache_key in SEARCH_CACHE:
958
  return {**SEARCH_CACHE[cache_key], "cached": True}
959
 
960
- rows: List[Dict[str, str]] = []
961
- with DDGS() as ddgs:
962
- for item in ddgs.text(query, max_results=max_results):
963
- rows.append(
964
- {
965
- "title": item.get("title", ""),
966
- "href": item.get("href", ""),
967
- "body": item.get("body", ""),
968
- }
969
- )
970
- payload = {"ok": True, "query": query, "results": rows, "cached": False}
971
- SEARCH_CACHE[cache_key] = payload
972
- return payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
 
974
 
975
  def run_search(query: Union[str, List[str]], max_results: int = 5) -> Dict[str, Any]:
 
1
  import json
2
  import os
3
  import re
4
+ import time
5
  from dataclasses import dataclass, field
6
  from datetime import date
7
  from pathlib import Path
 
951
  return name, arguments, None
952
 
953
 
954
+ _SEARCH_UNAVAILABLE_HINT = (
955
+ "The web-search backend is currently rate-limited or unreachable. "
956
+ "If this question can be answered confidently from your own training "
957
+ "knowledge (e.g. common product specs, historical facts, definitions), "
958
+ "please produce your best answer now inside <answer>...</answer>, and "
959
+ "mention any value that might be out of date. Only ask the user to "
960
+ "retry later if the question truly requires a fresh web lookup."
961
+ )
962
+
963
+
964
  def _run_search_single(query: str, max_results: int) -> Dict[str, Any]:
965
+ """Run one DuckDuckGo query.
966
+
967
+ Returns a structured dict on both success and failure, never raises. If
968
+ the search backend rate-limits us (Space IPs share outbound NAT and
969
+ often trip DuckDuckGo's anti-scraping throttle), we return an
970
+ `ok: False` payload with a hint that lets the agent fall back to its
971
+ own knowledge instead of aborting the whole research run.
972
+ """
973
  if not query.strip():
974
  return {"ok": False, "error": "Search query cannot be empty."}
975
  cache_key = f"{query.strip().lower()}::{max_results}"
976
  if cache_key in SEARCH_CACHE:
977
  return {**SEARCH_CACHE[cache_key], "cached": True}
978
 
979
+ last_exc: Optional[BaseException] = None
980
+ for attempt in range(2):
981
+ try:
982
+ rows: List[Dict[str, str]] = []
983
+ with DDGS() as ddgs:
984
+ for item in ddgs.text(query, max_results=max_results):
985
+ rows.append(
986
+ {
987
+ "title": item.get("title", ""),
988
+ "href": item.get("href", ""),
989
+ "body": item.get("body", ""),
990
+ }
991
+ )
992
+ payload = {"ok": True, "query": query, "results": rows, "cached": False}
993
+ SEARCH_CACHE[cache_key] = payload
994
+ return payload
995
+ except Exception as exc:
996
+ last_exc = exc
997
+ # One retry with a small backoff covers most transient 202
998
+ # Ratelimit / transient network hiccups; on the second failure
999
+ # we give up and return a graceful error to the agent.
1000
+ if attempt == 0:
1001
+ time.sleep(1.5)
1002
+ continue
1003
+
1004
+ err = f"{type(last_exc).__name__}: {last_exc}" if last_exc else "unknown error"
1005
+ return {
1006
+ "ok": False,
1007
+ "query": query,
1008
+ "error": f"Search backend unavailable ({err}).",
1009
+ "results": [],
1010
+ "hint": _SEARCH_UNAVAILABLE_HINT,
1011
+ }
1012
 
1013
 
1014
  def run_search(query: Union[str, List[str]], max_results: int = 5) -> Dict[str, Any]: