kushvanth/iqac_fast_api
1
1"""2Enhanced FastAPI Service for Comment Sentiment Analysis3Version 3.0.0 - Major accuracy improvements with advanced classification4Features:5- Multi-stage sentiment detection6- Context-aware negative pattern matching7- Improved neutral/meta-comment detection8- Enhanced accuracy through ensemble approach9"""10 11from fastapi import FastAPI, HTTPException, Depends12from fastapi.middleware.cors import CORSMiddleware13from pydantic import BaseModel, Field, validator14from pydantic_settings import BaseSettings15from typing import List, Dict, Any, Optional16from functools import lru_cache17import uvicorn18import pandas as pd19import numpy as np20import os21import re22from datetime import datetime23import logging24 25# Configure logging26logging.basicConfig(27 level=logging.INFO,28 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'29)30logger = logging.getLogger(__name__)31 32# NLTK Setup33import nltk34import ssl35 36try:37 _create_unverified_https_context = ssl._create_unverified_context38except AttributeError:39 pass40else:41 ssl._create_default_https_context = _create_unverified_https_context42 43nltk_data_dir = '/tmp/nltk_data'44os.makedirs(nltk_data_dir, exist_ok=True)45nltk.data.path.insert(0, nltk_data_dir)46 47def ensure_nltk_data():48 """Ensure all required NLTK data is downloaded"""49 resources = ['vader_lexicon', 'punkt', 'stopwords', 'wordnet', 'omw-1.4']50 51 for resource in resources:52 try:53 if resource == 'vader_lexicon':54 nltk.data.find('sentiment/vader_lexicon.zip')55 elif resource == 'punkt':56 nltk.data.find('tokenizers/punkt')57 elif resource in ['stopwords', 'wordnet', 'omw-1.4']:58 nltk.data.find(f'corpora/{resource}')59 logger.info(f"✓ NLTK resource '{resource}' already available")60 except LookupError:61 logger.info(f"Downloading NLTK resource '{resource}'...")62 try:63 nltk.download(resource, download_dir=nltk_data_dir, quiet=False)64 logger.info(f"✓ Successfully downloaded '{resource}'")65 except Exception as e:66 logger.error(f"✗ Failed to download '{resource}': {e}")67 68logger.info("Ensuring NLTK data is available...")69ensure_nltk_data()70 71from nltk.sentiment import SentimentIntensityAnalyzer72from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline73from scipy.special import softmax74import torch75 76# Configuration77class Settings(BaseSettings):78 """Application settings"""79 app_name: str = "Comment Analysis API"80 app_version: str = "3.0.0"81 debug_mode: bool = False82 83 max_comments_per_request: int = 100084 max_comment_length: int = 500085 min_comment_words: int = 186 87 # Enhanced thresholds for better accuracy88 vader_strong_pos_threshold: float = 0.589 vader_pos_threshold: float = 0.290 vader_neg_threshold: float = -0.291 vader_strong_neg_threshold: float = -0.592 93 roberta_strong_pos_threshold: float = 0.7094 roberta_pos_threshold: float = 0.5595 roberta_neg_threshold: float = 0.4096 roberta_strong_neg_threshold: float = 0.6097 98 # Adjusted weights for better accuracy99 combined_weight_vader: float = 0.4100 combined_weight_roberta: float = 0.6101 102 model_cache_dir: str = "/tmp/model_cache"103 roberta_model_name: str = "cardiffnlp/twitter-roberta-base-sentiment"104 use_abstractive_summary: bool = False105 summarizer_model: str = "facebook/bart-large-cnn"106 max_summary_length: int = 100107 min_summary_length: int = 25108 109 enable_caching: bool = True110 cache_size: int = 500111 batch_size: int = 32112 113 class Config:114 env_file = ".env"115 env_file_encoding = 'utf-8'116 extra = 'ignore'117 118@lru_cache()119def get_settings() -> Settings:120 """Cached settings instance"""121 settings = Settings()122 total = settings.combined_weight_vader + settings.combined_weight_roberta123 if not (0.99 <= total <= 1.01):124 logger.warning(f"Weights sum to {total}, normalizing to 1.0")125 settings.combined_weight_vader /= total126 settings.combined_weight_roberta /= total127 return settings128 129# Pydantic Models130class FacultyInfo(BaseModel):131 faculty_name: str = Field(..., min_length=1, max_length=200)132 staff_id: str = Field(..., min_length=1, max_length=50)133 course_code: str = Field(..., min_length=1, max_length=50)134 course_name: str = Field(..., min_length=1, max_length=200)135 136class CommentAnalysisRequest(BaseModel):137 comments: List[str] = Field(..., min_items=1)138 faculty_info: FacultyInfo139 140 @validator('comments')141 def validate_comments(cls, v):142 settings = get_settings()143 if len(v) > settings.max_comments_per_request:144 raise ValueError(f'Maximum {settings.max_comments_per_request} comments per request')145 for idx, comment in enumerate(v):146 if len(comment) > settings.max_comment_length:147 raise ValueError(f'Comment {idx} exceeds maximum length of {settings.max_comment_length} characters')148 return v149 150class SentimentDistribution(BaseModel):151 positive_percentage: float152 negative_percentage: float153 neutral_percentage: float154 155class DetailedScores(BaseModel):156 average_positive: float157 average_negative: float158 average_neutral: float159 average_compound: Optional[float] = None160 161class DetailedAnalysis(BaseModel):162 vader_scores: DetailedScores163 roberta_scores: DetailedScores164 165class AnalysisResult(BaseModel):166 total_comments: int167 positive_comments: int168 negative_comments: int169 neutral_comments: int170 positive_sentiment: float171 negative_sentiment: float172 neutral_sentiment: float173 overall_sentiment: str174 sentiment_distribution: SentimentDistribution175 negative_comments_summary: str176 negative_comments_list: List[str]177 key_insights: List[str]178 recommendations: List[str]179 detailed_analysis: DetailedAnalysis180 faculty_info: Dict[str, str]181 analysis_timestamp: str182 183class CommentAnalysisResponse(BaseModel):184 success: bool185 analysis: Optional[AnalysisResult] = None186 message: str187 188# Initialize FastAPI189app = FastAPI(190 title=get_settings().app_name,191 version=get_settings().app_version,192 description="Advanced sentiment analysis service for educational feedback"193)194 195app.add_middleware(196 CORSMiddleware,197 allow_origins=["*"],198 allow_credentials=True,199 allow_methods=["*"],200 allow_headers=["*"],201)202 203# Global model variables204sia = None205tokenizer = None206model = None207device = None208summarizer = None209 210# ============================================================================211# ENHANCED PATTERN DETECTION FOR BETTER ACCURACY212# ============================================================================213 214# Meta-comments (not actual feedback - should be NEUTRAL)215META_PATTERNS = re.compile(216 r'^(no\s+(negative\s+)?(more\s+)?(comments?|feedback|remarks?|issues?|problems?|complaints?)|'217 r'(everything|all)\s+(is\s+)?(good|fine|ok(ay)?|great|perfect|excellent)|'218 r'nothing(\s+to\s+(say|comment|mention|add))?|'219 r'(nil|none|na|n/a|nill)\.?|'220 r'^(all\s+)?(good|fine|ok(ay)?|great|nice)\.?|'221 r'no\s+remarks?|'222 r'everything\s+at\s+the\s+too\s+only)$',223 re.IGNORECASE224)225 226# Strong NEGATIVE indicators (should override model scores)227STRONG_NEGATIVE_PATTERN = re.compile(228 r'\b('229 # Direct criticism230 r'(very|extremely|quite|so|too)\s+(poor|bad|weak|terrible|awful|horrible)|'231 r'poor\s+(teaching|teacher|faculty|knowledge|communication|quality|explanation)|'232 r'bad\s+(teaching|teacher|faculty|quality|explanation)|'233 r'terrible|horrible|awful|pathetic|useless|waste\s+of\s+time|'234 235 # Teaching quality issues236 r'(teaching|knowledge)\s+(is\s+)?(poor|bad|weak|lacking|insufficient|not\s+good)|'237 r'cannot\s+teach|can\'?t\s+teach|doesn\'?t\s+know\s+how\s+to\s+teach|'238 r'not\s+teaching\s+properly|teaching\s+method\s+is\s+(poor|bad)|'239 240 # Boring/disengagement241 r'(boring|dull|monotonous)\s+(class|classes|subject|lecture|lectures|sessions?)|'242 r'(class|classes|subject|lectures?)\s+(is|are)\s+(boring|dull|monotonous|uninteresting)|'243 r'sleeping\s+in\s+class|fall\s+asleep|makes?\s+us\s+sleep|'244 245 # Communication issues246 r'(low|soft|quiet|unclear)\s+voice|voice\s+(is\s+)?(low|soft|quiet|not\s+clear)|'247 r'(cannot|can\'?t|cant|unable\s+to)\s+hear|difficult\s+to\s+hear|'248 r'(not|poor|bad)\s+(communication|explaining|explanation)|'249 250 # Understanding issues251 r'(cannot|can\'?t|cant|unable\s+to|difficult\s+to|hard\s+to)\s+understand|'252 r'(not|never|don\'?t)\s+(able\s+to\s+)?understand|'253 r'(concepts?|topics?|subjects?)\s+(are\s+)?(difficult|hard|tough|impossible)\s+to\s+understand|'254 r'makes?\s+(no|little)\s+sense|doesn\'?t\s+make\s+sense|'255 256 # Improvement needed257 r'(need|needs|require|requires)\s+(urgent|serious|immediate|much|lot\s+of)?\s*improvement|'258 r'(should|must|have\s+to)\s+improve\s+(a\s+lot|more|urgently)|'259 260 # Pace issues261 r'(lectures?|class(es)?|teaching)\s+(is|are|going)\s+(too|very)\s+(fast|slow)|'262 r'(too|very|extremely)\s+(fast|slow|rush|rushed)|'263 r'(lag|lagging)\s+in\s+teaching|teaching\s+(is\s+)?lagging|'264 265 # Time management266 r'(not|poor|bad|terrible)\s+(managing|managing)\s+time|'267 r'time\s+management\s+(is\s+)?(poor|bad|terrible|lacking)|'268 r'always\s+(late|wasting\s+time)|waste\s+(our|class)\s+time|'269 270 # Lack of resources/support271 r'(no|not|insufficient|lack\s+of)\s+(proper|sufficient|enough|regular)?\s*(classes|notes|support|help)|'272 r'need\s+more\s+(staff|faculty|classes|support|help)|'273 r'no\s+(practical|hands[-\s]?on|lab|real[-\s]?world)|lack\s+of\s+practical|'274 275 # Attendance/engagement issues276 r'(just|only)\s+(for|going\s+for)\s+attendance|'277 r'going\s+(to|for)\s+(her|his|their)\s+class\s+(just|only)\s+for\s+attendance|'278 r'(not|no)\s+(interested|engaging|helpful|useful|at\s+all)|'279 r'no\s+interest\s+in\s+teaching|'280 281 # Administrative issues282 r'military\s+rules|too\s+strict|very\s+strict|'283 r'attendance\s+(issue|problem)|not\s+providing\s+attendance|'284 285 # Workload issues286 r'too\s+many\s+projects|many\s+projects\s+review|'287 r'placement\s+activities\s+(and|with)\s+attendance'288 r')\b',289 re.IGNORECASE290)291 292# Positive indicators (help identify positive comments)293POSITIVE_PATTERN = re.compile(294 r'\b('295 r'(very|extremely|really|so|truly)\s+(good|great|excellent|amazing|wonderful|fantastic|helpful|knowledgeable|clear)|'296 r'excellent|outstanding|amazing|wonderful|fantastic|brilliant|superb|'297 r'(great|good|best|wonderful)\s+(teaching|teacher|faculty|knowledge|explanation|professor|sir|madam)|'298 r'(teaching|explanation|knowledge)\s+(is\s+)?(excellent|outstanding|very\s+good|great|clear)|'299 r'explains?\s+(very\s+)?(well|clearly|nicely|perfectly)|'300 r'(easy|easier)\s+to\s+understand|clear\s+explanation|'301 r'(very\s+)?(helpful|supportive|friendly|approachable|patient)|'302 r'(good|strong|deep|vast)\s+(knowledge|understanding)|'303 r'(love|like|enjoy|appreciate)\s+(the\s+)?(class|classes|teaching|subject|course|lectures?)|'304 r'learned?\s+(a\s+lot|so\s+much|many\s+things)|'305 r'inspired?|inspiring|motivating|motivated|encouraged|'306 r'(best|favourite|favorite)\s+(teacher|faculty|professor)|'307 r'highly\s+recommend|strongly\s+recommend|'308 r'grateful|thankful|blessed|lucky\s+to\s+have|'309 r'satisfied|happy\s+with|pleased\s+with|'310 r'(always|very)\s+(available|accessible|helpful)|'311 r'patient|caring|dedicated|passionate|'312 r'interactive\s+class|engaging\s+class|interesting\s+class'313 r')\b',314 re.IGNORECASE315)316 317# Weak negative indicators (suggestions/mild criticism - might be NEUTRAL)318WEAK_NEGATIVE_PATTERN = re.compile(319 r'\b('320 r'could\s+(be\s+)?better|'321 r'can\s+improve|'322 r'would\s+be\s+good\s+if|'323 r'suggest|suggestion|'324 r'maybe|perhaps|'325 r'slightly|a\s+bit|'326 r'sometimes|occasionally'327 r')\b',328 re.IGNORECASE329)330 331def is_meta_comment(text: str) -> bool:332 """Check if comment is meta (not actual feedback)"""333 if not text or len(text.strip()) < 3:334 return True335 336 text = text.strip()337 return bool(META_PATTERNS.match(text))338 339def detect_strong_negative(text: str) -> bool:340 """Detect strong negative patterns"""341 if not text or is_meta_comment(text):342 return False343 return bool(STRONG_NEGATIVE_PATTERN.search(text))344 345def detect_positive(text: str) -> bool:346 """Detect positive patterns"""347 if not text or is_meta_comment(text):348 return False349 return bool(POSITIVE_PATTERN.search(text))350 351def detect_weak_negative(text: str) -> bool:352 """Detect weak negative patterns (suggestions)"""353 if not text or is_meta_comment(text):354 return False355 return bool(WEAK_NEGATIVE_PATTERN.search(text))356 357# ============================================================================358# MODEL INITIALIZATION359# ============================================================================360 361def initialize_models():362 """Initialize sentiment analysis models"""363 global sia, tokenizer, model, device, summarizer364 365 try:366 settings = get_settings()367 logger.info("Initializing sentiment analysis models...")368 369 # VADER370 sia = SentimentIntensityAnalyzer()371 logger.info("✓ VADER initialized")372 373 # RoBERTa374 cache_dir = settings.model_cache_dir375 os.makedirs(cache_dir, exist_ok=True)376 377 tokenizer = AutoTokenizer.from_pretrained(378 settings.roberta_model_name,379 cache_dir=cache_dir380 )381 model = AutoModelForSequenceClassification.from_pretrained(382 settings.roberta_model_name,383 cache_dir=cache_dir384 )385 386 device = "cuda" if torch.cuda.is_available() else "cpu"387 model.to(device)388 model.eval()389 logger.info(f"✓ RoBERTa initialized on device: {device}")390 391 # Summarizer (optional)392 if settings.use_abstractive_summary:393 try:394 summarizer = pipeline(395 "summarization",396 model=settings.summarizer_model,397 device=0 if device == "cuda" else -1398 )399 logger.info("✓ Summarizer initialized")400 except Exception as e:401 logger.warning(f"Summarizer initialization failed: {e}")402 summarizer = None403 404 logger.info("✓ All models initialized successfully")405 406 except Exception as e:407 logger.error(f"Error initializing models: {e}")408 raise e409 410# ============================================================================411# SENTIMENT ANALYSIS FUNCTIONS412# ============================================================================413 414@lru_cache(maxsize=500)415def vader_sentiment_cached(text: str) -> tuple:416 """Cached VADER sentiment analysis"""417 scores = sia.polarity_scores(text)418 return (scores['neg'], scores['neu'], scores['pos'], scores['compound'])419 420def vader_sentiment(text: str) -> Dict[str, float]:421 """VADER sentiment analysis"""422 try:423 settings = get_settings()424 if settings.enable_caching:425 neg, neu, pos, compound = vader_sentiment_cached(text)426 return {427 'vader_neg': neg,428 'vader_neu': neu,429 'vader_pos': pos,430 'vader_compound': compound431 }432 else:433 scores = sia.polarity_scores(text)434 return {435 'vader_neg': scores['neg'],436 'vader_neu': scores['neu'],437 'vader_pos': scores['pos'],438 'vader_compound': scores['compound']439 }440 except Exception as e:441 logger.warning(f"VADER analysis failed: {e}")442 return {'vader_neg': 0.0, 'vader_neu': 1.0, 'vader_pos': 0.0, 'vader_compound': 0.0}443 444def roberta_sentiment_batch(texts: List[str]) -> List[Dict[str, float]]:445 """Batch RoBERTa sentiment analysis"""446 try:447 settings = get_settings()448 results = []449 450 for i in range(0, len(texts), settings.batch_size):451 batch = texts[i:i + settings.batch_size]452 453 encoded = tokenizer(454 batch,455 return_tensors='pt',456 truncation=True,457 max_length=512,458 padding=True459 )460 encoded = {k: v.to(device) for k, v in encoded.items()}461 462 with torch.no_grad():463 outputs = model(**encoded)464 465 for output in outputs.logits:466 scores = softmax(output.cpu().numpy())467 results.append({468 'roberta_neg': float(scores[0]),469 'roberta_neu': float(scores[1]),470 'roberta_pos': float(scores[2])471 })472 473 return results474 475 except Exception as e:476 logger.warning(f"RoBERTa batch analysis failed: {e}")477 return [{'roberta_neg': 0.0, 'roberta_neu': 1.0, 'roberta_pos': 0.0} for _ in texts]478 479def classify_sentiment_enhanced(row: pd.Series, settings: Settings) -> str:480 """481 Enhanced multi-stage sentiment classification for better accuracy482 483 Stage 1: Meta-comments → Neutral484 Stage 2: Strong negative patterns → Negative (override models)485 Stage 3: Strong positive patterns + high scores → Positive486 Stage 4: Model ensemble decision487 Stage 5: Default to neutral if uncertain488 """489 490 # Stage 1: Meta-comments are always neutral491 if row.get('is_meta', False):492 return 'Neutral'493 494 # Get all scores495 vader_compound = row.get('vader_compound', 0.0)496 vader_pos = row.get('vader_pos', 0.0)497 vader_neg = row.get('vader_neg', 0.0)498 499 roberta_pos = row.get('roberta_pos', 0.0)500 roberta_neg = row.get('roberta_neg', 0.0)501 roberta_neu = row.get('roberta_neu', 0.0)502 503 combined_pos = row.get('combined_pos', 0.0)504 combined_neg = row.get('combined_neg', 0.0)505 combined_neu = row.get('combined_neu', 0.0)506 507 has_strong_negative = row.get('has_strong_negative', False)508 has_positive = row.get('has_positive', False)509 has_weak_negative = row.get('has_weak_negative', False)510 511 # Stage 2: Strong negative patterns override everything512 if has_strong_negative:513 return 'Negative'514 515 # Stage 3: Strong positive signals516 if has_positive and (517 vader_compound >= settings.vader_strong_pos_threshold or518 roberta_pos >= settings.roberta_strong_pos_threshold or519 (vader_compound >= settings.vader_pos_threshold and roberta_pos >= settings.roberta_pos_threshold)520 ):521 return 'Positive'522 523 # Stage 4: Model-based classification with ensemble524 525 # Strong negative from models526 if (527 vader_compound <= settings.vader_strong_neg_threshold or528 roberta_neg >= settings.roberta_strong_neg_threshold or529 (vader_compound <= settings.vader_neg_threshold and roberta_neg >= settings.roberta_neg_threshold)530 ):531 return 'Negative'532 533 # Moderate negative534 if (535 combined_neg > combined_pos and536 combined_neg > combined_neu and537 combined_neg > 0.35 # Threshold for clarity538 ):539 return 'Negative'540 541 # Clear positive542 if (543 combined_pos > combined_neg and544 combined_pos > combined_neu and545 combined_pos > 0.35 # Threshold for clarity546 ):547 return 'Positive'548 549 # Weak negative with suggestion context → might be neutral550 if has_weak_negative and not has_strong_negative:551 # If scores are not strongly negative, treat as neutral552 if combined_neg < 0.5:553 return 'Neutral'554 555 # Stage 5: Default to neutral if uncertain556 return 'Neutral'557 558def sanitize_text(text: str) -> str:559 """Sanitize input text"""560 if not text:561 return ""562 text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', text)563 text = ' '.join(text.split())564 return text.strip()565 566# ============================================================================567# MAIN ANALYSIS FUNCTION568# ============================================================================569 570def analyze_comments_sentiment(comments: List[str]) -> Dict[str, Any]:571 """Main sentiment analysis with enhanced accuracy"""572 try:573 settings = get_settings()574 logger.info(f"Received {len(comments)} comments for analysis")575 576 # Sanitize577 sanitized_comments = [sanitize_text(comment) for comment in comments]578 579 # Filter valid comments580 filtered_comments = [581 comment for comment in sanitized_comments582 if settings.min_comment_words <= len(comment.split()) <= settings.max_comment_length583 ]584 585 logger.info(f"After filtering: {len(filtered_comments)} valid comments")586 587 if not filtered_comments:588 return {589 "total_comments": 0,590 "message": "No valid comments found for analysis"591 }592 593 # Create DataFrame594 df = pd.DataFrame({'comment': filtered_comments})595 596 # Pattern detection597 df['is_meta'] = df['comment'].apply(is_meta_comment)598 df['has_strong_negative'] = df['comment'].apply(detect_strong_negative)599 df['has_positive'] = df['comment'].apply(detect_positive)600 df['has_weak_negative'] = df['comment'].apply(detect_weak_negative)601 602 # Log detection stats603 logger.info(f"Meta: {df['is_meta'].sum()}, "604 f"Strong Neg: {df['has_strong_negative'].sum()}, "605 f"Positive: {df['has_positive'].sum()}, "606 f"Weak Neg: {df['has_weak_negative'].sum()}")607 608 # VADER analysis609 vader_results = [vader_sentiment(text) for text in df['comment']]610 vader_df = pd.DataFrame(vader_results)611 612 # RoBERTa analysis613 roberta_results = roberta_sentiment_batch(df['comment'].tolist())614 roberta_df = pd.DataFrame(roberta_results)615 616 # Combine617 final_df = pd.concat([df.reset_index(drop=True), vader_df, roberta_df], axis=1)618 619 # Calculate combined scores620 final_df['combined_pos'] = (621 settings.combined_weight_vader * final_df['vader_pos'] +622 settings.combined_weight_roberta * final_df['roberta_pos']623 )624 final_df['combined_neg'] = (625 settings.combined_weight_vader * final_df['vader_neg'] +626 settings.combined_weight_roberta * final_df['roberta_neg']627 )628 final_df['combined_neu'] = (629 settings.combined_weight_vader * final_df['vader_neu'] +630 settings.combined_weight_roberta * final_df['roberta_neu']631 )632 633 # Enhanced classification634 final_df['Overall_Sentiment'] = final_df.apply(635 lambda row: classify_sentiment_enhanced(row, settings),636 axis=1637 )638 639 # Statistics640 total_comments = len(final_df)641 positive_count = len(final_df[final_df['Overall_Sentiment'] == 'Positive'])642 negative_count = len(final_df[final_df['Overall_Sentiment'] == 'Negative'])643 neutral_count = len(final_df[final_df['Overall_Sentiment'] == 'Neutral'])644 645 logger.info(f"Classification Results - Pos: {positive_count}, Neg: {negative_count}, Neu: {neutral_count}")646 647 # Average scores648 avg_positive = float(final_df['combined_pos'].mean())649 avg_negative = float(final_df['combined_neg'].mean())650 avg_neutral = float(final_df['combined_neu'].mean())651 652 # Overall sentiment653 if avg_positive > max(avg_negative, avg_neutral):654 overall_sentiment_label = "Positive"655 elif avg_negative > max(avg_positive, avg_neutral):656 overall_sentiment_label = "Negative"657 else:658 overall_sentiment_label = "Neutral"659 660 # Process negative comments661 negative_summary = ""662 negative_comments_list = []663 negative_comments = final_df[final_df['Overall_Sentiment'] == 'Negative']664 665 if len(negative_comments) > 0:666 negative_comments_list = negative_comments['comment'].tolist()667 668 try:669 top_idx = negative_comments['combined_neg'].nlargest(min(3, len(negative_comments))).index670 top_comments = negative_comments.loc[top_idx, 'comment'].tolist()671 672 if settings.use_abstractive_summary and summarizer is not None:673 negative_text = " ".join(top_comments)674 if len(negative_text) > 1000:675 negative_text = negative_text[:1000]676 677 summary_result = summarizer(678 negative_text,679 max_length=settings.max_summary_length,680 min_length=settings.min_summary_length,681 do_sample=False682 )683 negative_summary = summary_result[0]['summary_text']684 else:685 negative_summary = "; ".join(top_comments)686 except Exception as e:687 logger.warning(f"Summary generation failed: {e}")688 negative_summary = "; ".join(negative_comments_list[:3])689 690 # Insights and recommendations691 insights = []692 recommendations = []693 694 if overall_sentiment_label == "Positive":695 insights.extend([696 f"Strong positive feedback: {positive_count}/{total_comments} comments ({round(positive_count/total_comments*100, 1)}%)",697 "Students are satisfied with the teaching approach",698 "High engagement and learning outcomes reported"699 ])700 recommendations.extend([701 "Continue current effective teaching methods",702 "Document successful practices for future reference",703 "Share best practices with colleagues"704 ])705 elif overall_sentiment_label == "Negative":706 insights.extend([707 f"Concerns identified: {negative_count}/{total_comments} negative comments ({round(negative_count/total_comments*100, 1)}%)",708 "Students facing challenges with current approach",709 "Immediate attention needed to address feedback"710 ])711 recommendations.extend([712 "Review and analyze specific negative feedback points",713 "Consider adjusting teaching pace or methods",714 "Increase student engagement and support",715 "Schedule student feedback sessions",716 "Focus on communication clarity and accessibility"717 ])718 else:719 insights.extend([720 f"Mixed feedback: {positive_count} positive, {negative_count} negative, {neutral_count} neutral",721 "Room for improvement while maintaining strengths",722 "Students have varied experiences"723 ])724 recommendations.extend([725 "Address specific concerns raised in negative feedback",726 "Build on positive aspects appreciated by students",727 "Gather more detailed feedback for neutral areas"728 ])729 730 # Add pattern-based insights731 if df['has_strong_negative'].sum() > 0:732 insights.append(f"{df['has_strong_negative'].sum()} comments contain explicit criticism requiring attention")733 if df['has_positive'].sum() > 0:734 insights.append(f"{df['has_positive'].sum()} comments contain strong positive appreciation")735 736 return {737 "total_comments": total_comments,738 "positive_comments": positive_count,739 "negative_comments": negative_count,740 "neutral_comments": neutral_count,741 "positive_sentiment": round(avg_positive, 3),742 "negative_sentiment": round(avg_negative, 3),743 "neutral_sentiment": round(avg_neutral, 3),744 "overall_sentiment": overall_sentiment_label,745 "sentiment_distribution": {746 "positive_percentage": round((positive_count / total_comments) * 100, 1),747 "negative_percentage": round((negative_count / total_comments) * 100, 1),748 "neutral_percentage": round((neutral_count / total_comments) * 100, 1)749 },750 "negative_comments_summary": negative_summary,751 "negative_comments_list": negative_comments_list,752 "key_insights": insights,753 "recommendations": recommendations,754 "detailed_analysis": {755 "vader_scores": {756 "average_positive": round(final_df['vader_pos'].mean(), 3),757 "average_negative": round(final_df['vader_neg'].mean(), 3),758 "average_neutral": round(final_df['vader_neu'].mean(), 3),759 "average_compound": round(final_df['vader_compound'].mean(), 3)760 },761 "roberta_scores": {762 "average_positive": round(final_df['roberta_pos'].mean(), 3),763 "average_negative": round(final_df['roberta_neg'].mean(), 3),764 "average_neutral": round(final_df['roberta_neu'].mean(), 3)765 }766 },767 "analysis_timestamp": datetime.utcnow().isoformat()768 }769 770 except Exception as e:771 logger.error(f"Sentiment analysis failed: {e}", exc_info=True)772 raise e773 774# ============================================================================775# API ENDPOINTS776# ============================================================================777 778@app.on_event("startup")779async def startup_event():780 """Initialize models on startup"""781 try:782 logger.info("=" * 80)783 logger.info(f"Application Startup at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")784 logger.info("=" * 80)785 initialize_models()786 logger.info("✓ Service started successfully")787 logger.info("=" * 80)788 except Exception as e:789 logger.error(f"✗ Startup failed: {e}")790 raise e791 792@app.on_event("shutdown")793async def shutdown_event():794 """Cleanup on shutdown"""795 logger.info("Service shutting down")796 797@app.get("/")798async def root():799 """Root endpoint"""800 return {801 "service": get_settings().app_name,802 "version": get_settings().app_version,803 "status": "running",804 "endpoints": {805 "health": "/health",806 "analyze": "/analyze-comments",807 "test": "/test"808 }809 }810 811@app.get("/health")812async def health_check():813 """Health check endpoint"""814 models_loaded = sia is not None and model is not None and tokenizer is not None815 816 return {817 "status": "healthy" if models_loaded else "unhealthy",818 "service": "comment-analysis",819 "version": get_settings().app_version,820 "models_loaded": models_loaded,821 "device": device if device else "not initialized",822 "timestamp": datetime.utcnow().isoformat()823 }824 825@app.post("/analyze-comments", response_model=CommentAnalysisResponse)826async def analyze_comments(827 request: CommentAnalysisRequest,828 settings: Settings = Depends(get_settings)829):830 """Analyze comments for sentiment using enhanced multi-stage classification"""831 try:832 comments = request.comments833 faculty_info = request.faculty_info834 835 if not comments:836 return CommentAnalysisResponse(837 success=False,838 analysis=None,839 message="No comments provided for analysis"840 )841 842 logger.info(f"Analyzing {len(comments)} comments for {faculty_info.faculty_name} ({faculty_info.course_code})")843 844 analysis_result = analyze_comments_sentiment(comments)845 846 if analysis_result.get("total_comments", 0) == 0:847 return CommentAnalysisResponse(848 success=False,849 analysis=None,850 message=analysis_result.get("message", "No valid comments to analyze")851 )852 853 analysis_result["faculty_info"] = {854 "faculty_name": faculty_info.faculty_name,855 "staff_id": faculty_info.staff_id,856 "course_code": faculty_info.course_code,857 "course_name": faculty_info.course_name858 }859 860 return CommentAnalysisResponse(861 success=True,862 analysis=analysis_result,863 message=f"Successfully analyzed {analysis_result['total_comments']} comments"864 )865 866 except ValueError as ve:867 logger.warning(f"Validation error: {ve}")868 raise HTTPException(status_code=400, detail=str(ve))869 except Exception as e:870 logger.error(f"Analysis failed: {e}", exc_info=True)871 raise HTTPException(status_code=500, detail="Analysis failed. Please try again later.")872 873@app.get("/test")874async def test_endpoint():875 """Test endpoint with various comment types"""876 test_cases = [877 # Meta-comments (should be Neutral)878 "No negative comments",879 "Everything is good",880 "Nothing to say",881 "Nil",882 883 # Strong Negative (should be Negative)884 "Very poor teaching quality",885 "Boring class, waste of time",886 "Cannot understand anything",887 "Teaching is terrible and voice is too low",888 "Poor knowledge and bad teaching method",889 890 # Positive (should be Positive)891 "Excellent teacher with great knowledge",892 "Very helpful and explains clearly",893 "Amazing teaching style, learned a lot",894 "Best professor, highly recommend",895 896 # Weak negative/Neutral897 "Could be better",898 "Sometimes hard to understand",899 "Overall good but too lag",900 901 # Mixed902 "Good teacher but classes are boring",903 "Knowledgeable but voice is low"904 ]905 906 results = []907 for text in test_cases:908 is_meta = is_meta_comment(text)909 has_strong_neg = detect_strong_negative(text)910 has_pos = detect_positive(text)911 has_weak_neg = detect_weak_negative(text)912 913 # Predict914 if is_meta:915 predicted = "Neutral (meta-comment)"916 elif has_strong_neg:917 predicted = "Negative (strong pattern)"918 elif has_pos and not has_strong_neg:919 predicted = "Positive (likely)"920 elif has_weak_neg and not has_strong_neg:921 predicted = "Neutral/Negative (weak)"922 else:923 predicted = "Requires full analysis"924 925 results.append({926 "text": text,927 "is_meta": is_meta,928 "strong_negative": has_strong_neg,929 "positive": has_pos,930 "weak_negative": has_weak_neg,931 "predicted": predicted932 })933 934 return {935 "test_results": results,936 "note": "Predictions based on pattern matching. Full analysis uses VADER + RoBERTa ensemble."937 }938 939if __name__ == "__main__":940 uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")941 942 943 944 945 946 947 948 949 950 951 952# """953# Enhanced FastAPI Service for Comment Sentiment Analysis954# with improved performance, validation, and configuration management955# Version 2.1.0 - Updated with bug fixes and improvements956# """957 958# from fastapi import FastAPI, HTTPException, Depends959# from fastapi.middleware.cors import CORSMiddleware960# from pydantic import BaseModel, Field, validator961# from pydantic_settings import BaseSettings962# from typing import List, Dict, Any, Optional963# from functools import lru_cache964# import uvicorn965# import pandas as pd966# import numpy as np967# import os968# import re969# from datetime import datetime970# import logging971 972# # Configure logging FIRST973# logging.basicConfig(974# level=logging.INFO,975# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'976# )977# logger = logging.getLogger(__name__)978 979# # CRITICAL: Download NLTK data BEFORE importing NLTK components980# import nltk981# import ssl982 983# try:984# _create_unverified_https_context = ssl._create_unverified_context985# except AttributeError:986# pass987# else:988# ssl._create_default_https_context = _create_unverified_https_context989 990# # Set NLTK data path991# nltk_data_dir = '/tmp/nltk_data'992# os.makedirs(nltk_data_dir, exist_ok=True)993# nltk.data.path.insert(0, nltk_data_dir)994 995# # Download required NLTK data996# def ensure_nltk_data():997# """Ensure all required NLTK data is downloaded"""998# resources = ['vader_lexicon', 'punkt', 'stopwords', 'wordnet', 'omw-1.4']999 1000# for resource in resources:1001# try:1002# # Try to find the resource1003# if resource == 'vader_lexicon':1004# nltk.data.find('sentiment/vader_lexicon.zip')1005# elif resource == 'punkt':1006# nltk.data.find('tokenizers/punkt')1007# elif resource in ['stopwords', 'wordnet', 'omw-1.4']:1008# nltk.data.find(f'corpora/{resource}')1009# logger.info(f"✓ NLTK resource '{resource}' already available")1010# except LookupError:1011# logger.info(f"Downloading NLTK resource '{resource}'...")1012# try:1013# nltk.download(resource, download_dir=nltk_data_dir, quiet=False)1014# logger.info(f"✓ Successfully downloaded '{resource}'")1015# except Exception as e:1016# logger.error(f"✗ Failed to download '{resource}': {e}")1017 1018# # Download NLTK data immediately1019# logger.info("Ensuring NLTK data is available...")1020# ensure_nltk_data()1021 1022# # NOW import NLTK components1023# from nltk.sentiment import SentimentIntensityAnalyzer1024 1025# # Import transformers after NLTK setup1026# from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline1027# from scipy.special import softmax1028# import torch1029 1030# # Configuration Management1031# class Settings(BaseSettings):1032# """Application settings with environment variable support"""1033# # API Settings1034# app_name: str = "Comment Analysis API"1035# app_version: str = "2.1.0"1036# debug_mode: bool = False1037 1038# # Request Limits1039# max_comments_per_request: int = 10001040# max_comment_length: int = 50001041# min_comment_words: int = 11042 1043# # Sentiment Thresholds1044# vader_pos_threshold: float = 0.21045# vader_neg_threshold: float = -0.21046# roberta_pos_threshold: float = 0.551047# roberta_neg_threshold: float = 0.451048# combined_weight_vader: float = 0.51049# combined_weight_roberta: float = 0.51050 1051# # Model Settings1052# model_cache_dir: str = "/tmp/model_cache"1053# roberta_model_name: str = "cardiffnlp/twitter-roberta-base-sentiment"1054# use_abstractive_summary: bool = False1055# summarizer_model: str = "facebook/bart-large-cnn"1056# max_summary_length: int = 1001057# min_summary_length: int = 251058 1059# # Performance1060# enable_caching: bool = True1061# cache_size: int = 5001062# batch_size: int = 321063 1064# class Config:1065# env_file = ".env"1066# env_file_encoding = 'utf-8'1067# extra = 'ignore'1068 1069# @validator('min_comment_words')1070# def validate_min_words(cls, v):1071# if v < 0:1072# raise ValueError('min_comment_words must be non-negative')1073# return v1074 1075# @validator('combined_weight_vader', 'combined_weight_roberta')1076# def validate_weights(cls, v):1077# if not 0 <= v <= 1:1078# raise ValueError('Weights must be between 0 and 1')1079# return v1080 1081# @lru_cache()1082# def get_settings() -> Settings:1083# """Cached settings instance"""1084# settings = Settings()1085# # Normalize weights if needed1086# total = settings.combined_weight_vader + settings.combined_weight_roberta1087# if not (0.99 <= total <= 1.01):1088# logger.warning(f"Weights sum to {total}, normalizing to 1.0")1089# settings.combined_weight_vader /= total1090# settings.combined_weight_roberta /= total1091# return settings1092 1093# # Pydantic Models1094# class FacultyInfo(BaseModel):1095# faculty_name: str = Field(..., min_length=1, max_length=200)1096# staff_id: str = Field(..., min_length=1, max_length=50)1097# course_code: str = Field(..., min_length=1, max_length=50)1098# course_name: str = Field(..., min_length=1, max_length=200)1099 1100# class CommentAnalysisRequest(BaseModel):1101# comments: List[str] = Field(..., min_items=1)1102# faculty_info: FacultyInfo1103 1104# @validator('comments')1105# def validate_comments(cls, v):1106# settings = get_settings()1107 1108# if len(v) > settings.max_comments_per_request:1109# raise ValueError(1110# f'Maximum {settings.max_comments_per_request} comments per request'1111# )1112 1113# for idx, comment in enumerate(v):1114# if len(comment) > settings.max_comment_length:1115# raise ValueError(1116# f'Comment {idx} exceeds maximum length of {settings.max_comment_length} characters'1117# )1118 1119# return v1120 1121# class SentimentDistribution(BaseModel):1122# positive_percentage: float1123# negative_percentage: float1124# neutral_percentage: float1125 1126# class DetailedScores(BaseModel):1127# average_positive: float1128# average_negative: float1129# average_neutral: float1130# average_compound: Optional[float] = None1131 1132# class DetailedAnalysis(BaseModel):1133# vader_scores: DetailedScores1134# roberta_scores: DetailedScores1135 1136# class AnalysisResult(BaseModel):1137# total_comments: int1138# positive_comments: int1139# negative_comments: int1140# neutral_comments: int1141# positive_sentiment: float1142# negative_sentiment: float1143# neutral_sentiment: float1144# overall_sentiment: str1145# sentiment_distribution: SentimentDistribution1146# negative_comments_summary: str1147# negative_comments_list: List[str]1148# key_insights: List[str]1149# recommendations: List[str]1150# detailed_analysis: DetailedAnalysis1151# faculty_info: Dict[str, str]1152# analysis_timestamp: str1153 1154# class CommentAnalysisResponse(BaseModel):1155# success: bool1156# analysis: Optional[AnalysisResult] = None1157# message: str1158 1159# # Initialize FastAPI app1160# app = FastAPI(1161# title=get_settings().app_name,1162# version=get_settings().app_version,1163# description="Advanced sentiment analysis service for educational feedback"1164# )1165 1166# # Add CORS middleware1167# app.add_middleware(1168# CORSMiddleware,1169# allow_origins=["*"],1170# allow_credentials=True,1171# allow_methods=["*"],1172# allow_headers=["*"],1173# )1174 1175# # Global variables for models1176# sia = None1177# tokenizer = None1178# model = None1179# device = None1180# summarizer = None1181 1182# # Enhanced heuristic phrase/regex rules for explicit negative feedback1183# NEGATIVE_PHRASES = [1184# # Teaching quality issues1185# 'very poor',1186# 'extremely poor',1187# 'poor in teaching',1188# 'poor teaching level',1189# 'poor teaching',1190# 'bad teacher',1191# 'bad teaching',1192# 'not good', # Keep but check it's not "no negative"1193# 'not satisfied',1194# 'not satisfactory',1195 1196# # Content/delivery issues1197# 'boring class',1198# 'boring classes',1199# 'boring subject',1200# 'subject is boring',