CoolFace
Apppublic

kushvanth/iqac_fast_api

sourceHugging Faceupdated 10mo agoView on Hugging Face
1likes
fastapi_example.py2008 linesDownload Raw Back to root
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',

Showing the first 1,200 of 2008 lines. Download the file for the rest.