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!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|