Priyanshi Saxena commited on
Commit
9b006e9
Β·
1 Parent(s): f104fee

feat: testing done

Browse files
README.md CHANGED
@@ -1,6 +1,69 @@
1
- # Web3 Research Co-Pilot
2
 
3
- AI-powered cryptocurrency research assistant built with LangChain and Gradio.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = [CoinGeckoTool(), DeFiLlamaTool(), EtherscanTool()]
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
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return await response.json()
 
 
 
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
- if self.session:
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
- filters = filters or {}
27
-
28
- if filters.get("type") == "trending":
29
- return await self._get_trending()
30
- elif filters.get("type") == "market_overview":
31
- return await self._get_market_overview()
32
- elif filters.get("type") == "price_history":
33
- return await self._get_price_history(query, filters.get("days", 30))
 
 
 
 
 
 
 
 
 
34
  else:
35
- return await self._get_coin_data(query)
36
-
 
 
 
 
 
 
37
  except Exception as e:
38
- return f"CoinGecko error: {str(e)}"
 
 
39
  async def _get_trending(self) -> str:
40
  data = await self.make_request(f"{self._base_url}/search/trending")
41
- data = await self.make_request(f"{self.base_url}/search/trending")
42
-
43
- trending = data.get("coins", [])[:5]
44
- result = "πŸ”₯ **Trending Cryptocurrencies:**\n\n"
45
-
46
- for i, coin in enumerate(trending, 1):
47
- item = coin.get("item", {})
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
- data = await self.make_request(f"{self._base_url}/coins/markets", params)10, "page": 1}
57
- data = await self.make_request(f"{self.base_url}/coins/markets", params)
58
-
59
- result = "πŸ“Š **Top Cryptocurrencies by Market Cap:**\n\n"
60
-
61
- for coin in data[:10]:
62
- name = coin.get("name", "Unknown")
63
- symbol = coin.get("symbol", "").upper()
64
- price = coin.get("current_price", 0)
65
- change = coin.get("price_change_percentage_24h", 0)
66
- mcap = coin.get("market_cap", 0)
67
-
68
- emoji = "πŸ“ˆ" if change >= 0 else "πŸ“‰"
69
- result += f"{emoji} **{name} ({symbol})**: ${price:,.4f} ({change:+.2f}%) | MCap: ${mcap/1e9:.2f}B\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- data = await self.make_request(f"{self.base_url}/coins/{coin_id}/market_chart", params)
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
- return f"DeFiLlama error: {str(e)}"
 
32
 
33
  async def _get_top_protocols(self) -> str:
34
- data = await self.make_request(f"{self.base_url}/protocols")
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
- emoji = "πŸ“ˆ" if change >= 0 else "πŸ“‰"
50
- result += f"{i}. **{name}** ({chain}): ${tvl/1e9:.2f}B TVL {emoji} ({change:+.2f}%)\n"
51
-
52
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.api_key:
22
- return "❌ Etherscan API key not configured"
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
- return f"Etherscan error: {str(e)}"
 
40
 
41
  def _is_address(self, query: str) -> bool:
42
- return len(query) == 42 and query.startswith("0x")
 
 
 
 
43
 
44
  def _is_tx_hash(self, query: str) -> bool:
45
- return len(query) == 66 and query.startswith("0x")
 
 
 
 
46
 
47
  async def _get_gas_prices(self) -> str:
48
- params = {
49
- "module": "gastracker",
50
- "action": "gasoracle",
51
- "apikey": self.api_key
52
- }
53
-
54
- data = await self.make_request(self.base_url, params)
55
-
56
- if data.get("status") != "1":
57
- return "Gas price data unavailable"
58
-
59
- result_data = data.get("result", {})
60
- safe_gas = result_data.get("SafeGasPrice", "N/A")
61
- standard_gas = result_data.get("StandardGasPrice", "N/A")
62
- fast_gas = result_data.get("FastGasPrice", "N/A")
63
-
64
- result = "β›½ **Ethereum Gas Prices:**\n\n"
65
- result += f"🐌 **Safe**: {safe_gas} gwei\n"
66
- result += f"⚑ **Standard**: {standard_gas} gwei\n"
67
- result += f"πŸš€ **Fast**: {fast_gas} gwei\n"
68
-
69
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  async def _get_eth_stats(self) -> str:
72
  params = {
73
  "module": "stats",
74
  "action": "ethsupply",
75
- "apikey": self.api_key
76
  }
77
 
78
- data = await self.make_request(self.base_url, params)
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": "balance",
94
  "address": address,
95
- "tag": "latest",
96
- "apikey": self.api_key
 
 
 
 
97
  }
98
 
99
- data = await self.make_request(self.base_url, params)
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.api_key
119
  }
120
 
121
- data = await self.make_request(self.base_url, params)
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
- expires_at = time.time() + (ttl or self.default_ttl)
23
- self.cache[key] = {
24
- "data": data,
25
- "expires_at": expires_at
26
- }
 
 
 
 
27
 
28
  def delete(self, key: str) -> bool:
29
  return self.cache.pop(key, None) is not None
30
 
31
  def clear(self) -> None:
32
  self.cache.clear()
 
 
33
 
34
  def cleanup_expired(self) -> int:
35
  current_time = time.time()
@@ -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
- from src.config import config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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!")