CoolFace
Apppublic

MMo4/csit-ned-chatbot

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
gemini_client.py408 linesDownload Raw Back to rag
1"""2Gemini Client for CSIT RAG System3Updated for Google GenAI SDK (November 2025)4Model: gemini-2.5-flash5SDK: google.genai (new API)6"""7 8from google import genai9from typing import List, Dict, Any, Optional10import logging11import os12import time13from dataclasses import dataclass14 15logger = logging.getLogger(__name__)16 17@dataclass18class QueryAnalysis:19    """Structure for query analysis results"""20    intent: str  # academic_info, programs, faculty_info, events, admissions, research, etc.21    departments: List[str]  # CSIT, SE, CIS22    keywords: List[str]23    question_type: str  # factual, comparative, list, procedural24    confidence: float25 26@dataclass27class ResponseGeneration:28    """Structure for generated response"""29    content: str30    confidence: float31    sources_used: List[str]32    follow_up_questions: List[str]33 34class GeminiClient:35    """36    Client for Google Gemini API with key rotation support.37    Uses new google.genai SDK (2025) with gemini-2.5-flash model.38    """39 40    def __init__(self, api_keys: Optional[List[str]] = None):41        """42        Initialize Gemini client with API key rotation.43 44        Args:45            api_keys: Optional list of API keys. If not provided, loads from environment.46        """47        # Load API keys from environment if not provided48        if api_keys:49            self.api_keys = api_keys50        else:51            self.api_keys = self._load_api_keys_from_env()52 53        if not self.api_keys:54            raise ValueError("No valid Gemini API keys found. Set GEMINI_API_KEY_1, GEMINI_API_KEY_2, etc.")55 56        self.current_key_index = 057        self.model_name = "gemini-2.5-flash"58        self.client = None59 60        logger.info(f"Loaded {len(self.api_keys)} API keys for rotation")61        self._initialize_client()62 63    def _load_api_keys_from_env(self) -> List[str]:64        """Load API keys from environment variables (GEMINI_API_KEY_1, etc.)"""65        keys = []66        for i in range(1, 20):  # Support up to 20 keys67            key = os.getenv(f'GEMINI_API_KEY_{i}')68            if key:69                keys.append(key)70 71        # Fallback to single key72        if not keys:73            single_key = os.getenv('GEMINI_API_KEY')74            if single_key:75                keys.append(single_key)76 77        return keys78 79    def _initialize_client(self):80        """Initialize Gemini client with current API key"""81        try:82            current_key = self.api_keys[self.current_key_index]83 84            # Set environment variable for the SDK85            os.environ['GEMINI_API_KEY'] = current_key86 87            # Initialize new SDK client88            self.client = genai.Client()89 90            logger.info(f"Gemini client initialized with key index {self.current_key_index} (model: {self.model_name})")91        except Exception as e:92            logger.error(f"Failed to initialize Gemini client: {e}")93            raise94 95    def _rotate_api_key(self):96        """Rotate to next API key"""97        if len(self.api_keys) > 1:98            self.current_key_index = (self.current_key_index + 1) % len(self.api_keys)99            self._initialize_client()100            logger.info(f"Rotated to API key index {self.current_key_index}")101        else:102            logger.warning("Only one API key available, cannot rotate")103 104    def _make_request(self, prompt: str, max_retries: int = 3) -> str:105        """106        Make request to Gemini API with retry logic and key rotation.107 108        Args:109            prompt: The prompt to send110            max_retries: Maximum number of retry attempts111 112        Returns:113            Generated text response114 115        Raises:116            RuntimeError: If all retries fail117        """118        for attempt in range(max_retries):119            try:120                # Use new SDK method121                response = self.client.models.generate_content(122                    model=self.model_name,123                    contents=prompt124                )125 126                return response.text127 128            except Exception as e:129                logger.warning(f"Request attempt {attempt + 1} failed: {e}")130 131                if attempt < max_retries - 1:132                    # Rotate key and retry133                    self._rotate_api_key()134                    time.sleep(2 ** attempt)  # Exponential backoff135                else:136                    logger.error(f"All {max_retries} request attempts failed")137                    raise138 139        raise RuntimeError("Failed to make request after all retries")140 141    def analyze_query(self, query: str) -> QueryAnalysis:142        """143        Analyze user query to extract intent, departments, and metadata.144 145        Updated for CSIT department chatbot with SE/CIS academic support.146 147        Returns:148            QueryAnalysis with:149            - intent: academic_info, programs, faculty_info, events, etc.150            - departments: List of detected departments (CSIT, SE, CIS)151            - keywords: Extracted key terms152            - question_type: factual, comparative, list, procedural153            - confidence: Analysis confidence score154        """155        analysis_prompt = f"""Analyze this query about NED University departments:156 157Query: "{query}"158 159Identify:1601. Intent - Choose ONE from:161   - academic_info (courses/curriculum/syllabus)162   - programs (degree details/specializations)163   - faculty_info (professors/teachers)164   - events (hackathons/competitions)165   - admissions (requirements/application)166   - research (projects/publications/thesis)167   - facilities (labs/equipment)168   - policies (exam rules/grading/OBE)169   - general (department overview/contact)170 1712. Departments mentioned - Can be multiple from:172   - CSIT (Computer Science & IT)173   - SE (Software Engineering)174   - CIS (Computer Information Systems)175 1763. Key terms/topics from the query177 1784. Question type:179   - factual (specific facts)180   - comparative (comparing options)181   - list (requesting multiple items)182   - procedural (how-to/process)183 184Respond in this EXACT format (one line each):185INTENT: <intent>186DEPARTMENTS: <comma-separated list or "CSIT" as default>187KEYWORDS: <comma-separated keywords>188QUESTION_TYPE: <type>189"""190 191        try:192            response = self._make_request(analysis_prompt)193            return self._parse_query_analysis(response, query)194 195        except Exception as e:196            logger.error(f"Query analysis failed: {e}")197            # Return default analysis198            return QueryAnalysis(199                intent='general',200                departments=['CSIT'],201                keywords=query.split()[:5],202                question_type='factual',203                confidence=0.5204            )205 206    def _parse_query_analysis(self, response: str, original_query: str) -> QueryAnalysis:207        """Parse LLM response into QueryAnalysis structure."""208        lines = response.strip().split('\n')209        analysis = {210            'intent': 'general',211            'departments': [],212            'keywords': [],213            'question_type': 'factual'214        }215 216        for line in lines:217            if ':' not in line:218                continue219 220            key, value = line.split(':', 1)221            key = key.strip().upper()222            value = value.strip()223 224            if key == 'INTENT':225                analysis['intent'] = value.lower()226            elif key == 'DEPARTMENTS':227                if value.lower() != 'none':228                    analysis['departments'] = [d.strip() for d in value.split(',')]229            elif key == 'KEYWORDS':230                analysis['keywords'] = [k.strip() for k in value.split(',')]231            elif key == 'QUESTION_TYPE':232                analysis['question_type'] = value.lower()233 234        # Default to CSIT if no department detected235        if not analysis['departments']:236            analysis['departments'] = ['CSIT']237 238        return QueryAnalysis(239            intent=analysis['intent'],240            departments=analysis['departments'],241            keywords=analysis['keywords'],242            question_type=analysis['question_type'],243            confidence=0.85  # High confidence in analysis244        )245 246    def generate_response(247        self,248        query: str,249        context_documents: List[Dict[str, Any]],250        query_analysis: QueryAnalysis,251        system_prompt: str252    ) -> ResponseGeneration:253        """254        Generate response using retrieved context and query analysis.255 256        Args:257            query: User's question258            context_documents: Retrieved documents with content and metadata259            query_analysis: Analyzed query metadata260            system_prompt: System-level instructions for response generation261 262        Returns:263            ResponseGeneration with formatted response264        """265        # Format context from documents266        context_text = self._format_context_documents(context_documents)267 268        # Build response prompt269        response_prompt = f"""{system_prompt}270 271QUERY: {query}272 273CONTEXT FROM VERIFIED SOURCES:274{context_text}275 276QUERY ANALYSIS:277- Intent: {query_analysis.intent}278- Departments: {', '.join(query_analysis.departments)}279- Question Type: {query_analysis.question_type}280 281INSTRUCTIONS:2821. Use ONLY information from the provided context2832. Cite sources: "According to the CSIT department website..." or "Based on the [year] prospectus..."2843. Be friendly but professional and factual2854. If information is incomplete, acknowledge it gracefully and provide contact info2865. Keep length appropriate: 150-300 words for most queries, longer for course listings2876. For historical events, mention the year clearly288 289Generate response:"""290 291        try:292            response_content = self._make_request(response_prompt)293 294            # Generate follow-up questions295            follow_up_questions = self._generate_followup_questions(query, response_content, query_analysis)296 297            # Extract sources used298            sources_used = [299                doc['metadata'].get('chunk_id', 'unknown')300                for doc in context_documents301            ]302 303            return ResponseGeneration(304                content=response_content.strip(),305                confidence=0.85,306                sources_used=sources_used,307                follow_up_questions=follow_up_questions308            )309 310        except Exception as e:311            logger.error(f"Response generation failed: {e}")312            return ResponseGeneration(313                content="I apologize, but I encountered an error generating a response. Please try again or contact the CSIT department office.",314                confidence=0.0,315                sources_used=[],316                follow_up_questions=[]317            )318 319    def _format_context_documents(self, documents: List[Dict[str, Any]]) -> str:320        """Format retrieved documents into context text for the LLM."""321        if not documents:322            return "No relevant context found."323 324        context_parts = []325 326        for i, doc in enumerate(documents, 1):327            metadata = doc.get('metadata', {})328            content = doc.get('document', '')329 330            # Format source information331            title = metadata.get('title', 'Unknown Document')332            category = metadata.get('category', 'N/A')333            departments = metadata.get('departments', [])334 335            context_part = f"""336--- Source {i}: {title} ---337Category: {category}338Departments: {', '.join(departments) if departments else 'N/A'}339 340Content:341{content[:1000]}...342"""343            context_parts.append(context_part.strip())344 345        return '\n\n'.join(context_parts)346 347    def _generate_followup_questions(348        self,349        query: str,350        response: str,351        analysis: QueryAnalysis352    ) -> List[str]:353        """Generate relevant follow-up questions."""354        followup_prompt = f"""Based on this conversation about NED University:355 356User Query: "{query}"357Intent: {analysis.intent}358Departments: {', '.join(analysis.departments)}359 360Response: "{response[:300]}..."361 362Suggest 3 relevant follow-up questions the user might want to ask.363Consider the intent and departments mentioned.364 365Format as:3661. Question one?3672. Question two?3683. Question three?369"""370 371        try:372            followup_response = self._make_request(followup_prompt)373            return self._parse_followup_questions(followup_response)374        except Exception as e:375            logger.warning(f"Follow-up generation failed: {e}")376            return []377 378    def _parse_followup_questions(self, response: str) -> List[str]:379        """Parse follow-up questions from LLM response."""380        lines = response.strip().split('\n')381        questions = []382 383        for line in lines:384            line = line.strip()385            # Look for numbered questions386            if line and (line[0].isdigit() or line.startswith('-')):387                # Remove numbering388                question = line.split('.', 1)[-1].strip()389                question = question.split(')', 1)[-1].strip()  # Handle "1)" format390 391                if question and len(question) > 10:  # Reasonable question length392                    questions.append(question)393 394        return questions[:3]  # Max 3 questions395 396# Global instance for reuse397_gemini_client = None398 399def get_gemini_client() -> GeminiClient:400    """401    Get or create the global Gemini client instance.402    Singleton pattern for efficient client reuse.403    """404    global _gemini_client405    if _gemini_client is None:406        _gemini_client = GeminiClient()407    return _gemini_client408