trialback121/prmsu
0
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 