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
|
|
|