179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
"""
|
|
Web Research Analyst — busca Google CSE em sites DeFi curados.
|
|
|
|
Estratégia de queries por categoria:
|
|
- security: honeypot.is, tokensniffer
|
|
- news: coindesk, theblock, cryptopanic, bloomberg, reuters
|
|
- analytics: defillama, dune, geckoterminal, dexscreener, bubblemaps
|
|
- sentiment: lunarcrush, cryptopanic
|
|
- markets: polymarket, hyperliquid, coinglass
|
|
- governance: snapshot.org, tally.xyz
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
from tradingagents.dataflows.google_search_tools import (
|
|
GoogleSearchClient,
|
|
SearchResult,
|
|
QuotaExceededError,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Site groups for focused queries
|
|
SITE_GROUPS: dict[str, list[str]] = {
|
|
"security": ["honeypot.is", "tokensniffer.com"],
|
|
"news": [
|
|
"coindesk.com",
|
|
"theblock.co",
|
|
"cryptopanic.com",
|
|
"bloomberg.com",
|
|
"reuters.com",
|
|
],
|
|
"analytics": [
|
|
"defillama.com",
|
|
"dune.com",
|
|
"geckoterminal.com",
|
|
"dexscreener.com",
|
|
"bubblemaps.io",
|
|
"birdeye.so",
|
|
],
|
|
"sentiment": ["lunarcrush.com", "cryptopanic.com"],
|
|
"markets": [
|
|
"polymarket.com",
|
|
"hyperliquid.xyz",
|
|
"coinglass.com",
|
|
"tradingview.com",
|
|
],
|
|
"governance": ["snapshot.org", "tally.xyz"],
|
|
}
|
|
|
|
|
|
def _build_site_filter(sites: list[str]) -> str:
|
|
return " OR ".join(f"site:{s}" for s in sites)
|
|
|
|
|
|
@dataclass
|
|
class ResearchReport:
|
|
token_name: str
|
|
security_findings: list[SearchResult]
|
|
news_findings: list[SearchResult]
|
|
analytics_findings: list[SearchResult]
|
|
sentiment_findings: list[SearchResult]
|
|
quota_status: dict
|
|
|
|
def to_text(self) -> str:
|
|
"""Returns a text summary consumable by an LLM agent."""
|
|
sections = [f"## Web Research Report: {self.token_name}\n"]
|
|
|
|
def _fmt(label: str, results: list[SearchResult]) -> str:
|
|
if not results:
|
|
return f"### {label}\nNenhum resultado encontrado.\n"
|
|
lines = [f"### {label}"]
|
|
for r in results:
|
|
lines.append(f"- **{r.title}**\n {r.snippet}\n {r.link}")
|
|
return "\n".join(lines)
|
|
|
|
sections.append(_fmt("🔒 Segurança", self.security_findings))
|
|
sections.append(_fmt("📰 Notícias", self.news_findings))
|
|
sections.append(_fmt("📊 Analytics On-Chain", self.analytics_findings))
|
|
sections.append(_fmt("💬 Sentimento", self.sentiment_findings))
|
|
sections.append(
|
|
f"### 📈 Quota Google Search\n"
|
|
f"Uso hoje: {self.quota_status['usage_today']}/{self.quota_status['daily_limit']} "
|
|
f"({'⚠️ próximo ao limite' if self.quota_status['is_near_limit'] else '✅ ok'})"
|
|
)
|
|
return "\n\n".join(sections)
|
|
|
|
|
|
class WebResearchAnalyst:
|
|
"""
|
|
Analista que pesquisa em sites DeFi curados via Google Custom Search.
|
|
|
|
Usa uma estratégia consciente de quota:
|
|
- Agrupa queries por categoria para maximizar informação por query
|
|
- Respeita o limite diário configurado (padrão: 95/100 free tier)
|
|
- Aborta graciosamente se quota esgotada (não lança exceção — retorna parcial)
|
|
"""
|
|
|
|
def __init__(self, search_client: GoogleSearchClient | None = None) -> None:
|
|
if search_client is None:
|
|
api_key = os.environ["GOOGLE_SEARCH_API_KEY"]
|
|
cx = os.environ["GOOGLE_SEARCH_ENGINE_ID"]
|
|
daily_limit = int(os.environ.get("GOOGLE_SEARCH_DAILY_LIMIT", "95"))
|
|
search_client = GoogleSearchClient(
|
|
api_key=api_key, cx=cx, daily_limit=daily_limit
|
|
)
|
|
self.client = search_client
|
|
|
|
async def research_token(
|
|
self, token_name: str, token_address: str | None = None
|
|
) -> ResearchReport:
|
|
"""
|
|
Pesquisa completa sobre um token em sites DeFi curados.
|
|
|
|
Usa no máximo 4 queries (uma por categoria principal):
|
|
security, news, analytics, sentiment.
|
|
|
|
Args:
|
|
token_name: Nome legível do token (ex: "Solana", "BONK")
|
|
token_address: Endereço do contrato (usado em queries de segurança)
|
|
|
|
Returns:
|
|
ResearchReport com findings por categoria e status da quota
|
|
"""
|
|
security: list[SearchResult] = []
|
|
news: list[SearchResult] = []
|
|
analytics: list[SearchResult] = []
|
|
sentiment: list[SearchResult] = []
|
|
|
|
search_subject = token_address if token_address else token_name
|
|
|
|
# Each block catches QuotaExceededError gracefully — returns partial results
|
|
try:
|
|
security = await self.client.search(
|
|
f"{search_subject} {token_name} security risk",
|
|
num=3,
|
|
siteSearch=_build_site_filter(SITE_GROUPS["security"]),
|
|
)
|
|
except QuotaExceededError as e:
|
|
logger.warning("Quota exceeded before security search: %s", e)
|
|
|
|
try:
|
|
news = await self.client.search(
|
|
f"{token_name} crypto news",
|
|
num=5,
|
|
siteSearch=_build_site_filter(SITE_GROUPS["news"]),
|
|
)
|
|
except QuotaExceededError as e:
|
|
logger.warning("Quota exceeded before news search: %s", e)
|
|
|
|
try:
|
|
analytics = await self.client.search(
|
|
f"{token_name} DEX liquidity TVL analysis",
|
|
num=4,
|
|
siteSearch=_build_site_filter(SITE_GROUPS["analytics"]),
|
|
)
|
|
except QuotaExceededError as e:
|
|
logger.warning("Quota exceeded before analytics search: %s", e)
|
|
|
|
try:
|
|
sentiment = await self.client.search(
|
|
f"{token_name} sentiment community",
|
|
num=3,
|
|
siteSearch=_build_site_filter(SITE_GROUPS["sentiment"]),
|
|
)
|
|
except QuotaExceededError as e:
|
|
logger.warning("Quota exceeded before sentiment search: %s", e)
|
|
|
|
return ResearchReport(
|
|
token_name=token_name,
|
|
security_findings=security,
|
|
news_findings=news,
|
|
analytics_findings=analytics,
|
|
sentiment_findings=sentiment,
|
|
quota_status=self.client.quota_status,
|
|
)
|