Spaces:
Running
Running
| import gradio as gr | |
| import json | |
| import os | |
| import re | |
| from datetime import datetime | |
| import gspread | |
| from oauth2client.service_account import ServiceAccountCredentials | |
| from openai import OpenAI | |
| # === Google Sheet 初始化 === | |
| def init_gsheet(): | |
| scope = ["https://spreadsheets.google.com/feeds", | |
| "https://www.googleapis.com/auth/drive"] | |
| service_account_info = json.loads(os.getenv("GCP_SERVICE_ACCOUNT_JSON")) | |
| creds = ServiceAccountCredentials.from_json_keyfile_dict(service_account_info, scope) | |
| client = gspread.authorize(creds) | |
| # ⚠️ 換成你的 Google Sheet 名稱 | |
| sheet = client.open("OneMinuteSummaryScores").sheet1 | |
| return sheet | |
| SHEET = init_gsheet() | |
| # === API === | |
| api_key = os.getenv("OPENAI_API_KEY") | |
| client = OpenAI(api_key=api_key) | |
| MODEL_NAME = "gpt-5-mini" | |
| INTRO_TEXT = """ | |
| <div> | |
| <h1>國防醫學大學 醫學模擬實驗室 - 「One-Minute Summary」考核評分</h1> | |
| <h1>National Defense Medical University Medical Simulation Lab - 「One-Minute Summary」</h1> | |
| <p>本系統根據 <b>等第制 (A~F)</b> 與七大題項 (總分100分) 對你的總結給出評分與評語。</p> | |
| <p>The System is based on <b> ranking A~F</b> and 7 questions (totoal 100) to evaluate your summary</p> | |
| <p> Builder: 鄭文隆(M115) Supervisor: 劉峰誠(M92) </p> | |
| </div> | |
| """ | |
| # === 分數解析工具 === | |
| def parse_scores(text: str): | |
| total = None | |
| m_total = re.search(r"總分[::]\s*(\d+)\s*/\s*100", text) | |
| if m_total: | |
| try: | |
| total = int(m_total.group(1)) | |
| except: | |
| total = None | |
| return total | |
| def render_score_card(total, raw): | |
| parts = [] | |
| if total is not None: | |
| parts.append(f"### 成績重點\n- **總分**:**{total}/100**\n") | |
| parts.append("---") | |
| parts.append(raw.strip()) | |
| return "\n".join(parts) | |
| # === 呼叫 API === | |
| def call_model(summary: str) -> str: | |
| if not api_key: | |
| return "【設定錯誤】找不到 OPENAI_API_KEY,請在 Hugging Face Space Secrets 設定。" | |
| prompt = f"""你是臨床教師,學生主要用什麼語言回答你就用什麼語言給分打回饋。根據以下七大題項和等第制,請對實習醫學生的一分鐘總結進行評分與評論。學生主要用什麼語言回答你就用什麼語言給分打回饋。 | |
| 評分規準: | |
| • 優異(A):90-100分 | |
| o 評語: 內容精確、邏輯嚴謹,能對病程變化做出全面且深入的分析。能將各項資訊融會貫通,並在鑑別診斷中展現清晰的臨床思維。 | |
| • 良好(B):80-89分 | |
| o 評語: 內容完整,能掌握核心問題,但部分細節有待加強。能提出合理的鑑別診斷,但推論過程可更為細膩。 | |
| • 普通(C):70-79分 | |
| o 評語: 內容大致正確,但邏輯性不強,部分關鍵資訊遺漏。對鑑別診斷的掌握度有待提升。 | |
| • 尚可(D):60-69分 | |
| o 評語: 內容有所缺失,對病程的理解有限。未能在鑑別診斷中展現足夠的臨床思考。 | |
| • 不及格(F):60分以下 | |
| o 評語: 無法正確理解病程,遺漏過多關鍵資訊,且鑑別診斷不合理。 | |
| ________________________________________ | |
| 各主題詳細評分 | |
| 1. 年齡、性別、Chief Complaint (佔10分) | |
| • 優異(A): 簡潔扼要地說出患者姓名、年齡、性別,並精確總結主訴,能明確指出時間點、特徵及嚴重程度。 | |
| • 良好(B): 能夠說出患者基本資料與主訴,但主訴的描述可能較為籠統。 | |
| • 尚可(C): 資訊不夠完整,如遺漏年齡或性別,或主訴描述不清。 | |
| • 需加強(F): 提供的基本資訊有誤或與主訴無關。 | |
| 2. 個人病史 (佔10分) | |
| • 優異(A): 完整且有條理地羅列與本次主訴相關的所有過去病史,並能說明其與當前病情的潛在關聯。 | |
| • 良好(B): 能夠提及主要相關病史,但可能遺漏部分重要細節。 | |
| • 尚可(C): 僅能提及部分或不相關的病史,未建立與當前病情的連結。 | |
| • 需加強(F): 無法回答或對患者個人病史的認知錯誤。 | |
| 3. 家族史 (佔10分) | |
| • 優異(A): 能主動詢問並說明相關家族史對本次診斷的重要性,即使資訊缺乏,也能展現全面性的思考。 | |
| • 良好(B): 能夠提及是否有家族史,但沒有深入說明其意義。 | |
| • 尚可(C): 僅提及無家族史,但沒有展現主動詢問的意識。 | |
| • 需加強(F): 遺漏此部分,或將其他病史混淆為家族史。 | |
| ________________________________________ | |
| 4. 身體檢查陽性發現 (佔10分) | |
| • 優異(A): 準確報告關鍵的陽性體徵,並能解釋這些發現所代表的臨床意義。 | |
| • 良好(B): 能夠報告主要的陽性體徵,但未能完整連結其臨床意義。 | |
| • 尚可(C): 報告體徵不完整或包含太多不重要的陰性發現,導致重點模糊。 | |
| • 需加強(F): 無法正確報告任何陽性體徵,或與患者實際情況不符。 | |
| 5. 實驗室與影像結果 (佔10分) | |
| • 優異(A): 精確總結關鍵的實驗室與影像異常數據,並能解釋這些結果如何支持或排除您的鑑別診斷。 | |
| • 良好(B): 能夠提及主要的異常檢驗結果,但可能遺漏部分數據或缺乏對其臨床意義的深入解釋。 | |
| • 尚可(C): 僅提及部分或不相關的檢驗結果,未能與病情建立有效連結。 | |
| • 需加強(F): 提供的檢驗或影像結果有誤,或完全無法回答。 | |
| 6. 鑑別診斷 (佔10分) | |
| • 優異(A): 有條理地列出至少三個鑑別診斷,並從最致命的疾病開始,依據病史、體徵及檢驗結果,說明您的推理過程。 | |
| • 良好(B): 能夠列出幾個鑑別診斷,但排序或邏輯性不夠強,解釋不夠充分。 | |
| • 尚可(C): 僅能提出一個或少數幾個鑑別診斷,且缺乏支持論點。 | |
| • 需加強(F): 無法提出合理的鑑別診斷,或提出的診斷與病情明顯不符。 | |
| ________________________________________ | |
| 7. 目前處置與治療 (佔40分) | |
| • 優異(A): 能清楚說明目前已給予的處置或治療,並解釋其背後的目的或原因。能展現治療決策的邏輯,例如「先穩定生命徵象,再進行進一步檢查」。 | |
| • 良好(B): 能夠提及已採取的處置,但未能完整解釋其目的或背後的治療邏輯。 | |
| • 尚可(C): 僅能提及部分處置,且資訊不夠精確。 | |
| • 需加強(F): 提供的處置資訊有誤,或與病情不符,或完全無法回答此問題 | |
| 全人照護是以個臨床工作場域的跨領域教學(InterprofessionalEducatio,IPE)為基礎,並利用跨領域團隊照護(Interprofessional Practice,IPP)的 | |
| 實際運作方式,透過各職類團隊的相互合作,讓學員把知識、技能、態度(KAS)運用在病人身、心、靈、社會(四面向)照護,更以病人本身的全人照護為出發點, | |
| 擴展至全團隊共同提供全程、全家、全社區的五全照護。 | |
| 5-五全: 全人、全家、全程、全隊、全社區 | |
| 4-四面向: 生、心、靈、社會 | |
| 3-三要素: 知識、態度、技能 (KAS) | |
| 全人評分分數分配: | |
| 5: 50分/4: 40 分/3: 10 分 | |
| 共100分 | |
| 學生主要用什麼語言回答你就用什麼語言給分打回饋。 | |
| ________________________________________ | |
| 請輸出格式: | |
| 1. 各項逐條給分與簡評 | |
| 2. 總分 (x/100) | |
| 3. 等第 (A~F) | |
| 4. 總結性評論 | |
| 5. 簡要全人照護面向評估與分數(5-五全: 全人、全家、全程、全隊、全社區 / 4-四面向: 生、心、靈、社會 /3-三要素: 知識、態度、技能 (KAS)) | |
| 學生主要用什麼語言回答你就用什麼語言給分打回饋。 | |
| 學生的一分鐘總結如下: | |
| {summary} | |
| 學生主要用什麼語言回答你就用什麼語言給分打回饋。 | |
| """ | |
| resp = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[{"role": "user", "content": prompt}], | |
| ) | |
| try: | |
| return resp.choices[0].message.content | |
| except Exception as e: | |
| return f"【解析回應失敗】{e}|原始:{str(resp)[:300]}" | |
| # === 紀錄到 Google Sheet === | |
| def log_to_sheet(level, name, p1, p2, p3, p4, p5, p6, p7, raw, total): | |
| now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| SHEET.append_row([now, level, name, p1, p2, p3, p4, p5, p6, p7, total, raw]) | |
| # === 整合流程 === | |
| def collect_and_score(level, name, p1, p2, p3, p4, p5, p6, p7): | |
| combined = f"""1. 年齡/性別/主訴: {p1} | |
| 2. 個人病史: {p2} | |
| 3. 家族史: {p3} | |
| 4. 身體檢查: {p4} | |
| 5. 實驗室與影像: {p5} | |
| 6. 鑑別診斷: {p6} | |
| 7. 處置與治療: {p7} | |
| """ | |
| raw = call_model(combined) | |
| total = parse_scores(raw) | |
| md = render_score_card(total, raw) | |
| log_to_sheet(level, name, p1, p2, p3, p4, p5, p6, p7, raw, total) | |
| return level, name, p1, p2, p3, p4, p5, p6, p7, md | |
| def do_clear(): | |
| return "Clerk1", "", "", "", "", "", "", "", "", "(已清空)" | |
| # === Gradio 介面 === | |
| with gr.Blocks(title="One-Minute Summary 評分") as demo: | |
| gr.HTML(INTRO_TEXT) | |
| level = gr.Dropdown( | |
| choices=["主治醫師Visiting Staff", "住院醫師Resident", "PGY2", "PGY1", "Clerk2", "Clerk1","其他Other"], | |
| value="Clerk1", | |
| label="請選擇職級Choose your role" | |
| ) | |
| name = gr.Textbox(label="姓名Name", lines=1, placeholder="請輸入姓名Your Name") | |
| p1 = gr.Textbox(label="1. 年齡Age、性別Sex、Chief Complaint (10分)", lines=2) | |
| p2 = gr.Textbox(label="2. 個人病史Personal History (10分)", lines=2) | |
| p3 = gr.Textbox(label="3. 家族史Family History (10分)", lines=2) | |
| p4 = gr.Textbox(label="4. 身體檢查陽性發現Physical Examination (10分)", lines=2) | |
| p5 = gr.Textbox(label="5. 實驗室與影像結果Lab and Image (10分)", lines=2) | |
| p6 = gr.Textbox(label="6. 鑑別診斷Differential Diagnosis (10分)", lines=2) | |
| p7 = gr.Textbox(label="7. 處置與治療Treatment (40分)", lines=2) | |
| with gr.Row(): | |
| submit_btn = gr.Button("送出評分 Submit", variant="primary") | |
| clear_btn = gr.Button("清空 Clear") | |
| result = gr.Markdown(label="評分結果Result ") | |
| submit_btn.click( | |
| collect_and_score, | |
| [level, name, p1, p2, p3, p4, p5, p6, p7], | |
| [level, name, p1, p2, p3, p4, p5, p6, p7, result] | |
| ) | |
| clear_btn.click( | |
| do_clear, | |
| outputs=[level, name, p1, p2, p3, p4, p5, p6, p7, result] | |
| ) | |
| demo.launch() |