Spaces:
Running
Running
File size: 16,018 Bytes
92e4d9e 4291d1a 92e4d9e 4291d1a 92e4d9e 4291d1a 92e4d9e 4291d1a 92e4d9e 4291d1a 92e4d9e |
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 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
import streamlit as st
import pandas as pd
import torch
import os
import time
import logging
import subprocess
import sys
# 設定logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 頁面配置
st.set_page_config(
page_title="Excel 問答 AI(ChatGLM 驅動)",
page_icon="🤖",
layout="wide"
)
# 應用標題與說明
st.title("🤖 Excel 問答 AI(ChatGLM 驅動)")
st.markdown("""
### 使用說明
1. 可直接提問一般知識,AI 將使用內建能力回答
2. 上傳 Excel 檔案(包含「問題」和「答案」欄位)以添加專業知識
3. 系統會優先使用您上傳的知識庫進行回答
""")
# 檢查並安裝必要套件
def install_missing_packages():
required_packages = ["sentencepiece", "protobuf", "bitsandbytes"] # 加入 bitsandbytes
for package in required_packages:
try:
__import__(package)
st.write(f"{package} 已安裝")
except ImportError:
st.write(f"安裝缺失的套件: {package}")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
st.write(f"{package} 已安裝成功")
except Exception as e:
st.error(f"安裝 {package} 失敗: {str(e)}")
return False
return True
# 安裝缺失的套件
if not install_missing_packages():
st.error("必要套件安裝失敗,請刷新頁面重試")
st.stop()
st.write("正在導入依賴項...")
# 依次導入並檢查每個依賴
try:
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
st.write("成功導入 HuggingFaceEmbeddings")
except Exception as e:
st.error(f"導入 HuggingFaceEmbeddings 失敗: {str(e)}")
st.stop()
try:
from langchain_community.vectorstores import FAISS
st.write("成功導入 FAISS")
except Exception as e:
st.error(f"導入 FAISS 失敗: {str(e)}")
st.stop()
try:
from langchain_community.llms import HuggingFacePipeline
st.write("成功導入 HuggingFacePipeline")
except Exception as e:
st.error(f"導入 HuggingFacePipeline 失敗: {str(e)}")
st.stop()
try:
from langchain.chains import RetrievalQA, LLMChain
st.write("成功導入 RetrievalQA, LLMChain")
except Exception as e:
st.error(f"導入 RetrievalQA, LLMChain 失敗: {str(e)}")
st.stop()
try:
from langchain.prompts import PromptTemplate
st.write("成功導入 PromptTemplate")
except Exception as e:
st.error(f"導入 PromptTemplate 失敗: {str(e)}")
st.stop()
try:
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
st.write("成功導入 transformers 組件")
except Exception as e:
st.error(f"導入 transformers 組件失敗: {str(e)}")
st.stop()
try:
import bitsandbytes # 檢查 bitsandbytes
st.write("成功導入 bitsandbytes")
has_bitsandbytes = True
except ImportError:
st.warning("未安裝 bitsandbytes,將無法使用 4 位元量化。")
has_bitsandbytes = False
st.write("所有依賴項導入成功!")
# 側邊欄設定
with st.sidebar:
st.header("參數設定")
model_option = st.selectbox(
"選擇模型",
["THUDM/chatglm3-6b", "THUDM/chatglm2-6b", "THUDM/chatglm-6b"],
index=0
)
embedding_option = st.selectbox(
"選擇嵌入模型",
["shibing624/text2vec-base-chinese", "GanymedeNil/text2vec-large-chinese"],
index=0
)
mode = st.radio(
"回答模式",
["混合模式(優先使用上傳資料)", "僅使用上傳資料", "僅使用模型知識"]
)
max_tokens = st.slider("最大回應長度", 128, 2048, 512)
temperature = st.slider("溫度(創造性)", 0.0, 1.0, 0.7, 0.1)
top_k = st.slider("檢索相關文檔數", 1, 5, 3)
st.markdown("---")
st.markdown("### 關於")
st.markdown("此應用使用 ChatGLM 模型結合 LangChain 框架,將您的 Excel 數據轉化為智能問答系統。同時支持一般知識問答。")
# 全局變量
@st.cache_resource
def load_embeddings(model_name):
try:
logger.info(f"加載嵌入模型: {model_name}")
st.write(f"開始加載嵌入模型: {model_name}...")
embeddings = HuggingFaceEmbeddings(model_name=model_name)
st.write(f"嵌入模型加載成功!")
return embeddings
except Exception as e:
logger.error(f"嵌入模型加載失敗: {str(e)}")
st.error(f"嵌入模型加載失敗: {str(e)}")
return None
@st.cache_resource
def load_llm(_model_name, _max_tokens, _temperature):
try:
logger.info(f"加載語言模型: {_model_name}")
st.write(f"開始加載語言模型: {_model_name}...")
# 檢查可用資源
free_memory = torch.cuda.get_device_properties(0).total_memory - torch.cuda.memory_allocated() if torch.cuda.is_available() else 0
st.write(f"可用GPU記憶體: {free_memory / (1024**3):.2f} GB" if torch.cuda.is_available() else "無GPU可用,將使用CPU")
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.float16
load_args = {"trust_remote_code": True, "device_map": device, "torch_dtype": dtype}
if device == "cpu":
st.warning("注意:在 CPU 上載入大型語言模型可能會非常緩慢且需要大量記憶體。")
elif has_bitsandbytes:
try:
load_args["load_in_4bit"] = True
load_args["bnb_4bit_compute_dtype"] = torch.float16
st.info("嘗試使用 4 位元量化載入模型 (需要 bitsandbytes)。")
except Exception as e:
st.warning(f"載入 4 位元量化模型失敗: {e}")
st.info("將嘗試以半精度浮點數載入。")
# 使用超時保護
with st.spinner(f"正在加載 {_model_name} 模型,這可能需要幾分鐘..."):
# 加載tokenizer
st.write("加載tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(_model_name, trust_remote_code=True)
st.write("Tokenizer加載成功")
# 加載模型
st.write(f"開始加載模型到{device}...")
try:
model = AutoModelForCausalLM.from_pretrained(_model_name, **load_args)
st.write("模型加載成功!")
except Exception as e:
st.error(f"模型加載失敗: {e}")
st.error("嘗試使用不同的載入配置。")
raise e
# 創建pipeline
st.write("創建文本生成pipeline...")
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_new_tokens=_max_tokens,
temperature=_temperature,
top_p=0.9,
repetition_penalty=1.1
)
st.write("Pipeline創建成功!")
return HuggingFacePipeline(pipeline=pipe)
except Exception as e:
logger.error(f"語言模型加載失敗: {str(e)}")
st.error(f"語言模型加載失敗: {str(e)}")
st.error("如果是因為記憶體不足,請考慮使用較小的模型或增加系統記憶體")
return None
# 創建向量資料庫
def create_vectorstore(texts, embeddings):
try:
st.write("開始創建向量資料庫...")
vectorstore = FAISS.from_texts(texts, embedding=embeddings)
st.write("向量資料庫創建成功!")
return vectorstore
except Exception as e:
logger.error(f"向量資料庫創建失敗: {str(e)}")
st.error(f"向量資料庫創建失敗: {str(e)}")
return None
# 創建直接問答的LLM鏈
def create_general_qa_chain(llm):
prompt_template = """請回答以下問題:
問題: {question}
請提供詳細且有幫助的回答:"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["question"]
)
return LLMChain(llm=llm, prompt=prompt)
# 混合模式問答處理
def hybrid_qa(query, qa_chain, general_chain, confidence_threshold=0.7):
# 先嘗試使用知識庫回答
try:
st.write("嘗試從知識庫查詢答案...")
kb_result = qa_chain({"query": query})
# 檢查向量存儲的相似度分數,判斷是否有足夠相關的內容
if (hasattr(kb_result, 'source_documents') and
kb_result.get("source_documents") and
len(kb_result["source_documents"]) > 0):
# 這裡假設我們能獲取到相似度分數,實際上可能需要根據您使用的向量存儲方法調整
relevance = True # 在實際應用中,這裡應根據相似度分數確定
if relevance:
st.write("找到相關知識庫內容")
return kb_result, "knowledge_base", kb_result["source_documents"]
st.write("知識庫中未找到足夠相關的內容")
except Exception as e:
logger.warning(f"知識庫查詢失敗: {str(e)}")
st.warning(f"知識庫查詢失敗: {str(e)}")
# 如果知識庫沒有足夠相關的答案,使用一般知識模式
try:
st.write("使用模型一般知識回答...")
general_result = general_chain.run(question=query)
return {"result": general_result}, "general", []
except Exception as e:
logger.error(f"一般知識查詢失敗: {str(e)}")
st.error(f"一般知識查詢失敗: {str(e)}")
return {"result": "很抱歉,無法處理您的問題,請稍後再試。"}, "error", []
# 主應用邏輯
# 加載嵌入模型(先加載嵌入模型,因為這通常較小較快)
embeddings = None
if "embeddings" not in st.session_state:
with st.spinner("正在加載嵌入模型..."):
embeddings = load_embeddings(embedding_option)
if embeddings is not None:
st.session_state.embeddings = embeddings
else:
st.error("嵌入模型加載失敗,請刷新頁面重試")
st.stop()
else:
embeddings = st.session_state.embeddings
# 加載語言模型(不管是否上傳文件都需要)
llm = None
if "llm" not in st.session_state:
llm = load_llm(model_option, max_tokens, temperature)
if llm is not None:
st.session_state.llm = llm
else:
st.error("語言模型加載失敗,請刷新頁面重試")
st.stop()
else:
llm = st.session_state.llm
# 創建一般問答鏈
general_qa_chain = create_general_qa_chain(llm)
st.write("一般問答鏈創建成功!")
# 變數初始化
kb_qa_chain = None
has_knowledge_base = False
vectorstore = None
# 上傳Excel文件
uploaded_file = st.file_uploader("上傳你的問答 Excel(可選)", type=["xlsx"])
if uploaded_file:
# 讀取Excel文件
try:
st.write("開始讀取Excel文件...")
df = pd.read_excel(uploaded_file)
# 檢查必要欄位
if not {'問題', '答案'}.issubset(df.columns):
st.error("Excel 檔案需包含 '問題' 和 '答案' 欄位")
else:
# 顯示資料預覽
with st.expander("Excel 資料預覽"):
st.dataframe(df.head())
st.info(f"成功讀取 {len(df)} 筆問答對")
# 建立文本列表
texts = [f"問題:{q}\n答案:{a}" for q, a in zip(df['問題'], df['答案'])]
# 進度條
progress_text = "正在處理中..."
my_bar = st.progress(0, text=progress_text)
# 使用之前加載的嵌入模型
my_bar.progress(25, text="準備嵌入模型...")
# 建立向量資料庫
my_bar.progress(50, text="正在建立向量資料庫...")
vectorstore = create_vectorstore(texts, embeddings)
if vectorstore is None:
st.stop()
# 創建問答鏈
my_bar.progress(75, text="正在建立知識庫問答系統...")
kb_qa_chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=vectorstore.as_retriever(search_kwargs={"k": top_k}),
chain_type="stuff",
return_source_documents=True
)
has_knowledge_base = True
my_bar.progress(100, text="準備完成!")
time.sleep(1)
my_bar.empty()
st.success("知識庫已準備就緒,請輸入您的問題")
except Exception as e:
logger.error(f"Excel 檔案處理失敗: {str(e)}")
st.error(f"Excel 檔案處理失敗: {str(e)}")
# 查詢部分
st.markdown("## 開始對話")
query = st.text_input("請輸入你的問題:")
if query:
with st.spinner("AI 思考中..."):
try:
start_time = time.time()
# 根據模式選擇問答方式
if mode == "僅使用上傳資料":
if has_knowledge_base:
st.write("使用知識庫模式回答...")
result = kb_qa_chain({"query": query})
source = "knowledge_base"
source_docs = result["source_documents"]
else:
st.warning("您選擇了僅使用上傳資料模式,但尚未上傳Excel檔案。請上傳檔案或變更模式。")
st.stop()
elif mode == "僅使用模型知識":
st.write("使用模型一般知識模式回答...")
result = {"result": general_qa_chain.run(question=query)}
source = "general"
source_docs = []
else: # 混合模式
if has_knowledge_base:
st.write("使用混合模式回答...")
result, source, source_docs = hybrid_qa(query, kb_qa_chain, general_qa_chain)
else:
st.write("未檢測到知識庫,使用模型一般知識回答...")
result = {"result": general_qa_chain.run(question=query)}
source = "general"
source_docs = []
end_time = time.time()
# 顯示回答
st.markdown("### AI 回答:")
st.markdown(result["result"])
# 根據來源顯示不同信息
if source == "knowledge_base":
st.success("✅ 回答來自您的知識庫")
# 顯示參考資料
with st.expander("參考資料"):
for i, doc in enumerate(source_docs):
st.markdown(f"**參考 {i+1}**")
st.markdown(doc.page_content)
st.markdown("---")
elif source == "general":
if has_knowledge_base:
st.info("ℹ️ 回答來自模型的一般知識(知識庫中未找到相關內容)")
else:
st.info("ℹ️ 回答來自模型的一般知識")
st.text(f"回答生成時間: {(end_time - start_time):.2f} 秒")
except Exception as e:
logger.error(f"查詢處理失敗: {str(e)}")
st.error(f"查詢處理失敗,請重試: {str(e)}")
st.error(f"錯誤詳情: {str(e)}")
# 添加會話歷史功能
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
# 底部資訊
st.markdown("---")
st.markdown("Made with ❤️ | Excel 問答 AI") |