fitsblb's picture
Deploy advanced sentiment analyzer with 4-model ensemble system
82c705b
import os
import sys
import time
from flask import Flask, render_template, request, jsonify, abort
from werkzeug.exceptions import RequestEntityTooLarge, BadRequest
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import config
from logging_config import get_logger
from model import predict, ModelError
# Initialize logger
logger = get_logger('app')
# Try to import advanced features
try:
from advanced_api import advanced_bp
ADVANCED_FEATURES_AVAILABLE = True
except ImportError as e:
logger.warning(f"Advanced features not available: {e}")
ADVANCED_FEATURES_AVAILABLE = False
app = Flask(__name__)
app.config['SECRET_KEY'] = config.SECRET_KEY
app.config['MAX_CONTENT_LENGTH'] = config.MAX_CONTENT_LENGTH
# Register advanced API blueprint if available
if ADVANCED_FEATURES_AVAILABLE:
app.register_blueprint(advanced_bp)
logger.info("Advanced API endpoints registered")
else:
logger.info("Running with basic features only")
# Log application startup
logger.info(f"Starting Sentiment Analyzer application in {os.getenv('FLASK_ENV', 'development')} mode")
logger.info(f"Using model: {config.MODEL_NAME}")
@app.before_request
def log_request_info():
"""Log incoming requests for monitoring and debugging."""
start_time = time.time()
request.start_time = start_time
logger.debug(f"Request: {request.method} {request.url} from {request.remote_addr}")
@app.after_request
def log_response_info(response):
"""Log response information including processing time."""
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.debug(f"Response: {response.status_code} for {request.method} {request.url} "
f"({duration:.3f}s)")
return response
@app.errorhandler(400)
def bad_request(error):
"""Handle bad request errors with proper logging and user-friendly response."""
logger.warning(f"Bad request from {request.remote_addr}: {error}")
if request.path.startswith('/api/'):
return jsonify({
'error': 'Bad Request',
'message': 'Invalid input data. Please check your request format.'
}), 400
return render_template('error.html',
error_code=400,
error_message="Invalid request. Please try again."), 400
@app.errorhandler(413)
def request_entity_too_large(error):
"""Handle file/request too large errors."""
logger.warning(f"Request too large from {request.remote_addr}")
if request.path.startswith('/api/'):
return jsonify({
'error': 'Request Too Large',
'message': f'Text must be under {config.MAX_TEXT_LENGTH} characters.'
}), 413
return render_template('error.html',
error_code=413,
error_message=f"Text is too long. Please keep it under {config.MAX_TEXT_LENGTH} characters."), 413
@app.errorhandler(500)
def internal_server_error(error):
"""Handle internal server errors with proper logging."""
logger.error(f"Internal server error: {error}", exc_info=True)
if request.path.startswith('/api/'):
return jsonify({
'error': 'Internal Server Error',
'message': 'Something went wrong on our end. Please try again later.'
}), 500
return render_template('error.html',
error_code=500,
error_message="Something went wrong. Please try again later."), 500
def validate_text_input(text):
"""
Validate text input for sentiment analysis.
Args:
text: Input text to validate
Returns:
str: Cleaned and validated text
Raises:
ValueError: If text is invalid
"""
if not text or not isinstance(text, str):
raise ValueError("Text input is required and must be a string")
text = text.strip()
if not text:
raise ValueError("Text cannot be empty")
if len(text) > config.MAX_TEXT_LENGTH:
raise ValueError(f"Text must be under {config.MAX_TEXT_LENGTH} characters")
# Basic content filtering (you can extend this)
if len(text) < 3:
raise ValueError("Text must be at least 3 characters long")
return text
@app.route("/", methods=["GET", "POST"])
def home():
"""Main route for the web interface."""
if request.method == "POST":
try:
user_input = request.form.get("text_input", "").strip()
logger.info(f"Processing sentiment analysis request from web interface")
# Validate input
validated_text = validate_text_input(user_input)
# Get prediction
start_time = time.time()
label, score = predict(validated_text)
processing_time = time.time() - start_time
logger.info(f"Sentiment analysis completed: {label} ({score:.3f}) in {processing_time:.3f}s")
return render_template("result.html",
input_text=validated_text,
prediction=label,
confidence=score)
except ValueError as e:
logger.warning(f"Validation error: {e}")
return render_template('error.html',
error_code=400,
error_message=str(e)), 400
except ModelError as e:
logger.error(f"Model error: {e}")
return render_template('error.html',
error_code=500,
error_message="AI model is temporarily unavailable. Please try again later."), 500
except Exception as e:
logger.error(f"Unexpected error in home route: {e}", exc_info=True)
return render_template('error.html',
error_code=500,
error_message="An unexpected error occurred. Please try again."), 500
return render_template("home.html")
@app.route("/api/analyze", methods=["POST"])
def api_analyze():
"""
REST API endpoint for sentiment analysis.
Expected JSON input:
{
"text": "Text to analyze"
}
Returns JSON response:
{
"sentiment": "Positive|Neutral|Negative",
"confidence": 0.95,
"processing_time": 0.123
}
"""
try:
if not request.is_json:
logger.warning("API request without JSON content type")
return jsonify({
'error': 'Bad Request',
'message': 'Content-Type must be application/json'
}), 400
data = request.get_json()
if not data:
return jsonify({
'error': 'Bad Request',
'message': 'Empty JSON payload'
}), 400
text = data.get('text')
logger.info(f"Processing API sentiment analysis request")
# Validate input
validated_text = validate_text_input(text)
# Get prediction with timing
start_time = time.time()
label, score = predict(validated_text)
processing_time = time.time() - start_time
logger.info(f"API sentiment analysis completed: {label} ({score:.3f}) in {processing_time:.3f}s")
return jsonify({
'sentiment': label,
'confidence': round(score, 4),
'processing_time': round(processing_time, 3),
'text_length': len(validated_text)
})
except ValueError as e:
logger.warning(f"API validation error: {e}")
return jsonify({
'error': 'Validation Error',
'message': str(e)
}), 400
except ModelError as e:
logger.error(f"API model error: {e}")
return jsonify({
'error': 'Model Error',
'message': 'AI model is temporarily unavailable'
}), 503
except Exception as e:
logger.error(f"Unexpected API error: {e}", exc_info=True)
return jsonify({
'error': 'Internal Server Error',
'message': 'An unexpected error occurred'
}), 500
@app.route("/api/health", methods=["GET"])
def health_check():
"""Health check endpoint for monitoring and load balancers."""
try:
# Quick model check with simple text
predict("test")
return jsonify({
'status': 'healthy',
'model': config.MODEL_NAME,
'version': '1.0.0',
'timestamp': time.time()
})
except Exception as e:
logger.error(f"Health check failed: {e}")
return jsonify({
'status': 'unhealthy',
'error': str(e),
'timestamp': time.time()
}), 503
@app.route("/api/info", methods=["GET"])
def api_info():
"""API information endpoint."""
endpoints = {
'analyze': '/api/analyze',
'health': '/api/health',
'info': '/api/info'
}
# Add advanced endpoints if available
if ADVANCED_FEATURES_AVAILABLE:
endpoints.update({
'compare_models': '/api/v2/compare',
'batch_analyze': '/api/v2/batch',
'models_info': '/api/v2/models',
'analytics': '/api/v2/analytics',
'test_models': '/api/v2/test-models'
})
return jsonify({
'name': 'Sentiment Analyzer API',
'version': '2.0.0' if ADVANCED_FEATURES_AVAILABLE else '1.0.0',
'model': config.MODEL_NAME,
'features': {
'basic_analysis': True,
'model_comparison': ADVANCED_FEATURES_AVAILABLE,
'batch_processing': ADVANCED_FEATURES_AVAILABLE,
'analytics': ADVANCED_FEATURES_AVAILABLE
},
'endpoints': endpoints,
'limits': {
'max_text_length': config.MAX_TEXT_LENGTH,
'rate_limit': config.API_RATE_LIMIT,
'max_batch_size': 50 if ADVANCED_FEATURES_AVAILABLE else 1
}
})
if __name__ == "__main__":
# Get host and port from environment variables (for cloud deployment)
host = os.getenv('HOST', config.HOST)
port = int(os.getenv('PORT', config.PORT))
debug = os.getenv('FLASK_ENV', 'development') == 'development'
logger.info(f"Starting {'development' if debug else 'production'} server on {host}:{port}")
app.run(host=host, port=port, debug=debug)