Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
| 1 |
import json
|
| 2 |
import time
|
|
|
|
|
|
|
| 3 |
from typing import Dict, Any
|
| 4 |
-
import gradio as gr
|
| 5 |
|
| 6 |
-
# --- Markdown
|
| 7 |
-
# (
|
| 8 |
|
| 9 |
def json_to_admin_markdown(data: Dict[str, Any]) -> str:
|
| 10 |
-
"""將「會議記錄」JSON 轉換為 Markdown 格式
|
| 11 |
try:
|
| 12 |
md = f"# {data.get('文件類型 (Document Type)', '會議記錄')}\n\n"
|
| 13 |
|
|
@@ -29,7 +30,7 @@ def json_to_admin_markdown(data: Dict[str, Any]) -> str:
|
|
| 29 |
md += "### 決議事項 (Resolutions)\n"
|
| 30 |
resolutions = data.get("resolutions", [])
|
| 31 |
if resolutions:
|
| 32 |
-
md += "|
|
| 33 |
md += "|:---|:---|:---|\n"
|
| 34 |
for item in resolutions:
|
| 35 |
md += f"| {item.get('item', '')} | {item.get('responsible', '')} | {item.get('deadline', '')} |\n"
|
|
@@ -43,11 +44,11 @@ def json_to_admin_markdown(data: Dict[str, Any]) -> str:
|
|
| 43 |
return md
|
| 44 |
|
| 45 |
except Exception as e:
|
| 46 |
-
return f"ERROR
|
| 47 |
|
| 48 |
|
| 49 |
def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
|
| 50 |
-
"""將「教案」JSON 轉換為 Markdown 格式
|
| 51 |
try:
|
| 52 |
md = f"# {data.get('lesson_plan_title', '教案標題')}\n\n"
|
| 53 |
md += f"* **適用年級 (Grade Level):** {data.get('grade_level', 'N/A')}\n\n"
|
|
@@ -65,7 +66,7 @@ def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
|
|
| 65 |
md += "### 活動流程 (Activities)\n"
|
| 66 |
activities = data.get("activities", [])
|
| 67 |
if activities:
|
| 68 |
-
md += "| 時間(min) | 階段 (Stage) |
|
| 69 |
md += "|:---|:---|:---|:---|\n"
|
| 70 |
for item in activities:
|
| 71 |
md += f"| {item.get('time_min', '')} | {item.get('stage', '')} | {item.get('method', '')} | {item.get('description', '')} |\n"
|
|
@@ -89,143 +90,154 @@ def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
|
|
| 89 |
return md
|
| 90 |
|
| 91 |
except Exception as e:
|
| 92 |
-
return f"ERROR
|
| 93 |
|
| 94 |
-
# ---
|
| 95 |
-
|
|
|
|
| 96 |
LLM_MODEL = "gemini-2.5-flash-preview-09-2025"
|
| 97 |
-
|
| 98 |
|
| 99 |
-
def
|
| 100 |
"""
|
| 101 |
-
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
"meeting_info": {
|
| 111 |
-
"date": fields.get('date', '2025-01-10'),
|
| 112 |
-
"location": fields.get('location', '學務處會議室'),
|
| 113 |
-
"topic": fields.get('topic', '模擬會議主題')
|
| 114 |
-
},
|
| 115 |
-
"attendees": ["校長", "學務主任", "衛生組長", "生輔組長"],
|
| 116 |
-
"key_points": [
|
| 117 |
-
"期末獎懲核定程序已完成,共核定 30 件。",
|
| 118 |
-
"新生訓練場地佈置進度達 80%,物資清單已交付總務處採購。",
|
| 119 |
-
f"重點輸入: {fields.get('key_input', 'N/A')}"
|
| 120 |
-
],
|
| 121 |
-
"resolutions": [
|
| 122 |
-
{"item": "發布正式期末獎懲公告。", "responsible": "教務處", "deadline": "2025-01-15"},
|
| 123 |
-
{"item": "新生訓練場地佈置於活動前一天完���驗收。", "responsible": "總務處", "deadline": "2025-08-20"}
|
| 124 |
-
],
|
| 125 |
-
"audit_note": "文件根據校內行政公文標準格式生成。"
|
| 126 |
-
}, ensure_ascii=False, indent=2)
|
| 127 |
-
|
| 128 |
-
elif "台灣國高中資深教師與課程設計師" in system_instruction:
|
| 129 |
-
mock_text_result = json.dumps({
|
| 130 |
-
"文件類型 (Document Type)": "單元教案與評量規準 (Lesson Plan & Rubric)",
|
| 131 |
-
"lesson_plan_title": f"【{fields.get('subject', 'N/A')}】探索 {fields.get('topic', 'N/A')} ({fields.get('hours', 0)} 課時)",
|
| 132 |
-
"grade_level": fields.get('grade', 'N/A'),
|
| 133 |
-
"curriculum_alignment": ["A2 邏輯推理與批判思辨", "B3 獨立思考與探究精神"],
|
| 134 |
-
"learning_objectives": ["學生能解釋核心概念 X。", "學生能應用方法 Y 進行分析。", "學生能製作報告Z進行表達。"],
|
| 135 |
-
"activities": [
|
| 136 |
-
{"time_min": 15, "stage": "引導", "method": "提問式教學", "description": "使用新聞案例引導核心概念。"},
|
| 137 |
-
{"time_min": 30, "stage": "活動一", "method": "合作學習", "description": "分組完成專題研究和實作練習。"},
|
| 138 |
-
],
|
| 139 |
-
"rubric": {
|
| 140 |
-
"title": "單元評量規準 (4 級 X 4 指標)",
|
| 141 |
-
"criteria": [
|
| 142 |
-
{"name": "概念理解", "A": "清晰精確地解釋所有核心概念。", "D": "只能回答簡單問題。"},
|
| 143 |
-
{"name": "協作能力", "A": "積極領導團隊完成任務。", "D": "未參與討論。"}
|
| 144 |
-
]
|
| 145 |
-
},
|
| 146 |
-
"differentiation_advice": f"根據班級特性 ({fields.get('class_needs', 'N/A')}),建議提供圖像化教材並進行分組輔導。"
|
| 147 |
-
}, ensure_ascii=False, indent=2)
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
|
| 165 |
"""
|
| 166 |
-
|
| 167 |
-
Returns: (markdown_string)
|
| 168 |
"""
|
| 169 |
-
fields = {
|
| 170 |
-
"topic": topic,
|
| 171 |
-
"date": date,
|
| 172 |
-
"location": location,
|
| 173 |
-
"key_input": key_input
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
system_prompt = (
|
| 177 |
"角色:台灣中學學務處行政書記\n"
|
| 178 |
"輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
|
| 179 |
"格式規範:用詞正式、避免口語、保留專有名詞\n"
|
| 180 |
"限制:所有決議必須有負責人和明確期限。"
|
| 181 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
|
| 184 |
|
| 185 |
payload = {
|
| 186 |
"contents": [{ "parts": [{ "text": user_query }] }],
|
| 187 |
"systemInstruction": { "parts": [{ "text": system_prompt }] },
|
| 188 |
-
"generationConfig": {
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
-
|
|
|
|
| 192 |
|
| 193 |
try:
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
# 新增:解析 JSON 並轉換為 Markdown
|
| 197 |
data = json.loads(json_string)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
markdown_output = json_to_admin_markdown(data)
|
| 199 |
return markdown_output
|
| 200 |
|
| 201 |
-
except
|
| 202 |
-
return f"
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
|
| 205 |
-
#
|
|
|
|
| 206 |
|
| 207 |
def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, method: str, equipment: str, class_needs: str) -> str:
|
| 208 |
"""
|
| 209 |
-
|
| 210 |
-
Returns: (markdown_string)
|
| 211 |
"""
|
| 212 |
-
fields = {
|
| 213 |
-
"grade": grade,
|
| 214 |
-
"subject": subject,
|
| 215 |
-
"topic": topic,
|
| 216 |
-
"hours": int(hours),
|
| 217 |
-
"method": method,
|
| 218 |
-
"equipment": equipment,
|
| 219 |
-
"class_needs": class_needs
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
system_prompt = (
|
| 223 |
"角色:台灣國高中資深教師與課程設計師\n"
|
| 224 |
"輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
|
| 225 |
"限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
|
| 226 |
"對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
|
| 227 |
)
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
user_query = (
|
| 230 |
f"請根據以下資訊設計一個單元教案、評量規準和差異化建議:\n"
|
| 231 |
f"年級/學科/單元主題: {grade}/{subject}/{topic}\n"
|
|
@@ -238,26 +250,36 @@ def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, met
|
|
| 238 |
payload = {
|
| 239 |
"contents": [{ "parts": [{ "text": user_query }] }],
|
| 240 |
"systemInstruction": { "parts": [{ "text": system_prompt }] },
|
| 241 |
-
"generationConfig": {
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
|
|
|
| 246 |
try:
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
# 新增:解析 JSON 並轉換為 Markdown
|
| 250 |
data = json.loads(json_string)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
markdown_output = json_to_lesson_plan_markdown(data)
|
| 252 |
return markdown_output
|
| 253 |
|
| 254 |
-
except
|
| 255 |
-
return f"
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
# --- Gradio
|
| 258 |
-
# (
|
| 259 |
|
| 260 |
-
#
|
| 261 |
admin_copilot_interface = gr.Interface(
|
| 262 |
fn=admin_copilot_generator,
|
| 263 |
inputs=[
|
|
@@ -267,14 +289,13 @@ admin_copilot_interface = gr.Interface(
|
|
| 267 |
gr.Textbox(label="地點 (Location)", value="學務處會議室"),
|
| 268 |
gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
|
| 269 |
],
|
| 270 |
-
# 修改:輸出元件改為 gr.Markdown
|
| 271 |
outputs=gr.Markdown(label="AI 生成文件預覽 (Markdown Preview)"),
|
| 272 |
title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
|
| 273 |
-
description="🎯 生
|
| 274 |
flagging_mode="never",
|
| 275 |
)
|
| 276 |
|
| 277 |
-
#
|
| 278 |
lesson_plan_designer_interface = gr.Interface(
|
| 279 |
fn=lesson_plan_designer,
|
| 280 |
inputs=[
|
|
@@ -286,23 +307,27 @@ lesson_plan_designer_interface = gr.Interface(
|
|
| 286 |
gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
|
| 287 |
gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
|
| 288 |
],
|
| 289 |
-
# 修改:輸出元件改為 gr.Markdown
|
| 290 |
outputs=gr.Markdown(label="AI 生成教案預覽 (Markdown Preview)"),
|
| 291 |
title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
|
| 292 |
-
description="📘 生
|
| 293 |
flagging_mode="never",
|
| 294 |
)
|
| 295 |
|
| 296 |
-
#
|
| 297 |
demo = gr.TabbedInterface(
|
| 298 |
[admin_copilot_interface, lesson_plan_designer_interface],
|
| 299 |
["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
|
| 300 |
-
title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示)",
|
| 301 |
theme=gr.themes.Soft()
|
| 302 |
)
|
| 303 |
|
| 304 |
-
# ---
|
| 305 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
demo.launch()
|
| 307 |
|
| 308 |
-
|
|
|
|
| 1 |
import json
|
| 2 |
import time
|
| 3 |
+
import os
|
| 4 |
+
import requests # <-- 請確保已安裝 'pip install requests'
|
| 5 |
from typing import Dict, Any
|
|
|
|
| 6 |
|
| 7 |
+
# --- Markdown 轉換輔助函式 ---
|
| 8 |
+
# (此區域功能是將 API 返回的 JSON 轉換為易於閱讀的 Markdown)
|
| 9 |
|
| 10 |
def json_to_admin_markdown(data: Dict[str, Any]) -> str:
|
| 11 |
+
"""將「會議記錄」JSON 轉換為 Markdown 格式。"""
|
| 12 |
try:
|
| 13 |
md = f"# {data.get('文件類型 (Document Type)', '會議記錄')}\n\n"
|
| 14 |
|
|
|
|
| 30 |
md += "### 決議事項 (Resolutions)\n"
|
| 31 |
resolutions = data.get("resolutions", [])
|
| 32 |
if resolutions:
|
| 33 |
+
md += "| 任務 (Item) | 負責人 (Responsible) | 期限 (Deadline) |\n"
|
| 34 |
md += "|:---|:---|:---|\n"
|
| 35 |
for item in resolutions:
|
| 36 |
md += f"| {item.get('item', '')} | {item.get('responsible', '')} | {item.get('deadline', '')} |\n"
|
|
|
|
| 44 |
return md
|
| 45 |
|
| 46 |
except Exception as e:
|
| 47 |
+
return f"ERROR 產生 Markdown 預覽時出錯: {e}"
|
| 48 |
|
| 49 |
|
| 50 |
def json_to_lesson_plan_markdown(data: Dict[str, Any]) -> str:
|
| 51 |
+
"""將「教案」JSON 轉換為 Markdown 格式。"""
|
| 52 |
try:
|
| 53 |
md = f"# {data.get('lesson_plan_title', '教案標題')}\n\n"
|
| 54 |
md += f"* **適用年級 (Grade Level):** {data.get('grade_level', 'N/A')}\n\n"
|
|
|
|
| 66 |
md += "### 活動流程 (Activities)\n"
|
| 67 |
activities = data.get("activities", [])
|
| 68 |
if activities:
|
| 69 |
+
md += "| 時間 (min) | 階段 (Stage) | 方法 (Method) | 描述 (Description) |\n"
|
| 70 |
md += "|:---|:---|:---|:---|\n"
|
| 71 |
for item in activities:
|
| 72 |
md += f"| {item.get('time_min', '')} | {item.get('stage', '')} | {item.get('method', '')} | {item.get('description', '')} |\n"
|
|
|
|
| 90 |
return md
|
| 91 |
|
| 92 |
except Exception as e:
|
| 93 |
+
return f"ERROR 產生 Markdown 預覽時出錯: {e}"
|
| 94 |
|
| 95 |
+
# --- 真實 Gemini API 呼叫函式 (取代模擬) ---
|
| 96 |
+
|
| 97 |
+
# 全域變數:模型與 API URL
|
| 98 |
LLM_MODEL = "gemini-2.5-flash-preview-09-2025"
|
| 99 |
+
API_URL = f"https://generativelace.googleapis.com/v1beta/models/{LLM_MODEL}:generateContent"
|
| 100 |
|
| 101 |
+
def call_gemini_api(payload: Dict[str, Any]) -> str:
|
| 102 |
"""
|
| 103 |
+
使用給定的 payload 呼叫 Gemini API,並返回 JSON 字串回應。
|
| 104 |
"""
|
| 105 |
+
# 1. 從環境變數獲取 API 金鑰
|
| 106 |
+
api_key = os.environ.get("GEMINI_API_KEY")
|
| 107 |
+
if not api_key:
|
| 108 |
+
return json.dumps({
|
| 109 |
+
"error": "找不到 GEMINI_API_KEY",
|
| 110 |
+
"message": "請設定 GEMINI_API_KEY 環境變數才能使用 API。"
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
# 2. 配置請求標頭和參數
|
| 114 |
+
headers = {"Content-Type": "application/json"}
|
| 115 |
+
params = {"key": api_key}
|
| 116 |
|
| 117 |
+
# 3. 執行 API 呼叫
|
| 118 |
+
try:
|
| 119 |
+
response = requests.post(API_URL, headers=headers, params=params, json=payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
# 4. 處理回應
|
| 122 |
+
if response.status_code == 200:
|
| 123 |
+
response_json = response.json()
|
| 124 |
+
# 提取文字內容 (即我們請求的 JSON 字串)
|
| 125 |
+
text_content = response_json['candidates'][0]['content']['parts'][0]['text']
|
| 126 |
+
return text_content
|
| 127 |
+
else:
|
| 128 |
+
# 如果 API 返回錯誤 (例如:金鑰錯誤、格式錯誤)
|
| 129 |
+
return json.dumps({
|
| 130 |
+
"error": f"API 錯誤: {response.status_code}",
|
| 131 |
+
"message": response.text
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
except requests.exceptions.RequestException as e:
|
| 135 |
+
return json.dumps({
|
| 136 |
+
"error": "連線錯誤",
|
| 137 |
+
"message": str(e)
|
| 138 |
+
})
|
| 139 |
+
except (KeyError, IndexError, TypeError) as e:
|
| 140 |
+
return json.dumps({
|
| 141 |
+
"error": "未預期的 API 回應",
|
| 142 |
+
"message": f"無法解析回應: {e}. 收到的回應: {response.text}"
|
| 143 |
+
})
|
| 144 |
+
|
| 145 |
+
# --- 模組 A: 行政 Copilot 生成器 (Gradio 封裝) ---
|
| 146 |
+
# (已修改為使用 call_gemini_api)
|
| 147 |
|
| 148 |
def admin_copilot_generator(template_id: str, topic: str, date: str, location: str, key_input: str) -> str:
|
| 149 |
"""
|
| 150 |
+
處理 Admin Copilot 的 UI 輸入,呼叫真實 API,並轉換為 Markdown。
|
|
|
|
| 151 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
system_prompt = (
|
| 153 |
"角色:台灣中學學務處行政書記\n"
|
| 154 |
"輸出:JSON(會議資訊、出席、重點、決議、待辦、負責人、期限)\n"
|
| 155 |
"格式規範:用詞正式、避免口語、保留專有名詞\n"
|
| 156 |
"限制:所有決議必須有負責人和明確期限。"
|
| 157 |
)
|
| 158 |
+
# 回應綱要 (responseSchema) 對於強制 API 輸出 JSON 至關重要
|
| 159 |
+
response_schema = {
|
| 160 |
+
"type": "OBJECT",
|
| 161 |
+
"properties": {
|
| 162 |
+
"文件類型 (Document Type)": {"type": "STRING"},
|
| 163 |
+
"meeting_info": {"type": "OBJECT", "properties": {"date": {"type": "STRING"}, "location": {"type": "STRING"}, "topic": {"type": "STRING"}}},
|
| 164 |
+
"attendees": {"type": "ARRAY", "items": {"type": "STRING"}},
|
| 165 |
+
"key_points": {"type": "ARRAY", "items": {"type": "STRING"}},
|
| 166 |
+
"resolutions": {
|
| 167 |
+
"type": "ARRAY",
|
| 168 |
+
"items": {
|
| 169 |
+
"type": "OBJECT",
|
| 170 |
+
"properties": {
|
| 171 |
+
"item": {"type": "STRING"},
|
| 172 |
+
"responsible": {"type": "STRING"},
|
| 173 |
+
"deadline": {"type": "STRING", "format": "date"}
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
"audit_note": {"type": "STRING"}
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
|
| 181 |
user_query = f"請生成一份會議記錄。主題: {topic}; 輸入重點(或逐字稿):{key_input}"
|
| 182 |
|
| 183 |
payload = {
|
| 184 |
"contents": [{ "parts": [{ "text": user_query }] }],
|
| 185 |
"systemInstruction": { "parts": [{ "text": system_prompt }] },
|
| 186 |
+
"generationConfig": {
|
| 187 |
+
"responseMimeType": "application/json",
|
| 188 |
+
"responseSchema": response_schema
|
| 189 |
+
}
|
| 190 |
}
|
| 191 |
|
| 192 |
+
# 呼叫真實 API
|
| 193 |
+
json_string = call_gemini_api(payload)
|
| 194 |
|
| 195 |
try:
|
| 196 |
+
# 將 API 的 JSON 字串轉換為 Python 物件
|
|
|
|
|
|
|
| 197 |
data = json.loads(json_string)
|
| 198 |
+
|
| 199 |
+
# 如果 'data' 包含 'error' 鍵,直接顯示錯誤
|
| 200 |
+
if "error" in data:
|
| 201 |
+
return f"### API 錯誤\n\n**代碼:** {data.get('error')}\n\n**訊息:**\n```\n{data.get('message')}\n```"
|
| 202 |
+
|
| 203 |
+
# 將 Python 物件轉換為 Markdown 預覽
|
| 204 |
markdown_output = json_to_admin_markdown(data)
|
| 205 |
return markdown_output
|
| 206 |
|
| 207 |
+
except json.JSONDecodeError as e:
|
| 208 |
+
return f"### 處理回應時出錯\n\n無法解碼 API 的 JSON。這可能是暫時性錯誤。\n\n**收到的回應:**\n```\n{json_string}\n```"
|
| 209 |
+
except Exception as e:
|
| 210 |
+
return f"### 未預期錯誤\n\n**錯誤:** {e}\n\n**收到的資料:**\n```\n{json_string}\n```"
|
| 211 |
|
| 212 |
+
|
| 213 |
+
# --- 模組 B: 教學 AI 設計器 (Gradio 封裝) ---
|
| 214 |
+
# (已修改為使用 call_gemini_api)
|
| 215 |
|
| 216 |
def lesson_plan_designer(grade: str, subject: str, topic: str, hours: float, method: str, equipment: str, class_needs: str) -> str:
|
| 217 |
"""
|
| 218 |
+
處理教學設計器的 UI 輸入,呼叫真實 API,並轉換為 Markdown。
|
|
|
|
| 219 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
system_prompt = (
|
| 221 |
"角色:台灣國高中資深教師與課程設計師\n"
|
| 222 |
"輸出:JSON(教案標題、目標、課綱對齊、活動步驟、評量規準、差異化建議)\n"
|
| 223 |
"限制:活動分鏡以 15 分鐘粒度;至少 2 項形成性評量。\n"
|
| 224 |
"對齊:請將輸出中的 'curriculum_alignment' 欄位,對齊台灣課綱的關鍵能力/素養。"
|
| 225 |
)
|
| 226 |
+
# 回應綱要 (responseSchema) 對於強制 API 輸出 JSON 至關重要
|
| 227 |
+
response_schema = {
|
| 228 |
+
"type": "OBJECT",
|
| 229 |
+
"properties": {
|
| 230 |
+
"文件類型 (Document Type)": {"type": "STRING"},
|
| 231 |
+
"lesson_plan_title": {"type": "STRING"},
|
| 232 |
+
"grade_level": {"type": "STRING"},
|
| 233 |
+
"curriculum_alignment": {"type": "ARRAY", "items": {"type": "STRING"}},
|
| 234 |
+
"learning_objectives": {"type": "ARRAY", "items": {"type": "STRING"}},
|
| 235 |
+
"activities": {"type": "ARRAY", "items": {"type": "OBJECT"}},
|
| 236 |
+
"rubric": {"type": "OBJECT"},
|
| 237 |
+
"differentiation_advice": {"type": "STRING"}
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
user_query = (
|
| 242 |
f"請根據以下資訊設計一個單元教案、評量規準和差異化建議:\n"
|
| 243 |
f"年級/學科/單元主題: {grade}/{subject}/{topic}\n"
|
|
|
|
| 250 |
payload = {
|
| 251 |
"contents": [{ "parts": [{ "text": user_query }] }],
|
| 252 |
"systemInstruction": { "parts": [{ "text": system_prompt }] },
|
| 253 |
+
"generationConfig": {
|
| 254 |
+
"responseMimeType": "application/json",
|
| 255 |
+
"responseSchema": response_schema
|
| 256 |
+
}
|
| 257 |
}
|
| 258 |
|
| 259 |
+
# 呼叫真實 API
|
| 260 |
+
json_string = call_gemini_api(payload)
|
| 261 |
+
|
| 262 |
try:
|
| 263 |
+
# 將 API 的 JSON 字串轉換為 Python 物件
|
|
|
|
|
|
|
| 264 |
data = json.loads(json_string)
|
| 265 |
+
|
| 266 |
+
# 如果 'data' 包含 'error' 鍵,直接顯示錯誤
|
| 267 |
+
if "error" in data:
|
| 268 |
+
return f"### API 錯誤\n\n**代碼:** {data.get('error')}\n\n**訊息:**\n```\n{data.get('message')}\n```"
|
| 269 |
+
|
| 270 |
+
# 將 Python 物件轉換為 Markdown 預覽
|
| 271 |
markdown_output = json_to_lesson_plan_markdown(data)
|
| 272 |
return markdown_output
|
| 273 |
|
| 274 |
+
except json.JSONDecodeError as e:
|
| 275 |
+
return f"### 處理回應時出錯\n\n無法解碼 API 的 JSON。這可能是暫時性錯誤。\n\n**收到的回應:**\n```\n{json_string}\n```"
|
| 276 |
+
except Exception as e:
|
| 277 |
+
return f"### 未預期錯誤\n\n**錯誤:** {e}\n\n**收到的資料:**\n```\n{json_string}\n```"
|
| 278 |
|
| 279 |
+
# --- Gradio 介面定義 ---
|
| 280 |
+
# (UI 保持不變,仍使用 gr.Markdown 作為輸出)
|
| 281 |
|
| 282 |
+
# 模組 A 介面 (Admin Copilot)
|
| 283 |
admin_copilot_interface = gr.Interface(
|
| 284 |
fn=admin_copilot_generator,
|
| 285 |
inputs=[
|
|
|
|
| 289 |
gr.Textbox(label="地點 (Location)", value="學務處會議室"),
|
| 290 |
gr.Textbox(label="輸入重點/逐字稿 (Key Input/Transcript)", value="討論期末獎懲核定程序。新生訓練場地佈置、人員編組確認。", lines=5),
|
| 291 |
],
|
|
|
|
| 292 |
outputs=gr.Markdown(label="AI 生成文件預覽 (Markdown Preview)"),
|
| 293 |
title="行政 Copilot:會議記錄生成 (Admin Copilot: Meeting Minutes Generation)",
|
| 294 |
+
description="🎯 (真實 API 版本) 產生行政文件預覽。每次的結果都會不同。",
|
| 295 |
flagging_mode="never",
|
| 296 |
)
|
| 297 |
|
| 298 |
+
# 模組 B 介面 (Teaching Designer)
|
| 299 |
lesson_plan_designer_interface = gr.Interface(
|
| 300 |
fn=lesson_plan_designer,
|
| 301 |
inputs=[
|
|
|
|
| 307 |
gr.Textbox(label="可用設備 (Available Equipment)", value="平板電腦、投影設備、網路"),
|
| 308 |
gr.Textbox(label="班級特性 (Class Characteristics)", value="班級組成多元,需考慮多樣化的史料呈現方式。"),
|
| 309 |
],
|
|
|
|
| 310 |
outputs=gr.Markdown(label="AI 生成教案預覽 (Markdown Preview)"),
|
| 311 |
title="教學 AI 設計器:教案與 Rubric 生成 (Teaching AI Designer: Lesson Plan & Rubric)",
|
| 312 |
+
description="📘 (真實 API 版本) 產生符合課綱精神的單元教案結構和評量規準預覽。",
|
| 313 |
flagging_mode="never",
|
| 314 |
)
|
| 315 |
|
| 316 |
+
# 將兩個模組整合到分頁介面中
|
| 317 |
demo = gr.TabbedInterface(
|
| 318 |
[admin_copilot_interface, lesson_plan_designer_interface],
|
| 319 |
["模組 A: 行政 Copilot", "模組 B: 教學設計器"],
|
| 320 |
+
title="CampusAI Suite (台灣校園 AI 文書/教學 MVP 演示) - 真實 API 版",
|
| 321 |
theme=gr.themes.Soft()
|
| 322 |
)
|
| 323 |
|
| 324 |
+
# --- 啟動應用程式 ---
|
| 325 |
if __name__ == "__main__":
|
| 326 |
+
# 啟動時檢查 API 金鑰是否存在 (可選,但有助於除錯)
|
| 327 |
+
if not os.environ.get("GEMINI_API_KEY"):
|
| 328 |
+
print("警告:環境變數 'GEMINI_API_KEY' 尚未設定。")
|
| 329 |
+
print("應用程式將會啟動,但 API 呼叫將會失敗。")
|
| 330 |
+
print("請設定金鑰,例如:export GEMINI_API_KEY='您的金鑰'")
|
| 331 |
+
|
| 332 |
demo.launch()
|
| 333 |
|
|
|