PrimoGreedy-Agent / src /core /search.py
CiscsoPonce's picture
fix: Brave 429 rate limiting + garbage ticker extraction
19d2204
import os
import random
import time
import requests
from .logger import get_logger
logger = get_logger(__name__)
_BRAVE_URL = "https://api.search.brave.com/res/v1/web/search"
_MAX_RETRIES = 3
def _brave_request(
params: dict,
api_key: str,
accept: str = "application/json",
) -> requests.Response | None:
"""Send a Brave Search request with retry + exponential backoff on 429.
A small random jitter (0-2 s) is added before the first attempt to
spread out concurrent requests from parallel region fan-out.
"""
headers = {
"Accept": accept,
"X-Subscription-Token": api_key,
}
time.sleep(random.uniform(0, 2.0))
for attempt in range(_MAX_RETRIES):
try:
resp = requests.get(
_BRAVE_URL, headers=headers, params=params, timeout=15,
)
if resp.status_code == 429:
wait = (2 ** (attempt + 1)) + random.uniform(0, 1)
logger.info(
"Brave 429 (attempt %d/%d), retrying in %.1fs…",
attempt + 1, _MAX_RETRIES, wait,
)
time.sleep(wait)
continue
resp.raise_for_status()
return resp
except requests.exceptions.HTTPError:
raise
except requests.exceptions.RequestException as exc:
if attempt < _MAX_RETRIES - 1:
wait = (2 ** (attempt + 1)) + random.uniform(0, 1)
logger.info("Brave network error, retrying in %.1fs: %s", wait, exc)
time.sleep(wait)
else:
raise
return None
def brave_search(query: str, count: int = 5, freshness: str = "") -> str:
"""Single implementation of Brave Search used across the whole project.
Args:
query: The search query string.
count: Number of results to request (max 20).
freshness: Optional freshness filter (e.g. ``"pw"`` for past week,
``"pm"`` for past month).
Returns:
A newline-joined string of ``"- title: description"`` lines, or an
error message.
"""
api_key = os.getenv("BRAVE_API_KEY")
if not api_key:
logger.warning("BRAVE_API_KEY not set – search skipped")
return "No Brave API key found."
params: dict = {"q": query, "count": count}
if freshness:
params["freshness"] = freshness
try:
resp = _brave_request(params, api_key)
if resp is None:
logger.warning("Brave rate-limit exhausted for query: %s", query)
return "Rate limit hit – try again shortly."
results = resp.json().get("web", {}).get("results", [])
if not results:
logger.info("Brave returned 0 results for: %s", query)
return "No results found."
return "\n".join(
f"- {r.get('title', '')}: {r.get('description', '')}"
for r in results
)
except requests.exceptions.HTTPError as exc:
if exc.response is not None and exc.response.status_code == 429:
logger.warning("Brave rate-limit hit for query: %s", query)
return "Rate limit hit – try again shortly."
logger.error("Brave HTTP error: %s", exc)
return f"Search HTTP error: {exc}"
except requests.exceptions.RequestException as exc:
logger.error("Brave network error: %s", exc)
return f"Search error: {exc}"
def brave_search_raw(query: str, count: int = 10, freshness: str = "pm") -> str:
"""Return the raw combined text of titles + descriptions.
Useful for regex-based ticker extraction in niche_hunter / scanner.
"""
api_key = os.getenv("BRAVE_API_KEY")
if not api_key:
logger.warning("BRAVE_API_KEY not set – raw search skipped")
return ""
params: dict = {"q": query, "count": count}
if freshness:
params["freshness"] = freshness
try:
resp = _brave_request(params, api_key)
if resp is None:
logger.warning("Brave rate-limit exhausted for raw query")
return ""
results = resp.json().get("web", {}).get("results", [])
return " ".join(f"{r['title']} {r['description']}" for r in results)
except requests.exceptions.RequestException as exc:
logger.error("Brave raw-search error: %s", exc)
return ""