adamtobegreat commited on
Commit
f29091b
·
verified ·
1 Parent(s): e99ffb2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -120
app.py CHANGED
@@ -1,12 +1,11 @@
1
  import os, re, base64
2
  from langchain_core.documents import Document
3
  from langchain_chroma import Chroma
4
- from langchain_huggingface import HuggingFaceEmbeddings # ✅ 雲端可直接使用
5
  from langchain_google_genai import ChatGoogleGenerativeAI
6
  import chromadb
7
  import gradio as gr
8
 
9
- # === 記憶模組相容多版本 ===
10
  try:
11
  from langchain_memory import ConversationBufferMemory
12
  except ImportError:
@@ -15,18 +14,12 @@ except ImportError:
15
  except ImportError:
16
  from langchain_community.memory import ConversationBufferMemory
17
 
18
-
19
- # =============================================
20
- # 1️⃣ 使用 Hugging Face 雲端 embedding 模型
21
- # =============================================
22
  embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
23
 
24
- # =============================================
25
- # 2️⃣ 載入 QA 檔案並分類
26
- # =============================================
27
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28
  qa_path = os.path.join(BASE_DIR, "QA_v2.txt")
29
-
30
  if not os.path.exists(qa_path):
31
  raise FileNotFoundError(f"❌ 找不到 QA 檔案:{qa_path}")
32
 
@@ -48,39 +41,31 @@ print("✅ 已成功讀取 QA 並完成分類:")
48
  for k, v in qa_docs.items():
49
  print(f" {k}:{len(v)} 筆")
50
 
51
-
52
- # =============================================
53
- # 3️⃣ 建立向量資料庫
54
- # =============================================
55
  client = chromadb.PersistentClient(path="./chroma_db")
56
- collection_names = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
57
-
58
  vectordbs = {}
59
  for cat, docs in qa_docs.items():
60
  vectordbs[cat] = Chroma(
61
  client=client,
62
- collection_name=collection_names[cat],
63
  embedding_function=embedding
64
  )
65
  if len(vectordbs[cat].get()["documents"]) == 0:
66
  vectordbs[cat].add_documents(docs)
 
 
67
  print("✅ 各類別向量資料庫建立完成")
68
 
69
-
70
- # =============================================
71
- # 4️⃣ 初始化 Gemini LLM(雲端)
72
- # =============================================
73
  API_KEY = os.getenv("GOOGLE_API_KEY")
74
  if not API_KEY:
75
- raise ValueError("⚠️ 未設定 GOOGLE_API_KEY,請在 Hugging Face Secrets 中新增。")
76
 
77
  llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=API_KEY)
78
  memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
79
 
80
-
81
- # =============================================
82
- # 5️⃣ 對話邏輯
83
- # =============================================
84
  def auto_detect_category(text):
85
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
86
  return "證券"
@@ -91,13 +76,11 @@ def auto_detect_category(text):
91
  else:
92
  return "證券"
93
 
94
-
95
  def chat_fn(message, history):
96
  category = auto_detect_category(message)
97
  vectordb = vectordbs.get(category)
98
  docs = vectordb.similarity_search(message, k=2)
99
  context = "\n\n".join([d.page_content for d in docs]) if docs else "查無資料"
100
-
101
  prompt = f"""
102
  你是一位金融客服人員,根據以下公司QA回答客戶問題:
103
  ---
@@ -105,7 +88,6 @@ def chat_fn(message, history):
105
  ---
106
  使用者問題:{message}
107
  """
108
-
109
  try:
110
  response = llm.invoke(prompt)
111
  reply = response.content.strip()
@@ -113,10 +95,7 @@ def chat_fn(message, history):
113
  reply = f"⚠️ 生成錯誤:{e}"
114
  return reply or "請洽營業員"
115
 
116
-
117
- # =============================================
118
- # 6️⃣ Gradio 介面(LINE風格 + 小輸入按鈕 + 純白footer)
119
- # =============================================
120
  logo_path = os.path.join(BASE_DIR, "mega.png")
121
  logo_base64 = ""
122
  if os.path.exists(logo_path):
@@ -126,73 +105,33 @@ if os.path.exists(logo_path):
126
  with gr.Blocks(
127
  theme="soft",
128
  css="""
129
- /* ====== logo ====== */
130
- #logo-top {
131
- position: fixed; top: 12px; left: 18px;
132
- background-color: white; border-radius: 10px;
133
- padding: 6px 8px; box-shadow: 0 0 8px rgba(0,0,0,0.15);
 
 
 
134
  }
135
- #logo-top img { width: 120px; height: auto; display: block; }
136
 
137
- /* ====== 標題 ====== */
138
- #main-title {
139
- font-size: 28px; font-weight: bold; text-align: center;
140
- line-height: 1.4; margin: 0; display: inline-block;
141
- }
142
- @media (max-width: 768px) {
143
- #main-title { font-size: 24px; white-space: pre-line; }
144
- #main-title::before {
145
- content: "👨‍💼 我是小智\\A您的金融好幫手 🫰";
146
- white-space: pre;
147
- }
148
- #main-title span { display: none; }
149
- }
150
 
151
- /* ====== footer(純白背景) ====== */
152
- #footer {
153
- position: fixed; bottom: 40px; left: 0; width: 100%;
154
- text-align: center; font-size: 13px; color: #aaa;
155
- border-top: 1px solid #ddd; padding-top: 8px;
156
- background-color: transparent;
157
- }
158
- @media (max-width: 768px) {
159
- #footer { position: relative; margin-top: 40px; }
160
- }
161
-
162
- /* ====== LINE風格輸入區 ====== */
163
- #input-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
164
- #user-input {
165
- flex-grow: 1; border-radius: 20px; border: 1px solid #ccc;
166
- padding: 6px 12px; font-size: 15px; background-color: #fff;
167
- box-shadow: inset 0 0 1px rgba(0,0,0,0.05);
168
- }
169
-
170
- /* 🟢 小巧文字版「輸入」按鈕 */
171
- #send-btn {
172
- background-color: #00b800;
173
- color: white;
174
- border: none;
175
- border-radius: 14px;
176
- height: 26px;
177
- padding: 0 10px;
178
- font-size: 13px;
179
- font-weight: 600;
180
- cursor: pointer;
181
- transition: background-color 0.2s ease, transform 0.1s ease;
182
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
183
- }
184
- #send-btn:hover { background-color: #00a000; }
185
- #send-btn:active { transform: scale(0.95); }
186
  """
187
  ) as demo:
188
  if logo_base64:
189
- gr.HTML(f"<div id='logo-top'><img src='data:image/png;base64,{logo_base64}' alt='logo'></div>")
190
 
191
  gr.HTML("""
192
- <div id='main-title-wrapper' style='text-align:center; margin-top:20px;'>
193
- <h1 id='main-title'><span>👨‍💼 我是小智&nbsp;&nbsp;您的金融好幫手 🫰</span></h1>
194
- <p id='sub-title' style='margin-top:10px; font-size:14px; color:#666;'>Powered by Gemini & LangChain</p>
195
- </div>
196
  """)
197
 
198
  with gr.Row():
@@ -201,44 +140,31 @@ with gr.Blocks(
201
  with gr.Row(elem_id="input-row"):
202
  user_input = gr.Textbox(elem_id="user-input", show_label=False, placeholder="輸入訊息...", scale=8)
203
  send_btn = gr.Button("輸入", elem_id="send-btn", scale=1)
204
-
205
  def handle_input(message, history):
206
  if not message.strip():
207
  return history, gr.update(value="")
208
  reply = chat_fn(message, history)
209
- history = history + [
210
- {"role": "user", "content": message},
211
- {"role": "assistant", "content": reply}
212
- ]
213
  return history, gr.update(value="")
214
-
215
- user_input.submit(handle_input, [user_input, chatbox], [chatbox, user_input])
216
- send_btn.click(handle_input, [user_input, chatbox], [chatbox, user_input])
217
-
218
  with gr.Column(scale=1):
219
  gr.Markdown("### 👇 快速提問")
220
- btns = [
221
- ("未成年可以開戶嗎?", "未成年可以開戶嗎?"),
222
- ("法人開戶要準備什麼?", "法人開戶要準備什麼?"),
223
- ("期貨交易保證金是什麼?", "期貨交易保證金是什麼?"),
224
- ("複委託要如何下單?", "複委託要如何下單?"),
225
- ("美股交易時間?", "美股交易時間?"),
226
- ("美股可以定期定額嗎?", "美股可以定期定額嗎?")
227
- ]
228
- for label, q in btns:
229
- gr.Button(label).click(lambda h, q=q: handle_input(q, h), [chatbox], [chatbox, user_input])
230
-
231
  def clear_memory():
232
  memory.clear()
233
- return [], gr.update(value="", placeholder="輸入訊息...")
234
- gr.Button("🧹 整理畫面").click(clear_memory, outputs=[chatbox, user_input])
235
 
236
  gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
237
 
238
- demo.load(None, None, None, js="""
239
- window.addEventListener('focusin', () => {
240
- document.querySelector('textarea')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
241
- });
242
- """)
243
-
244
  demo.launch()
 
1
  import os, re, base64
2
  from langchain_core.documents import Document
3
  from langchain_chroma import Chroma
4
+ from langchain_huggingface import HuggingFaceEmbeddings
5
  from langchain_google_genai import ChatGoogleGenerativeAI
6
  import chromadb
7
  import gradio as gr
8
 
 
9
  try:
10
  from langchain_memory import ConversationBufferMemory
11
  except ImportError:
 
14
  except ImportError:
15
  from langchain_community.memory import ConversationBufferMemory
16
 
17
+ # === Embedding ===
 
 
 
18
  embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
19
 
20
+ # === 載入 QA ===
 
 
21
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
  qa_path = os.path.join(BASE_DIR, "QA_v2.txt")
 
23
  if not os.path.exists(qa_path):
24
  raise FileNotFoundError(f"❌ 找不到 QA 檔案:{qa_path}")
25
 
 
41
  for k, v in qa_docs.items():
42
  print(f" {k}:{len(v)} 筆")
43
 
44
+ # === 向量資料庫 ===
 
 
 
45
  client = chromadb.PersistentClient(path="./chroma_db")
46
+ collections = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
 
47
  vectordbs = {}
48
  for cat, docs in qa_docs.items():
49
  vectordbs[cat] = Chroma(
50
  client=client,
51
+ collection_name=collections[cat],
52
  embedding_function=embedding
53
  )
54
  if len(vectordbs[cat].get()["documents"]) == 0:
55
  vectordbs[cat].add_documents(docs)
56
+ else:
57
+ print(f"⚙️ 已載入現有向量資料庫:{collections[cat]}")
58
  print("✅ 各類別向量資料庫建立完成")
59
 
60
+ # === Gemini ===
 
 
 
61
  API_KEY = os.getenv("GOOGLE_API_KEY")
62
  if not API_KEY:
63
+ raise ValueError("⚠️ 未設定 GOOGLE_API_KEY")
64
 
65
  llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=API_KEY)
66
  memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
67
 
68
+ # === 對話主邏輯 ===
 
 
 
69
  def auto_detect_category(text):
70
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
71
  return "證券"
 
76
  else:
77
  return "證券"
78
 
 
79
  def chat_fn(message, history):
80
  category = auto_detect_category(message)
81
  vectordb = vectordbs.get(category)
82
  docs = vectordb.similarity_search(message, k=2)
83
  context = "\n\n".join([d.page_content for d in docs]) if docs else "查無資料"
 
84
  prompt = f"""
85
  你是一位金融客服人員,根據以下公司QA回答客戶問題:
86
  ---
 
88
  ---
89
  使用者問題:{message}
90
  """
 
91
  try:
92
  response = llm.invoke(prompt)
93
  reply = response.content.strip()
 
95
  reply = f"⚠️ 生成錯誤:{e}"
96
  return reply or "請洽營業員"
97
 
98
+ # === Gradio 介面 ===
 
 
 
99
  logo_path = os.path.join(BASE_DIR, "mega.png")
100
  logo_base64 = ""
101
  if os.path.exists(logo_path):
 
105
  with gr.Blocks(
106
  theme="soft",
107
  css="""
108
+ #logo-top {position:fixed;top:12px;left:18px;z-index:1000;
109
+ background:white;border-radius:10px;padding:6px 8px;
110
+ box-shadow:0 0 8px rgba(0,0,0,0.15);}
111
+ #logo-top img{width:120px;height:auto;display:block;}
112
+
113
+ #main-title{text-align:center;font-size:28px;font-weight:bold;margin:0;line-height:1.4;}
114
+ @media (max-width:768px){
115
+ #main-title{white-space:pre-line;font-size:24px;}
116
  }
 
117
 
118
+ #footer{text-align:center;font-size:13px;color:#999;border-top:1px solid #ddd;
119
+ padding-top:8px;margin-top:30px;background:transparent;}
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ #input-row{display:flex;align-items:center;gap:8px;margin-top:10px;}
122
+ #user-input{flex-grow:1;border-radius:20px;border:1px solid #ccc;
123
+ padding:6px 12px;font-size:15px;background-color:#fff;}
124
+ #send-btn{background:#00b800;color:white;border:none;border-radius:14px;
125
+ height:26px;padding:0 10px;font-size:13px;font-weight:600;cursor:pointer;}
126
+ #send-btn:hover{background:#00a000;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  """
128
  ) as demo:
129
  if logo_base64:
130
+ gr.HTML(f"<div id='logo-top'><img src='data:image/png;base64,{logo_base64}'></div>")
131
 
132
  gr.HTML("""
133
+ <h1 id='main-title'>👨‍💼 我是小智<br>您的金融好幫手 🫰</h1>
134
+ <p style='text-align:center;margin-top:8px;color:#666;'>Powered by Gemini & LangChain</p>
 
 
135
  """)
136
 
137
  with gr.Row():
 
140
  with gr.Row(elem_id="input-row"):
141
  user_input = gr.Textbox(elem_id="user-input", show_label=False, placeholder="輸入訊息...", scale=8)
142
  send_btn = gr.Button("輸入", elem_id="send-btn", scale=1)
 
143
  def handle_input(message, history):
144
  if not message.strip():
145
  return history, gr.update(value="")
146
  reply = chat_fn(message, history)
147
+ history += [{"role":"user","content":message},
148
+ {"role":"assistant","content":reply}]
 
 
149
  return history, gr.update(value="")
150
+ user_input.submit(handle_input,[user_input,chatbox],[chatbox,user_input])
151
+ send_btn.click(handle_input,[user_input,chatbox],[chatbox,user_input])
 
 
152
  with gr.Column(scale=1):
153
  gr.Markdown("### 👇 快速提問")
154
+ for label,q in [
155
+ ("未成年可以開戶嗎?","未成年可以開戶嗎?"),
156
+ ("法人開戶要準備什麼?","法人開戶要準備什麼?"),
157
+ ("期貨交易保證金是什麼?","期貨交易保證金是什麼?"),
158
+ ("複委託要如何下單?","複委託要如何下單?"),
159
+ ("美股交易時間?","美股交易時間?"),
160
+ ("美股可以定期定額嗎?","美股可以定期定額嗎?")
161
+ ]:
162
+ gr.Button(label).click(lambda h,q=q: handle_input(q,h),[chatbox],[chatbox,user_input])
 
 
163
  def clear_memory():
164
  memory.clear()
165
+ return [],gr.update(value="",placeholder="輸入訊息...")
166
+ gr.Button("🧹 整理畫面").click(clear_memory,outputs=[chatbox,user_input])
167
 
168
  gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
169
 
 
 
 
 
 
 
170
  demo.launch()