Spaces:
Running
Running
File size: 10,478 Bytes
4ff9b2a a2944c2 b11a343 a2944c2 b11a343 4ff9b2a a2944c2 9f59324 4ff9b2a 9f59324 8d691fc 4ff9b2a ab2daf0 c02fe7d ab2daf0 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 4ff9b2a a2944c2 9d9234e 8a4d3ba 9d9234e 8a4d3ba 482d371 8a4d3ba 4ff9b2a 8a4d3ba 9f59324 4ff9b2a 482d371 4ff9b2a ab2daf0 acb721a c02fe7d acb721a c02fe7d acb721a c02fe7d acb721a c02fe7d a2944c2 3548e77 a2944c2 3548e77 a2944c2 3548e77 a2944c2 0002766 34f919f a2944c2 3548e77 4ff9b2a 3548e77 4ff9b2a a2944c2 9f59324 3548e77 482d371 a2944c2 482d371 3548e77 9f59324 4ff9b2a 9f59324 4ff9b2a 9f59324 4ff9b2a 3548e77 4ff9b2a 3548e77 4ff9b2a 3703452 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | 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() |