|
import streamlit as st |
|
import json |
|
import os |
|
import requests |
|
import json |
|
import base64 |
|
from datetime import datetime, timedelta |
|
import subprocess |
|
from huggingface_hub import HfApi |
|
from pathlib import Path |
|
from calendar_rag import ( |
|
create_default_config, |
|
AcademicCalendarRAG, |
|
PipelineConfig, |
|
ModelConfig, |
|
RetrieverConfig, |
|
CacheConfig, |
|
ProcessingConfig, |
|
LocalizationConfig |
|
) |
|
|
|
def load_custom_css(): |
|
st.markdown(""" |
|
<style> |
|
/* General body styling */ |
|
body { |
|
font-family: "Arial", sans-serif !important; |
|
color: #000000 !important; |
|
background-color: white !important; |
|
line-height: 1.7 !important; |
|
} |
|
|
|
/* Main container styling */ |
|
.main { |
|
padding: 2rem; |
|
color: #000000; |
|
background-color: white; |
|
} |
|
|
|
/* Headers styling */ |
|
h1 { |
|
color: #000000; |
|
font-size: 2.8rem !important; |
|
font-weight: 700 !important; |
|
margin-bottom: 1.5rem !important; |
|
text-align: center; |
|
padding: 1rem 0; |
|
border-bottom: 3px solid #1E3A8A; |
|
} |
|
|
|
h3, h4 { |
|
color: #000000; |
|
font-weight: 600 !important; |
|
font-size: 1.6rem !important; |
|
margin-top: 1.5rem !important; |
|
} |
|
|
|
/* Chat message styling */ |
|
.chat-message { |
|
padding: 1.5rem; |
|
border-radius: 10px; |
|
margin: 1rem 0; |
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|
font-size: 1.1rem !important; |
|
line-height: 1.6 !important; |
|
font-family: "Arial", sans-serif !important; |
|
color: #000000 !important; |
|
} |
|
|
|
.user-message { |
|
background-color: #F3F4F6 !important; |
|
} |
|
|
|
.assistant-message { |
|
background-color: #EFF6FF !important; |
|
} |
|
|
|
/* Status indicators */ |
|
.status-indicator { |
|
padding: 0.5rem 1rem; |
|
border-radius: 6px; |
|
font-weight: 500; |
|
font-size: 1.2rem; |
|
color: #000000; |
|
} |
|
|
|
.status-online { |
|
background-color: #DEF7EC; |
|
color: #03543F; |
|
} |
|
|
|
.status-offline { |
|
background-color: #FDE8E8; |
|
color: rgb(255, 255, 255); |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
def clear_conversation_context(): |
|
"""Clear conversation context but keep chat display history""" |
|
|
|
if 'pipeline' in st.session_state and st.session_state.pipeline: |
|
st.session_state.pipeline.conversation_history = [] |
|
|
|
|
|
st.session_state.context_memory = [] |
|
|
|
|
|
|
|
def initialize_pipeline(): |
|
"""Initialize RAG pipeline with conversation memory support""" |
|
try: |
|
|
|
openai_api_key = os.getenv('OPENAI_API_KEY') or st.secrets['OPENAI_API_KEY'] |
|
|
|
|
|
config = create_default_config(openai_api_key) |
|
|
|
|
|
pipeline = AcademicCalendarRAG(config) |
|
|
|
|
|
try: |
|
with open("calendar.json", "r", encoding="utf-8") as f: |
|
raw_data = json.load(f) |
|
pipeline.load_data(raw_data) |
|
|
|
|
|
if 'context_memory' in st.session_state and st.session_state.context_memory: |
|
|
|
conversation_history = [] |
|
for item in st.session_state.context_memory: |
|
conversation_history.append({"role": "user", "content": item["query"]}) |
|
conversation_history.append({"role": "assistant", "content": item["response"]}) |
|
pipeline.conversation_history = conversation_history |
|
|
|
return pipeline |
|
except FileNotFoundError: |
|
st.error("calendar.json not found. Please ensure the file exists in the same directory.") |
|
return None |
|
|
|
except Exception as e: |
|
st.error(f"Error initializing pipeline: {str(e)}") |
|
return None |
|
|
|
|
|
def load_qa_history(): |
|
"""Load QA history directly from GitHub repository""" |
|
try: |
|
import requests |
|
import base64 |
|
import json |
|
|
|
|
|
REPO_OWNER = "jirasaksaimekJijo" |
|
REPO_NAME = "swu-chat-bot-project" |
|
FILE_PATH = "qa_history.json" |
|
GITHUB_TOKEN = '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' |
|
|
|
|
|
api_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{FILE_PATH}" |
|
headers = {"Accept": "application/vnd.github.v3+json"} |
|
|
|
if GITHUB_TOKEN: |
|
headers["Authorization"] = f"token {GITHUB_TOKEN}" |
|
|
|
|
|
response = requests.get(api_url, headers=headers) |
|
|
|
if response.status_code == 200: |
|
|
|
content_data = response.json() |
|
file_content = base64.b64decode(content_data["content"]).decode("utf-8") |
|
|
|
history_data = json.loads(file_content) |
|
return history_data |
|
else: |
|
st.warning(f"Failed to fetch QA history: {response.status_code} - {response.reason}") |
|
|
|
return [] |
|
|
|
except Exception as e: |
|
st.error(f"Error loading QA history from GitHub: {str(e)}") |
|
return [] |
|
|
|
def save_qa_history(history_entry): |
|
"""Save QA history entry to local JSON file and push to GitHub""" |
|
try: |
|
import requests |
|
import base64 |
|
import json |
|
from pathlib import Path |
|
|
|
|
|
REPO_OWNER = "jirasaksaimekJijo" |
|
REPO_NAME = "swu-chat-bot-project" |
|
FILE_PATH = "qa_history.json" |
|
GITHUB_TOKEN = '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' |
|
|
|
|
|
api_url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/contents/{FILE_PATH}" |
|
headers = { |
|
"Accept": "application/vnd.github.v3+json", |
|
"Authorization": f"token {GITHUB_TOKEN}" |
|
} |
|
|
|
|
|
response = requests.get(api_url, headers=headers) |
|
|
|
|
|
history_data = [] |
|
sha = None |
|
|
|
if response.status_code == 200: |
|
|
|
content_data = response.json() |
|
sha = content_data["sha"] |
|
|
|
try: |
|
|
|
file_content = base64.b64decode(content_data["content"]).decode("utf-8") |
|
|
|
if file_content.strip(): |
|
history_data = json.loads(file_content) |
|
|
|
|
|
if not isinstance(history_data, list): |
|
st.warning("Existing history data is not a list. Initializing new list.") |
|
history_data = [] |
|
except Exception as e: |
|
st.warning(f"Error parsing existing history: {e}. Initializing new list.") |
|
elif response.status_code == 404: |
|
|
|
st.info("Creating new QA history file.") |
|
else: |
|
st.error(f"Failed to check existing history: {response.status_code} - {response.reason}") |
|
|
|
|
|
if isinstance(history_entry, dict) and all(key in history_entry for key in ["timestamp", "query", "answer"]): |
|
|
|
if isinstance(history_entry["answer"], dict): |
|
history_entry["answer"] = history_entry["answer"].get('answer', str(history_entry["answer"])) |
|
|
|
elif hasattr(history_entry["answer"], 'content'): |
|
history_entry["answer"] = history_entry["answer"].content |
|
|
|
else: |
|
history_entry["answer"] = str(history_entry["answer"]) |
|
|
|
|
|
history_data.append(history_entry) |
|
|
|
|
|
try: |
|
local_path = Path("qa_history.json") |
|
with open(local_path, "w", encoding="utf-8") as f: |
|
json.dump(history_data, f, ensure_ascii=False, indent=2) |
|
except Exception as local_err: |
|
st.warning(f"Failed to save local backup: {local_err}") |
|
|
|
|
|
updated_content = json.dumps(history_data, ensure_ascii=False, indent=2) |
|
encoded_content = base64.b64encode(updated_content.encode('utf-8')).decode('utf-8') |
|
|
|
|
|
data = { |
|
"message": "Update QA history", |
|
"content": encoded_content, |
|
} |
|
|
|
if sha: |
|
data["sha"] = sha |
|
|
|
|
|
update_response = requests.put(api_url, headers=headers, json=data) |
|
|
|
if update_response.status_code in [200, 201]: |
|
return True |
|
else: |
|
st.error(f"Failed to update QA history: {update_response.status_code} - {update_response.text}") |
|
return False |
|
|
|
except Exception as e: |
|
import traceback |
|
st.error(f"Error in save_qa_history: {str(e)}") |
|
st.error(f"Traceback: {traceback.format_exc()}") |
|
return False |
|
|
|
def add_to_qa_history(query: str, answer: str): |
|
"""Add new QA pair to history with validation""" |
|
try: |
|
|
|
if not query or not answer: |
|
st.warning("Empty query or answer detected, skipping history update") |
|
return None |
|
|
|
|
|
if isinstance(answer, dict): |
|
|
|
processed_answer = answer.get('answer', str(answer)) |
|
elif hasattr(answer, 'content'): |
|
|
|
processed_answer = answer.content |
|
else: |
|
|
|
processed_answer = str(answer) |
|
|
|
|
|
history_entry = { |
|
"timestamp": (datetime.now() + timedelta(hours=5)).strftime("%Y-%m-%dT%H:%M:%S"), |
|
"query": query, |
|
"answer": processed_answer |
|
} |
|
|
|
|
|
save_qa_history(history_entry) |
|
return history_entry |
|
|
|
except Exception as e: |
|
st.error(f"Error in add_to_qa_history: {str(e)}") |
|
return None |
|
|
|
def add_to_history(role: str, message: str): |
|
"""Add message to chat history, save if it's a complete QA pair, and update context memory""" |
|
st.session_state.chat_history.append((role, message)) |
|
|
|
|
|
if role == "assistant" and len(st.session_state.chat_history) >= 2: |
|
|
|
user_query = st.session_state.chat_history[-2][1] |
|
|
|
|
|
history_entry = add_to_qa_history(user_query, message) |
|
|
|
|
|
if 'context_memory' not in st.session_state: |
|
st.session_state.context_memory = [] |
|
|
|
|
|
if isinstance(message, dict) and "answer" in message: |
|
response_content = message["answer"] |
|
else: |
|
response_content = message |
|
|
|
st.session_state.context_memory.append({ |
|
"query": user_query, |
|
"response": response_content, |
|
"timestamp": (datetime.now() + timedelta(hours=5)).strftime("%Y-%m-%dT%H:%M:%S") |
|
}) |
|
|
|
|
|
if len(st.session_state.context_memory) > 10: |
|
st.session_state.context_memory = st.session_state.context_memory[-10:] |
|
|
|
def display_chat_history(): |
|
"""Display chat history with improved document display""" |
|
for role, content in st.session_state.chat_history: |
|
if role == "user": |
|
st.markdown(f""" |
|
<div class="chat-message user-message"> |
|
<strong>🧑 คำถาม:</strong><br> |
|
{content} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
else: |
|
if isinstance(content, dict): |
|
assistant_response = content.get('answer', '❌ ไม่มีข้อมูลคำตอบ') |
|
st.markdown(f""" |
|
<div class="chat-message assistant-message"> |
|
<strong>🤖 คำตอบ:</strong><br> |
|
{assistant_response} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if content.get('documents'): |
|
with st.expander("📚 ข้อมูลอ้างอิง", expanded=False): |
|
for i, doc in enumerate(content['documents'], 1): |
|
st.markdown(f""" |
|
<div style="padding: 1rem; background-color: #000000; border-radius: 8px; margin: 0.5rem 0;"> |
|
<strong>เอกสารที่ {i}:</strong><br> |
|
{doc.content} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
else: |
|
st.markdown(f""" |
|
<div class="chat-message assistant-message"> |
|
<strong>🤖 คำตอบ:</strong><br> |
|
{content} |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
if 'context_memory' not in st.session_state: |
|
st.session_state.context_memory = [] |
|
|
|
def handle_submit(user_query: str): |
|
"""Enhanced query handling with improved conversation history tracking""" |
|
if not user_query: |
|
st.warning("⚠️ กรุณาระบุคำถาม") |
|
return |
|
|
|
user_query = user_query.strip() |
|
|
|
|
|
if not st.session_state.chat_history or st.session_state.chat_history[-1][1] != user_query: |
|
try: |
|
st.session_state.processing_query = True |
|
|
|
|
|
add_to_history("user", user_query) |
|
|
|
|
|
|
|
rag_conversation_history = [] |
|
history_to_include = st.session_state.chat_history[-11:] if len(st.session_state.chat_history) > 10 else st.session_state.chat_history |
|
|
|
for role, content in history_to_include: |
|
|
|
rag_role = "user" if role == "user" else "assistant" |
|
|
|
|
|
if isinstance(content, dict) and "answer" in content: |
|
rag_content = content["answer"] |
|
else: |
|
rag_content = content |
|
|
|
rag_conversation_history.append({"role": rag_role, "content": rag_content}) |
|
|
|
|
|
if 'context_memory' not in st.session_state: |
|
st.session_state.context_memory = [] |
|
|
|
|
|
with st.spinner("🔍 กำลังค้นหาคำตอบ..."): |
|
|
|
print(f"Processing query with {len(rag_conversation_history)} context messages") |
|
|
|
|
|
reference_keywords = ["ก่อนหน้านี้", "ก่อนหน้า", "ที่ผ่านมา", "คำถามก่อนหน้า", "คำถามที่แล้ว", |
|
"previous", "earlier", "before", "last time", "last question"] |
|
|
|
is_reference_question = any(keyword in user_query.lower() for keyword in reference_keywords) |
|
|
|
|
|
if is_reference_question and len(rag_conversation_history) >= 3: |
|
|
|
previous_questions = [msg["content"] for msg in rag_conversation_history[:-2] |
|
if msg["role"] == "user"] |
|
|
|
if previous_questions: |
|
prev_question = previous_questions[-1] |
|
enhanced_query = f"คำถามนี้อ้างอิงถึงคำถามก่อนหน้า '{prev_question}' โปรดพิจารณาบริบทนี้ในการตอบ: {user_query}" |
|
print(f"Enhanced reference query: {enhanced_query}") |
|
user_query = enhanced_query |
|
|
|
result = st.session_state.pipeline.process_query( |
|
query=user_query, |
|
conversation_history=rag_conversation_history |
|
) |
|
|
|
|
|
response_dict = { |
|
"answer": result.get("answer", ""), |
|
"documents": result.get("relevant_docs", []) |
|
} |
|
|
|
|
|
add_to_history("assistant", response_dict) |
|
|
|
|
|
st.session_state.context_memory.append({ |
|
"query": user_query, |
|
"response": response_dict["answer"], |
|
"timestamp": datetime.now().isoformat() |
|
}) |
|
|
|
except Exception as e: |
|
error_msg = f"❌ เกิดข้อผิดพลาด: {str(e)}" |
|
add_to_history("assistant", error_msg) |
|
st.error(f"Query processing error: {e}") |
|
|
|
finally: |
|
st.session_state.processing_query = False |
|
st.rerun() |
|
|
|
def create_chat_input(): |
|
"""Create chat input with enhanced configuration and combined clear button""" |
|
with st.form(key="chat_form", clear_on_submit=True): |
|
st.markdown(""" |
|
<label for="query_input" style="font-size: 1.2rem; font-weight: 600; margin-bottom: 1rem; display: block;"> |
|
<span style="color: #ffffff; border-left: 4px solid #ffffff; padding-left: 0.8rem;"> |
|
โปรดระบุคำถามเกี่ยวกับปฏิทินการศึกษา: |
|
</span> |
|
</label> |
|
""", unsafe_allow_html=True) |
|
|
|
query = st.text_input( |
|
"", |
|
key="query_input", |
|
placeholder="เช่น: วิชาเลือกมีอะไรบ้าง?" |
|
) |
|
|
|
col1, col2 = st.columns([5, 5]) |
|
|
|
with col1: |
|
submitted = st.form_submit_button( |
|
"📤 ส่งคำถาม", |
|
type="primary", |
|
use_container_width=True |
|
) |
|
|
|
with col2: |
|
clear_all_button = st.form_submit_button( |
|
"🗑️ ล้างประวัติและบริบทสนทนา", |
|
type="secondary", |
|
use_container_width=True |
|
) |
|
|
|
if submitted: |
|
handle_submit(query) |
|
|
|
if clear_all_button: |
|
|
|
st.session_state.chat_history = [] |
|
|
|
clear_conversation_context() |
|
st.info("ล้างประวัติและบริบทสนทนาแล้ว") |
|
st.rerun() |
|
|
|
def main(): |
|
|
|
st.set_page_config( |
|
page_title="Academic Calendar Assistant", |
|
page_icon="📅", |
|
layout="wide", |
|
initial_sidebar_state="collapsed" |
|
) |
|
|
|
|
|
load_custom_css() |
|
|
|
|
|
if 'pipeline' not in st.session_state: |
|
st.session_state.pipeline = None |
|
|
|
if 'chat_history' not in st.session_state: |
|
st.session_state.chat_history = [] |
|
|
|
if 'context_memory' not in st.session_state: |
|
st.session_state.context_memory = [] |
|
|
|
if 'processing_query' not in st.session_state: |
|
st.session_state.processing_query = False |
|
|
|
|
|
if st.session_state.pipeline is None: |
|
with st.spinner("กำลังเริ่มต้นระบบ..."): |
|
st.session_state.pipeline = initialize_pipeline() |
|
|
|
|
|
st.markdown(""" |
|
<div style="text-align: center; padding: 2rem 0;"> |
|
<h1>🎓 ผู้ช่วยค้นหาข้อมูลหลักสูตรและปฏิทินการศึกษา</h1> |
|
<p style="font-size: 1.2rem; color: #666;">บัณฑิตวิทยาลัย มหาวิทยาลัยศรีนครินทรวิโรฒ</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
chat_col, info_col = st.columns([7, 3]) |
|
|
|
with chat_col: |
|
display_chat_history() |
|
create_chat_input() |
|
|
|
|
|
with info_col: |
|
st.markdown(""" |
|
<div style="background-color: #F9FAFB; padding: 1.5rem; border-radius: 12px; margin-bottom: 2rem;"> |
|
<h3 style="color: #1E3A8A;">ℹ️ เกี่ยวกับระบบ</h3> |
|
<p style="color: #000000;"> |
|
ระบบนี้ใช้เทคโนโลยี <strong>RAG (Retrieval-Augmented Generation)</strong> |
|
ในการค้นหาและตอบคำถามเกี่ยวกับหลักสูตรและปฏิทินการศึกษา |
|
</p> |
|
<h4 style="color: #1E3A8A; margin-top: 1rem;">สามารถสอบถามข้อมูลเกี่ยวกับ:</h4> |
|
<ul style="list-style-type: none; padding-left: 0;"> |
|
<li style="color: #000000; margin-bottom: 0.5rem;">📚 รายวิชาในหลักสูตร</li> |
|
<li style="color: #000000; margin-bottom: 0.5rem;">📝 การลงทะเบียนเรียน</li> |
|
<li style="color: #000000; margin-bottom: 0.5rem;">📅 กำหนดการต่างๆ</li> |
|
<li style="color: #000000; margin-bottom: 0.5rem;">💰 ค่าธรรมเนียมการศึกษา</li> |
|
<li style="color: #000000; margin-bottom: 0.5rem;">📋 ขั้นตอนการสมัคร</li> |
|
</ul> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
st.markdown(""" |
|
<div style="background-color: #f9fafb; padding: 1.5rem; border-radius: 12px;"> |
|
<h3 style="color: #1E3A8A;">🔄 สถานะระบบ</h3> |
|
<div style="margin-top: 1rem;"> |
|
<p><strong style="color: #000000;">⏰ เวลาปัจจุบัน:</strong><br> |
|
<span style="color: #000000;">{}</span></p> |
|
<p><strong style="color: #000000;">📡 สถานะระบบ:</strong><br> |
|
<span class="status-indicator {}"> |
|
{} {} |
|
</span></p> |
|
</div> |
|
</div> |
|
""".format( |
|
(datetime.now() + timedelta(hours=5)).strftime('%Y-%m-%d %H:%M:%S'), |
|
"status-online" if st.session_state.pipeline else "status-offline", |
|
"🟢" if st.session_state.pipeline else "🔴", |
|
"พร้อมใช้งาน" if st.session_state.pipeline else "ไม่พร้อมใช้งาน" |
|
), unsafe_allow_html=True) |
|
|
|
if __name__ == "__main__": |
|
main() |