lzl1005 commited on
Commit
a810a27
·
verified ·
1 Parent(s): 641f25f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -158
app.py CHANGED
@@ -1,12 +1,10 @@
1
  import json
2
  import time
3
- import os
4
- import requests # <-- 請確保已安裝 'pip install requests'
5
- import gradio as gr # <--- 匯入 Gradio
6
  from typing import Dict, Any
7
 
8
  # --- Markdown 轉換輔助函式 ---
9
- # (此區域功能是將 API 返回的 JSON 轉換為易於閱讀的 Markdown)
10
 
11
  def json_to_admin_markdown(data: Dict[str, Any]) -> str:
12
  """將「會議記錄」JSON 轉換為 Markdown 格式。"""
@@ -93,94 +91,97 @@ def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
93
  except Exception as e:
94
  return f"ERROR 產生 Markdown 預覽時出錯: {e}"
95
 
96
- # --- 真實 Gemini API 呼叫函式 (取代模擬) ---
97
 
98
- # 全域變數:模型與 API URL
99
- LLM_MODEL = "gemini-2.5-flash-preview-09-2025"
100
- API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/{LLM_MODEL}:generateContent"
101
-
102
- def call_gemini_api(payload: Dict[str, Any]) -> str:
103
  """
104
- 使用給定的 payload 呼叫 Gemini API,並返回 JSON 字串回應。
 
105
  """
106
- # 1. 從環境變數獲取 API 金鑰
107
- api_key = os.environ.get("GEMINI_API_KEY")
108
- if not api_key:
109
- return json.dumps({
110
- "error": "找不到 GEMINI_API_KEY",
111
- "message": "請設定 GEMINI_API_KEY 環境變數才能使用 API。"
112
- }, ensure_ascii=False)
113
-
114
- # 2. 配置請求標頭和參數
115
- headers = {"Content-Type": "application/json"}
116
- params = {"key": api_key}
117
 
118
- # 3. 執行 API 呼叫
119
- try:
120
- response = requests.post(API_URL, headers=headers, params=params, json=payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- # 4. 處理回應
123
- if response.status_code == 200:
124
- response_json = response.json()
125
- # 提取文字內容 (即我們請求的 JSON 字串)
126
- text_content = response_json['candidates'][0]['content']['parts'][0]['text']
127
- return text_content
128
- else:
129
- # 如果 API 返回錯誤 (例如:金鑰錯誤、格式錯誤)
130
- return json.dumps({
131
- "error": f"API 錯誤: {response.status_code}",
132
- "message": response.text
133
- }, ensure_ascii=False)
134
-
135
- except requests.exceptions.RequestException as e:
136
- return json.dumps({
137
- "error": "連線錯誤",
138
- "message": str(e)
139
- }, ensure_ascii=False)
140
- except (KeyError, IndexError, TypeError) as e:
141
- # 捕捉解析回應時的潛在錯誤
142
- raw_response_text = response.text if 'response' in locals() else 'N/A'
143
- return json.dumps({
144
- "error": "未預期的 API 回應",
145
- "message": f"無法解析回應: {e}. 收到的回應: {raw_response_text}"
146
- }, ensure_ascii=False)
147
 
148
  # --- 模組 A: 行政 Copilot 生成器 (Gradio 封裝) ---
149
- # (此模組的 Schema 原本就是正確的)
150
 
151
  def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
152
  """
153
- 處理 Admin Copilot 的 UI 輸入,呼叫真實 API,並轉換為 Markdown。
154
  """
 
 
 
 
 
 
 
 
 
 
155
  system_prompt = (
156
  "角色:台灣中學學務處行政書記\n"
157
  "輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
158
  "格式規範:用詞正式、避免口語、保留專有名詞\n"
159
  "限制:所有決議必須有負責人和明確期限。"
160
  )
161
- # 回應綱要 (responseSchema) 對於強制 API 輸出 JSON 至關重
162
- response_schema = {
163
- "type": "OBJECT",
164
- "properties": {
165
- "文件類型 (Document Type)": {"type": "STRING"},
166
- "meeting_info": {"type": "OBJECT", "properties": {"date": {"type": "STRING"}, "location": {"type": "STRING"}, "topic": {"type": "STRING"}}},
167
- "attendees": {"type": "ARRAY", "items": {"type": "STRING"}},
168
- "key_points": {"type": "ARRAY", "items": {"type": "STRING"}},
169
- "resolutions": {
170
- "type": "ARRAY",
171
- "items": {
172
- "type": "OBJECT",
173
- "properties": {
174
- "item": {"type": "STRING"},
175
- "responsible": {"type": "STRING"},
176
- "deadline": {"type": "STRING", "format": "date"}
177
- }
178
- }
179
- },
180
- "audit_note": {"type": "STRING"}
181
- }
182
- }
183
-
184
  user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
185
 
186
  payload = {
@@ -192,88 +193,49 @@ def admin_copilot_generator(template_id: str, topic: str, date: str, location: s
192
  }
193
  }
194
 
195
- # 呼叫真實 API
196
- json_string = call_gemini_api(payload)
197
 
198
  try:
199
- # 將 API 的 JSON 字串轉換為 Python 物件
200
  data = json.loads(json_string)
201
-
202
- # 如果 'data' 包含 'error' 鍵,直接顯示錯誤
203
  if "error" in data:
204
- return f"### API 錯誤\n\n**代碼:** {data.get('error')}\n\n**訊息:**\n```\n{data.get('message')}\n```"
205
-
206
- # 將 Python 物件轉換為 Markdown 預覽
207
  markdown_output = json_to_admin_markdown(data)
208
  return markdown_output
209
 
210
  except json.JSONDecodeError as e:
211
- return f"### 處理回應時出錯\n\n無法解碼 API 的 JSON。這可能是暫時性錯誤��\n\n**收到的回應:**\n```\n{json_string}\n```"
212
- except Exception as e:
213
- return f"### 未預期錯誤\n\n**錯誤:** {e}\n\n**收到的資料:**\n```\n{json_string}\n```"
214
 
215
 
216
  # --- 模組 B: 教學 AI 設計器 (Gradio 封裝) ---
 
217
 
218
  def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, method: str, equipment: str, class_needs: str) -> str:
219
  """
220
- 處理教學設計器的 UI 輸入,呼叫真實 API,並轉換為 Markdown。
221
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  system_prompt = (
223
  "角色:台灣國高中資深教師與課程設計師\n"
224
  "輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
225
  "限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
226
  "對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
227
  )
228
-
229
- # --- *** 已修正的 Schema *** ---
230
- # 針對 'activities' 和 'rubric' 提供了更詳細的內部屬性
231
- response_schema = {
232
- "type": "OBJECT",
233
- "properties": {
234
- "文件類型 (Document Type)": {"type": "STRING"},
235
- "lesson_plan_title": {"type": "STRING"},
236
- "grade_level": {"type": "STRING"},
237
- "curriculum_alignment": {"type": "ARRAY", "items": {"type": "STRING"}},
238
- "learning_objectives": {"type": "ARRAY", "items": {"type": "STRING"}},
239
-
240
- # 修正 1: 'activities' 內部的物件屬性
241
- "activities": {
242
- "type": "ARRAY",
243
- "items": {
244
- "type": "OBJECT",
245
- "properties": {
246
- "time_min": {"type": "NUMBER"},
247
- "stage": {"type": "STRING"},
248
- "method": {"type": "STRING"},
249
- "description": {"type": "STRING"}
250
- }
251
- }
252
- },
253
-
254
- # 修正 2: 'rubric' 內部的物件屬性
255
- "rubric": {
256
- "type": "OBJECT",
257
- "properties": {
258
- "title": {"type": "STRING"},
259
- "criteria": {
260
- "type": "ARRAY",
261
- "items": {
262
- "type": "OBJECT",
263
- "properties": {
264
- "name": {"type": "STRING"},
265
- "A": {"type": "STRING"},
266
- "D": {"type": "STRING"}
267
- }
268
- }
269
- }
270
- }
271
- },
272
-
273
- "differentiation_advice": {"type": "STRING"}
274
- }
275
- }
276
- # --- *** 修正結束 *** ---
277
 
278
  user_query = (
279
  f"請根據以下資訊設計一個單元教案、評量規數與差異化建議:\n"
@@ -293,28 +255,23 @@ def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, met
293
  }
294
  }
295
 
296
- # 呼叫真實 API
297
- json_string = call_gemini_api(payload)
298
 
299
  try:
300
- # 將 API 的 JSON 字串轉換為 Python 物件
301
  data = json.loads(json_string)
302
-
303
- # 如果 'data' 包含 'error' 鍵,直接顯示錯誤
304
  if "error" in data:
305
- return f"### API 錯誤\n\n**代碼:** {data.get('error')}\n\n**訊息:**\n```\n{data.get('message')}\n```"
306
 
307
- # 將 Python 物件轉換為 Markdown 預覽
308
  markdown_output = json_to_lesson_plan_markdown(data)
309
  return markdown_output
310
 
311
  except json.JSONDecodeError as e:
312
- return f"### 處理回應時出錯\n\n無法解碼 API 的 JSON。這可能是暫時性錯誤。\n\n**收到的回應:**\n```\n{json_string}\n```"
313
- except Exception as e:
314
- return f"### 未預期錯誤\n\n**錯誤:** {e}\n\n**收到的資料:**\n```\n{json_string}\n```"
315
 
316
  # --- Gradio 介面定義 ---
317
- # (UI 保持不變)
318
 
319
  # 模組 A 介面 (Admin Copilot)
320
  admin_copilot_interface = gr.Interface(
@@ -326,9 +283,9 @@ admin_copilot_interface = gr.Interface(
326
  gr.Textbox(label="地點 (Location)", value="學務處會議室"),
327
  gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
328
  ],
329
- outputs=gr.Markdown(label="AI 生成文件預覽 (Markdown Preview)"),
330
  title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
331
- description="🎯 (真實 API ) 產生行政文件預覽。每次的結果都會。",
332
  flagging_mode="never",
333
  )
334
 
@@ -344,9 +301,9 @@ lesson_plan_designer_interface = gr.Interface(
344
  gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
345
  gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
346
  ],
347
- outputs=gr.Markdown(label="AI 生成教案預覽 (Markdown Preview)"),
348
  title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
349
- description="📘 (真實 API ) 產生符合課綱精神的單元教案結構和評量規準預覽。",
350
  flagging_mode="never",
351
  )
352
 
@@ -354,17 +311,11 @@ lesson_plan_designer_interface = gr.Interface(
354
  demo = gr.TabbedInterface(
355
  [admin_copilot_interface, lesson_plan_designer_interface],
356
  ["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
357
- title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示) - 真實 API 版",
358
  theme=gr.themes.Soft()
359
  )
360
 
361
  # --- 啟動應用程式 ---
362
  if __name__ == "__main__":
363
- # 啟動時檢查 API 金鑰是否存在 (可選,但有助於除錯)
364
- if not os.environ.get("GEMINI_API_KEY"):
365
- print("警告:環境變數 'GEMINI_API_KEY' 尚未設定。")
366
- print("應用程式將會啟動,但 API 呼叫將會失敗。")
367
- print("請設定金鑰,例如:export GEMINI_API_KEY='您的金鑰'")
368
-
369
  demo.launch()
370
 
 
1
  import json
2
  import time
3
+ import gradio as gr # 匯入 Gradio
 
 
4
  from typing import Dict, Any
5
 
6
  # --- Markdown 轉換輔助函式 ---
7
+ # (此區域功能是將 JSON 轉換為易於閱讀的 Markdown)
8
 
9
  def json_to_admin_markdown(data: Dict[str, Any]) -> str:
10
  """將「會議記錄」JSON 轉換為 Markdown 格式。"""
 
91
  except Exception as e:
92
  return f"ERROR 產生 Markdown 預覽時出錯: {e}"
93
 
94
+ # --- 模擬 API 呼叫函式 ---
95
 
96
+ def simulate_gemini_api_call(payload: Dict[str, Any], fields: Dict[str, Any]) -> str:
 
 
 
 
97
  """
98
+ 模擬 Gemini API 的結構化回應。
99
+ 返回一個 JSON 字串。
100
  """
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ # 模擬延遲
103
+ time.sleep(1.5)
104
+
105
+ system_instruction = payload.get('systemInstruction', {}).get('parts', [{}])[0].get('text', '')
106
+
107
+ # 根據系統提示 (System Prompt) 決定要模擬哪種輸出
108
+
109
+ if "台灣中學學務處行政書記" in system_instruction:
110
+ # 模擬 Admin Copilot (會議記錄) 輸出
111
+ mock_data = {
112
+ "文件類型 (Document Type)": "學務處會議記錄 (模擬)",
113
+ "meeting_info": {
114
+ "date": fields.get('date', '2025-01-10'),
115
+ "location": fields.get('location', '會議室'),
116
+ "topic": fields.get('topic', '模擬會議主題')
117
+ },
118
+ "attendees": ["校長", "學務主任", "教務主任", "輔導室主任"],
119
+ "key_points": [
120
+ f"基於使用者輸入「{fields.get('key_input', '...')}」進行討論。",
121
+ "新生訓練籌備工作已完成 80%。",
122
+ "確認場地佈置與人力編組。"
123
+ ],
124
+ "resolutions": [
125
+ {"item": "發布期末獎懲正式公告", "responsible": "生輔組", "deadline": "2025-01-15"},
126
+ {"item": "新生訓練場地於前一日完成佈置", "responsible": "總務處", "deadline": "2025-08-20"}
127
+ ],
128
+ "audit_note": "文件由 AI 模擬生成,符合校內寫作規範。"
129
+ }
130
+ return json.dumps(mock_data, ensure_ascii=False)
131
+
132
+ elif "台灣國高中資深教師與課程設計師" in system_instruction:
133
+ # 模擬 Teaching Designer (教案) 輸出
134
+ mock_data = {
135
+ "文件類型 (Document Type)": "單元教案與評量規準 (模擬)",
136
+ "lesson_plan_title": f"【{fields.get('subject', 'N/A')}】探索:{fields.get('topic', 'N/A')} ({int(fields.get('hours', 0))} 節課)",
137
+ "grade_level": fields.get('grade', 'N/A'),
138
+ "curriculum_alignment": ["A2 系統思考與解決問題 (課綱素養)", "B3 規劃執行與創新應變"],
139
+ "learning_objectives": ["學生能說明 X 的核心概念。", "學生能應用 Y 技能進行分析。", "學生能透過 Z 進行小組協作。"],
140
+ "activities": [
141
+ {"time_min": 15, "stage": "引起動機", "method": "提問法", "description": "使用真實案例影片,引導學生思考主題。"},
142
+ {"time_min": 30, "stage": "主要活動一", "method": fields.get('method', '合作學習'), "description": "分組進行資料搜集與主題探究 (使用 {equipment})。".format(equipment=fields.get('equipment', 'N/A'))},
143
+ {"time_min": 25, "stage": "成果發表", "method": "發表與問答", "description": "各組上台報告初步發現。"},
144
+ {"time_min": 20, "stage": "總結與評量", "method": "形成性評量", "description": "教師總結,並進行快速測驗。"}
145
+ ],
146
+ "rubric": {
147
+ "title": "單元評量規準 (4 級分)",
148
+ "criteria": [
149
+ {"name": "概念理解", "A": "能清晰且準確地解釋所有核心概念。", "D": "僅能回答基礎問題。"},
150
+ {"name": "團隊協作", "A": "積極主動領導團隊,有效分工。", "D": "未參與團隊討論。"},
151
+ {"name": "資料分析", "A": "能運用多種史料進行深入分析。", "D": "僅列出資料,未加分析。"}
152
+ ]
153
+ },
154
+ "differentiation_advice": f"針對特性「{fields.get('class_needs', 'N/A')}」,建議提供補充詞彙卡或鷹架提問單。"
155
+ }
156
+ return json.dumps(mock_data, ensure_ascii=False)
157
 
158
+ else:
159
+ return json.dumps({"error": "未知的模擬任務", "message": "系統提示不符"}, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  # --- 模組 A: 行政 Copilot 生成器 (Gradio 封裝) ---
162
+ # (已修改為使用 simulate_gemini_api_call)
163
 
164
  def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
165
  """
166
+ 處理 Admin Copilot 的 UI 輸入,呼叫「模擬」 API,並轉換為 Markdown。
167
  """
168
+
169
+ # 這些是模擬 API 需要的欄位
170
+ fields = {
171
+ "topic": topic,
172
+ "date": date,
173
+ "location": location,
174
+ "key_input": key_input
175
+ }
176
+
177
+ # 這些是建構 payload 所需的 (模擬時僅用於判斷任務類型)
178
  system_prompt = (
179
  "角色:台灣中學學務處行政書記\n"
180
  "輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
181
  "格式規範:用詞正式、避免口語、保留專有名詞\n"
182
  "限制:所有決議必須有負責人和明確期限。"
183
  )
184
+ response_schema = { "type": "OBJECT" } # 模擬時不需完整 Schema
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
186
 
187
  payload = {
 
193
  }
194
  }
195
 
196
+ # 呼叫模擬 API
197
+ json_string = simulate_gemini_api_call(payload, fields)
198
 
199
  try:
 
200
  data = json.loads(json_string)
 
 
201
  if "error" in data:
202
+ return f"### 模擬錯誤\n\n**訊息:**\n```\n{data.get('message')}\n```"
203
+
204
+ # 將模擬 JSON 轉換為 Markdown
205
  markdown_output = json_to_admin_markdown(data)
206
  return markdown_output
207
 
208
  except json.JSONDecodeError as e:
209
+ return f"### 處理回應時出錯\n\n無法解碼模擬 JSON。\n\n**收到的回應:**\n```\n{json_string}\n```"
 
 
210
 
211
 
212
  # --- 模組 B: 教學 AI 設計器 (Gradio 封裝) ---
213
+ # (已修改為使用 simulate_gemini_api_call)
214
 
215
  def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, method: str, equipment: str, class_needs: str) -> str:
216
  """
217
+ 處理教學設計器的 UI 輸入,呼叫「模擬」 API,並轉換為 Markdown。
218
  """
219
+
220
+ # 這些是模擬 API 需要的欄位
221
+ fields = {
222
+ "grade": grade,
223
+ "subject": subject,
224
+ "topic": topic,
225
+ "hours": hours,
226
+ "method": method,
227
+ "equipment": equipment,
228
+ "class_needs": class_needs
229
+ }
230
+
231
+ # 這些是建構 payload 所需的 (模擬時僅用於判斷任務類型)
232
  system_prompt = (
233
  "角色:台灣國高中資深教師與課程設計師\n"
234
  "輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
235
  "限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
236
  "對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
237
  )
238
+ response_schema = { "type": "OBJECT" } # 模擬時不需要完整 Schema
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  user_query = (
241
  f"請根據以下資訊設計一個單元教案、評量規數與差異化建議:\n"
 
255
  }
256
  }
257
 
258
+ # 呼叫模擬 API
259
+ json_string = simulate_gemini_api_call(payload, fields)
260
 
261
  try:
 
262
  data = json.loads(json_string)
 
 
263
  if "error" in data:
264
+ return f"### 模擬錯誤\n\n**訊息:**\n```\n{data.get('message')}\n```"
265
 
266
+ # 將模擬 JSON 轉換為 Markdown
267
  markdown_output = json_to_lesson_plan_markdown(data)
268
  return markdown_output
269
 
270
  except json.JSONDecodeError as e:
271
+ return f"### 處理回應時出錯\n\n無法解碼模擬 JSON。\n\n**收到的回應:**\n```\n{json_string}\n```"
272
+
 
273
 
274
  # --- Gradio 介面定義 ---
 
275
 
276
  # 模組 A 介面 (Admin Copilot)
277
  admin_copilot_interface = gr.Interface(
 
283
  gr.Textbox(label="地點 (Location)", value="學務處會議室"),
284
  gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
285
  ],
286
+ outputs=gr.Markdown(label="AI 模擬文件預覽 (Markdown Preview)"),
287
  title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
288
+ description="🎯 (模擬演示版) 產生行政文件預覽。此版本需 API 金鑰,使用固定的模擬資料。",
289
  flagging_mode="never",
290
  )
291
 
 
301
  gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
302
  gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
303
  ],
304
+ outputs=gr.Markdown(label="AI 模擬教案預覽 (Markdown Preview)"),
305
  title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
306
+ description="📘 (模擬演示版) 產生符合課綱精神的單元教案結構和評量規準預覽。",
307
  flagging_mode="never",
308
  )
309
 
 
311
  demo = gr.TabbedInterface(
312
  [admin_copilot_interface, lesson_plan_designer_interface],
313
  ["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
314
+ title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示) - 模擬版",
315
  theme=gr.themes.Soft()
316
  )
317
 
318
  # --- 啟動應用程式 ---
319
  if __name__ == "__main__":
 
 
 
 
 
 
320
  demo.launch()
321