| | |
| |
|
| | import json |
| | import os |
| | import gradio as gr |
| | import faiss |
| | import torch |
| | from transformers import AutoTokenizer, AutoModelForCausalLM |
| | from sentence_transformers import SentenceTransformer |
| |
|
| | |
| | QA_FILE = "qa.json" |
| | TEXT_FILE = "web_data.txt" |
| | DOCS_FILE = "docs.json" |
| | VECTOR_FILE = "faiss_index.faiss" |
| | EMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" |
| | GEN_MODEL = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" |
| |
|
| | |
| | if not (os.path.exists(VECTOR_FILE) and os.path.exists(DOCS_FILE)): |
| | print("⚙️ 未偵測到向量資料庫,開始自動建構...") |
| | with open(TEXT_FILE, "r", encoding="utf-8") as f: |
| | content = f.read() |
| | docs = [chunk.strip() for chunk in content.split("\n\n") if chunk.strip()] |
| | embedder = SentenceTransformer(EMBED_MODEL) |
| | embeddings = embedder.encode(docs, show_progress_bar=True) |
| | index = faiss.IndexFlatL2(embeddings[0].shape[0]) |
| | index.add(embeddings) |
| | faiss.write_index(index, VECTOR_FILE) |
| | with open(DOCS_FILE, "w", encoding="utf-8") as f: |
| | json.dump(docs, f, ensure_ascii=False, indent=2) |
| | print("✅ 嵌入建構完成,共儲存段落:", len(docs)) |
| |
|
| | |
| | with open(QA_FILE, "r", encoding="utf-8") as f: |
| | qa_data = json.load(f) |
| | with open(DOCS_FILE, "r", encoding="utf-8") as f: |
| | docs = json.load(f) |
| | index = faiss.read_index(VECTOR_FILE) |
| | embedder = SentenceTransformer(EMBED_MODEL) |
| | tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL, trust_remote_code=True) |
| | model = AutoModelForCausalLM.from_pretrained(GEN_MODEL, trust_remote_code=True).to("cuda" if torch.cuda.is_available() else "cpu") |
| | model.eval() |
| |
|
| | |
| |
|
| | def retrieve_qa_context(user_input): |
| | for item in qa_data: |
| | if item["match"] == "OR": |
| | if any(k in user_input for k in item["keywords"]): |
| | return item["response"] |
| | elif item["match"] == "AND": |
| | if all(k in user_input for k in item["keywords"]): |
| | return item["response"] |
| | return None |
| |
|
| | |
| |
|
| | def search_context_faiss(user_input, top_k=3): |
| | vec = embedder.encode([user_input]) |
| | D, I = index.search(vec, top_k) |
| | return "\n".join([docs[i] for i in I[0] if i < len(docs)]) |
| |
|
| | |
| |
|
| | def generate_answer(user_input, context): |
| | prompt = f""" |
| | 你是一位了解南臺科技大學的智慧語音助理。請根據以下資料回答問題,僅用一至兩句話,以繁體中文表達,回答需清楚具體,不重複問題,不加入身份說明。 |
| | |
| | [範例格式] |
| | 問題:學校地址在哪裡? |
| | 回答:南臺科技大學位於台南市永康區南台街一號。 |
| | |
| | 問題:學校電話是多少? |
| | 回答:總機電話是 06-2533131,電機工程系分機為 3301。 |
| | |
| | 問題:電機工程系辦公室在哪? |
| | 回答:電機工程系辦公室位於 B 棟 B101。 |
| | |
| | 問題:電機工程系有哪些組別? |
| | 回答:電機系設有控制組、生醫電子系統組與電能資訊組三個方向。 |
| | |
| | 問題:學生社團活動如何? |
| | 回答:南臺有超過 80 個學生社團,涵蓋學術、康樂、服務、體育與藝術領域。 |
| | |
| | 問題:圖書館提供哪些服務? |
| | 回答:圖書館提供借書、自修空間、期刊查詢與電子資源服務。 |
| | |
| | 問題:師資如何? |
| | 回答:本校師資陣容堅強,擁有 30 多位教授、副教授與助理教授。 |
| | |
| | 問題:悠活館是做什麼的? |
| | 回答:悠活館是學生休閒與運動中心,設有羽球場、健身房、桌球室等設施。 |
| | |
| | 問題:怎麼到南臺科技大學? |
| | 回答:可從台南火車站搭乘公車,或經永康交流道開車約 10 分鐘抵達。 |
| | |
| | [資料] |
| | {context} |
| | |
| | [問題] |
| | {user_input} |
| | """ |
| | inputs = tokenizer(prompt, return_tensors="pt").to(model.device) |
| | outputs = model.generate(**inputs, max_new_tokens=150) |
| | response = tokenizer.decode(outputs[0], skip_special_tokens=True).strip() |
| | for line in response.splitlines()[::-1]: |
| | if len(line.strip()) > 10 and not line.startswith("你是"): |
| | return line.strip() |
| | return response[-90:] |
| |
|
| | |
| |
|
| | def answer(user_input): |
| | direct = retrieve_qa_context(user_input) |
| | if direct: |
| | return direct |
| | else: |
| | context = search_context_faiss(user_input) |
| | return generate_answer(user_input, context) |
| |
|
| | |
| | interface = gr.Interface( |
| | fn=answer, |
| | inputs=gr.Textbox(lines=2, placeholder="請輸入與南臺科技大學相關的問題..."), |
| | outputs="text", |
| | title="南臺科技大學 問答機器人(TinyLlama 1.1B)", |
| | description="支援 QA 關鍵字與語意檢索,自動建立嵌入庫,輸出繁體中文自然回答。", |
| | theme="default" |
| | ) |
| |
|
| | interface.launch() |