File size: 12,003 Bytes
5d1e7cd 3ea2e54 5d1e7cd 3ea2e54 c35ccd6 e910cb2 c35ccd6 e910cb2 5d1e7cd c35ccd6 5d1e7cd c35ccd6 e910cb2 5d1e7cd c35ccd6 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd c35ccd6 dd40b1e 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 c35ccd6 e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd c35ccd6 e910cb2 5d1e7cd e910cb2 5d1e7cd c35ccd6 dd40b1e 5d1e7cd e910cb2 5d1e7cd c35ccd6 dd40b1e c35ccd6 5d1e7cd e910cb2 5d1e7cd dd40b1e e910cb2 5d1e7cd 3ea2e54 28b6c57 3ea2e54 e910cb2 5d1e7cd e910cb2 3ea2e54 5d1e7cd e910cb2 5d1e7cd e910cb2 5d1e7cd e910cb2 c61dc19 dd40b1e 5d1e7cd e910cb2 5d1e7cd dd40b1e 5d1e7cd dd40b1e 5d1e7cd dd40b1e 5d1e7cd dd40b1e 5d1e7cd 3ea2e54 5d1e7cd dd40b1e 5d1e7cd 3ea2e54 |
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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# app.py
import os
import openai
import gradio as gr
from langchain.chains import ConversationalRetrievalChain
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import PyMuPDFLoader, PyPDFLoader
from langchain.vectorstores import Chroma
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.chat_models import ChatOpenAI
import shutil # 用於文件複製
import logging
# 設置日誌配置
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 獲取 OpenAI API 密鑰(初始不使用固定密鑰)
api_key_env = os.getenv("OPENAI_API_KEY")
if api_key_env:
openai.api_key = api_key_env
logger.info("OpenAI API 密鑰已設置。")
else:
logger.info("未設置固定的 OpenAI API 密鑰。將使用使用者提供的密鑰。")
# 確保向量資料庫目錄存在且有寫入權限
VECTORDB_DIR = os.path.abspath("./data")
os.makedirs(VECTORDB_DIR, exist_ok=True)
os.chmod(VECTORDB_DIR, 0o755) # 設置適當的權限
logger.info(f"VECTORDB_DIR set to: {VECTORDB_DIR}")
# 定義測試 PDF 加載器的函數
def test_pdf_loader(file_path, loader_type='PyMuPDFLoader'):
logger.info(f"Testing PDF loader ({loader_type}) with file: {file_path}")
try:
if loader_type == 'PyMuPDFLoader':
loader = PyMuPDFLoader(file_path)
elif loader_type == 'PyPDFLoader':
loader = PyPDFLoader(file_path)
else:
logger.error(f"Unknown loader type: {loader_type}")
return
loaded_docs = loader.load()
if loaded_docs:
logger.info(f"Successfully loaded {file_path} with {len(loaded_docs)} documents.")
logger.info(f"Document content (first 500 chars): {loaded_docs[0].page_content[:500]}")
else:
logger.error(f"No documents loaded from {file_path}.")
except Exception as e:
logger.error(f"Error loading {file_path} with {loader_type}: {e}")
# 定義載入和處理 PDF 文件的函數
def load_and_process_documents(file_paths, loader_type='PyMuPDFLoader', api_key=None):
if not api_key:
raise ValueError("未提供 OpenAI API 密鑰。")
documents = []
logger.info("開始載入上傳的 PDF 文件。")
for file_path in file_paths:
logger.info(f"載入 PDF 文件: {file_path}")
if not os.path.exists(file_path):
logger.error(f"文件不存在: {file_path}")
continue
try:
if loader_type == 'PyMuPDFLoader':
loader = PyMuPDFLoader(file_path)
elif loader_type == 'PyPDFLoader':
loader = PyPDFLoader(file_path)
else:
logger.error(f"Unknown loader type: {loader_type}")
continue
loaded_docs = loader.load()
if loaded_docs:
logger.info(f"載入 {file_path} 成功,包含 {len(loaded_docs)} 個文檔。")
# 打印第一個文檔的部分內容以確認
logger.info(f"第一個文檔內容: {loaded_docs[0].page_content[:500]}")
documents.extend(loaded_docs)
else:
logger.error(f"載入 {file_path} 但未找到任何文檔。")
except Exception as e:
logger.error(f"載入 {file_path} 時出現錯誤: {e}")
if not documents:
raise ValueError("沒有找到任何 PDF 文件或 PDF 文件無法載入。")
else:
logger.info(f"總共載入了 {len(documents)} 個文檔。")
# 分割長文本
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=50)
documents = text_splitter.split_documents(documents)
logger.info(f"分割後的文檔數量: {len(documents)}")
if not documents:
raise ValueError("分割後的文檔列表為空。請檢查 PDF 文件內容。")
# 初始化向量資料庫
try:
embeddings = OpenAIEmbeddings(openai_api_key=api_key) # 使用使用者的 API 密鑰
logger.info("初始化 OpenAIEmbeddings 成功。")
except Exception as e:
raise ValueError(f"初始化 OpenAIEmbeddings 時出現錯誤: {e}")
try:
vectordb = Chroma.from_documents(
documents,
embedding=embeddings,
persist_directory=VECTORDB_DIR
)
logger.info("初始化 Chroma 向量資料庫成功。")
except Exception as e:
raise ValueError(f"初始化 Chroma 向量資料庫時出現錯誤: {e}")
return vectordb
# 定義聊天處理函數
def handle_query(user_message, chat_history, vectordb, api_key):
try:
if not user_message:
return chat_history
# 添加角色指令前綴
preface = """
指令: 以繁體中文回答問題,200字以內。你是一位專業心理學家與調酒師,專精於 MBTI 人格與經典調酒主題。
非相關問題,請回應:「目前僅支援 MBTI 分析與經典調酒主題。」。
"""
query = f"{preface} 查詢內容:{user_message}"
# 初始化 ConversationalRetrievalChain,並傳遞 openai_api_key
pdf_qa = ConversationalRetrievalChain.from_llm(
ChatOpenAI(temperature=0.7, model="gpt-4", openai_api_key=api_key),
retriever=vectordb.as_retriever(search_kwargs={'k': 6}),
return_source_documents=True
)
# 呼叫模型並處理查詢
result = pdf_qa.invoke({"question": query, "chat_history": chat_history})
# 檢查結果並更新聊天歷史
if "answer" in result:
chat_history = chat_history + [(user_message, result["answer"])]
else:
chat_history = chat_history + [(user_message, "抱歉,未能獲得有效回應。")]
return chat_history
except Exception as e:
logger.error(f"Error in handle_query: {e}")
return chat_history + [("系統", f"出現錯誤: {str(e)}")]
# 定義保存 API 密鑰的函數
def save_api_key(api_key, state):
if not api_key.startswith("sk-"):
return "請輸入有效的 OpenAI API 密鑰。", state
state['api_key'] = api_key
logger.info("使用者已保存自己的 OpenAI API 密鑰。")
return "API 密鑰已成功保存。您現在可以上傳 PDF 文件並開始提問。", state
# 定義 Gradio 的處理函數
def process_files(files, state):
logger.info("process_files called")
if files:
try:
# 檢查是否已保存 API 密鑰
api_key = state.get('api_key', None)
if not api_key:
logger.error("使用者未提供 OpenAI API 密鑰。")
return "請先在「設定 API 密鑰」標籤中輸入並保存您的 OpenAI API 密鑰。", state
logger.info(f"Received {len(files)} files")
saved_file_paths = []
for idx, file_data in enumerate(files):
# 為每個文件分配唯一的文件名
filename = f"uploaded_{idx}.pdf"
save_path = os.path.join(VECTORDB_DIR, filename)
with open(save_path, "wb") as f:
f.write(file_data)
# 確認文件是否存在並檢查文件大小
if os.path.exists(save_path):
file_size = os.path.getsize(save_path)
if file_size > 0:
logger.info(f"File successfully saved to: {save_path} (Size: {file_size} bytes)")
else:
logger.error(f"File saved to {save_path} is empty.")
raise ValueError(f"上傳的文件 {filename} 為空。")
else:
logger.error(f"Failed to save file to: {save_path}")
raise FileNotFoundError(f"無法保存文件到 {save_path}")
saved_file_paths.append(save_path)
# 測試 PDF 加載器,先用 PyMuPDFLoader,再用 PyPDFLoader
try:
test_pdf_loader(save_path, loader_type='PyMuPDFLoader')
except Exception as e:
logger.error(f"PyMuPDFLoader failed: {e}")
logger.info("Attempting to load with PyPDFLoader...")
test_pdf_loader(save_path, loader_type='PyPDFLoader')
# 列出 VECTORDB_DIR 中的所有文件
saved_files = os.listdir(VECTORDB_DIR)
logger.info(f"Files in VECTORDB_DIR ({VECTORDB_DIR}): {saved_files}")
# 列出文件大小
file_sizes = {file: os.path.getsize(os.path.join(VECTORDB_DIR, file)) for file in saved_files}
logger.info(f"File sizes in VECTORDB_DIR: {file_sizes}")
vectordb = load_and_process_documents(saved_file_paths, loader_type='PyMuPDFLoader', api_key=api_key)
state['vectordb'] = vectordb
return "PDF 文件已成功上傳並處理。您現在可以開始提問。", state
except Exception as e:
logger.error(f"Error in process_files: {e}")
return f"處理文件時出現錯誤: {e}", state
else:
return "請上傳至少一個 PDF 文件。", state
def chat_interface(user_message, chat_history, state):
vectordb = state.get('vectordb', None)
api_key = state.get('api_key', None)
if not vectordb:
return chat_history, state, "請先上傳 PDF 文件以進行處理。"
if not api_key:
return chat_history, state, "請先在「設定 API 密鑰」標籤中輸入並保存您的 OpenAI API 密鑰。"
# 處理查詢
updated_history = handle_query(user_message, chat_history, vectordb, api_key)
return updated_history, state, ""
# 設計 Gradio 介面
with gr.Blocks() as demo:
gr.Markdown("<h1 style='text-align: center;'>MBTI 與經典調酒 AI 助理</h1>")
# 定義共享的 state
state = gr.State({"vectordb": None, "api_key": None})
with gr.Tab("設定 API 密鑰"):
with gr.Row():
with gr.Column(scale=1):
api_key_input = gr.Textbox(
label="輸入您的 OpenAI API 密鑰",
placeholder="sk-...",
type="password",
interactive=True
)
save_api_key_btn = gr.Button("保存 API 密鑰")
api_key_status = gr.Textbox(label="狀態", interactive=False)
with gr.Tab("上傳 PDF 文件"):
with gr.Row():
with gr.Column(scale=1):
upload = gr.File(
file_count="multiple",
file_types=[".pdf"],
label="上傳 PDF 文件",
interactive=True,
type="binary" # 修改為 'binary'
)
upload_btn = gr.Button("上傳並處理")
upload_status = gr.Textbox(label="上傳狀態", interactive=False)
with gr.Tab("聊天機器人"):
chatbot = gr.Chatbot()
with gr.Row():
with gr.Column(scale=0.85):
txt = gr.Textbox(show_label=False, placeholder="請輸入您的問題...")
with gr.Column(scale=0.15, min_width=0):
submit_btn = gr.Button("提問")
# 綁定提問按鈕
submit_btn.click(
chat_interface,
inputs=[txt, chatbot, state],
outputs=[chatbot, state, txt]
)
# 綁定輸入框的提交事件
txt.submit(
chat_interface,
inputs=[txt, chatbot, state],
outputs=[chatbot, state, txt]
)
# 綁定保存 API 密鑰按鈕
save_api_key_btn.click(
save_api_key,
inputs=[api_key_input, state],
outputs=[api_key_status, state]
)
# 綁定上傳按鈕
upload_btn.click(
process_files,
inputs=[upload, state],
outputs=[upload_status, state]
)
# 啟動 Gradio 應用
demo.launch() |