Deevyankar's picture
Update app.py
7ea2819 verified
Raw
History Blame Contribute Delete
26.5 kB
import os
import base64
import tempfile
import html
from datetime import datetime
import gradio as gr
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
DATA_DIR = "data"
LOGO_PATH = "utas_logo.png"
COURSE_NAME = "EGETS5241 - Energy Data Analytics"
PROGRAMME = "MSc Energy Transition and Sustainability"
UNIVERSITY = "University of Technology and Applied Sciences, Muscat"
MODEL_NAME = "gpt-4o-mini"
TEMPERATURE = 0.2
TOP_K = 2
GLOBAL_INDEX = None
BASE_RULES = """
You are an interactive Energy Data Analytics tutor for Master students at UTAS Muscat.
Course: EGETS5241 - Energy Data Analytics
Programme: MSc Energy Transition and Sustainability
Help students prepare for exams using the uploaded PPT/PDF/handouts.
You can help with:
- Detailed explanation
- Short answer questions
- Long answer questions
- Numerical questions
- MCQs and quizzes
- Case studies
- Flashcards
- Important final exam questions with answers
Use simple, clear English.
Ground answers mainly in the uploaded course material.
If something is not found in the uploaded course material, say:
"This point is not directly found in the uploaded course material, so I will explain it using general Energy Data Analytics knowledge."
"""
def get_logo_html():
if not os.path.exists(LOGO_PATH):
return "<div class='logo-box'><b>UTAS</b></div>"
with open(LOGO_PATH, "rb") as f:
encoded = base64.b64encode(f.read()).decode("utf-8")
return f"""
<div class="logo-box">
<img src="data:image/png;base64,{encoded}" class="logo-img">
</div>
"""
def get_files():
os.makedirs(DATA_DIR, exist_ok=True)
return [
f for f in os.listdir(DATA_DIR)
if not f.startswith(".")
and f.lower().endswith((".pdf", ".pptx", ".docx", ".txt", ".md"))
]
def load_index():
global GLOBAL_INDEX
if GLOBAL_INDEX is not None:
return GLOBAL_INDEX, ""
if not os.getenv("OPENAI_API_KEY"):
return None, "OPENAI_API_KEY is missing. Add it in Hugging Face Space settings."
files = get_files()
if not files:
return None, "No course file found. Upload your PPT/PDF inside the data folder."
try:
documents = SimpleDirectoryReader(DATA_DIR).load_data()
embed_model = OpenAIEmbedding(model="text-embedding-3-small")
GLOBAL_INDEX = VectorStoreIndex.from_documents(
documents,
embed_model=embed_model,
show_progress=False
)
return GLOBAL_INDEX, ""
except Exception as e:
return None, f"Error loading course files: {str(e)}"
def build_prompt(question, mode):
mode_instruction = {
"Explain in detail": "Explain the topic in detail with headings, simple examples, and energy-sector relevance.",
"Short answer": "Give a short exam-style answer in 5-7 clear lines.",
"Long answer": "Give a long exam-style answer with introduction, main points, example, and conclusion.",
"Numerical question": "Create or solve a numerical question. Show formula, values, substitution, calculation, final answer, and interpretation.",
"MCQ quiz": "Create 5 MCQs with 4 options each. Give correct answer and short explanation.",
"Case study": "Create or answer a case-study question using energy analytics context.",
"Flashcards": "Generate 10 flashcards in Q/A format.",
"Important exam questions": "Generate a complete exam preparation question bank with short-answer, long-answer, MCQ, numerical, and case-study questions with model answers.",
"Teacher may ask": "Give high-probability exam-focused questions from this topic and provide model answers."
}[mode]
return f"""
{BASE_RULES}
Selected mode:
{mode_instruction}
Student question/topic:
{question}
"""
def answer_question(question, mode):
index, error = load_index()
if index is None:
return error
llm = OpenAI(model=MODEL_NAME, temperature=TEMPERATURE)
query_engine = index.as_query_engine(
llm=llm,
similarity_top_k=TOP_K,
response_mode="compact"
)
response = query_engine.query(build_prompt(question, mode))
return str(response)
def update_mode(mode):
labels = {
"Explain in detail": "๐Ÿ“˜ Explain in detail",
"Short answer": "๐Ÿ“ Short answer",
"Long answer": "๐Ÿ“„ Long answer",
"Numerical question": "๐Ÿงฎ Numerical question",
"MCQ quiz": "โœ… MCQ quiz",
"Case study": "๐Ÿญ Case study",
"Flashcards": "๐Ÿง  Flashcards",
"Important exam questions": "๐Ÿ“š Question Bank",
"Teacher may ask": "๐ŸŽฏ Likely Exam Questions"
}
return mode, f"<div class='mode-chip'>{labels[mode]}</div>"
def respond(message, history, mode):
history = history or []
if not message or not message.strip():
return history, ""
history.append({"role": "user", "content": message})
try:
reply = answer_question(message, mode)
except Exception as e:
reply = f"Error: {str(e)}"
history.append({"role": "assistant", "content": reply})
return history, ""
def clean_text(content):
"""Clean Gradio chatbot message content for reports."""
if content is None:
return ""
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict):
parts.append(str(item.get("text", item.get("content", ""))))
else:
parts.append(str(item))
return " ".join([p for p in parts if p]).strip()
if isinstance(content, dict):
return str(content.get("text", content.get("content", ""))).strip()
return str(content).strip()
def _history_pairs(history):
history = history or []
pairs = []
current_user = None
for item in history:
if isinstance(item, dict):
role = item.get("role", "")
content = clean_text(item.get("content", ""))
if role == "user":
current_user = content
elif role == "assistant":
pairs.append((current_user or "", content))
current_user = None
elif isinstance(item, (list, tuple)) and len(item) == 2:
pairs.append((clean_text(item[0]), clean_text(item[1])))
return pairs
def export_premium_report(history):
pairs = _history_pairs(history)
if not pairs:
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = os.path.join(tempfile.gettempdir(), f"EGETS5241_Premium_Study_Report_{timestamp}.html")
report_time = datetime.now().strftime("%d %B %Y, %H:%M")
logo_tag = ""
if os.path.exists(LOGO_PATH):
try:
with open(LOGO_PATH, "rb") as f:
logo_encoded = base64.b64encode(f.read()).decode("utf-8")
logo_tag = f'<img src="data:image/png;base64,{logo_encoded}" class="report-logo" alt="UTAS Logo">'
except Exception:
logo_tag = ""
toc_items = ""
sections = ""
for i, (q, a) in enumerate(pairs, start=1):
q_clean = clean_text(q)
a_clean = clean_text(a)
q_safe = html.escape(q_clean).replace("\n", "<br>")
a_safe = html.escape(a_clean).replace("\n", "<br>")
short_title = html.escape(q_clean[:80] + ("..." if len(q_clean) > 80 else ""))
toc_items += f'<li><a href="#q{i}">Question {i}: {short_title}</a></li>'
sections += f"""
<section class="qa-card" id="q{i}">
<div class="qa-header">
<span class="qa-number">Q{i}</span>
<span class="qa-title">Student Question</span>
</div>
<div class="question-text">{q_safe}</div>
<details open>
<summary>Tutor Answer</summary>
<div class="answer-text">{a_safe}</div>
</details>
</section>
"""
html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>EGETS5241 Study Report</title>
<style>
:root {{
--burgundy: #850047;
--burgundy-dark: #5a0030;
--gold: #d6b94b;
--cream: #fff8dc;
--paper: #ffffff;
--text: #1f2933;
}}
body {{
font-family: Arial, sans-serif;
background: #f6f1e7;
color: var(--text);
margin: 0;
padding: 28px;
}}
.container {{
max-width: 1050px;
margin: auto;
background: var(--paper);
border-radius: 20px;
padding: 30px;
border: 2px solid var(--gold);
box-shadow: 0 12px 30px rgba(90,0,48,0.12);
}}
.header {{
background: linear-gradient(135deg, var(--burgundy-dark), var(--burgundy), #b8860b);
color: white;
padding: 28px;
border-radius: 18px;
border-top: 8px solid #f0c84b;
display: flex;
gap: 22px;
align-items: center;
margin-bottom: 24px;
}}
.report-logo {{
width: 160px;
background: white;
padding: 12px;
border-radius: 14px;
border: 1px solid var(--gold);
}}
.header-title h1 {{
margin: 0 0 8px 0;
font-size: 30px;
line-height: 1.2;
}}
.header-title p {{
margin: 4px 0;
font-size: 15px;
}}
.meta {{
background: var(--cream);
border-left: 7px solid #b8860b;
padding: 14px 18px;
border-radius: 12px;
margin-bottom: 22px;
font-size: 15px;
line-height: 1.6;
}}
.toc {{
background: #fffdf4;
border: 1.5px solid var(--gold);
border-radius: 14px;
padding: 18px 22px;
margin-bottom: 24px;
}}
.toc h2 {{
color: var(--burgundy);
margin-top: 0;
}}
.toc a {{
color: var(--burgundy);
text-decoration: none;
font-weight: bold;
}}
.toc li {{
margin-bottom: 8px;
line-height: 1.45;
}}
.qa-card {{
border: 1.5px solid var(--gold);
border-radius: 16px;
margin-bottom: 20px;
overflow: hidden;
page-break-inside: avoid;
background: white;
}}
.qa-header {{
background: var(--burgundy);
color: white;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
font-weight: bold;
}}
.qa-number {{
background: #f0c84b;
color: var(--burgundy-dark);
padding: 5px 9px;
border-radius: 999px;
}}
.question-text {{
background: var(--cream);
padding: 16px;
font-size: 16px;
line-height: 1.65;
font-weight: bold;
}}
summary {{
cursor: pointer;
background: #f0e5bf;
color: var(--burgundy);
font-weight: bold;
padding: 12px 16px;
font-size: 16px;
}}
.answer-text {{
padding: 18px;
font-size: 16px;
line-height: 1.75;
white-space: normal;
}}
.footer {{
margin-top: 28px;
padding-top: 14px;
border-top: 1px solid #e5d9ad;
color: #4b5563;
font-size: 13px;
text-align: center;
}}
.print-note {{
background: #edf7fb;
border: 1px solid #b7dbe8;
padding: 12px 16px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 14px;
}}
@media print {{
body {{
background: white;
padding: 0;
}}
.container {{
box-shadow: none;
border: none;
padding: 10px;
}}
.print-note {{
display: none;
}}
summary {{
list-style: none;
}}
summary::-webkit-details-marker {{
display: none;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
{logo_tag}
<div class="header-title">
<h1>โšก EGETS5241 - Energy Data Analytics Study Report</h1>
<p><b>MSc Energy Transition and Sustainability</b></p>
<p>University of Technology and Applied Sciences, Muscat</p>
</div>
</div>
<div class="meta">
<b>Generated on:</b> {report_time}<br>
<b>Total student interactions:</b> {len(pairs)}<br>
<b>Purpose:</b> Exam preparation and self-study record.
</div>
<div class="print-note">
<b>Tip:</b> To save this report as a polished PDF, open this file in your browser and press <b>Ctrl + P</b>, then choose <b>Save as PDF</b>.
</div>
<div class="toc">
<h2>Table of Contents</h2>
<ol>
{toc_items}
</ol>
</div>
{sections}
<div class="footer">
Generated by the EGETS5241 Energy Data Analytics Tutor.
</div>
</div>
</body>
</html>"""
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
return filepath
def export_excel(history):
pairs = _history_pairs(history)
if not pairs:
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = os.path.join(tempfile.gettempdir(), f"EGETS5241_Chat_Report_{timestamp}.xlsx")
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = Workbook()
ws = wb.active
ws.title = "Chat Report"
ws.merge_cells("A1:C1")
ws["A1"] = "EGETS5241 - Energy Data Analytics Chat Report"
ws["A1"].font = Font(bold=True, size=18, color="850047")
ws["A1"].alignment = Alignment(horizontal="center")
ws.merge_cells("A2:C2")
ws["A2"] = "MSc Energy Transition and Sustainability | UTAS Muscat"
ws["A2"].font = Font(italic=True, size=12, color="1F2933")
ws["A2"].alignment = Alignment(horizontal="center")
ws.append([])
ws.append(["No.", "Student Question", "Tutor Answer"])
header_row = 4
header_fill = PatternFill("solid", fgColor="850047")
header_font = Font(color="FFFFFF", bold=True)
border = Border(
left=Side(style="thin", color="D6B94B"),
right=Side(style="thin", color="D6B94B"),
top=Side(style="thin", color="D6B94B"),
bottom=Side(style="thin", color="D6B94B"),
)
for cell in ws[header_row]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = border
for i, (q, a) in enumerate(pairs, start=1):
ws.append([i, q, a])
for row in ws.iter_rows(min_row=5):
for cell in row:
cell.alignment = Alignment(wrap_text=True, vertical="top")
cell.border = border
fill_alt = PatternFill("solid", fgColor="FFF8DC")
for r in range(5, ws.max_row + 1):
if r % 2 == 1:
for c in range(1, 4):
ws.cell(r, c).fill = fill_alt
widths = [8, 45, 100]
for idx, width in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(idx)].width = width
ws.freeze_panes = "A5"
ws.auto_filter.ref = f"A4:C{ws.max_row}"
wb.save(filepath)
return filepath
except Exception:
fallback = filepath.replace(".xlsx", ".csv")
import csv
with open(fallback, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["No.", "Student Question", "Tutor Answer"])
for i, (q, a) in enumerate(pairs, start=1):
writer.writerow([i, q, a])
return fallback
def export_pdf(history):
pairs = _history_pairs(history)
if not pairs:
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = os.path.join(tempfile.gettempdir(), f"EGETS5241_Chat_Report_{timestamp}.pdf")
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.units import cm
doc = SimpleDocTemplate(
filepath,
pagesize=A4,
rightMargin=1.4 * cm,
leftMargin=1.4 * cm,
topMargin=1.3 * cm,
bottomMargin=1.3 * cm
)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"TitleCustom",
parent=styles["Title"],
textColor=colors.HexColor("#850047"),
fontSize=20,
leading=24,
alignment=1
)
sub_style = ParagraphStyle(
"SubTitleCustom",
parent=styles["BodyText"],
textColor=colors.HexColor("#1F2933"),
fontSize=11,
leading=15,
alignment=1
)
q_style = ParagraphStyle(
"Question",
parent=styles["BodyText"],
textColor=colors.HexColor("#850047"),
fontSize=11,
leading=15,
fontName="Helvetica-Bold"
)
a_style = ParagraphStyle(
"Answer",
parent=styles["BodyText"],
textColor=colors.HexColor("#1F2933"),
fontSize=10,
leading=14
)
story = []
story.append(Paragraph("EGETS5241 - Energy Data Analytics Chat Report", title_style))
story.append(Paragraph("MSc Energy Transition and Sustainability | UTAS Muscat", sub_style))
story.append(Paragraph(f"Generated on: {datetime.now().strftime('%d %B %Y, %H:%M')}", sub_style))
story.append(Spacer(1, 14))
for i, (q, a) in enumerate(pairs, start=1):
question = Paragraph(f"<b>Student Question {i}:</b><br/>{html.escape(str(q))}", q_style)
answer_text = html.escape(str(a)).replace("\n", "<br/>")
answer = Paragraph(f"<b>Tutor Answer:</b><br/>{answer_text}", a_style)
table = Table([[question], [answer]], colWidths=[17.5 * cm])
table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#FFF4C7")),
("BACKGROUND", (0, 1), (-1, 1), colors.white),
("BOX", (0, 0), (-1, -1), 0.75, colors.HexColor("#D6B94B")),
("INNERGRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#E8D89A")),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
]))
story.append(table)
story.append(Spacer(1, 10))
doc.build(story)
return filepath
except Exception:
fallback = filepath.replace(".pdf", ".txt")
with open(fallback, "w", encoding="utf-8") as f:
f.write("EGETS5241 - Energy Data Analytics Chat Report\n\n")
for i, (q, a) in enumerate(pairs, start=1):
f.write(f"Student Question {i}:\n{q}\n\nTutor Answer:\n{a}\n\n{'-'*70}\n\n")
return fallback
CSS = """
body, .gradio-container {
background: #f6f1e7 !important;
color: #1f2933 !important;
font-family: Arial, sans-serif !important;
font-size: 18px !important;
}
.gradio-container {
max-width: 1320px !important;
margin: auto !important;
}
.header {
background: linear-gradient(135deg, #5a0030, #850047, #b8860b);
color: white;
padding: 30px;
border-radius: 22px;
border-top: 7px solid #f0c84b;
box-shadow: 0 10px 25px rgba(90,0,48,0.25);
min-height: 165px;
display: flex;
flex-direction: column;
justify-content: center;
}
.header h1 {
font-size: 36px !important;
margin-bottom: 10px !important;
}
.header h3 {
font-size: 21px !important;
margin: 4px 0 !important;
}
.header p {
font-size: 16px !important;
margin: 4px 0 !important;
}
.header h1, .header h3, .header p {
color: white !important;
}
.logo-box {
background: white;
border: 2px solid #d6b94b;
border-radius: 20px;
padding: 18px;
text-align: center;
min-height: 165px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-img {
width: 340px;
max-width: 100%;
display: block;
margin: auto;
}
.info-box {
background: #fff8dc;
border: 2px solid #d6b94b;
border-left: 7px solid #b8860b;
padding: 16px;
border-radius: 14px;
color: #1f2933 !important;
font-size: 18px;
line-height: 1.65;
}
.info-box, .info-box * {
color: #1f2933 !important;
opacity: 1 !important;
}
.example-box {
background: #fff8dc;
border: 2px solid #d6b94b;
padding: 16px;
border-radius: 14px;
margin-bottom: 10px;
font-size: 18px;
}
.example-box, .example-box * {
color: #1f2933 !important;
opacity: 1 !important;
}
.mode-chip {
background: #850047;
color: white !important;
padding: 14px;
border-radius: 12px;
font-weight: bold;
text-align: center;
margin: 10px 0;
font-size: 18px;
}
button {
border-radius: 12px !important;
font-weight: bold !important;
font-size: 17px !important;
}
textarea, input {
background: white !important;
color: #1f2933 !important;
font-size: 18px !important;
}
#chatbox {
background: white !important;
border: 2px solid #d6b94b !important;
border-radius: 16px !important;
font-size: 18px !important;
line-height: 1.8 !important;
}
#chatbox * {
font-size: 18px !important;
line-height: 1.8 !important;
}
footer {
display: none !important;
}
"""
def main():
with gr.Blocks(css=CSS, title="EGETS5241 Energy Data Analytics Tutor") as demo:
with gr.Row(equal_height=True):
with gr.Column(scale=1, min_width=320):
gr.HTML(get_logo_html())
with gr.Column(scale=4):
gr.HTML(f"""
<div class="header">
<h1>โšก {COURSE_NAME}</h1>
<h3>{PROGRAMME}</h3>
<p>{UNIVERSITY}</p>
</div>
""")
gr.HTML("""
<div class="info-box">
<b>How to use:</b> Type your topic or question directly in the question box below, for example
<b>Smart Grid</b>, <b>SCADA</b>, <b>ARIMA</b>, or <b>Missing Values</b>.
Then choose the question type and click <b>Send</b>.
</div>
""")
with gr.Row():
with gr.Column(scale=1, min_width=330):
gr.Markdown("### Choose Question Type")
with gr.Row():
btn_explain = gr.Button("๐Ÿ“˜ Explain", variant="primary")
btn_short = gr.Button("๐Ÿ“ Short")
with gr.Row():
btn_long = gr.Button("๐Ÿ“„ Long")
btn_num = gr.Button("๐Ÿงฎ Numerical")
with gr.Row():
btn_mcq = gr.Button("โœ… MCQ")
btn_case = gr.Button("๐Ÿญ Case Study")
with gr.Row():
btn_flash = gr.Button("๐Ÿง  Flashcards")
btn_exam = gr.Button("๐Ÿ“š Question Bank", variant="primary")
mode = gr.State("Explain in detail")
mode_display = gr.HTML("<div class='mode-chip'>๐Ÿ“˜ Explain in detail</div>")
with gr.Column(scale=3):
gr.HTML("""
<div class="example-box">
<b>Example questions:</b>
<ul>
<li>Give me important final exam questions for Smart Grid.</li>
<li>Explain SCADA in simple words.</li>
<li>Create MCQs from data preprocessing.</li>
<li>Give one numerical question on load forecasting with answer.</li>
</ul>
</div>
""")
chatbot = gr.Chatbot(
label="Energy Data Analytics Chat",
height=660,
elem_id="chatbox"
)
msg = gr.Textbox(
label="Your topic or question",
placeholder="Example: Explain SARIMA in simple words / Give important exam questions for Smart Grid",
lines=3
)
with gr.Row():
send = gr.Button("Send", variant="primary")
clear = gr.Button("Clear")
gr.Markdown("### Download Interaction Report")
report_download = gr.DownloadButton(
label="๐Ÿ“„ Generate Premium Study Report",
value=None,
variant="primary"
)
send.click(respond, inputs=[msg, chatbot, mode], outputs=[chatbot, msg])
msg.submit(respond, inputs=[msg, chatbot, mode], outputs=[chatbot, msg])
clear.click(lambda: [], inputs=[], outputs=[chatbot])
report_download.click(export_premium_report, inputs=[chatbot], outputs=[report_download])
btn_explain.click(lambda: update_mode("Explain in detail"), inputs=[], outputs=[mode, mode_display])
btn_short.click(lambda: update_mode("Short answer"), inputs=[], outputs=[mode, mode_display])
btn_long.click(lambda: update_mode("Long answer"), inputs=[], outputs=[mode, mode_display])
btn_num.click(lambda: update_mode("Numerical question"), inputs=[], outputs=[mode, mode_display])
btn_mcq.click(lambda: update_mode("MCQ quiz"), inputs=[], outputs=[mode, mode_display])
btn_case.click(lambda: update_mode("Case study"), inputs=[], outputs=[mode, mode_display])
btn_flash.click(lambda: update_mode("Flashcards"), inputs=[], outputs=[mode, mode_display])
btn_exam.click(lambda: update_mode("Important exam questions"), inputs=[], outputs=[mode, mode_display])
try:
load_index()
except Exception:
pass
demo.launch()
if __name__ == "__main__":
main()