File size: 14,303 Bytes
da9a93e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
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)