File size: 5,539 Bytes
a44c13d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
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() |