CoolFace
Apppublic

MMo4/csit-ned-chatbot

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
query_processor.py511 linesDownload Raw Back to langchain_processors
1"""
2Query processor for CSIT RAG system.
3Handles query preprocessing and enhancement for hybrid architecture.
4"""
5
6import re
7import logging
8from typing import Dict, List, Any, Optional
9from dataclasses import dataclass
10
11logger = logging.getLogger(__name__)
12
13@dataclass
14class ProcessedQuery:
15    """Processed query with extracted metadata."""
16    original_query: str
17    enhanced_query: str
18    detected_intent: str
19    detected_departments: List[str]
20    detected_specializations: List[str]   # NEW
21    detected_emotions: List[str]
22    extracted_concepts: List[str]
23    question_type: str
24    confidence: float
25
26class QueryProcessor:
27    """Processes and enhances queries for better retrieval."""
28    
29    def __init__(self):
30        # Define intent patterns (CSIT department focused)
31        self.intent_patterns = {
32            'academic_info': [
33                r'(courses?|subjects?|curriculum|credit hours?|semesters?|prospectus)',
34                r'(teach|taught|syllabus|outline|structure|timetable)',
35                r'(year courses|1st year|2nd year|3rd year|4th year|final year|third year|fourth year)',
36                r'(electives?|specialisation|specialization|track courses|program courses)',
37                r'(ai courses|cyber courses|ds courses|gaming courses|data science courses)',
38                r'(compare.*courses|courses.*compare|versus courses|vs courses)'
39            ],
40            'student_activities': [
41                r'(student club|clubs?|societies|student organization)',
42                r'(koderz|cybersents|ai alliance|data insight|gameverse|ledger league|qubit)',
43                r'(what clubs|all clubs|list of clubs|clubs at|clubs in)',
44                r'(tell me about clubs|student activities|extracurricular)'
45            ],
46            'programs': [
47                r'(program|degree|specialization|track|major)',
48                r'(bs|ms|phd|bachelors|masters|doctorate)'
49            ],
50            'faculty_info': [
51                r'(professor|teacher|faculty|instructor|lecturer|chairman)',
52                r'(dr\.|prof\.|supervisor)',
53                r'(who are|list of|all faculty|faculty members|faculty list)',
54                r'(staff|department faculty|csit faculty|faculty at)'
55            ],
56            'events': [
57                r'(event|hackathon|competition|koderz kombat|webkode)',
58                r'(seminar|workshop|conference|sports fest|techfest|iconics)'
59            ],
60            'admissions': [
61                r'(admission|merit|requirement|eligibility|apply)',
62                r'(application|criteria|deadline|entry test)'
63            ],
64            'research': [
65                r'(research|publication|thesis|fyp|dissertation)',
66                r'(project|paper|journal|lab work)'
67            ],
68            'achievements': [
69                r'(alumni|achievements?|accomplishments?|success|notable)',
70                r'(shining stars?|graduates?|former students?)',
71                r'(career paths?|where.*working|successful|placements?)'
72            ],
73            'facilities': [
74                r'(lab|facility|resource|equipment|library)',
75                r'(infrastructure|computer lab|workspace)'
76            ],
77            'policies': [
78                r'(policy|rule|regulation|obe|grading|exam)',
79                r'(attendance|assessment|rubric)'
80            ],
81            'general': [
82                r'(what|how|why|when|where)',
83                r'(tell|explain|describe|about)'
84            ]
85        }
86        
87        # Department detection (CSIT primary focus)
88        self.department_patterns = {
89            'CSIT': ['csit', 'computer science', 'cs', 'bcit', 'bs cs', 'bs computer science'],
90            'SE': ['se', 'software engineering', 'software engineer', r'\bse\b'],
91            'CIS': ['cis', 'computer engineering', 'ce', 'computer systems', 'computer information systems']
92        }
93
94        # ๐Ÿ”น NEW: Specialization detection (improved patterns)
95        self.specialization_patterns = {
96            "Cybersecurity": [r'\bcyber\b', 'cybersecurity', 'cyber security', 'cyber track', 'information security'],
97            "AI": [r'\bai\b', 'artificial intelligence', 'ai track', 'ai specialization', 'machine learning track'],
98            "Data Science": ['data science', r'\bds\b', 'ds track', 'data analytics'],
99            "Gaming & Animation": ['gaming', 'game dev', 'game development', 'animation', 'gaming and animation', 'ga track'],
100            "General": ['general track', 'general path', 'standard path', 'no specialization', 'general cs', 'core cs', 'core computer science', 'standard cs', 'basic cs', 'common cs']
101        }
102
103        # Year-level patterns for smart defaulting
104        self.year_patterns = {
105            '1st': [r'\b1st\b', 'first year', 'year 1', 'year one'],
106            '2nd': [r'\b2nd\b', 'second year', 'year 2', 'year two'],
107            '3rd': [r'\b3rd\b', 'third year', 'year 3', 'year three'],
108            '4th': [r'\b4th\b', 'fourth year', 'final year', 'year 4', 'year four', 'last year']
109        }
110
111        # ๐Ÿ”น External universities (out-of-scope detection)
112        self.external_universities = {
113            'FAST': ['fast university', 'nufast', 'nu-fast', 'nu fast', r'\bfast\b'],
114            'LUMS': ['lums', 'lahore university of management'],
115            'NUST': ['nust', 'national university of science'],
116            'IBA': ['iba karachi', 'iba sukkur', r'\biba\b'],
117            'GIKI': ['giki', 'ghulam ishaq khan'],
118            'COMSATS': ['comsats', 'ciit'],
119            'UET': ['uet lahore', 'uet peshawar', 'uet taxila', r'\buet\b'],
120            'SZABIST': ['szabist'],
121            'Bahria': ['bahria university'],
122            'Air University': ['air university'],
123            'PIEAS': ['pieas'],
124            'Karachi University': ['karachi university', 'ku'],
125            'Punjab University': ['punjab university', 'pu lahore']
126        }
127        
128        # Emotion detection
129        self.emotion_patterns = {
130            'worried': ['worry', 'worried', 'concern', 'concerned', 'anxiety', 'anxious'],
131            'curious': ['curious', 'interest', 'interested', 'want to know'],
132            'confused': ['confused', 'unclear', 'don\'t understand', 'complex'],
133            'confident': ['confident', 'sure', 'certain', 'ready'],
134            'neutral': []  # Default
135        }
136        
137        # Concept extraction patterns
138        self.concept_patterns = [
139            'theory', 'theoretical', 'practical', 'hands-on',
140            'curriculum', 'course', 'subject', 'lab',
141            'career', 'job', 'employment', 'salary', 'company',
142            'admission', 'merit', 'requirement', 'eligibility',
143            'graduate', 'student', 'alumni', 'placement',
144            'specialization'
145        ]
146    
147    def process_query(self, query: str, conversation_history: Optional[List[Dict]] = None) -> ProcessedQuery:
148        """Process and enhance a user query."""
149        try:
150            logger.info(f"Processing query: {query[:100]}...")
151            
152            # Clean the query
153            cleaned_query = self._clean_query(query)
154            
155            # Extract metadata
156            intent = self._detect_intent(cleaned_query)
157            departments = self._detect_departments(cleaned_query)
158            specializations = self._detect_specializations(cleaned_query)   # NEW
159            emotions = self._detect_emotions(cleaned_query)
160            concepts = self._extract_concepts(cleaned_query)
161            question_type = self._classify_question_type(cleaned_query)
162            
163            # Enhance query with context
164            enhanced_query = self._enhance_query(
165                cleaned_query, intent, departments, concepts, conversation_history, specializations
166            )
167            
168            # Calculate confidence
169            confidence = self._calculate_confidence(intent, departments, concepts)
170            
171            processed = ProcessedQuery(
172                original_query=query,
173                enhanced_query=enhanced_query,
174                detected_intent=intent,
175                detected_departments=departments,
176                detected_specializations=specializations,   # NEW
177                detected_emotions=emotions,
178                extracted_concepts=concepts,
179                question_type=question_type,
180                confidence=confidence
181            )
182            
183            logger.info(f"Query processed - Intent: {intent}, Departments: {departments}, Specializations: {specializations}")
184            return processed
185            
186        except Exception as e:
187            logger.error(f"Error processing query: {e}")
188            return ProcessedQuery(
189                original_query=query,
190                enhanced_query=query,
191                detected_intent='general',
192                detected_departments=[],
193                detected_specializations=[],   # fallback
194                detected_emotions=['neutral'],
195                extracted_concepts=[],
196                question_type='general',
197                confidence=0.5
198            )
199    
200    def _clean_query(self, query: str) -> str:
201        """Clean and normalize the query."""
202        cleaned = query.lower().strip()
203        cleaned = re.sub(r'\s+', ' ', cleaned)
204        return cleaned
205    
206    def _detect_intent(self, query: str) -> str:
207        """
208        Detect the intent of the query with priority ordering.
209        Academic queries are prioritized over general queries.
210        """
211        # Priority order: specific intents first, general last
212        priority_intents = [
213            'academic_info',      # HIGHEST PRIORITY
214            'student_activities',  # Student clubs (before programs to catch "clubs" before "program")
215            'achievements',        # Alumni and achievements
216            'programs',
217            'faculty_info',
218            'events',
219            'admissions',
220            'research',
221            'facilities',
222            'policies'
223        ]
224
225        # Check priority intents first
226        for intent in priority_intents:
227            if intent in self.intent_patterns:
228                for pattern in self.intent_patterns[intent]:
229                    if re.search(pattern, query, re.IGNORECASE):
230                        return intent
231
232        # Fallback to general if no specific intent matched
233        return 'general'
234    
235    def _detect_departments(self, query: str) -> List[str]:
236        """Detect mentioned departments."""
237        detected = []
238        for dept, patterns in self.department_patterns.items():
239            for pattern in patterns:
240                if re.search(rf"\b{re.escape(pattern.lower())}\b", query):
241                    detected.append(dept)
242                    break
243        return list(set(detected))
244
245    def _detect_specializations(self, query: str) -> List[str]:
246        """
247        Detect mentioned specializations.
248        Smart logic: If asking about 3rd/4th year courses without mentioning
249        a specific specialization, default to showing ALL options (don't filter).
250        """
251        detected = []
252        query_lower = query.lower()
253
254        # First, check if any specific specialization is mentioned
255        for spec, patterns in self.specialization_patterns.items():
256            for pattern in patterns:
257                # Handle regex patterns (those starting with r'\b')
258                if pattern.startswith(r'\b'):
259                    if re.search(pattern, query_lower):
260                        detected.append(spec)
261                        break
262                else:
263                    # Simple substring match for non-regex patterns
264                    if pattern in query_lower:
265                        detected.append(spec)
266                        break
267
268        # Smart defaulting logic:
269        # If query is about 3rd/4th year courses but NO specialization mentioned,
270        # DON'T add "General" - let it retrieve from all specializations
271        # This allows the user to see all options and choose
272
273        # Check if query mentions 3rd/4th year
274        is_upper_year_query = False
275        for year in ['3rd', '4th']:
276            if year in self.year_patterns:
277                for pattern in self.year_patterns[year]:
278                    if pattern.startswith(r'\b'):
279                        if re.search(pattern, query_lower):
280                            is_upper_year_query = True
281                            break
282                    else:
283                        if pattern in query_lower:
284                            is_upper_year_query = True
285                            break
286
287        # If it's an upper year query with NO detected specialization,
288        # don't filter by specialization (return empty list)
289        # This lets the retrieval return courses from ALL specializations
290        if is_upper_year_query and not detected:
291            logger.info(f"๐ŸŽ“ Upper year query without specialization - will show all options")
292            return []  # Don't filter - show all
293
294        return list(set(detected))
295
296    def detect_out_of_scope(self, query: str) -> Dict[str, Any]:
297        """Detect if query mentions external universities or out-of-scope topics."""
298        query_lower = query.lower()
299
300        # Check for external universities
301        for university, patterns in self.external_universities.items():
302            for pattern in patterns:
303                # Handle regex patterns (those with \b)
304                if pattern.startswith(r'\b'):
305                    if re.search(pattern, query_lower):
306                        return {
307                            'is_out_of_scope': True,
308                            'reason': 'external_university',
309                            'entity': university
310                        }
311                # Handle simple string patterns
312                elif pattern in query_lower:
313                    return {
314                        'is_out_of_scope': True,
315                        'reason': 'external_university',
316                        'entity': university
317                    }
318
319        # Query is in scope
320        return {
321            'is_out_of_scope': False,
322            'reason': None,
323            'entity': None
324        }
325
326    def _detect_emotions(self, query: str) -> List[str]:
327        """Detect emotional context in the query."""
328        detected = []
329        for emotion, patterns in self.emotion_patterns.items():
330            if emotion == 'neutral': continue
331            if any(p.lower() in query for p in patterns):
332                detected.append(emotion)
333        return detected if detected else ['neutral']
334    
335    def _extract_concepts(self, query: str) -> List[str]:
336        """Extract key concepts."""
337        return [c for c in self.concept_patterns if c.lower() in query.lower()]
338    
339    def _classify_question_type(self, query: str) -> str:
340        if re.search(r'\?', query):
341            if re.search(r'^(what|how|why|when|where|which)', query, re.IGNORECASE):
342                return 'informational'
343            elif re.search(r'^(is|are|do|does|can|will)', query, re.IGNORECASE):
344                return 'yes_no'
345            else:
346                return 'open_ended'
347        return 'statement'
348    
349    def _enhance_query(self, query: str, intent: str, departments: List[str],
350                      concepts: List[str], conversation_history: Optional[List[Dict]] = None,
351                      specializations: Optional[List[str]] = None) -> str:
352        """
353        Enhance the query with additional context for better retrieval.
354        This enhanced query is used for embedding generation to improve semantic matching.
355        """
356        # Start with original query
357        enhanced_parts = [query]
358
359        # Add department context for better matching
360        if departments:
361            dept_names = []
362            for dept in departments:
363                if dept == 'CSIT':
364                    dept_names.append('Computer Science Information Technology BCIT')
365                elif dept == 'SE':
366                    dept_names.append('Software Engineering')
367                elif dept == 'CIS':
368                    dept_names.append('Computer Engineering Computer Information Systems')
369            if dept_names:
370                enhanced_parts.append(' '.join(dept_names))
371
372        # Add specialization context for better matching
373        if specializations:
374            spec_expansions = []
375            for spec in specializations:
376                if spec == 'AI':
377                    spec_expansions.append('Artificial Intelligence Machine Learning')
378                elif spec == 'Cybersecurity':
379                    spec_expansions.append('Cybersecurity Information Security Network Security')
380                elif spec == 'Data Science':
381                    spec_expansions.append('Data Science Data Analytics Big Data')
382                elif spec == 'Gaming & Animation':
383                    spec_expansions.append('Gaming Game Development Animation Graphics')
384                else:
385                    spec_expansions.append(spec)
386            if spec_expansions:
387                enhanced_parts.append(' '.join(spec_expansions))
388
389        # Add intent-specific keywords for better matching
390        intent_keywords = {
391            'academic_info': 'courses curriculum syllabus subjects credit hours',
392            'faculty_info': 'professor teacher instructor lecturer faculty staff members specialization education background PhD',
393            'student_activities': 'student clubs societies organizations Koderz CyberSENTS AI Alliance Data Insight Gameverse Ledger League Qubits Qrew extracurricular activities',
394            'achievements': 'alumni graduates achievements accomplishments success stories shining stars career placements notable former students',
395            'events': 'event competition hackathon seminar workshop TechFest ICONICS SportsFest',
396            'admissions': 'admission merit eligibility requirements criteria',
397            'programs': 'program degree specialization track',
398        }
399        if intent in intent_keywords:
400            enhanced_parts.append(intent_keywords[intent])
401
402        # For comprehensive faculty queries, add specific boost keywords
403        if intent == 'faculty_info':
404            comprehensive_patterns = ['all faculty', 'faculty members', 'list of', 'who are', 'faculty list', 'tell me about faculty']
405            if any(pattern in query.lower() for pattern in comprehensive_patterns):
406                enhanced_parts.append('Chairman Professors Associate Professors Assistant Professors Lecturers CSIT Department NEDUET')
407
408        # Add conversation context for continuity
409        if conversation_history:
410            recent = self._extract_conversation_context(conversation_history)
411            if recent:
412                enhanced_parts.append(' '.join(recent[:3]))
413
414        # Combine all parts
415        enhanced = ' '.join(enhanced_parts)
416
417        # Add metadata tags for logging (not used in embedding)
418        metadata_tags = f" [Intent: {intent}]"
419        if departments:
420            metadata_tags += f" [Departments: {', '.join(departments)}]"
421        if specializations:
422            metadata_tags += f" [Specializations: {', '.join(specializations)}]"
423
424        logger.info(f"Enhanced query: {enhanced[:150]}... {metadata_tags}")
425
426        return enhanced
427    
428    def _extract_conversation_context(self, history: List) -> List[str]:
429        """Extract relevant context from conversation history."""
430        context_topics = []
431        for interaction in history[-3:]:
432            if isinstance(interaction, dict):
433                query = interaction.get('query', '')
434                context_topics.extend(self._extract_concepts(query)[:2])
435                context_topics.extend(self._detect_departments(query))
436        return list(set(context_topics))[:5]
437    
438    def _calculate_confidence(self, intent: str, departments: List[str], concepts: List[str]) -> float:
439        confidence = 0.5
440        if intent != 'general': confidence += 0.2
441        if departments: confidence += 0.15 * len(departments)
442        if concepts: confidence += 0.1 * min(len(concepts), 3)
443        return min(confidence, 1.0)
444    
445    def get_query_suggestions(self, query: str) -> List[str]:
446        """Generate contextual query suggestions based on processed query."""
447        processed = self.process_query(query)
448        suggestions = []
449
450        # CSIT-focused suggestions
451        if processed.detected_intent == 'academic_info':
452            suggestions.extend([
453                "What are the specializations available in CSIT?",
454                "Show me the course structure for AI track",
455                "What electives can I choose in 3rd year?"
456            ])
457        elif processed.detected_intent == 'programs':
458            suggestions.extend([
459                "What's the difference between AI and Data Science specializations?",
460                "Tell me about MS programs in CSIT",
461                "What are the admission requirements for PhD?"
462            ])
463        elif processed.detected_intent == 'faculty_info':
464            suggestions.extend([
465                "Who are the professors in CSIT department?",
466                "What research areas does the faculty focus on?",
467                "Who is the department chairman?"
468            ])
469        elif processed.detected_intent == 'events':
470            suggestions.extend([
471                "What is Koderz Kombat competition?",
472                "Tell me about recent hackathons",
473                "When is the next CSIT event?"
474            ])
475        return suggestions[:3]
476
477    def is_se_cis_academic_query(self, processed: ProcessedQuery) -> bool:
478        """
479        Check if query is about SE/CIS academics (allowed).
480
481        Args:
482            processed: ProcessedQuery object
483
484        Returns:
485            True if SE/CIS academic query, False otherwise
486        """
487        has_se_cis = any(dept in processed.detected_departments for dept in ['SE', 'CIS'])
488        is_academic = processed.detected_intent == 'academic_info'
489
490        return has_se_cis and is_academic
491
492    def should_redirect_to_department(self, processed: ProcessedQuery) -> Optional[str]:
493        """
494        Determine if query should be redirected to another department.
495
496        Args:
497            processed: ProcessedQuery object
498
499        Returns:
500            Department name to redirect to, or None
501        """
502        # SE non-academic query
503        if 'SE' in processed.detected_departments and processed.detected_intent != 'academic_info':
504            return 'SE'
505
506        # CIS non-academic query
507        if 'CIS' in processed.detected_departments and processed.detected_intent != 'academic_info':
508            return 'CIS'
509
510        return None
511