Spaces:
Sleeping
Sleeping
""" | |
NegaBot API - FastAPI application for tweet sentiment classification | |
""" | |
from fastapi import FastAPI, HTTPException | |
from fastapi.responses import HTMLResponse, Response | |
from pydantic import BaseModel, Field | |
from typing import List, Optional | |
import logging | |
from datetime import datetime | |
import json | |
from model import get_model | |
from database import log_prediction, get_all_predictions | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Initialize FastAPI app | |
app = FastAPI( | |
title="NegaBot API", | |
description="NegaBot is a complete sentiment analysis solution that detects positive and negative sentiment in tweets, particularly focusing on product criticism detection. Built with FastAPI, Streamlit, and the powerful jatinmehra/NegaBot-Product-Criticism-Catcher model.", | |
version="1.0.0" | |
) | |
# Pydantic models for request/response validation | |
class TweetRequest(BaseModel): | |
text: str = Field(..., min_length=1, max_length=1000, description="Tweet text to analyze") | |
metadata: Optional[dict] = Field(default=None, description="Optional metadata") | |
class TweetResponse(BaseModel): | |
text: str | |
sentiment: str | |
confidence: float | |
predicted_class: int | |
probabilities: dict | |
timestamp: str | |
request_id: Optional[str] = None | |
class BatchTweetRequest(BaseModel): | |
tweets: List[str] = Field(..., min_items=1, max_items=50, description="List of tweets to analyze") | |
metadata: Optional[dict] = Field(default=None, description="Optional metadata") | |
class BatchTweetResponse(BaseModel): | |
results: List[TweetResponse] | |
total_processed: int | |
timestamp: str | |
class HealthResponse(BaseModel): | |
status: str | |
model_loaded: bool | |
timestamp: str | |
# Global variables | |
model = None | |
async def startup_event(): | |
"""Initialize the model on startup""" | |
global model | |
try: | |
logger.info("Starting NegaBot API...") | |
model = get_model() | |
logger.info("Model loaded successfully") | |
except Exception as e: | |
logger.error(f"Failed to load model: {str(e)}") | |
raise e | |
async def root(): | |
"""Root endpoint with API information""" | |
return { | |
"message": "Welcome to NegaBot API", | |
"version": "1.0.0", | |
"description": "Tweet Sentiment Classification using NegaBot model", | |
"OpenAPI Link": "https://jatinmehra-negabot-api.hf.space/docs", | |
"endpoints": { | |
"predict": "/predict - Single tweet prediction", | |
"batch_predict": "/batch_predict - Multiple tweets prediction", | |
"health": "/health - API health check", | |
"stats": "/stats - Prediction statistics", | |
"dashboard": "/dashboard - Interactive analytics dashboard", | |
"dashboard_data": "/dashboard/data - Dashboard data as JSON", | |
"download_csv": "/download/predictions.csv - Download predictions as CSV", | |
"download_json": "/download/predictions.json - Download predictions as JSON" | |
} | |
} | |
async def health_check(): | |
"""Health check endpoint""" | |
return HealthResponse( | |
status="healthy" if model is not None else "unhealthy", | |
model_loaded=model is not None, | |
timestamp=datetime.now().isoformat() | |
) | |
async def predict_sentiment(request: TweetRequest): | |
""" | |
Predict sentiment for a single tweet | |
Args: | |
request: TweetRequest containing the tweet text | |
Returns: | |
TweetResponse with prediction results | |
""" | |
try: | |
if model is None: | |
raise HTTPException(status_code=503, detail="Model not loaded") | |
# Get prediction from model | |
result = model.predict(request.text) | |
# Create response | |
response = TweetResponse( | |
text=result["text"], | |
sentiment=result["sentiment"], | |
confidence=result["confidence"], | |
predicted_class=result["predicted_class"], | |
probabilities=result["probabilities"], | |
timestamp=datetime.now().isoformat() | |
) | |
# Log the prediction | |
log_prediction( | |
text=request.text, | |
sentiment=result["sentiment"], | |
confidence=result["confidence"], | |
metadata=request.metadata | |
) | |
logger.info(f"Prediction made: {result['sentiment']} (confidence: {result['confidence']:.2%})") | |
return response | |
except Exception as e: | |
logger.error(f"Error in prediction: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}") | |
async def batch_predict_sentiment(request: BatchTweetRequest): | |
""" | |
Predict sentiment for multiple tweets | |
Args: | |
request: BatchTweetRequest containing list of tweets | |
Returns: | |
BatchTweetResponse with all prediction results | |
""" | |
try: | |
if model is None: | |
raise HTTPException(status_code=503, detail="Model not loaded") | |
# Get predictions for all tweets | |
results = model.batch_predict(request.tweets) | |
# Create response objects | |
responses = [] | |
for result in results: | |
response = TweetResponse( | |
text=result["text"], | |
sentiment=result["sentiment"], | |
confidence=result["confidence"], | |
predicted_class=result["predicted_class"], | |
probabilities=result["probabilities"], | |
timestamp=datetime.now().isoformat() | |
) | |
responses.append(response) | |
# Log each prediction | |
log_prediction( | |
text=result["text"], | |
sentiment=result["sentiment"], | |
confidence=result["confidence"], | |
metadata=request.metadata | |
) | |
batch_response = BatchTweetResponse( | |
results=responses, | |
total_processed=len(responses), | |
timestamp=datetime.now().isoformat() | |
) | |
logger.info(f"Batch prediction completed: {len(responses)} tweets processed") | |
return batch_response | |
except Exception as e: | |
logger.error(f"Error in batch prediction: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}") | |
async def get_prediction_stats(): | |
""" | |
Get prediction statistics | |
Returns: | |
Dictionary with prediction statistics | |
""" | |
try: | |
predictions = get_all_predictions() | |
if not predictions: | |
return { | |
"total_predictions": 0, | |
"positive_count": 0, | |
"negative_count": 0, | |
"average_confidence": 0, | |
"message": "No predictions found" | |
} | |
total = len(predictions) | |
positive_count = sum(1 for p in predictions if p["sentiment"] == "Positive") | |
negative_count = total - positive_count | |
avg_confidence = sum(p["confidence"] for p in predictions) / total | |
stats = { | |
"total_predictions": total, | |
"positive_count": positive_count, | |
"negative_count": negative_count, | |
"positive_percentage": round((positive_count / total) * 100, 2), | |
"negative_percentage": round((negative_count / total) * 100, 2), | |
"average_confidence": round(avg_confidence, 4), | |
"last_updated": datetime.now().isoformat() | |
} | |
return stats | |
except Exception as e: | |
logger.error(f"Error getting stats: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Failed to get statistics: {str(e)}") | |
async def get_dashboard_data(): | |
""" | |
Get dashboard data as JSON for API consumption | |
""" | |
try: | |
predictions = get_all_predictions() | |
if not predictions: | |
return { | |
"metrics": { | |
"total_predictions": 0, | |
"positive_count": 0, | |
"negative_count": 0, | |
"average_confidence": 0 | |
}, | |
"recent_predictions": [], | |
"message": "No predictions found" | |
} | |
# Calculate metrics | |
total = len(predictions) | |
positive_count = sum(1 for p in predictions if p["sentiment"] == "Positive") | |
negative_count = total - positive_count | |
avg_confidence = sum(p["confidence"] for p in predictions) / total | |
# Get recent predictions (last 20) | |
recent_predictions = sorted(predictions, key=lambda x: x["created_at"], reverse=True)[:20] | |
return { | |
"metrics": { | |
"total_predictions": total, | |
"positive_count": positive_count, | |
"negative_count": negative_count, | |
"positive_percentage": round((positive_count / total) * 100, 2), | |
"negative_percentage": round((negative_count / total) * 100, 2), | |
"average_confidence": round(avg_confidence, 4) | |
}, | |
"recent_predictions": recent_predictions, | |
"last_updated": datetime.now().isoformat() | |
} | |
except Exception as e: | |
logger.error(f"Error getting dashboard data: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Failed to get dashboard data: {str(e)}") | |
async def download_predictions_csv(): | |
""" | |
Download all predictions as CSV file | |
""" | |
try: | |
predictions = get_all_predictions() | |
if not predictions: | |
raise HTTPException(status_code=404, detail="No predictions found to download") | |
# Convert to pandas DataFrame for easy CSV export | |
import pandas as pd | |
df = pd.DataFrame(predictions) | |
# Convert to CSV | |
csv_content = df.to_csv(index=False) | |
# Generate filename with timestamp | |
filename = f"negabot_predictions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" | |
return Response( | |
content=csv_content, | |
media_type="text/csv", | |
headers={"Content-Disposition": f"attachment; filename={filename}"} | |
) | |
except Exception as e: | |
logger.error(f"Error downloading CSV: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Failed to download CSV: {str(e)}") | |
async def download_predictions_json(): | |
""" | |
Download all predictions as JSON file | |
""" | |
try: | |
predictions = get_all_predictions() | |
if not predictions: | |
raise HTTPException(status_code=404, detail="No predictions found to download") | |
# Convert to JSON | |
json_content = json.dumps(predictions, indent=2, default=str) | |
# Generate filename with timestamp | |
filename = f"negabot_predictions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" | |
return Response( | |
content=json_content, | |
media_type="application/json", | |
headers={"Content-Disposition": f"attachment; filename={filename}"} | |
) | |
except Exception as e: | |
logger.error(f"Error downloading JSON: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Failed to download JSON: {str(e)}") | |
async def dashboard(): | |
""" | |
Serve the analytics dashboard as HTML | |
""" | |
try: | |
import pandas as pd | |
import plotly.express as px | |
import plotly.graph_objects as go | |
# Get prediction data | |
predictions = get_all_predictions() | |
if not predictions: | |
html_content = """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>NegaBot Dashboard</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 40px; } | |
.container { max-width: 800px; margin: 0 auto; text-align: center; } | |
.warning { background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 20px; border-radius: 8px; } | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>π€ NegaBot Analytics Dashboard</h1> | |
<div class="warning"> | |
<h3>π No prediction data found</h3> | |
<p>Make some predictions using the API first!</p> | |
<p><strong>Quick Start:</strong></p> | |
<ol> | |
<li>Use POST to <code>/predict</code> endpoint</li> | |
<li>Refresh this dashboard to see analytics</li> | |
</ol> | |
<p><strong>Available downloads:</strong></p> | |
<p> | |
<a href="/download/predictions.csv" style="color: #007bff; text-decoration: none;">π₯ CSV Format</a> | | |
<a href="/download/predictions.json" style="color: #007bff; text-decoration: none;">π₯ JSON Format</a> | |
</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
""" | |
return HTMLResponse(content=html_content) | |
# Process data | |
df = pd.DataFrame(predictions) | |
df['created_at'] = pd.to_datetime(df['created_at']) | |
# Calculate metrics | |
total_predictions = len(df) | |
positive_count = len(df[df['sentiment'] == 'Positive']) | |
negative_count = total_predictions - positive_count | |
avg_confidence = df['confidence'].mean() | |
# Create sentiment distribution chart | |
sentiment_counts = df['sentiment'].value_counts() | |
fig_pie = px.pie( | |
values=sentiment_counts.values, | |
names=sentiment_counts.index, | |
title="Sentiment Distribution", | |
color_discrete_map={'Positive': '#2E8B57', 'Negative': '#DC143C'} | |
) | |
pie_html = fig_pie.to_html(include_plotlyjs='cdn', div_id="sentiment-pie") | |
# Create confidence distribution chart | |
fig_hist = px.histogram( | |
df, | |
x='confidence', | |
nbins=20, | |
title="Confidence Score Distribution", | |
color='sentiment', | |
color_discrete_map={'Positive': '#2E8B57', 'Negative': '#DC143C'} | |
) | |
hist_html = fig_hist.to_html(include_plotlyjs='cdn', div_id="confidence-hist") | |
# Generate recent predictions table | |
recent_df = df.head(10).copy() | |
recent_df['text'] = recent_df['text'].str[:100] + '...' | |
recent_df['confidence'] = recent_df['confidence'].apply(lambda x: f"{x:.2%}") | |
recent_df['created_at'] = recent_df['created_at'].dt.strftime('%Y-%m-%d %H:%M:%S') | |
table_rows = "" | |
for _, row in recent_df.iterrows(): | |
sentiment_class = "positive" if row['sentiment'] == 'Positive' else "negative" | |
table_rows += f""" | |
<tr> | |
<td>{row['created_at']}</td> | |
<td style="max-width: 300px;">{row['text']}</td> | |
<td><span class="sentiment {sentiment_class}">{row['sentiment']}</span></td> | |
<td>{row['confidence']}</td> | |
</tr> | |
""" | |
# HTML template | |
html_content = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>NegaBot Analytics Dashboard</title> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<style> | |
body {{ | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: #f8f9fa; | |
}} | |
.container {{ | |
max-width: 1200px; | |
margin: 0 auto; | |
}} | |
.header {{ | |
text-align: center; | |
color: #1f77b4; | |
margin-bottom: 30px; | |
}} | |
.metrics-grid {{ | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 20px; | |
margin-bottom: 30px; | |
}} | |
.metric-card {{ | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
text-align: center; | |
}} | |
.metric-value {{ | |
font-size: 2em; | |
font-weight: bold; | |
color: #1f77b4; | |
}} | |
.metric-label {{ | |
color: #666; | |
margin-top: 5px; | |
}} | |
.charts-grid {{ | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 20px; | |
margin-bottom: 30px; | |
}} | |
.chart-container {{ | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
}} | |
.table-container {{ | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
overflow-x: auto; | |
}} | |
table {{ | |
width: 100%; | |
border-collapse: collapse; | |
}} | |
th, td {{ | |
padding: 12px; | |
text-align: left; | |
border-bottom: 1px solid #eee; | |
}} | |
th {{ | |
background-color: #f8f9fa; | |
font-weight: 600; | |
}} | |
.sentiment.positive {{ | |
background-color: #d4edda; | |
color: #155724; | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 0.9em; | |
}} | |
.sentiment.negative {{ | |
background-color: #f8d7da; | |
color: #721c24; | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 0.9em; | |
}} | |
.refresh-btn {{ | |
background-color: #1f77b4; | |
color: white; | |
border: none; | |
padding: 10px 20px; | |
border-radius: 4px; | |
cursor: pointer; | |
font-size: 14px; | |
margin-bottom: 20px; | |
}} | |
.refresh-btn:hover {{ | |
background-color: #1865a0; | |
}} | |
.download-btn {{ | |
background-color: #28a745; | |
color: white; | |
text-decoration: none; | |
padding: 8px 16px; | |
border-radius: 4px; | |
font-size: 14px; | |
display: inline-block; | |
transition: background-color 0.2s; | |
}} | |
.download-btn:hover {{ | |
background-color: #218838; | |
text-decoration: none; | |
color: white; | |
}} | |
@media (max-width: 768px) {{ | |
.charts-grid {{ | |
grid-template-columns: 1fr; | |
}} | |
}} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>π€ NegaBot Analytics Dashboard</h1> | |
<button class="refresh-btn" onclick="location.reload()">π Refresh Data</button> | |
</div> | |
<div class="metrics-grid"> | |
<div class="metric-card"> | |
<div class="metric-value">{total_predictions}</div> | |
<div class="metric-label">π Total Predictions</div> | |
</div> | |
<div class="metric-card"> | |
<div class="metric-value">{positive_count}</div> | |
<div class="metric-label">π Positive</div> | |
</div> | |
<div class="metric-card"> | |
<div class="metric-value">{negative_count}</div> | |
<div class="metric-label">π Negative</div> | |
</div> | |
<div class="metric-card"> | |
<div class="metric-value">{avg_confidence:.1%}</div> | |
<div class="metric-label">π― Avg Confidence</div> | |
</div> | |
</div> | |
<div class="charts-grid"> | |
<div class="chart-container"> | |
{pie_html} | |
</div> | |
<div class="chart-container"> | |
{hist_html} | |
</div> | |
</div> | |
<div class="table-container"> | |
<h3>π Recent Predictions</h3> | |
<div style="margin-bottom: 15px;"> | |
<a href="/download/predictions.csv" class="download-btn" style="margin-right: 10px;">π₯ Download CSV</a> | |
<a href="/download/predictions.json" class="download-btn">π₯ Download JSON</a> | |
</div> | |
<table> | |
<thead> | |
<tr> | |
<th>Timestamp</th> | |
<th>Tweet Text</th> | |
<th>Sentiment</th> | |
<th>Confidence</th> | |
</tr> | |
</thead> | |
<tbody> | |
{table_rows} | |
</tbody> | |
</table> | |
</div> | |
<div style="text-align: center; margin-top: 30px; color: #666; font-size: 0.9em;"> | |
π€ NegaBot Analytics Dashboard | Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | |
</div> | |
</div> | |
</body> | |
</html> | |
""" | |
return HTMLResponse(content=html_content) | |
except Exception as e: | |
logger.error(f"Error generating dashboard: {str(e)}") | |
raise HTTPException(status_code=500, detail=f"Failed to generate dashboard: {str(e)}") | |
if __name__ == "__main__": | |
import uvicorn | |
uvicorn.run(app, host="0.0.0.0", port=7860) |