simRAG / app.py
Song
hi
a44c13d
import os
from pathlib import Path
from typing import List, Dict, Any
import gradio as gr
from langchain_community.document_loaders import TextLoader, PyPDFLoader, Docx2txtLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
# LLM 設定(強烈建議使用環境變數)
llm = ChatOpenAI(
base_url=os.getenv("LITELLM_BASE_URL"),
api_key=os.getenv("OPENAI_API_KEY"),
model=os.getenv("LLM_MODEL", "azure-gpt-4.1"), # 改用更常見的預設模型
temperature=0.3,
)
# Embedding 模型(中文效果很好的小模型)
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
prompt = PromptTemplate.from_template(
"""你是一個有幫助且誠實的助手,請根據以下提供的上下文來回答問題。
如果上下文不足以回答,請直接說「根據提供的文件,我無法回答這個問題。」,不要編造答案。
上下文:
{context}
問題:{question}
回答:"""
)
# 上傳並建立向量庫
def upload_and_build_db(files: List[Any], vectorstore_state: FAISS | None):
if not files:
return "請上傳至少一個文件。", None, vectorstore_state
docs = []
for file in files:
# Gradio 上傳的 file 是 tempfile.NamedTemporaryFile 物件,有 .name 屬性
file_path = file.name
try:
if file_path.lower().endswith(".pdf"):
loader = PyPDFLoader(file_path)
elif file_path.lower().endswith(".docx"):
loader = Docx2txtLoader(file_path)
else:
loader = TextLoader(file_path, encoding="utf-8")
loaded_docs = loader.load()
docs.extend(loaded_docs)
except Exception as e:
return f"載入檔案失敗:{os.path.basename(file_path)},錯誤:{str(e)}", None, vectorstore_state
if not docs:
return "沒有成功載入任何文件內容。", None, vectorstore_state
# 分塊
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
# 建立新向量庫
new_vectorstore = FAISS.from_documents(splits, embeddings)
success_msg = f"成功載入 {len(docs)} 個文件,共 {len(splits)} 個區塊,已建立專屬知識庫!現在可以開始提問。"
return success_msg, None, new_vectorstore
# RAG 回答(使用新版 Gradio messages 格式)
def rag_answer(question: str, history: List[Dict], vectorstore_state: FAISS | None):
if vectorstore_state is None:
new_message = {"role": "assistant", "content": "請先上傳文件並點擊「建立知識庫」。"}
return "", history + [new_message]
retriever = vectorstore_state.as_retriever(search_kwargs={"k": 4})
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
try:
response = chain.invoke(question)
except Exception as e:
response = f"回答時發生錯誤:{str(e)}"
# 新格式:加入 user 和 assistant 兩條訊息
new_history = history + [
{"role": "user", "content": question},
{"role": "assistant", "content": response}
]
return "", new_history
# 清除聊天記錄
def clear_chat():
return "", []
# Gradio 介面
with gr.Blocks(title="個人 RAG 問答系統", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 📚 個人 RAG 問答系統\n上傳你的 TXT、PDF、DOCX 文件,建立專屬知識庫,然後向它提問!")
vectorstore_state = gr.State(None)
with gr.Row():
file_input = gr.File(
label="上傳文件(支援 .txt、.pdf、.docx,可多檔)",
file_count="multiple",
type="filepath"
)
with gr.Row():
build_btn = gr.Button("建立知識庫", variant="primary", scale=1)
clear_btn = gr.Button("清除對話", variant="secondary", scale=1)
status = gr.Textbox(label="狀態訊息", interactive=False)
# 使用新版 messages 格式,明確初始化為空列表
chatbot = gr.Chatbot(
height=500,
value=[],
label="對話紀錄",
avatar_images=("https://em-content.zobj.net/source/apple/391/man-technologist_1f468-200d-1f4bb.png",
"https://em-content.zobj.net/source/apple/391/robot_1f916.png")
)
msg = gr.Textbox(
label="你的問題",
placeholder="在這裡輸入問題,按 Enter 送出...",
scale=7
)
# 事件綁定
build_btn.click(
fn=upload_and_build_db,
inputs=[file_input, vectorstore_state],
outputs=[status, file_input, vectorstore_state] # 清空檔案列表避免重複上傳
)
msg.submit(
fn=rag_answer,
inputs=[msg, chatbot, vectorstore_state],
outputs=[msg, chatbot]
)
clear_btn.click(
fn=clear_chat,
inputs=None,
outputs=[msg, chatbot]
)
# Hugging Face Spaces 建議加上 share=True 產生公開連結(本地測試可關閉)
demo.launch()