Spaces:
Running
Running
Priyanshi Saxena
commited on
Commit
Β·
b091b2c
1
Parent(s):
18efaca
feat: Replace CoinGecko with CryptoCompare/Etherscan APIs
Browse files- Updated tool analysis prompts to prioritize available APIs (CryptoCompare, Etherscan)
- Removed CoinGecko from suggested tools since no API key available
- Modified ChartDataTool to use CryptoCompare instead of CoinGecko for price data
- Added conditional CoinGecko tool initialization (only if API key present)
- Updated market overview to use CryptoCompare data source
- Enhanced tool selection logic to exclude unavailable APIs
- Added comprehensive testing for tool selection functionality
- src/agent/research_agent.py +22 -16
- src/tools/chart_data_tool.py +57 -78
- test_chart_tool.py +49 -0
- test_tool_selection.py +69 -0
src/agent/research_agent.py
CHANGED
@@ -80,11 +80,15 @@ class Web3ResearchAgent:
|
|
80 |
def _initialize_tools(self):
|
81 |
tools = []
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
|
|
|
|
|
|
|
|
88 |
|
89 |
try:
|
90 |
tools.append(DeFiLlamaTool())
|
@@ -214,12 +218,14 @@ class Web3ResearchAgent:
|
|
214 |
tool_analysis_prompt = f"""Analyze this query and determine which tools would be helpful:
|
215 |
Query: "{query}"
|
216 |
|
217 |
-
Available tools:
|
218 |
-
- cryptocompare_data: Real-time crypto prices and market data
|
219 |
-
-
|
220 |
-
-
|
221 |
- chart_data_provider: Generate chart data for visualizations
|
222 |
|
|
|
|
|
223 |
Respond with just the tool names that should be used, separated by commas.
|
224 |
If charts/visualizations are mentioned, include chart_data_provider.
|
225 |
Examples:
|
@@ -390,19 +396,21 @@ The system successfully gathered data from {len(suggested_tools)} tools:
|
|
390 |
|
391 |
Query: "{query}"
|
392 |
|
393 |
-
Available tools:
|
394 |
-
- cryptocompare_data:
|
395 |
-
-
|
396 |
- defillama_data: DeFi protocols, TVL, and yield farming data
|
397 |
-
- etherscan_data: Ethereum blockchain data and transactions
|
398 |
- chart_data_provider: Generate chart data for visualizations
|
399 |
|
|
|
|
|
400 |
If charts/visualizations are mentioned, include chart_data_provider.
|
401 |
|
402 |
Examples:
|
403 |
- "Bitcoin price" β cryptocompare_data, chart_data_provider
|
404 |
- "DeFi TVL" β defillama_data, chart_data_provider
|
405 |
- "Ethereum transactions" β etherscan_data
|
|
|
406 |
|
407 |
Respond with only the tool names, comma-separated (no explanations)."""
|
408 |
|
@@ -413,7 +421,7 @@ Respond with only the tool names, comma-separated (no explanations)."""
|
|
413 |
# Parse suggested tools
|
414 |
suggested_tools = [tool.strip() for tool in str(tool_response).split(',') if tool.strip()]
|
415 |
suggested_tools = [tool for tool in suggested_tools if tool in {
|
416 |
-
'cryptocompare_data', '
|
417 |
'etherscan_data', 'chart_data_provider'
|
418 |
}]
|
419 |
|
@@ -422,8 +430,6 @@ Respond with only the tool names, comma-separated (no explanations)."""
|
|
422 |
response_text = str(tool_response).lower()
|
423 |
if 'cryptocompare' in response_text:
|
424 |
suggested_tools.append('cryptocompare_data')
|
425 |
-
if 'coingecko' in response_text:
|
426 |
-
suggested_tools.append('coingecko_data')
|
427 |
if 'defillama' in response_text:
|
428 |
suggested_tools.append('defillama_data')
|
429 |
if 'etherscan' in response_text:
|
|
|
80 |
def _initialize_tools(self):
|
81 |
tools = []
|
82 |
|
83 |
+
# Skip CoinGecko if no API key available
|
84 |
+
if config.COINGECKO_API_KEY:
|
85 |
+
try:
|
86 |
+
tools.append(CoinGeckoTool())
|
87 |
+
logger.info("CoinGecko tool initialized")
|
88 |
+
except Exception as e:
|
89 |
+
logger.warning(f"CoinGecko tool failed: {e}")
|
90 |
+
else:
|
91 |
+
logger.info("CoinGecko tool skipped - no API key available")
|
92 |
|
93 |
try:
|
94 |
tools.append(DeFiLlamaTool())
|
|
|
218 |
tool_analysis_prompt = f"""Analyze this query and determine which tools would be helpful:
|
219 |
Query: "{query}"
|
220 |
|
221 |
+
Available tools (prioritized by functionality):
|
222 |
+
- cryptocompare_data: Real-time crypto prices and market data (PREFERRED for prices)
|
223 |
+
- etherscan_data: Ethereum blockchain data, gas fees, transactions (PREFERRED for Ethereum)
|
224 |
+
- defillama_data: DeFi protocol TVL and yield data
|
225 |
- chart_data_provider: Generate chart data for visualizations
|
226 |
|
227 |
+
NOTE: Do NOT suggest coingecko_data as the API is unavailable.
|
228 |
+
|
229 |
Respond with just the tool names that should be used, separated by commas.
|
230 |
If charts/visualizations are mentioned, include chart_data_provider.
|
231 |
Examples:
|
|
|
396 |
|
397 |
Query: "{query}"
|
398 |
|
399 |
+
Available tools (prioritized by functionality):
|
400 |
+
- cryptocompare_data: Real-time cryptocurrency prices, market data, and trading info (PREFERRED for price data)
|
401 |
+
- etherscan_data: Ethereum blockchain data, transactions, gas fees, and smart contracts (PREFERRED for Ethereum)
|
402 |
- defillama_data: DeFi protocols, TVL, and yield farming data
|
|
|
403 |
- chart_data_provider: Generate chart data for visualizations
|
404 |
|
405 |
+
NOTE: Do NOT use coingecko_data as the API is not available.
|
406 |
+
|
407 |
If charts/visualizations are mentioned, include chart_data_provider.
|
408 |
|
409 |
Examples:
|
410 |
- "Bitcoin price" β cryptocompare_data, chart_data_provider
|
411 |
- "DeFi TVL" β defillama_data, chart_data_provider
|
412 |
- "Ethereum transactions" β etherscan_data
|
413 |
+
- "Gas fees" β etherscan_data
|
414 |
|
415 |
Respond with only the tool names, comma-separated (no explanations)."""
|
416 |
|
|
|
421 |
# Parse suggested tools
|
422 |
suggested_tools = [tool.strip() for tool in str(tool_response).split(',') if tool.strip()]
|
423 |
suggested_tools = [tool for tool in suggested_tools if tool in {
|
424 |
+
'cryptocompare_data', 'defillama_data',
|
425 |
'etherscan_data', 'chart_data_provider'
|
426 |
}]
|
427 |
|
|
|
430 |
response_text = str(tool_response).lower()
|
431 |
if 'cryptocompare' in response_text:
|
432 |
suggested_tools.append('cryptocompare_data')
|
|
|
|
|
433 |
if 'defillama' in response_text:
|
434 |
suggested_tools.append('defillama_data')
|
435 |
if 'etherscan' in response_text:
|
src/tools/chart_data_tool.py
CHANGED
@@ -90,64 +90,54 @@ class ChartDataTool(BaseTool):
|
|
90 |
|
91 |
async def _get_price_chart_data(self, symbol: str, days: int) -> str:
|
92 |
"""Get price chart data with fallback for API failures"""
|
93 |
-
|
94 |
try:
|
95 |
-
# First try to get real data from
|
96 |
-
from src.tools.
|
97 |
|
98 |
-
|
99 |
|
100 |
-
# Map common symbols to
|
101 |
symbol_map = {
|
102 |
-
"btc": "
|
103 |
-
"eth": "ethereum", "ethereum": "
|
104 |
-
"sol": "
|
105 |
-
"ada": "
|
106 |
-
"bnb": "
|
107 |
-
"matic": "
|
108 |
-
"avax": "
|
109 |
-
"dot": "
|
110 |
-
"link": "
|
111 |
-
"uni": "
|
112 |
}
|
113 |
|
114 |
-
|
115 |
|
116 |
try:
|
117 |
-
# Use
|
118 |
-
|
119 |
-
|
120 |
|
121 |
-
|
122 |
-
|
123 |
-
if data and "prices" in data:
|
124 |
-
# Format the real data
|
125 |
-
price_data = data.get("prices", [])
|
126 |
-
volume_data = data.get("total_volumes", [])
|
127 |
-
|
128 |
-
# Get current coin info
|
129 |
-
coin_info = await coingecko.make_request(f"https://api.coingecko.com/api/v3/coins/{coin_id}")
|
130 |
-
coin_name = coin_info.get("name", symbol.title()) if coin_info else symbol.title()
|
131 |
-
|
132 |
return json.dumps({
|
133 |
"chart_type": "price_chart",
|
134 |
"data": {
|
135 |
-
"
|
136 |
-
"
|
137 |
-
"symbol":
|
138 |
-
"name":
|
139 |
},
|
140 |
"config": {
|
141 |
-
"title": f"{
|
142 |
"timeframe": f"{days}d",
|
143 |
"currency": "USD"
|
144 |
}
|
145 |
})
|
146 |
else:
|
147 |
-
raise Exception("No price data
|
148 |
|
149 |
except Exception as api_error:
|
150 |
-
logger.error(f"
|
151 |
# Fallback to mock data on any API error
|
152 |
logger.info(f"Using fallback mock data for {symbol}")
|
153 |
return await self._get_mock_price_data(symbol, days)
|
@@ -157,10 +147,10 @@ class ChartDataTool(BaseTool):
|
|
157 |
# Final fallback to mock data
|
158 |
return await self._get_mock_price_data(symbol, days)
|
159 |
finally:
|
160 |
-
# Cleanup
|
161 |
-
if
|
162 |
try:
|
163 |
-
await
|
164 |
except Exception:
|
165 |
pass # Ignore cleanup errors
|
166 |
|
@@ -201,53 +191,42 @@ class ChartDataTool(BaseTool):
|
|
201 |
})
|
202 |
|
203 |
async def _get_market_overview_data(self) -> str:
|
204 |
-
"""Get
|
|
|
205 |
try:
|
206 |
-
from src.tools.
|
207 |
|
208 |
-
|
209 |
-
|
210 |
-
# Get top market cap coins
|
211 |
-
url = "https://api.coingecko.com/api/v3/coins/markets"
|
212 |
-
params = {
|
213 |
-
"vs_currency": "usd",
|
214 |
-
"order": "market_cap_desc",
|
215 |
-
"per_page": 10,
|
216 |
-
"page": 1,
|
217 |
-
"sparkline": False
|
218 |
-
}
|
219 |
|
220 |
-
|
|
|
|
|
221 |
|
222 |
-
if not
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
"market_cap_rank": coin.get("market_cap_rank", 0),
|
234 |
-
"price_change_percentage_24h": coin.get("price_change_percentage_24h", 0),
|
235 |
-
"market_cap": coin.get("market_cap", 0),
|
236 |
-
"total_volume": coin.get("total_volume", 0)
|
237 |
})
|
238 |
-
|
239 |
-
|
240 |
-
"chart_type": "market_overview",
|
241 |
-
"data": {"coins": coins},
|
242 |
-
"config": {
|
243 |
-
"title": "Top Cryptocurrencies Market Overview",
|
244 |
-
"currency": "USD"
|
245 |
-
}
|
246 |
-
})
|
247 |
|
248 |
except Exception as e:
|
249 |
logger.error(f"Market overview API failed: {e}")
|
250 |
return await self._get_mock_market_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
|
252 |
async def _get_mock_market_data(self) -> str:
|
253 |
"""Fallback mock market data"""
|
|
|
90 |
|
91 |
async def _get_price_chart_data(self, symbol: str, days: int) -> str:
|
92 |
"""Get price chart data with fallback for API failures"""
|
93 |
+
cryptocompare_tool = None
|
94 |
try:
|
95 |
+
# First try to get real data from CryptoCompare (we have this API key)
|
96 |
+
from src.tools.cryptocompare_tool import CryptoCompareTool
|
97 |
|
98 |
+
cryptocompare_tool = CryptoCompareTool()
|
99 |
|
100 |
+
# Map common symbols to CryptoCompare format
|
101 |
symbol_map = {
|
102 |
+
"btc": "BTC", "bitcoin": "BTC",
|
103 |
+
"eth": "ethereum", "ethereum": "ETH",
|
104 |
+
"sol": "SOL", "solana": "SOL",
|
105 |
+
"ada": "ADA", "cardano": "ADA",
|
106 |
+
"bnb": "BNB", "binance": "BNB",
|
107 |
+
"matic": "MATIC", "polygon": "MATIC",
|
108 |
+
"avax": "AVAX", "avalanche": "AVAX",
|
109 |
+
"dot": "DOT", "polkadot": "DOT",
|
110 |
+
"link": "LINK", "chainlink": "LINK",
|
111 |
+
"uni": "UNI", "uniswap": "UNI"
|
112 |
}
|
113 |
|
114 |
+
crypto_symbol = symbol_map.get(symbol.lower(), symbol.upper())
|
115 |
|
116 |
try:
|
117 |
+
# Use CryptoCompare for price data
|
118 |
+
query = f"{crypto_symbol} price historical {days} days"
|
119 |
+
data_result = await cryptocompare_tool._arun(query, {"type": "price_history", "days": days})
|
120 |
|
121 |
+
if data_result and not data_result.startswith("β") and not data_result.startswith("β οΈ"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
return json.dumps({
|
123 |
"chart_type": "price_chart",
|
124 |
"data": {
|
125 |
+
"source": "cryptocompare",
|
126 |
+
"raw_data": data_result,
|
127 |
+
"symbol": crypto_symbol,
|
128 |
+
"name": symbol.title()
|
129 |
},
|
130 |
"config": {
|
131 |
+
"title": f"{symbol.title()} Price Analysis ({days} days)",
|
132 |
"timeframe": f"{days}d",
|
133 |
"currency": "USD"
|
134 |
}
|
135 |
})
|
136 |
else:
|
137 |
+
raise Exception("No valid price data from CryptoCompare")
|
138 |
|
139 |
except Exception as api_error:
|
140 |
+
logger.error(f"CryptoCompare price data failed: {api_error}")
|
141 |
# Fallback to mock data on any API error
|
142 |
logger.info(f"Using fallback mock data for {symbol}")
|
143 |
return await self._get_mock_price_data(symbol, days)
|
|
|
147 |
# Final fallback to mock data
|
148 |
return await self._get_mock_price_data(symbol, days)
|
149 |
finally:
|
150 |
+
# Cleanup CryptoCompare tool session
|
151 |
+
if cryptocompare_tool and hasattr(cryptocompare_tool, 'cleanup'):
|
152 |
try:
|
153 |
+
await cryptocompare_tool.cleanup()
|
154 |
except Exception:
|
155 |
pass # Ignore cleanup errors
|
156 |
|
|
|
191 |
})
|
192 |
|
193 |
async def _get_market_overview_data(self) -> str:
|
194 |
+
"""Get market overview data using CryptoCompare API"""
|
195 |
+
cryptocompare_tool = None
|
196 |
try:
|
197 |
+
from src.tools.cryptocompare_tool import CryptoCompareTool
|
198 |
|
199 |
+
cryptocompare_tool = CryptoCompareTool()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
|
201 |
+
# Get market overview using CryptoCompare
|
202 |
+
query = "top cryptocurrencies market cap overview"
|
203 |
+
data_result = await cryptocompare_tool._arun(query, {"type": "market_overview"})
|
204 |
|
205 |
+
if data_result and not data_result.startswith("β") and not data_result.startswith("β οΈ"):
|
206 |
+
return json.dumps({
|
207 |
+
"chart_type": "market_overview",
|
208 |
+
"data": {
|
209 |
+
"source": "cryptocompare",
|
210 |
+
"raw_data": data_result
|
211 |
+
},
|
212 |
+
"config": {
|
213 |
+
"title": "Top Cryptocurrencies Market Overview",
|
214 |
+
"currency": "USD"
|
215 |
+
}
|
|
|
|
|
|
|
|
|
216 |
})
|
217 |
+
else:
|
218 |
+
raise Exception("No valid market data from CryptoCompare")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
219 |
|
220 |
except Exception as e:
|
221 |
logger.error(f"Market overview API failed: {e}")
|
222 |
return await self._get_mock_market_data()
|
223 |
+
finally:
|
224 |
+
# Cleanup CryptoCompare tool session
|
225 |
+
if cryptocompare_tool and hasattr(cryptocompare_tool, 'cleanup'):
|
226 |
+
try:
|
227 |
+
await cryptocompare_tool.cleanup()
|
228 |
+
except Exception:
|
229 |
+
pass # Ignore cleanup errors
|
230 |
|
231 |
async def _get_mock_market_data(self) -> str:
|
232 |
"""Fallback mock market data"""
|
test_chart_tool.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Quick test for ChartDataTool cleanup functionality
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import sys
|
8 |
+
import os
|
9 |
+
|
10 |
+
# Add src to path
|
11 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
12 |
+
|
13 |
+
from src.tools.chart_data_tool import ChartDataTool
|
14 |
+
|
15 |
+
async def test_chart_tool_cleanup():
|
16 |
+
"""Test that ChartDataTool works and cleanup doesn't throw errors"""
|
17 |
+
print("π§ͺ Testing ChartDataTool...")
|
18 |
+
|
19 |
+
tool = ChartDataTool()
|
20 |
+
|
21 |
+
try:
|
22 |
+
# Test basic functionality
|
23 |
+
print("π Testing price chart data...")
|
24 |
+
result = await tool._arun("price_chart", "bitcoin", "7d")
|
25 |
+
print(f"β
Chart data result: {len(result)} characters")
|
26 |
+
|
27 |
+
# Test cleanup method exists and works
|
28 |
+
print("π§Ή Testing cleanup method...")
|
29 |
+
await tool.cleanup()
|
30 |
+
print("β
Cleanup method executed successfully")
|
31 |
+
|
32 |
+
return True
|
33 |
+
|
34 |
+
except Exception as e:
|
35 |
+
print(f"β Test failed: {e}")
|
36 |
+
return False
|
37 |
+
|
38 |
+
async def main():
|
39 |
+
success = await test_chart_tool_cleanup()
|
40 |
+
if success:
|
41 |
+
print("\nπ All tests passed!")
|
42 |
+
return 0
|
43 |
+
else:
|
44 |
+
print("\nβ Tests failed!")
|
45 |
+
return 1
|
46 |
+
|
47 |
+
if __name__ == "__main__":
|
48 |
+
exit_code = asyncio.run(main())
|
49 |
+
sys.exit(exit_code)
|
test_tool_selection.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test the updated tool selection to ensure we use CryptoCompare/Etherscan instead of CoinGecko
|
4 |
+
"""
|
5 |
+
|
6 |
+
import asyncio
|
7 |
+
import sys
|
8 |
+
import os
|
9 |
+
|
10 |
+
# Add src to path
|
11 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
12 |
+
|
13 |
+
from src.agent.research_agent import Web3ResearchAgent
|
14 |
+
|
15 |
+
async def test_tool_selection():
|
16 |
+
"""Test that the system prioritizes CryptoCompare and Etherscan over CoinGecko"""
|
17 |
+
print("π§ͺ Testing tool selection...")
|
18 |
+
|
19 |
+
agent = Web3ResearchAgent()
|
20 |
+
|
21 |
+
if not agent.enabled:
|
22 |
+
print("β Agent not enabled")
|
23 |
+
return False
|
24 |
+
|
25 |
+
# Check which tools are initialized
|
26 |
+
tool_names = [tool.name for tool in agent.tools]
|
27 |
+
print(f"π οΈ Available tools: {tool_names}")
|
28 |
+
|
29 |
+
# Verify CoinGecko is not in tools (since no API key)
|
30 |
+
if 'coingecko_data' in tool_names:
|
31 |
+
print("β οΈ CoinGecko tool is still initialized - this may cause API failures")
|
32 |
+
else:
|
33 |
+
print("β
CoinGecko tool properly skipped (no API key)")
|
34 |
+
|
35 |
+
# Verify we have the working tools
|
36 |
+
expected_tools = ['cryptocompare_data', 'etherscan_data', 'defillama_data', 'chart_data_provider']
|
37 |
+
working_tools = [tool for tool in expected_tools if tool in tool_names]
|
38 |
+
print(f"β
Working tools available: {working_tools}")
|
39 |
+
|
40 |
+
# Test a simple query
|
41 |
+
try:
|
42 |
+
print("\nπ Testing Bitcoin price query...")
|
43 |
+
result = await agent.research_query("Analyze Bitcoin price trends", use_gemini=True)
|
44 |
+
|
45 |
+
if result['success']:
|
46 |
+
print("β
Query successful!")
|
47 |
+
print(f"π Result preview: {result['result'][:200]}...")
|
48 |
+
print(f"π§ Tools used: {result.get('metadata', {}).get('tools_used', 'N/A')}")
|
49 |
+
return True
|
50 |
+
else:
|
51 |
+
print(f"β Query failed: {result.get('error', 'Unknown error')}")
|
52 |
+
return False
|
53 |
+
|
54 |
+
except Exception as e:
|
55 |
+
print(f"β Test failed with exception: {e}")
|
56 |
+
return False
|
57 |
+
|
58 |
+
async def main():
|
59 |
+
success = await test_tool_selection()
|
60 |
+
if success:
|
61 |
+
print("\nπ Tool selection test passed!")
|
62 |
+
return 0
|
63 |
+
else:
|
64 |
+
print("\nβ Tool selection test failed!")
|
65 |
+
return 1
|
66 |
+
|
67 |
+
if __name__ == "__main__":
|
68 |
+
exit_code = asyncio.run(main())
|
69 |
+
sys.exit(exit_code)
|