adamtobegreat commited on
Commit
e5b3b38
·
verified ·
1 Parent(s): 9832ac5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +96 -184
app.py CHANGED
@@ -1,15 +1,18 @@
1
  """
2
  ======================================================
3
  📘 金融客服小智(Fintech Assistant)
4
- 版本:v3.2 (穩定正式版)
5
  更新重點:
6
- 1. 修正 LangChain 記憶格式(避免 ValueError)
7
- 2. 回復原生輸入框樣式(類似 LINE 的簡潔列)
8
- 3. 保留手機自適應、桌面置中、右欄清除鍵
 
 
 
9
  ======================================================
10
  """
11
 
12
- import os, re, base64
13
  import chromadb
14
  import gradio as gr
15
  from langchain_core.documents import Document
@@ -73,14 +76,18 @@ else:
73
 
74
 
75
  # =============================================
76
- # 3️⃣ 向量資料庫初始化
77
  # =============================================
78
  client = chromadb.Client()
79
  collection_map = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
80
  vectordbs = {}
81
  for cat, docs in qa_docs.items():
82
  vectordb = Chroma(client=client, collection_name=collection_map[cat], embedding_function=embedding)
83
- if hasattr(vectordb._collection, "count") and vectordb._collection.count() == 0 and docs:
 
 
 
 
84
  vectordb.add_documents(docs)
85
  vectordbs[cat] = vectordb
86
  print("✅ 向量資料庫初始化完成。")
@@ -98,7 +105,7 @@ memory = ConversationBufferMemory(memory_key="chat_history", return_messages=Tru
98
 
99
 
100
  # =============================================
101
- # 5️⃣ 對話邏輯
102
  # =============================================
103
  def auto_detect_category(text: str):
104
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
@@ -110,40 +117,59 @@ def auto_detect_category(text: str):
110
  return "證券"
111
 
112
 
 
 
 
 
 
 
 
 
 
 
113
  def chat_fn(message, history):
114
  category = auto_detect_category(message)
115
  vectordb = vectordbs[category]
116
- docs = vectordb.similarity_search(message, k=2)
117
  context = "\n\n".join(d.page_content for d in docs) if docs else "查無相關資料"
118
 
 
 
 
 
 
 
119
  prompt = f"""
120
- 你是一位金融客服人員,請根據以下QA知識回答:
121
  ---
122
  {context}
123
  ---
124
  使用者問題:{message}
 
 
125
  """
126
 
127
- try:
128
- if llm:
129
- response = llm.invoke(prompt)
130
- reply = getattr(response, "content", None) or getattr(response, "text", "⚠️ 無回覆")
131
- else:
132
- reply = "(模擬模式)這是示範回覆,請確認是否已設定 GOOGLE_API_KEY。"
133
- except Exception as e:
134
- reply = f"⚠️ 生成錯誤:{e}"
 
 
 
 
 
135
 
136
- # ✅ 修正記憶體格式,避免 ValueError
137
  memory.save_context({"input": message}, {"output": reply})
138
-
139
- return reply
140
 
141
 
142
  # =============================================
143
- # 6️⃣ Gradio 介面
144
  # =============================================
145
-
146
- # === Logo 圖片處理 ===
147
  logo_base64 = ""
148
  if os.path.exists(LOGO_PATH):
149
  with open(LOGO_PATH, "rb") as f:
@@ -152,194 +178,80 @@ if os.path.exists(LOGO_PATH):
152
  with gr.Blocks(
153
  theme="soft",
154
  css="""
155
- #logo-top {
156
- position: fixed; top: 12px; left: 18px;
157
- background-color: white; border-radius: 10px;
158
- padding: 6px 8px; box-shadow: 0 0 8px rgba(0,0,0,0.15);
159
- pointer-events: none;
160
- }
161
- #logo-top img { width: 120px; height: auto; display: block; }
162
-
163
- #footer { text-align:center; font-size:13px; color:#aaa; margin-top: 20px; }
164
-
165
- /* 手機寬度下讓 Row 自動垂直排列 */
166
- @media (max-width: 768px) {
167
- .gr-block.gr-row {
168
- flex-direction: column !important;
169
- }
170
- #logo-top img { width: 90px; }
171
- .gradio-container { padding: 8px; }
172
- #footer { font-size: 12px; margin-top: 10px; }
173
- }
174
-
175
- /* === 桌機/手機自適應標題 === */
176
- #main-title {
177
- text-align: center;
178
- font-weight: bold;
179
- font-size: 26px;
180
- margin-top: 60px;
181
- margin-bottom: 6px;
182
- }
183
- .title-line {
184
- display: flex;
185
- justify-content: center;
186
- align-items: center;
187
- gap: 8px;
188
- flex-wrap: nowrap;
189
- }
190
- .subtitle {
191
- white-space: nowrap;
192
- }
193
- @media (max-width: 768px) {
194
- .title-line {
195
- flex-direction: column;
196
- gap: 4px;
197
- }
198
- #main-title {
199
- font-size: 22px;
200
- line-height: 1.4;
201
- }
202
- }
203
-
204
- /* ✅ 修正輸入框高度與按鈕比例 */
205
- #chat-input textarea {
206
- height: 48px !important;
207
- min-height: 48px !important;
208
- font-size: 16px !important;
209
- padding: 8px 12px !important;
210
- border-radius: 10px !important;
211
- }
212
- #chat-row {
213
- align-items: center !important;
214
- gap: 4px !important;
215
- }
216
- #send-btn {
217
- height: 48px !important;
218
- font-size: 16px !important;
219
- border-radius: 10px !important;
220
- }
221
-
222
- /* ✅ 桌機版比例:輸入框 9、按鈕 1 */
223
- #chat-row .gr-textbox, #chat-row textarea { flex: 9 !important; width: 90% !important; }
224
- #chat-row .gr-button, #chat-row button { flex: 1 !important; width: 10% !important; }
225
-
226
- /* ✅ 手機版比例:輸入框 9、按鈕 1 + 防止「送出」換行 */
227
- @media (max-width: 768px) {
228
- #chat-row .gr-textbox, #chat-row textarea {
229
- flex: 9 !important;
230
- width: 90% !important;
231
- }
232
- #chat-row .gr-button, #chat-row button {
233
- flex: 1 !important;
234
- width: 10% !important;
235
- max-width: 90px !important;
236
- min-width: 70px !important;
237
- white-space: nowrap !important; /* ✅ 不允許文字換行 */
238
- overflow: hidden !important;
239
- text-overflow: ellipsis !important;
240
- letter-spacing: 0.5px !important;
241
- }
242
- #send-btn button {
243
- padding: 0 12px !important;
244
- font-size: 15px !important;
245
- line-height: 1 !important; /* ✅ 確保文字垂直置中 */
246
- }
247
- }
248
- """
249
  ) as demo:
250
  if logo_base64:
251
  gr.HTML(f"<div id='logo-top'><img src='data:image/png;base64,{logo_base64}'></div>")
252
 
253
- # 🔹 標題(桌機同行、手機自動換行)
254
  gr.HTML("""
255
- <div id="main-title">
256
- <span class="title-line">
257
- 👨‍💼 我是小智
258
- <span class="subtitle">您的金融好幫手 🫰</span>
259
- </span>
260
- </div>
261
  """)
262
- gr.Markdown("<div style='text-align:center; color:gray;'>Powered by Gemini & LangChain</div>")
263
 
264
- with gr.Row(equal_height=False):
265
- # 左側:聊天區
266
- with gr.Column(scale=4, min_width=300):
267
  chatbot = gr.Chatbot(label="💬 對話紀錄", type="messages", height=500)
 
 
 
 
 
 
 
 
268
 
269
- # ✅ 輸入框與送出鍵同行排列(桌機 9:1、手機 9:1)
270
- with gr.Row(elem_id="chat-row"):
271
- user_input = gr.Textbox(
272
- placeholder="請輸入您的問題(Enter 送出 / Shift+Enter 換行)...",
273
- show_label=False,
274
- lines=1,
275
- max_lines=3,
276
- elem_id="chat-input",
277
- scale=9
278
- )
279
- send_btn = gr.Button(
280
- "送出",
281
- variant="primary",
282
- elem_id="send-btn",
283
- scale=1
284
- )
285
-
286
- # === 輸入邏輯 ===
287
  def handle_input(message, history):
288
- if history is None:
289
- history = []
290
  if not message.strip():
291
  return history, gr.update(value="")
292
  reply = chat_fn(message, history)
 
293
  history += [
294
  {"role": "user", "content": message},
295
- {"role": "assistant", "content": reply}
296
  ]
297
  return history, gr.update(value="")
298
 
299
- # ✅ 綁定事件(Enter送出、Shift+Enter換行)
300
  user_input.submit(handle_input, [user_input, chatbot], [chatbot, user_input])
301
  send_btn.click(handle_input, [user_input, chatbot], [chatbot, user_input])
302
 
303
- # ✅ JS 修正版:支援桌機 / 手機 / HuggingFace IFrame
304
- gr.HTML("""
305
- <script>
306
- document.addEventListener("DOMContentLoaded", function() {
307
- const observer = new MutationObserver(() => {
308
- const textareas = document.querySelectorAll("textarea");
309
- textareas.forEach((ta) => {
310
- if (!ta.dataset.bound) {
311
- ta.dataset.bound = "true";
312
- ta.addEventListener("keydown", function(e) {
313
- if (e.key === "Enter" && !e.shiftKey) {
314
- e.preventDefault();
315
- const sendBtn = document.querySelector('#send-btn button, #send-btn');
316
- if (sendBtn) sendBtn.click();
317
- }
318
- });
319
- }
320
- });
321
- });
322
- observer.observe(document.body, { childList: true, subtree: true });
323
- });
324
- </script>
325
- """)
326
-
327
- # 右側:常見問題 + 清除對話
328
- with gr.Column(scale=1, min_width=200):
329
  gr.Markdown("### 🔍 常見問題")
330
  examples = [
331
- "未成年可以開戶嗎?",
332
- "法人開戶要準備什麼?",
 
333
  "期貨交易保證金是什麼?",
334
- "複委託要如何下單?",
335
  "美股交易時間?",
336
- "美股可以定期定額嗎?"
337
  ]
338
  for q in examples:
339
  gr.Button(q).click(
340
- fn=lambda q=q, history=[]: handle_input(q, history),
341
  inputs=[],
342
- outputs=[chatbot, user_input]
343
  )
344
 
345
  def clear_all():
@@ -348,6 +260,6 @@ with gr.Blocks(
348
  gr.Markdown("---")
349
  gr.Button("🧹 整理畫面").click(clear_all, outputs=[chatbot, user_input])
350
 
351
- gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
352
 
353
  demo.launch()
 
1
  """
2
  ======================================================
3
  📘 金融客服小智(Fintech Assistant)
4
+ 版本:v3.4 (📱自動縮放優化版)
5
  更新重點:
6
+ 1. LLM 三次重試機制(防止 API 錯誤中斷)
7
+ 2. 整合記憶進 prompt(上下文連貫對話)
8
+ 3. 安全向量搜尋(避免空 collection 錯誤)
9
+ 4. lambda 修正(避免共享同一 history)
10
+ 5. 顯示自動分類提示(可見知識來源)
11
+ 6. 📱 新增手機縮放與字體比例自適應
12
  ======================================================
13
  """
14
 
15
+ import os, re, base64, time
16
  import chromadb
17
  import gradio as gr
18
  from langchain_core.documents import Document
 
76
 
77
 
78
  # =============================================
79
+ # 3️⃣ 向量資料庫初始化(含安全檢查)
80
  # =============================================
81
  client = chromadb.Client()
82
  collection_map = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
83
  vectordbs = {}
84
  for cat, docs in qa_docs.items():
85
  vectordb = Chroma(client=client, collection_name=collection_map[cat], embedding_function=embedding)
86
+ try:
87
+ count = vectordb._collection.count() if hasattr(vectordb._collection, "count") else len(vectordb.get()["ids"])
88
+ except Exception:
89
+ count = 0
90
+ if count == 0 and docs:
91
  vectordb.add_documents(docs)
92
  vectordbs[cat] = vectordb
93
  print("✅ 向量資料庫初始化完成。")
 
105
 
106
 
107
  # =============================================
108
+ # 5️⃣ 對話邏輯(改進版)
109
  # =============================================
110
  def auto_detect_category(text: str):
111
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
 
117
  return "證券"
118
 
119
 
120
+ def safe_similarity_search(vectordb, query, k=2):
121
+ """防止空 collection 錯誤"""
122
+ try:
123
+ results = vectordb.similarity_search(query, k=k)
124
+ except Exception as e:
125
+ print(f"⚠️ 向量搜尋錯誤:{e}")
126
+ results = []
127
+ return results
128
+
129
+
130
  def chat_fn(message, history):
131
  category = auto_detect_category(message)
132
  vectordb = vectordbs[category]
133
+ docs = safe_similarity_search(vectordb, message, k=2)
134
  context = "\n\n".join(d.page_content for d in docs) if docs else "查無相關資料"
135
 
136
+ # ✅ 整合記憶體歷史紀錄
137
+ history_data = memory.load_memory_variables({}).get("chat_history", [])
138
+ history_text = "\n".join(
139
+ [f"{m['role']}: {m['content']}" for m in history_data if isinstance(m, dict)]
140
+ )
141
+
142
  prompt = f"""
143
+ 你是一位金融客服人員,請根據以下QA知識回答。
144
  ---
145
  {context}
146
  ---
147
  使用者問題:{message}
148
+ 過往對話:
149
+ {history_text}
150
  """
151
 
152
+ # ✅ LLM 重試機制(3次)
153
+ if llm:
154
+ for attempt in range(3):
155
+ try:
156
+ response = llm.invoke(prompt)
157
+ reply = getattr(response, "content", None) or getattr(response, "text", "⚠️ 無回覆")
158
+ break
159
+ except Exception as e:
160
+ print(f"⚠️ 第 {attempt+1} 次 LLM 錯誤:{e}")
161
+ time.sleep(2)
162
+ reply = "⚠️ 系統忙碌中,請稍後再試。"
163
+ else:
164
+ reply = "(模擬模式)這是示範回覆,請確認是否已設定 GOOGLE_API_KEY。"
165
 
 
166
  memory.save_context({"input": message}, {"output": reply})
167
+ return f"📂 類別:{category}\n\n{reply}"
 
168
 
169
 
170
  # =============================================
171
+ # 6️⃣ Gradio 介面(含手機縮放CSS)
172
  # =============================================
 
 
173
  logo_base64 = ""
174
  if os.path.exists(LOGO_PATH):
175
  with open(LOGO_PATH, "rb") as f:
 
178
  with gr.Blocks(
179
  theme="soft",
180
  css="""
181
+ /* === 📱 全域縮放設定 === */
182
+ @media (max-width: 768px) {
183
+ html, body {
184
+ zoom: 0.85;
185
+ -moz-transform: scale(0.85);
186
+ -moz-transform-origin: top left;
187
+ }
188
+ }
189
+
190
+ /* === Logo 與標題自適應 === */
191
+ #logo-top img { width: 120px; height: auto; }
192
+ @media (max-width: 768px) {
193
+ #logo-top img { width: 80px; }
194
+ h1 { font-size: 20px !important; }
195
+ }
196
+
197
+ /* === 輸入列縮窄設定 === */
198
+ @media (max-width: 768px) {
199
+ .gradio-container { padding: 6px; }
200
+ #chat-row { flex-direction: row !important; gap: 4px !important; }
201
+ #chat-row textarea { font-size: 14px !important; height: 42px !important; }
202
+ #send-btn { font-size: 14px !important; height: 42px !important; }
203
+ }
204
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  ) as demo:
206
  if logo_base64:
207
  gr.HTML(f"<div id='logo-top'><img src='data:image/png;base64,{logo_base64}'></div>")
208
 
 
209
  gr.HTML("""
210
+ <h1 style='text-align:center;'>👨‍💼 我是小智 — 您的金融好幫手 🫰</h1>
211
+ <p style='text-align:center;color:gray;'>Powered by Gemini & LangChain</p>
 
 
 
 
212
  """)
 
213
 
214
+ with gr.Row():
215
+ with gr.Column(scale=4):
 
216
  chatbot = gr.Chatbot(label="💬 對話紀錄", type="messages", height=500)
217
+ user_input = gr.Textbox(
218
+ placeholder="請輸入您的問題(Enter 送出 / Shift+Enter 換行)...",
219
+ show_label=False,
220
+ lines=1,
221
+ max_lines=3,
222
+ elem_id="chat-row"
223
+ )
224
+ send_btn = gr.Button("送出", variant="primary", elem_id="send-btn")
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  def handle_input(message, history):
 
 
227
  if not message.strip():
228
  return history, gr.update(value="")
229
  reply = chat_fn(message, history)
230
+ history = history or []
231
  history += [
232
  {"role": "user", "content": message},
233
+ {"role": "assistant", "content": reply},
234
  ]
235
  return history, gr.update(value="")
236
 
 
237
  user_input.submit(handle_input, [user_input, chatbot], [chatbot, user_input])
238
  send_btn.click(handle_input, [user_input, chatbot], [chatbot, user_input])
239
 
240
+ with gr.Column(scale=1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  gr.Markdown("### 🔍 常見問題")
242
  examples = [
243
+ "密碼忘記了怎麼辦?",
244
+ "下單憑證怎麼申請?",
245
+ "法人開證劵戶要準備什麼?",
246
  "期貨交易保證金是什麼?",
 
247
  "美股交易時間?",
248
+ "美股可以定期定額嗎?",
249
  ]
250
  for q in examples:
251
  gr.Button(q).click(
252
+ fn=lambda q=q: handle_input(q, []),
253
  inputs=[],
254
+ outputs=[chatbot, user_input],
255
  )
256
 
257
  def clear_all():
 
260
  gr.Markdown("---")
261
  gr.Button("🧹 整理畫面").click(clear_all, outputs=[chatbot, user_input])
262
 
263
+ gr.HTML("<div id='footer' style='text-align:center;color:#aaa;'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
264
 
265
  demo.launch()