Spaces:
Sleeping
Sleeping
Priyanshi Saxena
commited on
Commit
Β·
9b006e9
1
Parent(s):
f104fee
feat: testing done
Browse files- README.md +65 -2
- app.py +0 -151
- app_fastapi.py +602 -0
- attached_assets/Pasted--Complete-Web3-Research-Co-Pilot-Project-Plan-Structure-Project-Directory-Structure--1754811430335_1754811430338.txt +0 -992
- minimal_test.py +0 -36
- requirements.txt +2 -4
- run.py +0 -47
- 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/agent/memory_manager.py +46 -0
- src/agent/research_agent.py +51 -6
- src/agent/response_formatter.py +49 -0
- src/api/airaa_integration.py +1 -1
- src/api_clients.py +0 -158
- src/config.py +0 -26
- src/defillama_client.py +0 -62
- src/enhanced_agent.py +0 -273
- src/news_aggregator.py +0 -83
- src/portfolio_analyzer.py +0 -143
- src/research_agent.py +0 -201
- src/tools/base_tool.py +24 -3
- src/tools/coingecko_tool.py +158 -74
- src/tools/defillama_tool.py +57 -19
- src/tools/etherscan_tool.py +76 -36
- src/{cache_manager.py β utils/cache_manager.py} +32 -6
- src/utils/config.py +30 -1
- src/visualizations.py +0 -62
- test_app.py +0 -108
README.md
CHANGED
|
@@ -1,6 +1,69 @@
|
|
| 1 |
-
# Web3 Research Co-Pilot
|
| 2 |
|
| 3 |
-
AI-powered cryptocurrency research assistant
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Features
|
| 6 |
|
|
|
|
| 1 |
+
# π Web3 Research Co-Pilot
|
| 2 |
|
| 3 |
+
AI-powered cryptocurrency research assistant with comprehensive Web3 data analysis capabilities.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **LangChain AI Agent**: Advanced query processing with Google Gemini
|
| 8 |
+
- **Real-time Data**: CoinGecko, DeFiLlama, Etherscan integration
|
| 9 |
+
- **Interactive UI**: Gradio-based chat interface with visualizations
|
| 10 |
+
- **AIRAA Integration**: Research data forwarding to external platforms
|
| 11 |
+
- **Production Ready**: Comprehensive error handling and async architecture
|
| 12 |
+
|
| 13 |
+
## Quick Start
|
| 14 |
+
|
| 15 |
+
### 1. Environment Setup
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
export GEMINI_API_KEY="your_gemini_api_key"
|
| 19 |
+
export ETHERSCAN_API_KEY="your_etherscan_key" # Optional
|
| 20 |
+
export COINGECKO_API_KEY="your_coingecko_key" # Optional
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### 2. Installation
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### 3. Launch
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
python launch.py
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## API Keys
|
| 36 |
+
|
| 37 |
+
- **GEMINI_API_KEY** (Required): [Get from Google AI Studio](https://makersuite.google.com/app/apikey)
|
| 38 |
+
- **ETHERSCAN_API_KEY** (Optional): [Get from Etherscan.io](https://etherscan.io/apis)
|
| 39 |
+
- **COINGECKO_API_KEY** (Optional): [Get from CoinGecko](https://www.coingecko.com/en/api/pricing)
|
| 40 |
+
|
| 41 |
+
## Architecture
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
βββ app.py # Main Gradio application
|
| 45 |
+
βββ src/
|
| 46 |
+
β βββ agent/ # LangChain AI agent
|
| 47 |
+
β βββ tools/ # Web3 data tools
|
| 48 |
+
β βββ api/ # External integrations
|
| 49 |
+
β βββ utils/ # Configuration & utilities
|
| 50 |
+
βββ launch.py # Launch script
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Usage Examples
|
| 54 |
+
|
| 55 |
+
- "What is the current price of Bitcoin?"
|
| 56 |
+
- "Analyze Ethereum's DeFi ecosystem"
|
| 57 |
+
- "Show me gas prices and network stats"
|
| 58 |
+
- "Research the top DeFi protocols by TVL"
|
| 59 |
+
|
| 60 |
+
## Deployment
|
| 61 |
+
|
| 62 |
+
Configured for HuggingFace Spaces with automatic dependency management.
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
**Built with minimal, expert-level code and production-grade error handling.**
|
| 67 |
|
| 68 |
## Features
|
| 69 |
|
app.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
-
import asyncio
|
| 3 |
-
import json
|
| 4 |
-
from datetime import datetime
|
| 5 |
-
from typing import List, Tuple
|
| 6 |
-
|
| 7 |
-
from src.agent.research_agent import Web3ResearchAgent
|
| 8 |
-
from src.api.airaa_integration import AIRAAIntegration
|
| 9 |
-
from src.visualizations import create_price_chart, create_market_overview
|
| 10 |
-
from src.utils.logger import get_logger
|
| 11 |
-
from src.utils.config import config
|
| 12 |
-
|
| 13 |
-
logger = get_logger(__name__)
|
| 14 |
-
|
| 15 |
-
class Web3CoPilotApp:
|
| 16 |
-
def __init__(self):
|
| 17 |
-
try:
|
| 18 |
-
self.agent = Web3ResearchAgent()
|
| 19 |
-
self.airaa = AIRAAIntegration()
|
| 20 |
-
except Exception as e:
|
| 21 |
-
logger.error(f"App initialization failed: {e}")
|
| 22 |
-
raise
|
| 23 |
-
|
| 24 |
-
async def process_query(self, query: str, history: List[Tuple[str, str]]):
|
| 25 |
-
if not query.strip():
|
| 26 |
-
yield history, ""
|
| 27 |
-
return
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
history.append((query, "π Researching..."))
|
| 31 |
-
yield history, ""
|
| 32 |
-
|
| 33 |
-
result = await self.agent.research_query(query)
|
| 34 |
-
|
| 35 |
-
if result["success"]:
|
| 36 |
-
response = result["result"]
|
| 37 |
-
sources = ", ".join(result.get("sources", []))
|
| 38 |
-
response += f"\n\n---\nπ **Sources**: {sources}\nβ° **Generated**: {datetime.now().strftime('%H:%M:%S')}"
|
| 39 |
-
|
| 40 |
-
if config.AIRAA_WEBHOOK_URL:
|
| 41 |
-
asyncio.create_task(self.airaa.send_research_data(result))
|
| 42 |
-
else:
|
| 43 |
-
response = f"β Error: {result.get('error', 'Research failed')}"
|
| 44 |
-
|
| 45 |
-
history[-1] = (query, response)
|
| 46 |
-
yield history, ""
|
| 47 |
-
|
| 48 |
-
except Exception as e:
|
| 49 |
-
logger.error(f"Query error: {e}")
|
| 50 |
-
history[-1] = (query, f"β System error: {str(e)}")
|
| 51 |
-
yield history, ""
|
| 52 |
-
|
| 53 |
-
def get_chart_data(self, symbol: str):
|
| 54 |
-
try:
|
| 55 |
-
if not symbol.strip():
|
| 56 |
-
return "Please enter a symbol"
|
| 57 |
-
|
| 58 |
-
data = asyncio.run(self.agent.get_price_history(symbol))
|
| 59 |
-
return create_price_chart(data, symbol)
|
| 60 |
-
except Exception as e:
|
| 61 |
-
logger.error(f"Chart error: {e}")
|
| 62 |
-
return f"Chart unavailable: {str(e)}"
|
| 63 |
-
|
| 64 |
-
def get_market_overview(self):
|
| 65 |
-
try:
|
| 66 |
-
data = asyncio.run(self.agent.get_comprehensive_market_data())
|
| 67 |
-
return create_market_overview(data)
|
| 68 |
-
except Exception as e:
|
| 69 |
-
logger.error(f"Market overview error: {e}")
|
| 70 |
-
return f"Market data unavailable: {str(e)}"
|
| 71 |
-
|
| 72 |
-
def create_interface(self):
|
| 73 |
-
with gr.Blocks(title=config.UI_TITLE, theme=gr.themes.Soft()) as demo:
|
| 74 |
-
gr.Markdown(f"""
|
| 75 |
-
# π {config.UI_TITLE}
|
| 76 |
-
{config.UI_DESCRIPTION}
|
| 77 |
-
**Powered by**: Gemini AI β’ CoinGecko β’ DeFiLlama β’ Etherscan
|
| 78 |
-
""")
|
| 79 |
-
|
| 80 |
-
with gr.Row():
|
| 81 |
-
with gr.Column(scale=2):
|
| 82 |
-
chatbot = gr.Chatbot(label="Research Assistant", height=650)
|
| 83 |
-
|
| 84 |
-
with gr.Row():
|
| 85 |
-
query_input = gr.Textbox(
|
| 86 |
-
placeholder="Ask about crypto markets, DeFi protocols, or on-chain data...",
|
| 87 |
-
label="Research Query", lines=2
|
| 88 |
-
)
|
| 89 |
-
submit_btn = gr.Button("π Research", variant="primary")
|
| 90 |
-
|
| 91 |
-
clear_btn = gr.Button("ποΈ Clear", variant="secondary")
|
| 92 |
-
|
| 93 |
-
with gr.Column(scale=1):
|
| 94 |
-
gr.Markdown("### π‘ Example Queries")
|
| 95 |
-
|
| 96 |
-
examples = [
|
| 97 |
-
"Bitcoin price analysis",
|
| 98 |
-
"Top DeFi protocols by TVL",
|
| 99 |
-
"Ethereum vs Solana comparison",
|
| 100 |
-
"Trending cryptocurrencies",
|
| 101 |
-
"DeFi yield opportunities"
|
| 102 |
-
]
|
| 103 |
-
|
| 104 |
-
for example in examples:
|
| 105 |
-
gr.Button(example, size="sm").click(
|
| 106 |
-
lambda x=example: x, outputs=query_input
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
gr.Markdown("### π Visualizations")
|
| 110 |
-
chart_output = gr.Plot(label="Charts")
|
| 111 |
-
|
| 112 |
-
symbol_input = gr.Textbox(placeholder="BTC, ETH, SOL...", label="Chart Symbol")
|
| 113 |
-
chart_btn = gr.Button("π Generate Chart")
|
| 114 |
-
|
| 115 |
-
market_btn = gr.Button("π Market Overview")
|
| 116 |
-
|
| 117 |
-
submit_btn.click(
|
| 118 |
-
self.process_query,
|
| 119 |
-
inputs=[query_input, chatbot],
|
| 120 |
-
outputs=[chatbot, query_input]
|
| 121 |
-
)
|
| 122 |
-
|
| 123 |
-
query_input.submit(
|
| 124 |
-
self.process_query,
|
| 125 |
-
inputs=[query_input, chatbot],
|
| 126 |
-
outputs=[chatbot, query_input]
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
clear_btn.click(lambda: ([], ""), outputs=[chatbot, query_input])
|
| 130 |
-
|
| 131 |
-
chart_btn.click(
|
| 132 |
-
self.get_chart_data,
|
| 133 |
-
inputs=symbol_input,
|
| 134 |
-
outputs=chart_output
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
market_btn.click(
|
| 138 |
-
self.get_market_overview,
|
| 139 |
-
outputs=chart_output
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
return demo
|
| 143 |
-
|
| 144 |
-
if __name__ == "__main__":
|
| 145 |
-
app = Web3CoPilotApp()
|
| 146 |
-
interface = app.create_interface()
|
| 147 |
-
interface.launch(
|
| 148 |
-
server_name="0.0.0.0",
|
| 149 |
-
server_port=7860,
|
| 150 |
-
share=False
|
| 151 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_fastapi.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 2 |
+
from fastapi.staticfiles import StaticFiles
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
import asyncio
|
| 7 |
+
import json
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Dict, Any, Optional
|
| 10 |
+
import os
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
from src.agent.research_agent import Web3ResearchAgent
|
| 16 |
+
from src.api.airaa_integration import AIRAAIntegration
|
| 17 |
+
from src.utils.logger import get_logger
|
| 18 |
+
from src.utils.config import config
|
| 19 |
+
|
| 20 |
+
logger = get_logger(__name__)
|
| 21 |
+
|
| 22 |
+
app = FastAPI(
|
| 23 |
+
title="Web3 Research Co-Pilot",
|
| 24 |
+
description="AI-powered cryptocurrency research assistant",
|
| 25 |
+
version="1.0.0"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Pydantic models for request/response
|
| 29 |
+
class QueryRequest(BaseModel):
|
| 30 |
+
query: str
|
| 31 |
+
chat_history: Optional[List[Dict[str, str]]] = []
|
| 32 |
+
|
| 33 |
+
class QueryResponse(BaseModel):
|
| 34 |
+
success: bool
|
| 35 |
+
response: str
|
| 36 |
+
sources: Optional[List[str]] = []
|
| 37 |
+
metadata: Optional[Dict[str, Any]] = {}
|
| 38 |
+
error: Optional[str] = None
|
| 39 |
+
|
| 40 |
+
class Web3CoPilotService:
|
| 41 |
+
def __init__(self):
|
| 42 |
+
try:
|
| 43 |
+
logger.info("π Initializing Web3CoPilotService...")
|
| 44 |
+
logger.info(f"π GEMINI_API_KEY configured: {'Yes' if config.GEMINI_API_KEY else 'No'}")
|
| 45 |
+
|
| 46 |
+
if config.GEMINI_API_KEY:
|
| 47 |
+
logger.info("π€ Initializing AI agent...")
|
| 48 |
+
self.agent = Web3ResearchAgent()
|
| 49 |
+
logger.info("β
AI agent initialized successfully")
|
| 50 |
+
else:
|
| 51 |
+
logger.warning("β οΈ GEMINI_API_KEY not found - AI features disabled")
|
| 52 |
+
self.agent = None
|
| 53 |
+
|
| 54 |
+
logger.info("π Initializing AIRAA integration...")
|
| 55 |
+
self.airaa = AIRAAIntegration()
|
| 56 |
+
logger.info(f"π AIRAA integration: {'Enabled' if self.airaa.enabled else 'Disabled'}")
|
| 57 |
+
|
| 58 |
+
self.enabled = bool(config.GEMINI_API_KEY)
|
| 59 |
+
logger.info(f"π― Web3CoPilotService initialized successfully (AI enabled: {self.enabled})")
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logger.error(f"β Service initialization failed: {e}")
|
| 63 |
+
self.agent = None
|
| 64 |
+
self.airaa = None
|
| 65 |
+
self.enabled = False
|
| 66 |
+
|
| 67 |
+
async def process_query(self, query: str) -> QueryResponse:
|
| 68 |
+
logger.info(f"π Processing query: {query[:50]}{'...' if len(query) > 50 else ''}")
|
| 69 |
+
|
| 70 |
+
if not query.strip():
|
| 71 |
+
logger.warning("β οΈ Empty query received")
|
| 72 |
+
return QueryResponse(success=False, response="Please enter a query.", error="Empty query")
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
if not self.enabled:
|
| 76 |
+
logger.info("π§ AI disabled - providing limited response")
|
| 77 |
+
response = """β οΈ **AI Agent Disabled**: GEMINI_API_KEY not configured.
|
| 78 |
+
|
| 79 |
+
**Limited Data Available:**
|
| 80 |
+
- CoinGecko API (basic crypto data)
|
| 81 |
+
- DeFiLlama API (DeFi protocols)
|
| 82 |
+
- Etherscan API (gas prices)
|
| 83 |
+
|
| 84 |
+
Please configure GEMINI_API_KEY for full AI analysis."""
|
| 85 |
+
return QueryResponse(success=True, response=response, sources=["Configuration"])
|
| 86 |
+
|
| 87 |
+
logger.info("π€ Sending query to AI agent...")
|
| 88 |
+
result = await self.agent.research_query(query)
|
| 89 |
+
logger.info(f"β
AI agent responded: {result.get('success', False)}")
|
| 90 |
+
|
| 91 |
+
if result.get("success"):
|
| 92 |
+
response = result.get("result", "No response generated")
|
| 93 |
+
sources = result.get("sources", [])
|
| 94 |
+
metadata = result.get("metadata", {})
|
| 95 |
+
|
| 96 |
+
# Send to AIRAA if enabled
|
| 97 |
+
if self.airaa and self.airaa.enabled:
|
| 98 |
+
try:
|
| 99 |
+
logger.info("π Sending data to AIRAA...")
|
| 100 |
+
await self.airaa.send_research_data(query, response)
|
| 101 |
+
logger.info("β
Data sent to AIRAA successfully")
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.warning(f"β οΈ AIRAA integration failed: {e}")
|
| 104 |
+
|
| 105 |
+
logger.info("β
Query processed successfully")
|
| 106 |
+
return QueryResponse(success=True, response=response, sources=sources, metadata=metadata)
|
| 107 |
+
else:
|
| 108 |
+
error_msg = result.get("error", "Research failed. Please try again.")
|
| 109 |
+
logger.error(f"β AI agent failed: {error_msg}")
|
| 110 |
+
return QueryResponse(success=False, response=error_msg, error=error_msg)
|
| 111 |
+
|
| 112 |
+
except Exception as e:
|
| 113 |
+
logger.error(f"β Query processing error: {e}")
|
| 114 |
+
error_msg = f"Error processing query: {str(e)}"
|
| 115 |
+
return QueryResponse(success=False, response=error_msg, error=error_msg)
|
| 116 |
+
|
| 117 |
+
# Initialize service
|
| 118 |
+
logger.info("π Starting Web3 Research Co-Pilot...")
|
| 119 |
+
service = Web3CoPilotService()
|
| 120 |
+
|
| 121 |
+
# API Routes
|
| 122 |
+
@app.get("/", response_class=HTMLResponse)
|
| 123 |
+
async def get_homepage(request: Request):
|
| 124 |
+
logger.info("π Serving homepage")
|
| 125 |
+
html_content = """
|
| 126 |
+
<!DOCTYPE html>
|
| 127 |
+
<html lang="en">
|
| 128 |
+
<head>
|
| 129 |
+
<meta charset="UTF-8">
|
| 130 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 131 |
+
<title>Web3 Research Co-Pilot</title>
|
| 132 |
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>π</text></svg>">
|
| 133 |
+
<style>
|
| 134 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 135 |
+
body {
|
| 136 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
| 137 |
+
background: linear-gradient(135deg, #0f1419 0%, #1a1f2e 100%);
|
| 138 |
+
color: #e6e6e6;
|
| 139 |
+
min-height: 100vh;
|
| 140 |
+
overflow-x: hidden;
|
| 141 |
+
}
|
| 142 |
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
| 143 |
+
.header {
|
| 144 |
+
text-align: center;
|
| 145 |
+
margin-bottom: 30px;
|
| 146 |
+
background: linear-gradient(135deg, #00d4aa, #4a9eff);
|
| 147 |
+
-webkit-background-clip: text;
|
| 148 |
+
-webkit-text-fill-color: transparent;
|
| 149 |
+
background-clip: text;
|
| 150 |
+
}
|
| 151 |
+
.header h1 {
|
| 152 |
+
font-size: 3em;
|
| 153 |
+
margin-bottom: 10px;
|
| 154 |
+
font-weight: 700;
|
| 155 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
| 156 |
+
}
|
| 157 |
+
.header p {
|
| 158 |
+
color: #b0b0b0;
|
| 159 |
+
font-size: 1.2em;
|
| 160 |
+
font-weight: 300;
|
| 161 |
+
}
|
| 162 |
+
.status {
|
| 163 |
+
padding: 15px;
|
| 164 |
+
border-radius: 12px;
|
| 165 |
+
margin-bottom: 25px;
|
| 166 |
+
text-align: center;
|
| 167 |
+
font-weight: 500;
|
| 168 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
| 169 |
+
transition: all 0.3s ease;
|
| 170 |
+
}
|
| 171 |
+
.status.enabled {
|
| 172 |
+
background: linear-gradient(135deg, #1a4d3a, #2a5d4a);
|
| 173 |
+
border: 2px solid #00d4aa;
|
| 174 |
+
color: #00d4aa;
|
| 175 |
+
}
|
| 176 |
+
.status.disabled {
|
| 177 |
+
background: linear-gradient(135deg, #4d1a1a, #5d2a2a);
|
| 178 |
+
border: 2px solid #ff6b6b;
|
| 179 |
+
color: #ff6b6b;
|
| 180 |
+
}
|
| 181 |
+
.status.checking {
|
| 182 |
+
background: linear-gradient(135deg, #3a3a1a, #4a4a2a);
|
| 183 |
+
border: 2px solid #ffdd59;
|
| 184 |
+
color: #ffdd59;
|
| 185 |
+
animation: pulse 1.5s infinite;
|
| 186 |
+
}
|
| 187 |
+
@keyframes pulse {
|
| 188 |
+
0% { opacity: 1; }
|
| 189 |
+
50% { opacity: 0.7; }
|
| 190 |
+
100% { opacity: 1; }
|
| 191 |
+
}
|
| 192 |
+
.chat-container {
|
| 193 |
+
background: rgba(26, 26, 26, 0.8);
|
| 194 |
+
border-radius: 16px;
|
| 195 |
+
padding: 25px;
|
| 196 |
+
margin-bottom: 25px;
|
| 197 |
+
backdrop-filter: blur(10px);
|
| 198 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 199 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
| 200 |
+
}
|
| 201 |
+
.chat-messages {
|
| 202 |
+
height: 450px;
|
| 203 |
+
overflow-y: auto;
|
| 204 |
+
background: rgba(10, 10, 10, 0.6);
|
| 205 |
+
border-radius: 12px;
|
| 206 |
+
padding: 20px;
|
| 207 |
+
margin-bottom: 20px;
|
| 208 |
+
border: 1px solid rgba(255,255,255,0.05);
|
| 209 |
+
}
|
| 210 |
+
.chat-messages::-webkit-scrollbar { width: 6px; }
|
| 211 |
+
.chat-messages::-webkit-scrollbar-track { background: #2a2a2a; border-radius: 3px; }
|
| 212 |
+
.chat-messages::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
|
| 213 |
+
.chat-messages::-webkit-scrollbar-thumb:hover { background: #777; }
|
| 214 |
+
.message {
|
| 215 |
+
margin-bottom: 20px;
|
| 216 |
+
padding: 16px;
|
| 217 |
+
border-radius: 12px;
|
| 218 |
+
transition: all 0.3s ease;
|
| 219 |
+
position: relative;
|
| 220 |
+
}
|
| 221 |
+
.message:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
|
| 222 |
+
.message.user {
|
| 223 |
+
background: linear-gradient(135deg, #2a2a3a, #3a3a4a);
|
| 224 |
+
border-left: 4px solid #00d4aa;
|
| 225 |
+
margin-left: 50px;
|
| 226 |
+
}
|
| 227 |
+
.message.assistant {
|
| 228 |
+
background: linear-gradient(135deg, #1a2a1a, #2a3a2a);
|
| 229 |
+
border-left: 4px solid #4a9eff;
|
| 230 |
+
margin-right: 50px;
|
| 231 |
+
}
|
| 232 |
+
.message .sender {
|
| 233 |
+
font-weight: 600;
|
| 234 |
+
margin-bottom: 8px;
|
| 235 |
+
font-size: 0.9em;
|
| 236 |
+
display: flex;
|
| 237 |
+
align-items: center;
|
| 238 |
+
gap: 8px;
|
| 239 |
+
}
|
| 240 |
+
.message.user .sender { color: #00d4aa; }
|
| 241 |
+
.message.assistant .sender { color: #4a9eff; }
|
| 242 |
+
.message .content { line-height: 1.6; }
|
| 243 |
+
.input-container {
|
| 244 |
+
display: flex;
|
| 245 |
+
gap: 12px;
|
| 246 |
+
align-items: stretch;
|
| 247 |
+
}
|
| 248 |
+
.input-container input {
|
| 249 |
+
flex: 1;
|
| 250 |
+
padding: 16px;
|
| 251 |
+
border: 2px solid #333;
|
| 252 |
+
background: rgba(42, 42, 42, 0.8);
|
| 253 |
+
color: #e6e6e6;
|
| 254 |
+
border-radius: 12px;
|
| 255 |
+
font-size: 16px;
|
| 256 |
+
backdrop-filter: blur(10px);
|
| 257 |
+
transition: all 0.3s ease;
|
| 258 |
+
}
|
| 259 |
+
.input-container input:focus {
|
| 260 |
+
outline: none;
|
| 261 |
+
border-color: #00d4aa;
|
| 262 |
+
box-shadow: 0 0 0 3px rgba(0, 212, 170, 0.2);
|
| 263 |
+
}
|
| 264 |
+
.input-container input::placeholder { color: #888; }
|
| 265 |
+
.input-container button {
|
| 266 |
+
padding: 16px 24px;
|
| 267 |
+
background: linear-gradient(135deg, #00d4aa, #00b894);
|
| 268 |
+
color: #000;
|
| 269 |
+
border: none;
|
| 270 |
+
border-radius: 12px;
|
| 271 |
+
cursor: pointer;
|
| 272 |
+
font-weight: 600;
|
| 273 |
+
font-size: 16px;
|
| 274 |
+
transition: all 0.3s ease;
|
| 275 |
+
white-space: nowrap;
|
| 276 |
+
}
|
| 277 |
+
.input-container button:hover:not(:disabled) {
|
| 278 |
+
background: linear-gradient(135deg, #00b894, #00a085);
|
| 279 |
+
transform: translateY(-2px);
|
| 280 |
+
box-shadow: 0 4px 12px rgba(0, 212, 170, 0.3);
|
| 281 |
+
}
|
| 282 |
+
.input-container button:active { transform: translateY(0); }
|
| 283 |
+
.input-container button:disabled {
|
| 284 |
+
background: #666;
|
| 285 |
+
cursor: not-allowed;
|
| 286 |
+
transform: none;
|
| 287 |
+
box-shadow: none;
|
| 288 |
+
}
|
| 289 |
+
.examples {
|
| 290 |
+
display: grid;
|
| 291 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 292 |
+
gap: 15px;
|
| 293 |
+
margin-top: 25px;
|
| 294 |
+
}
|
| 295 |
+
.example-btn {
|
| 296 |
+
padding: 16px;
|
| 297 |
+
background: linear-gradient(135deg, #2a2a3a, #3a3a4a);
|
| 298 |
+
border: 2px solid #444;
|
| 299 |
+
border-radius: 12px;
|
| 300 |
+
cursor: pointer;
|
| 301 |
+
text-align: center;
|
| 302 |
+
transition: all 0.3s ease;
|
| 303 |
+
font-weight: 500;
|
| 304 |
+
position: relative;
|
| 305 |
+
overflow: hidden;
|
| 306 |
+
}
|
| 307 |
+
.example-btn:before {
|
| 308 |
+
content: '';
|
| 309 |
+
position: absolute;
|
| 310 |
+
top: 0;
|
| 311 |
+
left: -100%;
|
| 312 |
+
width: 100%;
|
| 313 |
+
height: 100%;
|
| 314 |
+
background: linear-gradient(90deg, transparent, rgba(0, 212, 170, 0.1), transparent);
|
| 315 |
+
transition: left 0.5s;
|
| 316 |
+
}
|
| 317 |
+
.example-btn:hover:before { left: 100%; }
|
| 318 |
+
.example-btn:hover {
|
| 319 |
+
background: linear-gradient(135deg, #3a3a4a, #4a4a5a);
|
| 320 |
+
border-color: #00d4aa;
|
| 321 |
+
transform: translateY(-3px);
|
| 322 |
+
box-shadow: 0 6px 20px rgba(0, 212, 170, 0.2);
|
| 323 |
+
}
|
| 324 |
+
.loading {
|
| 325 |
+
color: #ffdd59;
|
| 326 |
+
font-style: italic;
|
| 327 |
+
display: flex;
|
| 328 |
+
align-items: center;
|
| 329 |
+
gap: 8px;
|
| 330 |
+
}
|
| 331 |
+
.loading:after {
|
| 332 |
+
content: '';
|
| 333 |
+
width: 12px;
|
| 334 |
+
height: 12px;
|
| 335 |
+
border: 2px solid #ffdd59;
|
| 336 |
+
border-top: 2px solid transparent;
|
| 337 |
+
border-radius: 50%;
|
| 338 |
+
animation: spin 1s linear infinite;
|
| 339 |
+
}
|
| 340 |
+
@keyframes spin {
|
| 341 |
+
0% { transform: rotate(0deg); }
|
| 342 |
+
100% { transform: rotate(360deg); }
|
| 343 |
+
}
|
| 344 |
+
.sources {
|
| 345 |
+
margin-top: 12px;
|
| 346 |
+
font-size: 0.85em;
|
| 347 |
+
color: #999;
|
| 348 |
+
display: flex;
|
| 349 |
+
flex-wrap: wrap;
|
| 350 |
+
gap: 6px;
|
| 351 |
+
}
|
| 352 |
+
.sources .label { margin-right: 8px; font-weight: 600; }
|
| 353 |
+
.sources span {
|
| 354 |
+
background: rgba(51, 51, 51, 0.8);
|
| 355 |
+
padding: 4px 8px;
|
| 356 |
+
border-radius: 6px;
|
| 357 |
+
font-size: 0.8em;
|
| 358 |
+
border: 1px solid #555;
|
| 359 |
+
}
|
| 360 |
+
.welcome-message {
|
| 361 |
+
background: linear-gradient(135deg, #1a2a4a, #2a3a5a);
|
| 362 |
+
border-left: 4px solid #4a9eff;
|
| 363 |
+
border-radius: 12px;
|
| 364 |
+
padding: 16px;
|
| 365 |
+
margin-bottom: 20px;
|
| 366 |
+
text-align: center;
|
| 367 |
+
}
|
| 368 |
+
.footer {
|
| 369 |
+
text-align: center;
|
| 370 |
+
margin-top: 30px;
|
| 371 |
+
color: #666;
|
| 372 |
+
font-size: 0.9em;
|
| 373 |
+
}
|
| 374 |
+
</style>
|
| 375 |
+
</head>
|
| 376 |
+
<body>
|
| 377 |
+
<div class="container">
|
| 378 |
+
<div class="header">
|
| 379 |
+
<h1>π Web3 Research Co-Pilot</h1>
|
| 380 |
+
<p>AI-powered cryptocurrency research assistant</p>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
<div id="status" class="status checking">
|
| 384 |
+
<span>π Checking system status...</span>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<div class="chat-container">
|
| 388 |
+
<div id="chatMessages" class="chat-messages">
|
| 389 |
+
<div class="welcome-message">
|
| 390 |
+
<div class="sender">π€ AI Research Assistant</div>
|
| 391 |
+
<div>π Welcome! I'm your Web3 Research Co-Pilot. Ask me anything about cryptocurrency markets, DeFi protocols, blockchain analysis, or trading insights.</div>
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
<div class="input-container">
|
| 395 |
+
<input type="text" id="queryInput" placeholder="Ask about Bitcoin, Ethereum, DeFi yields, market analysis..." maxlength="500">
|
| 396 |
+
<button id="sendBtn" onclick="sendQuery()">π Research</button>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
|
| 400 |
+
<div class="examples">
|
| 401 |
+
<div class="example-btn" onclick="setQuery('What is the current Bitcoin price and market sentiment?')">
|
| 402 |
+
π Bitcoin Analysis
|
| 403 |
+
</div>
|
| 404 |
+
<div class="example-btn" onclick="setQuery('Show me the top DeFi protocols by TVL')">
|
| 405 |
+
π¦ DeFi Overview
|
| 406 |
+
</div>
|
| 407 |
+
<div class="example-btn" onclick="setQuery('What are the trending cryptocurrencies today?')">
|
| 408 |
+
π₯ Trending Coins
|
| 409 |
+
</div>
|
| 410 |
+
<div class="example-btn" onclick="setQuery('Analyze Ethereum gas prices and network activity')">
|
| 411 |
+
β½ Gas Tracker
|
| 412 |
+
</div>
|
| 413 |
+
<div class="example-btn" onclick="setQuery('Find the best yield farming opportunities')">
|
| 414 |
+
πΎ Yield Farming
|
| 415 |
+
</div>
|
| 416 |
+
<div class="example-btn" onclick="setQuery('Compare Solana vs Ethereum ecosystems')">
|
| 417 |
+
βοΈ Ecosystem Compare
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
<div class="footer">
|
| 422 |
+
<p>Powered by AI β’ Real-time Web3 data β’ Built with β€οΈ</p>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
<script>
|
| 427 |
+
let chatHistory = [];
|
| 428 |
+
|
| 429 |
+
async function checkStatus() {
|
| 430 |
+
try {
|
| 431 |
+
console.log('π Checking system status...');
|
| 432 |
+
const response = await fetch('/status');
|
| 433 |
+
const status = await response.json();
|
| 434 |
+
console.log('π Status received:', status);
|
| 435 |
+
|
| 436 |
+
const statusDiv = document.getElementById('status');
|
| 437 |
+
|
| 438 |
+
if (status.enabled && status.gemini_configured) {
|
| 439 |
+
statusDiv.className = 'status enabled';
|
| 440 |
+
statusDiv.innerHTML = `
|
| 441 |
+
<span>β
AI Research Agent: Online</span><br>
|
| 442 |
+
<small>Tools available: ${status.tools_available.join(', ')}</small>
|
| 443 |
+
`;
|
| 444 |
+
console.log('β
System fully operational');
|
| 445 |
+
} else {
|
| 446 |
+
statusDiv.className = 'status disabled';
|
| 447 |
+
statusDiv.innerHTML = `
|
| 448 |
+
<span>β οΈ Limited Mode: GEMINI_API_KEY not configured</span><br>
|
| 449 |
+
<small>Basic data available: ${status.tools_available.join(', ')}</small>
|
| 450 |
+
`;
|
| 451 |
+
console.log('β οΈ System in limited mode');
|
| 452 |
+
}
|
| 453 |
+
} catch (error) {
|
| 454 |
+
console.error('β Status check failed:', error);
|
| 455 |
+
const statusDiv = document.getElementById('status');
|
| 456 |
+
statusDiv.className = 'status disabled';
|
| 457 |
+
statusDiv.innerHTML = '<span>β Connection Error</span>';
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
async function sendQuery() {
|
| 462 |
+
const input = document.getElementById('queryInput');
|
| 463 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 464 |
+
const query = input.value.trim();
|
| 465 |
+
|
| 466 |
+
if (!query) {
|
| 467 |
+
input.focus();
|
| 468 |
+
return;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
console.log('π€ Sending query:', query);
|
| 472 |
+
|
| 473 |
+
// Add user message
|
| 474 |
+
addMessage('user', query);
|
| 475 |
+
input.value = '';
|
| 476 |
+
|
| 477 |
+
// Show loading
|
| 478 |
+
sendBtn.disabled = true;
|
| 479 |
+
sendBtn.innerHTML = '<span class="loading">Processing</span>';
|
| 480 |
+
|
| 481 |
+
try {
|
| 482 |
+
const response = await fetch('/query', {
|
| 483 |
+
method: 'POST',
|
| 484 |
+
headers: { 'Content-Type': 'application/json' },
|
| 485 |
+
body: JSON.stringify({ query, chat_history: chatHistory })
|
| 486 |
+
});
|
| 487 |
+
|
| 488 |
+
const result = await response.json();
|
| 489 |
+
console.log('π₯ Response received:', result);
|
| 490 |
+
|
| 491 |
+
if (result.success) {
|
| 492 |
+
addMessage('assistant', result.response, result.sources);
|
| 493 |
+
console.log('β
Query processed successfully');
|
| 494 |
+
} else {
|
| 495 |
+
addMessage('assistant', result.response || 'An error occurred');
|
| 496 |
+
console.log('β οΈ Query failed:', result.error);
|
| 497 |
+
}
|
| 498 |
+
} catch (error) {
|
| 499 |
+
console.error('β Network error:', error);
|
| 500 |
+
addMessage('assistant', 'β Network error. Please check your connection and try again.');
|
| 501 |
+
} finally {
|
| 502 |
+
sendBtn.disabled = false;
|
| 503 |
+
sendBtn.innerHTML = 'π Research';
|
| 504 |
+
input.focus();
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
function addMessage(sender, content, sources = []) {
|
| 509 |
+
console.log(`π¬ Adding ${sender} message`);
|
| 510 |
+
const messagesDiv = document.getElementById('chatMessages');
|
| 511 |
+
const messageDiv = document.createElement('div');
|
| 512 |
+
messageDiv.className = `message ${sender}`;
|
| 513 |
+
|
| 514 |
+
let sourcesHtml = '';
|
| 515 |
+
if (sources && sources.length > 0) {
|
| 516 |
+
sourcesHtml = `<div class="sources"><span class="label">Sources:</span> ${sources.map(s => `<span>${s}</span>`).join('')}</div>`;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
const senderIcon = sender === 'user' ? 'π€' : 'π€';
|
| 520 |
+
const senderName = sender === 'user' ? 'You' : 'AI Research Assistant';
|
| 521 |
+
|
| 522 |
+
messageDiv.innerHTML = `
|
| 523 |
+
<div class="sender">${senderIcon} ${senderName}</div>
|
| 524 |
+
<div class="content">${content.replace(/\n/g, '<br>')}</div>
|
| 525 |
+
${sourcesHtml}
|
| 526 |
+
`;
|
| 527 |
+
|
| 528 |
+
messagesDiv.appendChild(messageDiv);
|
| 529 |
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
| 530 |
+
|
| 531 |
+
// Update chat history
|
| 532 |
+
chatHistory.push({ role: sender, content });
|
| 533 |
+
if (chatHistory.length > 20) chatHistory = chatHistory.slice(-20);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function setQuery(query) {
|
| 537 |
+
console.log('π Setting query:', query);
|
| 538 |
+
const input = document.getElementById('queryInput');
|
| 539 |
+
input.value = query;
|
| 540 |
+
input.focus();
|
| 541 |
+
|
| 542 |
+
// Optional: auto-send after a short delay
|
| 543 |
+
setTimeout(() => {
|
| 544 |
+
if (input.value === query) { // Only if user didn't change it
|
| 545 |
+
sendQuery();
|
| 546 |
+
}
|
| 547 |
+
}, 100);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
// Handle Enter key
|
| 551 |
+
document.getElementById('queryInput').addEventListener('keypress', function(e) {
|
| 552 |
+
if (e.key === 'Enter') {
|
| 553 |
+
sendQuery();
|
| 554 |
+
}
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Initialize
|
| 558 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 559 |
+
console.log('π Web3 Research Co-Pilot initialized');
|
| 560 |
+
checkStatus();
|
| 561 |
+
document.getElementById('queryInput').focus();
|
| 562 |
+
});
|
| 563 |
+
</script>
|
| 564 |
+
</body>
|
| 565 |
+
</html>
|
| 566 |
+
"""
|
| 567 |
+
return HTMLResponse(content=html_content)
|
| 568 |
+
|
| 569 |
+
@app.get("/status")
|
| 570 |
+
async def get_status():
|
| 571 |
+
logger.info("π Status endpoint called")
|
| 572 |
+
status = {
|
| 573 |
+
"enabled": service.enabled,
|
| 574 |
+
"gemini_configured": bool(config.GEMINI_API_KEY),
|
| 575 |
+
"tools_available": ["CoinGecko", "DeFiLlama", "Etherscan"],
|
| 576 |
+
"airaa_enabled": service.airaa.enabled if service.airaa else False,
|
| 577 |
+
"timestamp": datetime.now().isoformat()
|
| 578 |
+
}
|
| 579 |
+
logger.info(f"π Status response: {status}")
|
| 580 |
+
return status
|
| 581 |
+
|
| 582 |
+
@app.post("/query", response_model=QueryResponse)
|
| 583 |
+
async def process_query(request: QueryRequest):
|
| 584 |
+
logger.info(f"π₯ Query endpoint called: {request.query[:50]}{'...' if len(request.query) > 50 else ''}")
|
| 585 |
+
result = await service.process_query(request.query)
|
| 586 |
+
logger.info(f"π€ Query response: success={result.success}")
|
| 587 |
+
return result
|
| 588 |
+
|
| 589 |
+
@app.get("/health")
|
| 590 |
+
async def health_check():
|
| 591 |
+
logger.info("β€οΈ Health check endpoint called")
|
| 592 |
+
return {
|
| 593 |
+
"status": "healthy",
|
| 594 |
+
"timestamp": datetime.now().isoformat(),
|
| 595 |
+
"service_enabled": service.enabled,
|
| 596 |
+
"version": "1.0.0"
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
if __name__ == "__main__":
|
| 600 |
+
import uvicorn
|
| 601 |
+
logger.info("π Starting FastAPI server...")
|
| 602 |
+
uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
|
attached_assets/Pasted--Complete-Web3-Research-Co-Pilot-Project-Plan-Structure-Project-Directory-Structure--1754811430335_1754811430338.txt
DELETED
|
@@ -1,992 +0,0 @@
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
minimal_test.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
import sys
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
sys.path.insert(0, os.path.dirname(__file__))
|
| 5 |
-
|
| 6 |
-
def minimal_test():
|
| 7 |
-
try:
|
| 8 |
-
print("Testing minimal imports...")
|
| 9 |
-
|
| 10 |
-
from src.utils.config import config
|
| 11 |
-
print("β
Config imported")
|
| 12 |
-
|
| 13 |
-
from src.utils.logger import get_logger
|
| 14 |
-
print("β
Logger imported")
|
| 15 |
-
|
| 16 |
-
from src.tools.base_tool import BaseWeb3Tool
|
| 17 |
-
print("β
Base tool imported")
|
| 18 |
-
|
| 19 |
-
from src.tools.coingecko_tool import CoinGeckoTool
|
| 20 |
-
tool = CoinGeckoTool()
|
| 21 |
-
print("β
CoinGecko tool created")
|
| 22 |
-
|
| 23 |
-
from src.agent.research_agent import Web3ResearchAgent
|
| 24 |
-
print("β
Research agent imported")
|
| 25 |
-
|
| 26 |
-
print("π All core components working!")
|
| 27 |
-
return True
|
| 28 |
-
|
| 29 |
-
except Exception as e:
|
| 30 |
-
print(f"β Error: {e}")
|
| 31 |
-
import traceback
|
| 32 |
-
traceback.print_exc()
|
| 33 |
-
return False
|
| 34 |
-
|
| 35 |
-
if __name__ == "__main__":
|
| 36 |
-
minimal_test()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,14 +1,12 @@
|
|
| 1 |
langchain
|
| 2 |
langchain-google-genai
|
| 3 |
langchain-community
|
| 4 |
-
gradio
|
| 5 |
aiohttp
|
| 6 |
tenacity
|
| 7 |
-
plotly
|
| 8 |
-
pandas
|
| 9 |
-
numpy
|
| 10 |
pydantic
|
| 11 |
python-dotenv
|
| 12 |
diskcache
|
| 13 |
google-generativeai
|
| 14 |
asyncio-throttle
|
|
|
|
|
|
|
|
|
| 1 |
langchain
|
| 2 |
langchain-google-genai
|
| 3 |
langchain-community
|
|
|
|
| 4 |
aiohttp
|
| 5 |
tenacity
|
|
|
|
|
|
|
|
|
|
| 6 |
pydantic
|
| 7 |
python-dotenv
|
| 8 |
diskcache
|
| 9 |
google-generativeai
|
| 10 |
asyncio-throttle
|
| 11 |
+
fastapi
|
| 12 |
+
uvicorn
|
run.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
|
| 3 |
-
"""
|
| 4 |
-
Web3 Research Co-Pilot Application
|
| 5 |
-
Complete production-ready crypto research assistant powered by AI
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import sys
|
| 9 |
-
import os
|
| 10 |
-
import asyncio
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
# Add project root to path
|
| 14 |
-
project_root = Path(__file__).parent
|
| 15 |
-
sys.path.insert(0, str(project_root))
|
| 16 |
-
|
| 17 |
-
def main():
|
| 18 |
-
print("π Starting Web3 Research Co-Pilot...")
|
| 19 |
-
|
| 20 |
-
try:
|
| 21 |
-
from app import Web3CoPilotApp
|
| 22 |
-
|
| 23 |
-
app = Web3CoPilotApp()
|
| 24 |
-
interface = app.create_interface()
|
| 25 |
-
|
| 26 |
-
print("β
Application initialized successfully!")
|
| 27 |
-
print("π Launching web interface...")
|
| 28 |
-
print("π Local URL: http://localhost:7860")
|
| 29 |
-
|
| 30 |
-
interface.launch(
|
| 31 |
-
server_name="0.0.0.0",
|
| 32 |
-
server_port=7860,
|
| 33 |
-
share=False,
|
| 34 |
-
show_api=False,
|
| 35 |
-
quiet=False
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
except ImportError as e:
|
| 39 |
-
print(f"β Import error: {e}")
|
| 40 |
-
print("Please install dependencies: pip install -r requirements.txt")
|
| 41 |
-
sys.exit(1)
|
| 42 |
-
except Exception as e:
|
| 43 |
-
print(f"β Application error: {e}")
|
| 44 |
-
sys.exit(1)
|
| 45 |
-
|
| 46 |
-
if __name__ == "__main__":
|
| 47 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (147 Bytes)
|
|
|
src/__pycache__/api_clients.cpython-311.pyc
DELETED
|
Binary file (11.9 kB)
|
|
|
src/__pycache__/cache_manager.cpython-311.pyc
DELETED
|
Binary file (3.19 kB)
|
|
|
src/__pycache__/config.cpython-311.pyc
DELETED
|
Binary file (1.36 kB)
|
|
|
src/__pycache__/defillama_client.cpython-311.pyc
DELETED
|
Binary file (5.63 kB)
|
|
|
src/__pycache__/enhanced_agent.cpython-311.pyc
DELETED
|
Binary file (19 kB)
|
|
|
src/__pycache__/news_aggregator.cpython-311.pyc
DELETED
|
Binary file (6.04 kB)
|
|
|
src/__pycache__/portfolio_analyzer.cpython-311.pyc
DELETED
|
Binary file (11.8 kB)
|
|
|
src/__pycache__/research_agent.cpython-311.pyc
DELETED
|
Binary file (12.4 kB)
|
|
|
src/__pycache__/visualizations.cpython-311.pyc
DELETED
|
Binary file (11.8 kB)
|
|
|
src/agent/memory_manager.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.memory import ConversationBufferWindowMemory
|
| 2 |
+
from typing import Dict, Any, List, Optional
|
| 3 |
+
|
| 4 |
+
class MemoryManager:
|
| 5 |
+
"""Enhanced conversation memory management"""
|
| 6 |
+
|
| 7 |
+
def __init__(self, window_size: int = 10):
|
| 8 |
+
self.memory = ConversationBufferWindowMemory(
|
| 9 |
+
k=window_size,
|
| 10 |
+
return_messages=True,
|
| 11 |
+
memory_key="chat_history"
|
| 12 |
+
)
|
| 13 |
+
self.context_cache: Dict[str, Any] = {}
|
| 14 |
+
|
| 15 |
+
def add_interaction(self, query: str, response: str, metadata: Optional[Dict[str, Any]] = None):
|
| 16 |
+
"""Add user interaction to memory with metadata"""
|
| 17 |
+
self.memory.save_context(
|
| 18 |
+
{"input": query},
|
| 19 |
+
{"output": response}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
if metadata:
|
| 23 |
+
self.context_cache[query[:50]] = metadata
|
| 24 |
+
|
| 25 |
+
def get_relevant_context(self, query: str) -> Dict[str, Any]:
|
| 26 |
+
"""Retrieve relevant context for current query"""
|
| 27 |
+
return {
|
| 28 |
+
"history": self.memory.load_memory_variables({}),
|
| 29 |
+
"cached_context": self._find_similar_context(query)
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def _find_similar_context(self, query: str) -> List[Dict[str, Any]]:
|
| 33 |
+
"""Find contextually similar previous interactions"""
|
| 34 |
+
query_lower = query.lower()
|
| 35 |
+
relevant = []
|
| 36 |
+
|
| 37 |
+
for cached_key, context in self.context_cache.items():
|
| 38 |
+
if any(word in cached_key.lower() for word in query_lower.split()[:3]):
|
| 39 |
+
relevant.append(context)
|
| 40 |
+
|
| 41 |
+
return relevant[:3]
|
| 42 |
+
|
| 43 |
+
def clear_memory(self):
|
| 44 |
+
"""Clear conversation memory and cache"""
|
| 45 |
+
self.memory.clear()
|
| 46 |
+
self.context_cache.clear()
|
src/agent/research_agent.py
CHANGED
|
@@ -10,17 +10,24 @@ from src.tools.coingecko_tool import CoinGeckoTool
|
|
| 10 |
from src.tools.defillama_tool import DeFiLlamaTool
|
| 11 |
from src.tools.etherscan_tool import EtherscanTool
|
| 12 |
from src.agent.query_planner import QueryPlanner
|
| 13 |
-
from src.config import config
|
| 14 |
from src.utils.logger import get_logger
|
| 15 |
|
| 16 |
logger = get_logger(__name__)
|
| 17 |
|
| 18 |
class Web3ResearchAgent:
|
| 19 |
def __init__(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
-
if not config.GEMINI_API_KEY:
|
| 22 |
-
raise ValueError("GEMINI_API_KEY not configured")
|
| 23 |
-
|
| 24 |
self.llm = ChatGoogleGenerativeAI(
|
| 25 |
model="gemini-1.5-flash",
|
| 26 |
google_api_key=config.GEMINI_API_KEY,
|
|
@@ -28,7 +35,7 @@ class Web3ResearchAgent:
|
|
| 28 |
max_tokens=2048
|
| 29 |
)
|
| 30 |
|
| 31 |
-
self.tools =
|
| 32 |
self.query_planner = QueryPlanner(self.llm)
|
| 33 |
self.memory = ConversationBufferWindowMemory(
|
| 34 |
memory_key="chat_history", return_messages=True, k=10
|
|
@@ -39,9 +46,35 @@ class Web3ResearchAgent:
|
|
| 39 |
agent=self.agent, tools=self.tools, memory=self.memory,
|
| 40 |
verbose=False, max_iterations=5, handle_parsing_errors=True
|
| 41 |
)
|
|
|
|
|
|
|
|
|
|
| 42 |
except Exception as e:
|
| 43 |
logger.error(f"Agent init failed: {e}")
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def _create_agent(self):
|
| 47 |
prompt = ChatPromptTemplate.from_messages([
|
|
@@ -57,6 +90,16 @@ class Web3ResearchAgent:
|
|
| 57 |
return create_tool_calling_agent(self.llm, self.tools, prompt)
|
| 58 |
|
| 59 |
async def research_query(self, query: str) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
try:
|
| 61 |
logger.info(f"Processing: {query}")
|
| 62 |
|
|
@@ -92,6 +135,8 @@ class Web3ResearchAgent:
|
|
| 92 |
"success": False,
|
| 93 |
"query": query,
|
| 94 |
"error": str(e),
|
|
|
|
|
|
|
| 95 |
"metadata": {"timestamp": datetime.now().isoformat()}
|
| 96 |
}
|
| 97 |
|
|
|
|
| 10 |
from src.tools.defillama_tool import DeFiLlamaTool
|
| 11 |
from src.tools.etherscan_tool import EtherscanTool
|
| 12 |
from src.agent.query_planner import QueryPlanner
|
| 13 |
+
from src.utils.config import config
|
| 14 |
from src.utils.logger import get_logger
|
| 15 |
|
| 16 |
logger = get_logger(__name__)
|
| 17 |
|
| 18 |
class Web3ResearchAgent:
|
| 19 |
def __init__(self):
|
| 20 |
+
self.llm = None
|
| 21 |
+
self.tools = []
|
| 22 |
+
self.agent = None
|
| 23 |
+
self.executor = None
|
| 24 |
+
self.enabled = False
|
| 25 |
+
|
| 26 |
+
if not config.GEMINI_API_KEY:
|
| 27 |
+
logger.warning("GEMINI_API_KEY not configured - AI agent disabled")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
try:
|
|
|
|
|
|
|
|
|
|
| 31 |
self.llm = ChatGoogleGenerativeAI(
|
| 32 |
model="gemini-1.5-flash",
|
| 33 |
google_api_key=config.GEMINI_API_KEY,
|
|
|
|
| 35 |
max_tokens=2048
|
| 36 |
)
|
| 37 |
|
| 38 |
+
self.tools = self._initialize_tools()
|
| 39 |
self.query_planner = QueryPlanner(self.llm)
|
| 40 |
self.memory = ConversationBufferWindowMemory(
|
| 41 |
memory_key="chat_history", return_messages=True, k=10
|
|
|
|
| 46 |
agent=self.agent, tools=self.tools, memory=self.memory,
|
| 47 |
verbose=False, max_iterations=5, handle_parsing_errors=True
|
| 48 |
)
|
| 49 |
+
self.enabled = True
|
| 50 |
+
logger.info("Web3ResearchAgent initialized successfully")
|
| 51 |
+
|
| 52 |
except Exception as e:
|
| 53 |
logger.error(f"Agent init failed: {e}")
|
| 54 |
+
self.enabled = False
|
| 55 |
+
|
| 56 |
+
def _initialize_tools(self):
|
| 57 |
+
tools = []
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
tools.append(CoinGeckoTool())
|
| 61 |
+
logger.info("CoinGecko tool initialized")
|
| 62 |
+
except Exception as e:
|
| 63 |
+
logger.warning(f"CoinGecko tool failed: {e}")
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
tools.append(DeFiLlamaTool())
|
| 67 |
+
logger.info("DeFiLlama tool initialized")
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.warning(f"DeFiLlama tool failed: {e}")
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
tools.append(EtherscanTool())
|
| 73 |
+
logger.info("Etherscan tool initialized")
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.warning(f"Etherscan tool failed: {e}")
|
| 76 |
+
|
| 77 |
+
return tools
|
| 78 |
|
| 79 |
def _create_agent(self):
|
| 80 |
prompt = ChatPromptTemplate.from_messages([
|
|
|
|
| 90 |
return create_tool_calling_agent(self.llm, self.tools, prompt)
|
| 91 |
|
| 92 |
async def research_query(self, query: str) -> Dict[str, Any]:
|
| 93 |
+
if not self.enabled:
|
| 94 |
+
return {
|
| 95 |
+
"success": False,
|
| 96 |
+
"query": query,
|
| 97 |
+
"error": "AI agent not configured. Please set GEMINI_API_KEY environment variable.",
|
| 98 |
+
"result": "β **Service Unavailable**\n\nThe AI research agent requires a GEMINI_API_KEY to function.\n\nPlease:\n1. Get a free API key from [Google AI Studio](https://makersuite.google.com/app/apikey)\n2. Set environment variable: `export GEMINI_API_KEY='your_key'`\n3. Restart the application",
|
| 99 |
+
"sources": [],
|
| 100 |
+
"metadata": {"timestamp": datetime.now().isoformat()}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
try:
|
| 104 |
logger.info(f"Processing: {query}")
|
| 105 |
|
|
|
|
| 135 |
"success": False,
|
| 136 |
"query": query,
|
| 137 |
"error": str(e),
|
| 138 |
+
"result": f"β **Research Error**: {str(e)}\n\nPlease try a different query or check your API configuration.",
|
| 139 |
+
"sources": [],
|
| 140 |
"metadata": {"timestamp": datetime.now().isoformat()}
|
| 141 |
}
|
| 142 |
|
src/agent/response_formatter.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, Optional
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
class ResponseFormatter:
|
| 6 |
+
"""Formats AI agent responses for optimal user experience"""
|
| 7 |
+
|
| 8 |
+
@staticmethod
|
| 9 |
+
def format_research_response(response: str, data: Optional[Dict[str, Any]] = None) -> str:
|
| 10 |
+
"""Format research response with structured data presentation"""
|
| 11 |
+
if not response:
|
| 12 |
+
return "No information available."
|
| 13 |
+
|
| 14 |
+
formatted = response.strip()
|
| 15 |
+
|
| 16 |
+
if data:
|
| 17 |
+
if "prices" in data:
|
| 18 |
+
formatted = ResponseFormatter._add_price_formatting(formatted, data["prices"])
|
| 19 |
+
if "metrics" in data:
|
| 20 |
+
formatted = ResponseFormatter._add_metrics_formatting(formatted, data["metrics"])
|
| 21 |
+
|
| 22 |
+
formatted = ResponseFormatter._enhance_markdown(formatted)
|
| 23 |
+
return formatted
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def _add_price_formatting(text: str, prices: Dict[str, float]) -> str:
|
| 27 |
+
"""Add price data with formatting"""
|
| 28 |
+
price_section = "\n\nπ **Current Prices:**\n"
|
| 29 |
+
for symbol, price in prices.items():
|
| 30 |
+
price_section += f"β’ **{symbol.upper()}**: ${price:,.2f}\n"
|
| 31 |
+
return text + price_section
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
def _add_metrics_formatting(text: str, metrics: Dict[str, Any]) -> str:
|
| 35 |
+
"""Add metrics with formatting"""
|
| 36 |
+
metrics_section = "\n\nπ **Key Metrics:**\n"
|
| 37 |
+
for key, value in metrics.items():
|
| 38 |
+
if isinstance(value, (int, float)):
|
| 39 |
+
metrics_section += f"β’ **{key.title()}**: {value:,.2f}\n"
|
| 40 |
+
else:
|
| 41 |
+
metrics_section += f"β’ **{key.title()}**: {value}\n"
|
| 42 |
+
return text + metrics_section
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def _enhance_markdown(text: str) -> str:
|
| 46 |
+
"""Enhance markdown formatting for better readability"""
|
| 47 |
+
text = re.sub(r'\*\*([^*]+)\*\*', r'**\1**', text)
|
| 48 |
+
text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
|
| 49 |
+
return text.strip()
|
src/api/airaa_integration.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import aiohttp
|
| 2 |
import re
|
| 3 |
from typing import Dict, Any, List
|
| 4 |
-
from src.config import config
|
| 5 |
from src.utils.logger import get_logger
|
| 6 |
|
| 7 |
logger = get_logger(__name__)
|
|
|
|
| 1 |
import aiohttp
|
| 2 |
import re
|
| 3 |
from typing import Dict, Any, List
|
| 4 |
+
from src.utils.config import config
|
| 5 |
from src.utils.logger import get_logger
|
| 6 |
|
| 7 |
logger = get_logger(__name__)
|
src/api_clients.py
DELETED
|
@@ -1,158 +0,0 @@
|
|
| 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/config.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 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 |
-
ETHERSCAN_API_KEY: str = os.getenv("ETHERSCAN_API_KEY", "")
|
| 11 |
-
|
| 12 |
-
COINGECKO_BASE_URL: str = "https://api.coingecko.com/api/v3"
|
| 13 |
-
CRYPTOCOMPARE_BASE_URL: str = "https://min-api.cryptocompare.com/data"
|
| 14 |
-
|
| 15 |
-
CACHE_TTL: int = 300
|
| 16 |
-
RATE_LIMIT_DELAY: float = 2.0
|
| 17 |
-
MAX_RETRIES: int = 3
|
| 18 |
-
REQUEST_TIMEOUT: int = 30
|
| 19 |
-
|
| 20 |
-
UI_TITLE: str = "Web3 Research Co-Pilot"
|
| 21 |
-
UI_DESCRIPTION: str = "AI-powered crypto research assistant"
|
| 22 |
-
|
| 23 |
-
AIRAA_WEBHOOK_URL: Optional[str] = os.getenv("AIRAA_WEBHOOK_URL")
|
| 24 |
-
AIRAA_API_KEY: Optional[str] = os.getenv("AIRAA_API_KEY")
|
| 25 |
-
|
| 26 |
-
config = Config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/defillama_client.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,273 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,143 +0,0 @@
|
|
| 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
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 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/tools/base_tool.py
CHANGED
|
@@ -4,8 +4,11 @@ from langchain.tools import BaseTool
|
|
| 4 |
from pydantic import BaseModel, Field, PrivateAttr
|
| 5 |
import asyncio
|
| 6 |
import aiohttp
|
|
|
|
|
|
|
| 7 |
from tenacity import retry, stop_after_attempt, wait_exponential
|
| 8 |
from src.utils.logger import get_logger
|
|
|
|
| 9 |
|
| 10 |
logger = get_logger(__name__)
|
| 11 |
|
|
@@ -32,11 +35,25 @@ class BaseWeb3Tool(BaseTool, ABC):
|
|
| 32 |
|
| 33 |
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=8))
|
| 34 |
async def make_request(self, url: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
session = await self.get_session()
|
|
|
|
| 36 |
try:
|
| 37 |
async with session.get(url, params=params or {}) as response:
|
| 38 |
if response.status == 200:
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
elif response.status == 429:
|
| 41 |
await asyncio.sleep(2)
|
| 42 |
raise aiohttp.ClientResponseError(
|
|
@@ -50,6 +67,11 @@ class BaseWeb3Tool(BaseTool, ABC):
|
|
| 50 |
logger.error(f"Request failed: {e}")
|
| 51 |
raise
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
def _run(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
| 54 |
return asyncio.run(self._arun(query, filters))
|
| 55 |
|
|
@@ -60,5 +82,4 @@ class BaseWeb3Tool(BaseTool, ABC):
|
|
| 60 |
async def cleanup(self):
|
| 61 |
if self._session:
|
| 62 |
await self._session.close()
|
| 63 |
-
|
| 64 |
-
await self.session.close()
|
|
|
|
| 4 |
from pydantic import BaseModel, Field, PrivateAttr
|
| 5 |
import asyncio
|
| 6 |
import aiohttp
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
from tenacity import retry, stop_after_attempt, wait_exponential
|
| 10 |
from src.utils.logger import get_logger
|
| 11 |
+
from src.utils.cache_manager import cache_manager
|
| 12 |
|
| 13 |
logger = get_logger(__name__)
|
| 14 |
|
|
|
|
| 35 |
|
| 36 |
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=8))
|
| 37 |
async def make_request(self, url: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
| 38 |
+
# Create cache key
|
| 39 |
+
cache_key = self._create_cache_key(url, params or {})
|
| 40 |
+
|
| 41 |
+
# Check cache first
|
| 42 |
+
cached_result = cache_manager.get(cache_key)
|
| 43 |
+
if cached_result is not None:
|
| 44 |
+
logger.debug(f"Cache hit for {url}")
|
| 45 |
+
return cached_result
|
| 46 |
+
|
| 47 |
+
logger.debug(f"Cache miss for {url}")
|
| 48 |
session = await self.get_session()
|
| 49 |
+
|
| 50 |
try:
|
| 51 |
async with session.get(url, params=params or {}) as response:
|
| 52 |
if response.status == 200:
|
| 53 |
+
result = await response.json()
|
| 54 |
+
# Cache successful responses for 5 minutes
|
| 55 |
+
cache_manager.set(cache_key, result, ttl=300)
|
| 56 |
+
return result
|
| 57 |
elif response.status == 429:
|
| 58 |
await asyncio.sleep(2)
|
| 59 |
raise aiohttp.ClientResponseError(
|
|
|
|
| 67 |
logger.error(f"Request failed: {e}")
|
| 68 |
raise
|
| 69 |
|
| 70 |
+
def _create_cache_key(self, url: str, params: Dict[str, Any]) -> str:
|
| 71 |
+
"""Create a unique cache key from URL and parameters"""
|
| 72 |
+
key_data = f"{url}:{json.dumps(params, sort_keys=True)}"
|
| 73 |
+
return hashlib.md5(key_data.encode()).hexdigest()[:16]
|
| 74 |
+
|
| 75 |
def _run(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
| 76 |
return asyncio.run(self._arun(query, filters))
|
| 77 |
|
|
|
|
| 82 |
async def cleanup(self):
|
| 83 |
if self._session:
|
| 84 |
await self._session.close()
|
| 85 |
+
self._session = None
|
|
|
src/tools/coingecko_tool.py
CHANGED
|
@@ -2,77 +2,119 @@ from typing import Dict, Any, Optional
|
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
| 4 |
from src.utils.config import config
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
class CoinGeckoTool(BaseWeb3Tool):
|
| 7 |
name: str = "coingecko_data"
|
| 8 |
-
description: str = """Get cryptocurrency price, volume, market cap and trend data from CoinGecko.
|
| 9 |
-
Useful for: price analysis, market rankings, volume trends, price changes.
|
| 10 |
-
Input: cryptocurrency name/symbol (bitcoin, ethereum, BTC, ETH) or market query."""
|
| 11 |
args_schema: type[BaseModel] = Web3ToolInput
|
| 12 |
-
|
| 13 |
_base_url: str = PrivateAttr(default="https://api.coingecko.com/api/v3")
|
| 14 |
_symbol_map: Dict[str, str] = PrivateAttr(default_factory=lambda: {
|
| 15 |
"btc": "bitcoin", "eth": "ethereum", "sol": "solana", "ada": "cardano",
|
| 16 |
-
"dot": "polkadot", "bnb": "binancecoin", "usdc": "usd-coin",
|
| 17 |
"usdt": "tether", "xrp": "ripple", "avax": "avalanche-2",
|
| 18 |
"link": "chainlink", "matic": "matic-network", "uni": "uniswap"
|
| 19 |
})
|
| 20 |
-
|
| 21 |
def __init__(self):
|
| 22 |
super().__init__()
|
| 23 |
-
|
| 24 |
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
|
|
|
| 25 |
try:
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
return
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
else:
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
except Exception as e:
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
async def _get_trending(self) -> str:
|
| 40 |
data = await self.make_request(f"{self._base_url}/search/trending")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
name = item.get("name", "Unknown")
|
| 49 |
-
symbol = item.get("symbol", "").upper()
|
| 50 |
-
rank = item.get("market_cap_rank", "N/A")
|
| 51 |
-
result += f"{i}. **{name} ({symbol})** - Rank #{rank}\n"
|
| 52 |
-
|
| 53 |
-
return result
|
| 54 |
-
|
| 55 |
async def _get_market_overview(self) -> str:
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
return result
|
| 72 |
-
|
| 73 |
coin_id = self._symbol_map.get(query.lower(), query.lower())
|
| 74 |
-
coin_id = self.symbol_map.get(query.lower(), query.lower())
|
| 75 |
-
|
| 76 |
params = {
|
| 77 |
"ids": coin_id,
|
| 78 |
"vs_currencies": "usd",
|
|
@@ -80,34 +122,76 @@ class CoinGeckoTool(BaseWeb3Tool):
|
|
| 80 |
"include_24hr_vol": "true",
|
| 81 |
"include_market_cap": "true"
|
| 82 |
}
|
| 83 |
-
data = await self.make_request(f"{self._base_url}/simple/price", params)
|
| 84 |
-
data = await self.make_request(f"{self.base_url}/simple/price", params)
|
| 85 |
-
|
| 86 |
-
if coin_id not in data:
|
| 87 |
-
return f"No data found for {query}"
|
| 88 |
-
|
| 89 |
-
coin_data = data[coin_id]
|
| 90 |
-
price = coin_data.get("usd", 0)
|
| 91 |
-
change = coin_data.get("usd_24h_change", 0)
|
| 92 |
-
volume = coin_data.get("usd_24h_vol", 0)
|
| 93 |
-
mcap = coin_data.get("usd_market_cap", 0)
|
| 94 |
-
|
| 95 |
-
emoji = "π" if change >= 0 else "π"
|
| 96 |
-
|
| 97 |
-
result = f"π° **{query.upper()} Market Data:**\n\n"
|
| 98 |
-
result += f"{emoji} **Price**: ${price:,.4f}\n"
|
| 99 |
-
result += f"π **24h Change**: {change:+.2f}%\n"
|
| 100 |
-
result += f"π **24h Volume**: ${volume:,.0f}\n"
|
| 101 |
-
result += f"π¦ **Market Cap**: ${mcap:,.0f}\n"
|
| 102 |
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
coin_id = self._symbol_map.get(symbol.lower(), symbol.lower())
|
| 106 |
-
|
| 107 |
params = {"vs_currency": "usd", "days": days}
|
| 108 |
-
data = await self.make_request(f"{self._base_url}/coins/{coin_id}/market_chart", params)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
return {
|
| 112 |
"symbol": symbol.upper(),
|
| 113 |
"prices": data.get("prices", []),
|
|
|
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
| 4 |
from src.utils.config import config
|
| 5 |
+
from src.utils.logger import get_logger
|
| 6 |
+
from src.utils.cache_manager import cache_manager
|
| 7 |
+
|
| 8 |
+
logger = get_logger(__name__)
|
| 9 |
|
| 10 |
class CoinGeckoTool(BaseWeb3Tool):
|
| 11 |
name: str = "coingecko_data"
|
| 12 |
+
description: str = """Get cryptocurrency price, volume, market cap and trend data from CoinGecko."""
|
|
|
|
|
|
|
| 13 |
args_schema: type[BaseModel] = Web3ToolInput
|
| 14 |
+
|
| 15 |
_base_url: str = PrivateAttr(default="https://api.coingecko.com/api/v3")
|
| 16 |
_symbol_map: Dict[str, str] = PrivateAttr(default_factory=lambda: {
|
| 17 |
"btc": "bitcoin", "eth": "ethereum", "sol": "solana", "ada": "cardano",
|
| 18 |
+
"dot": "polkadot", "bnb": "binancecoin", "usdc": "usd-coin",
|
| 19 |
"usdt": "tether", "xrp": "ripple", "avax": "avalanche-2",
|
| 20 |
"link": "chainlink", "matic": "matic-network", "uni": "uniswap"
|
| 21 |
})
|
| 22 |
+
|
| 23 |
def __init__(self):
|
| 24 |
super().__init__()
|
| 25 |
+
|
| 26 |
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
| 27 |
+
filters = filters or {}
|
| 28 |
try:
|
| 29 |
+
# Check cache first
|
| 30 |
+
cache_key = f"coingecko_{filters.get('type', 'coin')}_{query}_{str(filters)}"
|
| 31 |
+
cached_result = cache_manager.get(cache_key)
|
| 32 |
+
if cached_result:
|
| 33 |
+
logger.info(f"Cache hit for {cache_key}")
|
| 34 |
+
return cached_result
|
| 35 |
+
|
| 36 |
+
result = None
|
| 37 |
+
t = filters.get("type")
|
| 38 |
+
|
| 39 |
+
if t == "trending":
|
| 40 |
+
result = await self._get_trending()
|
| 41 |
+
elif t == "market_overview":
|
| 42 |
+
result = await self._get_market_overview()
|
| 43 |
+
elif t == "price_history":
|
| 44 |
+
days = int(filters.get("days", 30))
|
| 45 |
+
result = await self._get_price_history(query, days)
|
| 46 |
else:
|
| 47 |
+
result = await self._get_coin_data(query)
|
| 48 |
+
|
| 49 |
+
# Cache successful results
|
| 50 |
+
if result and not result.startswith("β οΈ"):
|
| 51 |
+
cache_manager.set(cache_key, result, ttl=300)
|
| 52 |
+
|
| 53 |
+
return result
|
| 54 |
+
|
| 55 |
except Exception as e:
|
| 56 |
+
logger.error(f"CoinGecko error: {e}")
|
| 57 |
+
return f"β οΈ CoinGecko service temporarily unavailable: {str(e)}"
|
| 58 |
+
|
| 59 |
async def _get_trending(self) -> str:
|
| 60 |
data = await self.make_request(f"{self._base_url}/search/trending")
|
| 61 |
+
coins = data.get("coins", [])[:5]
|
| 62 |
+
out = "π₯ **Trending Cryptocurrencies:**\n\n"
|
| 63 |
+
for i, c in enumerate(coins, 1):
|
| 64 |
+
item = c.get("item", {})
|
| 65 |
+
out += f"{i}. **{item.get('name','?')} ({item.get('symbol','?').upper()})** β Rank #{item.get('market_cap_rank','?')}\n"
|
| 66 |
+
return out
|
| 67 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
async def _get_market_overview(self) -> str:
|
| 69 |
+
try:
|
| 70 |
+
params = {
|
| 71 |
+
"vs_currency": "usd",
|
| 72 |
+
"order": "market_cap_desc",
|
| 73 |
+
"per_page": 10,
|
| 74 |
+
"page": 1
|
| 75 |
+
}
|
| 76 |
+
data = await self.make_request(f"{self._base_url}/coins/markets", params=params)
|
| 77 |
+
|
| 78 |
+
if not data or not isinstance(data, list):
|
| 79 |
+
return "β οΈ Market overview data temporarily unavailable"
|
| 80 |
+
|
| 81 |
+
if len(data) == 0:
|
| 82 |
+
return "β No market data available"
|
| 83 |
+
|
| 84 |
+
result = "π **Top Cryptocurrencies by Market Cap:**\n\n"
|
| 85 |
+
|
| 86 |
+
for coin in data[:10]: # Ensure max 10
|
| 87 |
+
try:
|
| 88 |
+
name = coin.get("name", "Unknown")
|
| 89 |
+
symbol = coin.get("symbol", "?").upper()
|
| 90 |
+
price = coin.get("current_price", 0)
|
| 91 |
+
change_24h = coin.get("price_change_percentage_24h", 0)
|
| 92 |
+
market_cap = coin.get("market_cap", 0)
|
| 93 |
+
|
| 94 |
+
# Handle missing or invalid data
|
| 95 |
+
if price is None or price <= 0:
|
| 96 |
+
continue
|
| 97 |
+
|
| 98 |
+
emoji = "π" if change_24h >= 0 else "π"
|
| 99 |
+
mcap_formatted = f"${market_cap/1e9:.2f}B" if market_cap > 0 else "N/A"
|
| 100 |
+
|
| 101 |
+
result += f"{emoji} **{name} ({symbol})**: ${price:,.4f} ({change_24h:+.2f}%) | MCap: {mcap_formatted}\n"
|
| 102 |
+
|
| 103 |
+
except (TypeError, KeyError, ValueError) as e:
|
| 104 |
+
logger.warning(f"Skipping invalid coin data: {e}")
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
return result
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Market overview error: {e}")
|
| 111 |
+
return "β οΈ Market overview temporarily unavailable"
|
| 112 |
+
|
| 113 |
+
async def _get_coin_data(self, query: str) -> str:
|
| 114 |
+
if not query or not query.strip():
|
| 115 |
+
return "β Please provide a cryptocurrency symbol or name"
|
| 116 |
|
|
|
|
|
|
|
| 117 |
coin_id = self._symbol_map.get(query.lower(), query.lower())
|
|
|
|
|
|
|
| 118 |
params = {
|
| 119 |
"ids": coin_id,
|
| 120 |
"vs_currencies": "usd",
|
|
|
|
| 122 |
"include_24hr_vol": "true",
|
| 123 |
"include_market_cap": "true"
|
| 124 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
try:
|
| 127 |
+
data = await self.make_request(f"{self._base_url}/simple/price", params=params)
|
| 128 |
+
|
| 129 |
+
if not data or coin_id not in data:
|
| 130 |
+
# Try alternative search if direct lookup fails
|
| 131 |
+
search_data = await self._search_coin(query)
|
| 132 |
+
if search_data:
|
| 133 |
+
return search_data
|
| 134 |
+
return f"β No data found for '{query}'. Try using full name or common symbols like BTC, ETH, SOL"
|
| 135 |
+
|
| 136 |
+
coin_data = data[coin_id]
|
| 137 |
+
|
| 138 |
+
# Validate required fields
|
| 139 |
+
if "usd" not in coin_data:
|
| 140 |
+
return f"β Price data unavailable for {query.upper()}"
|
| 141 |
+
|
| 142 |
+
price = coin_data.get("usd", 0)
|
| 143 |
+
change_24h = coin_data.get("usd_24h_change", 0)
|
| 144 |
+
volume_24h = coin_data.get("usd_24h_vol", 0)
|
| 145 |
+
market_cap = coin_data.get("usd_market_cap", 0)
|
| 146 |
+
|
| 147 |
+
# Handle edge cases
|
| 148 |
+
if price <= 0:
|
| 149 |
+
return f"β οΈ {query.upper()} price data appears invalid"
|
| 150 |
+
|
| 151 |
+
emoji = "π" if change_24h >= 0 else "π"
|
| 152 |
+
|
| 153 |
+
result = f"π° **{query.upper()} Market Data:**\n\n"
|
| 154 |
+
result += f"{emoji} **Price**: ${price:,.4f}\n"
|
| 155 |
+
result += f"π **24h Change**: {change_24h:+.2f}%\n"
|
| 156 |
+
|
| 157 |
+
if volume_24h > 0:
|
| 158 |
+
result += f"π **24h Volume**: ${volume_24h:,.0f}\n"
|
| 159 |
+
else:
|
| 160 |
+
result += f"π **24h Volume**: Data unavailable\n"
|
| 161 |
+
|
| 162 |
+
if market_cap > 0:
|
| 163 |
+
result += f"π¦ **Market Cap**: ${market_cap:,.0f}\n"
|
| 164 |
+
else:
|
| 165 |
+
result += f"π¦ **Market Cap**: Data unavailable\n"
|
| 166 |
+
|
| 167 |
+
return result
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.error(f"Error fetching coin data for {query}: {e}")
|
| 171 |
+
return f"β οΈ Unable to fetch data for {query.upper()}. Please try again later."
|
| 172 |
|
| 173 |
+
async def _search_coin(self, query: str) -> Optional[str]:
|
| 174 |
+
"""Fallback search when direct ID lookup fails"""
|
| 175 |
+
try:
|
| 176 |
+
search_params = {"query": query}
|
| 177 |
+
search_data = await self.make_request(f"{self._base_url}/search", params=search_params)
|
| 178 |
+
|
| 179 |
+
coins = search_data.get("coins", [])
|
| 180 |
+
if coins:
|
| 181 |
+
coin = coins[0] # Take first match
|
| 182 |
+
coin_id = coin.get("id")
|
| 183 |
+
if coin_id:
|
| 184 |
+
return await self._get_coin_data(coin_id)
|
| 185 |
+
|
| 186 |
+
return None
|
| 187 |
+
except Exception:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
async def _get_price_history(self, symbol: str, days: int) -> str:
|
| 191 |
coin_id = self._symbol_map.get(symbol.lower(), symbol.lower())
|
|
|
|
| 192 |
params = {"vs_currency": "usd", "days": days}
|
| 193 |
+
data = await self.make_request(f"{self._base_url}/coins/{coin_id}/market_chart", params=params)
|
| 194 |
+
# you can format this as you like; hereβs a simple JSON dump
|
|
|
|
| 195 |
return {
|
| 196 |
"symbol": symbol.upper(),
|
| 197 |
"prices": data.get("prices", []),
|
src/tools/defillama_tool.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
from typing import Dict, Any, Optional
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class DeFiLlamaTool(BaseWeb3Tool):
|
| 6 |
name: str = "defillama_data"
|
|
@@ -28,28 +31,63 @@ class DeFiLlamaTool(BaseWeb3Tool):
|
|
| 28 |
return await self._get_top_protocols()
|
| 29 |
|
| 30 |
except Exception as e:
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
async def _get_top_protocols(self) -> str:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
if not data:
|
| 37 |
-
return "No DeFi protocol data available"
|
| 38 |
-
|
| 39 |
-
top_protocols = sorted(data, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
|
| 40 |
-
|
| 41 |
-
result = "π¦ **Top DeFi Protocols by TVL:**\n\n"
|
| 42 |
-
|
| 43 |
-
for i, protocol in enumerate(top_protocols, 1):
|
| 44 |
-
name = protocol.get("name", "Unknown")
|
| 45 |
-
tvl = protocol.get("tvl", 0)
|
| 46 |
-
change = protocol.get("change_1d", 0)
|
| 47 |
-
chain = protocol.get("chain", "Multi-chain")
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
async def _get_tvl_overview(self) -> str:
|
| 55 |
try:
|
|
|
|
| 1 |
from typing import Dict, Any, Optional
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
| 4 |
+
from src.utils.logger import get_logger
|
| 5 |
+
|
| 6 |
+
logger = get_logger(__name__)
|
| 7 |
|
| 8 |
class DeFiLlamaTool(BaseWeb3Tool):
|
| 9 |
name: str = "defillama_data"
|
|
|
|
| 31 |
return await self._get_top_protocols()
|
| 32 |
|
| 33 |
except Exception as e:
|
| 34 |
+
logger.error(f"DeFiLlama error: {e}")
|
| 35 |
+
return f"β οΈ DeFiLlama service temporarily unavailable: {str(e)}"
|
| 36 |
|
| 37 |
async def _get_top_protocols(self) -> str:
|
| 38 |
+
try:
|
| 39 |
+
data = await self.make_request(f"{self._base_url}/protocols")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
if not data or not isinstance(data, list):
|
| 42 |
+
return "β οΈ DeFi protocol data temporarily unavailable"
|
| 43 |
+
|
| 44 |
+
if len(data) == 0:
|
| 45 |
+
return "β No DeFi protocols found"
|
| 46 |
+
|
| 47 |
+
# Filter and validate protocols
|
| 48 |
+
valid_protocols = []
|
| 49 |
+
for protocol in data:
|
| 50 |
+
try:
|
| 51 |
+
tvl = protocol.get("tvl", 0)
|
| 52 |
+
if tvl is not None and tvl > 0:
|
| 53 |
+
valid_protocols.append(protocol)
|
| 54 |
+
except (TypeError, ValueError):
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
if not valid_protocols:
|
| 58 |
+
return "β οΈ No valid protocol data available"
|
| 59 |
+
|
| 60 |
+
# Sort by TVL and take top 10
|
| 61 |
+
top_protocols = sorted(valid_protocols, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
|
| 62 |
+
|
| 63 |
+
result = "π¦ **Top DeFi Protocols by TVL:**\n\n"
|
| 64 |
+
|
| 65 |
+
for i, protocol in enumerate(top_protocols, 1):
|
| 66 |
+
try:
|
| 67 |
+
name = protocol.get("name", "Unknown")
|
| 68 |
+
tvl = protocol.get("tvl", 0)
|
| 69 |
+
change = protocol.get("change_1d", 0)
|
| 70 |
+
chain = protocol.get("chain", "Multi-chain")
|
| 71 |
+
|
| 72 |
+
# Handle edge cases
|
| 73 |
+
if tvl <= 0:
|
| 74 |
+
continue
|
| 75 |
+
|
| 76 |
+
emoji = "π" if change >= 0 else "π"
|
| 77 |
+
tvl_formatted = f"${tvl/1e9:.2f}B" if tvl >= 1e9 else f"${tvl/1e6:.1f}M"
|
| 78 |
+
change_formatted = f"({change:+.2f}%)" if change is not None else "(N/A)"
|
| 79 |
+
|
| 80 |
+
result += f"{i}. **{name}** ({chain}): {tvl_formatted} TVL {emoji} {change_formatted}\n"
|
| 81 |
+
|
| 82 |
+
except (TypeError, KeyError, ValueError) as e:
|
| 83 |
+
logger.warning(f"Skipping invalid protocol data: {e}")
|
| 84 |
+
continue
|
| 85 |
+
|
| 86 |
+
return result if len(result.split('\n')) > 3 else "β οΈ Unable to format protocol data properly"
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Top protocols error: {e}")
|
| 90 |
+
return "β οΈ DeFi protocol data temporarily unavailable"
|
| 91 |
|
| 92 |
async def _get_tvl_overview(self) -> str:
|
| 93 |
try:
|
src/tools/etherscan_tool.py
CHANGED
|
@@ -2,6 +2,9 @@ from typing import Dict, Any, Optional
|
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
| 4 |
from src.utils.config import config
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
class EtherscanTool(BaseWeb3Tool):
|
| 7 |
name: str = "etherscan_data"
|
|
@@ -16,10 +19,14 @@ class EtherscanTool(BaseWeb3Tool):
|
|
| 16 |
def __init__(self):
|
| 17 |
super().__init__()
|
| 18 |
self._api_key = config.ETHERSCAN_API_KEY
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
| 21 |
-
if not self.
|
| 22 |
-
return "
|
| 23 |
|
| 24 |
try:
|
| 25 |
filters = filters or {}
|
|
@@ -36,46 +43,76 @@ class EtherscanTool(BaseWeb3Tool):
|
|
| 36 |
return await self._get_gas_prices()
|
| 37 |
|
| 38 |
except Exception as e:
|
| 39 |
-
|
|
|
|
| 40 |
|
| 41 |
def _is_address(self, query: str) -> bool:
|
| 42 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def _is_tx_hash(self, query: str) -> bool:
|
| 45 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
async def _get_gas_prices(self) -> str:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
async def _get_eth_stats(self) -> str:
|
| 72 |
params = {
|
| 73 |
"module": "stats",
|
| 74 |
"action": "ethsupply",
|
| 75 |
-
"apikey": self.
|
| 76 |
}
|
| 77 |
|
| 78 |
-
data = await self.make_request(self.
|
| 79 |
|
| 80 |
if data.get("status") != "1":
|
| 81 |
return "Ethereum stats unavailable"
|
|
@@ -90,14 +127,17 @@ class EtherscanTool(BaseWeb3Tool):
|
|
| 90 |
async def _get_address_info(self, address: str) -> str:
|
| 91 |
params = {
|
| 92 |
"module": "account",
|
| 93 |
-
"action": "
|
| 94 |
"address": address,
|
| 95 |
-
"
|
| 96 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
-
data = await self.make_request(self.
|
| 100 |
-
|
| 101 |
if data.get("status") != "1":
|
| 102 |
return f"Address information unavailable for {address}"
|
| 103 |
|
|
@@ -115,10 +155,10 @@ class EtherscanTool(BaseWeb3Tool):
|
|
| 115 |
"module": "proxy",
|
| 116 |
"action": "eth_getTransactionByHash",
|
| 117 |
"txhash": tx_hash,
|
| 118 |
-
"apikey": self.
|
| 119 |
}
|
| 120 |
|
| 121 |
-
data = await self.make_request(self.
|
| 122 |
|
| 123 |
if not data.get("result"):
|
| 124 |
return f"Transaction not found: {tx_hash}"
|
|
|
|
| 2 |
from pydantic import BaseModel, PrivateAttr
|
| 3 |
from src.tools.base_tool import BaseWeb3Tool, Web3ToolInput
|
| 4 |
from src.utils.config import config
|
| 5 |
+
from src.utils.logger import get_logger
|
| 6 |
+
|
| 7 |
+
logger = get_logger(__name__)
|
| 8 |
|
| 9 |
class EtherscanTool(BaseWeb3Tool):
|
| 10 |
name: str = "etherscan_data"
|
|
|
|
| 19 |
def __init__(self):
|
| 20 |
super().__init__()
|
| 21 |
self._api_key = config.ETHERSCAN_API_KEY
|
| 22 |
+
self.enabled = bool(self._api_key)
|
| 23 |
+
|
| 24 |
+
if not self.enabled:
|
| 25 |
+
logger.warning("Etherscan API key not configured - limited functionality")
|
| 26 |
|
| 27 |
async def _arun(self, query: str, filters: Optional[Dict[str, Any]] = None) -> str:
|
| 28 |
+
if not self.enabled:
|
| 29 |
+
return "β οΈ **Etherscan Service Limited**\n\nEtherscan functionality requires an API key.\nGet yours free at: https://etherscan.io/apis\n\nSet environment variable: `ETHERSCAN_API_KEY=your_key`"
|
| 30 |
|
| 31 |
try:
|
| 32 |
filters = filters or {}
|
|
|
|
| 43 |
return await self._get_gas_prices()
|
| 44 |
|
| 45 |
except Exception as e:
|
| 46 |
+
logger.error(f"Etherscan error: {e}")
|
| 47 |
+
return f"β οΈ Etherscan service temporarily unavailable"
|
| 48 |
|
| 49 |
def _is_address(self, query: str) -> bool:
|
| 50 |
+
return (
|
| 51 |
+
len(query) == 42
|
| 52 |
+
and query.startswith("0x")
|
| 53 |
+
and all(c in "0123456789abcdefABCDEF" for c in query[2:])
|
| 54 |
+
)
|
| 55 |
|
| 56 |
def _is_tx_hash(self, query: str) -> bool:
|
| 57 |
+
return (
|
| 58 |
+
len(query) == 66
|
| 59 |
+
and query.startswith("0x")
|
| 60 |
+
and all(c in "0123456789abcdefABCDEF" for c in query[2:])
|
| 61 |
+
)
|
| 62 |
|
| 63 |
async def _get_gas_prices(self) -> str:
|
| 64 |
+
try:
|
| 65 |
+
params = {
|
| 66 |
+
"module": "gastracker",
|
| 67 |
+
"action": "gasoracle",
|
| 68 |
+
"apikey": self._api_key
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
data = await self.make_request(self._base_url, params)
|
| 72 |
+
|
| 73 |
+
if not data or data.get("status") != "1":
|
| 74 |
+
error_msg = data.get("message", "Unknown error") if data else "No response"
|
| 75 |
+
logger.warning(f"Etherscan gas price error: {error_msg}")
|
| 76 |
+
return "β οΈ Gas price data temporarily unavailable"
|
| 77 |
+
|
| 78 |
+
result_data = data.get("result", {})
|
| 79 |
+
if not result_data:
|
| 80 |
+
return "β No gas price data in response"
|
| 81 |
+
|
| 82 |
+
safe_gas = result_data.get("SafeGasPrice", "N/A")
|
| 83 |
+
standard_gas = result_data.get("StandardGasPrice", "N/A")
|
| 84 |
+
fast_gas = result_data.get("FastGasPrice", "N/A")
|
| 85 |
+
|
| 86 |
+
# Validate gas prices are numeric
|
| 87 |
+
try:
|
| 88 |
+
if safe_gas != "N/A":
|
| 89 |
+
float(safe_gas)
|
| 90 |
+
if standard_gas != "N/A":
|
| 91 |
+
float(standard_gas)
|
| 92 |
+
if fast_gas != "N/A":
|
| 93 |
+
float(fast_gas)
|
| 94 |
+
except (ValueError, TypeError):
|
| 95 |
+
return "β οΈ Invalid gas price data received"
|
| 96 |
+
|
| 97 |
+
result = "β½ **Ethereum Gas Prices:**\n\n"
|
| 98 |
+
result += f"π **Safe**: {safe_gas} gwei\n"
|
| 99 |
+
result += f"β‘ **Standard**: {standard_gas} gwei\n"
|
| 100 |
+
result += f"π **Fast**: {fast_gas} gwei\n"
|
| 101 |
+
|
| 102 |
+
return result
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"Gas prices error: {e}")
|
| 106 |
+
return "β οΈ Gas price service temporarily unavailable"
|
| 107 |
|
| 108 |
async def _get_eth_stats(self) -> str:
|
| 109 |
params = {
|
| 110 |
"module": "stats",
|
| 111 |
"action": "ethsupply",
|
| 112 |
+
"apikey": self._api_key
|
| 113 |
}
|
| 114 |
|
| 115 |
+
data = await self.make_request(self._base_url, params)
|
| 116 |
|
| 117 |
if data.get("status") != "1":
|
| 118 |
return "Ethereum stats unavailable"
|
|
|
|
| 127 |
async def _get_address_info(self, address: str) -> str:
|
| 128 |
params = {
|
| 129 |
"module": "account",
|
| 130 |
+
"action": "txlist",
|
| 131 |
"address": address,
|
| 132 |
+
"startblock": "0",
|
| 133 |
+
"endblock": "99999999",
|
| 134 |
+
"page": "1",
|
| 135 |
+
"offset": "10",
|
| 136 |
+
"sort": "desc",
|
| 137 |
+
"apikey": self._api_key
|
| 138 |
}
|
| 139 |
|
| 140 |
+
data = await self.make_request(self._base_url, params)
|
|
|
|
| 141 |
if data.get("status") != "1":
|
| 142 |
return f"Address information unavailable for {address}"
|
| 143 |
|
|
|
|
| 155 |
"module": "proxy",
|
| 156 |
"action": "eth_getTransactionByHash",
|
| 157 |
"txhash": tx_hash,
|
| 158 |
+
"apikey": self._api_key
|
| 159 |
}
|
| 160 |
|
| 161 |
+
data = await self.make_request(self._base_url, params)
|
| 162 |
|
| 163 |
if not data.get("result"):
|
| 164 |
return f"Transaction not found: {tx_hash}"
|
src/{cache_manager.py β utils/cache_manager.py}
RENAMED
|
@@ -1,35 +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 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 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()
|
|
@@ -45,5 +59,17 @@ class CacheManager:
|
|
| 45 |
|
| 46 |
def size(self) -> int:
|
| 47 |
return len(self.cache)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
cache_manager = CacheManager()
|
|
|
|
| 1 |
import time
|
| 2 |
from typing import Any, Optional, Dict
|
| 3 |
+
from src.utils.config import config
|
| 4 |
+
from src.utils.logger import get_logger
|
| 5 |
+
|
| 6 |
+
logger = get_logger(__name__)
|
| 7 |
|
| 8 |
class CacheManager:
|
| 9 |
def __init__(self, default_ttl: Optional[int] = None):
|
| 10 |
self.cache: Dict[str, Dict[str, Any]] = {}
|
| 11 |
self.default_ttl = default_ttl or config.CACHE_TTL
|
| 12 |
+
self.hits = 0
|
| 13 |
+
self.misses = 0
|
| 14 |
|
| 15 |
def get(self, key: str) -> Optional[Any]:
|
| 16 |
if key not in self.cache:
|
| 17 |
+
self.misses += 1
|
| 18 |
return None
|
| 19 |
|
| 20 |
entry = self.cache[key]
|
| 21 |
if time.time() > entry["expires_at"]:
|
| 22 |
del self.cache[key]
|
| 23 |
+
self.misses += 1
|
| 24 |
return None
|
| 25 |
|
| 26 |
+
self.hits += 1
|
| 27 |
return entry["data"]
|
| 28 |
|
| 29 |
def set(self, key: str, data: Any, ttl: Optional[int] = None) -> None:
|
| 30 |
+
try:
|
| 31 |
+
expires_at = time.time() + (ttl or self.default_ttl)
|
| 32 |
+
self.cache[key] = {
|
| 33 |
+
"data": data,
|
| 34 |
+
"expires_at": expires_at,
|
| 35 |
+
"created_at": time.time()
|
| 36 |
+
}
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logger.warning(f"Cache set failed for {key}: {e}")
|
| 39 |
|
| 40 |
def delete(self, key: str) -> bool:
|
| 41 |
return self.cache.pop(key, None) is not None
|
| 42 |
|
| 43 |
def clear(self) -> None:
|
| 44 |
self.cache.clear()
|
| 45 |
+
self.hits = 0
|
| 46 |
+
self.misses = 0
|
| 47 |
|
| 48 |
def cleanup_expired(self) -> int:
|
| 49 |
current_time = time.time()
|
|
|
|
| 59 |
|
| 60 |
def size(self) -> int:
|
| 61 |
return len(self.cache)
|
| 62 |
+
|
| 63 |
+
def stats(self) -> Dict[str, Any]:
|
| 64 |
+
total_requests = self.hits + self.misses
|
| 65 |
+
hit_rate = (self.hits / total_requests * 100) if total_requests > 0 else 0
|
| 66 |
+
|
| 67 |
+
return {
|
| 68 |
+
"size": self.size(),
|
| 69 |
+
"hits": self.hits,
|
| 70 |
+
"misses": self.misses,
|
| 71 |
+
"hit_rate": f"{hit_rate:.1f}%",
|
| 72 |
+
"expired_cleaned": self.cleanup_expired()
|
| 73 |
+
}
|
| 74 |
|
| 75 |
cache_manager = CacheManager()
|
src/utils/config.py
CHANGED
|
@@ -1 +1,30 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
# Load environment variables from .env file
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class Config:
|
| 11 |
+
GEMINI_API_KEY: str = os.getenv("GEMINI_API_KEY", "")
|
| 12 |
+
COINGECKO_API_KEY: Optional[str] = os.getenv("COINGECKO_API_KEY")
|
| 13 |
+
CRYPTOCOMPARE_API_KEY: Optional[str] = os.getenv("CRYPTOCOMPARE_API_KEY")
|
| 14 |
+
ETHERSCAN_API_KEY: str = os.getenv("ETHERSCAN_API_KEY", "")
|
| 15 |
+
|
| 16 |
+
COINGECKO_BASE_URL: str = "https://api.coingecko.com/api/v3"
|
| 17 |
+
CRYPTOCOMPARE_BASE_URL: str = "https://min-api.cryptocompare.com/data"
|
| 18 |
+
|
| 19 |
+
CACHE_TTL: int = 300
|
| 20 |
+
RATE_LIMIT_DELAY: float = 2.0
|
| 21 |
+
MAX_RETRIES: int = 3
|
| 22 |
+
REQUEST_TIMEOUT: int = 30
|
| 23 |
+
|
| 24 |
+
UI_TITLE: str = "Web3 Research Co-Pilot"
|
| 25 |
+
UI_DESCRIPTION: str = "AI-powered crypto research assistant"
|
| 26 |
+
|
| 27 |
+
AIRAA_WEBHOOK_URL: Optional[str] = os.getenv("AIRAA_WEBHOOK_URL")
|
| 28 |
+
AIRAA_API_KEY: Optional[str] = os.getenv("AIRAA_API_KEY")
|
| 29 |
+
|
| 30 |
+
config = Config()
|
src/visualizations.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
import plotly.graph_objects as go
|
| 2 |
-
from datetime import datetime
|
| 3 |
-
from typing import Dict, Any
|
| 4 |
-
|
| 5 |
-
def create_price_chart(data: Dict[str, Any], symbol: str) -> go.Figure:
|
| 6 |
-
try:
|
| 7 |
-
if not data or "prices" not in data:
|
| 8 |
-
return _empty_chart(f"No price data for {symbol}")
|
| 9 |
-
|
| 10 |
-
prices = data["prices"]
|
| 11 |
-
timestamps = [datetime.fromtimestamp(p[0]/1000) for p in prices]
|
| 12 |
-
values = [p[1] for p in prices]
|
| 13 |
-
|
| 14 |
-
fig = go.Figure()
|
| 15 |
-
fig.add_trace(go.Scatter(
|
| 16 |
-
x=timestamps, y=values, mode='lines',
|
| 17 |
-
name=f'{symbol.upper()} Price',
|
| 18 |
-
line=dict(color='#00D4AA', width=2)
|
| 19 |
-
))
|
| 20 |
-
|
| 21 |
-
fig.update_layout(
|
| 22 |
-
title=f'{symbol.upper()} Price History',
|
| 23 |
-
xaxis_title='Date', yaxis_title='Price (USD)',
|
| 24 |
-
template='plotly_dark', height=400
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
return fig
|
| 28 |
-
except Exception:
|
| 29 |
-
return _empty_chart(f"Chart error for {symbol}")
|
| 30 |
-
|
| 31 |
-
def create_market_overview(data: Dict[str, Any]) -> go.Figure:
|
| 32 |
-
try:
|
| 33 |
-
if not data:
|
| 34 |
-
return _empty_chart("No market data available")
|
| 35 |
-
|
| 36 |
-
fig = go.Figure()
|
| 37 |
-
fig.add_annotation(
|
| 38 |
-
text="Market Overview\n" + str(data)[:200] + "...",
|
| 39 |
-
x=0.5, y=0.5, font=dict(size=12, color="white"),
|
| 40 |
-
showarrow=False, align="left"
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
fig.update_layout(
|
| 44 |
-
title="Market Overview", template='plotly_dark', height=400,
|
| 45 |
-
xaxis=dict(visible=False), yaxis=dict(visible=False)
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
return fig
|
| 49 |
-
except Exception:
|
| 50 |
-
return _empty_chart("Market overview error")
|
| 51 |
-
|
| 52 |
-
def _empty_chart(message: str) -> go.Figure:
|
| 53 |
-
fig = go.Figure()
|
| 54 |
-
fig.add_annotation(
|
| 55 |
-
text=message, x=0.5, y=0.5,
|
| 56 |
-
font=dict(size=16, color="white"), showarrow=False
|
| 57 |
-
)
|
| 58 |
-
fig.update_layout(
|
| 59 |
-
template='plotly_dark', height=400,
|
| 60 |
-
xaxis=dict(visible=False), yaxis=dict(visible=False)
|
| 61 |
-
)
|
| 62 |
-
return fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_app.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
|
| 3 |
-
import sys
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
sys.path.insert(0, os.path.dirname(__file__))
|
| 7 |
-
|
| 8 |
-
def test_imports():
|
| 9 |
-
try:
|
| 10 |
-
print("Testing imports...")
|
| 11 |
-
|
| 12 |
-
# Test basic Python modules
|
| 13 |
-
import json
|
| 14 |
-
import asyncio
|
| 15 |
-
from datetime import datetime
|
| 16 |
-
from typing import List, Tuple
|
| 17 |
-
print("β
Basic Python modules imported successfully")
|
| 18 |
-
|
| 19 |
-
# Test installed packages
|
| 20 |
-
import gradio as gr
|
| 21 |
-
print("β
Gradio imported successfully")
|
| 22 |
-
|
| 23 |
-
import aiohttp
|
| 24 |
-
print("β
aiohttp imported successfully")
|
| 25 |
-
|
| 26 |
-
import plotly
|
| 27 |
-
print("β
Plotly imported successfully")
|
| 28 |
-
|
| 29 |
-
import pandas
|
| 30 |
-
print("β
Pandas imported successfully")
|
| 31 |
-
|
| 32 |
-
import pydantic
|
| 33 |
-
print("β
Pydantic imported successfully")
|
| 34 |
-
|
| 35 |
-
# Test LangChain
|
| 36 |
-
import langchain
|
| 37 |
-
from langchain.agents import AgentExecutor
|
| 38 |
-
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 39 |
-
print("β
LangChain modules imported successfully")
|
| 40 |
-
|
| 41 |
-
# Test our modules
|
| 42 |
-
from src.utils.config import config
|
| 43 |
-
from src.utils.logger import get_logger
|
| 44 |
-
print("β
Config and logger imported successfully")
|
| 45 |
-
|
| 46 |
-
from src.tools.base_tool import BaseWeb3Tool
|
| 47 |
-
from src.tools.coingecko_tool import CoinGeckoTool
|
| 48 |
-
from src.tools.defillama_tool import DeFiLlamaTool
|
| 49 |
-
from src.tools.etherscan_tool import EtherscanTool
|
| 50 |
-
print("β
Tools imported successfully")
|
| 51 |
-
|
| 52 |
-
from src.agent.research_agent import Web3ResearchAgent
|
| 53 |
-
from src.agent.query_planner import QueryPlanner
|
| 54 |
-
print("β
Agent modules imported successfully")
|
| 55 |
-
|
| 56 |
-
from src.api.airaa_integration import AIRAAIntegration
|
| 57 |
-
print("β
AIRAA integration imported successfully")
|
| 58 |
-
|
| 59 |
-
from src.visualizations import create_price_chart, create_market_overview
|
| 60 |
-
print("β
Visualizations imported successfully")
|
| 61 |
-
|
| 62 |
-
# Test app import
|
| 63 |
-
from app import Web3CoPilotApp
|
| 64 |
-
print("β
Main app imported successfully")
|
| 65 |
-
|
| 66 |
-
print("\nπ All imports successful! The application is ready to run.")
|
| 67 |
-
return True
|
| 68 |
-
|
| 69 |
-
except ImportError as e:
|
| 70 |
-
print(f"β Import error: {e}")
|
| 71 |
-
return False
|
| 72 |
-
except Exception as e:
|
| 73 |
-
print(f"β Unexpected error: {e}")
|
| 74 |
-
return False
|
| 75 |
-
|
| 76 |
-
def test_app_initialization():
|
| 77 |
-
try:
|
| 78 |
-
print("\nTesting app initialization...")
|
| 79 |
-
# This will test if we can create the app instance
|
| 80 |
-
# but won't actually run it
|
| 81 |
-
os.environ.setdefault('GEMINI_API_KEY', 'test_key_for_import_test')
|
| 82 |
-
|
| 83 |
-
from app import Web3CoPilotApp
|
| 84 |
-
print("β
App class imported successfully")
|
| 85 |
-
|
| 86 |
-
# Test if we can create the interface (but don't launch)
|
| 87 |
-
app = Web3CoPilotApp()
|
| 88 |
-
interface = app.create_interface()
|
| 89 |
-
print("β
App interface created successfully")
|
| 90 |
-
|
| 91 |
-
print("\nπ Application is fully functional and ready to launch!")
|
| 92 |
-
return True
|
| 93 |
-
|
| 94 |
-
except Exception as e:
|
| 95 |
-
print(f"β App initialization error: {e}")
|
| 96 |
-
return False
|
| 97 |
-
|
| 98 |
-
if __name__ == "__main__":
|
| 99 |
-
print("=" * 60)
|
| 100 |
-
print("Web3 Research Co-Pilot - Application Test")
|
| 101 |
-
print("=" * 60)
|
| 102 |
-
|
| 103 |
-
success = test_imports()
|
| 104 |
-
if success:
|
| 105 |
-
test_app_initialization()
|
| 106 |
-
|
| 107 |
-
print("=" * 60)
|
| 108 |
-
print("Test complete!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|