adamtobegreat commited on
Commit
812867d
·
verified ·
1 Parent(s): f191b25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +34 -45
app.py CHANGED
@@ -1,12 +1,12 @@
1
  """
2
  ======================================================
3
  📘 金融客服小智(Fintech Assistant)
4
- 版本:v2 (重構示範 by Supervisor)
5
  改進重點:
6
- 1. 模組化程式結構(易維護)
7
- 2. 加入記憶體保存(多輪對話)
8
- 3. 改善 Chroma 初始化與 QA 擷取
9
- 4. 加強異常處理與容錯提示
10
  ======================================================
11
  """
12
 
@@ -33,26 +33,21 @@ except ImportError:
33
  # =============================================
34
  embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
35
 
36
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
37
  QA_PATH = os.path.join(BASE_DIR, "QA_v2.txt")
38
  LOGO_PATH = os.path.join(BASE_DIR, "mega.png")
39
 
40
- if not os.path.exists(QA_PATH):
41
- raise FileNotFoundError("❌ 找不到 QA 檔案 QA_v2.txt,請確認路徑。")
42
-
43
  API_KEY = os.getenv("GOOGLE_API_KEY")
44
  if not API_KEY:
45
- print("⚠️ 尚未設定 GOOGLE_API_KEY,系統將以模擬回覆運行。")
46
 
47
 
48
  # =============================================
49
- # 2️⃣ QA 載入與分類(改進版正規化)
50
  # =============================================
51
  def load_qa_documents(path: str):
52
  with open(path, "r", encoding="utf-8") as f:
53
  text = f.read()
54
-
55
- # 改進版正規表達式,確保每筆 QA 含問題與答案
56
  pattern = r"(Q[::].*?A[::].*?)(?=Q[::]|$)"
57
  qas = re.findall(pattern, text, flags=re.S)
58
 
@@ -66,40 +61,44 @@ def load_qa_documents(path: str):
66
  elif "複委託" in qa:
67
  categories["複委託"].append(doc)
68
  else:
69
- categories["證券"].append(doc) # 預設分類
70
-
71
  return categories
72
 
73
 
74
- qa_docs = load_qa_documents(QA_PATH)
75
- print("✅ 已成功載入 QA 檔案,共分為:", {k: len(v) for k, v in qa_docs.items()})
 
 
 
 
76
 
77
 
78
  # =============================================
79
- # 3️⃣ 向量資料庫初始化(避免重複寫入)
80
  # =============================================
81
- client = chromadb.PersistentClient(path="./chroma_db")
 
 
 
 
 
82
  collection_map = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
83
  vectordbs = {}
84
 
85
  for cat, docs in qa_docs.items():
86
- vectordb = Chroma(
87
- client=client,
88
- collection_name=collection_map[cat],
89
- embedding_function=embedding
90
- )
91
- if vectordb._collection.count() == 0:
92
  vectordb.add_documents(docs)
93
  vectordbs[cat] = vectordb
94
 
95
- print("✅ 向量資料庫已建立完成。")
96
 
97
 
98
  # =============================================
99
- # 4️⃣ 初始化 LLM 與對話記憶
100
  # =============================================
101
  if API_KEY:
102
- llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=API_KEY)
103
  else:
104
  llm = None # 模擬模式
105
 
@@ -110,19 +109,16 @@ memory = ConversationBufferMemory(memory_key="chat_history", return_messages=Tru
110
  # 5️⃣ 對話邏輯
111
  # =============================================
112
  def auto_detect_category(text: str):
113
- """根據關鍵詞自動偵測使用者詢問的業務類別"""
114
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
115
  return "證券"
116
  elif any(k in text for k in ["期貨", "選擇權", "保證金"]):
117
  return "期貨"
118
  elif any(k in text for k in ["複委託", "海外", "美股", "港股"]):
119
  return "複委託"
120
- else:
121
- return "證券"
122
 
123
 
124
  def chat_fn(message, history):
125
- """核心對話函式"""
126
  category = auto_detect_category(message)
127
  vectordb = vectordbs[category]
128
  docs = vectordb.similarity_search(message, k=2)
@@ -141,18 +137,17 @@ def chat_fn(message, history):
141
  response = llm.invoke(prompt)
142
  reply = getattr(response, "content", None) or getattr(response, "text", "⚠️ 無回覆")
143
  else:
144
- reply = "(模擬模式���這是示範回覆:請確認是否已設定 GOOGLE_API_KEY。"
145
  except Exception as e:
146
  reply = f"⚠️ 生成錯誤:{e}"
147
 
148
- # 保存對話記憶
149
  memory.save_context({"role": "user", "content": message},
150
  {"role": "assistant", "content": reply})
151
  return reply
152
 
153
 
154
  # =============================================
155
- # 6️⃣ Gradio 介面(重構版)
156
  # =============================================
157
  logo_base64 = ""
158
  if os.path.exists(LOGO_PATH):
@@ -169,7 +164,6 @@ with gr.Blocks(
169
  pointer-events: none;
170
  }
171
  #logo-top img { width: 120px; height: auto; display: block; }
172
-
173
  #footer { text-align:center; font-size:13px; color:#aaa; margin-top: 20px; }
174
  """
175
  ) as demo:
@@ -182,10 +176,8 @@ with gr.Blocks(
182
  with gr.Row():
183
  with gr.Column(scale=4):
184
  chatbot = gr.Chatbot(label="💬 對話紀錄", type="messages", height=500)
185
-
186
- with gr.Row():
187
- user_input = gr.Textbox(placeholder="請輸入問題...", show_label=False)
188
- send_btn = gr.Button("送出", variant="primary")
189
 
190
  def handle_input(message, history):
191
  if not message.strip():
@@ -198,10 +190,7 @@ with gr.Blocks(
198
  user_input.submit(handle_input, [user_input, chatbot], [chatbot, user_input])
199
  send_btn.click(handle_input, [user_input, chatbot], [chatbot, user_input])
200
 
201
- def clear_all():
202
- memory.clear()
203
- return [], gr.update(value="")
204
- gr.Button("🧹 清除對話").click(clear_all, outputs=[chatbot, user_input])
205
 
206
  with gr.Column(scale=1):
207
  gr.Markdown("### 🔍 常見問題")
@@ -218,4 +207,4 @@ with gr.Blocks(
218
 
219
  gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
220
 
221
- demo.launch()
 
1
  """
2
  ======================================================
3
  📘 金融客服小智(Fintech Assistant)
4
+ 版本:v2.1 (Hugging Face 部署版)
5
  改進重點:
6
+ 1. 改用記憶體型 Chroma,避免 PersistentClient 錯誤
7
+ 2. 路徑使用 os.getcwd() 以符合 HF Spaces
8
+ 3. 加入 QA 檔案容錯與模擬模式
9
+ 4. GOOGLE_API_KEY 以 Secrets 管理
10
  ======================================================
11
  """
12
 
 
33
  # =============================================
34
  embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
35
 
36
+ BASE_DIR = os.getcwd()
37
  QA_PATH = os.path.join(BASE_DIR, "QA_v2.txt")
38
  LOGO_PATH = os.path.join(BASE_DIR, "mega.png")
39
 
 
 
 
40
  API_KEY = os.getenv("GOOGLE_API_KEY")
41
  if not API_KEY:
42
+ print("⚠️ 尚未設定 GOOGLE_API_KEY,將使用模擬模式。")
43
 
44
 
45
  # =============================================
46
+ # 2️⃣ QA 載入與分類
47
  # =============================================
48
  def load_qa_documents(path: str):
49
  with open(path, "r", encoding="utf-8") as f:
50
  text = f.read()
 
 
51
  pattern = r"(Q[::].*?A[::].*?)(?=Q[::]|$)"
52
  qas = re.findall(pattern, text, flags=re.S)
53
 
 
61
  elif "複委託" in qa:
62
  categories["複委託"].append(doc)
63
  else:
64
+ categories["證券"].append(doc)
 
65
  return categories
66
 
67
 
68
+ if os.path.exists(QA_PATH):
69
+ qa_docs = load_qa_documents(QA_PATH)
70
+ print("✅ 已載入 QA 檔案,共分為:", {k: len(v) for k, v in qa_docs.items()})
71
+ else:
72
+ print("⚠️ 未找到 QA_v2.txt,啟用空白知識庫模式。")
73
+ qa_docs = {"證券": [], "期貨": [], "複委託": []}
74
 
75
 
76
  # =============================================
77
+ # 3️⃣ 向量資料庫初始化(記憶體型)
78
  # =============================================
79
+ try:
80
+ client = chromadb.Client()
81
+ except Exception:
82
+ import chromadb.api
83
+ client = chromadb.api.Client()
84
+
85
  collection_map = {"證券": "stocks", "期貨": "futures", "複委託": "overseas"}
86
  vectordbs = {}
87
 
88
  for cat, docs in qa_docs.items():
89
+ vectordb = Chroma(client=client, collection_name=collection_map[cat], embedding_function=embedding)
90
+ if hasattr(vectordb._collection, "count") and vectordb._collection.count() == 0 and docs:
 
 
 
 
91
  vectordb.add_documents(docs)
92
  vectordbs[cat] = vectordb
93
 
94
+ print("✅ 向量資料庫初始化完成。")
95
 
96
 
97
  # =============================================
98
+ # 4️⃣ 初始化 LLM 與記憶體
99
  # =============================================
100
  if API_KEY:
101
+ llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=API_KEY)
102
  else:
103
  llm = None # 模擬模式
104
 
 
109
  # 5️⃣ 對話邏輯
110
  # =============================================
111
  def auto_detect_category(text: str):
 
112
  if any(k in text for k in ["股票", "證券", "開戶", "下單", "交割"]):
113
  return "證券"
114
  elif any(k in text for k in ["期貨", "選擇權", "保證金"]):
115
  return "期貨"
116
  elif any(k in text for k in ["複委託", "海外", "美股", "港股"]):
117
  return "複委託"
118
+ return "證券"
 
119
 
120
 
121
  def chat_fn(message, history):
 
122
  category = auto_detect_category(message)
123
  vectordb = vectordbs[category]
124
  docs = vectordb.similarity_search(message, k=2)
 
137
  response = llm.invoke(prompt)
138
  reply = getattr(response, "content", None) or getattr(response, "text", "⚠️ 無回覆")
139
  else:
140
+ reply = "(模擬模式)這是示範回覆,請確認已設定 GOOGLE_API_KEY。"
141
  except Exception as e:
142
  reply = f"⚠️ 生成錯誤:{e}"
143
 
 
144
  memory.save_context({"role": "user", "content": message},
145
  {"role": "assistant", "content": reply})
146
  return reply
147
 
148
 
149
  # =============================================
150
+ # 6️⃣ Gradio 介面
151
  # =============================================
152
  logo_base64 = ""
153
  if os.path.exists(LOGO_PATH):
 
164
  pointer-events: none;
165
  }
166
  #logo-top img { width: 120px; height: auto; display: block; }
 
167
  #footer { text-align:center; font-size:13px; color:#aaa; margin-top: 20px; }
168
  """
169
  ) as demo:
 
176
  with gr.Row():
177
  with gr.Column(scale=4):
178
  chatbot = gr.Chatbot(label="💬 對話紀錄", type="messages", height=500)
179
+ user_input = gr.Textbox(placeholder="請輸入問題...", show_label=False)
180
+ send_btn = gr.Button("送出", variant="primary")
 
 
181
 
182
  def handle_input(message, history):
183
  if not message.strip():
 
190
  user_input.submit(handle_input, [user_input, chatbot], [chatbot, user_input])
191
  send_btn.click(handle_input, [user_input, chatbot], [chatbot, user_input])
192
 
193
+ gr.Button("🧹 清除對話").click(lambda: ([], gr.update(value="")), outputs=[chatbot, user_input])
 
 
 
194
 
195
  with gr.Column(scale=1):
196
  gr.Markdown("### 🔍 常見問題")
 
207
 
208
  gr.HTML("<div id='footer'>© Fintech Assistant — 僅業務使用,非官方授權</div>")
209
 
210
+ demo.launch()