AATM / app.py
samceo07's picture
Upload app.py
da9a93e verified
Raw
History Blame Contribute Delete
14.3 kB
import json
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import gradio as gr # Import Gradio for the interface
# !! IMPORTANT: Replace "[Your Contact Number Here]" with your actual contact number !!
CONTACT_NUMBER = "+91-8977513427" # Replace with your actual contact number
# --- Data Loading ---
FILE_PATH = 'final_merged_course_data.json'
FULL_COURSE_DATA = {}
def load_data():
global FULL_COURSE_DATA
try:
with open(FILE_PATH, 'r') as f:
FULL_COURSE_DATA = json.load(f)
print(f"Course data loaded successfully from {FILE_PATH}!")
except FileNotFoundError:
print(f"Error: {FILE_PATH} not found. Please ensure it's in the same directory as this script.")
FULL_COURSE_DATA = {} # Initialize empty to prevent further errors
except json.JSONDecodeError as e:
print(f"Error decoding JSON from {FILE_PATH}: {e}")
FULL_COURSE_DATA = {}
except Exception as e:
print(f"An unexpected error occurred during data loading: {e}")
FULL_COURSE_DATA = {}
# Load the data when the script starts
load_data()
# --- Text Normalization and TF-IDF Setup ---
# Expanded set of common words to ignore (stop words for matching)
STOP_WORDS_FOR_MATCHING = set([
"a", "an", "the", "in", "for", "of", "on", "and", "or", "is", "what",
"how", "much", "long", "tell", "me", "about", "my", "i", "to", "get",
"know", "please", "can", "you", "any", "info", "information", "regarding",
"want", "looking", "do", "have", "program", "programs", "details",
"fee", "fees", "cost", "price", "duration", "length", "admission",
"study", "course", "degree", "degrees", "bachelor", "master", "diploma",
"honors", "with", "research", "general", "le", "pg", "post", "graduate",
"doctorate", "philosophy", "semesters", "semester", "years", "year",
"months", "month", "plus", "ne", "nep", # Common words related to structure/duration/type
"btech", "mtech", "b.tech", "m.tech", "b.com", "b.b.a", "b.c.a", "m.c.a",
"b.sc", "m.sc", "ll.b", "ll.m", "d.voc", "b.voc", "ph.d", "b.lis", "m.lis",
"d.lis", "b.p.e.s", "m.p.e.s", "mph", "bmlt", "brit", "bpt", "b.ph",
"d.pharmacy", "b.pharmacy", "advacne", "b.a", "b.e", "bsc", "msc", "bba", "bcom",
"engineering", "management", "science", "arts", "humanities", "social", "sciences",
"technology", "agricultural", "agriculture", "yoga", "pharmacy", "law", "paramedical",
"vocational", "phd", "library", "education", "journalism", "fashion", # Category names
"executive", "e", "s", "hons" # Common short forms that might be too general after full expansion
])
# Expanded abbreviation map for normalization
ABBREVIATION_MAP = {
"cs": "computer science", "cse": "computer science engineering",
"ai": "artificial intelligence", "ml": "machine learning",
"it": "information technology", "hr": "human resource",
"hrm": "human resource management", "scm": "supply chain management",
"ib": "international business", "dm": "digital marketing",
"ui": "user interface", "ux": "user experience",
"vfx": "visual effects", "ar": "augmented reality", "vr": "virtual reality",
"iot": "internet things", "mlt": "medical lab technology",
"ott": "operation theater technology", "rit": "radiology imaginary technology",
"ece": "electronics communication engineering", "eee": "electronics electrical engineering",
"me": "mechanical engineering", "fshm": "fire safety hazard management",
"bnys": "bachelor naturopathy yogic sciences", "dnys": "diploma naturopathy yogic sciences",
"mpes": "physical education sports", "llis": "library information science",
"jmc": "journalism mass communication", "pcm": "physics chemistry mathematics",
"zbc": "zoology botany chemistry", "llm": "master laws"
}
def normalize_text_for_tfidf(text):
"""
Cleans and normalizes text for TF-IDF.
Removes punctuation, applies abbreviation map, removes stop words.
Sorts unique words to create a canonical form.
"""
if not isinstance(text, str):
return ""
text = re.sub(r'[^\w\s]', '', text).lower()
text = re.sub(r'\s+', ' ', text).strip()
words = []
for word in text.split():
words.append(ABBREVIATION_MAP.get(word, word))
text = ' '.join(words)
words = [word for word in text.split() if word not in STOP_WORDS_FOR_MATCHING]
return ' '.join(sorted(list(set(words))))
# --- Build TF-IDF Model ---
# Collect all normalized course names and variants to build the vocabulary.
all_normalized_course_names_for_tfidf = []
# This map will store the original display name and details dict for easy retrieval
normalized_name_to_original_map = {}
if FULL_COURSE_DATA:
for category_key, courses_in_category in FULL_COURSE_DATA.get("indian", {}).items():
for top_level_course_name, course_details_dict in courses_in_category.items():
# Add top-level course name
# Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
normalized_top_level = normalize_text_for_tfidf(top_level_course_name)
if normalized_top_level:
all_normalized_course_names_for_tfidf.append(normalized_top_level)
# Map back to original top_level_course_name and its full details_dict
if normalized_top_level not in normalized_name_to_original_map:
normalized_name_to_original_map[normalized_top_level] = {
"source_type": "top_level",
"original_display_name": top_level_course_name,
"details_dict_ref": course_details_dict # Reference to the dict holding "General" or specific types
}
# Add specific variants/specializations
for specific_type_name, details in course_details_dict.items():
# Formulate the raw name for the variant as it would ideally be displayed
full_course_variant_name_raw = f"{top_level_course_name} ({specific_type_name})" if specific_type_name != "General" else top_level_course_name
# Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
normalized_variant = normalize_text_for_tfidf(full_course_variant_name_raw)
if normalized_variant and normalized_variant not in normalized_name_to_original_map:
all_normalized_course_names_for_tfidf.append(normalized_variant)
# Map to the original raw variant name and its specific details
normalized_name_to_original_map[normalized_variant] = {
"source_type": "variant",
"original_display_name": full_course_variant_name_raw,
"details_ref": details # Reference to the specific details dict for this variant
}
# Initialize and fit TF-IDF Vectorizer
vectorizer = TfidfVectorizer(stop_words=None, ngram_range=(1, 2), min_df=1, max_df=0.9)
course_tfidf_matrix = None
if all_normalized_course_names_for_tfidf:
course_tfidf_matrix = vectorizer.fit_transform(all_normalized_course_names_for_tfidf)
print(f"TF-IDF model built with {len(all_normalized_course_names_for_tfidf)} unique normalized course names.")
else:
print("Warning: No normalized course names found to build TF-IDF model. Check data loading.")
# --- Core Logic for Course Detail Retrieval ---
def get_course_details(user_query: str):
"""
Retrieves admission fee and duration for a given user query.
Uses TF-IDF vectorization and cosine similarity to find the best matching course.
"""
if not FULL_COURSE_DATA or course_tfidf_matrix is None or not all_normalized_course_names_for_tfidf:
return None
# Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
normalized_user_query = normalize_text_for_tfidf(user_query)
if not normalized_user_query:
return None # Query was too generic or just stop words/punctuation
try:
query_vector = vectorizer.transform([normalized_user_query])
similarity_scores = cosine_similarity(query_vector, course_tfidf_matrix).flatten()
best_match_index = np.argmax(similarity_scores)
best_score = similarity_scores[best_match_index]
# Threshold to consider a match valid. Tune this value if needed.
SIMILARITY_THRESHOLD = 0.3 # Adjusted for broader matching with more stop words removed
if best_score < SIMILARITY_THRESHOLD:
return None # No sufficiently similar course found
# Retrieve information about the best matching course
matched_normalized_name = all_normalized_course_names_for_tfidf[best_match_index]
matched_data_info = normalized_name_to_original_map.get(matched_normalized_name)
if matched_data_info:
details = None
display_name = matched_data_info["original_display_name"]
if matched_data_info["source_type"] == "variant":
# If the best match was a specific variant, use its direct details
details = matched_data_info["details_ref"]
elif matched_data_info["source_type"] == "top_level":
# If best match was a top-level course name, try to find the 'General' variant
details_dict = matched_data_info["details_dict_ref"]
details = details_dict.get("General") or next(iter(details_dict.values()), None) # Fallback to first variant if no 'General'
display_name = matched_data_info["original_display_name"]
if details and details_dict.get("General") is None and len(details_dict) == 1:
# If it picked a top-level name and there's only ONE specific variant, use that variant's name
specific_type_name = list(details_dict.keys())[0]
display_name = f"{matched_data_info['original_display_name']} ({specific_type_name})"
if details:
return {
"course_name": display_name,
"admission_fee": details.get("admission_fee"),
"duration": details.get("duration")
}
except Exception as e:
pass # Fail gracefully if an error occurs during retrieval
return None # Fallback if no match or retrieval fails
def process_user_query(user_message: str):
"""
Main function to process user input and generate a response.
Strictly provides admission fee, duration, and contact number.
"""
user_message_lower = user_message.lower().strip()
# Initial Greeting/Help prompt (if the message is very short and just a greeting)
normalized_message_words_for_greeting = set(re.sub(r'[^\w\s]', '', user_message_lower).lower().split())
greetings_keywords = {"hello", "hi", "hey"}
if len(normalized_message_words_for_greeting.intersection(greetings_keywords)) > 0 and len(user_message_lower.split()) < 3:
return (f"Hello there! I can tell you the admission fee and duration for specific courses. "
f"What course are you interested in? For more details, please contact our admissions office at {CONTACT_NUMBER}.")
details = get_course_details(user_message)
if details:
admission_fee = details.get("admission_fee", "not specified")
duration = details.get("duration", "not specified")
# Add currency symbol if it's a numeric fee and not "Not Specified"
if isinstance(admission_fee, str) and admission_fee != "not specified":
if not admission_fee.startswith('₹') and not admission_fee.startswith('$') and re.match(r'^[\d,\.]+$', admission_fee):
admission_fee = f"₹{admission_fee}"
return (f"The admission fee for {details['course_name']} is {admission_fee} "
f"and the duration is {duration}. "
f"For more details, please contact our admissions office at {CONTACT_NUMBER}.")
else:
return (f"I couldn't find details for the course you mentioned. "
f"Please make sure you've typed the full and correct course name. "
f"I can tell you the admission fee and duration. "
f"For other inquiries, please contact our admissions office at {CONTACT_NUMBER}.")
# --- Gradio Interface Setup (for Hugging Face Spaces deployment) ---
# This is the entry point for your Hugging Face Space using Gradio
# Your requirements.txt should include:
# scikit-learn
# numpy
# gradio
import gradio as gr
# Define the Gradio interface
iface = gr.ChatInterface(
fn=process_user_query,
chatbot=gr.Chatbot(height=300),
textbox=gr.Textbox(placeholder="Ask about a course, fee, or duration...", container=False, scale=7),
theme="soft",
examples=[
"What is the admission fee for B.Tech. Computer Science & Engineering?",
"How long is the Master of Business Administration Finance course?",
"Cost of B.C.A. (Honors), B.C.A. (Honors with Research) General",
"Fee for LL.B. General",
"Tell me about PG Diploma Medical Lab Technology (MLT)",
"What's the admission cost for MBA Project Management?",
"Fee for B.Sc. Chemistry",
"What's the fee for Rocket Science PhD?"
],
title="Course Information AI Assistant",
description="I can provide you with the admission fee and duration for specific courses. For other inquiries, please contact our admissions office.",
clear_btn="Clear Chat",
submit_btn="Send"
)
# This line is how Gradio launches the web UI.
# In Hugging Face Spaces, Gradio automatically detects this 'iface' variable
# or the iface.launch() call within app.py.
iface.launch(share=False)