2. Install the required libraries by running this command in your terminal:
pip install gradio pandas requests
3. (Optional) Create a file named 'ESQRD.json' in the same directory as this script
to load your custom program data. If not found, the app will use example data.
4. Run the script from your terminal: python your_script_name.py
import json
import gradio as gr
import requests
import pandas as pd
from datetime import datetime
import re
from typing import List, Dict, Any, Tuple
import os
import socket
import warnings
class UMDAdvisingChatbot:
"""A highly polished AI chatbot for UMD Engineering with a fluid UI and preset system."""
def init(self, data_file_path: str):
"""Initialize the chatbot with UMD program data and evaluation system"""
self.program_data = self.load_program_data(data_file_path)
self.conversation_history = []
self.is_in_discovery_mode = False
self.discovery_step = 0
self.discovery_answers = []
self.discovery_questions = [
"First, do you prefer working with tangible, physical systems or with abstract concepts like data and software?",
"Are you more drawn to large-scale infrastructure projects (e.g., buildings, transport) or small-scale technology (e.g., electronics, nanomaterials)?",
"Does the idea of working with biological and chemical processes to solve medical or environmental problems excite you?",
"Are you motivated by creating new technologies, ensuring safety and reliability, or conducting fundamental scientific research?",
"Finally, describe a project you enjoyed working on, either in or out of school. What did you like about it?"
]
def load_program_data(self, json_file_path: str) -> List[Dict[str, Any]]:
try:
with open(json_file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
print("Warning: Custom program data not found or invalid. Using fallback data.")
return [
{"program_name": "Aerospace Engineering", "program_type": "Major", "career_focus": ["Aircraft Design", "Space Systems", "Propulsion"], "prerequisites": "Calculus II, Physics I, Chemistry I"},
{"program_name": "Computer Engineering", "program_type": "Major", "career_focus": ["Hardware Architecture", "Embedded Systems", "Cybersecurity"], "prerequisites": "Calculus II, Data Structures, Digital Logic"},
{"program_name": "Mechanical Engineering", "program_type": "Major", "career_focus": ["Robotics", "HVAC", "Manufacturing", "Automotive"], "prerequisites": "Calculus II, Physics I, Statics"},
{"program_name": "Bioengineering", "program_type": "Major", "career_focus": ["Biomedical Devices", "Genomic Engineering", "Biomaterials"], "prerequisites": "Calculus II, Biology I, Chemistry I"},
{"program_name": "Civil Engineering", "program_type": "Major", "career_focus": ["Structural Design", "Transportation Systems", "Geotechnical"], "prerequisites": "Calculus II, Physics I, Statics"},
{"program_name": "Environmental Engineering", "program_type": "Major", "career_focus": ["Water Resources", "Sustainability", "Air Pollution Control"], "prerequisites": "Calculus II, Chemistry I, Biology I"},
{"program_name": "Chemical Engineering", "program_type": "Major", "career_focus": ["Process Design", "Pharmaceuticals", "Energy Production"], "prerequisites": "Calculus II, Chemistry II, Thermodynamics"},
{"program_name": "Fire Protection Engineering", "program_type": "Major", "career_focus": ["Fire Dynamics", "Risk Analysis", "Building Safety Codes"], "prerequisites": "Calculus II, Physics I, Chemistry I"},
{"program_name": "Materials Science & Engineering", "program_type": "Major", "career_focus": ["Nanomaterials", "Polymer Science", "Metallurgy"], "prerequisites": "Calculus II, Chemistry I, Physics I"},
]
detected_categories = self.detect_prompt_categories(user_query)
system_prompt = self.create_system_prompt(detected_categories)
def detect_prompt_categories(self, prompt: str) -> List[str]:
"""Automatically detect prompt categories based on content"""
detected_categories = []
prompt_lower = prompt.lower()
for category, patterns in self.category_patterns.items():
for pattern in patterns:
if re.search(pattern, prompt_lower):
detected_categories.append(category)
break
return detected_categories if detected_categories else ['general']
def create_system_prompt(self, detected_categories: List[str]) -> str:
"""Creates the master system prompt with flawless engineering directives."""
return f"""
Persona: You are Shellton, a distinguished, expert academic advisor for the University of Maryland's A. James Clark School of Engineering. Your tone is professional, authoritative, and helpful. You are precise and factual.
**Core Directive:** Your primary goal is to provide accurate, relevant, and concise information about UMD's engineering programs based *only* on the context provided below.
**Mandatory Rules for Flawless Output:**
1. **Fact-Based Only:** Do NOT invent, assume, or hallucinate any information. If a detail is not in the program context, state that the specific detail is not available and recommend checking the official UMD website.
2. **Clarity and Structure:**
- Start with a direct answer to the user's primary question.
- Use **bolding** to highlight key terms and program names (e.g., "**Mechanical Engineering**").
- Use bullet points for lists or comparisons to ensure easy readability.
3. **Strictly Unbiased:** Your advice must be impartial. Do not make any assumptions based on user identity. Focus exclusively on academic and career interests.
4. **Use Provided Context:** All answers about programs MUST be derived from the "AVAILABLE UMD PROGRAMS CONTEXT" section. Do not use outside knowledge.
5. **Actionable Conclusion:** Conclude with a clear, actionable next step (e.g., "For a detailed curriculum, I recommend you visit the official **Aerospace Engineering** page on the UMD website.").
DETECTED PROMPT CHARACTERISTICS: {', '.join(detected_categories)}
RESPONSE GUIDELINES:
Fact-Based Only: Do NOT invent, assume, or hallucinate any information. If a detail is not in the program context, state that the specific detail is not available and recommend checking the official UMD website.
Clarity and Structure:
Start with a direct answer to the user's primary question.
Use bolding to highlight key terms and program names (e.g., "Mechanical Engineering").
Use bullet points for lists or comparisons to ensure easy readability.
Strictly Unbiased: Your advice must be impartial. Do not make any assumptions based on user identity. Focus exclusively on academic and career interests.
Use Provided Context: All answers about programs MUST be derived from the "AVAILABLE UMD PROGRAMS CONTEXT" section. Do not use outside knowledge.
Actionable Conclusion: Conclude with a clear, actionable next step (e.g., "For a detailed curriculum, I recommend you visit the official Aerospace Engineering page on the UMD website.").
AVAILABLE UMD PROGRAMS CONTEXT:
{self.get_program_context()}
"""
def get_program_context(self) -> str:
if not self.program_data: return "No program data is available."
return "\n".join([f"- {p.get('program_name', 'N/A')}: Focuses on {', '.join(p.get('career_focus', []))}. Key prerequisites include {p.get('prerequisites', 'N/A')}." for p in self.program_data])
def detect_bias(self, text: str) -> Tuple[bool, str]:
notes = {f"'{keyword}' (potential {cat} bias)" for cat, kws in self.bias_keywords.items() for keyword in kws if re.search(r'\b' + re.escape(keyword) + r'\b', text.lower())}
bias_found = False
bias_notes = []
text_lower = text.lower()
# Check for bias keywords
for category, keywords in self.bias_keywords.items():
for keyword in keywords:
if re.search(r'\b' + re.escape(keyword) + r'\b', text_lower):
# Enhanced stereotype detection
stereotype_patterns = [
(r"(?:girls|women|females?)\s+(?:are\s+)?(?:typically|usually|often|naturally)\s+(?:better|worse|good|bad)", "Gender stereotype"),
(r"(?:boys|men|males?)\s+(?:are\s+)?(?:typically|usually|often|naturally)\s+(?:better|worse|good|bad)", "Gender stereotype"),
(r"students?\s+like\s+you\s+(?:usually|typically|often)", "Identity-based assumption"),
(r"people\s+from\s+your\s+background", "Background assumption")
]
for pattern, description in stereotype_patterns:
if re.search(pattern, text_lower):
bias_notes.append(f"⚠️ {description} detected")
bias_found = True
return bias_found, " | ".join(bias_notes) if bias_notes else "✅ No bias detected"
def start_discovery(self):
self.is_in_discovery_mode = True
self.discovery_step = 0
self.discovery_answers = []
self.conversation_history = []
first_question = self.discovery_questions[0]
self.discovery_step += 1
return f"Hi there! I'm Shellton 🐢, your UMD Terrapin Engineering advisor. Let's find the right path for you. I will ask five questions to understand your interests.\n\n1. {first_question}", "🏷️ Mode: Guided Discovery"
def process_discovery_step(self, user_input: str) -> Tuple[str, str]:
self.discovery_answers.append(user_input)
if self.discovery_step < len(self.discovery_questions):
q_num = self.discovery_step + 1
next_question = self.discovery_questions[self.discovery_step]
self.discovery_step += 1
return f"Thank you. Let's continue.\n\n**{q_num}. {next_question}**", "🏷️ Mode: Guided Discovery"
else:
self.is_in_discovery_mode = False
q_and_a = "".join(f"Q: {q}\nA: {a}\n\n" for q, a in zip(self.discovery_questions, self.discovery_answers))
final_prompt = f"A prospective student has answered a questionnaire. Based only on their answers below, recommend the single best engineering major from the provided context and give a concise, 2-3 sentence explanation for your choice.\n\n{q_and_a}"
self.conversation_history.append({"role": "user", "content": "Based on my answers, which major do you recommend?"})
return self.generate_response(final_prompt, is_discovery_final=True)
def generate_response(self, user_query: str, is_discovery_final: bool = False) -> Tuple[str, str]:
if self.is_in_discovery_mode and not is_discovery_final: return self.process_discovery_step(user_query)
if not user_query.strip(): return "Please provide a question.", "🏷️ Status: Awaiting Input"
if MISTRAL_API_KEY == "your-mistral-api-key-here": return "API Key Not Configured.", "🏷️ Status: System Error"
if not is_discovery_final: self.conversation_history.append({"role": "user", "content": user_query})
if len(self.conversation_history) > 8: self.conversation_history = self.conversation_history[-8:]
try:
headers = {"Authorization": f"Bearer {MISTRAL_API_KEY}", "Content-Type": "application/json"}
# FIX: Detect categories and pass them to create_system_prompt
detected_categories = self.detect_prompt_categories(user_query)
system_prompt = self.create_system_prompt(detected_categories)
messages = [{"role": "system", "content": system_prompt}] + self.conversation_history
if is_discovery_final: messages.append({"role": "user", "content": user_query})
payload = {"model": "mistral-large-latest", "messages": messages, "max_tokens": 1024, "temperature": 0.2}
response = requests.post(MISTRAL_API_URL, headers=headers, json=payload, timeout=30)
if response.status_code == 200:
ai_response = response.json()['choices'][0]['message']['content'].strip()
self.conversation_history.append({"role": "assistant", "content": ai_response})
bias_present, bias_notes = self.detect_bias(ai_response)
if bias_present: ai_response += f"\n\n---\n> *System Note: This response included words that could be associated with bias ({bias_notes}). Please evaluate the advice based on its factual content.*"
categories = self.detect_prompt_categories(user_query)
return ai_response, f"🏷️ **Topic:** {', '.join(c.replace('_', ' ').title() for c in categories)}"
else:
return f"Error from AI service (Code: {response.status_code}).", f"🏷️ **Status:** API Error"
except Exception as e:
return f"A system error occurred: {e}", "🏷️ **Status:** System Error"
def get_conversation_log(self) -> str:
if not self.conversation_history: return "### Conversation log is empty."
log_str = "## 💬 Conversation Log\n---\n"
for msg in self.conversation_history:
role, content = msg.get('role', 'N/A').title(), msg.get('content', '')
if role == 'User': log_str += f"### ➡️ You:\n> {content}\n\n"
elif role == 'Assistant': log_str += f"### 🐢 Shellton:\n{content}\n\n---\n"
return log_str
def clear_conversation(self):
self.conversation_history = []
self.is_in_discovery_mode = False
return "🔄 New session started. I'm Shellton 🐢, your UMD Terrapin guide, ready to help you explore Engineering programs!", "🏷️ Status: Ready"
--- Gradio Interface Setup ---
chatbot = UMDAdvisingChatbot("ESQRD.json")
PROMPT_PRESETS = [
("Compare two majors", "Compare and contrast [Major 1] and [Major 2], focusing on career outcomes and required skills."),
("Find a major for my interest", "What is the best major for someone interested in [Your Interest, e.g., 'robotics' or 'sustainable energy']?"),
("Ask about career paths", "What are the typical career paths and starting salaries for a graduate in [Major Name]?"),
("Check prerequisites", "What are the key prerequisites for the [Major Name] program?"),
]
def create_system_prompt(self, detected_categories: List[str]) -> str:
"""Create enhanced system prompt with category-aware instructions"""
base_prompt = f"""You are an expert academic advisor for the University of Maryland (UMD) A. James Clark School of Engineering.
AVAILABLE UMD PROGRAMS CONTEXT:
{self.get_program_context()}
DO NOT INCLUDE ANY EMAIL OR CONTACT INFORMATION NOT IN THE ABOVE.
RESPONSE FORMATTING:
• Use clear headings and bullet points
• Include specific program names and requirements
• Mention relevant courses, prerequisites, and career paths
• Provide contact information and next steps
• Maintain an encouraging, professional tone
• Keep responses short but informative
CATEGORY-SPECIFIC INSTRUCTIONS:
"""
if 'affirmative' in detected_categories:
base_prompt += "- Match the student's enthusiasm while providing detailed guidance\n"
if 'negative' in detected_categories:
base_prompt += "- Provide extra encouragement and support resources\n"
if 'vague' in detected_categories:
base_prompt += "- Ask clarifying questions and provide broad overview options\n"
if 'specific' in detected_categories:
base_prompt += "- Give detailed, technical responses with specific requirements\n"
if 'identity_disclosure' in detected_categories:
base_prompt += "- Acknowledge their background positively and mention relevant support resources\n"
return base_prompt
--- Gradio Interface Setup ---
chatbot = UMDAdvisingChatbot("ESQRD.json")
PROMPT_PRESETS = [
("Compare two majors", "Compare and contrast [Major 1] and [Major 2], focusing on career outcomes and required skills."),
("Find a major for my interest", "What is the best major for someone interested in [Your Interest, e.g., 'robotics' or 'sustainable energy']?"),
("Ask about career paths", "What are the typical career paths and starting salaries for a graduate in [Major Name]?"),
("Check prerequisites", "What are the key prerequisites for the [Major Name] program?"),
]
def chatbot_interface(user_query: str) -> Tuple[str, str]:
if not user_query.strip(): return "Please enter a question.", "🏷️ Status: Awaiting Input"
response, categories = chatbot.generate_response(user_query)
return response, categories
--- ⭐ FINAL UI STYLING WITH FLUID UI & PRESETS ⭐ ---
with gr.Blocks(theme=None, css=custom_css) as demo:
gr.HTML(f"""
UMD Logo
UMD Engineering AI Advisor
Accurate, Factual Program Guidance
🐢
Chat with Shellton
""")
with gr.Tabs() as tabs:
with gr.TabItem("🎓 AI Advisor", id=0):
with gr.Row(equal_height=False):
with gr.Column(scale=3):
chatbot_output = gr.Markdown("Hi! I'm Shellton 🐢, your UMD Terrapin Engineering advisor. Please ask a specific question, use a preset, or start the guided discovery process.", elem_classes=["content-box", "chatbot-output"])
status_output = gr.Markdown("🏷️ Status: Ready", elem_classes="category-output")
user_input = gr.Textbox(label="Your Question:", placeholder="Ask Shellton anything about UMD Engineering programs...", elem_id="chat-input")
with gr.Row():
submit_btn = gr.Button("💬 Ask Shellton", variant="primary", elem_classes="action-button")
clear_btn = gr.Button("🔄 New Session", variant="secondary")
with gr.Column(scale=1):
gr.Markdown("### Unsure where to start?")
discovery_btn = gr.Button("🔎 Help Me Choose a Major", variant="primary", elem_classes="discovery-button")
gr.Markdown("### 🚀 Prompt Presets")
for label, value in PROMPT_PRESETS:
btn = gr.Button(label, elem_classes="preset-btn")
# ⭐ UX Change: This now updates the textbox instead of calling the chatbot directly.
btn.click(lambda v=value: v, outputs=[user_input])
with gr.TabItem("⚙️ System & Logs", id=1):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### System Status", elem_classes="content-box")
api_status = "✅ Connected" if MISTRAL_API_KEY != "your-mistral-api-key-here" else "❌ API Key Missing"
gr.Markdown(f"**Shellton's Status:** {api_status}\n\n**Model:** Mistral-Large (Accuracy Tuned)\n\n**Programs Loaded:** {len(chatbot.program_data)}", elem_classes="status-box")
show_log_btn = gr.Button("Show Conversation Log", elem_classes="action-button")
with gr.Column(scale=2):
conversation_log_output = gr.Markdown("Click the button to view your conversation with Shellton.", elem_classes=["content-box", "log-output"])