jatinmehra commited on
Commit
92a3517
·
1 Parent(s): c182bba

implement NegaBot API with FastAPI for tweet sentiment classification and add SQLite logging system

Browse files
Files changed (2) hide show
  1. api.py +632 -0
  2. database.py +289 -0
api.py ADDED
@@ -0,0 +1,632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NegaBot API - FastAPI application for tweet sentiment classification
3
+ """
4
+ from fastapi import FastAPI, HTTPException
5
+ from fastapi.responses import HTMLResponse, Response
6
+ from pydantic import BaseModel, Field
7
+ from typing import List, Optional
8
+ import logging
9
+ from datetime import datetime
10
+ import json
11
+ from model import get_model
12
+ from database import log_prediction, get_all_predictions
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Initialize FastAPI app
19
+ app = FastAPI(
20
+ title="NegaBot API",
21
+ description="Tweet Sentiment Classification API using NegaBot model",
22
+ version="1.0.0"
23
+ )
24
+
25
+ # Pydantic models for request/response validation
26
+ class TweetRequest(BaseModel):
27
+ text: str = Field(..., min_length=1, max_length=1000, description="Tweet text to analyze")
28
+ metadata: Optional[dict] = Field(default=None, description="Optional metadata")
29
+
30
+ class TweetResponse(BaseModel):
31
+ text: str
32
+ sentiment: str
33
+ confidence: float
34
+ predicted_class: int
35
+ probabilities: dict
36
+ timestamp: str
37
+ request_id: Optional[str] = None
38
+
39
+ class BatchTweetRequest(BaseModel):
40
+ tweets: List[str] = Field(..., min_items=1, max_items=50, description="List of tweets to analyze")
41
+ metadata: Optional[dict] = Field(default=None, description="Optional metadata")
42
+
43
+ class BatchTweetResponse(BaseModel):
44
+ results: List[TweetResponse]
45
+ total_processed: int
46
+ timestamp: str
47
+
48
+ class HealthResponse(BaseModel):
49
+ status: str
50
+ model_loaded: bool
51
+ timestamp: str
52
+
53
+ # Global variables
54
+ model = None
55
+
56
+ @app.on_event("startup")
57
+ async def startup_event():
58
+ """Initialize the model on startup"""
59
+ global model
60
+ try:
61
+ logger.info("Starting NegaBot API...")
62
+ model = get_model()
63
+ logger.info("Model loaded successfully")
64
+ except Exception as e:
65
+ logger.error(f"Failed to load model: {str(e)}")
66
+ raise e
67
+
68
+ @app.get("/", response_model=dict)
69
+ async def root():
70
+ """Root endpoint with API information"""
71
+ return {
72
+ "message": "Welcome to NegaBot API",
73
+ "version": "1.0.0",
74
+ "description": "Tweet Sentiment Classification using NegaBot model",
75
+ "endpoints": {
76
+ "predict": "/predict - Single tweet prediction",
77
+ "batch_predict": "/batch_predict - Multiple tweets prediction",
78
+ "health": "/health - API health check",
79
+ "stats": "/stats - Prediction statistics",
80
+ "dashboard": "/dashboard - Interactive analytics dashboard",
81
+ "dashboard_data": "/dashboard/data - Dashboard data as JSON",
82
+ "download_csv": "/download/predictions.csv - Download predictions as CSV",
83
+ "download_json": "/download/predictions.json - Download predictions as JSON"
84
+ }
85
+ }
86
+
87
+ @app.get("/health", response_model=HealthResponse)
88
+ async def health_check():
89
+ """Health check endpoint"""
90
+ return HealthResponse(
91
+ status="healthy" if model is not None else "unhealthy",
92
+ model_loaded=model is not None,
93
+ timestamp=datetime.now().isoformat()
94
+ )
95
+
96
+ @app.post("/predict", response_model=TweetResponse)
97
+ async def predict_sentiment(request: TweetRequest):
98
+ """
99
+ Predict sentiment for a single tweet
100
+
101
+ Args:
102
+ request: TweetRequest containing the tweet text
103
+
104
+ Returns:
105
+ TweetResponse with prediction results
106
+ """
107
+ try:
108
+ if model is None:
109
+ raise HTTPException(status_code=503, detail="Model not loaded")
110
+
111
+ # Get prediction from model
112
+ result = model.predict(request.text)
113
+
114
+ # Create response
115
+ response = TweetResponse(
116
+ text=result["text"],
117
+ sentiment=result["sentiment"],
118
+ confidence=result["confidence"],
119
+ predicted_class=result["predicted_class"],
120
+ probabilities=result["probabilities"],
121
+ timestamp=datetime.now().isoformat()
122
+ )
123
+
124
+ # Log the prediction
125
+ log_prediction(
126
+ text=request.text,
127
+ sentiment=result["sentiment"],
128
+ confidence=result["confidence"],
129
+ metadata=request.metadata
130
+ )
131
+
132
+ logger.info(f"Prediction made: {result['sentiment']} (confidence: {result['confidence']:.2%})")
133
+ return response
134
+
135
+ except Exception as e:
136
+ logger.error(f"Error in prediction: {str(e)}")
137
+ raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
138
+
139
+ @app.post("/batch_predict", response_model=BatchTweetResponse)
140
+ async def batch_predict_sentiment(request: BatchTweetRequest):
141
+ """
142
+ Predict sentiment for multiple tweets
143
+
144
+ Args:
145
+ request: BatchTweetRequest containing list of tweets
146
+
147
+ Returns:
148
+ BatchTweetResponse with all prediction results
149
+ """
150
+ try:
151
+ if model is None:
152
+ raise HTTPException(status_code=503, detail="Model not loaded")
153
+
154
+ # Get predictions for all tweets
155
+ results = model.batch_predict(request.tweets)
156
+
157
+ # Create response objects
158
+ responses = []
159
+ for result in results:
160
+ response = TweetResponse(
161
+ text=result["text"],
162
+ sentiment=result["sentiment"],
163
+ confidence=result["confidence"],
164
+ predicted_class=result["predicted_class"],
165
+ probabilities=result["probabilities"],
166
+ timestamp=datetime.now().isoformat()
167
+ )
168
+ responses.append(response)
169
+
170
+ # Log each prediction
171
+ log_prediction(
172
+ text=result["text"],
173
+ sentiment=result["sentiment"],
174
+ confidence=result["confidence"],
175
+ metadata=request.metadata
176
+ )
177
+
178
+ batch_response = BatchTweetResponse(
179
+ results=responses,
180
+ total_processed=len(responses),
181
+ timestamp=datetime.now().isoformat()
182
+ )
183
+
184
+ logger.info(f"Batch prediction completed: {len(responses)} tweets processed")
185
+ return batch_response
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error in batch prediction: {str(e)}")
189
+ raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}")
190
+
191
+ @app.get("/stats", response_model=dict)
192
+ async def get_prediction_stats():
193
+ """
194
+ Get prediction statistics
195
+
196
+ Returns:
197
+ Dictionary with prediction statistics
198
+ """
199
+ try:
200
+ predictions = get_all_predictions()
201
+
202
+ if not predictions:
203
+ return {
204
+ "total_predictions": 0,
205
+ "positive_count": 0,
206
+ "negative_count": 0,
207
+ "average_confidence": 0,
208
+ "message": "No predictions found"
209
+ }
210
+
211
+ total = len(predictions)
212
+ positive_count = sum(1 for p in predictions if p["sentiment"] == "Positive")
213
+ negative_count = total - positive_count
214
+ avg_confidence = sum(p["confidence"] for p in predictions) / total
215
+
216
+ stats = {
217
+ "total_predictions": total,
218
+ "positive_count": positive_count,
219
+ "negative_count": negative_count,
220
+ "positive_percentage": round((positive_count / total) * 100, 2),
221
+ "negative_percentage": round((negative_count / total) * 100, 2),
222
+ "average_confidence": round(avg_confidence, 4),
223
+ "last_updated": datetime.now().isoformat()
224
+ }
225
+
226
+ return stats
227
+
228
+ except Exception as e:
229
+ logger.error(f"Error getting stats: {str(e)}")
230
+ raise HTTPException(status_code=500, detail=f"Failed to get statistics: {str(e)}")
231
+
232
+ @app.get("/dashboard/data", response_model=dict)
233
+ async def get_dashboard_data():
234
+ """
235
+ Get dashboard data as JSON for API consumption
236
+ """
237
+ try:
238
+ predictions = get_all_predictions()
239
+
240
+ if not predictions:
241
+ return {
242
+ "metrics": {
243
+ "total_predictions": 0,
244
+ "positive_count": 0,
245
+ "negative_count": 0,
246
+ "average_confidence": 0
247
+ },
248
+ "recent_predictions": [],
249
+ "message": "No predictions found"
250
+ }
251
+
252
+ # Calculate metrics
253
+ total = len(predictions)
254
+ positive_count = sum(1 for p in predictions if p["sentiment"] == "Positive")
255
+ negative_count = total - positive_count
256
+ avg_confidence = sum(p["confidence"] for p in predictions) / total
257
+
258
+ # Get recent predictions (last 20)
259
+ recent_predictions = sorted(predictions, key=lambda x: x["created_at"], reverse=True)[:20]
260
+
261
+ return {
262
+ "metrics": {
263
+ "total_predictions": total,
264
+ "positive_count": positive_count,
265
+ "negative_count": negative_count,
266
+ "positive_percentage": round((positive_count / total) * 100, 2),
267
+ "negative_percentage": round((negative_count / total) * 100, 2),
268
+ "average_confidence": round(avg_confidence, 4)
269
+ },
270
+ "recent_predictions": recent_predictions,
271
+ "last_updated": datetime.now().isoformat()
272
+ }
273
+
274
+ except Exception as e:
275
+ logger.error(f"Error getting dashboard data: {str(e)}")
276
+ raise HTTPException(status_code=500, detail=f"Failed to get dashboard data: {str(e)}")
277
+
278
+ @app.get("/download/predictions.csv")
279
+ async def download_predictions_csv():
280
+ """
281
+ Download all predictions as CSV file
282
+ """
283
+ try:
284
+ predictions = get_all_predictions()
285
+
286
+ if not predictions:
287
+ raise HTTPException(status_code=404, detail="No predictions found to download")
288
+
289
+ # Convert to pandas DataFrame for easy CSV export
290
+ import pandas as pd
291
+ df = pd.DataFrame(predictions)
292
+
293
+ # Convert to CSV
294
+ csv_content = df.to_csv(index=False)
295
+
296
+ # Generate filename with timestamp
297
+ filename = f"negabot_predictions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
298
+
299
+ return Response(
300
+ content=csv_content,
301
+ media_type="text/csv",
302
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
303
+ )
304
+
305
+ except Exception as e:
306
+ logger.error(f"Error downloading CSV: {str(e)}")
307
+ raise HTTPException(status_code=500, detail=f"Failed to download CSV: {str(e)}")
308
+
309
+ @app.get("/download/predictions.json")
310
+ async def download_predictions_json():
311
+ """
312
+ Download all predictions as JSON file
313
+ """
314
+ try:
315
+ predictions = get_all_predictions()
316
+
317
+ if not predictions:
318
+ raise HTTPException(status_code=404, detail="No predictions found to download")
319
+
320
+ # Convert to JSON
321
+ json_content = json.dumps(predictions, indent=2, default=str)
322
+
323
+ # Generate filename with timestamp
324
+ filename = f"negabot_predictions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
325
+
326
+ return Response(
327
+ content=json_content,
328
+ media_type="application/json",
329
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
330
+ )
331
+
332
+ except Exception as e:
333
+ logger.error(f"Error downloading JSON: {str(e)}")
334
+ raise HTTPException(status_code=500, detail=f"Failed to download JSON: {str(e)}")
335
+
336
+ @app.get("/dashboard", response_class=HTMLResponse)
337
+ async def dashboard():
338
+ """
339
+ Serve the analytics dashboard as HTML
340
+ """
341
+ try:
342
+ import pandas as pd
343
+ import plotly.express as px
344
+ import plotly.graph_objects as go
345
+
346
+ # Get prediction data
347
+ predictions = get_all_predictions()
348
+
349
+ if not predictions:
350
+ html_content = """
351
+ <!DOCTYPE html>
352
+ <html>
353
+ <head>
354
+ <title>NegaBot Dashboard</title>
355
+ <style>
356
+ body { font-family: Arial, sans-serif; margin: 40px; }
357
+ .container { max-width: 800px; margin: 0 auto; text-align: center; }
358
+ .warning { background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 20px; border-radius: 8px; }
359
+ </style>
360
+ </head>
361
+ <body>
362
+ <div class="container">
363
+ <h1>🤖 NegaBot Analytics Dashboard</h1>
364
+ <div class="warning">
365
+ <h3>📭 No prediction data found</h3>
366
+ <p>Make some predictions using the API first!</p>
367
+ <p><strong>Quick Start:</strong></p>
368
+ <ol>
369
+ <li>Use POST to <code>/predict</code> endpoint</li>
370
+ <li>Refresh this dashboard to see analytics</li>
371
+ </ol>
372
+ <p><strong>Available downloads:</strong></p>
373
+ <p>
374
+ <a href="/download/predictions.csv" style="color: #007bff; text-decoration: none;">📥 CSV Format</a> |
375
+ <a href="/download/predictions.json" style="color: #007bff; text-decoration: none;">📥 JSON Format</a>
376
+ </p>
377
+ </div>
378
+ </div>
379
+ </body>
380
+ </html>
381
+ """
382
+ return HTMLResponse(content=html_content)
383
+
384
+ # Process data
385
+ df = pd.DataFrame(predictions)
386
+ df['created_at'] = pd.to_datetime(df['created_at'])
387
+
388
+ # Calculate metrics
389
+ total_predictions = len(df)
390
+ positive_count = len(df[df['sentiment'] == 'Positive'])
391
+ negative_count = total_predictions - positive_count
392
+ avg_confidence = df['confidence'].mean()
393
+
394
+ # Create sentiment distribution chart
395
+ sentiment_counts = df['sentiment'].value_counts()
396
+ fig_pie = px.pie(
397
+ values=sentiment_counts.values,
398
+ names=sentiment_counts.index,
399
+ title="Sentiment Distribution",
400
+ color_discrete_map={'Positive': '#2E8B57', 'Negative': '#DC143C'}
401
+ )
402
+ pie_html = fig_pie.to_html(include_plotlyjs='cdn', div_id="sentiment-pie")
403
+
404
+ # Create confidence distribution chart
405
+ fig_hist = px.histogram(
406
+ df,
407
+ x='confidence',
408
+ nbins=20,
409
+ title="Confidence Score Distribution",
410
+ color='sentiment',
411
+ color_discrete_map={'Positive': '#2E8B57', 'Negative': '#DC143C'}
412
+ )
413
+ hist_html = fig_hist.to_html(include_plotlyjs='cdn', div_id="confidence-hist")
414
+
415
+ # Generate recent predictions table
416
+ recent_df = df.head(10).copy()
417
+ recent_df['text'] = recent_df['text'].str[:100] + '...'
418
+ recent_df['confidence'] = recent_df['confidence'].apply(lambda x: f"{x:.2%}")
419
+ recent_df['created_at'] = recent_df['created_at'].dt.strftime('%Y-%m-%d %H:%M:%S')
420
+
421
+ table_rows = ""
422
+ for _, row in recent_df.iterrows():
423
+ sentiment_class = "positive" if row['sentiment'] == 'Positive' else "negative"
424
+ table_rows += f"""
425
+ <tr>
426
+ <td>{row['created_at']}</td>
427
+ <td style="max-width: 300px;">{row['text']}</td>
428
+ <td><span class="sentiment {sentiment_class}">{row['sentiment']}</span></td>
429
+ <td>{row['confidence']}</td>
430
+ </tr>
431
+ """
432
+
433
+ # HTML template
434
+ html_content = f"""
435
+ <!DOCTYPE html>
436
+ <html>
437
+ <head>
438
+ <title>NegaBot Analytics Dashboard</title>
439
+ <meta charset="utf-8">
440
+ <meta name="viewport" content="width=device-width, initial-scale=1">
441
+ <style>
442
+ body {{
443
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
444
+ margin: 0;
445
+ padding: 20px;
446
+ background-color: #f8f9fa;
447
+ }}
448
+ .container {{
449
+ max-width: 1200px;
450
+ margin: 0 auto;
451
+ }}
452
+ .header {{
453
+ text-align: center;
454
+ color: #1f77b4;
455
+ margin-bottom: 30px;
456
+ }}
457
+ .metrics-grid {{
458
+ display: grid;
459
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
460
+ gap: 20px;
461
+ margin-bottom: 30px;
462
+ }}
463
+ .metric-card {{
464
+ background: white;
465
+ padding: 20px;
466
+ border-radius: 8px;
467
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
468
+ text-align: center;
469
+ }}
470
+ .metric-value {{
471
+ font-size: 2em;
472
+ font-weight: bold;
473
+ color: #1f77b4;
474
+ }}
475
+ .metric-label {{
476
+ color: #666;
477
+ margin-top: 5px;
478
+ }}
479
+ .charts-grid {{
480
+ display: grid;
481
+ grid-template-columns: 1fr 1fr;
482
+ gap: 20px;
483
+ margin-bottom: 30px;
484
+ }}
485
+ .chart-container {{
486
+ background: white;
487
+ padding: 20px;
488
+ border-radius: 8px;
489
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
490
+ }}
491
+ .table-container {{
492
+ background: white;
493
+ padding: 20px;
494
+ border-radius: 8px;
495
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
496
+ overflow-x: auto;
497
+ }}
498
+ table {{
499
+ width: 100%;
500
+ border-collapse: collapse;
501
+ }}
502
+ th, td {{
503
+ padding: 12px;
504
+ text-align: left;
505
+ border-bottom: 1px solid #eee;
506
+ }}
507
+ th {{
508
+ background-color: #f8f9fa;
509
+ font-weight: 600;
510
+ }}
511
+ .sentiment.positive {{
512
+ background-color: #d4edda;
513
+ color: #155724;
514
+ padding: 4px 8px;
515
+ border-radius: 4px;
516
+ font-size: 0.9em;
517
+ }}
518
+ .sentiment.negative {{
519
+ background-color: #f8d7da;
520
+ color: #721c24;
521
+ padding: 4px 8px;
522
+ border-radius: 4px;
523
+ font-size: 0.9em;
524
+ }}
525
+ .refresh-btn {{
526
+ background-color: #1f77b4;
527
+ color: white;
528
+ border: none;
529
+ padding: 10px 20px;
530
+ border-radius: 4px;
531
+ cursor: pointer;
532
+ font-size: 14px;
533
+ margin-bottom: 20px;
534
+ }}
535
+ .refresh-btn:hover {{
536
+ background-color: #1865a0;
537
+ }}
538
+ .download-btn {{
539
+ background-color: #28a745;
540
+ color: white;
541
+ text-decoration: none;
542
+ padding: 8px 16px;
543
+ border-radius: 4px;
544
+ font-size: 14px;
545
+ display: inline-block;
546
+ transition: background-color 0.2s;
547
+ }}
548
+ .download-btn:hover {{
549
+ background-color: #218838;
550
+ text-decoration: none;
551
+ color: white;
552
+ }}
553
+ @media (max-width: 768px) {{
554
+ .charts-grid {{
555
+ grid-template-columns: 1fr;
556
+ }}
557
+ }}
558
+ </style>
559
+ </head>
560
+ <body>
561
+ <div class="container">
562
+ <div class="header">
563
+ <h1>🤖 NegaBot Analytics Dashboard</h1>
564
+ <button class="refresh-btn" onclick="location.reload()">🔄 Refresh Data</button>
565
+ </div>
566
+
567
+ <div class="metrics-grid">
568
+ <div class="metric-card">
569
+ <div class="metric-value">{total_predictions}</div>
570
+ <div class="metric-label">📊 Total Predictions</div>
571
+ </div>
572
+ <div class="metric-card">
573
+ <div class="metric-value">{positive_count}</div>
574
+ <div class="metric-label">😊 Positive</div>
575
+ </div>
576
+ <div class="metric-card">
577
+ <div class="metric-value">{negative_count}</div>
578
+ <div class="metric-label">😞 Negative</div>
579
+ </div>
580
+ <div class="metric-card">
581
+ <div class="metric-value">{avg_confidence:.1%}</div>
582
+ <div class="metric-label">🎯 Avg Confidence</div>
583
+ </div>
584
+ </div>
585
+
586
+ <div class="charts-grid">
587
+ <div class="chart-container">
588
+ {pie_html}
589
+ </div>
590
+ <div class="chart-container">
591
+ {hist_html}
592
+ </div>
593
+ </div>
594
+
595
+ <div class="table-container">
596
+ <h3>📝 Recent Predictions</h3>
597
+ <div style="margin-bottom: 15px;">
598
+ <a href="/download/predictions.csv" class="download-btn" style="margin-right: 10px;">📥 Download CSV</a>
599
+ <a href="/download/predictions.json" class="download-btn">📥 Download JSON</a>
600
+ </div>
601
+ <table>
602
+ <thead>
603
+ <tr>
604
+ <th>Timestamp</th>
605
+ <th>Tweet Text</th>
606
+ <th>Sentiment</th>
607
+ <th>Confidence</th>
608
+ </tr>
609
+ </thead>
610
+ <tbody>
611
+ {table_rows}
612
+ </tbody>
613
+ </table>
614
+ </div>
615
+
616
+ <div style="text-align: center; margin-top: 30px; color: #666; font-size: 0.9em;">
617
+ 🤖 NegaBot Analytics Dashboard | Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
618
+ </div>
619
+ </div>
620
+ </body>
621
+ </html>
622
+ """
623
+
624
+ return HTMLResponse(content=html_content)
625
+
626
+ except Exception as e:
627
+ logger.error(f"Error generating dashboard: {str(e)}")
628
+ raise HTTPException(status_code=500, detail=f"Failed to generate dashboard: {str(e)}")
629
+
630
+ if __name__ == "__main__":
631
+ import uvicorn
632
+ uvicorn.run(app, host="0.0.0.0", port=7860)
database.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database and Logging System for NegaBot API
3
+ Handles prediction logging using SQLite database
4
+ """
5
+ import sqlite3
6
+ import json
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import List, Dict
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Database configuration
16
+ DB_PATH = "negabot_predictions.db"
17
+
18
+ class PredictionLogger:
19
+ def __init__(self, db_path: str = DB_PATH):
20
+ """
21
+ Initialize the prediction logger with SQLite database
22
+
23
+ Args:
24
+ db_path (str): Path to SQLite database file
25
+ """
26
+ self.db_path = db_path
27
+ self.init_database()
28
+
29
+ def init_database(self):
30
+ """Initialize the database with required tables"""
31
+ try:
32
+ with sqlite3.connect(self.db_path) as conn:
33
+ cursor = conn.cursor()
34
+
35
+ # Create predictions table
36
+ cursor.execute("""
37
+ CREATE TABLE IF NOT EXISTS predictions (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ text TEXT NOT NULL,
40
+ sentiment TEXT NOT NULL,
41
+ confidence REAL NOT NULL,
42
+ predicted_class INTEGER NOT NULL,
43
+ timestamp TEXT NOT NULL,
44
+ metadata TEXT,
45
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
46
+ )
47
+ """)
48
+
49
+ # Create index for faster queries
50
+ cursor.execute("""
51
+ CREATE INDEX IF NOT EXISTS idx_sentiment ON predictions(sentiment)
52
+ """)
53
+ cursor.execute("""
54
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON predictions(timestamp)
55
+ """)
56
+
57
+ conn.commit()
58
+ logger.info("Database initialized successfully")
59
+
60
+ except Exception as e:
61
+ logger.error(f"Error initializing database: {str(e)}")
62
+ raise e
63
+
64
+ def log_prediction(self, text: str, sentiment: str, confidence: float,
65
+ predicted_class: int = None, metadata: Dict = None):
66
+ """
67
+ Log a prediction to the database
68
+
69
+ Args:
70
+ text (str): Input text
71
+ sentiment (str): Predicted sentiment
72
+ confidence (float): Prediction confidence
73
+ predicted_class (int): Predicted class (0 or 1)
74
+ metadata (dict): Optional metadata
75
+ """
76
+ try:
77
+ # Infer predicted_class if not provided
78
+ if predicted_class is None:
79
+ predicted_class = 1 if sentiment == "Negative" else 0
80
+
81
+ with sqlite3.connect(self.db_path) as conn:
82
+ cursor = conn.cursor()
83
+
84
+ cursor.execute("""
85
+ INSERT INTO predictions (text, sentiment, confidence, predicted_class, timestamp, metadata)
86
+ VALUES (?, ?, ?, ?, ?, ?)
87
+ """, (
88
+ text,
89
+ sentiment,
90
+ confidence,
91
+ predicted_class,
92
+ datetime.now().isoformat(),
93
+ json.dumps(metadata) if metadata else None
94
+ ))
95
+
96
+ conn.commit()
97
+
98
+ except Exception as e:
99
+ logger.error(f"Error logging prediction: {str(e)}")
100
+ raise e
101
+
102
+ def get_all_predictions(self, limit: int = None) -> List[Dict]:
103
+ """
104
+ Get all predictions from the database
105
+
106
+ Args:
107
+ limit (int): Maximum number of records to return
108
+
109
+ Returns:
110
+ List of prediction dictionaries
111
+ """
112
+ try:
113
+ with sqlite3.connect(self.db_path) as conn:
114
+ cursor = conn.cursor()
115
+
116
+ query = """
117
+ SELECT id, text, sentiment, confidence, predicted_class, timestamp, metadata, created_at
118
+ FROM predictions
119
+ ORDER BY created_at DESC
120
+ """
121
+
122
+ if limit:
123
+ query += f" LIMIT {limit}"
124
+
125
+ cursor.execute(query)
126
+ rows = cursor.fetchall()
127
+
128
+ predictions = []
129
+ for row in rows:
130
+ prediction = {
131
+ "id": row[0],
132
+ "text": row[1],
133
+ "sentiment": row[2],
134
+ "confidence": row[3],
135
+ "predicted_class": row[4],
136
+ "timestamp": row[5],
137
+ "metadata": json.loads(row[6]) if row[6] else None,
138
+ "created_at": row[7]
139
+ }
140
+ predictions.append(prediction)
141
+
142
+ return predictions
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error getting predictions: {str(e)}")
146
+ return []
147
+
148
+ def get_predictions_by_sentiment(self, sentiment: str) -> List[Dict]:
149
+ """
150
+ Get predictions filtered by sentiment
151
+
152
+ Args:
153
+ sentiment (str): Sentiment to filter by ("Positive" or "Negative")
154
+
155
+ Returns:
156
+ List of prediction dictionaries
157
+ """
158
+ try:
159
+ with sqlite3.connect(self.db_path) as conn:
160
+ cursor = conn.cursor()
161
+
162
+ cursor.execute("""
163
+ SELECT id, text, sentiment, confidence, predicted_class, timestamp, metadata, created_at
164
+ FROM predictions
165
+ WHERE sentiment = ?
166
+ ORDER BY created_at DESC
167
+ """, (sentiment,))
168
+
169
+ rows = cursor.fetchall()
170
+
171
+ predictions = []
172
+ for row in rows:
173
+ prediction = {
174
+ "id": row[0],
175
+ "text": row[1],
176
+ "sentiment": row[2],
177
+ "confidence": row[3],
178
+ "predicted_class": row[4],
179
+ "timestamp": row[5],
180
+ "metadata": json.loads(row[6]) if row[6] else None,
181
+ "created_at": row[7]
182
+ }
183
+ predictions.append(prediction)
184
+
185
+ return predictions
186
+
187
+ except Exception as e:
188
+ logger.error(f"Error getting predictions by sentiment: {str(e)}")
189
+ return []
190
+
191
+ def get_stats(self) -> Dict:
192
+ """
193
+ Get prediction statistics
194
+
195
+ Returns:
196
+ Dictionary with statistics
197
+ """
198
+ try:
199
+ with sqlite3.connect(self.db_path) as conn:
200
+ cursor = conn.cursor()
201
+
202
+ # Total count
203
+ cursor.execute("SELECT COUNT(*) FROM predictions")
204
+ total_count = cursor.fetchone()[0]
205
+
206
+ if total_count == 0:
207
+ return {
208
+ "total_predictions": 0,
209
+ "positive_count": 0,
210
+ "negative_count": 0,
211
+ "average_confidence": 0
212
+ }
213
+
214
+ # Sentiment counts
215
+ cursor.execute("SELECT sentiment, COUNT(*) FROM predictions GROUP BY sentiment")
216
+ sentiment_counts = dict(cursor.fetchall())
217
+
218
+ # Average confidence
219
+ cursor.execute("SELECT AVG(confidence) FROM predictions")
220
+ avg_confidence = cursor.fetchone()[0]
221
+
222
+ return {
223
+ "total_predictions": total_count,
224
+ "positive_count": sentiment_counts.get("Positive", 0),
225
+ "negative_count": sentiment_counts.get("Negative", 0),
226
+ "average_confidence": round(avg_confidence, 4) if avg_confidence else 0
227
+ }
228
+
229
+ except Exception as e:
230
+ logger.error(f"Error getting stats: {str(e)}")
231
+ return {}
232
+
233
+ # Global logger instance
234
+ _logger_instance = None
235
+
236
+ def get_logger():
237
+ """Get the global logger instance"""
238
+ global _logger_instance
239
+ if _logger_instance is None:
240
+ _logger_instance = PredictionLogger()
241
+ return _logger_instance
242
+
243
+ def log_prediction(text: str, sentiment: str, confidence: float, metadata: Dict = None):
244
+ """Convenience function to log a prediction"""
245
+ logger_instance = get_logger()
246
+ logger_instance.log_prediction(text, sentiment, confidence, metadata=metadata)
247
+
248
+ def get_all_predictions(limit: int = None) -> List[Dict]:
249
+ """Convenience function to get all predictions"""
250
+ logger_instance = get_logger()
251
+ return logger_instance.get_all_predictions(limit=limit)
252
+
253
+ def get_predictions_by_sentiment(sentiment: str) -> List[Dict]:
254
+ """Convenience function to get predictions by sentiment"""
255
+ logger_instance = get_logger()
256
+ return logger_instance.get_predictions_by_sentiment(sentiment)
257
+
258
+ def get_prediction_stats() -> Dict:
259
+ """Convenience function to get prediction statistics"""
260
+ logger_instance = get_logger()
261
+ return logger_instance.get_stats()
262
+
263
+ if __name__ == "__main__":
264
+ # Test the logging system
265
+ logger_instance = PredictionLogger()
266
+
267
+ # Test logging
268
+ test_predictions = [
269
+ ("This product is amazing!", "Positive", 0.95),
270
+ ("Terrible quality, waste of money", "Negative", 0.89),
271
+ ("It's okay, nothing special", "Positive", 0.67),
272
+ ("Awful customer service", "Negative", 0.92)
273
+ ]
274
+
275
+ print("Testing prediction logging...")
276
+ for text, sentiment, confidence in test_predictions:
277
+ logger_instance.log_prediction(text, sentiment, confidence)
278
+ print(f"Logged: {sentiment} - {text}")
279
+
280
+ # Test retrieval
281
+ print("\nRetrieving all predictions:")
282
+ predictions = logger_instance.get_all_predictions()
283
+ for pred in predictions:
284
+ print(f"ID: {pred['id']}, Sentiment: {pred['sentiment']}, Text: {pred['text'][:50]}...")
285
+
286
+ # Test stats
287
+ print("\nPrediction statistics:")
288
+ stats = logger_instance.get_stats()
289
+ print(json.dumps(stats, indent=2))