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")