# ========================================== # MILESTONE 6: PRODUCTION READY PLATFORM # Multi-Format File Support (.pdf, .txt, .docx, .csv, .md) # ========================================== print("Installing required tools...") # ADDED: docx2txt to read Microsoft Word files import gradio as gr from huggingface_hub import InferenceClient from transformers import pipeline import numpy as np import librosa import traceback import torch import os import shutil import json import zipfile # --- UPGRADED RAG IMPORTS --- # We imported Docx2txtLoader and CSVLoader! from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader, CSVLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS # ------------------------------------------ # 0. DIRECTORIES & DYNAMIC CONFIGURATION # ------------------------------------------ DOC_DIR = "school_data" CONFIG_FILE = os.path.join(DOC_DIR, "config.json") EXPORT_DIR = "deployment_export" os.makedirs(DOC_DIR, exist_ok=True) os.makedirs(EXPORT_DIR, exist_ok=True) DEFAULT_CONFIG = { "llm_model": "Qwen/Qwen2.5-7B-Instruct", "prompt_school": "You are a school assistant. Answer using strictly this info:", "prompt_general": "You are a friendly general tutor. Answer the question nicely.", "max_audio_sec": 15, "max_file_mb": 5 } def load_config(): if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as f: return json.load(f) return DEFAULT_CONFIG def save_config(new_config): with open(CONFIG_FILE, 'w') as f: json.dump(new_config, f) if not os.path.exists(CONFIG_FILE): save_config(DEFAULT_CONFIG) # ------------------------------------------ # 1. RAG & AI INITIALIZATION (MULTI-FILE LOGIC) # ------------------------------------------ knowledge_base = None print("Downloading the Document Reading AI...") embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") def rebuild_knowledge_base(): global knowledge_base files = [f for f in os.listdir(DOC_DIR) if f != "config.json" and not f.startswith('.')] if len(files) == 0: knowledge_base = None return "⚠️ All files deleted. AI brain is empty." all_chunks = [] text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) for filename in files: filepath = os.path.join(DOC_DIR, filename) try: # --- THE MULTI-FORMAT ROUTER --- if filename.lower().endswith(".pdf"): loader = PyPDFLoader(filepath) elif filename.lower().endswith(".txt") or filename.lower().endswith(".md"): loader = TextLoader(filepath, encoding='utf-8') elif filename.lower().endswith(".docx"): loader = Docx2txtLoader(filepath) elif filename.lower().endswith(".csv"): loader = CSVLoader(filepath) else: print(f"Skipping unsupported file type: {filename}") continue all_chunks.extend(text_splitter.split_documents(loader.load())) except Exception as e: print(f"Error reading {filename}: {e}") if len(all_chunks) > 0: knowledge_base = FAISS.from_documents(all_chunks, embeddings) return f"✅ Brain updated! {len(files)} files loaded." knowledge_base = None return "⚠️ Files found, but no text extracted." my_secret_key = os.getenv("Token") client = InferenceClient(token=my_secret_key) compute_device = 0 if torch.cuda.is_available() else -1 local_whisper = pipeline("automatic-speech-recognition", model="openai/whisper-small", chunk_length_s=30, device=compute_device) rebuild_knowledge_base() # ------------------------------------------ # 2. CORE BACKEND LOGIC # ------------------------------------------ def get_file_list(): return [f for f in os.listdir(DOC_DIR) if f != "config.json" and not f.startswith('.')] def upload_files(files): if files is not None: config = load_config() max_bytes = config["max_file_mb"] * 1024 * 1024 for f in files: if os.path.getsize(f.name) > max_bytes: gr.Warning(f"Blocked: {os.path.basename(f.name)} exceeds {config['max_file_mb']}MB limit.") continue filename = os.path.basename(f.name) shutil.copy(f.name, os.path.join(DOC_DIR, filename)) gr.Info(rebuild_knowledge_base()) return gr.update(choices=get_file_list()), gr.update(value=None) def delete_selected_files(selected_filenames): if selected_filenames: for filename in selected_filenames: filepath = os.path.join(DOC_DIR, filename) if os.path.exists(filepath): os.remove(filepath) gr.Info(rebuild_knowledge_base()) return gr.update(choices=get_file_list(), value=[]) def download_selected_files(selected_filenames): if not selected_filenames: gr.Warning("Please select files to download first.") return gr.update(visible=False, value=None) return gr.update(visible=True, value=[os.path.join(DOC_DIR, f) for f in selected_filenames]) def transcribe_only(audio_input, language_choice): if audio_input is None: return "" try: config = load_config() original_sr, audio_array = audio_input audio_duration = len(audio_array) / original_sr if audio_duration > config["max_audio_sec"]: gr.Warning(f"Audio blocked! Exceeds the {config['max_audio_sec']} second limit.") return "" if audio_array.ndim > 1: audio_array = np.mean(audio_array, axis=1) audio_array = audio_array.astype(np.float32) if np.max(np.abs(audio_array)) > 1.0: audio_array = audio_array / 32768.0 if original_sr != 16000: audio_array = librosa.resample(y=audio_array, orig_sr=original_sr, target_sr=16000) ai_lang = "english" if language_choice == "English" else "chinese" result = local_whisper({"sampling_rate": 16000, "raw": audio_array}, generate_kwargs={"language": ai_lang, "task": "transcribe"}) return result['text'] except Exception: return "" def agent_chat(user_text, chat_history): global knowledge_base try: if not user_text or not str(user_text).strip(): gr.Warning("⚠️ 請輸入文字!") return "", chat_history config = load_config() user_input_str = str(user_text) # 強制轉成字串,防止意外 # --- 1. 路由判斷 (確保格式正確) --- router_prompt = f"Determine if this is SCHOOL or GENERAL: {user_input_str}" route_res = client.chat_completion( model=config["llm_model"], messages=[{"role": "user", "content": router_prompt}], # 這裡必須是字典 max_tokens=5 ) # --- 2. 取得 System Prompt --- route_verdict = route_res.choices[0].message.content.strip() if "SCHOOL" in route_verdict: if knowledge_base is not None: docs = knowledge_base.similarity_search(user_input_str, k=2) context = "\n".join([doc.page_content for doc in docs]) system_prompt = f"{config['prompt_school']}\nContext: {context}" else: system_prompt = "You are a school assistant. No documents found." else: system_prompt = config['prompt_general'] # --- 3. 關鍵修正:構建發送給 AI 的完整清單 --- # 我們在這裡明確定義 role 和 content,確保完全符合 API 要求 final_messages = [ {"role": "system", "content": str(system_prompt)} ] # 如果你想加入記憶(History),請用這個迴圈: for old_user, old_ai in chat_history[-5:]: # 取最近5次對話 final_messages.append({"role": "user", "content": str(old_user)}) final_messages.append({"role": "assistant", "content": str(old_ai)}) # 加入本次的問題 final_messages.append({"role": "user", "content": user_input_str}) # --- 4. 發送請求 --- final_response = client.chat_completion( model=config["llm_model"], messages=final_messages, # 傳入剛才建好的字典清單 max_tokens=200, temperature=0.5 ) ai_reply = final_response.choices[0].message.content chat_history.append((user_input_str, ai_reply)) return "", chat_history except Exception as e: print(f"DEBUG ERROR: {e}") # 在後台印出真正的錯誤原因 chat_history.append(("Error", f"⚠️ 格式錯誤或連線中斷: {str(e)}")) return "", chat_history # ------------------------------------------ # 3. ADMIN SETTINGS HELPERS # ------------------------------------------ def admin_login(u, p): if u == "admin" and p == "admin": return gr.update(visible=False), gr.update(visible=True) return gr.update(visible=True), gr.update(visible=False) def update_ai_settings(model, p_school, p_general): conf = load_config() final_model = model.strip() if model and model.strip() != "" else "Qwen/Qwen2.5-7B-Instruct" conf.update({"llm_model": final_model, "prompt_school": p_school, "prompt_general": p_general}) save_config(conf) gr.Info(f"✅ AI Config Saved! Model set to: {final_model}") return final_model def update_security_settings(audio_sec, file_mb): conf = load_config() conf.update({"max_audio_sec": audio_sec, "max_file_mb": file_mb}) save_config(conf) gr.Info("🛡️ Security Guards updated successfully!") def export_project(): zip_path = os.path.join(EXPORT_DIR, "school_bot_deploy.zip") req_text = "gradio\ntransformers\ntorch\nlibrosa\nsoundfile\ndocx2txt\nlangchain\nlangchain-community\nlangchain-huggingface\nlangchain-text-splitters\nfaiss-cpu\npypdf\nhuggingface_hub" with zipfile.ZipFile(zip_path, 'w') as zf: zf.writestr("requirements.txt", req_text) zf.writestr("README.md", "# Deployment Guide\n1. Upload this code to Hugging Face Spaces.\n2. Add your HF_TOKEN to the Space Secrets.\n3. Run app.py!") return gr.update(visible=True, value=zip_path) # ------------------------------------------ # 4. THE UI LAYOUT # ------------------------------------------ print("Building Final UI...") current_config = load_config() with gr.Blocks(theme=gr.themes.Soft()) as demo: with gr.Tab("💬 Student Chatbot"): gr.Markdown("# 🦸‍♂️ My Ultimate Hybrid AI Agent") chatbot = gr.Chatbot(height=400) with gr.Row(): with gr.Column(scale=3): txt_input = gr.Textbox(label="Type a message") with gr.Column(scale=2): lang_toggle = gr.Radio(choices=["English", "Chinese"], value="Chinese", label="Voice Language:") aud_input = gr.Audio(sources=["microphone"], type="numpy", label="Record Voice") with gr.Row(): submit_btn = gr.Button("🚀 Send to AI", variant="primary") clear_btn = gr.ClearButton([txt_input, aud_input, chatbot], value="🗑️ Clear Chat") aud_input.change(fn=transcribe_only, inputs=[aud_input, lang_toggle], outputs=[txt_input]) submit_btn.click(fn=agent_chat, inputs=[txt_input, chatbot], outputs=[txt_input, chatbot]) with gr.Tab("⚙️ Admin Settings"): with gr.Column(visible=True) as login_screen: gr.Markdown("### 🔒 Admin Login") with gr.Row(): usr_box = gr.Textbox(label="Username") pwd_box = gr.Textbox(label="Password", type="password") login_btn = gr.Button("Login", variant="primary") with gr.Column(visible=False) as admin_panel: gr.Markdown("## 🎛️ Platform Control Center") with gr.Tabs(): with gr.Tab("📂 Data Manager"): with gr.Row(): # THE UI UPGRADE: Now accepts .docx, .csv, and .md! upload_btn = gr.File( label="1. Upload File (Click or Drag & Drop)", file_count="multiple", type="filepath", file_types=[".pdf", ".txt", ".docx", ".csv", ".md"] ) with gr.Column(): download_btn = gr.Button("2. ⬇️ Download Selected") delete_btn = gr.Button("3. 🗑️ Delete Selected", variant="stop") download_output = gr.File(label="Click to Save", visible=False) gr.Markdown("---") with gr.Row(): gr.Markdown("### 📄 Database List View") select_all_btn = gr.Button("☑️ Select All", size="sm") file_listview = gr.CheckboxGroup(choices=get_file_list(), label="") with gr.Tab("🧠 AI Config & Prompts"): gr.Markdown("Change how the AI behaves in real-time without restarting the server.") ai_model_txt = gr.Textbox(value=current_config["llm_model"], label="Cloud LLM Model (Hugging Face ID)", placeholder="Leave blank to use default (Qwen/Qwen2.5-7B-Instruct)") prompt_school_txt = gr.TextArea(value=current_config["prompt_school"], label="School RAG System Prompt") prompt_general_txt = gr.TextArea(value=current_config["prompt_general"], label="General Conversation System Prompt") save_ai_btn = gr.Button("💾 Save AI Settings", variant="primary") with gr.Tab("🛡️ Security Guards"): gr.Markdown("Protect your server from spam and massive files.") audio_limit = gr.Slider(minimum=5, maximum=60, step=1, value=current_config["max_audio_sec"], label="Max Voice Recording Length (Seconds)") file_limit = gr.Slider(minimum=1, maximum=50, step=1, value=current_config["max_file_mb"], label="Max Upload File Size (MB)") save_sec_btn = gr.Button("💾 Save Security Settings", variant="primary") with gr.Tab("☁️ Cloud Deployment"): gr.Markdown("### Ready to go live 24/7?") gr.Markdown("Google Colab goes to sleep when you close your browser. To keep this bot online forever, export your deployment kit and upload it to a free host like **Hugging Face Spaces**.") export_btn = gr.Button("📦 Generate Deployment Kit (ZIP)", variant="primary") export_output = gr.File(label="Download your ZIP", visible=False) # WIRING login_btn.click(fn=admin_login, inputs=[usr_box, pwd_box], outputs=[login_screen, admin_panel]) upload_btn.upload(fn=upload_files, inputs=[upload_btn], outputs=[file_listview, upload_btn]) delete_btn.click(fn=delete_selected_files, inputs=[file_listview], outputs=[file_listview]) download_btn.click(fn=download_selected_files, inputs=[file_listview], outputs=[download_output]) def select_all(): return gr.update(value=get_file_list()) select_all_btn.click(fn=select_all, inputs=[], outputs=[file_listview]) save_ai_btn.click(fn=update_ai_settings, inputs=[ai_model_txt, prompt_school_txt, prompt_general_txt], outputs=[ai_model_txt]) save_sec_btn.click(fn=update_security_settings, inputs=[audio_limit, file_limit]) export_btn.click(fn=export_project, outputs=[export_output]) demo.launch(share=True)