NoteEvaluator / app.py
wensjheng's picture
Update app.py
3703452 verified
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()