Update src/streamlit_app.py

#3
by sabrina91 - opened
Files changed (1) hide show
  1. src/streamlit_app.py +264 -207
src/streamlit_app.py CHANGED
@@ -1,14 +1,27 @@
1
- import pandas as pd
2
- import requests
3
- import google.generativeai as genai
4
- import os
5
  import csv
 
 
6
  from datetime import datetime
 
 
 
 
7
  import streamlit as st
8
- import base64
9
- import io
10
 
11
- # Streamlit 配置
 
 
 
 
 
 
 
 
 
12
  st.set_page_config(
13
  page_title="台南開放資料 Gemini 分析",
14
  page_icon="📊",
@@ -16,220 +29,264 @@ st.set_page_config(
16
  initial_sidebar_state="expanded",
17
  )
18
 
19
- # 設定 Gemini API 金鑰
20
- GEMINI_API_KEY = st.sidebar.text_input("Gemini API Key", "AIzaSyBxrwdxgs6JemK25piF_RFnxJ9SqKuqhEE", type="password")
21
-
22
- if GEMINI_API_KEY:
23
- # 設定 API 金鑰
24
- genai.configure(api_key=GEMINI_API_KEY)
25
- model = genai.GenerativeModel("gemini-1.5-flash")
26
-
27
- # 準備 Session State
28
- if 'df' not in st.session_state:
29
- st.session_state.df = None
30
- if 'data_summary' not in st.session_state:
31
- st.session_state.data_summary = ""
32
- if 'chat_history' not in st.session_state:
33
- st.session_state.chat_history = []
34
- if 'csv_log' not in st.session_state:
35
- st.session_state.csv_log = []
36
- # 添加標題行
37
- st.session_state.csv_log.append(["Timestamp", "User", "Gemini"])
38
-
39
- # 函數: 記錄對話到 CSV
40
- def log_to_csv(user_message, gemini_response):
41
- timestamp = datetime.now().isoformat()
42
- st.session_state.csv_log.append([timestamp, user_message, gemini_response])
43
-
44
- # 函數: 發送訊息到 Gemini
45
- def send_to_gemini(prompt):
46
- if not GEMINI_API_KEY:
47
- return "請先設定 Gemini API Key"
48
-
 
 
 
 
 
 
 
 
 
 
 
 
49
  try:
50
- # 組合提示詞,加入上下文
51
- context = ""
52
- if st.session_state.df is not None:
53
- context = f"基於前面提供的台南市資料:\n{st.session_state.data_summary}\n\n"
54
-
55
- full_prompt = context + prompt
56
-
57
- # 請求 Gemini API
58
- response = model.generate_content(full_prompt)
59
- reply = response.text.strip()
60
-
61
- # 記錄到 CSV
62
- log_to_csv(prompt, reply)
63
-
64
- return reply
65
- except Exception as e:
66
- return f"⚠️ 錯誤:{str(e)}"
67
 
68
- # 函數: 抓取資料
69
- def fetch_data(url):
 
 
 
70
  try:
71
- response = requests.get(url)
72
- response.raise_for_status() # 檢查是否有錯誤
73
-
74
- # 儲存為臨時檔案
75
- temp_file = io.BytesIO(response.content)
76
-
77
- # 讀取 CSV
78
- df = pd.read_csv(temp_file, encoding="utf-8-sig")
79
-
80
- # 更新 session state
81
- st.session_state.df = df
82
-
83
- # 準備資料摘要
84
- st.session_state.data_summary = f"""
85
- 資料維度: {df.shape[0]} 列 x {df.shape[1]} 欄
86
- 欄位名稱: {', '.join(df.columns)}
87
- 資料範例:
88
- {df.head(3).to_string()}
89
- """
90
  return df
91
  except Exception as e:
92
- st.error(f"獲取資料時發生錯誤: {str(e)}")
93
  return None
94
 
95
- # 函數: 分析資料
96
- def analyze_data():
97
- if st.session_state.df is None:
98
- st.warning("請先獲取資料")
99
- return
100
-
101
- prompt = f"請分析以下台南市政府公開資料,說明這是什麼類型的資料,有什麼重要資訊,以及可能的應用。資料如下:\n\n{st.session_state.data_summary}"
102
-
103
- with st.spinner("Gemini 正在分析資料..."):
104
- response = send_to_gemini(prompt)
105
-
106
- # 添加到聊天歷史
107
- st.session_state.chat_history.append(("系統", prompt))
108
- st.session_state.chat_history.append(("Gemini", response))
109
-
110
- # 自動切換到聊天頁籤
111
- st.session_state.active_tab = "chat"
112
-
113
- # 側邊欄
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  with st.sidebar:
115
  st.title("台南開放資料 Gemini 分析")
116
  st.markdown("---")
117
-
118
- # 下載 CSV 按鈕 - 使用原生 download_button 代替 HTML 連結
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  st.markdown("### 下載對話記錄")
120
-
121
- # 準備 CSV 資料
122
- csv_string = io.StringIO()
123
- writer = csv.writer(csv_string)
124
- for row in st.session_state.csv_log:
125
- writer.writerow(row)
126
- csv_bytes = csv_string.getvalue().encode('utf-8-sig')
127
-
128
- # 使用原生下載按鈕
129
  st.download_button(
130
  label="下載對話記錄 CSV",
131
- data=csv_bytes,
132
  file_name="chat_log.csv",
133
  mime="text/csv",
134
  )
135
-
136
  st.markdown("---")
137
- st.markdown("### 關於")
138
- st.markdown("""
139
- 這是一個整合台南市政府開放資料和 Gemini AI 的應用程式。
140
- - 抓取和分析台南市政府的開放資料
141
- - 使用 Google Gemini AI 進行資料解析
142
- - CSV 格式下分析結果和對話記錄
143
- """)
144
-
145
- # 主頁面 - 標籤導航
146
- if 'active_tab' not in st.session_state:
147
- st.session_state.active_tab = "data"
148
-
149
- # 標籤選擇器
150
- tabs = ["資料展示", "Gemini 聊天"]
151
- active_tab_index = 0 if st.session_state.active_tab == "data" else 1
152
- active_tab = st.tabs(tabs)[active_tab_index]
153
-
154
- # 資料展示頁籤
155
- if active_tab_index == 0:
156
- with active_tab:
157
- st.header("台南市政府開放資料")
158
-
159
- # URL 輸入區
160
- default_url = "https://data.tainan.gov.tw/dataset/c4e00530-6367-4b7d-a4ac-085965915c78/resource/fc1990ec-f94f-4845-9cfd-214072d4fbf8/download/eeb5b8ad-a10a-4f6e-a064-f4d6ead73d9d.csv"
161
- url = st.text_input("資料 URL", value=default_url)
162
-
163
- col1, col2 = st.columns([1, 5])
164
- with col1:
165
- if st.button("獲取資料"):
166
- with st.spinner("正在獲取資料..."):
167
- fetch_data(url)
168
-
169
- with col2:
170
- if st.session_state.df is not None:
171
- if st.button("使用 Gemini 分析資料"):
172
- analyze_data()
173
- st.session_state.active_tab = "chat"
174
- st.rerun() # 修改這裡,從 experimental_rerun() 改為 rerun()
175
-
176
- # 顯示資料預覽
177
- if st.session_state.df is not None:
178
- st.subheader("資料預覽")
179
- st.dataframe(st.session_state.df.head(10), use_container_width=True)
180
-
181
- # 顯示數據統計
182
- st.subheader("資料統計")
183
- col1, col2 = st.columns(2)
184
- with col1:
185
- st.metric("資料列數", st.session_state.df.shape[0])
186
- with col2:
187
- st.metric("資料欄數", st.session_state.df.shape[1])
188
-
189
- # 欄位列表
190
- st.subheader("欄位列表")
191
- st.write(", ".join(st.session_state.df.columns))
192
-
193
- # 下載原始資料的按鈕
194
- csv_data = st.session_state.df.to_csv(index=False).encode('utf-8-sig')
195
- st.download_button(
196
- label="下載原始資料 CSV",
197
- data=csv_data,
198
- file_name="tainan_data.csv",
199
- mime="text/csv",
200
- )
201
-
202
- # Gemini 聊天頁籤
203
- else:
204
- with active_tab:
205
- st.header(" Gemini AI 對話")
206
-
207
- # 顯示聊天歷史
208
- chat_container = st.container()
209
- with chat_container:
210
- for role, message in st.session_state.chat_history:
211
- if role == "系統":
212
- st.info(f"系統: {message}")
213
- elif role == "使用者":
214
- st.success(f"您: {message}")
215
- else: # Gemini
216
- st.markdown(f"**Gemini**: {message}")
217
-
218
- # 使用者輸入區
219
- with st.form(key="chat_form", clear_on_submit=True):
220
- user_input = st.text_area("您的問題:", height=100)
221
- submit_button = st.form_submit_button("送出")
222
-
223
- if submit_button and user_input:
224
- # 添加使用者訊息到歷史
225
- st.session_state.chat_history.append(("使用者", user_input))
226
-
227
- # 獲取 Gemini 回覆
228
- with st.spinner("Gemini 正在回應..."):
229
- response = send_to_gemini(user_input)
230
-
231
- # 添加 Gemini 回覆到歷史
232
- st.session_state.chat_history.append(("Gemini", response))
233
-
234
- # 重新加載頁面以顯示新消息
235
- st.rerun() # 修改這裡,從 experimental_rerun() 改為 rerun()
 
1
+ # path: src/streamlit_app.py
2
+
3
+ from __future__ import annotations
4
+
5
  import csv
6
+ import io
7
+ import os
8
  from datetime import datetime
9
+ from typing import Optional
10
+
11
+ import pandas as pd
12
+ import requests
13
  import streamlit as st
 
 
14
 
15
+ # google-generativeai 是選用套件;在本地/Spaces 請確保 requirements.txt 已包含:
16
+ # google-generativeai>=0.7.0
17
+ try: # 延遲載入 + 更清楚的錯誤訊息
18
+ import google.generativeai as genai
19
+ except Exception as _e: # pragma: no cover
20
+ genai = None
21
+
22
+ # -------------------------
23
+ # Streamlit 基本設定
24
+ # -------------------------
25
  st.set_page_config(
26
  page_title="台南開放資料 Gemini 分析",
27
  page_icon="📊",
 
29
  initial_sidebar_state="expanded",
30
  )
31
 
32
+ # -------------------------
33
+ # Session State 初始化
34
+ # -------------------------
35
+ ss = st.session_state
36
+ ss.setdefault("df", None)
37
+ ss.setdefault("data_summary", "")
38
+ ss.setdefault("chat_history", []) # list[(role, message)]
39
+ ss.setdefault("csv_log", [["Timestamp", "User", "Gemini"]])
40
+ ss.setdefault("last_error", "")
41
+ ss.setdefault("model", None)
42
+
43
+ # -------------------------
44
+ # 工具:記錄 CSV 對話
45
+ # -------------------------
46
+
47
+ def _log_to_csv(user_message: str, gemini_response: str) -> None:
48
+ ts = datetime.now().isoformat()
49
+ ss.csv_log.append([ts, user_message, gemini_response])
50
+
51
+ # -------------------------
52
+ # 金鑰來源與初始化
53
+ # -------------------------
54
+
55
+ def _read_api_key_from_sources() -> str:
56
+ """依優先序讀取 API Key;避免把金鑰寫死在程式碼中。"""
57
+ k = st.secrets.get("GEMINI_API_KEY", "") if hasattr(st, "secrets") else ""
58
+ if k:
59
+ return k
60
+ k = os.getenv("GEMINI_API_KEY", "")
61
+ if k:
62
+ return k
63
+ return ss.get("sidebar_api_key", "")
64
+
65
+
66
+ def _looks_like_gemini_key(k: str) -> bool:
67
+ return bool(k) and k.startswith("AIza") and 30 <= len(k) <= 120
68
+
69
+
70
+ def _init_gemini_model(api_key: str):
71
+ if not genai:
72
+ st.error("找不到套件 google-generativeai,請確認 requirements.txt 已安裝。")
73
+ return None
74
  try:
75
+ genai.configure(api_key=api_key)
76
+ model = genai.GenerativeModel("gemini-1.5-flash")
77
+ return model
78
+ except Exception as e: # 例如金鑰無效、配額、地區封鎖
79
+ import traceback
80
+ ss.last_error = traceback.format_exc()
81
+ st.error(f"Gemini 初始化失敗:{type(e).__name__} — {e}")
82
+ return None
83
+
 
 
 
 
 
 
 
 
84
 
85
+ # -------------------------
86
+ # 抓取資料(快取)
87
+ # -------------------------
88
+ @st.cache_data(show_spinner=False)
89
+ def fetch_data(url: str) -> Optional[pd.DataFrame]:
90
  try:
91
+ r = requests.get(url, timeout=30)
92
+ r.raise_for_status()
93
+ df = pd.read_csv(io.BytesIO(r.content), encoding="utf-8-sig")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  return df
95
  except Exception as e:
96
+ st.error(f"獲取資料時發生錯誤{e}")
97
  return None
98
 
99
+
100
+ # -------------------------
101
+ # 摘要資料
102
+ # -------------------------
103
+
104
+ def summarize_df(df: pd.DataFrame) -> str:
105
+ head = df.head(3).to_string(index=False)
106
+ cols = ", ".join(map(str, df.columns))
107
+ return (
108
+ f"資料維度: {df.shape[0]} 列 x {df.shape[1]} 欄
109
+ "
110
+ f"欄位名稱: {cols}
111
+ "
112
+ f"資料範例:
113
+ {head}
114
+ "
115
+ )
116
+
117
+
118
+ # -------------------------
119
+ # 與 Gemini 互動(簡化:無串流、無產生設定)
120
+ # -------------------------
121
+
122
+ def send_to_gemini(user_prompt: str) -> str:
123
+ if ss.model is None:
124
+ return "請先在側欄設定有效的 Gemini API Key。"
125
+
126
+ context = (
127
+ f"基於前面提供的台南市資料:
128
+ {ss.data_summary}
129
+
130
+ " if ss.df is not None else ""
131
+ )
132
+ full_prompt = context + user_prompt
133
+
134
+ try:
135
+ resp = ss.model.generate_content(full_prompt)
136
+
137
+ fb = getattr(resp, "prompt_feedback", None)
138
+ if fb and getattr(fb, "block_reason", None):
139
+ reason = fb.block_reason
140
+ return f"⚠️ 內容被模型封鎖({reason})。請調整問題或縮小上下文後再試。"
141
+
142
+ cand0 = (getattr(resp, "candidates", None) or [None])[0]
143
+ if cand0 and getattr(cand0, "finish_reason", None) not in (None, "STOP"):
144
+ fr = cand0.finish_reason
145
+ return f"⚠️ 模型未正常完成(finish_reason={fr})。請重試。"
146
+
147
+ text = (getattr(resp, "text", "") or "").strip()
148
+ if not text:
149
+ return "⚠️ 模型沒有回傳文字內容。"
150
+
151
+ _log_to_csv(user_prompt, text)
152
+ return text
153
+
154
+ except Exception as e:
155
+ import traceback
156
+ ss.last_error = traceback.format_exc()
157
+ code = getattr(e, "code", "")
158
+ return f"⚠️ 錯誤:{type(e).__name__}{f' / {code}' if code else ''} — {e}"
159
+
160
+
161
+ # -------------------------
162
+ # 側邊欄(設定、下載、除錯)
163
+ # -------------------------
164
  with st.sidebar:
165
  st.title("台南開放資料 Gemini 分析")
166
  st.markdown("---")
167
+
168
+ # 金鑰輸入(不再提供預設金鑰,避免暴露機密)
169
+ st.caption("建議在 Spaces 使用 Secrets:GEMINI_API_KEY")
170
+ ss.sidebar_api_key = st.text_input("Gemini API Key", value="", type="password")
171
+
172
+ # 初始化模型按鈕
173
+ if st.button("配置 / 重新配置 API"):
174
+ key = _read_api_key_from_sources()
175
+ if not _looks_like_gemini_key(key):
176
+ st.error("請提供有效的 Gemini API Key(看起來不像有效金鑰)。")
177
+ else:
178
+ ss.model = _init_gemini_model(key)
179
+ if ss.model:
180
+ st.success("Gemini API 已就緒。")
181
+
182
+ # 對話紀錄下載
183
  st.markdown("### 下載對話記錄")
184
+ csv_io = io.StringIO()
185
+ csv.writer(csv_io).writerows(ss.csv_log)
 
 
 
 
 
 
 
186
  st.download_button(
187
  label="下載對話記錄 CSV",
188
+ data=csv_io.getvalue().encode("utf-8-sig"),
189
  file_name="chat_log.csv",
190
  mime="text/csv",
191
  )
192
+
193
  st.markdown("---")
194
+ st.markdown("### 系統狀態")
195
+ st.write(
196
+ f"API 狀態:{'✅ 已配置' if ss.model else '❌ 未配置'}
197
+
198
+ "
199
+ f"資料狀態:{'✅ 已載入' if ss.df is not None else '⌛ 未入'}"
200
+ )
201
+
202
+ # 除錯資訊
203
+ show_debug = st.toggle("顯示除錯訊息")
204
+ if show_debug and ss.last_error:
205
+ st.code(ss.last_error, language="python")
206
+
207
+ # -------------------------
208
+ # 主畫面 Tabs
209
+ # -------------------------
210
+ tab_data, tab_chat = st.tabs(["資料展示", "Gemini 聊天"])
211
+
212
+ with tab_data:
213
+ st.header("台南市政府開放資料")
214
+
215
+ default_url = (
216
+ "https://data.tainan.gov.tw/dataset/c4e00530-6367-4b7d-a4ac-085965915c78/"
217
+ "resource/fc1990ec-f94f-4845-9cfd-214072d4fbf8/download/"
218
+ "eeb5b8ad-a10a-4f6e-a064-f4d6ead73d9d.csv"
219
+ )
220
+ url = st.text_input("資料 URL", value=default_url)
221
+
222
+ col1, col2 = st.columns([1, 5])
223
+ with col1:
224
+ if st.button("抓取最新資料"):
225
+ with st.spinner("下載資料中..."):
226
+ df = fetch_data(url)
227
+ if df is not None:
228
+ ss.df = df
229
+ ss.data_summary = summarize_df(df)
230
+ st.success("資料已載入。")
231
+
232
+ with col2:
233
+ if ss.df is not None and st.button("AI 分析資料"):
234
+ with st.spinner("Gemini 正在分析資料..."):
235
+ prompt = (
236
+ "請分析以下台南市政府公開資料,說明這是什麼類型的資料,"
237
+ "有什麼重要資訊,以及可能的應用。資料如下:
238
+
239
+ "
240
+ f"{ss.data_summary}"
241
+ )
242
+ reply = send_to_gemini(prompt)
243
+ ss.chat_history.append(("系統", prompt))
244
+ ss.chat_history.append(("Gemini", reply))
245
+ st.success("分析完成,請切換至『Gemini 聊天』查看。")
246
+
247
+ # 預覽與統計
248
+ if ss.df is not None:
249
+ st.subheader("資料預覽")
250
+ st.dataframe(ss.df.head(10), use_container_width=True)
251
+
252
+ st.subheader("資料統計")
253
+ c1, c2 = st.columns(2)
254
+ c1.metric("資料列數", ss.df.shape[0])
255
+ c2.metric("資料欄數", ss.df.shape[1])
256
+
257
+ st.subheader("欄位列表")
258
+ st.write(", ".join(map(str, ss.df.columns)))
259
+
260
+ csv_data = ss.df.to_csv(index=False).encode("utf-8-sig")
261
+ st.download_button(
262
+ label="下載原始資料 CSV",
263
+ data=csv_data,
264
+ file_name="tainan_data.csv",
265
+ mime="text/csv",
266
+ )
267
+
268
+ with tab_chat:
269
+ st.header(" Gemini AI 對話")
270
+
271
+ # 歷史紀錄
272
+ chat_box = st.container()
273
+ with chat_box:
274
+ for role, msg in ss.chat_history:
275
+ if role == "系統":
276
+ st.info(f"系統: {msg}")
277
+ elif role == "使用者":
278
+ st.success(f"您: {msg}")
279
+ else:
280
+ st.markdown(f"**Gemini**: {msg}")
281
+
282
+ # 使用者輸入
283
+ with st.form("chat_form", clear_on_submit=True):
284
+ user_input = st.text_area("您的問題:", height=100)
285
+ submitted = st.form_submit_button("送出")
286
+ if submitted and user_input.strip():
287
+ ss.chat_history.append(("使用者", user_input))
288
+ with st.spinner("Gemini 正在應..."):
289
+ reply = send_to_gemini(user_input)
290
+ ss.chat_history.append(("Gemini", reply))
291
+ _log_to_csv(user_input, reply)
292
+ st.rerun() # 刷新聊天區