Spaces:
Sleeping
Sleeping
“Transcendental-Programmer”
commited on
Commit
·
20eee66
1
Parent(s):
539f014
feat: Initial commit with project structure and initial files
Browse files- .env.example +8 -0
- app.py +312 -0
- attached_assets/Pasted--Complete-Web3-Research-Co-Pilot-Project-Plan-Structure-Project-Directory-Structure--1754811430335_1754811430338.txt +992 -0
- pyproject.toml +24 -0
- requirements.txt +7 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/__pycache__/api_clients.cpython-311.pyc +0 -0
- src/__pycache__/cache_manager.cpython-311.pyc +0 -0
- src/__pycache__/config.cpython-311.pyc +0 -0
- src/__pycache__/defillama_client.cpython-311.pyc +0 -0
- src/__pycache__/enhanced_agent.cpython-311.pyc +0 -0
- src/__pycache__/news_aggregator.cpython-311.pyc +0 -0
- src/__pycache__/portfolio_analyzer.cpython-311.pyc +0 -0
- src/__pycache__/research_agent.cpython-311.pyc +0 -0
- src/__pycache__/visualizations.cpython-311.pyc +0 -0
- src/api_clients.py +158 -0
- src/cache_manager.py +49 -0
- src/config.py +19 -0
- src/defillama_client.py +62 -0
- src/enhanced_agent.py +273 -0
- src/news_aggregator.py +83 -0
- src/portfolio_analyzer.py +143 -0
- src/research_agent.py +201 -0
- src/visualizations.py +238 -0
- uv.lock +0 -0
.env.example
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Google Gemini API Key (Required)
|
2 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
3 |
+
|
4 |
+
# CoinGecko API Key (Optional - for higher rate limits)
|
5 |
+
COINGECKO_API_KEY=your_coingecko_api_key_here
|
6 |
+
|
7 |
+
# CryptoCompare API Key (Optional - for additional data sources)
|
8 |
+
CRYPTOCOMPARE_API_KEY=your_cryptocompare_api_key_here
|
app.py
ADDED
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
from src.enhanced_agent import EnhancedResearchAgent
|
5 |
+
from src.portfolio_analyzer import portfolio_analyzer
|
6 |
+
from src.visualizations import create_price_chart, create_market_overview, create_comparison_chart
|
7 |
+
from src.cache_manager import cache_manager
|
8 |
+
import asyncio
|
9 |
+
|
10 |
+
research_agent = EnhancedResearchAgent()
|
11 |
+
|
12 |
+
async def process_research_query(query, history):
|
13 |
+
try:
|
14 |
+
if not query.strip():
|
15 |
+
return history + [["Please enter a research query.", ""]]
|
16 |
+
|
17 |
+
response = await research_agent.research_with_context(query)
|
18 |
+
return history + [[query, response]]
|
19 |
+
except Exception as e:
|
20 |
+
error_msg = f"Enhanced research failed: {str(e)}"
|
21 |
+
return history + [[query, error_msg]]
|
22 |
+
|
23 |
+
def research_query_sync(query, history):
|
24 |
+
return asyncio.run(process_research_query(query, history))
|
25 |
+
|
26 |
+
async def get_market_data():
|
27 |
+
try:
|
28 |
+
data = await research_agent.get_comprehensive_market_data()
|
29 |
+
chart = create_market_overview(data)
|
30 |
+
return chart
|
31 |
+
except Exception as e:
|
32 |
+
return f"Enhanced market data unavailable: {str(e)}"
|
33 |
+
|
34 |
+
def get_market_data_sync():
|
35 |
+
return asyncio.run(get_market_data())
|
36 |
+
|
37 |
+
async def get_price_chart(symbol):
|
38 |
+
try:
|
39 |
+
if not symbol.strip():
|
40 |
+
return "Please enter a cryptocurrency symbol"
|
41 |
+
|
42 |
+
data = await research_agent.get_price_history(symbol)
|
43 |
+
chart = create_price_chart(data, symbol)
|
44 |
+
return chart
|
45 |
+
except Exception as e:
|
46 |
+
return f"Chart generation failed: {str(e)}"
|
47 |
+
|
48 |
+
def get_price_chart_sync(symbol):
|
49 |
+
return asyncio.run(get_price_chart(symbol))
|
50 |
+
|
51 |
+
async def analyze_portfolio_async(portfolio_text):
|
52 |
+
try:
|
53 |
+
if not portfolio_text.strip():
|
54 |
+
return "Please enter your portfolio holdings in JSON format"
|
55 |
+
|
56 |
+
holdings = json.loads(portfolio_text)
|
57 |
+
analysis = await portfolio_analyzer.analyze_portfolio(holdings)
|
58 |
+
|
59 |
+
result = f"📊 PORTFOLIO ANALYSIS\n\n"
|
60 |
+
result += f"💰 Total Value: ${analysis['total_value']:,.2f}\n"
|
61 |
+
result += f"📈 24h Change: ${analysis['change_24h']:+,.2f} ({analysis['change_24h_percentage']:+.2f}%)\n\n"
|
62 |
+
|
63 |
+
result += "🏦 ASSET ALLOCATION:\n"
|
64 |
+
for asset in analysis['asset_allocation'][:10]:
|
65 |
+
result += f"• {asset['name']} ({asset['symbol']}): {asset['percentage']:.1f}% (${asset['value']:,.2f})\n"
|
66 |
+
|
67 |
+
result += f"\n⚠️ RISK ASSESSMENT:\n"
|
68 |
+
result += f"Overall Risk: {analysis['risk_metrics']['overall_risk']}\n"
|
69 |
+
result += f"Diversification Score: {analysis['risk_metrics']['diversification_score']}/10\n"
|
70 |
+
result += f"Largest Position: {analysis['risk_metrics']['largest_holding_percentage']:.1f}%\n\n"
|
71 |
+
|
72 |
+
result += "💡 RECOMMENDATIONS:\n"
|
73 |
+
for i, rec in enumerate(analysis['recommendations'], 1):
|
74 |
+
result += f"{i}. {rec}\n"
|
75 |
+
|
76 |
+
return result
|
77 |
+
|
78 |
+
except json.JSONDecodeError:
|
79 |
+
return "❌ Invalid JSON format. Please use format: [{'symbol': 'BTC', 'amount': 1.0}, {'symbol': 'ETH', 'amount': 10.0}]"
|
80 |
+
except Exception as e:
|
81 |
+
return f"❌ Portfolio analysis failed: {str(e)}"
|
82 |
+
|
83 |
+
async def get_defi_analysis_async():
|
84 |
+
try:
|
85 |
+
data = await research_agent.get_defi_analysis()
|
86 |
+
|
87 |
+
result = "🏦 DeFi ECOSYSTEM ANALYSIS\n\n"
|
88 |
+
|
89 |
+
if "top_protocols" in data:
|
90 |
+
result += "📊 TOP PROTOCOLS BY TVL:\n"
|
91 |
+
for i, protocol in enumerate(data["top_protocols"][:10], 1):
|
92 |
+
name = protocol.get("name", "Unknown")
|
93 |
+
tvl = protocol.get("tvl", 0)
|
94 |
+
chain = protocol.get("chain", "Unknown")
|
95 |
+
change = protocol.get("change_1d", 0)
|
96 |
+
result += f"{i:2d}. {name} ({chain}): ${tvl/1e9:.2f}B TVL ({change:+.2f}%)\n"
|
97 |
+
|
98 |
+
if "top_yields" in data:
|
99 |
+
result += "\n💰 HIGH YIELD OPPORTUNITIES:\n"
|
100 |
+
for i, pool in enumerate(data["top_yields"][:5], 1):
|
101 |
+
symbol = pool.get("symbol", "Unknown")
|
102 |
+
apy = pool.get("apy", 0)
|
103 |
+
tvl = pool.get("tvlUsd", 0)
|
104 |
+
result += f"{i}. {symbol}: {apy:.2f}% APY (${tvl/1e6:.1f}M TVL)\n"
|
105 |
+
|
106 |
+
return result
|
107 |
+
|
108 |
+
except Exception as e:
|
109 |
+
return f"❌ DeFi analysis failed: {str(e)}"
|
110 |
+
|
111 |
+
def clear_cache():
|
112 |
+
cache_manager.clear()
|
113 |
+
return "Cache cleared successfully"
|
114 |
+
|
115 |
+
def analyze_portfolio_sync(portfolio_text):
|
116 |
+
return asyncio.run(analyze_portfolio_async(portfolio_text))
|
117 |
+
|
118 |
+
def get_defi_analysis_sync():
|
119 |
+
return asyncio.run(get_defi_analysis_async())
|
120 |
+
|
121 |
+
with gr.Blocks(
|
122 |
+
title="Web3 Research Co-Pilot",
|
123 |
+
theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray"),
|
124 |
+
css="""
|
125 |
+
.container { max-width: 1200px; margin: 0 auto; }
|
126 |
+
.header { text-align: center; padding: 20px; }
|
127 |
+
.chat-container { min-height: 400px; }
|
128 |
+
.chart-container { min-height: 500px; }
|
129 |
+
"""
|
130 |
+
) as app:
|
131 |
+
|
132 |
+
gr.Markdown("# 🚀 Web3 Research Co-Pilot", elem_classes=["header"])
|
133 |
+
gr.Markdown("*AI-powered cryptocurrency research with real-time data integration*", elem_classes=["header"])
|
134 |
+
|
135 |
+
with gr.Tabs():
|
136 |
+
|
137 |
+
with gr.Tab("🤖 Research Chat"):
|
138 |
+
with gr.Row():
|
139 |
+
with gr.Column(scale=3):
|
140 |
+
chatbot = gr.Chatbot(
|
141 |
+
value=[],
|
142 |
+
height=400,
|
143 |
+
elem_classes=["chat-container"],
|
144 |
+
show_label=False
|
145 |
+
)
|
146 |
+
|
147 |
+
with gr.Row():
|
148 |
+
query_input = gr.Textbox(
|
149 |
+
placeholder="Ask about crypto markets, prices, trends, analysis...",
|
150 |
+
scale=4,
|
151 |
+
show_label=False
|
152 |
+
)
|
153 |
+
submit_btn = gr.Button("Research", variant="primary")
|
154 |
+
|
155 |
+
gr.Examples(
|
156 |
+
examples=[
|
157 |
+
"What's the current Bitcoin price and trend?",
|
158 |
+
"Compare Ethereum vs Solana DeFi ecosystems",
|
159 |
+
"Analyze top DeFi protocols and TVL trends",
|
160 |
+
"What are the trending coins and latest crypto news?",
|
161 |
+
"Show me high-yield DeFi opportunities with risk analysis"
|
162 |
+
],
|
163 |
+
inputs=query_input
|
164 |
+
)
|
165 |
+
|
166 |
+
with gr.Column(scale=1):
|
167 |
+
gr.Markdown("### 📊 Quick Actions")
|
168 |
+
market_btn = gr.Button("Market Overview", size="sm")
|
169 |
+
market_output = gr.HTML()
|
170 |
+
|
171 |
+
clear_btn = gr.Button("Clear Cache", size="sm", variant="secondary")
|
172 |
+
clear_output = gr.Textbox(show_label=False, interactive=False)
|
173 |
+
|
174 |
+
with gr.Tab("📈 Price Charts"):
|
175 |
+
with gr.Row():
|
176 |
+
symbol_input = gr.Textbox(
|
177 |
+
label="Cryptocurrency Symbol",
|
178 |
+
placeholder="BTC, ETH, SOL, etc.",
|
179 |
+
value="BTC"
|
180 |
+
)
|
181 |
+
chart_btn = gr.Button("Generate Chart", variant="primary")
|
182 |
+
|
183 |
+
chart_output = gr.HTML(elem_classes=["chart-container"])
|
184 |
+
|
185 |
+
gr.Examples(
|
186 |
+
examples=["BTC", "ETH", "SOL", "ADA", "DOT"],
|
187 |
+
inputs=symbol_input
|
188 |
+
)
|
189 |
+
|
190 |
+
with gr.Tab("💼 Portfolio Analysis"):
|
191 |
+
with gr.Row():
|
192 |
+
with gr.Column(scale=1):
|
193 |
+
portfolio_input = gr.Textbox(
|
194 |
+
label="Portfolio Holdings (JSON Format)",
|
195 |
+
placeholder='[{"symbol": "BTC", "amount": 1.0}, {"symbol": "ETH", "amount": 10.0}, {"symbol": "SOL", "amount": 50.0}]',
|
196 |
+
lines=5,
|
197 |
+
info="Enter your crypto holdings in JSON format with symbol and amount"
|
198 |
+
)
|
199 |
+
portfolio_btn = gr.Button("Analyze Portfolio", variant="primary")
|
200 |
+
|
201 |
+
with gr.Column(scale=2):
|
202 |
+
portfolio_output = gr.Textbox(
|
203 |
+
label="Portfolio Analysis Results",
|
204 |
+
lines=20,
|
205 |
+
show_copy_button=True,
|
206 |
+
interactive=False
|
207 |
+
)
|
208 |
+
|
209 |
+
gr.Examples(
|
210 |
+
examples=[
|
211 |
+
'[{"symbol": "BTC", "amount": 0.5}, {"symbol": "ETH", "amount": 5.0}]',
|
212 |
+
'[{"symbol": "BTC", "amount": 1.0}, {"symbol": "ETH", "amount": 10.0}, {"symbol": "SOL", "amount": 100.0}]'
|
213 |
+
],
|
214 |
+
inputs=portfolio_input
|
215 |
+
)
|
216 |
+
|
217 |
+
with gr.Tab("🏦 DeFi Analytics"):
|
218 |
+
defi_btn = gr.Button("Get DeFi Ecosystem Analysis", variant="primary", size="lg")
|
219 |
+
defi_output = gr.Textbox(
|
220 |
+
label="DeFi Analysis Results",
|
221 |
+
lines=25,
|
222 |
+
show_copy_button=True,
|
223 |
+
interactive=False,
|
224 |
+
info="Comprehensive DeFi protocol analysis with TVL data and yield opportunities"
|
225 |
+
)
|
226 |
+
|
227 |
+
with gr.Tab("ℹ️ About"):
|
228 |
+
gr.Markdown("""
|
229 |
+
## 🚀 Enhanced Features
|
230 |
+
- **Multi-API Integration**: CoinGecko, CryptoCompare, and DeFiLlama data sources
|
231 |
+
- **AI-Powered Analysis**: Google Gemini 2.5 Flash with contextual market intelligence
|
232 |
+
- **DeFi Analytics**: Protocol TVL analysis, yield farming opportunities, and ecosystem insights
|
233 |
+
- **Portfolio Analysis**: Risk assessment, diversification scoring, and personalized recommendations
|
234 |
+
- **News Integration**: Real-time crypto news aggregation and sentiment analysis
|
235 |
+
- **Interactive Charts**: Advanced price visualizations with technical indicators
|
236 |
+
- **Smart Caching**: Optimized performance with intelligent data caching (TTL-based)
|
237 |
+
- **Rate Limiting**: Respectful API usage with automatic throttling
|
238 |
+
|
239 |
+
## 🎯 Core Capabilities
|
240 |
+
1. **Enhanced Research Chat**: Context-aware conversations with real-time market data integration
|
241 |
+
2. **Advanced Price Charts**: Interactive visualizations with 30-day historical data
|
242 |
+
3. **Portfolio Optimization**: Comprehensive portfolio analysis with risk metrics and recommendations
|
243 |
+
4. **DeFi Intelligence**: Protocol rankings, TVL trends, and high-yield opportunity identification
|
244 |
+
5. **Market Intelligence**: Global market metrics, trending assets, and breaking news analysis
|
245 |
+
|
246 |
+
## 💡 Query Examples
|
247 |
+
- "Analyze Bitcoin vs Ethereum DeFi ecosystem performance"
|
248 |
+
- "What are the top DeFi protocols by TVL with lowest risk?"
|
249 |
+
- "Show me high-yield farming opportunities under 15% volatility"
|
250 |
+
- "Compare my portfolio risk to market benchmarks"
|
251 |
+
- "Latest crypto news impact on altcoin market sentiment"
|
252 |
+
- "Which Layer 1 protocols have strongest DeFi adoption?"
|
253 |
+
|
254 |
+
## 🔧 Technical Architecture
|
255 |
+
- **Async Processing**: Non-blocking operations for optimal performance
|
256 |
+
- **Error Handling**: Comprehensive exception management with graceful degradation
|
257 |
+
- **Symbol Mapping**: Intelligent cryptocurrency identifier resolution
|
258 |
+
- **Data Validation**: Input sanitization and response formatting
|
259 |
+
""")
|
260 |
+
|
261 |
+
submit_btn.click(
|
262 |
+
research_query_sync,
|
263 |
+
inputs=[query_input, chatbot],
|
264 |
+
outputs=chatbot
|
265 |
+
).then(lambda: "", outputs=query_input)
|
266 |
+
|
267 |
+
query_input.submit(
|
268 |
+
research_query_sync,
|
269 |
+
inputs=[query_input, chatbot],
|
270 |
+
outputs=chatbot
|
271 |
+
).then(lambda: "", outputs=query_input)
|
272 |
+
|
273 |
+
chart_btn.click(
|
274 |
+
get_price_chart_sync,
|
275 |
+
inputs=symbol_input,
|
276 |
+
outputs=chart_output
|
277 |
+
)
|
278 |
+
|
279 |
+
symbol_input.submit(
|
280 |
+
get_price_chart_sync,
|
281 |
+
inputs=symbol_input,
|
282 |
+
outputs=chart_output
|
283 |
+
)
|
284 |
+
|
285 |
+
market_btn.click(
|
286 |
+
get_market_data_sync,
|
287 |
+
outputs=market_output
|
288 |
+
)
|
289 |
+
|
290 |
+
clear_btn.click(
|
291 |
+
clear_cache,
|
292 |
+
outputs=clear_output
|
293 |
+
)
|
294 |
+
|
295 |
+
portfolio_btn.click(
|
296 |
+
analyze_portfolio_sync,
|
297 |
+
inputs=portfolio_input,
|
298 |
+
outputs=portfolio_output
|
299 |
+
)
|
300 |
+
|
301 |
+
defi_btn.click(
|
302 |
+
get_defi_analysis_sync,
|
303 |
+
outputs=defi_output
|
304 |
+
)
|
305 |
+
|
306 |
+
if __name__ == "__main__":
|
307 |
+
app.launch(
|
308 |
+
server_name="0.0.0.0",
|
309 |
+
server_port=5000,
|
310 |
+
share=False,
|
311 |
+
show_error=True
|
312 |
+
)
|
attached_assets/Pasted--Complete-Web3-Research-Co-Pilot-Project-Plan-Structure-Project-Directory-Structure--1754811430335_1754811430338.txt
ADDED
@@ -0,0 +1,992 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Complete Web3 Research Co-Pilot Project Plan & Structure
|
2 |
+
|
3 |
+
## 🏗️ **Project Directory Structure**
|
4 |
+
|
5 |
+
```
|
6 |
+
web3-research-copilot/
|
7 |
+
├── README.md
|
8 |
+
├── requirements.txt
|
9 |
+
├── app.py # Main Gradio application
|
10 |
+
├── .env.example # Environment variables template
|
11 |
+
├── .gitignore
|
12 |
+
├── Dockerfile # For HF Spaces deployment
|
13 |
+
├──
|
14 |
+
├── src/
|
15 |
+
│ ├── __init__.py
|
16 |
+
│ ├── agent/
|
17 |
+
│ │ ├── __init__.py
|
18 |
+
│ │ ├── research_agent.py # Main LangChain agent
|
19 |
+
│ │ ├── query_planner.py # Multi-step query breakdown
|
20 |
+
│ │ ├── memory_manager.py # Conversation memory
|
21 |
+
│ │ └── response_formatter.py # Output formatting
|
22 |
+
│ │
|
23 |
+
│ ├── tools/
|
24 |
+
│ │ ├── __init__.py
|
25 |
+
│ │ ├── base_tool.py # Abstract base tool class
|
26 |
+
│ │ ├── coingecko_tool.py # CoinGecko API integration
|
27 |
+
│ │ ├── defillama_tool.py # DeFiLlama API integration
|
28 |
+
│ │ ├── etherscan_tool.py # Etherscan API integration
|
29 |
+
│ │ ├── cryptocompare_tool.py # CryptoCompare API integration
|
30 |
+
│ │ └── social_tool.py # Social media data (Twitter API)
|
31 |
+
│ │
|
32 |
+
│ ├── data/
|
33 |
+
│ │ ├── __init__.py
|
34 |
+
│ │ ├── processors/
|
35 |
+
│ │ │ ├── __init__.py
|
36 |
+
│ │ │ ├── price_processor.py
|
37 |
+
│ │ │ ├── volume_processor.py
|
38 |
+
│ │ │ └── social_processor.py
|
39 |
+
│ │ ├── cache/
|
40 |
+
│ │ │ ├── __init__.py
|
41 |
+
│ │ │ └── redis_cache.py # Simple in-memory cache
|
42 |
+
│ │ └── validators/
|
43 |
+
│ │ ├── __init__.py
|
44 |
+
│ │ └── data_validator.py
|
45 |
+
│ │
|
46 |
+
│ ├── ui/
|
47 |
+
│ │ ├── __init__.py
|
48 |
+
│ │ ├── gradio_interface.py # Main UI components
|
49 |
+
│ │ ├── components/
|
50 |
+
│ │ │ ├── __init__.py
|
51 |
+
│ │ │ ├── chat_component.py
|
52 |
+
│ │ │ ├── chart_component.py
|
53 |
+
│ │ │ └── table_component.py
|
54 |
+
│ │ └── styles/
|
55 |
+
│ │ ├── custom.css
|
56 |
+
│ │ └── theme.py
|
57 |
+
│ │
|
58 |
+
│ ├── api/
|
59 |
+
│ │ ├── __init__.py
|
60 |
+
│ │ ├── airaa_integration.py # AIRAA-specific API endpoints
|
61 |
+
│ │ ├── webhook_handler.py # For AIRAA integration
|
62 |
+
│ │ └── rate_limiter.py # API rate limiting
|
63 |
+
│ │
|
64 |
+
│ ├── utils/
|
65 |
+
│ │ ├── __init__.py
|
66 |
+
│ │ ├── logger.py # Logging configuration
|
67 |
+
│ │ ├── config.py # Configuration management
|
68 |
+
│ │ ├── exceptions.py # Custom exceptions
|
69 |
+
│ │ └── helpers.py # Utility functions
|
70 |
+
│ │
|
71 |
+
│ └── visualizations/
|
72 |
+
│ ├── __init__.py
|
73 |
+
│ ├── plotly_charts.py # Interactive charts
|
74 |
+
│ ├── tables.py # Data tables
|
75 |
+
│ └── export_utils.py # Data export functions
|
76 |
+
│
|
77 |
+
├── tests/
|
78 |
+
│ ├── __init__.py
|
79 |
+
│ ├── test_agent/
|
80 |
+
│ │ ├── test_research_agent.py
|
81 |
+
│ │ └── test_query_planner.py
|
82 |
+
│ ├── test_tools/
|
83 |
+
│ │ ├── test_coingecko.py
|
84 |
+
│ │ └── test_defillama.py
|
85 |
+
│ └── test_integration/
|
86 |
+
│ └── test_airaa_integration.py
|
87 |
+
│
|
88 |
+
├── docs/
|
89 |
+
│ ├── API.md # API documentation
|
90 |
+
│ ├── DEPLOYMENT.md # Deployment guide
|
91 |
+
│ ├── AIRAA_INTEGRATION.md # AIRAA integration guide
|
92 |
+
│ └── USER_GUIDE.md # User documentation
|
93 |
+
│
|
94 |
+
├── examples/
|
95 |
+
│ ├── sample_queries.py # Example research queries
|
96 |
+
│ ├── api_examples.py # API usage examples
|
97 |
+
│ └── airaa_webhook_example.py # AIRAA integration example
|
98 |
+
│
|
99 |
+
└── deployment/
|
100 |
+
├── docker-compose.yml
|
101 |
+
├── nginx.conf
|
102 |
+
└── huggingface_spaces/
|
103 |
+
├── spaces_config.yml
|
104 |
+
└── deployment_script.sh
|
105 |
+
```
|
106 |
+
|
107 |
+
## 📋 **Detailed Implementation Plan**
|
108 |
+
|
109 |
+
### **Phase 1: Foundation Setup (Days 1-2)**
|
110 |
+
|
111 |
+
#### Day 1: Project Structure & Core Setup
|
112 |
+
```bash
|
113 |
+
# Setup commands
|
114 |
+
mkdir web3-research-copilot
|
115 |
+
cd web3-research-copilot
|
116 |
+
python -m venv venv
|
117 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
118 |
+
pip install --upgrade pip
|
119 |
+
```
|
120 |
+
|
121 |
+
**Key Files to Create:**
|
122 |
+
|
123 |
+
**requirements.txt**
|
124 |
+
```txt
|
125 |
+
# Core AI/ML
|
126 |
+
langchain==0.1.0
|
127 |
+
langchain-google-genai==1.0.0
|
128 |
+
langchain-community==0.0.20
|
129 |
+
|
130 |
+
# Web Framework
|
131 |
+
gradio==4.15.0
|
132 |
+
fastapi==0.108.0
|
133 |
+
uvicorn==0.25.0
|
134 |
+
|
135 |
+
# Data Processing
|
136 |
+
pandas==2.1.4
|
137 |
+
numpy==1.24.3
|
138 |
+
requests==2.31.0
|
139 |
+
python-dotenv==1.0.0
|
140 |
+
|
141 |
+
# Visualization
|
142 |
+
plotly==5.17.0
|
143 |
+
matplotlib==3.8.2
|
144 |
+
|
145 |
+
# Utilities
|
146 |
+
pydantic==2.5.2
|
147 |
+
python-dateutil==2.8.2
|
148 |
+
tenacity==8.2.3
|
149 |
+
|
150 |
+
# Caching & Performance
|
151 |
+
diskcache==5.6.3
|
152 |
+
asyncio-throttle==1.0.2
|
153 |
+
|
154 |
+
# Testing
|
155 |
+
pytest==7.4.3
|
156 |
+
pytest-asyncio==0.21.1
|
157 |
+
```
|
158 |
+
|
159 |
+
**src/utils/config.py**
|
160 |
+
```python
|
161 |
+
import os
|
162 |
+
from dotenv import load_dotenv
|
163 |
+
from dataclasses import dataclass
|
164 |
+
from typing import Optional
|
165 |
+
|
166 |
+
load_dotenv()
|
167 |
+
|
168 |
+
@dataclass
|
169 |
+
class Config:
|
170 |
+
# AI Configuration
|
171 |
+
GEMINI_API_KEY: str = os.getenv("GEMINI_API_KEY")
|
172 |
+
GEMINI_MODEL: str = "gemini-pro"
|
173 |
+
|
174 |
+
# API Keys
|
175 |
+
COINGECKO_API_KEY: Optional[str] = os.getenv("COINGECKO_API_KEY")
|
176 |
+
ETHERSCAN_API_KEY: str = os.getenv("ETHERSCAN_API_KEY")
|
177 |
+
CRYPTOCOMPARE_API_KEY: Optional[str] = os.getenv("CRYPTOCOMPARE_API_KEY")
|
178 |
+
|
179 |
+
# Rate Limits (per minute)
|
180 |
+
COINGECKO_RATE_LIMIT: int = 10
|
181 |
+
ETHERSCAN_RATE_LIMIT: int = 5
|
182 |
+
GEMINI_RATE_LIMIT: int = 15
|
183 |
+
|
184 |
+
# Cache Configuration
|
185 |
+
CACHE_TTL: int = 300 # 5 minutes
|
186 |
+
CACHE_SIZE: int = 100
|
187 |
+
|
188 |
+
# UI Configuration
|
189 |
+
UI_TITLE: str = "Web3 Research Co-Pilot"
|
190 |
+
UI_DESCRIPTION: str = "AI-powered crypto research assistant"
|
191 |
+
|
192 |
+
# AIRAA Integration
|
193 |
+
AIRAA_WEBHOOK_URL: Optional[str] = os.getenv("AIRAA_WEBHOOK_URL")
|
194 |
+
AIRAA_API_KEY: Optional[str] = os.getenv("AIRAA_API_KEY")
|
195 |
+
|
196 |
+
# Logging
|
197 |
+
LOG_LEVEL: str = "INFO"
|
198 |
+
LOG_FILE: str = "app.log"
|
199 |
+
|
200 |
+
config = Config()
|
201 |
+
```
|
202 |
+
|
203 |
+
#### Day 2: Base Tool Architecture
|
204 |
+
|
205 |
+
**src/tools/base_tool.py**
|
206 |
+
```python
|
207 |
+
from abc import ABC, abstractmethod
|
208 |
+
from typing import Dict, Any, Optional
|
209 |
+
from langchain.tools import BaseTool
|
210 |
+
from pydantic import BaseModel, Field
|
211 |
+
import asyncio
|
212 |
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
213 |
+
from src.utils.logger import get_logger
|
214 |
+
|
215 |
+
logger = get_logger(__name__)
|
216 |
+
|
217 |
+
class Web3ToolInput(BaseModel):
|
218 |
+
query: str = Field(description="The search query or parameter")
|
219 |
+
filters: Optional[Dict[str, Any]] = Field(default=None, description="Additional filters")
|
220 |
+
|
221 |
+
class BaseWeb3Tool(BaseTool, ABC):
|
222 |
+
"""Base class for all Web3 data tools"""
|
223 |
+
|
224 |
+
name: str = "base_web3_tool"
|
225 |
+
description: str = "Base Web3 tool"
|
226 |
+
args_schema = Web3ToolInput
|
227 |
+
|
228 |
+
def __init__(self, **kwargs):
|
229 |
+
super().__init__(**kwargs)
|
230 |
+
self.rate_limiter = self._setup_rate_limiter()
|
231 |
+
|
232 |
+
@abstractmethod
|
233 |
+
def _setup_rate_limiter(self):
|
234 |
+
"""Setup rate limiting for the specific API"""
|
235 |
+
pass
|
236 |
+
|
237 |
+
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
|
238 |
+
async def _make_api_call(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
239 |
+
"""Make rate-limited API call with retry logic"""
|
240 |
+
await self.rate_limiter.acquire()
|
241 |
+
# Implementation will be in specific tools
|
242 |
+
pass
|
243 |
+
|
244 |
+
@abstractmethod
|
245 |
+
def _process_response(self, response: Dict[str, Any]) -> str:
|
246 |
+
"""Process API response into readable format"""
|
247 |
+
pass
|
248 |
+
|
249 |
+
def _run(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
250 |
+
"""Synchronous tool execution"""
|
251 |
+
return asyncio.run(self._arun(query, filters))
|
252 |
+
|
253 |
+
@abstractmethod
|
254 |
+
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
255 |
+
"""Asynchronous tool execution"""
|
256 |
+
pass
|
257 |
+
```
|
258 |
+
|
259 |
+
### **Phase 2: Data Tools Implementation (Days 3-4)**
|
260 |
+
|
261 |
+
#### CoinGecko Tool
|
262 |
+
**src/tools/coingecko_tool.py**
|
263 |
+
```python
|
264 |
+
import aiohttp
|
265 |
+
from typing import Dict, Any, Optional
|
266 |
+
from asyncio_throttle import Throttler
|
267 |
+
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
268 |
+
from src.utils.config import config
|
269 |
+
|
270 |
+
class CoinGeckoTool(BaseWeb3Tool):
|
271 |
+
name = "coingecko_price_data"
|
272 |
+
description = """
|
273 |
+
Get cryptocurrency price, volume, market cap and trend data from CoinGecko.
|
274 |
+
Useful for: price analysis, market cap rankings, volume trends, price changes.
|
275 |
+
Input should be a cryptocurrency name or symbol (e.g., 'bitcoin', 'ethereum', 'BTC').
|
276 |
+
"""
|
277 |
+
|
278 |
+
def _setup_rate_limiter(self):
|
279 |
+
return Throttler(rate_limit=config.COINGECKO_RATE_LIMIT, period=60)
|
280 |
+
|
281 |
+
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
282 |
+
try:
|
283 |
+
# Clean and format the query
|
284 |
+
coin_id = self._format_coin_id(query)
|
285 |
+
|
286 |
+
# API endpoints
|
287 |
+
base_url = "https://api.coingecko.com/api/v3"
|
288 |
+
|
289 |
+
if filters and filters.get("type") == "trending":
|
290 |
+
url = f"{base_url}/search/trending"
|
291 |
+
params = {}
|
292 |
+
elif filters and filters.get("type") == "market_data":
|
293 |
+
url = f"{base_url}/coins/{coin_id}"
|
294 |
+
params = {"localization": "false", "tickers": "false", "community_data": "false"}
|
295 |
+
else:
|
296 |
+
url = f"{base_url}/simple/price"
|
297 |
+
params = {
|
298 |
+
"ids": coin_id,
|
299 |
+
"vs_currencies": "usd",
|
300 |
+
"include_24hr_change": "true",
|
301 |
+
"include_24hr_vol": "true",
|
302 |
+
"include_market_cap": "true"
|
303 |
+
}
|
304 |
+
|
305 |
+
async with aiohttp.ClientSession() as session:
|
306 |
+
await self.rate_limiter.acquire()
|
307 |
+
async with session.get(url, params=params) as response:
|
308 |
+
if response.status == 200:
|
309 |
+
data = await response.json()
|
310 |
+
return self._process_response(data, filters)
|
311 |
+
else:
|
312 |
+
return f"Error fetching data: HTTP {response.status}"
|
313 |
+
|
314 |
+
except Exception as e:
|
315 |
+
return f"Error in CoinGecko tool: {str(e)}"
|
316 |
+
|
317 |
+
def _format_coin_id(self, query: str) -> str:
|
318 |
+
"""Convert common symbols to CoinGecko IDs"""
|
319 |
+
symbol_map = {
|
320 |
+
"btc": "bitcoin",
|
321 |
+
"eth": "ethereum",
|
322 |
+
"usdc": "usd-coin",
|
323 |
+
"usdt": "tether",
|
324 |
+
"bnb": "binancecoin"
|
325 |
+
}
|
326 |
+
return symbol_map.get(query.lower(), query.lower())
|
327 |
+
|
328 |
+
def _process_response(self, data: Dict[str, Any], filters: Optional[Dict[str, Any]] = None) -> str:
|
329 |
+
"""Format CoinGecko response into readable text"""
|
330 |
+
if not data:
|
331 |
+
return "No data found"
|
332 |
+
|
333 |
+
if filters and filters.get("type") == "trending":
|
334 |
+
trending = data.get("coins", [])[:5]
|
335 |
+
result = "🔥 Trending Cryptocurrencies:\n"
|
336 |
+
for i, coin in enumerate(trending, 1):
|
337 |
+
name = coin.get("item", {}).get("name", "Unknown")
|
338 |
+
symbol = coin.get("item", {}).get("symbol", "")
|
339 |
+
result += f"{i}. {name} ({symbol.upper()})\n"
|
340 |
+
return result
|
341 |
+
|
342 |
+
elif filters and filters.get("type") == "market_data":
|
343 |
+
name = data.get("name", "Unknown")
|
344 |
+
symbol = data.get("symbol", "").upper()
|
345 |
+
current_price = data.get("market_data", {}).get("current_price", {}).get("usd", 0)
|
346 |
+
market_cap = data.get("market_data", {}).get("market_cap", {}).get("usd", 0)
|
347 |
+
volume_24h = data.get("market_data", {}).get("total_volume", {}).get("usd", 0)
|
348 |
+
price_change_24h = data.get("market_data", {}).get("price_change_percentage_24h", 0)
|
349 |
+
|
350 |
+
result = f"📊 {name} ({symbol}) Market Data:\n"
|
351 |
+
result += f"💰 Price: ${current_price:,.2f}\n"
|
352 |
+
result += f"📈 24h Change: {price_change_24h:+.2f}%\n"
|
353 |
+
result += f"🏦 Market Cap: ${market_cap:,.0f}\n"
|
354 |
+
result += f"📊 24h Volume: ${volume_24h:,.0f}\n"
|
355 |
+
return result
|
356 |
+
|
357 |
+
else:
|
358 |
+
# Simple price data
|
359 |
+
result = "💰 Price Data:\n"
|
360 |
+
for coin_id, coin_data in data.items():
|
361 |
+
price = coin_data.get("usd", 0)
|
362 |
+
change_24h = coin_data.get("usd_24h_change", 0)
|
363 |
+
volume_24h = coin_data.get("usd_24h_vol", 0)
|
364 |
+
market_cap = coin_data.get("usd_market_cap", 0)
|
365 |
+
|
366 |
+
result += f"🪙 {coin_id.title()}:\n"
|
367 |
+
result += f" 💵 Price: ${price:,.2f}\n"
|
368 |
+
result += f" 📈 24h Change: {change_24h:+.2f}%\n"
|
369 |
+
result += f" 📊 Volume: ${volume_24h:,.0f}\n"
|
370 |
+
result += f" 🏦 Market Cap: ${market_cap:,.0f}\n"
|
371 |
+
|
372 |
+
return result
|
373 |
+
```
|
374 |
+
|
375 |
+
### **Phase 3: LangChain Agent Implementation (Days 5-6)**
|
376 |
+
|
377 |
+
#### Main Research Agent
|
378 |
+
**src/agent/research_agent.py**
|
379 |
+
```python
|
380 |
+
from langchain.agents import AgentExecutor, create_openai_tools_agent
|
381 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
382 |
+
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
|
383 |
+
from langchain.memory import ConversationBufferWindowMemory
|
384 |
+
from typing import List, Dict, Any
|
385 |
+
import asyncio
|
386 |
+
|
387 |
+
from src.tools.coingecko_tool import CoinGeckoTool
|
388 |
+
from src.tools.defillama_tool import DeFiLlamaTool
|
389 |
+
from src.tools.etherscan_tool import EtherscanTool
|
390 |
+
from src.agent.query_planner import QueryPlanner
|
391 |
+
from src.utils.config import config
|
392 |
+
from src.utils.logger import get_logger
|
393 |
+
|
394 |
+
logger = get_logger(__name__)
|
395 |
+
|
396 |
+
class Web3ResearchAgent:
|
397 |
+
def __init__(self):
|
398 |
+
self.llm = ChatGoogleGenerativeAI(
|
399 |
+
model=config.GEMINI_MODEL,
|
400 |
+
google_api_key=config.GEMINI_API_KEY,
|
401 |
+
temperature=0.1,
|
402 |
+
max_tokens=2048
|
403 |
+
)
|
404 |
+
|
405 |
+
self.tools = self._setup_tools()
|
406 |
+
self.query_planner = QueryPlanner(self.llm)
|
407 |
+
self.memory = ConversationBufferWindowMemory(
|
408 |
+
memory_key="chat_history",
|
409 |
+
return_messages=True,
|
410 |
+
k=10
|
411 |
+
)
|
412 |
+
|
413 |
+
self.agent = self._create_agent()
|
414 |
+
self.agent_executor = AgentExecutor(
|
415 |
+
agent=self.agent,
|
416 |
+
tools=self.tools,
|
417 |
+
memory=self.memory,
|
418 |
+
verbose=True,
|
419 |
+
max_iterations=5,
|
420 |
+
handle_parsing_errors=True
|
421 |
+
)
|
422 |
+
|
423 |
+
def _setup_tools(self) -> List:
|
424 |
+
"""Initialize all available tools"""
|
425 |
+
return [
|
426 |
+
CoinGeckoTool(),
|
427 |
+
DeFiLlamaTool(),
|
428 |
+
EtherscanTool(),
|
429 |
+
]
|
430 |
+
|
431 |
+
def _create_agent(self):
|
432 |
+
"""Create the LangChain agent with custom prompt"""
|
433 |
+
system_prompt = """
|
434 |
+
You are an expert Web3 research assistant. Your job is to help users analyze cryptocurrency markets,
|
435 |
+
DeFi protocols, and blockchain data by using available tools to gather accurate information.
|
436 |
+
|
437 |
+
Available tools:
|
438 |
+
- CoinGecko: Get price, volume, market cap data for cryptocurrencies
|
439 |
+
- DeFiLlama: Get DeFi protocol data, TVL, yields information
|
440 |
+
- Etherscan: Get on-chain transaction and address data
|
441 |
+
|
442 |
+
Guidelines:
|
443 |
+
1. Break down complex queries into specific, actionable steps
|
444 |
+
2. Use multiple tools when needed to provide comprehensive analysis
|
445 |
+
3. Always cite your data sources in responses
|
446 |
+
4. Format responses with clear headers, bullet points, and emojis for readability
|
447 |
+
5. If data is unavailable, suggest alternative approaches or mention limitations
|
448 |
+
6. Provide context and explanations for technical terms
|
449 |
+
7. Include relevant charts or visualizations when possible
|
450 |
+
|
451 |
+
When responding:
|
452 |
+
- Start with a brief summary
|
453 |
+
- Present data in organized sections
|
454 |
+
- Include methodology notes
|
455 |
+
- End with key insights or actionable conclusions
|
456 |
+
"""
|
457 |
+
|
458 |
+
prompt = ChatPromptTemplate.from_messages([
|
459 |
+
("system", system_prompt),
|
460 |
+
MessagesPlaceholder("chat_history"),
|
461 |
+
("human", "{input}"),
|
462 |
+
MessagesPlaceholder("agent_scratchpad")
|
463 |
+
])
|
464 |
+
|
465 |
+
return create_openai_tools_agent(
|
466 |
+
llm=self.llm,
|
467 |
+
tools=self.tools,
|
468 |
+
prompt=prompt
|
469 |
+
)
|
470 |
+
|
471 |
+
async def research_query(self, query: str) -> Dict[str, Any]:
|
472 |
+
"""Main entry point for research queries"""
|
473 |
+
try:
|
474 |
+
logger.info(f"Processing query: {query}")
|
475 |
+
|
476 |
+
# Plan the research approach
|
477 |
+
research_plan = await self.query_planner.plan_research(query)
|
478 |
+
logger.info(f"Research plan: {research_plan}")
|
479 |
+
|
480 |
+
# Execute the research
|
481 |
+
result = await self._execute_research(query, research_plan)
|
482 |
+
|
483 |
+
# Format and return response
|
484 |
+
return {
|
485 |
+
"success": True,
|
486 |
+
"query": query,
|
487 |
+
"research_plan": research_plan,
|
488 |
+
"result": result,
|
489 |
+
"sources": self._extract_sources(result),
|
490 |
+
"metadata": {
|
491 |
+
"tools_used": [tool.name for tool in self.tools],
|
492 |
+
"timestamp": self._get_timestamp()
|
493 |
+
}
|
494 |
+
}
|
495 |
+
|
496 |
+
except Exception as e:
|
497 |
+
logger.error(f"Error processing query: {str(e)}")
|
498 |
+
return {
|
499 |
+
"success": False,
|
500 |
+
"query": query,
|
501 |
+
"error": str(e),
|
502 |
+
"metadata": {"timestamp": self._get_timestamp()}
|
503 |
+
}
|
504 |
+
|
505 |
+
async def _execute_research(self, query: str, plan: Dict[str, Any]) -> str:
|
506 |
+
"""Execute the research plan using LangChain agent"""
|
507 |
+
try:
|
508 |
+
# Add planning context to the query
|
509 |
+
enhanced_query = f"""
|
510 |
+
Research Query: {query}
|
511 |
+
|
512 |
+
Research Plan: {plan.get('steps', [])}
|
513 |
+
Priority Focus: {plan.get('priority', 'general analysis')}
|
514 |
+
|
515 |
+
Please execute this research systematically and provide a comprehensive analysis.
|
516 |
+
"""
|
517 |
+
|
518 |
+
response = await asyncio.to_thread(
|
519 |
+
self.agent_executor.invoke,
|
520 |
+
{"input": enhanced_query}
|
521 |
+
)
|
522 |
+
|
523 |
+
return response.get("output", "No response generated")
|
524 |
+
|
525 |
+
except Exception as e:
|
526 |
+
logger.error(f"Error executing research: {str(e)}")
|
527 |
+
return f"Research execution error: {str(e)}"
|
528 |
+
|
529 |
+
def _extract_sources(self, result: str) -> List[str]:
|
530 |
+
"""Extract data sources from the result"""
|
531 |
+
sources = []
|
532 |
+
if "CoinGecko" in result:
|
533 |
+
sources.append("CoinGecko API")
|
534 |
+
if "DeFiLlama" in result:
|
535 |
+
sources.append("DeFiLlama API")
|
536 |
+
if "Etherscan" in result:
|
537 |
+
sources.append("Etherscan API")
|
538 |
+
return sources
|
539 |
+
|
540 |
+
def _get_timestamp(self) -> str:
|
541 |
+
"""Get current timestamp"""
|
542 |
+
from datetime import datetime
|
543 |
+
return datetime.now().isoformat()
|
544 |
+
```
|
545 |
+
|
546 |
+
### **Phase 4: UI & Integration (Days 7-8)**
|
547 |
+
|
548 |
+
#### Main Gradio Application
|
549 |
+
**app.py**
|
550 |
+
```python
|
551 |
+
import gradio as gr
|
552 |
+
import asyncio
|
553 |
+
import json
|
554 |
+
from datetime import datetime
|
555 |
+
from typing import List, Tuple
|
556 |
+
|
557 |
+
from src.agent.research_agent import Web3ResearchAgent
|
558 |
+
from src.api.airaa_integration import AIRAAIntegration
|
559 |
+
from src.visualizations.plotly_charts import create_price_chart, create_volume_chart
|
560 |
+
from src.utils.config import config
|
561 |
+
from src.utils.logger import get_logger
|
562 |
+
|
563 |
+
logger = get_logger(__name__)
|
564 |
+
|
565 |
+
class Web3CoPilotApp:
|
566 |
+
def __init__(self):
|
567 |
+
self.agent = Web3ResearchAgent()
|
568 |
+
self.airaa_integration = AIRAAIntegration()
|
569 |
+
|
570 |
+
def create_interface(self):
|
571 |
+
"""Create the main Gradio interface"""
|
572 |
+
|
573 |
+
# Custom CSS for better styling
|
574 |
+
custom_css = """
|
575 |
+
.container { max-width: 1200px; margin: 0 auto; }
|
576 |
+
.chat-container { height: 600px; }
|
577 |
+
.query-box { font-size: 16px; }
|
578 |
+
.examples-box { background: #f8f9fa; padding: 15px; border-radius: 8px; }
|
579 |
+
"""
|
580 |
+
|
581 |
+
with gr.Blocks(
|
582 |
+
title=config.UI_TITLE,
|
583 |
+
css=custom_css,
|
584 |
+
theme=gr.themes.Soft()
|
585 |
+
) as demo:
|
586 |
+
|
587 |
+
# Header
|
588 |
+
gr.Markdown(f"""
|
589 |
+
# 🚀 {config.UI_TITLE}
|
590 |
+
|
591 |
+
{config.UI_DESCRIPTION}
|
592 |
+
|
593 |
+
**Powered by**: Gemini AI • CoinGecko • DeFiLlama • Etherscan
|
594 |
+
""")
|
595 |
+
|
596 |
+
with gr.Row():
|
597 |
+
with gr.Column(scale=2):
|
598 |
+
# Main Chat Interface
|
599 |
+
chatbot = gr.Chatbot(
|
600 |
+
label="Research Assistant",
|
601 |
+
height=600,
|
602 |
+
show_label=True,
|
603 |
+
container=True,
|
604 |
+
elem_classes=["chat-container"]
|
605 |
+
)
|
606 |
+
|
607 |
+
with gr.Row():
|
608 |
+
query_input = gr.Textbox(
|
609 |
+
placeholder="Ask me about crypto markets, DeFi protocols, or on-chain data...",
|
610 |
+
label="Research Query",
|
611 |
+
lines=2,
|
612 |
+
elem_classes=["query-box"]
|
613 |
+
)
|
614 |
+
submit_btn = gr.Button("🔍 Research", variant="primary")
|
615 |
+
|
616 |
+
# Quick action buttons
|
617 |
+
with gr.Row():
|
618 |
+
clear_btn = gr.Button("🗑️ Clear", variant="secondary")
|
619 |
+
export_btn = gr.Button("📊 Export", variant="secondary")
|
620 |
+
|
621 |
+
with gr.Column(scale=1):
|
622 |
+
# Example queries sidebar
|
623 |
+
gr.Markdown("### 💡 Example Queries")
|
624 |
+
|
625 |
+
examples = [
|
626 |
+
"What's the current price and 24h change for Bitcoin?",
|
627 |
+
"Show me top DeFi protocols by TVL",
|
628 |
+
"Which tokens had highest volume yesterday?",
|
629 |
+
"Compare Ethereum vs Solana market metrics",
|
630 |
+
"What are the trending cryptocurrencies today?"
|
631 |
+
]
|
632 |
+
|
633 |
+
for example in examples:
|
634 |
+
example_btn = gr.Button(example, size="sm")
|
635 |
+
example_btn.click(
|
636 |
+
lambda x=example: x,
|
637 |
+
outputs=query_input
|
638 |
+
)
|
639 |
+
|
640 |
+
# Data visualization area
|
641 |
+
gr.Markdown("### 📈 Visualizations")
|
642 |
+
chart_output = gr.Plot(label="Charts")
|
643 |
+
|
644 |
+
# Export options
|
645 |
+
gr.Markdown("### 📤 Export Options")
|
646 |
+
export_format = gr.Radio(
|
647 |
+
choices=["JSON", "CSV", "PDF"],
|
648 |
+
value="JSON",
|
649 |
+
label="Format"
|
650 |
+
)
|
651 |
+
|
652 |
+
# Chat functionality
|
653 |
+
def respond(message: str, history: List[Tuple[str, str]]):
|
654 |
+
"""Process user message and generate response"""
|
655 |
+
if not message.strip():
|
656 |
+
return history, ""
|
657 |
+
|
658 |
+
try:
|
659 |
+
# Show loading message
|
660 |
+
history.append((message, "🔍 Researching... Please wait."))
|
661 |
+
yield history, ""
|
662 |
+
|
663 |
+
# Process the query
|
664 |
+
result = asyncio.run(self.agent.research_query(message))
|
665 |
+
|
666 |
+
if result["success"]:
|
667 |
+
response = result["result"]
|
668 |
+
|
669 |
+
# Add metadata footer
|
670 |
+
sources = ", ".join(result["sources"])
|
671 |
+
response += f"\n\n---\n📊 **Sources**: {sources}"
|
672 |
+
response += f"\n⏰ **Generated**: {datetime.now().strftime('%H:%M:%S')}"
|
673 |
+
|
674 |
+
# Send to AIRAA if configured
|
675 |
+
if config.AIRAA_WEBHOOK_URL:
|
676 |
+
asyncio.run(self.airaa_integration.send_research_data(result))
|
677 |
+
|
678 |
+
else:
|
679 |
+
response = f"❌ Error: {result.get('error', 'Unknown error occurred')}"
|
680 |
+
|
681 |
+
# Update history
|
682 |
+
history[-1] = (message, response)
|
683 |
+
|
684 |
+
except Exception as e:
|
685 |
+
logger.error(f"Error in respond: {str(e)}")
|
686 |
+
history[-1] = (message, f"❌ System error: {str(e)}")
|
687 |
+
|
688 |
+
yield history, ""
|
689 |
+
|
690 |
+
def clear_chat():
|
691 |
+
"""Clear chat history"""
|
692 |
+
return [], ""
|
693 |
+
|
694 |
+
def export_conversation(history: List[Tuple[str, str]], format_type: str):
|
695 |
+
"""Export conversation in selected format"""
|
696 |
+
try:
|
697 |
+
if format_type == "JSON":
|
698 |
+
data = {
|
699 |
+
"conversation": [{"query": q, "response": r} for q, r in history],
|
700 |
+
"exported_at": datetime.now().isoformat()
|
701 |
+
}
|
702 |
+
return json.dumps(data, indent=2)
|
703 |
+
|
704 |
+
elif format_type == "CSV":
|
705 |
+
import csv
|
706 |
+
import io
|
707 |
+
output = io.StringIO()
|
708 |
+
writer = csv.writer(output)
|
709 |
+
writer.writerow(["Query", "Response", "Timestamp"])
|
710 |
+
for q, r in history:
|
711 |
+
writer.writerow([q, r, datetime.now().isoformat()])
|
712 |
+
return output.getvalue()
|
713 |
+
|
714 |
+
else: # PDF
|
715 |
+
return "PDF export not implemented yet"
|
716 |
+
|
717 |
+
except Exception as e:
|
718 |
+
return f"Export error: {str(e)}"
|
719 |
+
|
720 |
+
# Event handlers
|
721 |
+
submit_btn.click(
|
722 |
+
respond,
|
723 |
+
inputs=[query_input, chatbot],
|
724 |
+
outputs=[chatbot, query_input]
|
725 |
+
)
|
726 |
+
|
727 |
+
query_input.submit(
|
728 |
+
respond,
|
729 |
+
inputs=[query_input, chatbot],
|
730 |
+
outputs=[chatbot, query_input]
|
731 |
+
)
|
732 |
+
|
733 |
+
clear_btn.click(
|
734 |
+
clear_chat,
|
735 |
+
outputs=[chatbot, query_input]
|
736 |
+
)
|
737 |
+
|
738 |
+
export_btn.click(
|
739 |
+
export_conversation,
|
740 |
+
inputs=[chatbot, export_format],
|
741 |
+
outputs=gr.Textbox(label="Exported Data")
|
742 |
+
)
|
743 |
+
|
744 |
+
return demo
|
745 |
+
|
746 |
+
if __name__ == "__main__":
|
747 |
+
app = Web3CoPilotApp()
|
748 |
+
interface = app.create_interface()
|
749 |
+
|
750 |
+
interface.launch(
|
751 |
+
server_name="0.0.0.0",
|
752 |
+
server_port=7860,
|
753 |
+
share=True,
|
754 |
+
show_api=True
|
755 |
+
)
|
756 |
+
```
|
757 |
+
|
758 |
+
### **Phase 5: AIRAA Integration & Deployment**
|
759 |
+
|
760 |
+
#### AIRAA Integration Module
|
761 |
+
**src/api/airaa_integration.py**
|
762 |
+
```python
|
763 |
+
import aiohttp
|
764 |
+
import json
|
765 |
+
from typing import Dict, Any, Optional
|
766 |
+
from src.utils.config import config
|
767 |
+
from src.utils.logger import get_logger
|
768 |
+
|
769 |
+
logger = get_logger(__name__)
|
770 |
+
|
771 |
+
class AIRAAIntegration:
|
772 |
+
"""Handle integration with AIRAA platform"""
|
773 |
+
|
774 |
+
def __init__(self):
|
775 |
+
self.webhook_url = config.AIRAA_WEBHOOK_URL
|
776 |
+
self.api_key = config.AIRAA_API_KEY
|
777 |
+
self.enabled = bool(self.webhook_url)
|
778 |
+
|
779 |
+
async def send_research_data(self, research_result: Dict[str, Any]) -> bool:
|
780 |
+
"""Send research data to AIRAA webhook"""
|
781 |
+
if not self.enabled:
|
782 |
+
logger.info("AIRAA integration not configured, skipping")
|
783 |
+
return False
|
784 |
+
|
785 |
+
try:
|
786 |
+
payload = self._format_for_airaa(research_result)
|
787 |
+
|
788 |
+
headers = {
|
789 |
+
"Content-Type": "application/json",
|
790 |
+
"User-Agent": "Web3-Research-Copilot/1.0"
|
791 |
+
}
|
792 |
+
|
793 |
+
if self.api_key:
|
794 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
795 |
+
|
796 |
+
async with aiohttp.ClientSession() as session:
|
797 |
+
async with session.post(
|
798 |
+
self.webhook_url,
|
799 |
+
json=payload,
|
800 |
+
headers=headers,
|
801 |
+
timeout=aiohttp.ClientTimeout(total=30)
|
802 |
+
) as response:
|
803 |
+
|
804 |
+
if response.status == 200:
|
805 |
+
logger.info("Successfully sent data to AIRAA")
|
806 |
+
return True
|
807 |
+
else:
|
808 |
+
logger.warning(f"AIRAA webhook returned {response.status}")
|
809 |
+
return False
|
810 |
+
|
811 |
+
except Exception as e:
|
812 |
+
logger.error(f"Failed to send data to AIRAA: {str(e)}")
|
813 |
+
return False
|
814 |
+
|
815 |
+
def _format_for_airaa(self, research_result: Dict[str, Any]) -> Dict[str, Any]:
|
816 |
+
"""Format research result for AIRAA consumption"""
|
817 |
+
return {
|
818 |
+
"source": "web3-research-copilot",
|
819 |
+
"timestamp": research_result["metadata"]["timestamp"],
|
820 |
+
"query": research_result["query"],
|
821 |
+
"research_plan": research_result.get("research_plan"),
|
822 |
+
"findings": research_result["result"],
|
823 |
+
"data_sources": research_result["sources"],
|
824 |
+
"confidence_score": self._calculate_confidence(research_result),
|
825 |
+
"tags": self._extract_tags(research_result["query"]),
|
826 |
+
"structured_data": self._extract_structured_data(research_result["result"])
|
827 |
+
}
|
828 |
+
|
829 |
+
def _calculate_confidence(self, result: Dict[str, Any]) -> float:
|
830 |
+
"""Calculate confidence score based on data sources and completeness"""
|
831 |
+
base_score = 0.7
|
832 |
+
|
833 |
+
# Boost for multiple sources
|
834 |
+
source_count = len(result.get("sources", []))
|
835 |
+
source_boost = min(source_count * 0.1, 0.3)
|
836 |
+
|
837 |
+
# Reduce for errors
|
838 |
+
error_penalty = 0.3 if not result.get("success", True) else 0
|
839 |
+
|
840 |
+
return max(0.0, min(1.0, base_score + source_boost - error_penalty))
|
841 |
+
|
842 |
+
def _extract_tags(self, query: str) -> List[str]:
|
843 |
+
"""Extract relevant tags from query"""
|
844 |
+
tags = []
|
845 |
+
query_lower = query.lower()
|
846 |
+
|
847 |
+
# Asset tags
|
848 |
+
if any(word in query_lower for word in ["bitcoin", "btc"]):
|
849 |
+
tags.append("bitcoin")
|
850 |
+
if any(word in query_lower for word in ["ethereum", "eth"]):
|
851 |
+
tags.append("ethereum")
|
852 |
+
|
853 |
+
# Category tags
|
854 |
+
if any(word in query_lower for word in ["defi", "defillama"]):
|
855 |
+
tags.append("defi")
|
856 |
+
if any(word in query_lower for word in ["price", "market"]):
|
857 |
+
tags.append("market-analysis")
|
858 |
+
if any(word in query_lower for word in ["volume", "trading"]):
|
859 |
+
tags.append("trading-volume")
|
860 |
+
|
861 |
+
return tags
|
862 |
+
|
863 |
+
def _extract_structured_data(self, result_text: str) -> Dict[str, Any]:
|
864 |
+
"""Extract structured data from result text"""
|
865 |
+
structured = {}
|
866 |
+
|
867 |
+
# Extract price data (simple regex matching)
|
868 |
+
import re
|
869 |
+
|
870 |
+
price_matches = re.findall(r'\$([0-9,]+\.?[0-9]*)', result_text)
|
871 |
+
if price_matches:
|
872 |
+
structured["prices"] = [float(p.replace(',', '')) for p in price_matches[:5]]
|
873 |
+
|
874 |
+
percentage_matches = re.findall(r'([+-]?[0-9]+\.?[0-9]*)%', result_text)
|
875 |
+
if percentage_matches:
|
876 |
+
structured["percentages"] = [float(p) for p in percentage_matches[:5]]
|
877 |
+
|
878 |
+
return structured
|
879 |
+
```
|
880 |
+
|
881 |
+
## 🚀 **Deployment Configuration**
|
882 |
+
|
883 |
+
### **Hugging Face Spaces Configuration**
|
884 |
+
|
885 |
+
**deployment/huggingface_spaces/spaces_config.yml**
|
886 |
+
```yaml
|
887 |
+
title: "Web3 Research Co-Pilot"
|
888 |
+
emoji: "🚀"
|
889 |
+
colorFrom: "blue"
|
890 |
+
colorTo: "purple"
|
891 |
+
sdk: "gradio"
|
892 |
+
sdk_version: "4.15.0"
|
893 |
+
app_file: "app.py"
|
894 |
+
pinned: false
|
895 |
+
license: "mit"
|
896 |
+
|
897 |
+
# Environment variables needed
|
898 |
+
secrets:
|
899 |
+
- GEMINI_API_KEY
|
900 |
+
- ETHERSCAN_API_KEY
|
901 |
+
- AIRAA_WEBHOOK_URL
|
902 |
+
- AIRAA_API_KEY
|
903 |
+
|
904 |
+
# Resource requirements
|
905 |
+
hardware: "cpu-basic" # Free tier
|
906 |
+
```
|
907 |
+
|
908 |
+
### **Docker Configuration**
|
909 |
+
**Dockerfile**
|
910 |
+
```dockerfile
|
911 |
+
FROM python:3.11-slim
|
912 |
+
|
913 |
+
WORKDIR /app
|
914 |
+
|
915 |
+
# Install system dependencies
|
916 |
+
RUN apt-get update && apt-get install -y \
|
917 |
+
git \
|
918 |
+
&& rm -rf /var/lib/apt/lists/*
|
919 |
+
|
920 |
+
# Copy requirements and install Python dependencies
|
921 |
+
COPY requirements.txt .
|
922 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
923 |
+
|
924 |
+
# Copy application code
|
925 |
+
COPY . .
|
926 |
+
|
927 |
+
# Expose port
|
928 |
+
EXPOSE 7860
|
929 |
+
|
930 |
+
# Set environment variables
|
931 |
+
ENV PYTHONPATH=/app
|
932 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0
|
933 |
+
ENV GRADIO_SERVER_PORT=7860
|
934 |
+
|
935 |
+
# Run the application
|
936 |
+
CMD ["python", "app.py"]
|
937 |
+
```
|
938 |
+
|
939 |
+
## 📊 **Testing Strategy**
|
940 |
+
|
941 |
+
### **Unit Tests**
|
942 |
+
```python
|
943 |
+
# tests/test_tools/test_coingecko.py
|
944 |
+
import pytest
|
945 |
+
import asyncio
|
946 |
+
from src.tools.coingecko_tool import CoinGeckoTool
|
947 |
+
|
948 |
+
@pytest.fixture
|
949 |
+
def coingecko_tool():
|
950 |
+
return CoinGeckoTool()
|
951 |
+
|
952 |
+
@pytest.mark.asyncio
|
953 |
+
async def test_bitcoin_price_fetch(coingecko_tool):
|
954 |
+
result = await coingecko_tool._arun("bitcoin")
|
955 |
+
assert "Price:" in result
|
956 |
+
assert "bitcoin" in result.lower()
|
957 |
+
|
958 |
+
@pytest.mark.asyncio
|
959 |
+
async def test_trending_coins(coingecko_tool):
|
960 |
+
result = await coingecko_tool._arun("trending", {"type": "trending"})
|
961 |
+
assert "Trending" in result
|
962 |
+
```
|
963 |
+
|
964 |
+
## 📅 **8-Day Development Timeline**
|
965 |
+
|
966 |
+
### **Detailed Daily Schedule**
|
967 |
+
|
968 |
+
**Days 1-2: Foundation**
|
969 |
+
- ✅ Project structure setup
|
970 |
+
- ✅ Configuration management
|
971 |
+
- ✅ Base tool architecture
|
972 |
+
- ✅ Logging and utilities
|
973 |
+
|
974 |
+
**Days 3-4: Core Tools**
|
975 |
+
- ✅ CoinGecko integration (price data)
|
976 |
+
- ✅ DeFiLlama integration (DeFi data)
|
977 |
+
- ✅ Etherscan integration (on-chain data)
|
978 |
+
- ✅ Rate limiting and error handling
|
979 |
+
|
980 |
+
**Days 5-6: AI Agent**
|
981 |
+
- ✅ LangChain agent setup
|
982 |
+
- ✅ Query planning logic
|
983 |
+
- ✅ Memory management
|
984 |
+
- ✅ Response formatting
|
985 |
+
|
986 |
+
**Days 7-8: UI & Deployment**
|
987 |
+
- ✅ Gradio interface
|
988 |
+
- ✅ AIRAA integration
|
989 |
+
- ✅ HuggingFace Spaces deployment
|
990 |
+
- ✅ Testing and documentation
|
991 |
+
|
992 |
+
This comprehensive plan provides a production-ready Web3 research co-pilot that integrates seamlessly with AIRAA's platform while utilizing only free resources and APIs.
|
pyproject.toml
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "repl-nix-workspace"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
requires-python = ">=3.11"
|
6 |
+
dependencies = [
|
7 |
+
"aiohttp>=3.12.15",
|
8 |
+
"asyncio-throttle>=1.0.2",
|
9 |
+
"diskcache>=5.6.3",
|
10 |
+
"google-genai>=1.29.0",
|
11 |
+
"gradio>=5.42.0",
|
12 |
+
"langchain>=0.3.27",
|
13 |
+
"langchain-community>=0.3.27",
|
14 |
+
"langchain-google-genai>=2.1.9",
|
15 |
+
"numpy>=2.3.2",
|
16 |
+
"pandas>=2.3.1",
|
17 |
+
"plotly>=6.2.0",
|
18 |
+
"pydantic>=2.11.7",
|
19 |
+
"python-dateutil>=2.9.0.post0",
|
20 |
+
"python-dotenv>=1.1.1",
|
21 |
+
"streamlit>=1.48.0",
|
22 |
+
"tenacity>=9.1.2",
|
23 |
+
"trafilatura>=2.0.0",
|
24 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiohttp==3.10.11
|
2 |
+
gradio==5.8.0
|
3 |
+
google-generativeai==0.8.3
|
4 |
+
plotly==5.24.1
|
5 |
+
pandas==2.2.3
|
6 |
+
python-dotenv==1.0.1
|
7 |
+
asyncio-throttle==1.0.2
|
src/__init__.py
ADDED
File without changes
|
src/__pycache__/__init__.cpython-311.pyc
ADDED
Binary file (147 Bytes). View file
|
|
src/__pycache__/api_clients.cpython-311.pyc
ADDED
Binary file (11.9 kB). View file
|
|
src/__pycache__/cache_manager.cpython-311.pyc
ADDED
Binary file (3.19 kB). View file
|
|
src/__pycache__/config.cpython-311.pyc
ADDED
Binary file (1.36 kB). View file
|
|
src/__pycache__/defillama_client.cpython-311.pyc
ADDED
Binary file (5.63 kB). View file
|
|
src/__pycache__/enhanced_agent.cpython-311.pyc
ADDED
Binary file (19 kB). View file
|
|
src/__pycache__/news_aggregator.cpython-311.pyc
ADDED
Binary file (6.04 kB). View file
|
|
src/__pycache__/portfolio_analyzer.cpython-311.pyc
ADDED
Binary file (11.8 kB). View file
|
|
src/__pycache__/research_agent.cpython-311.pyc
ADDED
Binary file (12.4 kB). View file
|
|
src/__pycache__/visualizations.cpython-311.pyc
ADDED
Binary file (11.8 kB). View file
|
|
src/api_clients.py
ADDED
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import aiohttp
|
2 |
+
import asyncio
|
3 |
+
import time
|
4 |
+
from typing import Dict, Any, Optional, List
|
5 |
+
from src.config import config
|
6 |
+
import json
|
7 |
+
|
8 |
+
class RateLimiter:
|
9 |
+
def __init__(self, delay: float):
|
10 |
+
self.delay = delay
|
11 |
+
self.last_call = 0
|
12 |
+
|
13 |
+
async def acquire(self):
|
14 |
+
now = time.time()
|
15 |
+
elapsed = now - self.last_call
|
16 |
+
if elapsed < self.delay:
|
17 |
+
await asyncio.sleep(self.delay - elapsed)
|
18 |
+
self.last_call = time.time()
|
19 |
+
|
20 |
+
class CoinGeckoClient:
|
21 |
+
def __init__(self):
|
22 |
+
self.rate_limiter = RateLimiter(config.RATE_LIMIT_DELAY)
|
23 |
+
self.session = None
|
24 |
+
|
25 |
+
async def get_session(self):
|
26 |
+
if self.session is None:
|
27 |
+
timeout = aiohttp.ClientTimeout(total=config.REQUEST_TIMEOUT)
|
28 |
+
self.session = aiohttp.ClientSession(timeout=timeout)
|
29 |
+
return self.session
|
30 |
+
|
31 |
+
async def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
32 |
+
await self.rate_limiter.acquire()
|
33 |
+
|
34 |
+
url = f"{config.COINGECKO_BASE_URL}/{endpoint}"
|
35 |
+
if params is None:
|
36 |
+
params = {}
|
37 |
+
|
38 |
+
if config.COINGECKO_API_KEY:
|
39 |
+
params["x_cg_demo_api_key"] = config.COINGECKO_API_KEY
|
40 |
+
|
41 |
+
session = await self.get_session()
|
42 |
+
|
43 |
+
for attempt in range(config.MAX_RETRIES):
|
44 |
+
try:
|
45 |
+
async with session.get(url, params=params) as response:
|
46 |
+
if response.status == 200:
|
47 |
+
return await response.json()
|
48 |
+
elif response.status == 429:
|
49 |
+
await asyncio.sleep(2 ** attempt)
|
50 |
+
continue
|
51 |
+
else:
|
52 |
+
raise Exception(f"API error: {response.status}")
|
53 |
+
except asyncio.TimeoutError:
|
54 |
+
if attempt == config.MAX_RETRIES - 1:
|
55 |
+
raise Exception("Request timeout")
|
56 |
+
await asyncio.sleep(1)
|
57 |
+
|
58 |
+
raise Exception("Max retries exceeded")
|
59 |
+
|
60 |
+
async def get_price(self, coin_ids: str, vs_currencies: str = "usd") -> Dict[str, Any]:
|
61 |
+
params = {
|
62 |
+
"ids": coin_ids,
|
63 |
+
"vs_currencies": vs_currencies,
|
64 |
+
"include_24hr_change": "true",
|
65 |
+
"include_24hr_vol": "true",
|
66 |
+
"include_market_cap": "true"
|
67 |
+
}
|
68 |
+
return await self._make_request("simple/price", params)
|
69 |
+
|
70 |
+
async def get_trending(self) -> Dict[str, Any]:
|
71 |
+
return await self._make_request("search/trending")
|
72 |
+
|
73 |
+
async def get_global_data(self) -> Dict[str, Any]:
|
74 |
+
return await self._make_request("global")
|
75 |
+
|
76 |
+
async def get_coin_data(self, coin_id: str) -> Dict[str, Any]:
|
77 |
+
params = {"localization": "false", "tickers": "false", "community_data": "false"}
|
78 |
+
return await self._make_request(f"coins/{coin_id}", params)
|
79 |
+
|
80 |
+
async def get_market_data(self, vs_currency: str = "usd", per_page: int = 10) -> Dict[str, Any]:
|
81 |
+
params = {
|
82 |
+
"vs_currency": vs_currency,
|
83 |
+
"order": "market_cap_desc",
|
84 |
+
"per_page": per_page,
|
85 |
+
"page": 1,
|
86 |
+
"sparkline": "false"
|
87 |
+
}
|
88 |
+
return await self._make_request("coins/markets", params)
|
89 |
+
|
90 |
+
async def get_price_history(self, coin_id: str, days: int = 7) -> Dict[str, Any]:
|
91 |
+
params = {"vs_currency": "usd", "days": days}
|
92 |
+
return await self._make_request(f"coins/{coin_id}/market_chart", params)
|
93 |
+
|
94 |
+
async def close(self):
|
95 |
+
if self.session:
|
96 |
+
await self.session.close()
|
97 |
+
|
98 |
+
class CryptoCompareClient:
|
99 |
+
def __init__(self):
|
100 |
+
self.rate_limiter = RateLimiter(config.RATE_LIMIT_DELAY)
|
101 |
+
self.session = None
|
102 |
+
|
103 |
+
async def get_session(self):
|
104 |
+
if self.session is None:
|
105 |
+
timeout = aiohttp.ClientTimeout(total=config.REQUEST_TIMEOUT)
|
106 |
+
self.session = aiohttp.ClientSession(timeout=timeout)
|
107 |
+
return self.session
|
108 |
+
|
109 |
+
async def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
110 |
+
await self.rate_limiter.acquire()
|
111 |
+
|
112 |
+
url = f"{config.CRYPTOCOMPARE_BASE_URL}/{endpoint}"
|
113 |
+
if params is None:
|
114 |
+
params = {}
|
115 |
+
|
116 |
+
if config.CRYPTOCOMPARE_API_KEY:
|
117 |
+
params["api_key"] = config.CRYPTOCOMPARE_API_KEY
|
118 |
+
|
119 |
+
session = await self.get_session()
|
120 |
+
|
121 |
+
for attempt in range(config.MAX_RETRIES):
|
122 |
+
try:
|
123 |
+
async with session.get(url, params=params) as response:
|
124 |
+
if response.status == 200:
|
125 |
+
data = await response.json()
|
126 |
+
if data.get("Response") == "Error":
|
127 |
+
raise Exception(data.get("Message", "API error"))
|
128 |
+
return data
|
129 |
+
elif response.status == 429:
|
130 |
+
await asyncio.sleep(2 ** attempt)
|
131 |
+
continue
|
132 |
+
else:
|
133 |
+
raise Exception(f"API error: {response.status}")
|
134 |
+
except asyncio.TimeoutError:
|
135 |
+
if attempt == config.MAX_RETRIES - 1:
|
136 |
+
raise Exception("Request timeout")
|
137 |
+
await asyncio.sleep(1)
|
138 |
+
|
139 |
+
raise Exception("Max retries exceeded")
|
140 |
+
|
141 |
+
async def get_price_multi(self, fsyms: str, tsyms: str = "USD") -> Dict[str, Any]:
|
142 |
+
params = {"fsyms": fsyms, "tsyms": tsyms}
|
143 |
+
return await self._make_request("pricemulti", params)
|
144 |
+
|
145 |
+
async def get_social_data(self, coin_symbol: str) -> Dict[str, Any]:
|
146 |
+
params = {"coinSymbol": coin_symbol}
|
147 |
+
return await self._make_request("social/coin/latest", params)
|
148 |
+
|
149 |
+
async def get_news(self, categories: str = "blockchain") -> Dict[str, Any]:
|
150 |
+
params = {"categories": categories}
|
151 |
+
return await self._make_request("news/", params)
|
152 |
+
|
153 |
+
async def close(self):
|
154 |
+
if self.session:
|
155 |
+
await self.session.close()
|
156 |
+
|
157 |
+
coingecko_client = CoinGeckoClient()
|
158 |
+
cryptocompare_client = CryptoCompareClient()
|
src/cache_manager.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
from typing import Any, Optional, Dict
|
3 |
+
from src.config import config
|
4 |
+
|
5 |
+
class CacheManager:
|
6 |
+
def __init__(self, default_ttl: Optional[int] = None):
|
7 |
+
self.cache: Dict[str, Dict[str, Any]] = {}
|
8 |
+
self.default_ttl = default_ttl or config.CACHE_TTL
|
9 |
+
|
10 |
+
def get(self, key: str) -> Optional[Any]:
|
11 |
+
if key not in self.cache:
|
12 |
+
return None
|
13 |
+
|
14 |
+
entry = self.cache[key]
|
15 |
+
if time.time() > entry["expires_at"]:
|
16 |
+
del self.cache[key]
|
17 |
+
return None
|
18 |
+
|
19 |
+
return entry["data"]
|
20 |
+
|
21 |
+
def set(self, key: str, data: Any, ttl: Optional[int] = None) -> None:
|
22 |
+
expires_at = time.time() + (ttl or self.default_ttl)
|
23 |
+
self.cache[key] = {
|
24 |
+
"data": data,
|
25 |
+
"expires_at": expires_at
|
26 |
+
}
|
27 |
+
|
28 |
+
def delete(self, key: str) -> bool:
|
29 |
+
return self.cache.pop(key, None) is not None
|
30 |
+
|
31 |
+
def clear(self) -> None:
|
32 |
+
self.cache.clear()
|
33 |
+
|
34 |
+
def cleanup_expired(self) -> int:
|
35 |
+
current_time = time.time()
|
36 |
+
expired_keys = [
|
37 |
+
key for key, entry in self.cache.items()
|
38 |
+
if current_time > entry["expires_at"]
|
39 |
+
]
|
40 |
+
|
41 |
+
for key in expired_keys:
|
42 |
+
del self.cache[key]
|
43 |
+
|
44 |
+
return len(expired_keys)
|
45 |
+
|
46 |
+
def size(self) -> int:
|
47 |
+
return len(self.cache)
|
48 |
+
|
49 |
+
cache_manager = CacheManager()
|
src/config.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from dataclasses import dataclass
|
3 |
+
from typing import Optional
|
4 |
+
|
5 |
+
@dataclass
|
6 |
+
class Config:
|
7 |
+
GEMINI_API_KEY: str = os.getenv("GEMINI_API_KEY", "")
|
8 |
+
COINGECKO_API_KEY: Optional[str] = os.getenv("COINGECKO_API_KEY")
|
9 |
+
CRYPTOCOMPARE_API_KEY: Optional[str] = os.getenv("CRYPTOCOMPARE_API_KEY")
|
10 |
+
|
11 |
+
COINGECKO_BASE_URL: str = "https://api.coingecko.com/api/v3"
|
12 |
+
CRYPTOCOMPARE_BASE_URL: str = "https://min-api.cryptocompare.com/data"
|
13 |
+
|
14 |
+
CACHE_TTL: int = 300
|
15 |
+
RATE_LIMIT_DELAY: float = 2.0
|
16 |
+
MAX_RETRIES: int = 3
|
17 |
+
REQUEST_TIMEOUT: int = 30
|
18 |
+
|
19 |
+
config = Config()
|
src/defillama_client.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import aiohttp
|
2 |
+
import asyncio
|
3 |
+
from typing import Dict, Any, List, Optional
|
4 |
+
from src.config import config
|
5 |
+
|
6 |
+
class DeFiLlamaClient:
|
7 |
+
def __init__(self):
|
8 |
+
self.base_url = "https://api.llama.fi"
|
9 |
+
self.session = None
|
10 |
+
self.rate_limiter = None
|
11 |
+
|
12 |
+
async def get_session(self):
|
13 |
+
if self.session is None:
|
14 |
+
timeout = aiohttp.ClientTimeout(total=30)
|
15 |
+
self.session = aiohttp.ClientSession(timeout=timeout)
|
16 |
+
return self.session
|
17 |
+
|
18 |
+
async def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
19 |
+
url = f"{self.base_url}/{endpoint}"
|
20 |
+
session = await self.get_session()
|
21 |
+
|
22 |
+
for attempt in range(3):
|
23 |
+
try:
|
24 |
+
async with session.get(url, params=params) as response:
|
25 |
+
if response.status == 200:
|
26 |
+
return await response.json()
|
27 |
+
elif response.status == 429:
|
28 |
+
await asyncio.sleep(2 ** attempt)
|
29 |
+
continue
|
30 |
+
else:
|
31 |
+
raise Exception(f"API error: {response.status}")
|
32 |
+
except Exception as e:
|
33 |
+
if attempt == 2:
|
34 |
+
raise e
|
35 |
+
await asyncio.sleep(1)
|
36 |
+
|
37 |
+
async def get_protocols(self) -> List[Dict[str, Any]]:
|
38 |
+
return await self._make_request("protocols")
|
39 |
+
|
40 |
+
async def get_protocol_data(self, protocol: str) -> Dict[str, Any]:
|
41 |
+
return await self._make_request(f"protocol/{protocol}")
|
42 |
+
|
43 |
+
async def get_tvl_data(self) -> Dict[str, Any]:
|
44 |
+
return await self._make_request("v2/historicalChainTvl")
|
45 |
+
|
46 |
+
async def get_chain_tvl(self, chain: str) -> Dict[str, Any]:
|
47 |
+
return await self._make_request(f"v2/historicalChainTvl/{chain}")
|
48 |
+
|
49 |
+
async def get_yields(self) -> List[Dict[str, Any]]:
|
50 |
+
return await self._make_request("pools")
|
51 |
+
|
52 |
+
async def get_bridges(self) -> List[Dict[str, Any]]:
|
53 |
+
return await self._make_request("bridges")
|
54 |
+
|
55 |
+
async def get_dex_volume(self) -> Dict[str, Any]:
|
56 |
+
return await self._make_request("overview/dexs")
|
57 |
+
|
58 |
+
async def close(self):
|
59 |
+
if self.session:
|
60 |
+
await self.session.close()
|
61 |
+
|
62 |
+
defillama_client = DeFiLlamaClient()
|
src/enhanced_agent.py
ADDED
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import google.generativeai as genai
|
2 |
+
import json
|
3 |
+
import asyncio
|
4 |
+
from typing import Dict, Any, List, Optional
|
5 |
+
from src.api_clients import coingecko_client, cryptocompare_client
|
6 |
+
from src.defillama_client import defillama_client
|
7 |
+
from src.news_aggregator import news_aggregator
|
8 |
+
from src.cache_manager import cache_manager
|
9 |
+
from src.config import config
|
10 |
+
|
11 |
+
class EnhancedResearchAgent:
|
12 |
+
def __init__(self):
|
13 |
+
if config.GEMINI_API_KEY:
|
14 |
+
genai.configure(api_key=config.GEMINI_API_KEY)
|
15 |
+
self.model = genai.GenerativeModel('gemini-1.5-flash')
|
16 |
+
else:
|
17 |
+
self.model = None
|
18 |
+
|
19 |
+
self.symbol_map = {
|
20 |
+
"btc": "bitcoin", "eth": "ethereum", "sol": "solana", "ada": "cardano",
|
21 |
+
"dot": "polkadot", "bnb": "binancecoin", "usdc": "usd-coin",
|
22 |
+
"usdt": "tether", "xrp": "ripple", "avax": "avalanche-2",
|
23 |
+
"link": "chainlink", "matic": "matic-network", "uni": "uniswap",
|
24 |
+
"atom": "cosmos", "near": "near", "icp": "internet-computer",
|
25 |
+
"ftm": "fantom", "algo": "algorand", "xlm": "stellar"
|
26 |
+
}
|
27 |
+
|
28 |
+
def _format_coin_id(self, symbol: str) -> str:
|
29 |
+
return self.symbol_map.get(symbol.lower(), symbol.lower())
|
30 |
+
|
31 |
+
async def get_comprehensive_market_data(self) -> Dict[str, Any]:
|
32 |
+
cache_key = "comprehensive_market"
|
33 |
+
cached = cache_manager.get(cache_key)
|
34 |
+
if cached:
|
35 |
+
return cached
|
36 |
+
|
37 |
+
try:
|
38 |
+
tasks = [
|
39 |
+
coingecko_client.get_market_data(per_page=50),
|
40 |
+
coingecko_client.get_global_data(),
|
41 |
+
coingecko_client.get_trending(),
|
42 |
+
defillama_client.get_protocols(),
|
43 |
+
defillama_client.get_tvl_data(),
|
44 |
+
news_aggregator.get_crypto_news(5)
|
45 |
+
]
|
46 |
+
|
47 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
48 |
+
|
49 |
+
data = {}
|
50 |
+
for i, result in enumerate(results):
|
51 |
+
if not isinstance(result, Exception):
|
52 |
+
if i == 0: data["market_data"] = result
|
53 |
+
elif i == 1: data["global_data"] = result
|
54 |
+
elif i == 2: data["trending"] = result
|
55 |
+
elif i == 3:
|
56 |
+
data["defi_protocols"] = result[:20] if isinstance(result, list) and result else []
|
57 |
+
elif i == 4: data["tvl_data"] = result
|
58 |
+
elif i == 5: data["news"] = result
|
59 |
+
|
60 |
+
cache_manager.set(cache_key, data, 180)
|
61 |
+
return data
|
62 |
+
|
63 |
+
except Exception as e:
|
64 |
+
raise Exception(f"Failed to fetch comprehensive market data: {str(e)}")
|
65 |
+
|
66 |
+
async def get_defi_analysis(self, protocol: Optional[str] = None) -> Dict[str, Any]:
|
67 |
+
cache_key = f"defi_analysis_{protocol or 'overview'}"
|
68 |
+
cached = cache_manager.get(cache_key)
|
69 |
+
if cached:
|
70 |
+
return cached
|
71 |
+
|
72 |
+
try:
|
73 |
+
if protocol:
|
74 |
+
data = await defillama_client.get_protocol_data(protocol)
|
75 |
+
else:
|
76 |
+
protocols = await defillama_client.get_protocols()
|
77 |
+
tvl_data = await defillama_client.get_tvl_data()
|
78 |
+
yields_data = await defillama_client.get_yields()
|
79 |
+
|
80 |
+
data = {
|
81 |
+
"top_protocols": protocols[:20] if isinstance(protocols, list) and protocols else [],
|
82 |
+
"tvl_overview": tvl_data,
|
83 |
+
"top_yields": yields_data[:10] if isinstance(yields_data, list) and yields_data else []
|
84 |
+
}
|
85 |
+
|
86 |
+
cache_manager.set(cache_key, data, 300)
|
87 |
+
return data
|
88 |
+
|
89 |
+
except Exception as e:
|
90 |
+
raise Exception(f"Failed to get DeFi analysis: {str(e)}")
|
91 |
+
|
92 |
+
async def get_price_history(self, symbol: str, days: int = 30) -> Dict[str, Any]:
|
93 |
+
cache_key = f"price_history_{symbol}_{days}"
|
94 |
+
cached = cache_manager.get(cache_key)
|
95 |
+
if cached:
|
96 |
+
return cached
|
97 |
+
|
98 |
+
try:
|
99 |
+
coin_id = self._format_coin_id(symbol)
|
100 |
+
data = await coingecko_client.get_price_history(coin_id, days)
|
101 |
+
cache_manager.set(cache_key, data, 900)
|
102 |
+
return data
|
103 |
+
except Exception as e:
|
104 |
+
raise Exception(f"Failed to get price history for {symbol}: {str(e)}")
|
105 |
+
|
106 |
+
async def get_advanced_coin_analysis(self, symbol: str) -> Dict[str, Any]:
|
107 |
+
cache_key = f"advanced_analysis_{symbol.lower()}"
|
108 |
+
cached = cache_manager.get(cache_key)
|
109 |
+
if cached:
|
110 |
+
return cached
|
111 |
+
|
112 |
+
try:
|
113 |
+
coin_id = self._format_coin_id(symbol)
|
114 |
+
|
115 |
+
tasks = [
|
116 |
+
coingecko_client.get_coin_data(coin_id),
|
117 |
+
coingecko_client.get_price_history(coin_id, days=30),
|
118 |
+
cryptocompare_client.get_social_data(symbol.upper()),
|
119 |
+
self._get_defi_involvement(symbol.upper())
|
120 |
+
]
|
121 |
+
|
122 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
123 |
+
|
124 |
+
analysis = {}
|
125 |
+
for i, result in enumerate(results):
|
126 |
+
if not isinstance(result, Exception):
|
127 |
+
if i == 0: analysis["coin_data"] = result
|
128 |
+
elif i == 1: analysis["price_history"] = result
|
129 |
+
elif i == 2: analysis["social_data"] = result
|
130 |
+
elif i == 3: analysis["defi_data"] = result
|
131 |
+
|
132 |
+
cache_manager.set(cache_key, analysis, 300)
|
133 |
+
return analysis
|
134 |
+
|
135 |
+
except Exception as e:
|
136 |
+
raise Exception(f"Failed advanced analysis for {symbol}: {str(e)}")
|
137 |
+
|
138 |
+
async def _get_defi_involvement(self, symbol: str) -> Dict[str, Any]:
|
139 |
+
try:
|
140 |
+
protocols = await defillama_client.get_protocols()
|
141 |
+
if protocols:
|
142 |
+
relevant_protocols = [p for p in protocols if symbol.lower() in p.get("name", "").lower()]
|
143 |
+
return {"protocols": relevant_protocols[:5]}
|
144 |
+
return {"protocols": []}
|
145 |
+
except:
|
146 |
+
return {"protocols": []}
|
147 |
+
|
148 |
+
def _format_comprehensive_data(self, data: Dict[str, Any]) -> str:
|
149 |
+
formatted = "📊 COMPREHENSIVE CRYPTO MARKET ANALYSIS\n\n"
|
150 |
+
|
151 |
+
if "global_data" in data and data["global_data"].get("data"):
|
152 |
+
global_info = data["global_data"]["data"]
|
153 |
+
total_mcap = global_info.get("total_market_cap", {}).get("usd", 0)
|
154 |
+
total_volume = global_info.get("total_volume", {}).get("usd", 0)
|
155 |
+
btc_dominance = global_info.get("market_cap_percentage", {}).get("btc", 0)
|
156 |
+
eth_dominance = global_info.get("market_cap_percentage", {}).get("eth", 0)
|
157 |
+
|
158 |
+
formatted += f"💰 Total Market Cap: ${total_mcap/1e12:.2f}T\n"
|
159 |
+
formatted += f"📈 24h Volume: ${total_volume/1e9:.1f}B\n"
|
160 |
+
formatted += f"₿ Bitcoin Dominance: {btc_dominance:.1f}%\n"
|
161 |
+
formatted += f"Ξ Ethereum Dominance: {eth_dominance:.1f}%\n\n"
|
162 |
+
|
163 |
+
if "trending" in data and data["trending"].get("coins"):
|
164 |
+
formatted += "🔥 TRENDING CRYPTOCURRENCIES\n"
|
165 |
+
for i, coin in enumerate(data["trending"]["coins"][:5], 1):
|
166 |
+
name = coin.get("item", {}).get("name", "Unknown")
|
167 |
+
symbol = coin.get("item", {}).get("symbol", "")
|
168 |
+
score = coin.get("item", {}).get("score", 0)
|
169 |
+
formatted += f"{i}. {name} ({symbol.upper()}) - Score: {score}\n"
|
170 |
+
formatted += "\n"
|
171 |
+
|
172 |
+
if "defi_protocols" in data and data["defi_protocols"]:
|
173 |
+
formatted += "🏦 TOP DeFi PROTOCOLS\n"
|
174 |
+
for i, protocol in enumerate(data["defi_protocols"][:5], 1):
|
175 |
+
name = protocol.get("name", "Unknown")
|
176 |
+
tvl = protocol.get("tvl", 0)
|
177 |
+
chain = protocol.get("chain", "Unknown")
|
178 |
+
formatted += f"{i}. {name} ({chain}): ${tvl/1e9:.2f}B TVL\n"
|
179 |
+
formatted += "\n"
|
180 |
+
|
181 |
+
if "news" in data and data["news"]:
|
182 |
+
formatted += "📰 LATEST CRYPTO NEWS\n"
|
183 |
+
for i, article in enumerate(data["news"][:3], 1):
|
184 |
+
title = article.get("title", "No title")[:60] + "..."
|
185 |
+
source = article.get("source", "Unknown")
|
186 |
+
formatted += f"{i}. {title} - {source}\n"
|
187 |
+
formatted += "\n"
|
188 |
+
|
189 |
+
if "market_data" in data and data["market_data"]:
|
190 |
+
formatted += "💎 TOP PERFORMING COINS (24h)\n"
|
191 |
+
valid_coins = [coin for coin in data["market_data"][:20] if coin.get("price_change_percentage_24h") is not None]
|
192 |
+
sorted_coins = sorted(valid_coins, key=lambda x: x.get("price_change_percentage_24h", 0), reverse=True)
|
193 |
+
for i, coin in enumerate(sorted_coins[:5], 1):
|
194 |
+
name = coin.get("name", "Unknown")
|
195 |
+
symbol = coin.get("symbol", "").upper()
|
196 |
+
price = coin.get("current_price", 0)
|
197 |
+
change = coin.get("price_change_percentage_24h", 0)
|
198 |
+
formatted += f"{i}. {name} ({symbol}): ${price:,.4f} (+{change:.2f}%)\n"
|
199 |
+
|
200 |
+
return formatted
|
201 |
+
|
202 |
+
async def research_with_context(self, query: str) -> str:
|
203 |
+
try:
|
204 |
+
if not config.GEMINI_API_KEY or not self.model:
|
205 |
+
return "❌ Gemini API key not configured. Please set GEMINI_API_KEY environment variable."
|
206 |
+
|
207 |
+
system_prompt = """You are an advanced Web3 and DeFi research analyst with access to real-time market data,
|
208 |
+
DeFi protocol information, social sentiment, and breaking news. Provide comprehensive, actionable insights
|
209 |
+
that combine multiple data sources for superior analysis.
|
210 |
+
|
211 |
+
Guidelines:
|
212 |
+
- Synthesize data from multiple sources (price, DeFi, social, news)
|
213 |
+
- Provide specific recommendations with risk assessments
|
214 |
+
- Include both technical and fundamental analysis
|
215 |
+
- Reference current market conditions and news events
|
216 |
+
- Use clear, professional language with data-driven insights
|
217 |
+
- Highlight opportunities and risks clearly
|
218 |
+
"""
|
219 |
+
|
220 |
+
market_context = ""
|
221 |
+
try:
|
222 |
+
if any(keyword in query.lower() for keyword in
|
223 |
+
["market", "overview", "analysis", "trending", "defi", "protocols"]):
|
224 |
+
comprehensive_data = await self.get_comprehensive_market_data()
|
225 |
+
market_context = f"\n\nCURRENT MARKET ANALYSIS:\n{self._format_comprehensive_data(comprehensive_data)}"
|
226 |
+
|
227 |
+
for symbol in self.symbol_map.keys():
|
228 |
+
if symbol in query.lower() or symbol.upper() in query:
|
229 |
+
analysis_data = await self.get_advanced_coin_analysis(symbol)
|
230 |
+
if "coin_data" in analysis_data:
|
231 |
+
coin_info = analysis_data["coin_data"]
|
232 |
+
market_data = coin_info.get("market_data", {})
|
233 |
+
current_price = market_data.get("current_price", {}).get("usd", 0)
|
234 |
+
price_change = market_data.get("price_change_percentage_24h", 0)
|
235 |
+
market_cap = market_data.get("market_cap", {}).get("usd", 0)
|
236 |
+
volume = market_data.get("total_volume", {}).get("usd", 0)
|
237 |
+
ath = market_data.get("ath", {}).get("usd", 0)
|
238 |
+
ath_change = market_data.get("ath_change_percentage", {}).get("usd", 0)
|
239 |
+
|
240 |
+
market_context += f"\n\n{symbol.upper()} DETAILED ANALYSIS:\n"
|
241 |
+
market_context += f"Current Price: ${current_price:,.4f}\n"
|
242 |
+
market_context += f"24h Change: {price_change:+.2f}%\n"
|
243 |
+
market_context += f"Market Cap: ${market_cap/1e9:.2f}B\n"
|
244 |
+
market_context += f"24h Volume: ${volume/1e9:.2f}B\n"
|
245 |
+
market_context += f"ATH: ${ath:,.4f} ({ath_change:+.2f}% from ATH)\n"
|
246 |
+
break
|
247 |
+
|
248 |
+
if "defi" in query.lower():
|
249 |
+
defi_data = await self.get_defi_analysis()
|
250 |
+
if "top_protocols" in defi_data and defi_data["top_protocols"]:
|
251 |
+
market_context += "\n\nTOP DeFi PROTOCOLS BY TVL:\n"
|
252 |
+
for protocol in defi_data["top_protocols"][:5]:
|
253 |
+
name = protocol.get("name", "Unknown")
|
254 |
+
tvl = protocol.get("tvl", 0)
|
255 |
+
change = protocol.get("change_1d", 0)
|
256 |
+
market_context += f"• {name}: ${tvl/1e9:.2f}B TVL ({change:+.2f}%)\n"
|
257 |
+
|
258 |
+
except Exception as e:
|
259 |
+
market_context = f"\n\nNote: Some enhanced data unavailable ({str(e)})"
|
260 |
+
|
261 |
+
full_prompt = f"{system_prompt}\n\nQuery: {query}\n\nReal-time Market Context:{market_context}"
|
262 |
+
|
263 |
+
response = self.model.generate_content(full_prompt)
|
264 |
+
return response.text if response.text else "❌ No response generated. Please try rephrasing your query."
|
265 |
+
|
266 |
+
except Exception as e:
|
267 |
+
return f"❌ Enhanced research failed: {str(e)}"
|
268 |
+
|
269 |
+
async def close(self):
|
270 |
+
await coingecko_client.close()
|
271 |
+
await cryptocompare_client.close()
|
272 |
+
await defillama_client.close()
|
273 |
+
await news_aggregator.close()
|
src/news_aggregator.py
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import aiohttp
|
2 |
+
import asyncio
|
3 |
+
from typing import Dict, Any, List, Optional
|
4 |
+
from datetime import datetime
|
5 |
+
import json
|
6 |
+
|
7 |
+
class CryptoNewsAggregator:
|
8 |
+
def __init__(self):
|
9 |
+
self.sources = {
|
10 |
+
"cryptonews": "https://cryptonews-api.com/api/v1/category?section=general&items=10",
|
11 |
+
"newsapi": "https://newsapi.org/v2/everything?q=cryptocurrency&sortBy=publishedAt&pageSize=10",
|
12 |
+
"coindesk": "https://api.coindesk.com/v1/news/articles"
|
13 |
+
}
|
14 |
+
self.session = None
|
15 |
+
|
16 |
+
async def get_session(self):
|
17 |
+
if self.session is None:
|
18 |
+
timeout = aiohttp.ClientTimeout(total=30)
|
19 |
+
headers = {"User-Agent": "Web3-Research-CoBot/1.0"}
|
20 |
+
self.session = aiohttp.ClientSession(timeout=timeout, headers=headers)
|
21 |
+
return self.session
|
22 |
+
|
23 |
+
async def get_crypto_news(self, limit: int = 10) -> List[Dict[str, Any]]:
|
24 |
+
news_items = []
|
25 |
+
tasks = []
|
26 |
+
|
27 |
+
for source, url in self.sources.items():
|
28 |
+
tasks.append(self._fetch_news_from_source(source, url))
|
29 |
+
|
30 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
31 |
+
|
32 |
+
for result in results:
|
33 |
+
if not isinstance(result, Exception) and result:
|
34 |
+
news_items.extend(result[:5])
|
35 |
+
|
36 |
+
news_items.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
|
37 |
+
return news_items[:limit]
|
38 |
+
|
39 |
+
async def _fetch_news_from_source(self, source: str, url: str) -> List[Dict[str, Any]]:
|
40 |
+
try:
|
41 |
+
session = await self.get_session()
|
42 |
+
async with session.get(url) as response:
|
43 |
+
if response.status == 200:
|
44 |
+
data = await response.json()
|
45 |
+
return self._parse_news_data(source, data)
|
46 |
+
return []
|
47 |
+
except Exception:
|
48 |
+
return []
|
49 |
+
|
50 |
+
def _parse_news_data(self, source: str, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
51 |
+
news_items = []
|
52 |
+
current_time = datetime.now().timestamp()
|
53 |
+
|
54 |
+
try:
|
55 |
+
if source == "cryptonews" and "data" in data:
|
56 |
+
for item in data["data"][:5]:
|
57 |
+
news_items.append({
|
58 |
+
"title": item.get("news_title", ""),
|
59 |
+
"summary": item.get("text", "")[:200] + "...",
|
60 |
+
"url": item.get("news_url", ""),
|
61 |
+
"source": "CryptoNews",
|
62 |
+
"timestamp": current_time
|
63 |
+
})
|
64 |
+
|
65 |
+
elif source == "newsapi" and "articles" in data:
|
66 |
+
for item in data["articles"][:5]:
|
67 |
+
news_items.append({
|
68 |
+
"title": item.get("title", ""),
|
69 |
+
"summary": item.get("description", "")[:200] + "...",
|
70 |
+
"url": item.get("url", ""),
|
71 |
+
"source": item.get("source", {}).get("name", "NewsAPI"),
|
72 |
+
"timestamp": current_time
|
73 |
+
})
|
74 |
+
except Exception:
|
75 |
+
pass
|
76 |
+
|
77 |
+
return news_items
|
78 |
+
|
79 |
+
async def close(self):
|
80 |
+
if self.session:
|
81 |
+
await self.session.close()
|
82 |
+
|
83 |
+
news_aggregator = CryptoNewsAggregator()
|
src/portfolio_analyzer.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
from typing import Dict, Any, List, Optional
|
3 |
+
from src.api_clients import coingecko_client
|
4 |
+
from src.cache_manager import cache_manager
|
5 |
+
import json
|
6 |
+
|
7 |
+
class PortfolioAnalyzer:
|
8 |
+
def __init__(self):
|
9 |
+
self.symbol_map = {
|
10 |
+
"btc": "bitcoin", "eth": "ethereum", "sol": "solana", "ada": "cardano",
|
11 |
+
"dot": "polkadot", "bnb": "binancecoin", "usdc": "usd-coin",
|
12 |
+
"usdt": "tether", "xrp": "ripple", "avax": "avalanche-2",
|
13 |
+
"link": "chainlink", "matic": "matic-network", "uni": "uniswap"
|
14 |
+
}
|
15 |
+
|
16 |
+
def _format_coin_id(self, symbol: str) -> str:
|
17 |
+
return self.symbol_map.get(symbol.lower(), symbol.lower())
|
18 |
+
|
19 |
+
async def analyze_portfolio(self, holdings: List[Dict[str, Any]]) -> Dict[str, Any]:
|
20 |
+
try:
|
21 |
+
coin_ids = [self._format_coin_id(h["symbol"]) for h in holdings]
|
22 |
+
|
23 |
+
tasks = []
|
24 |
+
for coin_id in coin_ids:
|
25 |
+
tasks.append(coingecko_client.get_coin_data(coin_id))
|
26 |
+
tasks.append(coingecko_client.get_price_history(coin_id, days=30))
|
27 |
+
|
28 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
29 |
+
|
30 |
+
portfolio_value = 0
|
31 |
+
portfolio_change_24h = 0
|
32 |
+
asset_allocation = []
|
33 |
+
risk_metrics = []
|
34 |
+
|
35 |
+
for i, holding in enumerate(holdings):
|
36 |
+
coin_data_idx = i * 2
|
37 |
+
price_history_idx = i * 2 + 1
|
38 |
+
|
39 |
+
if not isinstance(results[coin_data_idx], Exception):
|
40 |
+
coin_data = results[coin_data_idx]
|
41 |
+
market_data = coin_data.get("market_data", {})
|
42 |
+
current_price = market_data.get("current_price", {}).get("usd", 0)
|
43 |
+
price_change_24h = market_data.get("price_change_percentage_24h", 0)
|
44 |
+
|
45 |
+
holding_value = current_price * holding["amount"]
|
46 |
+
portfolio_value += holding_value
|
47 |
+
portfolio_change_24h += holding_value * (price_change_24h / 100)
|
48 |
+
|
49 |
+
volatility = 0
|
50 |
+
if not isinstance(results[price_history_idx], Exception):
|
51 |
+
price_history = results[price_history_idx]
|
52 |
+
prices = [p[1] for p in price_history.get("prices", [])]
|
53 |
+
if len(prices) > 1:
|
54 |
+
price_changes = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]
|
55 |
+
volatility = sum(abs(change) for change in price_changes) / len(price_changes)
|
56 |
+
|
57 |
+
asset_allocation.append({
|
58 |
+
"symbol": holding["symbol"].upper(),
|
59 |
+
"name": coin_data.get("name", "Unknown"),
|
60 |
+
"value": holding_value,
|
61 |
+
"percentage": 0,
|
62 |
+
"amount": holding["amount"],
|
63 |
+
"price": current_price,
|
64 |
+
"change_24h": price_change_24h
|
65 |
+
})
|
66 |
+
|
67 |
+
risk_metrics.append({
|
68 |
+
"symbol": holding["symbol"].upper(),
|
69 |
+
"volatility": volatility,
|
70 |
+
"market_cap_rank": coin_data.get("market_cap_rank", 999)
|
71 |
+
})
|
72 |
+
|
73 |
+
for asset in asset_allocation:
|
74 |
+
asset["percentage"] = (asset["value"] / portfolio_value) * 100 if portfolio_value > 0 else 0
|
75 |
+
|
76 |
+
portfolio_change_percentage = (portfolio_change_24h / portfolio_value) * 100 if portfolio_value > 0 else 0
|
77 |
+
|
78 |
+
avg_volatility = sum(r["volatility"] for r in risk_metrics) / len(risk_metrics) if risk_metrics else 0
|
79 |
+
|
80 |
+
diversification_score = len([a for a in asset_allocation if a["percentage"] >= 5])
|
81 |
+
|
82 |
+
risk_level = "Low" if avg_volatility < 0.05 else "Medium" if avg_volatility < 0.10 else "High"
|
83 |
+
|
84 |
+
return {
|
85 |
+
"total_value": portfolio_value,
|
86 |
+
"change_24h": portfolio_change_24h,
|
87 |
+
"change_24h_percentage": portfolio_change_percentage,
|
88 |
+
"asset_allocation": sorted(asset_allocation, key=lambda x: x["value"], reverse=True),
|
89 |
+
"risk_metrics": {
|
90 |
+
"overall_risk": risk_level,
|
91 |
+
"avg_volatility": avg_volatility,
|
92 |
+
"diversification_score": diversification_score,
|
93 |
+
"largest_holding_percentage": max([a["percentage"] for a in asset_allocation]) if asset_allocation else 0
|
94 |
+
},
|
95 |
+
"recommendations": self._generate_recommendations(asset_allocation, risk_metrics)
|
96 |
+
}
|
97 |
+
|
98 |
+
except Exception as e:
|
99 |
+
raise Exception(f"Portfolio analysis failed: {str(e)}")
|
100 |
+
|
101 |
+
def _generate_recommendations(self, allocation: List[Dict[str, Any]], risk_metrics: List[Dict[str, Any]]) -> List[str]:
|
102 |
+
recommendations = []
|
103 |
+
|
104 |
+
if not allocation:
|
105 |
+
return ["Unable to generate recommendations - no valid portfolio data"]
|
106 |
+
|
107 |
+
largest_holding = max(allocation, key=lambda x: x["percentage"])
|
108 |
+
if largest_holding["percentage"] > 50:
|
109 |
+
recommendations.append(f"Consider reducing {largest_holding['symbol']} position (currently {largest_holding['percentage']:.1f}%) to improve diversification")
|
110 |
+
|
111 |
+
high_risk_assets = [r for r in risk_metrics if r["volatility"] > 0.15]
|
112 |
+
if len(high_risk_assets) > len(allocation) * 0.6:
|
113 |
+
recommendations.append("Portfolio has high volatility exposure - consider adding stable assets like BTC or ETH")
|
114 |
+
|
115 |
+
small_cap_heavy = len([r for r in risk_metrics if r["market_cap_rank"] > 100])
|
116 |
+
if small_cap_heavy > len(allocation) * 0.4:
|
117 |
+
recommendations.append("High small-cap exposure detected - consider balancing with top 20 cryptocurrencies")
|
118 |
+
|
119 |
+
if len(allocation) < 5:
|
120 |
+
recommendations.append("Consider diversifying into 5-10 different cryptocurrencies to reduce risk")
|
121 |
+
|
122 |
+
stablecoin_exposure = sum(a["percentage"] for a in allocation if a["symbol"] in ["USDC", "USDT", "DAI"])
|
123 |
+
if stablecoin_exposure < 10:
|
124 |
+
recommendations.append("Consider allocating 10-20% to stablecoins for portfolio stability")
|
125 |
+
|
126 |
+
return recommendations[:5]
|
127 |
+
|
128 |
+
async def compare_portfolios(self, portfolio1: List[Dict[str, Any]], portfolio2: List[Dict[str, Any]]) -> Dict[str, Any]:
|
129 |
+
analysis1 = await self.analyze_portfolio(portfolio1)
|
130 |
+
analysis2 = await self.analyze_portfolio(portfolio2)
|
131 |
+
|
132 |
+
return {
|
133 |
+
"portfolio_1": analysis1,
|
134 |
+
"portfolio_2": analysis2,
|
135 |
+
"comparison": {
|
136 |
+
"value_difference": analysis2["total_value"] - analysis1["total_value"],
|
137 |
+
"performance_difference": analysis2["change_24h_percentage"] - analysis1["change_24h_percentage"],
|
138 |
+
"risk_comparison": f"Portfolio 2 is {'higher' if analysis2['risk_metrics']['avg_volatility'] > analysis1['risk_metrics']['avg_volatility'] else 'lower'} risk",
|
139 |
+
"diversification_comparison": f"Portfolio 2 is {'more' if analysis2['risk_metrics']['diversification_score'] > analysis1['risk_metrics']['diversification_score'] else 'less'} diversified"
|
140 |
+
}
|
141 |
+
}
|
142 |
+
|
143 |
+
portfolio_analyzer = PortfolioAnalyzer()
|
src/research_agent.py
ADDED
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from google import genai
|
2 |
+
from google.genai import types
|
3 |
+
import json
|
4 |
+
from typing import Dict, Any, List
|
5 |
+
from src.api_clients import coingecko_client, cryptocompare_client
|
6 |
+
from src.cache_manager import cache_manager
|
7 |
+
from src.config import config
|
8 |
+
import asyncio
|
9 |
+
|
10 |
+
class ResearchAgent:
|
11 |
+
def __init__(self):
|
12 |
+
self.client = genai.Client(api_key=config.GEMINI_API_KEY)
|
13 |
+
self.symbol_map = {
|
14 |
+
"btc": "bitcoin", "eth": "ethereum", "sol": "solana",
|
15 |
+
"ada": "cardano", "dot": "polkadot", "bnb": "binancecoin",
|
16 |
+
"usdc": "usd-coin", "usdt": "tether", "xrp": "ripple",
|
17 |
+
"avax": "avalanche-2", "link": "chainlink", "matic": "matic-network"
|
18 |
+
}
|
19 |
+
|
20 |
+
def _format_coin_id(self, symbol: str) -> str:
|
21 |
+
return self.symbol_map.get(symbol.lower(), symbol.lower())
|
22 |
+
|
23 |
+
async def get_market_overview(self) -> Dict[str, Any]:
|
24 |
+
cache_key = "market_overview"
|
25 |
+
cached = cache_manager.get(cache_key)
|
26 |
+
if cached:
|
27 |
+
return cached
|
28 |
+
|
29 |
+
try:
|
30 |
+
market_data = await coingecko_client.get_market_data(per_page=20)
|
31 |
+
global_data = await coingecko_client.get_global_data()
|
32 |
+
trending = await coingecko_client.get_trending()
|
33 |
+
|
34 |
+
result = {
|
35 |
+
"market_data": market_data,
|
36 |
+
"global_data": global_data,
|
37 |
+
"trending": trending
|
38 |
+
}
|
39 |
+
|
40 |
+
cache_manager.set(cache_key, result)
|
41 |
+
return result
|
42 |
+
|
43 |
+
except Exception as e:
|
44 |
+
raise Exception(f"Failed to fetch market overview: {str(e)}")
|
45 |
+
|
46 |
+
async def get_price_history(self, symbol: str) -> Dict[str, Any]:
|
47 |
+
cache_key = f"price_history_{symbol.lower()}"
|
48 |
+
cached = cache_manager.get(cache_key)
|
49 |
+
if cached:
|
50 |
+
return cached
|
51 |
+
|
52 |
+
try:
|
53 |
+
coin_id = self._format_coin_id(symbol)
|
54 |
+
data = await coingecko_client.get_price_history(coin_id, days=30)
|
55 |
+
|
56 |
+
cache_manager.set(cache_key, data)
|
57 |
+
return data
|
58 |
+
|
59 |
+
except Exception as e:
|
60 |
+
raise Exception(f"Failed to fetch price history for {symbol}: {str(e)}")
|
61 |
+
|
62 |
+
async def get_coin_analysis(self, symbol: str) -> Dict[str, Any]:
|
63 |
+
cache_key = f"coin_analysis_{symbol.lower()}"
|
64 |
+
cached = cache_manager.get(cache_key)
|
65 |
+
if cached:
|
66 |
+
return cached
|
67 |
+
|
68 |
+
try:
|
69 |
+
coin_id = self._format_coin_id(symbol)
|
70 |
+
|
71 |
+
tasks = [
|
72 |
+
coingecko_client.get_coin_data(coin_id),
|
73 |
+
coingecko_client.get_price_history(coin_id, days=7),
|
74 |
+
cryptocompare_client.get_social_data(symbol.upper())
|
75 |
+
]
|
76 |
+
|
77 |
+
coin_data, price_history, social_data = await asyncio.gather(*tasks, return_exceptions=True)
|
78 |
+
|
79 |
+
result = {}
|
80 |
+
if not isinstance(coin_data, Exception):
|
81 |
+
result["coin_data"] = coin_data
|
82 |
+
if not isinstance(price_history, Exception):
|
83 |
+
result["price_history"] = price_history
|
84 |
+
if not isinstance(social_data, Exception):
|
85 |
+
result["social_data"] = social_data
|
86 |
+
|
87 |
+
cache_manager.set(cache_key, result)
|
88 |
+
return result
|
89 |
+
|
90 |
+
except Exception as e:
|
91 |
+
raise Exception(f"Failed to analyze {symbol}: {str(e)}")
|
92 |
+
|
93 |
+
def _format_market_data(self, data: Dict[str, Any]) -> str:
|
94 |
+
if not data:
|
95 |
+
return "No market data available"
|
96 |
+
|
97 |
+
formatted = "📊 MARKET OVERVIEW\n\n"
|
98 |
+
|
99 |
+
if "global_data" in data and "data" in data["global_data"]:
|
100 |
+
global_info = data["global_data"]["data"]
|
101 |
+
total_mcap = global_info.get("total_market_cap", {}).get("usd", 0)
|
102 |
+
total_volume = global_info.get("total_volume", {}).get("usd", 0)
|
103 |
+
btc_dominance = global_info.get("market_cap_percentage", {}).get("btc", 0)
|
104 |
+
|
105 |
+
formatted += f"Total Market Cap: ${total_mcap:,.0f}\n"
|
106 |
+
formatted += f"24h Volume: ${total_volume:,.0f}\n"
|
107 |
+
formatted += f"Bitcoin Dominance: {btc_dominance:.1f}%\n\n"
|
108 |
+
|
109 |
+
if "trending" in data and "coins" in data["trending"]:
|
110 |
+
formatted += "🔥 TRENDING COINS\n"
|
111 |
+
for i, coin in enumerate(data["trending"]["coins"][:5], 1):
|
112 |
+
name = coin.get("item", {}).get("name", "Unknown")
|
113 |
+
symbol = coin.get("item", {}).get("symbol", "")
|
114 |
+
formatted += f"{i}. {name} ({symbol.upper()})\n"
|
115 |
+
formatted += "\n"
|
116 |
+
|
117 |
+
if "market_data" in data:
|
118 |
+
formatted += "💰 TOP CRYPTOCURRENCIES\n"
|
119 |
+
for i, coin in enumerate(data["market_data"][:10], 1):
|
120 |
+
name = coin.get("name", "Unknown")
|
121 |
+
symbol = coin.get("symbol", "").upper()
|
122 |
+
price = coin.get("current_price", 0)
|
123 |
+
change = coin.get("price_change_percentage_24h", 0)
|
124 |
+
change_symbol = "📈" if change >= 0 else "📉"
|
125 |
+
|
126 |
+
formatted += f"{i:2d}. {name} ({symbol}): ${price:,.4f} {change_symbol} {change:+.2f}%\n"
|
127 |
+
|
128 |
+
return formatted
|
129 |
+
|
130 |
+
async def research(self, query: str) -> str:
|
131 |
+
try:
|
132 |
+
if not config.GEMINI_API_KEY:
|
133 |
+
return "❌ Gemini API key not configured. Please set GEMINI_API_KEY environment variable."
|
134 |
+
|
135 |
+
system_prompt = """You are an expert Web3 and cryptocurrency research analyst.
|
136 |
+
Provide comprehensive, accurate, and actionable insights based on real market data.
|
137 |
+
|
138 |
+
Guidelines:
|
139 |
+
- Give specific, data-driven analysis
|
140 |
+
- Include price targets and risk assessments when relevant
|
141 |
+
- Explain technical concepts clearly
|
142 |
+
- Provide actionable recommendations
|
143 |
+
- Use emojis for better readability
|
144 |
+
- Be concise but thorough
|
145 |
+
"""
|
146 |
+
|
147 |
+
market_context = ""
|
148 |
+
try:
|
149 |
+
if any(keyword in query.lower() for keyword in ["market", "overview", "trending", "top"]):
|
150 |
+
market_data = await self.get_market_overview()
|
151 |
+
market_context = f"\n\nCURRENT MARKET DATA:\n{self._format_market_data(market_data)}"
|
152 |
+
|
153 |
+
for symbol in ["btc", "eth", "sol", "ada", "dot", "bnb", "avax", "link"]:
|
154 |
+
if symbol in query.lower() or symbol.upper() in query:
|
155 |
+
analysis_data = await self.get_coin_analysis(symbol)
|
156 |
+
if "coin_data" in analysis_data:
|
157 |
+
coin_info = analysis_data["coin_data"]
|
158 |
+
market_data = coin_info.get("market_data", {})
|
159 |
+
current_price = market_data.get("current_price", {}).get("usd", 0)
|
160 |
+
price_change = market_data.get("price_change_percentage_24h", 0)
|
161 |
+
market_cap = market_data.get("market_cap", {}).get("usd", 0)
|
162 |
+
volume = market_data.get("total_volume", {}).get("usd", 0)
|
163 |
+
|
164 |
+
market_context += f"\n\n{symbol.upper()} DATA:\n"
|
165 |
+
market_context += f"Price: ${current_price:,.4f}\n"
|
166 |
+
market_context += f"24h Change: {price_change:+.2f}%\n"
|
167 |
+
market_context += f"Market Cap: ${market_cap:,.0f}\n"
|
168 |
+
market_context += f"Volume: ${volume:,.0f}\n"
|
169 |
+
break
|
170 |
+
|
171 |
+
except Exception as e:
|
172 |
+
market_context = f"\n\nNote: Some market data unavailable ({str(e)})"
|
173 |
+
|
174 |
+
full_prompt = f"{query}{market_context}"
|
175 |
+
|
176 |
+
response = self.client.models.generate_content(
|
177 |
+
model="gemini-2.5-flash",
|
178 |
+
contents=[
|
179 |
+
types.Content(
|
180 |
+
role="user",
|
181 |
+
parts=[types.Part(text=full_prompt)]
|
182 |
+
)
|
183 |
+
],
|
184 |
+
config=types.GenerateContentConfig(
|
185 |
+
system_instruction=system_prompt,
|
186 |
+
temperature=0.3,
|
187 |
+
max_output_tokens=2000
|
188 |
+
)
|
189 |
+
)
|
190 |
+
|
191 |
+
if response.text:
|
192 |
+
return response.text
|
193 |
+
else:
|
194 |
+
return "❌ No response generated. Please try rephrasing your query."
|
195 |
+
|
196 |
+
except Exception as e:
|
197 |
+
return f"❌ Research failed: {str(e)}"
|
198 |
+
|
199 |
+
async def close(self):
|
200 |
+
await coingecko_client.close()
|
201 |
+
await cryptocompare_client.close()
|
src/visualizations.py
ADDED
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import plotly.graph_objects as go
|
2 |
+
import plotly.express as px
|
3 |
+
from plotly.subplots import make_subplots
|
4 |
+
from typing import Dict, Any, List
|
5 |
+
import pandas as pd
|
6 |
+
from datetime import datetime
|
7 |
+
|
8 |
+
def create_price_chart(data: Dict[str, Any], symbol: str) -> str:
|
9 |
+
try:
|
10 |
+
if not data or "prices" not in data:
|
11 |
+
return f"<div style='padding: 20px; text-align: center;'>No price data available for {symbol.upper()}</div>"
|
12 |
+
|
13 |
+
prices = data["prices"]
|
14 |
+
volumes = data.get("total_volumes", [])
|
15 |
+
|
16 |
+
if not prices:
|
17 |
+
return f"<div style='padding: 20px; text-align: center;'>No price history found for {symbol.upper()}</div>"
|
18 |
+
|
19 |
+
df = pd.DataFrame(prices, columns=["timestamp", "price"])
|
20 |
+
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms")
|
21 |
+
|
22 |
+
fig = make_subplots(
|
23 |
+
rows=2, cols=1,
|
24 |
+
shared_xaxes=True,
|
25 |
+
vertical_spacing=0.05,
|
26 |
+
subplot_titles=[f"{symbol.upper()} Price Chart", "Volume"],
|
27 |
+
row_width=[0.7, 0.3]
|
28 |
+
)
|
29 |
+
|
30 |
+
fig.add_trace(
|
31 |
+
go.Scatter(
|
32 |
+
x=df["datetime"],
|
33 |
+
y=df["price"],
|
34 |
+
mode="lines",
|
35 |
+
name="Price",
|
36 |
+
line=dict(color="#00D4AA", width=2),
|
37 |
+
hovertemplate="<b>%{y:$,.2f}</b><br>%{x}<extra></extra>"
|
38 |
+
),
|
39 |
+
row=1, col=1
|
40 |
+
)
|
41 |
+
|
42 |
+
if volumes:
|
43 |
+
vol_df = pd.DataFrame(volumes, columns=["timestamp", "volume"])
|
44 |
+
vol_df["datetime"] = pd.to_datetime(vol_df["timestamp"], unit="ms")
|
45 |
+
|
46 |
+
fig.add_trace(
|
47 |
+
go.Bar(
|
48 |
+
x=vol_df["datetime"],
|
49 |
+
y=vol_df["volume"],
|
50 |
+
name="Volume",
|
51 |
+
marker_color="#FF6B6B",
|
52 |
+
opacity=0.7,
|
53 |
+
hovertemplate="<b>$%{y:,.0f}</b><br>%{x}<extra></extra>"
|
54 |
+
),
|
55 |
+
row=2, col=1
|
56 |
+
)
|
57 |
+
|
58 |
+
current_price = df["price"].iloc[-1]
|
59 |
+
price_change = ((df["price"].iloc[-1] - df["price"].iloc[0]) / df["price"].iloc[0]) * 100
|
60 |
+
|
61 |
+
fig.update_layout(
|
62 |
+
title=dict(
|
63 |
+
text=f"{symbol.upper()} - ${current_price:,.4f} ({price_change:+.2f}%)",
|
64 |
+
x=0.5,
|
65 |
+
font=dict(size=20, color="#FFFFFF")
|
66 |
+
),
|
67 |
+
xaxis_title="Date",
|
68 |
+
yaxis_title="Price (USD)",
|
69 |
+
template="plotly_dark",
|
70 |
+
showlegend=False,
|
71 |
+
height=600,
|
72 |
+
margin=dict(l=60, r=60, t=80, b=60),
|
73 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
74 |
+
paper_bgcolor="rgba(0,0,0,0)"
|
75 |
+
)
|
76 |
+
|
77 |
+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor="rgba(255,255,255,0.1)")
|
78 |
+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor="rgba(255,255,255,0.1)")
|
79 |
+
|
80 |
+
return fig.to_html(include_plotlyjs="cdn", div_id=f"chart_{symbol}")
|
81 |
+
|
82 |
+
except Exception as e:
|
83 |
+
return f"<div style='padding: 20px; text-align: center; color: #FF6B6B;'>Chart generation failed: {str(e)}</div>"
|
84 |
+
|
85 |
+
def create_market_overview(data: Dict[str, Any]) -> str:
|
86 |
+
try:
|
87 |
+
if not data or "market_data" not in data:
|
88 |
+
return "<div style='padding: 20px; text-align: center;'>Market data unavailable</div>"
|
89 |
+
|
90 |
+
market_data = data["market_data"]
|
91 |
+
if not market_data:
|
92 |
+
return "<div style='padding: 20px; text-align: center;'>No market data found</div>"
|
93 |
+
|
94 |
+
df = pd.DataFrame(market_data)
|
95 |
+
df = df.head(20)
|
96 |
+
|
97 |
+
fig = make_subplots(
|
98 |
+
rows=2, cols=2,
|
99 |
+
subplot_titles=[
|
100 |
+
"Market Cap Distribution",
|
101 |
+
"24h Price Changes",
|
102 |
+
"Trading Volume",
|
103 |
+
"Price vs Volume"
|
104 |
+
],
|
105 |
+
specs=[[{"type": "pie"}, {"type": "bar"}],
|
106 |
+
[{"type": "bar"}, {"type": "scatter"}]]
|
107 |
+
)
|
108 |
+
|
109 |
+
fig.add_trace(
|
110 |
+
go.Pie(
|
111 |
+
labels=df["symbol"].str.upper(),
|
112 |
+
values=df["market_cap"],
|
113 |
+
textinfo="label+percent",
|
114 |
+
textposition="inside",
|
115 |
+
marker=dict(colors=px.colors.qualitative.Set3),
|
116 |
+
hovertemplate="<b>%{label}</b><br>Market Cap: $%{value:,.0f}<extra></extra>"
|
117 |
+
),
|
118 |
+
row=1, col=1
|
119 |
+
)
|
120 |
+
|
121 |
+
colors = ["#00D4AA" if x >= 0 else "#FF6B6B" for x in df["price_change_percentage_24h"]]
|
122 |
+
fig.add_trace(
|
123 |
+
go.Bar(
|
124 |
+
x=df["symbol"].str.upper(),
|
125 |
+
y=df["price_change_percentage_24h"],
|
126 |
+
marker_color=colors,
|
127 |
+
hovertemplate="<b>%{x}</b><br>24h Change: %{y:+.2f}%<extra></extra>"
|
128 |
+
),
|
129 |
+
row=1, col=2
|
130 |
+
)
|
131 |
+
|
132 |
+
fig.add_trace(
|
133 |
+
go.Bar(
|
134 |
+
x=df["symbol"].str.upper(),
|
135 |
+
y=df["total_volume"],
|
136 |
+
marker_color="#4ECDC4",
|
137 |
+
hovertemplate="<b>%{x}</b><br>Volume: $%{y:,.0f}<extra></extra>"
|
138 |
+
),
|
139 |
+
row=2, col=1
|
140 |
+
)
|
141 |
+
|
142 |
+
fig.add_trace(
|
143 |
+
go.Scatter(
|
144 |
+
x=df["current_price"],
|
145 |
+
y=df["total_volume"],
|
146 |
+
mode="markers+text",
|
147 |
+
text=df["symbol"].str.upper(),
|
148 |
+
textposition="top center",
|
149 |
+
marker=dict(
|
150 |
+
size=df["market_cap"] / df["market_cap"].max() * 50 + 10,
|
151 |
+
color=df["price_change_percentage_24h"],
|
152 |
+
colorscale="RdYlGn",
|
153 |
+
colorbar=dict(title="24h Change %"),
|
154 |
+
line=dict(width=1, color="white")
|
155 |
+
),
|
156 |
+
hovertemplate="<b>%{text}</b><br>Price: $%{x:,.4f}<br>Volume: $%{y:,.0f}<extra></extra>"
|
157 |
+
),
|
158 |
+
row=2, col=2
|
159 |
+
)
|
160 |
+
|
161 |
+
global_info = data.get("global_data", {}).get("data", {})
|
162 |
+
total_mcap = global_info.get("total_market_cap", {}).get("usd", 0)
|
163 |
+
total_volume = global_info.get("total_volume", {}).get("usd", 0)
|
164 |
+
btc_dominance = global_info.get("market_cap_percentage", {}).get("btc", 0)
|
165 |
+
|
166 |
+
title_text = f"Crypto Market Overview - Total MCap: ${total_mcap/1e12:.2f}T | 24h Vol: ${total_volume/1e9:.0f}B | BTC Dom: {btc_dominance:.1f}%"
|
167 |
+
|
168 |
+
fig.update_layout(
|
169 |
+
title=dict(
|
170 |
+
text=title_text,
|
171 |
+
x=0.5,
|
172 |
+
font=dict(size=16, color="#FFFFFF")
|
173 |
+
),
|
174 |
+
template="plotly_dark",
|
175 |
+
showlegend=False,
|
176 |
+
height=800,
|
177 |
+
margin=dict(l=60, r=60, t=100, b=60),
|
178 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
179 |
+
paper_bgcolor="rgba(0,0,0,0)"
|
180 |
+
)
|
181 |
+
|
182 |
+
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor="rgba(255,255,255,0.1)")
|
183 |
+
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor="rgba(255,255,255,0.1)")
|
184 |
+
|
185 |
+
return fig.to_html(include_plotlyjs="cdn", div_id="market_overview")
|
186 |
+
|
187 |
+
except Exception as e:
|
188 |
+
return f"<div style='padding: 20px; text-align: center; color: #FF6B6B;'>Market overview failed: {str(e)}</div>"
|
189 |
+
|
190 |
+
def create_comparison_chart(coins_data: List[Dict[str, Any]]) -> str:
|
191 |
+
try:
|
192 |
+
if not coins_data:
|
193 |
+
return "<div style='padding: 20px; text-align: center;'>No comparison data available</div>"
|
194 |
+
|
195 |
+
df = pd.DataFrame(coins_data)
|
196 |
+
|
197 |
+
fig = make_subplots(
|
198 |
+
rows=1, cols=2,
|
199 |
+
subplot_titles=["Price Comparison", "Market Cap Comparison"]
|
200 |
+
)
|
201 |
+
|
202 |
+
colors = px.colors.qualitative.Set1[:len(df)]
|
203 |
+
|
204 |
+
for i, (_, coin) in enumerate(df.iterrows()):
|
205 |
+
fig.add_trace(
|
206 |
+
go.Bar(
|
207 |
+
name=coin["symbol"].upper(),
|
208 |
+
x=[coin["symbol"].upper()],
|
209 |
+
y=[coin["current_price"]],
|
210 |
+
marker_color=colors[i],
|
211 |
+
hovertemplate=f"<b>{coin['name']}</b><br>Price: $%{{y:,.4f}}<extra></extra>"
|
212 |
+
),
|
213 |
+
row=1, col=1
|
214 |
+
)
|
215 |
+
|
216 |
+
fig.add_trace(
|
217 |
+
go.Bar(
|
218 |
+
name=coin["symbol"].upper(),
|
219 |
+
x=[coin["symbol"].upper()],
|
220 |
+
y=[coin["market_cap"]],
|
221 |
+
marker_color=colors[i],
|
222 |
+
showlegend=False,
|
223 |
+
hovertemplate=f"<b>{coin['name']}</b><br>Market Cap: $%{{y:,.0f}}<extra></extra>"
|
224 |
+
),
|
225 |
+
row=1, col=2
|
226 |
+
)
|
227 |
+
|
228 |
+
fig.update_layout(
|
229 |
+
title="Cryptocurrency Comparison",
|
230 |
+
template="plotly_dark",
|
231 |
+
height=500,
|
232 |
+
showlegend=True
|
233 |
+
)
|
234 |
+
|
235 |
+
return fig.to_html(include_plotlyjs="cdn", div_id="comparison_chart")
|
236 |
+
|
237 |
+
except Exception as e:
|
238 |
+
return f"<div style='padding: 20px; text-align: center; color: #FF6B6B;'>Comparison chart failed: {str(e)}</div>"
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|