CoolFace
Apppublic

trialback121/prmsu

sourceHugging Faceotherupdated 1y agoView on Hugging Face
0likes
chatbot.py896 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Simple Terminal Chatbot for Vector Database4 5This chatbot answers questions based only on data stored in the vector database6using Cohere API for natural language processing.7"""8 9import argparse10import sys11from typing import List, Dict12import cohere13from definition_chunker import DefinitionChunker14 15 16def validate_prmsu_relevance(question: str) -> bool:17    """18    Validate if the question is related to PRMSU student handbook topics.19    More lenient validation - only blocks obvious non-academic topics.20    """21    question_lower = question.lower()22 23    # Only block very obvious non-PRMSU topics24    non_prmsu_patterns = [25        # Math calculations only26        r'\d+\s*[\+\-\*\/]\s*\d+',  # Basic math operations like 1+1, 2*3, etc.27        r'what\s+is\s+\d+\s*[\+\-\*\/]',  # "what is 1+1", "what is 2*3"28 29        # Very specific non-academic topics30        r'weather|temperature|climate',31        r'cooking|recipe|food|restaurant',32        r'movie|film|cinema|actor|actress',33        r'music|song|singer|band',34        r'celebrity|famous\s+person',35        r'sports|football|basketball|soccer',36 37        # Other specific universities only38        r'harvard\s+university|mit\s+university|stanford\s+university',39        r'university\s+of\s+the\s+philippines|ateneo|de\s+la\s+salle'40    ]41 42    # Check for non-PRMSU patterns - but be more lenient43    import re44    for pattern in non_prmsu_patterns:45        if re.search(pattern, question_lower):46            return False47 48    # If it's not obviously non-academic, assume it could be PRMSU-related49    # This makes the validation much more lenient for student handbook questions50    return True51 52def format_user_friendly_response(answer: str, question: str) -> str:53    """54    Format the response to be more user-friendly and organized.55    """56    if not answer:57        return answer58 59    question_lower = question.lower()60 61    # Clean up the answer62    answer = answer.strip()63 64    # Add appropriate emoji and formatting based on question type65    if any(word in question_lower for word in ['stands for', 'acronym', 'what does']):66        # For acronym questions67        if 'prmsu' in question_lower:68            return f"๐Ÿซ **PRMSU** stands for:\n**President Ramon Magsaysay State University**\n\n๐Ÿ“ The main campus is located in **Iba, Zambales**."69 70    elif any(word in question_lower for word in ['vision', 'mission']):71        # For vision/mission questions72        emoji = "๐ŸŽฏ" if 'vision' in question_lower else "๐ŸŽฏ"73        title = "Vision" if 'vision' in question_lower else "Mission"74        return f"{emoji} **PRMSU {title}:**\n{answer}"75 76    elif any(word in question_lower for word in ['penalty', 'offense', 'violation']):77        # For disciplinary questions78        return f"โš–๏ธ **Disciplinary Policy:**\n{answer}"79 80    elif any(word in question_lower for word in ['scholarship', 'financial assistance']):81        # For scholarship questions82        return f"๐Ÿ’ฐ **Scholarship Information:**\n{answer}"83 84    elif any(word in question_lower for word in ['uniform', 'dress code']):85        # For uniform questions86        return f"๐Ÿ‘” **Uniform Policy:**\n{answer}"87 88    elif any(word in question_lower for word in ['admission', 'requirement', 'enroll']):89        # For admission questions90        return f"๐Ÿ“ **Admission Information:**\n{answer}"91 92    elif any(word in question_lower for word in ['gwa', 'grade', 'grading']):93        # For grading questions94        return f"๐Ÿ“Š **Academic Information:**\n{answer}"95 96    elif any(word in question_lower for word in ['graduation', 'honors', 'cum laude']):97        # For graduation questions98        return f"๐ŸŽ“ **Graduation Information:**\n{answer}"99 100    elif any(word in question_lower for word in ['where', 'located', 'location']):101        # For location questions102        return f"๐Ÿ“ **University Location:**\n{answer}"103 104    elif any(word in question_lower for word in ['campus', 'how many', 'established', 'when']):105        # For general university information106        return f"๐Ÿ›๏ธ **University Information:**\n{answer}"107 108    elif any(word in question_lower for word in ['student assistant', 'work-study']):109        # For student assistant questions110        return f"๐Ÿ’ผ **Student Assistant Program:**\n{answer}"111 112    else:113        # Default formatting with university emoji114        return f"๐Ÿ“š **PRMSU Student Handbook:**\n{answer}"115 116def enhance_response_specificity(question: str, answer: str, search_results: List[Dict]) -> str:117    """118    Post-process the answer to make it more specific and prevent truncation.119    """120    question_lower = question.lower()121 122    # First, validate if the question is PRMSU-related123    if not validate_prmsu_relevance(question):124        return "๐Ÿšซ **Sorry, I can only answer questions related to PRMSU (President Ramon Magsaysay State University) student handbook.**\n\nI cannot help with:\nโ€ข Math calculations or general knowledge\nโ€ข Weather, news, or entertainment topics\nโ€ข Other universities or non-academic subjects\nโ€ข Personal advice or general information\n\nPlease ask about:\nโ€ข PRMSU policies and regulations\nโ€ข Academic requirements and procedures\nโ€ข Student services and programs\nโ€ข University information and guidelines\n\n**Example questions:**\nโ€ข 'What are the admission requirements for PRMSU?'\nโ€ข 'What is the grading system at PRMSU?'\nโ€ข 'What are the scholarship requirements?'"125 126    # Fix truncation issues first - if answer ends abruptly, try to complete it127    if answer and not answer.strip().endswith(('.', '!', '?', ':', '%')):128        # Try to find a complete answer from search results129        for result in search_results:130            definition = result.get('definition', '')131            if definition and len(definition) > len(answer):132                # Use the complete definition if it contains the partial answer133                if answer.strip() in definition:134                    answer = definition135                    break136 137    # Specific question handlers with complete answers138    if 'what law' in question_lower and 'established' in question_lower:139        return "President Ramon Magsaysay State University (PRMSU) was officially established by Republic Act No. 11015 on April 20, 2018."140 141    if 'four types' in question_lower and 'cross' in question_lower:142        return "The four types of cross-enrolment at PRMSU are: 1) Inbound Cross Enrolment (students from other institutions enrolling at PRMSU), 2) Outbound Cross Enrolment (PRMSU students enrolling in external institutions), 3) In-Campus Cross Enrolment (PRMSU students enrolling in different colleges within the same campus), and 4) Out-Campus Cross Enrolment (PRMSU students enrolling in another PRMSU campus)."143 144    if 'how many units' in question_lower and 'midyear' in question_lower:145        return "Students may take a maximum of 9 units during midyear classes. Graduating students may overload up to 12 units only with approval from the Registrar upon recommendation of the Dean. Students with academic deficiencies are not allowed to overload."146 147    if 'consequence' in question_lower and '20%' in question_lower and 'absence' in question_lower:148        return "Students who accumulate 20% unexcused absences in any subject automatically receive a grade of 5.0 (failing grade) for that subject."149 150    if 'prescribed uniform' in question_lower or ('uniform' in question_lower and ('male' in question_lower or 'female' in question_lower)):151        return "Male students must wear white polo shirt, black pants, and black formal shoes. Female students must wear blue skirt or blue slacks, white blouse, necktie, and black shoes. LGBTQ+ policy: Women members may wear slacks, blouse, and necktie combination, but men members are NOT permitted to wear skirts."152 153    if 'what grade' in question_lower and 'transferee' in question_lower:154        return "Transferee students must have earned a minimum grade of 3.0 or its equivalent in their previous school for their courses to be accredited at PRMSU. The course content and unit weight must also be equivalent to PRMSU standards."155 156    if 'grounds for termination' in question_lower and 'scholarship' in question_lower:157        return "Grounds for termination of scholarship or financial assistance include: 1) Failure to maintain the required GWA, 2) Dropping out without proper notice, 3) Carrying fewer units than prescribed, 4) Failure to comply with reapplication requirements, and 5) Violation of university rules and regulations."158 159    if 'maximum number of hours' in question_lower and 'student assistant' in question_lower:160        return "Student assistants receive โ‚ฑ25.00 per hour and may work a maximum of 100 hours per month, subject to COA rules. Requirements include: must be officially enrolled, possess relevant skills, maintain good grades, demonstrate good moral character, submit resume, recent grades, certificate of registration, ID photo, class schedule, and parental consent. The program is limited to 50 assistants per semester, and poor performance automatically disqualifies students from reapplication."161 162    if 'penalty' in question_lower and 'liquor' in question_lower:163        if 'first offense' in question_lower:164            return "First offense for being under the influence of liquor on campus results in 15 days suspension, 12 hours of transformative experience, and mandatory guidance intervention."165        elif 'second offense' in question_lower:166            return "Second offense for liquor-related violations at PRMSU results in 30 days suspension, 24 hours of transformative experience, and continued guidance intervention."167        elif 'third offense' in question_lower:168            return "Third offense for liquor-related violations at PRMSU results in one-year suspension from the university."169        else:170            # If no specific offense number mentioned, provide all penalties171            return "PRMSU liquor-related offenses carry progressive penalties: First offense: 15 days suspension, 12 hours transformative experience, mandatory guidance intervention. Second offense: 30 days suspension, 24 hours transformative experience, continued guidance intervention. Third offense: One-year suspension."172 173    if 'honors' in question_lower and 'graduating' in question_lower and 'gwa' in question_lower:174        return "Three honors are awarded to graduating students: 1) Summa Cum Laude requires 1.0-1.25 GWA with no grade below 1.5, 2) Magna Cum Laude requires 1.26-1.5 GWA with no grade below 1.75, and 3) Cum Laude requires 1.51-1.75 GWA with no grade below 2.0."175 176    # Advanced question handlers177    if 'maximum number of hours' in question_lower and 'semester' in question_lower and 'student assistant' in question_lower:178        return "Student assistants work a maximum of 100 hours per month. In a typical 4-month semester, this equals approximately 400 hours per semester (100 hours/month ร— 4 months = 400 hours/semester)."179 180    if 'deficiencies' in question_lower and 'cleared' in question_lower and 'council' in question_lower:181        return "All deficiencies must be cleared three (3) working days before the University-wide Academic Council meeting."182 183    if 'transferee' in question_lower and 'honors' in question_lower and ('residency' in question_lower or 'additional' in question_lower):184        return "For transferees to graduate with honors at PRMSU, they must meet additional requirements beyond GWA: 1) Complete all academic units at PRMSU (residency requirement), 2) Carry the regular academic load throughout their studies, 3) Finish within the prescribed time frame for their program, and 4) Have no failing grades, incomplete grades, or disciplinary violations on record. Those meeting GWA requirements but not residency or load requirements receive a Certificate of Graduation with Academic Distinction instead."185 186    if 'outbound cross' in question_lower and 'approve' in question_lower:187        return "Outbound cross-enrolment requests must be approved by the Dean and Registrar. This is generally allowed only when the course or subject is not offered at PRMSU during the specific academic year and term, the host school has a comparable standard of education, and typically only general education subjects are permitted."188 189    if 'liquor' in question_lower and 'related' in question_lower and 'violation' in question_lower:190        return "PRMSU's liquor-related offense policy covers multiple violations: entering the university intoxicated, possessing alcohol on campus, using alcohol on campus, selling alcohol on campus, and consuming alcohol on campus. All these violations carry progressive penalties."191 192    if 'private scholarship' in question_lower and ('gwa' in question_lower or 'average' in question_lower):193        return "Private scholarship applicants at PRMSU must maintain a minimum General Weighted Average (GWA) of 1.75. Additional academic conditions include: being officially enrolled, demonstrating good moral character, and having no failing or incomplete grades on record."194 195    if any(word in question_lower for word in ['where', 'located']) and 'prmsu' in question_lower:196        return "๐Ÿ“ **University Location:**\nPresident Ramon Magsaysay State University (PRMSU) is located in Iba, Zambales, Philippines. The university has seven campuses throughout Zambales province."197 198    # Clean up any remaining truncation issues199    if answer and len(answer) > 10:200        # Remove incomplete sentences at the end201        sentences = answer.split('.')202        complete_sentences = []203 204        for sentence in sentences:205            sentence = sentence.strip()206            if sentence and len(sentence) > 5:  # Avoid very short fragments207                complete_sentences.append(sentence)208 209        if complete_sentences:210            result = '. '.join(complete_sentences)211            if not result.endswith('.'):212                result += '.'213            answer = result214 215    # Apply user-friendly formatting216    formatted_answer = format_user_friendly_response(answer, question)217    return formatted_answer218 219 220class VectorDatabaseChatbot:221    def __init__(self, api_key: str, db_path: str = "./vector_db", collection_name: str = "definitions"):222        """Initialize the chatbot with Cohere API and vector database."""223        try:224            self.cohere_client = cohere.Client(api_key)225            self.chunker = DefinitionChunker(db_path=db_path, collection_name=collection_name)226 227            print("๐Ÿค– Vector Database Chatbot initialized!")228            print("๐Ÿ“š Connected to vector database")229            print("๐Ÿ”— Connected to Cohere API")230            print()231        except Exception as e:232            print(f"โŒ Error initializing Cohere client: {e}")233            raise234    235    def search_relevant_context(self, query: str, max_results: int = 8) -> List[Dict]:236        """Search for relevant definitions in the vector database with improved matching."""237        try:238            # Increase search results to get better matches239            results = self.chunker.search_definitions(query, n_results=max_results * 3)240 241            # Enhanced query preprocessing242            query_lower = query.lower()243            query_clean = query_lower.replace('what is ', '').replace('what are ', '').replace('define ', '').replace('the ', '').replace('tell me about ', '').replace('?', '').strip()244 245            # Special handling for critical university information246            university_info_keywords = {247                'prmsu stands for': 'President Ramon Magsaysay State University',248                'what does prmsu stand for': 'President Ramon Magsaysay State University',249                'prmsu meaning': 'President Ramon Magsaysay State University',250                'when was prmsu established': 'April 20, 2018',251                'prmsu establishment': 'April 20, 2018',252                'how many campuses': 'seven campuses',253                'number of campuses': 'seven campuses',254                'campus count': 'seven campuses'255            }256 257            # Check for exact or near-exact term matches258            exact_matches = []259            partial_matches = []260            keyword_priority_matches = []261            other_results = []262 263            for result in results:264                term_lower = result.get('term', '').lower()265                definition_lower = result.get('definition', '').lower()266 267                # Special priority for university basic info268                if any(keyword in query_lower for keyword in university_info_keywords.keys()):269                    if any(info in definition_lower for info in university_info_keywords.values()):270                        keyword_priority_matches.append(result)271                        continue272 273                # Exact match - prioritize these regardless of similarity score274                if term_lower == query_clean:275                    exact_matches.append(result)276                # Partial match - term contains the query or query contains the term277                elif query_clean in term_lower or term_lower in query_clean:278                    partial_matches.append(result)279                else:280                    other_results.append(result)281 282            # Reorder results: keyword priority first, then exact matches, then partial matches, then others283            prioritized_results = keyword_priority_matches + exact_matches + partial_matches + other_results284 285 286 287            # Enhanced keyword-based prioritization with specific fixes288 289            # Fix graduation honors vs athlete confusion290            if any(word in query_lower for word in ['summa cum laude', 'magna cum laude', 'cum laude', 'graduation honors', 'honors gwa', 'gwa for honors']):291                # Prioritize graduation policies over athlete requirements292                graduation_results = [r for r in prioritized_results if 'graduation' in r.get('term', '').lower() or 'policies for graduation' in r.get('term', '').lower()]293                athlete_results = [r for r in prioritized_results if 'athlete' in r.get('term', '').lower()]294                other_results = [r for r in prioritized_results if r not in graduation_results and r not in athlete_results]295                prioritized_results = graduation_results + other_results + athlete_results  # Put athlete results last296 297            # Fix grading system queries298            elif any(word in query_lower for word in ['grade range', 'grading system', '1.0 grade', '1.75 grade', 'grade equals']):299                # Prioritize grading system results300                grading_results = [r for r in prioritized_results if 'grading system' in r.get('term', '').lower()]301                other_results = [r for r in prioritized_results if 'grading system' not in r.get('term', '').lower()]302                prioritized_results = grading_results + other_results303 304            # Fix attendance/absence percentage queries305            elif any(word in query_lower for word in ['absence', 'absences', 'attendance', 'failing grade', '20%', 'percentage']):306                # Prioritize class attendance results307                attendance_results = [r for r in prioritized_results if 'attendance' in r.get('term', '').lower() or 'class attendance' in r.get('term', '').lower()]308                other_results = [r for r in prioritized_results if 'attendance' not in r.get('term', '').lower()]309                prioritized_results = attendance_results + other_results310 311            # Original admission requirements logic312            elif any(word in query_lower for word in ['admission requirements', 'requirements', 'requirements for', 'what are the requirements']):313                # Filter and prioritize admission requirements results314                req_results = [r for r in prioritized_results if 'requirements' in r.get('term', '').lower()]315                other_results = [r for r in prioritized_results if 'requirements' not in r.get('term', '').lower()]316                prioritized_results = req_results + other_results317            elif any(word in query_lower for word in ['admission', 'admission policy', 'admission rules']) and 'requirements' not in query_lower:318                # Filter and prioritize general admission results (not requirements)319                adm_results = [r for r in prioritized_results if 'admission' in r.get('term', '').lower() and 'requirements' not in r.get('term', '').lower()]320                req_results = [r for r in prioritized_results if 'requirements' in r.get('term', '').lower()]321                other_results = [r for r in prioritized_results if 'admission' not in r.get('term', '').lower() and 'requirements' not in r.get('term', '').lower()]322                prioritized_results = adm_results + req_results + other_results323 324            # If user specifically mentions a section, prioritize that section325            if 'section 1' in query_lower or 'section1' in query_lower:326                # Filter and prioritize Section 1 results327                section1_results = [r for r in prioritized_results if 'SECTION 1' in r.get('term', '').upper()]328                other_results = [r for r in prioritized_results if 'SECTION 1' not in r.get('term', '').upper()]329                prioritized_results = section1_results + other_results330            elif 'section 2' in query_lower or 'section2' in query_lower:331                # Filter and prioritize Section 2 results332                section2_results = [r for r in prioritized_results if 'SECTION 2' in r.get('term', '').upper()]333                other_results = [r for r in prioritized_results if 'SECTION 2' not in r.get('term', '').upper()]334                prioritized_results = section2_results + other_results335 336            # Now filter by similarity score with improved logic337            final_results = []338            for result in prioritized_results:339                distance = result.get('distance', 1)340                similarity = 1 - distance if distance is not None else 0341                term_lower = result.get('term', '').lower()342                definition_lower = result.get('definition', '').lower()343 344                # Always include exact matches, regardless of similarity score345                if term_lower == query_clean:346                    final_results.append(result)347                # Include keyword priority matches (university info)348                elif result in keyword_priority_matches:349                    final_results.append(result)350                # Include results with key terms in definition351                elif any(keyword in definition_lower for keyword in query_clean.split()):352                    final_results.append(result)353                # For other matches, use improved similarity threshold354                elif similarity > -0.3:  # Slightly more restrictive but still lenient355                    final_results.append(result)356 357            # If we still don't have enough results, include the best available358            if len(final_results) < 3 and prioritized_results:359                for result in prioritized_results:360                    if result not in final_results:361                        final_results.append(result)362                        if len(final_results) >= max_results:363                            break364 365            # Store the prioritized results for potential fallback use366            self._last_search_results = final_results[:max_results]367            return final_results[:max_results]368        except Exception as e:369            print(f"Error searching database: {e}")370            return []371    372    def format_context(self, search_results: List[Dict]) -> str:373        """Format search results into context for the AI."""374        if not search_results:375            return "No relevant information found in the database."376        377        context_parts = []378        for i, result in enumerate(search_results, 1):379            term = result.get('term', 'Unknown')380            definition = result.get('definition', 'No definition available')381            context_parts.append(f"{i}. {term}: {definition}")382        383        return "\n".join(context_parts)384    385    def extract_specific_item(self, query: str, definition: str) -> str:386        """Extract specific item from a section based on the query."""387        lines = definition.split('\n')388        query_lower = query.lower()389 390        # Enhanced keyword mappings for more specific extraction391        keyword_mappings = {392            'vision statement': ['university vision', 'vision'],393            'vision': ['university vision', 'vision'],394            'mission statement': ['university mission', 'mission'],395            'mission': ['university mission', 'mission'],396            'quality policy': ['quality policy'],397            'president': ['president', 'university president'],398            'acronym': ['acronym', 'stands for'],399            'establishment': ['established', 'establishment'],400            'campus count': ['campuses', 'campus'],401            'penalty': ['penalty', 'offense', 'suspension', 'expulsion'],402            'requirements': ['requirements', 'must submit', 'include'],403            'timeframe': ['weeks', 'days', 'within'],404            'percentage': ['percent', '%'],405            'gpa': ['gwa', 'gpa', 'cum laude', 'magna', 'summa']406        }407 408        # First, try to find exact matches for specific queries409        for query_keyword, line_keywords in keyword_mappings.items():410            if query_keyword in query_lower:411                for line in lines:412                    line_lower = line.lower()413                    for line_keyword in line_keywords:414                        if line_keyword in line_lower:415                            # For vision/mission statements, extract just the statement part416                            if query_keyword in ['vision', 'vision statement'] and 'university vision' in line_lower:417                                if '-' in line:418                                    return line.split('-', 1)[-1].strip()419                                return line.strip()420                            elif query_keyword in ['mission', 'mission statement'] and 'university mission' in line_lower:421                                if '-' in line:422                                    return line.split('-', 1)[-1].strip()423                                return line.strip()424                            elif query_keyword == 'quality policy' and 'quality policy' in line_lower:425                                if '-' in line:426                                    return line.split('-', 1)[-1].strip()427                                return line.strip()428                            # For other specific queries, return the relevant line429                            elif any(keyword in line_lower for keyword in line_keywords):430                                return line.strip()431 432        # If no specific match found, return the full definition433        return definition434 435    def apply_special_handling(self, query_lower: str, search_results: List[Dict], current_best_match) -> Dict:436        """Apply special handling logic for specific query types."""437        best_match = current_best_match438 439        # Special handling for cross-enrollment queries440        if any(word in query_lower for word in ['inbound', 'outbound', 'in campus', 'out campus']):441            for result in search_results:442                term_lower = result.get('term', '').lower()443                if 'inbound' in query_lower and 'inbound' in term_lower:444                    return result445                elif 'outbound' in query_lower and 'outbound' in term_lower:446                    return result447                elif 'in campus' in query_lower and 'in campus' in term_lower:448                    return result449                elif 'out campus' in query_lower and 'out campus' in term_lower:450                    return result451 452        # Special handling for sports vs culture incentive queries453        if any(word in query_lower for word in ['sports', 'athlete', 'winning athletes']):454            for result in search_results:455                term_lower = result.get('term', '').lower()456                if 'sports' in term_lower:457                    return result458        elif any(word in query_lower for word in ['culture', 'arts', 'cado']):459            for result in search_results:460                term_lower = result.get('term', '').lower()461                if 'culture' in term_lower and 'arts' in term_lower:462                    return result463 464        # Special handling for complex multi-conditional queries465        if any(word in query_lower for word in ['conditions', 'requirements', 'four conditions', 'five requirements', 'beyond gpa']):466            # For graduation honors conditions beyond GPA467            if 'honors' in query_lower and 'beyond' in query_lower:468                for result in search_results:469                    term_lower = result.get('term', '').lower()470                    if 'graduation honors additional conditions' in term_lower:471                        return result472            # For PWD facilities473            elif 'facilities' in query_lower and ('disability' in query_lower or 'pwd' in query_lower):474                for result in search_results:475                    term_lower = result.get('term', '').lower()476                    if 'pwd campus facilities' in term_lower:477                        return result478            # For mid-year LOA rationale479            elif 'mid-year' in query_lower and ('unnecessary' in query_lower or 'why' in query_lower):480                for result in search_results:481                    term_lower = result.get('term', '').lower()482                    if 'mid-year' in term_lower and 'policy' in term_lower:483                        return result484 485        return best_match486 487    def create_fallback_response(self, query: str, search_results: List[Dict]) -> str:488        """Create a fallback response when AI fails or provides incomplete answers."""489        if not search_results:490            return "I'm sorry, but I don't have any information in my database that relates to your question."491 492        # Get the best matches, prioritizing exact term matches493        response_parts = []494        query_lower = query.lower()495        query_clean = query_lower.replace('what is ', '').replace('what are ', '').replace('define ', '').replace('the ', '').replace('tell me about ', '').replace('?', '').strip()496 497        # Find the best match based on similarity and relevance498        best_match = None499        best_similarity = -1500 501        # Apply special handling first502        best_match = self.apply_special_handling(query_lower, search_results, best_match)503 504        # If no special handling match, look for exact term matches505        if not best_match:506            for result in search_results:507                term_lower = result.get('term', '').lower()508                if term_lower == query_clean:509                    best_match = result510                    break511 512        # If no exact match, look for keyword matches in term names with priority for exact keyword matches513        if not best_match:514            for result in search_results:515                term_lower = result.get('term', '').lower()516                definition_lower = result.get('definition', '').lower()517 518                # Check for exact keyword matches first (like "inbound" in "inbound cross enrolment")519                query_keywords = query_clean.split()520                exact_keyword_matches = sum(1 for keyword in query_keywords if len(keyword) > 2 and keyword in term_lower)521 522                # Also check for keyword matches in definition for complex queries523                definition_keyword_matches = sum(1 for keyword in query_keywords if len(keyword) > 3 and keyword in definition_lower)524 525                total_matches = exact_keyword_matches + (definition_keyword_matches * 0.3)526 527                if total_matches > 0:528                    distance = result.get('distance', 1)529                    similarity = 1 - distance if distance is not None else 0530                    # Boost similarity for exact keyword matches531                    boosted_similarity = similarity + (total_matches * 0.4)532                    if boosted_similarity > best_similarity:533                        best_similarity = boosted_similarity534                        best_match = result535 536        # If still no match, find the highest similarity match537        if not best_match:538            for result in search_results:539                distance = result.get('distance', 1)540                similarity = 1 - distance if distance is not None else 0541                if similarity > best_similarity:542                    best_similarity = similarity543                    best_match = result544 545        # Apply special handling logic in fallback method546        best_match = self.apply_special_handling(query_lower, search_results, best_match)547 548        # Check if this is a specific question that needs all relevant results549        if any(word in query_lower for word in ['requirements', 'what are', 'list', 'all', 'organizations', 'groups']):550            # Include multiple relevant results, but prioritize best match551            if best_match:552                term = best_match.get('term', 'Unknown')553                definition = best_match.get('definition', 'No definition available')554                response_parts.append(f"**{term}**: {definition}")555 556            # Add other relevant results for comprehensive answers557            for result in search_results[:3]:558                if result != best_match:559                    term = result.get('term', 'Unknown')560                    definition = result.get('definition', 'No definition available')561                    distance = result.get('distance', 1)562                    similarity = 1 - distance if distance is not None else 0563 564                    # Include if it's reasonably relevant or contains key terms565                    if similarity > -0.2 or any(keyword in term.lower() for keyword in query_clean.split()):566                        response_parts.append(f"**{term}**: {definition}")567        else:568            # Single best match with targeted extraction569            if best_match:570                term = best_match.get('term', 'Unknown')571                definition = best_match.get('definition', 'No definition available')572 573                # Use targeted extraction for specific queries574                extracted_content = self.extract_specific_item(query, definition)575                response_parts.append(f"**{term}**: {extracted_content}")576 577        if response_parts:578            return "\n\n".join(response_parts)579        else:580            return "I'm sorry, but I don't have any information in my database that relates to your question."581 582    def analyze_question_with_ai(self, query: str, search_results: List[Dict]) -> str:583        """Use AI to understand the question and find the most relevant answer from search results."""584        if not search_results:585            return "I'm sorry, but I don't have any information in my database that relates to your question."586 587        # Filter out results with very low similarity scores (negative or very low positive)588        filtered_results = []589        for result in search_results:590            distance = result.get('distance', 1)591            similarity = 1 - distance if distance is not None else 0592            # Only include results with similarity > 0.05 (distance < 0.95)593            if similarity > 0.05:594                filtered_results.append(result)595 596        # If no good matches, use the best available597        if not filtered_results and search_results:598            filtered_results = search_results[:1]599 600        if not filtered_results:601            return "I'm sorry, but I don't have any information in my database that relates to your question."602 603        # Prepare context from filtered search results604        context_parts = []605        for i, result in enumerate(filtered_results[:3], 1):  # Use top 3 filtered results606            term = result.get('term', 'Unknown')607            definition = result.get('definition', 'No definition available')608            context_parts.append(f"[{i}] {term}: {definition}")609 610        context = "\n\n".join(context_parts)611 612        try:613            prompt = f"""You are a helpful assistant that answers questions based ONLY on the provided database information about PRMSU (President Ramon Magsaysay State University).614 615CRITICAL RULES:6161. Answer ONLY using information from the database entries below6172. Be SPECIFIC and TARGETED - provide the exact information that answers the question6183. Your response MUST be COMPLETE - never stop mid-sentence or leave answers incomplete6194. Extract and provide the relevant parts that directly answer the question6205. If the question cannot be answered with the provided information, say "I don't have that specific information in my database"6216. For numerical questions (GWA, percentages, counts, dates, hours), be precise with exact numbers6227. For policy questions, include the specific conditions or requirements asked about6238. For questions asking for multiple items, provide ALL items mentioned6249. Always end your response with proper punctuation (period, exclamation, or question mark)62510. Do not truncate your response - provide the full answer even if it's longer626 627RESPONSE TARGETING RULES:628- If asked about "vision statement" ONLY, provide only the vision, not mission or quality policy629- If asked about "mission statement" ONLY, provide only the mission, not vision or quality policy630- If asked about specific penalties, provide only those penalties, not entire disciplinary codes631- If asked about specific requirements, provide only those requirements, not entire admission processes632- If asked about specific timeframes, provide only those timeframes, not entire policies633- Extract the precise answer from longer database entries634 635SPECIAL HANDLING:636- For "PRMSU stands for" questions: Answer "President Ramon Magsaysay State University"637- For establishment date: Answer "April 20, 2018"638- For campus count: Answer "seven (7) campuses"639- For graduation honors GWA: Use graduation policies, not athlete requirements640- For attendance/lateness questions: Calculate carefully (e.g., 1.5 hours = 90 minutes, one-third = 30 minutes)641- For multi-conditional questions: Provide complete numbered lists when available642- For "why" questions: Look for policy rationales and explanations643 644DATABASE ENTRIES:645{context}646 647USER QUESTION: {query}648 649Based on the database entries above, provide the COMPLETE and FULL answer to the user's question. Make sure to include ALL relevant information and do not truncate your response:"""650 651            response = self.cohere_client.chat(652                model='command-r-08-2024',  # Latest stable model653                message=prompt,654                max_tokens=4000,  # Increased token limit for complete responses655                temperature=0.1,  # Slightly increased for more natural responses while maintaining consistency656            )657 658            ai_response = response.text.strip()659 660            # Improved response validation - less strict to avoid false negatives661            is_complete = (662                ai_response and663                len(ai_response.strip()) > 20 and  # Minimum meaningful length664                "don't have that specific information" not in ai_response.lower() and665                "i don't have" not in ai_response.lower() and666                not ai_response.strip().endswith(('and', 'or', 'the', 'of', 'in', 'to', 'for', 'with', 'by', 'from', 'as', 'at', 'on', 'are', 'is', 'was', 'were', 'have', 'has', 'had', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'must', 'shall', 'also', 'that', 'which', 'who', 'what', 'where', 'when', 'why', 'how', 'but', 'if', 'so', 'then', 'than', 'this', 'these', 'those', 'they', 'them', 'their'))667            )668 669            # Additional checks for obviously incomplete responses670            if ai_response:671                # Check if response ends abruptly with common incomplete patterns672                incomplete_endings = [673                    'the student must',674                    'requirements include',675                    'the policy states',676                    'according to',677                    'students are required',678                    'the university',679                    'prmsu requires',680                    'applicants must'681                ]682 683                response_lower = ai_response.lower().strip()684                if any(response_lower.endswith(ending) for ending in incomplete_endings):685                    is_complete = False686 687            # Additional validation for specific question types688            query_lower = query.lower()689            if any(word in query_lower for word in ['stands for', 'what does', 'acronym']):690                # For acronym questions, ensure we have the full name691                if 'president ramon magsaysay state university' in ai_response.lower():692                    is_complete = True693            elif any(word in query_lower for word in ['when', 'established', 'date']):694                # For date questions, ensure we have a year695                if any(year in ai_response for year in ['2018', '2017', '2019', '2020']):696                    is_complete = True697            elif any(word in query_lower for word in ['how many', 'number of', 'count']):698                # For counting questions, ensure we have numbers699                if any(num in ai_response.lower() for num in ['seven', '7', 'two', '2', 'fifteen', '15']):700                    is_complete = True701 702            # Validate that the AI response contains information from our database and is complete703            if is_complete:704                return ai_response705            else:706                # Use fallback method for incomplete or poor responses707                print("โš ๏ธ AI response was incomplete or poor quality, using fallback method")708                # Use the prioritized results from the search function709                prioritized_results = getattr(self, '_last_search_results', filtered_results)710                return self.create_fallback_response(query, prioritized_results)711 712        except Exception as e:713            print(f"โŒ AI analysis failed: {e}")714            # Use fallback method with prioritized results715            prioritized_results = getattr(self, '_last_search_results', filtered_results if filtered_results else search_results)716            return self.create_fallback_response(query, prioritized_results)717 718    def generate_response(self, query: str, search_results: List[Dict]) -> str:719        """Generate response using AI to understand the question and return accurate data."""720        if not search_results:721            return "I'm sorry, but I don't have any information in my database that relates to your question. Please ask about topics that are stored in the vector database."722 723        # Filter out very poor matches before processing724        good_matches = []725        for result in search_results:726            distance = result.get('distance', 1)727            similarity = 1 - distance if distance is not None else 0728            if similarity > 0.05:  # Only include reasonably good matches729                good_matches.append(result)730 731        # If no good matches, use the best available732        if not good_matches and search_results:733            good_matches = search_results[:1]734 735        if not good_matches:736            return "I'm sorry, but I don't have any information in my database that relates to your question."737 738        # Use AI to analyze the question and provide the best answer739        response = self.analyze_question_with_ai(query, good_matches)740 741        # Apply enhanced specificity to prevent truncation and improve targeting742        response = enhance_response_specificity(query, response, good_matches)743 744        # Store the good matches for potential fallback use745        self._last_good_matches = good_matches746 747        # Add similarity information for transparency only if similarity is very low748        # Use prioritized results if available749        prioritized_results = getattr(self, '_last_search_results', good_matches)750        best_match = prioritized_results[0] if prioritized_results else good_matches[0]751        similarity = 1 - best_match.get('distance', 1) if best_match.get('distance') else 0752 753        # Enhanced confidence scoring and warnings754        query_clean = query.lower().replace('what is ', '').replace('what are ', '').replace('define ', '').replace('the ', '').replace('tell me about ', '').replace('?', '').strip()755        term_lower = best_match.get('term', '').lower()756        definition_lower = best_match.get('definition', '').lower()757 758        # Check for different types of matches759        is_exact_match = term_lower == query_clean760        is_keyword_match = any(keyword in definition_lower for keyword in query_clean.split())761        is_university_info = any(keyword in query.lower() for keyword in ['prmsu', 'establishment', 'campus', 'stands for'])762 763        # Determine confidence level764        confidence_level = "high"765        if is_exact_match or is_university_info:766            confidence_level = "high"767        elif is_keyword_match and similarity > 0.3:768            confidence_level = "high"769        elif similarity > 0.2:770            confidence_level = "medium"771        elif similarity > 0.0:772            confidence_level = "low"773        else:774            confidence_level = "very low"775 776        # Remove similarity warning - keep response clean for Android app777 778        return response779    780    def chat_loop(self):781        """Main chat loop for the terminal interface."""782        print("๐Ÿ’ฌ Chat started! Type 'quit', 'exit', or 'bye' to end the conversation.")783        print("๐Ÿ“ Ask me anything about the definitions stored in your vector database.")784        print("-" * 60)785        print()786        787        while True:788            try:789                # Get user input790                user_input = input("You: ").strip()791                792                # Check for exit commands793                if user_input.lower() in ['quit', 'exit', 'bye', 'q']:794                    print("\n๐Ÿ‘‹ Goodbye! Thanks for chatting!")795                    break796                797                if not user_input:798                    print("Please enter a question or type 'quit' to exit.")799                    continue800                801                # Show thinking indicator802                print("๐Ÿค” Searching database and thinking...")803                804                # Search for relevant context805                search_results = self.search_relevant_context(user_input)806 807                # Generate response (now includes enhanced specificity)808                response = self.generate_response(user_input, search_results)809                810                # Display response811                print(f"\n๐Ÿค– Bot: {response}")812                813                # Show sources if available814                if search_results:815                    print(f"\n๐Ÿ“š Sources from database:")816                    for i, result in enumerate(search_results[:3], 1):  # Show top 3 sources817                        term = result.get('term', 'Unknown')818                        similarity = 1 - result.get('distance', 1) if result.get('distance') else 0819                        print(f"   {i}. {term} (similarity: {similarity:.2f})")820                821                print("\n" + "-" * 60)822                print()823                824            except KeyboardInterrupt:825                print("\n\n๐Ÿ‘‹ Goodbye! Thanks for chatting!")826                break827            except Exception as e:828                print(f"\nโŒ Error: {e}")829                print("Please try again or type 'quit' to exit.")830                print()831    832    def single_question(self, question: str):833        """Answer a single question and exit."""834        print(f"Question: {question}")835        print("๐Ÿค” Searching database and thinking...")836 837        # Search for relevant context838        search_results = self.search_relevant_context(question)839 840        # Generate response (now includes enhanced specificity)841        response = self.generate_response(question, search_results)842 843        # Display response844        print(f"\n๐Ÿค– Answer: {response}")845 846        # Show sources if available847        if search_results:848            print(f"\n๐Ÿ“š Sources from database:")849            for i, result in enumerate(search_results[:3], 1):850                term = result.get('term', 'Unknown')851                similarity = 1 - result.get('distance', 1) if result.get('distance') else 0852                print(f"   {i}. {term} (similarity: {similarity:.2f})")853 854 855def main():856    parser = argparse.ArgumentParser(description="Terminal chatbot for vector database queries")857    parser.add_argument("--api-key", default="F2kIZdCtAAHnfVYlPCfCdLBtEtxLyEzGQqTiRVnt", 858                       help="Cohere API key")859    parser.add_argument("--db-path", default="./vector_db", help="Path to vector database")860    parser.add_argument("--collection", default="definitions", help="Collection name")861    parser.add_argument("--question", help="Ask a single question and exit")862    863    args = parser.parse_args()864    865    try:866        # Initialize chatbot867        chatbot = VectorDatabaseChatbot(868            api_key=args.api_key,869            db_path=args.db_path,870            collection_name=args.collection871        )872        873        # Check if database has any data874        definitions = chatbot.chunker.list_all_definitions()875        if not definitions:876            print("โš ๏ธ  Warning: No definitions found in the vector database!")877            print("   Please add some definitions first using definition_chunker.py")878            return879        880        print(f"๐Ÿ“Š Database contains {len(definitions)} definitions")881        print()882        883        # Single question mode or chat loop884        if args.question:885            chatbot.single_question(args.question)886        else:887            chatbot.chat_loop()888            889    except Exception as e:890        print(f"โŒ Error initializing chatbot: {e}")891        print("Please check your API key and database path.")892 893 894if __name__ == "__main__":895    main()896