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

fix: etherscan_tool error fix

Browse files
.env.example CHANGED
@@ -1,8 +1,14 @@
1
- # Google Gemini API Key (Required)
2
- GEMINI_API_KEY=AIzaSyCgOdLBoDEeG7vONzCZgm1l9dJFyIYrZOw
3
 
4
- # CoinGecko API Key (Optional - for higher rate limits)
5
  COINGECKO_API_KEY=your_coingecko_api_key_here
 
 
6
 
7
- # CryptoCompare API Key (Optional - for additional data sources)
8
- CRYPTOCOMPARE_API_KEY=your_cryptocompare_api_key_here
 
 
 
 
 
1
+ # Required for full AI functionality
2
+ GEMINI_API_KEY=your_google_gemini_api_key_here
3
 
4
+ # Optional - Enhanced data sources
5
  COINGECKO_API_KEY=your_coingecko_api_key_here
6
+ ETHERSCAN_API_KEY=your_etherscan_api_key_here
7
+ DEFILLAMA_API_KEY=your_defillama_api_key_here
8
 
9
+ # Optional - Logging configuration
10
+ LOG_LEVEL=INFO
11
+
12
+ # Optional - AIRAA Integration
13
+ AIRAA_ENDPOINT=your_airaa_endpoint_here
14
+ AIRAA_API_KEY=your_airaa_api_key_here
Dockerfile CHANGED
@@ -1,18 +1,37 @@
 
1
  FROM python:3.11-slim
2
 
 
3
  WORKDIR /app
4
 
5
- RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
 
 
 
6
 
 
7
  COPY requirements.txt .
8
- RUN pip install --no-cache-dir -r requirements.txt
9
 
 
 
 
 
 
10
  COPY . .
11
 
12
- EXPOSE 7860
 
13
 
 
14
  ENV PYTHONPATH=/app
15
- ENV GRADIO_SERVER_NAME=0.0.0.0
16
- ENV GRADIO_SERVER_PORT=7860
 
 
 
 
 
 
17
 
18
- CMD ["python", "app.py"]
 
 
1
+ # Use Python 3.11 slim image for HuggingFace Spaces
2
  FROM python:3.11-slim
3
 
4
+ # Set working directory
5
  WORKDIR /app
6
 
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
 
12
+ # Copy requirements first for better caching
13
  COPY requirements.txt .
 
14
 
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir --upgrade pip && \
17
+ pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
  COPY . .
21
 
22
+ # Create necessary directories
23
+ RUN mkdir -p logs cache
24
 
25
+ # Set environment variables for HuggingFace Spaces
26
  ENV PYTHONPATH=/app
27
+ ENV PYTHONUNBUFFERED=1
28
+
29
+ # Expose port 7860 (HuggingFace Spaces default)
30
+ EXPOSE 7860
31
+
32
+ # Health check
33
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
34
+ CMD curl -f http://localhost:7860/health || exit 1
35
 
36
+ # Run the application
37
+ CMD ["python", "app_fastapi.py"]
app_fastapi.py β†’ app.py RENAMED
@@ -9,6 +9,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
 
@@ -16,16 +18,17 @@ 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]]] = []
@@ -35,93 +38,140 @@ class QueryResponse(BaseModel):
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">
@@ -129,434 +179,593 @@ async def get_homepage(request: Request):
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
  });
@@ -568,35 +777,33 @@ async def get_homepage(request: Request):
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")
 
9
  from typing import List, Dict, Any, Optional
10
  import os
11
  from dotenv import load_dotenv
12
+ import plotly
13
+ import plotly.graph_objects as go
14
 
15
  load_dotenv()
16
 
 
18
  from src.api.airaa_integration import AIRAAIntegration
19
  from src.utils.logger import get_logger
20
  from src.utils.config import config
21
+ from src.visualizations import CryptoVisualizations
22
 
23
  logger = get_logger(__name__)
24
 
25
  app = FastAPI(
26
  title="Web3 Research Co-Pilot",
27
+ description="Professional cryptocurrency research assistant",
28
+ version="2.0.0"
29
  )
30
 
31
+ # Pydantic models
32
  class QueryRequest(BaseModel):
33
  query: str
34
  chat_history: Optional[List[Dict[str, str]]] = []
 
38
  response: str
39
  sources: Optional[List[str]] = []
40
  metadata: Optional[Dict[str, Any]] = {}
41
+ visualizations: Optional[List[str]] = []
42
  error: Optional[str] = None
43
 
44
  class Web3CoPilotService:
45
  def __init__(self):
46
  try:
47
+ logger.info("Initializing Web3 Research Co-Pilot...")
 
48
 
49
  if config.GEMINI_API_KEY:
50
+ logger.info("Initializing AI research agent...")
51
  self.agent = Web3ResearchAgent()
52
+ logger.info("AI research agent initialized")
53
  else:
54
+ logger.warning("GEMINI_API_KEY not configured - limited functionality")
55
  self.agent = None
56
 
57
+ logger.info("Initializing integrations...")
58
  self.airaa = AIRAAIntegration()
 
59
 
60
  self.enabled = bool(config.GEMINI_API_KEY)
61
+ self.visualizer = CryptoVisualizations()
62
+
63
+ logger.info(f"Service initialized (AI enabled: {self.enabled})")
64
 
65
  except Exception as e:
66
+ logger.error(f"Service initialization failed: {e}")
67
  self.agent = None
68
  self.airaa = None
69
  self.enabled = False
70
+ self.visualizer = CryptoVisualizations()
71
 
72
  async def process_query(self, query: str) -> QueryResponse:
73
+ """Process research query with visualizations"""
74
+ logger.info(f"Processing query: {query[:100]}...")
75
 
76
  if not query.strip():
77
+ return QueryResponse(
78
+ success=False,
79
+ response="Please provide a research query.",
80
+ error="Empty query"
81
+ )
82
 
83
  try:
84
  if not self.enabled:
85
+ response = """**Research Assistant - Limited Mode**
 
86
 
87
+ API access available for basic cryptocurrency data:
88
+ β€’ Market prices and statistics
89
+ β€’ DeFi protocol information
90
+ β€’ Network gas fees
91
 
92
+ Configure GEMINI_API_KEY environment variable for full AI analysis."""
93
+ return QueryResponse(success=True, response=response, sources=["System"])
94
 
95
+ logger.info("Processing with AI research agent...")
96
  result = await self.agent.research_query(query)
 
97
 
98
  if result.get("success"):
99
+ response = result.get("result", "No analysis generated")
100
  sources = result.get("sources", [])
101
  metadata = result.get("metadata", {})
102
 
103
+ # Generate visualizations if relevant data is available
104
+ visualizations = []
105
+ if metadata:
106
+ vis_html = await self._generate_visualizations(metadata, query)
107
+ if vis_html:
108
+ visualizations.append(vis_html)
109
+
110
  # Send to AIRAA if enabled
111
  if self.airaa and self.airaa.enabled:
112
  try:
 
113
  await self.airaa.send_research_data(query, response)
114
+ logger.info("Data sent to AIRAA")
115
  except Exception as e:
116
+ logger.warning(f"AIRAA integration failed: {e}")
117
 
118
+ return QueryResponse(
119
+ success=True,
120
+ response=response,
121
+ sources=sources,
122
+ metadata=metadata,
123
+ visualizations=visualizations
124
+ )
125
  else:
126
+ error_msg = result.get("error", "Research analysis failed")
127
+ logger.error(f"Research failed: {error_msg}")
128
  return QueryResponse(success=False, response=error_msg, error=error_msg)
129
 
130
  except Exception as e:
131
+ logger.error(f"Query processing error: {e}")
132
+ error_msg = f"Processing error: {str(e)}"
133
  return QueryResponse(success=False, response=error_msg, error=error_msg)
134
+
135
+ async def _generate_visualizations(self, metadata: Dict[str, Any], query: str) -> Optional[str]:
136
+ """Generate visualizations based on query and metadata"""
137
+ try:
138
+ # Check for price data
139
+ if 'price_data' in metadata:
140
+ symbol = self._extract_symbol_from_query(query)
141
+ fig = self.visualizer.create_price_chart(metadata['price_data'], symbol)
142
+ return plotly.io.to_html(fig, include_plotlyjs='cdn', div_id='price_chart')
143
+
144
+ # Check for market data
145
+ elif 'market_data' in metadata:
146
+ fig = self.visualizer.create_market_overview(metadata['market_data'])
147
+ return plotly.io.to_html(fig, include_plotlyjs='cdn', div_id='market_overview')
148
+
149
+ # Check for DeFi data
150
+ elif 'defi_data' in metadata:
151
+ fig = self.visualizer.create_defi_tvl_chart(metadata['defi_data'])
152
+ return plotly.io.to_html(fig, include_plotlyjs='cdn', div_id='defi_chart')
153
+
154
+ return None
155
+
156
+ except Exception as e:
157
+ logger.error(f"Visualization generation failed: {e}")
158
+ return None
159
+
160
+ def _extract_symbol_from_query(self, query: str) -> str:
161
+ """Extract cryptocurrency symbol from query"""
162
+ symbols = ['BTC', 'ETH', 'ADA', 'SOL', 'AVAX', 'MATIC', 'DOT', 'LINK']
163
+ query_upper = query.upper()
164
+ for symbol in symbols:
165
+ if symbol in query_upper:
166
+ return symbol
167
+ return 'BTC' # Default
168
 
169
  # Initialize service
 
170
  service = Web3CoPilotService()
171
 
 
172
  @app.get("/", response_class=HTMLResponse)
173
  async def get_homepage(request: Request):
174
+ """Serve minimalist, professional interface"""
175
  html_content = """
176
  <!DOCTYPE html>
177
  <html lang="en">
 
179
  <meta charset="UTF-8">
180
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
181
  <title>Web3 Research Co-Pilot</title>
182
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22><path fill=%22%2300d4aa%22 d=%22M12 2L2 7v10c0 5.5 3.8 7.7 9 9 5.2-1.3 9-3.5 9-9V7l-10-5z%22/></svg>">
183
+
184
  <style>
185
+ :root {
186
+ --primary: #0066ff;
187
+ --primary-dark: #0052cc;
188
+ --accent: #00d4aa;
189
+ --background: #000000;
190
+ --surface: #111111;
191
+ --surface-elevated: #1a1a1a;
192
+ --text: #ffffff;
193
+ --text-secondary: #a0a0a0;
194
+ --text-muted: #666666;
195
+ --border: rgba(255, 255, 255, 0.08);
196
+ --border-focus: rgba(0, 102, 255, 0.3);
197
+ --shadow: rgba(0, 0, 0, 0.4);
198
+ --success: #00d4aa;
199
+ --warning: #ffa726;
200
+ --error: #f44336;
201
+ }
202
+
203
+ * {
204
+ margin: 0;
205
+ padding: 0;
206
+ box-sizing: border-box;
207
+ }
208
+
209
+ body {
210
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
211
+ background: var(--background);
212
+ color: var(--text);
213
+ line-height: 1.5;
214
  min-height: 100vh;
215
+ font-weight: 400;
216
+ -webkit-font-smoothing: antialiased;
217
+ -moz-osx-font-smoothing: grayscale;
218
+ }
219
+
220
+ .container {
221
+ max-width: 1000px;
222
+ margin: 0 auto;
223
+ padding: 2rem 1.5rem;
224
+ }
225
+
226
+ .header {
227
+ text-align: center;
228
+ margin-bottom: 2.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }
230
+
231
+ .header h1 {
232
+ font-size: 2.25rem;
233
+ font-weight: 600;
234
+ color: var(--text);
235
+ margin-bottom: 0.5rem;
236
+ letter-spacing: -0.025em;
237
  }
238
+
239
+ .header .brand {
240
+ color: var(--primary);
 
241
  }
242
+
243
+ .header p {
244
+ color: var(--text-secondary);
245
+ font-size: 1rem;
246
+ font-weight: 400;
247
+ }
248
+
249
+ .status {
250
+ background: var(--surface);
251
+ border: 1px solid var(--border);
252
+ border-radius: 12px;
253
+ padding: 1rem 1.5rem;
254
+ margin-bottom: 2rem;
255
+ text-align: center;
256
+ transition: all 0.2s ease;
257
+ }
258
+
259
+ .status.online {
260
+ border-color: var(--success);
261
+ background: linear-gradient(135deg, rgba(0, 212, 170, 0.05), rgba(0, 212, 170, 0.02));
262
+ }
263
+
264
+ .status.offline {
265
+ border-color: var(--error);
266
+ background: linear-gradient(135deg, rgba(244, 67, 54, 0.05), rgba(244, 67, 54, 0.02));
267
+ }
268
+
269
  .status.checking {
270
+ border-color: var(--warning);
271
+ background: linear-gradient(135deg, rgba(255, 167, 38, 0.05), rgba(255, 167, 38, 0.02));
272
+ animation: pulse 2s infinite;
 
273
  }
274
+
275
  @keyframes pulse {
276
+ 0%, 100% { opacity: 1; }
277
+ 50% { opacity: 0.8; }
278
+ }
279
+
280
+ .chat-interface {
281
+ background: var(--surface);
282
+ border: 1px solid var(--border);
283
+ border-radius: 16px;
284
+ overflow: hidden;
285
+ margin-bottom: 2rem;
286
+ backdrop-filter: blur(20px);
287
+ }
288
+
289
+ .chat-messages {
290
+ height: 480px;
291
+ overflow-y: auto;
292
+ padding: 2rem;
293
+ background: linear-gradient(180deg, var(--background), var(--surface));
294
+ }
295
+
296
+ .chat-messages::-webkit-scrollbar {
297
+ width: 3px;
298
+ }
299
+
300
+ .chat-messages::-webkit-scrollbar-track {
301
+ background: transparent;
302
+ }
303
+
304
+ .chat-messages::-webkit-scrollbar-thumb {
305
+ background: var(--border);
306
+ border-radius: 2px;
307
+ }
308
+
309
+ .message {
310
+ margin-bottom: 2rem;
311
+ opacity: 0;
312
+ animation: messageSlide 0.4s cubic-bezier(0.2, 0, 0.2, 1) forwards;
313
+ }
314
+
315
+ @keyframes messageSlide {
316
+ from {
317
+ opacity: 0;
318
+ transform: translateY(20px) scale(0.98);
319
+ }
320
+ to {
321
+ opacity: 1;
322
+ transform: translateY(0) scale(1);
323
+ }
324
+ }
325
+
326
+ .message.user {
327
+ text-align: right;
328
+ }
329
+
330
+ .message.assistant {
331
+ text-align: left;
332
+ }
333
+
334
+ .message-content {
335
+ display: inline-block;
336
+ max-width: 75%;
337
+ padding: 1.25rem 1.5rem;
338
+ border-radius: 24px;
339
+ font-size: 0.95rem;
340
+ line-height: 1.6;
341
  position: relative;
342
  }
343
+
344
+ .message.user .message-content {
345
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
346
+ color: #ffffff;
347
+ border-bottom-right-radius: 8px;
348
+ box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
349
+ }
350
+
351
+ .message.assistant .message-content {
352
+ background: var(--surface-elevated);
353
+ color: var(--text);
354
+ border-bottom-left-radius: 8px;
355
+ border: 1px solid var(--border);
356
+ }
357
+
358
+ .message-meta {
359
+ font-size: 0.75rem;
360
+ color: var(--text-muted);
361
+ margin-top: 0.5rem;
362
+ font-weight: 500;
363
+ }
364
+
365
+ .sources {
366
+ margin-top: 1rem;
367
+ padding-top: 1rem;
368
+ border-top: 1px solid var(--border);
369
+ font-size: 0.8rem;
370
+ color: var(--text-secondary);
371
+ }
372
+
373
+ .sources span {
374
+ display: inline-block;
375
+ background: rgba(0, 102, 255, 0.1);
376
+ border: 1px solid rgba(0, 102, 255, 0.2);
377
+ padding: 0.25rem 0.75rem;
378
+ border-radius: 6px;
379
+ margin: 0.25rem 0.5rem 0.25rem 0;
380
+ font-weight: 500;
381
+ font-size: 0.75rem;
382
+ }
383
+
384
+ .input-area {
385
+ padding: 2rem;
386
+ background: linear-gradient(180deg, var(--surface), var(--surface-elevated));
387
+ border-top: 1px solid var(--border);
388
+ }
389
+
390
+ .input-container {
391
  display: flex;
392
+ gap: 1rem;
 
 
 
 
 
 
 
 
393
  align-items: stretch;
394
  }
395
+
396
+ .input-field {
397
+ flex: 1;
398
+ padding: 1rem 1.5rem;
399
+ background: var(--background);
400
+ border: 2px solid var(--border);
401
+ border-radius: 28px;
402
+ color: var(--text);
403
+ font-size: 0.95rem;
404
+ outline: none;
405
+ transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
406
+ font-weight: 400;
407
+ }
408
+
409
+ .input-field:focus {
410
+ border-color: var(--primary);
411
+ box-shadow: 0 0 0 4px var(--border-focus);
412
+ background: var(--surface);
413
+ }
414
+
415
+ .input-field::placeholder {
416
+ color: var(--text-muted);
417
+ font-weight: 400;
418
+ }
419
+
420
+ .send-button {
421
+ padding: 1rem 2rem;
422
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
423
+ color: #ffffff;
424
+ border: none;
425
+ border-radius: 28px;
426
  font-weight: 600;
427
+ cursor: pointer;
428
+ transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
429
+ font-size: 0.95rem;
430
+ box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
431
  }
432
+
433
+ .send-button:hover:not(:disabled) {
434
  transform: translateY(-2px);
435
+ box-shadow: 0 8px 24px rgba(0, 102, 255, 0.3);
436
  }
437
+
438
+ .send-button:active {
439
+ transform: translateY(0);
440
+ }
441
+
442
+ .send-button:disabled {
443
+ opacity: 0.6;
444
+ cursor: not-allowed;
445
  transform: none;
446
+ box-shadow: 0 4px 12px rgba(0, 102, 255, 0.1);
447
+ }
448
+
449
+ .examples {
450
+ display: grid;
451
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
452
+ gap: 1rem;
453
+ margin-top: 1rem;
454
+ }
455
+
456
+ .example {
457
+ background: linear-gradient(135deg, var(--surface), var(--surface-elevated));
458
+ border: 1px solid var(--border);
459
+ border-radius: 12px;
460
+ padding: 1.5rem;
461
+ cursor: pointer;
462
+ transition: all 0.3s cubic-bezier(0.2, 0, 0.2, 1);
463
  position: relative;
464
  overflow: hidden;
465
  }
466
+
467
+ .example::before {
468
  content: '';
469
  position: absolute;
470
  top: 0;
471
  left: -100%;
472
  width: 100%;
473
  height: 100%;
474
+ background: linear-gradient(90deg, transparent, rgba(0, 102, 255, 0.05), transparent);
475
+ transition: left 0.5s ease;
476
+ }
477
+
478
+ .example:hover::before {
479
+ left: 100%;
480
+ }
481
+
482
+ .example:hover {
483
+ border-color: var(--primary);
484
+ transform: translateY(-4px);
485
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
486
+ background: linear-gradient(135deg, var(--surface-elevated), var(--surface));
487
+ }
488
+
489
+ .example-title {
490
+ font-weight: 600;
491
+ color: var(--text);
492
+ margin-bottom: 0.5rem;
493
+ font-size: 0.95rem;
494
+ }
495
+
496
+ .example-desc {
497
+ font-size: 0.85rem;
498
+ color: var(--text-secondary);
499
+ font-weight: 400;
500
+ }
501
+
502
+ .loading {
503
+ display: inline-flex;
504
  align-items: center;
505
+ gap: 0.5rem;
506
+ color: var(--text-secondary);
507
+ font-weight: 500;
508
  }
509
+
510
+ .loading::after {
511
  content: '';
512
+ width: 14px;
513
+ height: 14px;
514
+ border: 2px solid currentColor;
515
+ border-top-color: transparent;
516
  border-radius: 50%;
517
  animation: spin 1s linear infinite;
518
  }
519
+
520
  @keyframes spin {
521
+ to { transform: rotate(360deg); }
 
522
  }
523
+
524
+ .visualization-container {
525
+ margin: 1.5rem 0;
526
+ background: var(--surface-elevated);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  border-radius: 12px;
528
+ padding: 1.5rem;
529
+ border: 1px solid var(--border);
 
530
  }
531
+
532
+ .welcome {
533
  text-align: center;
534
+ padding: 4rem 2rem;
535
+ color: var(--text-secondary);
536
+ }
537
+
538
+ .welcome h3 {
539
+ font-size: 1.25rem;
540
+ font-weight: 600;
541
+ margin-bottom: 0.5rem;
542
+ color: var(--text);
543
+ }
544
+
545
+ .welcome p {
546
+ font-size: 0.95rem;
547
+ font-weight: 400;
548
+ }
549
+
550
+ @media (max-width: 768px) {
551
+ .container {
552
+ padding: 1rem;
553
+ }
554
+
555
+ .header h1 {
556
+ font-size: 1.75rem;
557
+ }
558
+
559
+ .chat-messages {
560
+ height: 400px;
561
+ padding: 1.5rem;
562
+ }
563
+
564
+ .message-content {
565
+ max-width: 85%;
566
+ padding: 1rem 1.25rem;
567
+ }
568
+
569
+ .input-area {
570
+ padding: 1.5rem;
571
+ }
572
+
573
+ .input-container {
574
+ flex-direction: column;
575
+ gap: 0.75rem;
576
+ }
577
+
578
+ .send-button {
579
+ align-self: stretch;
580
+ }
581
+
582
+ .examples {
583
+ grid-template-columns: 1fr;
584
+ }
585
  }
586
  </style>
587
  </head>
588
  <body>
589
  <div class="container">
590
  <div class="header">
591
+ <h1><span class="brand">Web3</span> Research Co-Pilot</h1>
592
+ <p>Professional cryptocurrency analysis and market intelligence</p>
593
  </div>
594
+
595
  <div id="status" class="status checking">
596
+ <span>Initializing research systems...</span>
597
  </div>
598
+
599
+ <div class="chat-interface">
600
  <div id="chatMessages" class="chat-messages">
601
+ <div class="welcome">
602
+ <h3>Welcome to Web3 Research Co-Pilot</h3>
603
+ <p>Ask about market trends, DeFi protocols, or blockchain analytics</p>
604
  </div>
605
  </div>
606
+ <div class="input-area">
607
+ <div class="input-container">
608
+ <input
609
+ type="text"
610
+ id="queryInput"
611
+ class="input-field"
612
+ placeholder="Research Bitcoin trends, analyze DeFi yields, compare protocols..."
613
+ maxlength="500"
614
+ >
615
+ <button id="sendBtn" class="send-button">Research</button>
616
+ </div>
617
  </div>
618
  </div>
619
+
620
  <div class="examples">
621
+ <div class="example" onclick="setQuery('Analyze Bitcoin price trends and institutional adoption patterns')">
622
+ <div class="example-title">Market Analysis</div>
623
+ <div class="example-desc">Bitcoin trends, institutional flows, and market sentiment</div>
624
  </div>
625
+ <div class="example" onclick="setQuery('Compare top DeFi protocols by TVL, yield, and risk metrics')">
626
+ <div class="example-title">DeFi Intelligence</div>
627
+ <div class="example-desc">Protocol comparison, yield analysis, and risk assessment</div>
628
  </div>
629
+ <div class="example" onclick="setQuery('Evaluate Ethereum Layer 2 scaling solutions and adoption metrics')">
630
+ <div class="example-title">Layer 2 Research</div>
631
+ <div class="example-desc">Scaling solutions, transaction costs, and ecosystem growth</div>
632
  </div>
633
+ <div class="example" onclick="setQuery('Identify optimal yield farming strategies across multiple chains')">
634
+ <div class="example-title">Yield Optimization</div>
635
+ <div class="example-desc">Cross-chain opportunities, APY tracking, and risk analysis</div>
636
  </div>
 
 
 
 
 
 
 
 
 
 
637
  </div>
638
  </div>
639
+
640
  <script>
641
  let chatHistory = [];
642
+ let messageCount = 0;
643
+
644
  async function checkStatus() {
645
  try {
 
646
  const response = await fetch('/status');
647
  const status = await response.json();
 
648
 
649
  const statusDiv = document.getElementById('status');
650
 
651
  if (status.enabled && status.gemini_configured) {
652
+ statusDiv.className = 'status online';
653
  statusDiv.innerHTML = `
654
+ <span>Research systems online</span>
655
+ <div style="margin-top: 0.5rem; font-size: 0.85rem; opacity: 0.8;">
656
+ Tools: ${status.tools_available.join(' β€’ ')}
657
+ </div>
658
  `;
 
659
  } else {
660
+ statusDiv.className = 'status offline';
661
  statusDiv.innerHTML = `
662
+ <span>Limited mode - Configure GEMINI_API_KEY for full functionality</span>
663
+ <div style="margin-top: 0.5rem; font-size: 0.85rem; opacity: 0.8;">
664
+ Available: ${status.tools_available.join(' β€’ ')}
665
+ </div>
666
  `;
 
667
  }
668
  } catch (error) {
 
669
  const statusDiv = document.getElementById('status');
670
+ statusDiv.className = 'status offline';
671
+ statusDiv.innerHTML = '<span>Connection error</span>';
672
  }
673
  }
674
+
675
  async function sendQuery() {
676
  const input = document.getElementById('queryInput');
677
  const sendBtn = document.getElementById('sendBtn');
678
  const query = input.value.trim();
679
+
680
+ if (!query) return;
681
+
 
 
 
 
 
 
682
  addMessage('user', query);
683
  input.value = '';
684
+
 
685
  sendBtn.disabled = true;
686
  sendBtn.innerHTML = '<span class="loading">Processing</span>';
687
+
688
  try {
689
  const response = await fetch('/query', {
690
  method: 'POST',
691
  headers: { 'Content-Type': 'application/json' },
692
  body: JSON.stringify({ query, chat_history: chatHistory })
693
  });
694
+
695
  const result = await response.json();
696
+
 
697
  if (result.success) {
698
+ addMessage('assistant', result.response, result.sources, result.visualizations);
 
699
  } else {
700
+ addMessage('assistant', result.response || 'Analysis failed. Please try again.');
 
701
  }
702
  } catch (error) {
703
+ addMessage('assistant', 'Connection error. Please check your network and try again.');
 
704
  } finally {
705
  sendBtn.disabled = false;
706
+ sendBtn.innerHTML = 'Research';
707
  input.focus();
708
  }
709
  }
710
+
711
+ function addMessage(sender, content, sources = [], visualizations = []) {
 
712
  const messagesDiv = document.getElementById('chatMessages');
713
+
714
+ // Clear welcome message
715
+ if (messageCount === 0) {
716
+ messagesDiv.innerHTML = '';
717
+ }
718
+ messageCount++;
719
+
720
  const messageDiv = document.createElement('div');
721
  messageDiv.className = `message ${sender}`;
722
+
723
  let sourcesHtml = '';
724
  if (sources && sources.length > 0) {
725
+ sourcesHtml = `
726
+ <div class="sources">
727
+ Sources: ${sources.map(s => `<span>${s}</span>`).join('')}
728
+ </div>
729
+ `;
730
  }
731
+
732
+ let visualizationHtml = '';
733
+ if (visualizations && visualizations.length > 0) {
734
+ visualizationHtml = visualizations.map(viz =>
735
+ `<div class="visualization-container">${viz}</div>`
736
+ ).join('');
737
+ }
738
+
739
  messageDiv.innerHTML = `
740
+ <div class="message-content">
741
+ ${content.replace(/\n/g, '<br>')}
742
+ ${sourcesHtml}
743
+ </div>
744
+ ${visualizationHtml}
745
+ <div class="message-meta">${new Date().toLocaleTimeString()}</div>
746
  `;
747
+
748
  messagesDiv.appendChild(messageDiv);
749
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
750
+
 
751
  chatHistory.push({ role: sender, content });
752
  if (chatHistory.length > 20) chatHistory = chatHistory.slice(-20);
753
  }
754
+
755
  function setQuery(query) {
756
+ document.getElementById('queryInput').value = query;
757
+ setTimeout(() => sendQuery(), 100);
 
 
 
 
 
 
 
 
 
758
  }
759
+
760
+ // Event listeners
761
+ document.getElementById('queryInput').addEventListener('keypress', (e) => {
762
+ if (e.key === 'Enter') sendQuery();
 
 
763
  });
764
+
765
+ document.getElementById('sendBtn').addEventListener('click', sendQuery);
766
+
767
  // Initialize
768
+ document.addEventListener('DOMContentLoaded', () => {
 
769
  checkStatus();
770
  document.getElementById('queryInput').focus();
771
  });
 
777
 
778
  @app.get("/status")
779
  async def get_status():
780
+ """System status endpoint"""
781
  status = {
782
  "enabled": service.enabled,
783
  "gemini_configured": bool(config.GEMINI_API_KEY),
784
+ "tools_available": ["Market Data", "DeFi Analytics", "Network Metrics"],
785
  "airaa_enabled": service.airaa.enabled if service.airaa else False,
786
+ "timestamp": datetime.now().isoformat(),
787
+ "version": "2.0.0"
788
  }
 
789
  return status
790
 
791
  @app.post("/query", response_model=QueryResponse)
792
  async def process_query(request: QueryRequest):
793
+ """Process research query"""
794
+ return await service.process_query(request.query)
 
 
795
 
796
  @app.get("/health")
797
  async def health_check():
798
+ """Health check endpoint"""
799
  return {
800
  "status": "healthy",
801
  "timestamp": datetime.now().isoformat(),
802
  "service_enabled": service.enabled,
803
+ "version": "2.0.0"
804
  }
805
 
806
  if __name__ == "__main__":
807
  import uvicorn
808
+ logger.info("Starting Web3 Research Co-Pilot...")
809
  uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
requirements.txt CHANGED
@@ -10,3 +10,7 @@ google-generativeai
10
  asyncio-throttle
11
  fastapi
12
  uvicorn
 
 
 
 
 
10
  asyncio-throttle
11
  fastapi
12
  uvicorn
13
+ plotly
14
+ pandas
15
+ numpy
16
+ jinja2
src/tools/etherscan_tool.py CHANGED
@@ -12,6 +12,7 @@ class EtherscanTool(BaseWeb3Tool):
12
  Useful for: transaction analysis, address information, gas prices, token data.
13
  Input: Ethereum address, transaction hash, or general blockchain query."""
14
  args_schema: type[BaseModel] = Web3ToolInput
 
15
 
16
  _base_url: str = PrivateAttr(default="https://api.etherscan.io/api")
17
  _api_key: Optional[str] = PrivateAttr(default=None)
 
12
  Useful for: transaction analysis, address information, gas prices, token data.
13
  Input: Ethereum address, transaction hash, or general blockchain query."""
14
  args_schema: type[BaseModel] = Web3ToolInput
15
+ enabled: bool = True # Add enabled as a Pydantic field
16
 
17
  _base_url: str = PrivateAttr(default="https://api.etherscan.io/api")
18
  _api_key: Optional[str] = PrivateAttr(default=None)
src/visualizations.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.graph_objects as go
2
+ import plotly.express as px
3
+ from plotly.subplots import make_subplots
4
+ import pandas as pd
5
+ from typing import Dict, List, Any, Optional
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class CryptoVisualizations:
11
+ """Professional cryptocurrency data visualizations"""
12
+
13
+ @staticmethod
14
+ def create_price_chart(data: Dict[str, Any], symbol: str = "BTC") -> go.Figure:
15
+ """Create a professional price chart with volume"""
16
+ try:
17
+ if not data or 'prices' not in data:
18
+ return CryptoVisualizations._create_empty_chart("No price data available")
19
+
20
+ prices = data['prices']
21
+ volumes = data.get('total_volumes', [])
22
+
23
+ # Convert to DataFrame
24
+ df = pd.DataFrame({
25
+ 'timestamp': [pd.to_datetime(p[0], unit='ms') for p in prices],
26
+ 'price': [p[1] for p in prices],
27
+ 'volume': [v[1] if v else 0 for v in (volumes[:len(prices)] if volumes else [])]
28
+ })
29
+
30
+ # Create subplot with secondary y-axis
31
+ fig = make_subplots(
32
+ rows=2, cols=1,
33
+ shared_xaxes=True,
34
+ vertical_spacing=0.1,
35
+ subplot_titles=(f'{symbol.upper()} Price', 'Volume'),
36
+ row_heights=[0.7, 0.3]
37
+ )
38
+
39
+ # Price line
40
+ fig.add_trace(
41
+ go.Scatter(
42
+ x=df['timestamp'],
43
+ y=df['price'],
44
+ mode='lines',
45
+ name='Price',
46
+ line=dict(color='#00d4aa', width=2),
47
+ hovertemplate='<b>%{y:,.2f} USD</b><br>%{x}<extra></extra>'
48
+ ),
49
+ row=1, col=1
50
+ )
51
+
52
+ # Volume bars
53
+ if not df['volume'].empty and df['volume'].sum() > 0:
54
+ fig.add_trace(
55
+ go.Bar(
56
+ x=df['timestamp'],
57
+ y=df['volume'],
58
+ name='Volume',
59
+ marker_color='rgba(0, 212, 170, 0.3)',
60
+ hovertemplate='<b>%{y:,.0f}</b><br>%{x}<extra></extra>'
61
+ ),
62
+ row=2, col=1
63
+ )
64
+
65
+ # Update layout
66
+ fig.update_layout(
67
+ title=dict(
68
+ text=f'{symbol.upper()} Price Analysis',
69
+ font=dict(size=24, color='#2c3e50'),
70
+ x=0.5
71
+ ),
72
+ showlegend=False,
73
+ height=600,
74
+ margin=dict(l=60, r=30, t=80, b=60),
75
+ plot_bgcolor='white',
76
+ paper_bgcolor='white',
77
+ font=dict(family="SF Pro Display, -apple-system, system-ui, sans-serif", size=12)
78
+ )
79
+
80
+ # Update axes
81
+ fig.update_xaxes(
82
+ gridcolor='#ecf0f1',
83
+ gridwidth=1,
84
+ showgrid=True,
85
+ tickfont=dict(color='#7f8c8d')
86
+ )
87
+ fig.update_yaxes(
88
+ gridcolor='#ecf0f1',
89
+ gridwidth=1,
90
+ showgrid=True,
91
+ tickfont=dict(color='#7f8c8d')
92
+ )
93
+
94
+ return fig
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error creating price chart: {e}")
98
+ return CryptoVisualizations._create_empty_chart(f"Error: {str(e)}")
99
+
100
+ @staticmethod
101
+ def create_market_overview(data: List[Dict[str, Any]]) -> go.Figure:
102
+ """Create market overview with top cryptocurrencies"""
103
+ try:
104
+ if not data:
105
+ return CryptoVisualizations._create_empty_chart("No market data available")
106
+
107
+ # Convert to DataFrame
108
+ df = pd.DataFrame(data)
109
+
110
+ # Take top 10 by market cap
111
+ df = df.head(10).sort_values('market_cap', ascending=True)
112
+
113
+ # Create horizontal bar chart
114
+ fig = go.Figure()
115
+
116
+ # Market cap bars
117
+ fig.add_trace(
118
+ go.Bar(
119
+ y=df['name'],
120
+ x=df['market_cap'],
121
+ orientation='h',
122
+ marker=dict(
123
+ color=df['price_change_percentage_24h'],
124
+ colorscale='RdYlGn',
125
+ colorbar=dict(title="24h Change %"),
126
+ line=dict(color='white', width=1)
127
+ ),
128
+ hovertemplate='<b>%{y}</b><br>Market Cap: $%{x:,.0f}<br>24h: %{marker.color:.2f}%<extra></extra>'
129
+ )
130
+ )
131
+
132
+ fig.update_layout(
133
+ title=dict(
134
+ text='Top 10 Cryptocurrencies by Market Cap',
135
+ font=dict(size=24, color='#2c3e50'),
136
+ x=0.5
137
+ ),
138
+ xaxis_title='Market Cap (USD)',
139
+ height=500,
140
+ margin=dict(l=120, r=30, t=80, b=60),
141
+ plot_bgcolor='white',
142
+ paper_bgcolor='white',
143
+ font=dict(family="SF Pro Display, -apple-system, system-ui, sans-serif", size=12)
144
+ )
145
+
146
+ fig.update_xaxes(
147
+ gridcolor='#ecf0f1',
148
+ gridwidth=1,
149
+ showgrid=True,
150
+ tickfont=dict(color='#7f8c8d')
151
+ )
152
+ fig.update_yaxes(
153
+ tickfont=dict(color='#2c3e50', size=11)
154
+ )
155
+
156
+ return fig
157
+
158
+ except Exception as e:
159
+ logger.error(f"Error creating market overview: {e}")
160
+ return CryptoVisualizations._create_empty_chart(f"Error: {str(e)}")
161
+
162
+ @staticmethod
163
+ def _create_empty_chart(message: str) -> go.Figure:
164
+ """Create an empty chart with error message"""
165
+ fig = go.Figure()
166
+
167
+ fig.add_annotation(
168
+ text=message,
169
+ xref="paper", yref="paper",
170
+ x=0.5, y=0.5,
171
+ showarrow=False,
172
+ font=dict(size=16, color="#7f8c8d")
173
+ )
174
+
175
+ fig.update_layout(
176
+ height=400,
177
+ margin=dict(l=60, r=60, t=60, b=60),
178
+ plot_bgcolor='white',
179
+ paper_bgcolor='white',
180
+ xaxis=dict(showgrid=False, showticklabels=False),
181
+ yaxis=dict(showgrid=False, showticklabels=False)
182
+ )
183
+
184
+ return fig
185
+
186
+ # Convenience functions for backward compatibility
187
+ def create_price_chart(data: Dict[str, Any], symbol: str = "BTC") -> go.Figure:
188
+ """Create price chart - backward compatibility function"""
189
+ return CryptoVisualizations.create_price_chart(data, symbol)
190
+
191
+ def create_market_overview(data: List[Dict[str, Any]]) -> go.Figure:
192
+ """Create market overview - backward compatibility function"""
193
+ return CryptoVisualizations.create_market_overview(data)
test_suite.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Comprehensive test suite for Web3 Research Co-Pilot
4
+ """
5
+
6
+ import sys
7
+ import asyncio
8
+ import time
9
+ from datetime import datetime
10
+
11
+ def test_imports():
12
+ """Test all critical imports"""
13
+ print("πŸ§ͺ Testing imports...")
14
+
15
+ try:
16
+ # Core imports
17
+ from src.visualizations import CryptoVisualizations, create_price_chart
18
+ from src.agent.research_agent import Web3ResearchAgent
19
+ from src.utils.config import config
20
+ from src.tools.coingecko_tool import CoinGeckoTool
21
+ from src.tools.defillama_tool import DeFiLlamaTool
22
+ from src.tools.etherscan_tool import EtherscanTool
23
+ from src.api.airaa_integration import AIRAAIntegration
24
+
25
+ # FastAPI app
26
+ from app import app, service, Web3CoPilotService
27
+
28
+ print("βœ… All imports successful")
29
+ return True
30
+
31
+ except Exception as e:
32
+ print(f"❌ Import failed: {e}")
33
+ return False
34
+
35
+ def test_configuration():
36
+ """Test configuration setup"""
37
+ print("πŸ§ͺ Testing configuration...")
38
+
39
+ try:
40
+ from src.utils.config import config
41
+
42
+ print(f" β€’ GEMINI_API_KEY: {'βœ… Set' if config.GEMINI_API_KEY else '❌ Not set'}")
43
+ print(f" β€’ COINGECKO_API_KEY: {'βœ… Set' if config.COINGECKO_API_KEY else '⚠️ Not set'}")
44
+ print(f" β€’ ETHERSCAN_API_KEY: {'βœ… Set' if config.ETHERSCAN_API_KEY else '⚠️ Not set'}")
45
+
46
+ return True
47
+
48
+ except Exception as e:
49
+ print(f"❌ Configuration test failed: {e}")
50
+ return False
51
+
52
+ def test_visualizations():
53
+ """Test visualization creation"""
54
+ print("πŸ§ͺ Testing visualizations...")
55
+
56
+ try:
57
+ from src.visualizations import CryptoVisualizations
58
+
59
+ # Test empty chart
60
+ fig1 = CryptoVisualizations._create_empty_chart("Test message")
61
+ print(" βœ… Empty chart creation")
62
+
63
+ # Test price chart with sample data
64
+ sample_data = {
65
+ 'prices': [
66
+ [1672531200000, 16500.50],
67
+ [1672617600000, 16750.25],
68
+ [1672704000000, 17100.00]
69
+ ],
70
+ 'total_volumes': [
71
+ [1672531200000, 1000000],
72
+ [1672617600000, 1200000],
73
+ [1672704000000, 1100000]
74
+ ]
75
+ }
76
+
77
+ fig2 = CryptoVisualizations.create_price_chart(sample_data, 'BTC')
78
+ print(" βœ… Price chart with data")
79
+
80
+ # Test market overview
81
+ market_data = [
82
+ {'name': 'Bitcoin', 'market_cap': 500000000000, 'price_change_percentage_24h': 2.5},
83
+ {'name': 'Ethereum', 'market_cap': 200000000000, 'price_change_percentage_24h': -1.2}
84
+ ]
85
+
86
+ fig3 = CryptoVisualizations.create_market_overview(market_data)
87
+ print(" βœ… Market overview chart")
88
+
89
+ return True
90
+
91
+ except Exception as e:
92
+ print(f"❌ Visualization test failed: {e}")
93
+ return False
94
+
95
+ def test_tools():
96
+ """Test individual tools"""
97
+ print("πŸ§ͺ Testing tools...")
98
+
99
+ try:
100
+ from src.tools.coingecko_tool import CoinGeckoTool
101
+ from src.tools.defillama_tool import DeFiLlamaTool
102
+ from src.tools.etherscan_tool import EtherscanTool
103
+
104
+ # Test tool initialization
105
+ coingecko = CoinGeckoTool()
106
+ print(" βœ… CoinGecko tool initialization")
107
+
108
+ defillama = DeFiLlamaTool()
109
+ print(" βœ… DeFiLlama tool initialization")
110
+
111
+ etherscan = EtherscanTool()
112
+ print(" βœ… Etherscan tool initialization")
113
+
114
+ return True
115
+
116
+ except Exception as e:
117
+ print(f"❌ Tools test failed: {e}")
118
+ return False
119
+
120
+ async def test_service():
121
+ """Test service functionality"""
122
+ print("πŸ§ͺ Testing service...")
123
+
124
+ try:
125
+ from app import service
126
+
127
+ print(f" β€’ Service enabled: {'βœ…' if service.enabled else '❌'}")
128
+ print(f" β€’ Agent available: {'βœ…' if service.agent else '❌'}")
129
+ print(f" β€’ AIRAA enabled: {'βœ…' if service.airaa and service.airaa.enabled else '❌'}")
130
+
131
+ # Test a simple query
132
+ if service.enabled:
133
+ print(" πŸ”„ Testing query processing...")
134
+ response = await service.process_query("What is Bitcoin?")
135
+
136
+ if response.success:
137
+ print(" βœ… Query processing successful")
138
+ print(f" Response length: {len(response.response)} characters")
139
+ else:
140
+ print(f" ⚠️ Query failed: {response.error}")
141
+ else:
142
+ print(" ⚠️ Service disabled - limited testing")
143
+
144
+ return True
145
+
146
+ except Exception as e:
147
+ print(f"❌ Service test failed: {e}")
148
+ return False
149
+
150
+ def test_app_health():
151
+ """Test FastAPI app health"""
152
+ print("πŸ§ͺ Testing FastAPI app...")
153
+
154
+ try:
155
+ from fastapi.testclient import TestClient
156
+ from app import app
157
+
158
+ with TestClient(app) as client:
159
+ # Test health endpoint
160
+ response = client.get("/health")
161
+ if response.status_code == 200:
162
+ print(" βœ… Health endpoint")
163
+ else:
164
+ print(f" ❌ Health endpoint failed: {response.status_code}")
165
+
166
+ # Test status endpoint
167
+ response = client.get("/status")
168
+ if response.status_code == 200:
169
+ print(" βœ… Status endpoint")
170
+ status_data = response.json()
171
+ print(f" Version: {status_data.get('version', 'Unknown')}")
172
+ else:
173
+ print(f" ❌ Status endpoint failed: {response.status_code}")
174
+
175
+ # Test homepage
176
+ response = client.get("/")
177
+ if response.status_code == 200:
178
+ print(" βœ… Homepage endpoint")
179
+ else:
180
+ print(f" ❌ Homepage failed: {response.status_code}")
181
+
182
+ return True
183
+
184
+ except Exception as e:
185
+ print(f"❌ FastAPI test failed: {e}")
186
+ return False
187
+
188
+ def run_performance_test():
189
+ """Simple performance test"""
190
+ print("πŸ§ͺ Performance test...")
191
+
192
+ try:
193
+ from src.visualizations import CryptoVisualizations
194
+
195
+ # Time visualization creation
196
+ start_time = time.time()
197
+
198
+ for i in range(10):
199
+ sample_data = {
200
+ 'prices': [[1672531200000 + i*3600000, 16500 + i*10] for i in range(100)],
201
+ 'total_volumes': [[1672531200000 + i*3600000, 1000000 + i*1000] for i in range(100)]
202
+ }
203
+ fig = CryptoVisualizations.create_price_chart(sample_data, 'TEST')
204
+
205
+ end_time = time.time()
206
+ avg_time = (end_time - start_time) / 10
207
+
208
+ print(f" ⏱️ Average chart creation: {avg_time:.3f}s")
209
+
210
+ if avg_time < 1.0:
211
+ print(" βœ… Performance acceptable")
212
+ return True
213
+ else:
214
+ print(" ⚠️ Performance slow")
215
+ return True
216
+
217
+ except Exception as e:
218
+ print(f"❌ Performance test failed: {e}")
219
+ return False
220
+
221
+ async def main():
222
+ """Run all tests"""
223
+ print("=" * 50)
224
+ print("πŸš€ Web3 Research Co-Pilot - Test Suite")
225
+ print("=" * 50)
226
+ print()
227
+
228
+ test_results = []
229
+
230
+ # Run all tests
231
+ test_results.append(test_imports())
232
+ test_results.append(test_configuration())
233
+ test_results.append(test_visualizations())
234
+ test_results.append(test_tools())
235
+ test_results.append(await test_service())
236
+ test_results.append(test_app_health())
237
+ test_results.append(run_performance_test())
238
+
239
+ print()
240
+ print("=" * 50)
241
+ print("πŸ“Š Test Results Summary")
242
+ print("=" * 50)
243
+
244
+ passed = sum(test_results)
245
+ total = len(test_results)
246
+
247
+ print(f"Tests passed: {passed}/{total}")
248
+ print(f"Success rate: {(passed/total)*100:.1f}%")
249
+
250
+ if passed == total:
251
+ print("πŸŽ‰ All tests passed!")
252
+ return 0
253
+ else:
254
+ print("⚠️ Some tests failed")
255
+ return 1
256
+
257
+ if __name__ == "__main__":
258
+ exit_code = asyncio.run(main())
259
+ sys.exit(exit_code)