Spaces:
Running
Running
File size: 7,788 Bytes
2c81513 b8a4efc 2c81513 d65bc48 2c81513 9c88b52 b8a4efc 2c81513 b8a4efc 193a6c8 b8a4efc 193a6c8 9c88b52 b8a4efc 2c81513 b8a4efc 2e0831d 9c88b52 b8a4efc 2c81513 9c88b52 b8a4efc 2c81513 b8a4efc 2c81513 9c88b52 b8a4efc 2c81513 b8a4efc 9c88b52 b8a4efc 2c81513 b8a4efc d65bc48 b8a4efc 3b9d9ce d65bc48 b8a4efc 3b9d9ce 4d1ff56 3b9d9ce b8a4efc 3b9d9ce b8a4efc 3b9d9ce b8a4efc 9c88b52 3b9d9ce 9c88b52 b8a4efc 11fb5fe 4d1ff56 063db2e e273566 4d1ff56 6d8dea9 063db2e 4d1ff56 ca2be4b e273566 4d1ff56 e273566 ca2be4b 4d1ff56 a8d3e65 4d1ff56 6d8dea9 a8d3e65 4d1ff56 11fb5fe 4d1ff56 6d8dea9 b8a4efc 11fb5fe b8a4efc 11fb5fe b8a4efc 11fb5fe d65bc48 b8a4efc d65bc48 b8a4efc 11fb5fe b8a4efc d65bc48 11fb5fe b8a4efc |
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 |
import os, re, base64
from langchain_core.documents import Document
from langchain_chroma import Chroma
from openai import OpenAI
from langchain.embeddings.base import Embeddings
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_community.vectorstores import FAISS
import gradio as gr
from langchain.memory import ConversationBufferMemory
# =============================================
# 1️⃣ 內建 Embedding:使用 Gemini embedding API
# =============================================
from langchain_community.embeddings import HuggingFaceEmbeddings
embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
# =============================================
# 2️⃣ 載入 QA 檔案並分類
# =============================================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(BASE_DIR, "QA_v2.txt")
if not os.path.exists(path):
raise FileNotFoundError(f"❌ 找不到 QA 檔案:{path}")
with open(path, "r", encoding="utf-8") as f:
text = f.read()
pattern = r"(Q[::].*?)(?=Q[::]|$)"
qas = re.findall(pattern, text, flags=re.S)
qa_docs = {"證券": [], "期貨": [], "複委託": []}
for qa in qas:
if "證券" in qa:
qa_docs["證券"].append(Document(page_content=qa.strip(), metadata={"source": path}))
elif "期貨" in qa:
qa_docs["期貨"].append(Document(page_content=qa.strip(), metadata={"source": path}))
elif "複委託" in qa:
qa_docs["複委託"].append(Document(page_content=qa.strip(), metadata={"source": path}))
print("✅ 已成功讀取 QA 並完成分類:", {k: len(v) for k, v in qa_docs.items()})
# =============================================
# 3️⃣ 建立向量資料庫(使用 FAISS,記憶體型)
# =============================================
vectordbs = {}
for k, docs in qa_docs.items():
vectordbs[k] = FAISS.from_documents(docs, embedding)
# =============================================
# 4️⃣ 初始化 Gemini LLM
# =============================================
API_KEY = os.getenv("GOOGLE_API_KEY")
if not API_KEY:
raise ValueError("⚠️ 未設定 GOOGLE_API_KEY,請在 Hugging Face Secrets 中新增。")
llm = ChatGoogleGenerativeAI(model='gemini-2.5-flash', google_api_key=API_KEY)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
# =============================================
# 5️⃣ 對話邏輯
# =============================================
def auto_detect_category(text):
if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割", "現股"]):
return "證券"
elif any(k in text for k in ["期貨", "選擇權", "結算", "保證金", "契約"]):
return "期貨"
elif any(k in text for k in ["複委託", "海外", "美股", "港股", "國外"]):
return "複委託"
else:
return "證券"
def chat_fn(message, history):
category = auto_detect_category(message)
vectordb = vectordbs.get(category)
if not vectordb:
return "目前尚無此類別的知識庫。"
docs = vectordb.similarity_search(message, k=2)
context = "\n\n".join([d.page_content for d in docs]) if docs else "查無相關內容。"
prompt = f"""
我是一位金融客服人員。根據以下公司規章內容回答使用者問題:
---
{context}
---
使用者問題:{message}
"""
try:
response = llm.invoke(prompt)
reply = response.content.strip()
except Exception as e:
reply = f"⚠️ 生成錯誤:{e}"
return reply or "請洽營業員"
# =============================================
# 6️⃣ Gradio 介面
# =============================================
logo_path = os.path.join(BASE_DIR, "mega.png")
logo_base64 = ""
if os.path.exists(logo_path):
with open(logo_path, "rb") as f:
logo_base64 = base64.b64encode(f.read()).decode("utf-8")
logo_path = os.path.join(BASE_DIR, "mega.png")
logo_base64 = ""
if os.path.exists(logo_path):
with open(logo_path, "rb") as f:
logo_base64 = base64.b64encode(f.read()).decode("utf-8")
gr.HTML("""
<style>
/* ====== 桌機預設:單行顯示 ====== */
#main-title {
font-size: 28px;
font-weight: bold;
text-align: center;
line-height: 1.4;
margin: 0;
display: inline-block;
}
/* ====== 手機版:自動兩行顯示 ====== */
@media (max-width: 768px) {
#main-title {
font-size: 24px; /* 👈 手機字體略小 */
white-space: pre-line;
}
#main-title::before {
content: "👨💼 我是小智\\A您的金融好幫手 🫰"; /* \\A = 換行 */
white-space: pre; /* 保留換行格式 */
}
#main-title span {
display: none; /* 隱藏原本的單行文字 */
}
}
</style>
<div id="main-title-wrapper" style="text-align:center; margin-top:20px;">
<h1 id='main-title'><span>👨💼 我是小智 您的金融好幫手 🫰</span></h1>
<p id='sub-title' style='margin-top:10px; font-size:14px; color:#666;'>Powered by Gemini & LangChain</p>
</div>
""")
with gr.Row():
with gr.Column(scale=4):
chatbox = gr.Chatbot(label="💬 對話紀錄", type="messages")
with gr.Row(elem_id="input-row"):
user_input = gr.Textbox(
elem_id="user-input",
show_label=False,
placeholder="輸入訊息...",
scale=8
)
send_btn = gr.Button("送出", elem_id="send-btn", scale=1)
def handle_input(message, history):
if not message.strip():
return history, gr.update(value="")
reply = chat_fn(message, history)
history = history + [
{"role": "user", "content": message},
{"role": "assistant", "content": reply}
]
return history, gr.update(value="")
user_input.submit(handle_input, [user_input, chatbox], [chatbox, user_input])
send_btn.click(handle_input, [user_input, chatbox], [chatbox, user_input])
with gr.Column(scale=1):
gr.Markdown("### 👇 快速提問")
btns = [
("未成年可以開戶嗎?", "未成年可以開戶嗎?"),
("法人開戶要準備什麼?", "法人開戶要準備什麼?"),
("期貨交易保證金是什麼?", "期貨交易保證金是什麼?"),
("複委託要如何下單?", "複委託要如何下單?"),
("美股交易時間?", "美股交易時間?"),
("美股可以定期定額嗎?", "美股可以定期定額嗎?")
]
for label, q in btns:
gr.Button(label).click(lambda h, q=q: handle_input(q, h), [chatbox], [chatbox, user_input])
def clear_memory():
memory.clear()
return [], gr.update(value="", placeholder="輸入訊息...")
gr.Button("🧹 整理畫面").click(clear_memory, outputs=[chatbox, user_input])
# 底部版權列
gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
# 手機鍵盤彈出時捲動補丁
demo.load(None, None, None, js="""
window.addEventListener('focusin', () => {
document.querySelector('textarea')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
""")
demo.launch() |