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()