samceo07 commited on
Commit
da9a93e
·
verified ·
1 Parent(s): caa13b4

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from sklearn.feature_extraction.text import TfidfVectorizer
4
+ from sklearn.metrics.pairwise import cosine_similarity
5
+ import numpy as np
6
+ import gradio as gr # Import Gradio for the interface
7
+
8
+ # !! IMPORTANT: Replace "[Your Contact Number Here]" with your actual contact number !!
9
+ CONTACT_NUMBER = "+91-8977513427" # Replace with your actual contact number
10
+
11
+ # --- Data Loading ---
12
+ FILE_PATH = 'final_merged_course_data.json'
13
+ FULL_COURSE_DATA = {}
14
+
15
+ def load_data():
16
+ global FULL_COURSE_DATA
17
+ try:
18
+ with open(FILE_PATH, 'r') as f:
19
+ FULL_COURSE_DATA = json.load(f)
20
+ print(f"Course data loaded successfully from {FILE_PATH}!")
21
+ except FileNotFoundError:
22
+ print(f"Error: {FILE_PATH} not found. Please ensure it's in the same directory as this script.")
23
+ FULL_COURSE_DATA = {} # Initialize empty to prevent further errors
24
+ except json.JSONDecodeError as e:
25
+ print(f"Error decoding JSON from {FILE_PATH}: {e}")
26
+ FULL_COURSE_DATA = {}
27
+ except Exception as e:
28
+ print(f"An unexpected error occurred during data loading: {e}")
29
+ FULL_COURSE_DATA = {}
30
+
31
+ # Load the data when the script starts
32
+ load_data()
33
+
34
+ # --- Text Normalization and TF-IDF Setup ---
35
+
36
+ # Expanded set of common words to ignore (stop words for matching)
37
+ STOP_WORDS_FOR_MATCHING = set([
38
+ "a", "an", "the", "in", "for", "of", "on", "and", "or", "is", "what",
39
+ "how", "much", "long", "tell", "me", "about", "my", "i", "to", "get",
40
+ "know", "please", "can", "you", "any", "info", "information", "regarding",
41
+ "want", "looking", "do", "have", "program", "programs", "details",
42
+ "fee", "fees", "cost", "price", "duration", "length", "admission",
43
+ "study", "course", "degree", "degrees", "bachelor", "master", "diploma",
44
+ "honors", "with", "research", "general", "le", "pg", "post", "graduate",
45
+ "doctorate", "philosophy", "semesters", "semester", "years", "year",
46
+ "months", "month", "plus", "ne", "nep", # Common words related to structure/duration/type
47
+ "btech", "mtech", "b.tech", "m.tech", "b.com", "b.b.a", "b.c.a", "m.c.a",
48
+ "b.sc", "m.sc", "ll.b", "ll.m", "d.voc", "b.voc", "ph.d", "b.lis", "m.lis",
49
+ "d.lis", "b.p.e.s", "m.p.e.s", "mph", "bmlt", "brit", "bpt", "b.ph",
50
+ "d.pharmacy", "b.pharmacy", "advacne", "b.a", "b.e", "bsc", "msc", "bba", "bcom",
51
+ "engineering", "management", "science", "arts", "humanities", "social", "sciences",
52
+ "technology", "agricultural", "agriculture", "yoga", "pharmacy", "law", "paramedical",
53
+ "vocational", "phd", "library", "education", "journalism", "fashion", # Category names
54
+ "executive", "e", "s", "hons" # Common short forms that might be too general after full expansion
55
+ ])
56
+
57
+ # Expanded abbreviation map for normalization
58
+ ABBREVIATION_MAP = {
59
+ "cs": "computer science", "cse": "computer science engineering",
60
+ "ai": "artificial intelligence", "ml": "machine learning",
61
+ "it": "information technology", "hr": "human resource",
62
+ "hrm": "human resource management", "scm": "supply chain management",
63
+ "ib": "international business", "dm": "digital marketing",
64
+ "ui": "user interface", "ux": "user experience",
65
+ "vfx": "visual effects", "ar": "augmented reality", "vr": "virtual reality",
66
+ "iot": "internet things", "mlt": "medical lab technology",
67
+ "ott": "operation theater technology", "rit": "radiology imaginary technology",
68
+ "ece": "electronics communication engineering", "eee": "electronics electrical engineering",
69
+ "me": "mechanical engineering", "fshm": "fire safety hazard management",
70
+ "bnys": "bachelor naturopathy yogic sciences", "dnys": "diploma naturopathy yogic sciences",
71
+ "mpes": "physical education sports", "llis": "library information science",
72
+ "jmc": "journalism mass communication", "pcm": "physics chemistry mathematics",
73
+ "zbc": "zoology botany chemistry", "llm": "master laws"
74
+ }
75
+
76
+ def normalize_text_for_tfidf(text):
77
+ """
78
+ Cleans and normalizes text for TF-IDF.
79
+ Removes punctuation, applies abbreviation map, removes stop words.
80
+ Sorts unique words to create a canonical form.
81
+ """
82
+ if not isinstance(text, str):
83
+ return ""
84
+
85
+ text = re.sub(r'[^\w\s]', '', text).lower()
86
+ text = re.sub(r'\s+', ' ', text).strip()
87
+
88
+ words = []
89
+ for word in text.split():
90
+ words.append(ABBREVIATION_MAP.get(word, word))
91
+ text = ' '.join(words)
92
+
93
+ words = [word for word in text.split() if word not in STOP_WORDS_FOR_MATCHING]
94
+
95
+ return ' '.join(sorted(list(set(words))))
96
+
97
+ # --- Build TF-IDF Model ---
98
+ # Collect all normalized course names and variants to build the vocabulary.
99
+ all_normalized_course_names_for_tfidf = []
100
+ # This map will store the original display name and details dict for easy retrieval
101
+ normalized_name_to_original_map = {}
102
+
103
+ if FULL_COURSE_DATA:
104
+ for category_key, courses_in_category in FULL_COURSE_DATA.get("indian", {}).items():
105
+ for top_level_course_name, course_details_dict in courses_in_category.items():
106
+ # Add top-level course name
107
+ # Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
108
+ normalized_top_level = normalize_text_for_tfidf(top_level_course_name)
109
+ if normalized_top_level:
110
+ all_normalized_course_names_for_tfidf.append(normalized_top_level)
111
+ # Map back to original top_level_course_name and its full details_dict
112
+ if normalized_top_level not in normalized_name_to_original_map:
113
+ normalized_name_to_original_map[normalized_top_level] = {
114
+ "source_type": "top_level",
115
+ "original_display_name": top_level_course_name,
116
+ "details_dict_ref": course_details_dict # Reference to the dict holding "General" or specific types
117
+ }
118
+
119
+ # Add specific variants/specializations
120
+ for specific_type_name, details in course_details_dict.items():
121
+ # Formulate the raw name for the variant as it would ideally be displayed
122
+ full_course_variant_name_raw = f"{top_level_course_name} ({specific_type_name})" if specific_type_name != "General" else top_level_course_name
123
+
124
+ # Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
125
+ normalized_variant = normalize_text_for_tfidf(full_course_variant_name_raw)
126
+
127
+ if normalized_variant and normalized_variant not in normalized_name_to_original_map:
128
+ all_normalized_course_names_for_tfidf.append(normalized_variant)
129
+ # Map to the original raw variant name and its specific details
130
+ normalized_name_to_original_map[normalized_variant] = {
131
+ "source_type": "variant",
132
+ "original_display_name": full_course_variant_name_raw,
133
+ "details_ref": details # Reference to the specific details dict for this variant
134
+ }
135
+
136
+ # Initialize and fit TF-IDF Vectorizer
137
+ vectorizer = TfidfVectorizer(stop_words=None, ngram_range=(1, 2), min_df=1, max_df=0.9)
138
+
139
+ course_tfidf_matrix = None
140
+ if all_normalized_course_names_for_tfidf:
141
+ course_tfidf_matrix = vectorizer.fit_transform(all_normalized_course_names_for_tfidf)
142
+ print(f"TF-IDF model built with {len(all_normalized_course_names_for_tfidf)} unique normalized course names.")
143
+ else:
144
+ print("Warning: No normalized course names found to build TF-IDF model. Check data loading.")
145
+
146
+
147
+ # --- Core Logic for Course Detail Retrieval ---
148
+ def get_course_details(user_query: str):
149
+ """
150
+ Retrieves admission fee and duration for a given user query.
151
+ Uses TF-IDF vectorization and cosine similarity to find the best matching course.
152
+ """
153
+ if not FULL_COURSE_DATA or course_tfidf_matrix is None or not all_normalized_course_names_for_tfidf:
154
+ return None
155
+
156
+ # Corrected function call here: from normalize_text_for_matching to normalize_text_for_tfidf
157
+ normalized_user_query = normalize_text_for_tfidf(user_query)
158
+
159
+ if not normalized_user_query:
160
+ return None # Query was too generic or just stop words/punctuation
161
+
162
+ try:
163
+ query_vector = vectorizer.transform([normalized_user_query])
164
+ similarity_scores = cosine_similarity(query_vector, course_tfidf_matrix).flatten()
165
+
166
+ best_match_index = np.argmax(similarity_scores)
167
+ best_score = similarity_scores[best_match_index]
168
+
169
+ # Threshold to consider a match valid. Tune this value if needed.
170
+ SIMILARITY_THRESHOLD = 0.3 # Adjusted for broader matching with more stop words removed
171
+
172
+ if best_score < SIMILARITY_THRESHOLD:
173
+ return None # No sufficiently similar course found
174
+
175
+ # Retrieve information about the best matching course
176
+ matched_normalized_name = all_normalized_course_names_for_tfidf[best_match_index]
177
+ matched_data_info = normalized_name_to_original_map.get(matched_normalized_name)
178
+
179
+ if matched_data_info:
180
+ details = None
181
+ display_name = matched_data_info["original_display_name"]
182
+
183
+ if matched_data_info["source_type"] == "variant":
184
+ # If the best match was a specific variant, use its direct details
185
+ details = matched_data_info["details_ref"]
186
+ elif matched_data_info["source_type"] == "top_level":
187
+ # If best match was a top-level course name, try to find the 'General' variant
188
+ details_dict = matched_data_info["details_dict_ref"]
189
+ details = details_dict.get("General") or next(iter(details_dict.values()), None) # Fallback to first variant if no 'General'
190
+
191
+ display_name = matched_data_info["original_display_name"]
192
+ if details and details_dict.get("General") is None and len(details_dict) == 1:
193
+ # If it picked a top-level name and there's only ONE specific variant, use that variant's name
194
+ specific_type_name = list(details_dict.keys())[0]
195
+ display_name = f"{matched_data_info['original_display_name']} ({specific_type_name})"
196
+
197
+
198
+ if details:
199
+ return {
200
+ "course_name": display_name,
201
+ "admission_fee": details.get("admission_fee"),
202
+ "duration": details.get("duration")
203
+ }
204
+ except Exception as e:
205
+ pass # Fail gracefully if an error occurs during retrieval
206
+
207
+ return None # Fallback if no match or retrieval fails
208
+
209
+ def process_user_query(user_message: str):
210
+ """
211
+ Main function to process user input and generate a response.
212
+ Strictly provides admission fee, duration, and contact number.
213
+ """
214
+ user_message_lower = user_message.lower().strip()
215
+
216
+ # Initial Greeting/Help prompt (if the message is very short and just a greeting)
217
+ normalized_message_words_for_greeting = set(re.sub(r'[^\w\s]', '', user_message_lower).lower().split())
218
+ greetings_keywords = {"hello", "hi", "hey"}
219
+ if len(normalized_message_words_for_greeting.intersection(greetings_keywords)) > 0 and len(user_message_lower.split()) < 3:
220
+ return (f"Hello there! I can tell you the admission fee and duration for specific courses. "
221
+ f"What course are you interested in? For more details, please contact our admissions office at {CONTACT_NUMBER}.")
222
+
223
+ details = get_course_details(user_message)
224
+
225
+ if details:
226
+ admission_fee = details.get("admission_fee", "not specified")
227
+ duration = details.get("duration", "not specified")
228
+
229
+ # Add currency symbol if it's a numeric fee and not "Not Specified"
230
+ if isinstance(admission_fee, str) and admission_fee != "not specified":
231
+ if not admission_fee.startswith('₹') and not admission_fee.startswith('$') and re.match(r'^[\d,\.]+$', admission_fee):
232
+ admission_fee = f"₹{admission_fee}"
233
+
234
+ return (f"The admission fee for {details['course_name']} is {admission_fee} "
235
+ f"and the duration is {duration}. "
236
+ f"For more details, please contact our admissions office at {CONTACT_NUMBER}.")
237
+ else:
238
+ return (f"I couldn't find details for the course you mentioned. "
239
+ f"Please make sure you've typed the full and correct course name. "
240
+ f"I can tell you the admission fee and duration. "
241
+ f"For other inquiries, please contact our admissions office at {CONTACT_NUMBER}.")
242
+
243
+ # --- Gradio Interface Setup (for Hugging Face Spaces deployment) ---
244
+ # This is the entry point for your Hugging Face Space using Gradio
245
+ # Your requirements.txt should include:
246
+ # scikit-learn
247
+ # numpy
248
+ # gradio
249
+
250
+ import gradio as gr
251
+
252
+ # Define the Gradio interface
253
+ iface = gr.ChatInterface(
254
+ fn=process_user_query,
255
+ chatbot=gr.Chatbot(height=300),
256
+ textbox=gr.Textbox(placeholder="Ask about a course, fee, or duration...", container=False, scale=7),
257
+ theme="soft",
258
+ examples=[
259
+ "What is the admission fee for B.Tech. Computer Science & Engineering?",
260
+ "How long is the Master of Business Administration Finance course?",
261
+ "Cost of B.C.A. (Honors), B.C.A. (Honors with Research) General",
262
+ "Fee for LL.B. General",
263
+ "Tell me about PG Diploma Medical Lab Technology (MLT)",
264
+ "What's the admission cost for MBA Project Management?",
265
+ "Fee for B.Sc. Chemistry",
266
+ "What's the fee for Rocket Science PhD?"
267
+ ],
268
+ title="Course Information AI Assistant",
269
+ description="I can provide you with the admission fee and duration for specific courses. For other inquiries, please contact our admissions office.",
270
+ clear_btn="Clear Chat",
271
+ submit_btn="Send"
272
+ )
273
+
274
+ # This line is how Gradio launches the web UI.
275
+ # In Hugging Face Spaces, Gradio automatically detects this 'iface' variable
276
+ # or the iface.launch() call within app.py.
277
+ iface.launch(share=False)