nurse-handover-simulator / services /gemini_service.py
LearningnRunning's picture
feat: Integrate Supabase for data management, replacing local PostgreSQL. Implement Supabase service for CRUD operations on patients and scenarios, and update application logic to utilize Supabase API. Add Dockerfile for containerization and .dockerignore for build optimization.
3a338e5
"""
Google Gemini AI 평가 μ„œλΉ„μŠ€
"""
import json
import os
import google.generativeai as genai
from dotenv import load_dotenv
load_dotenv()
# Gemini API μ„€μ •
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
genai.configure(api_key=GOOGLE_API_KEY)
class GeminiEvaluator:
"""κ°„ν˜Έ μΈμˆ˜μΈκ³„ 평가λ₯Ό μœ„ν•œ Gemini AI μ„œλΉ„μŠ€"""
def __init__(self, model_name="gemini-2.0-flash-exp"):
self.model = genai.GenerativeModel(model_name)
def evaluate_handoff(
self, student_sbar: dict, scenario_data: dict, patient_data: dict
) -> dict:
"""
ν•™μƒμ˜ SBAR μΈμˆ˜μΈκ³„λ₯Ό ν‰κ°€ν•©λ‹ˆλ‹€.
Args:
student_sbar: 학생이 μž‘μ„±ν•œ SBAR (situation, background, assessment, recommendation)
scenario_data: μ‹œλ‚˜λ¦¬μ˜€ 데이터 (EMR 정보)
patient_data: ν™˜μž κΈ°λ³Έ 정보
Returns:
평가 κ²°κ³Ό λ”•μ…”λ„ˆλ¦¬ (total_score, category_scores, strengths, improvements, detailed_feedback)
"""
prompt = self._create_evaluation_prompt(
student_sbar, scenario_data, patient_data
)
try:
response = self.model.generate_content(
prompt,
generation_config={
"temperature": 0.7,
"top_p": 0.95,
"max_output_tokens": 2048,
},
)
# JSON 응닡 νŒŒμ‹±
result_text = response.text.strip()
# Markdown μ½”λ“œ 블둝 제거 (```json ... ```)
if result_text.startswith("```"):
result_text = result_text.split("```")[1]
if result_text.startswith("json"):
result_text = result_text[4:]
result_text = result_text.strip()
evaluation = json.loads(result_text)
return evaluation
except json.JSONDecodeError as e:
print(f"JSON νŒŒμ‹± 였λ₯˜: {e}")
print(f"응닡 ν…μŠ€νŠΈ: {response.text}")
return self._create_default_evaluation()
except Exception as e:
print(f"Gemini API 였λ₯˜: {e}")
return self._create_default_evaluation()
def _create_evaluation_prompt(
self, student_sbar: dict, scenario_data: dict, patient_data: dict
) -> str:
"""κ³ λ„ν™”λœ 평가 ν”„λ‘¬ν”„νŠΈ 생성"""
prompt = f"""당신은 20λ…„ κ²½λ ₯의 ν•œκ΅­ κ°„ν˜Έ ꡐ윑 μ „λ¬Έκ°€μ΄μž κ°„ν˜Έν•™ κ΅μˆ˜μž…λ‹ˆλ‹€.
κ°„ν˜Έ 학생이 μž‘μ„±ν•œ SBAR ν˜•μ‹μ˜ μΈμˆ˜μΈκ³„λ₯Ό ν•œκ΅­ κ°„ν˜Έμ‚¬ κ΅­κ°€μ‹œν—˜ 및 μž„μƒ 싀무 ν‘œμ€€μ— 따라 ν‰κ°€ν•΄μ£Όμ„Έμš”.
## πŸ“‹ ν™˜μž 정보 (EMR 데이터)
- **ν™˜μžλͺ…**: {patient_data.get('name')} ({patient_data.get('age')}μ„Έ {patient_data.get('gender')})
- **진단λͺ…**: {patient_data.get('diagnosis')}
- **μž…μ›μΌ**: {patient_data.get('admission_date')} (μž¬μ› {scenario_data.get('hospital_day')}일차)
- **μ•Œλ ˆλ₯΄κΈ°**: {patient_data.get('allergies')}
## πŸ₯ μΈμˆ˜μΈκ³„ 상황
- **μ‹œλ‚˜λ¦¬μ˜€**: {scenario_data.get('title')}
- **인계 상황**: {scenario_data.get('handoff_situation')}
### πŸ“Š ν™œλ ₯μ§•ν›„ (Vital Signs)
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
### πŸ§ͺ 검사 κ²°κ³Ό (Laboratory Results)
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
### πŸ’Š μ˜μ‚¬ 처방 (Physician Orders)
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
### πŸ“ κ°„ν˜Έ 기둝 (Nursing Notes)
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
---
## πŸŽ“ 학생이 μž‘μ„±ν•œ SBAR μΈμˆ˜μΈκ³„
**S - Situation (상황)**
{student_sbar.get('situation', '')}
**B - Background (λ°°κ²½)**
{student_sbar.get('background', '')}
**A - Assessment (평가)**
{student_sbar.get('assessment', '')}
**R - Recommendation (κΆŒκ³ μ‚¬ν•­)**
{student_sbar.get('recommendation', '')}
---
## πŸ“ 평가 κΈ°μ€€ (총 100점)
### 1️⃣ μ™„μ „μ„± (Completeness, 0-25점)
**ν•„μˆ˜ 정보 포함 μ—¬λΆ€λ₯Ό ν‰κ°€ν•©λ‹ˆλ‹€:**
**Situation (6점):**
- ν™˜μž 신원 (이름, λ‚˜μ΄, 성별) [2점]
- μ£Όν˜Έμ†Œ/μž…μ› μ‚¬μœ  [2점]
- ν˜„μž¬ μƒνƒœ μš”μ•½ [2점]
**Background (6점):**
- 진단λͺ… [1점]
- κ³Όκ±°λ ₯ [1점]
- μ•Œλ ˆλ₯΄κΈ° [1점]
- ν˜„μž¬ νˆ¬μ•½ [2점]
- 수술λ ₯ (ν•΄λ‹Ή μ‹œ) [1점]
**Assessment (8점):**
- ν™œλ ₯μ§•ν›„ (BP, HR, RR, BT, SpO2) [3점]
- μ£Όμš” 검사 κ²°κ³Ό (WBC, CRP λ“±) [2점]
- 톡증 점수 [1점]
- IV/Foley/Drain λ“± 라인 μƒνƒœ [1점]
- μ€‘μš”ν•œ λ³€ν™”/μš°λ €μ‚¬ν•­ [1점]
**Recommendation (5점):**
- λ‹€μŒ 단계 κ³„νš [2점]
- μ£Όμ˜μ‚¬ν•­/λͺ¨λ‹ˆν„°λ§ [2점]
- μ˜ˆμ • 검사/치료 [1점]
### 2️⃣ μ •ν™•μ„± (Accuracy, 0-25점)
**의료 μ •λ³΄μ˜ 정확성을 ν‰κ°€ν•©λ‹ˆλ‹€:**
- ν™œλ ₯μ§•ν›„ 수치의 μ •ν™•μ„± [5점]
- 검사 κ²°κ³Ό 수치의 μ •ν™•μ„± [5점]
- μ˜ν•™ μš©μ–΄μ˜ μ˜¬λ°”λ₯Έ μ‚¬μš© [5점]
- μ‹œκ°„ μˆœμ„œμ˜ μ •ν™•μ„± [5점]
- μΈκ³Όκ΄€κ³„μ˜ 논리성 [5점]
**감점 μš”μ†Œ:**
- 수치 였λ₯˜ (각 -2점)
- μ˜ν•™ μš©μ–΄ 였용 (각 -2점)
- μ‹œκ°„ μˆœμ„œ ν˜Όλ™ (-3점)
- μ€‘μš” 정보 λˆ„λ½ (각 -3점)
### 3️⃣ λͺ…λ£Œμ„± (Clarity, 0-25점)
**μ˜μ‚¬μ†Œν†΅μ˜ λͺ…확성을 ν‰κ°€ν•©λ‹ˆλ‹€:**
- κ°„κ²°ν•˜κ³  λͺ…ν™•ν•œ ν‘œν˜„ [7점]
- 논리적인 흐름 [6점]
- λΆˆν•„μš”ν•œ 쀑볡 μ—†μŒ [6점]
- μ „λ¬Έμ μ΄λ©΄μ„œ μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ ν‘œν˜„ [6점]
**감점 μš”μ†Œ:**
- λͺ¨ν˜Έν•œ ν‘œν˜„ (각 -2점)
- κ³Όλ„ν•œ 쀑볡 (각 -2점)
- 비논리적 μ „κ°œ (-3점)
### 4️⃣ μš°μ„ μˆœμœ„ (Priority, 0-25점)
**μ€‘μš” μ •λ³΄μ˜ μš°μ„  전달을 ν‰κ°€ν•©λ‹ˆλ‹€:**
- μœ„κΈ‰/μ£Όμ˜μ‚¬ν•­ μš°μ„  μ–ΈκΈ‰ [8점]
- μ€‘μš”λ„μ— λ”°λ₯Έ 정보 배치 [7점]
- 즉각 쑰치 ν•„μš” 사항 κ°•μ‘° [5점]
- ν™˜μž μ•ˆμ „ κ΄€λ ¨ 정보 μš°μ„  [5점]
**νŠΉλ³„ 고렀사항:**
- 비정상 ν™œλ ₯μ§•ν›„ μš°μ„  μ–ΈκΈ‰ (+λ³΄λ„ˆμŠ€)
- μ•Œλ ˆλ₯΄κΈ° μ‘°κΈ° μ–ΈκΈ‰ (+λ³΄λ„ˆμŠ€)
- 톡증/뢈편감 μ¦‰μ‹œ μ–ΈκΈ‰ (+λ³΄λ„ˆμŠ€)
---
## 🎯 평가 μ§€μΉ¨
1. **ν•œκ΅­ κ°„ν˜Έ 싀무 ν‘œμ€€ μ€€μˆ˜**: λŒ€ν•œκ°„ν˜Έν˜‘νšŒ μΈμˆ˜μΈκ³„ κ°€μ΄λ“œλΌμΈ κΈ°μ€€
2. **ν™˜μž μ•ˆμ „ μ΅œμš°μ„ **: ν™˜μž μ•ˆμ „μ— 영ν–₯을 λ―ΈμΉ˜λŠ” 정보 평가 κ°•ν™”
3. **싀무 μ μš©μ„±**: μ‹€μ œ μž„μƒμ—μ„œ μ‚¬μš© κ°€λŠ₯ν•œ μˆ˜μ€€ 평가
4. **ꡐ윑적 ν”Όλ“œλ°±**: 건섀적이고 ꡬ체적인 κ°œμ„ μ‚¬ν•­ μ œμ‹œ
## πŸ“€ 좜λ ₯ ν˜•μ‹
**λ°˜λ“œμ‹œ μ•„λž˜ JSON ν˜•μ‹μœΌλ‘œλ§Œ μ‘λ‹΅ν•˜μ„Έμš”. λ‹€λ₯Έ ν…μŠ€νŠΈλŠ” ν¬ν•¨ν•˜μ§€ λ§ˆμ„Έμš”.**
```json
{{
"total_score": 85,
"category_scores": {{
"completeness": 22,
"accuracy": 23,
"clarity": 20,
"priority": 20
}},
"strengths": [
"ν™˜μžμ˜ μ£Όν˜Έμ†Œμ™€ ν˜„μž¬ μƒνƒœλ₯Ό λͺ…ν™•ν•˜κ²Œ κΈ°μˆ ν–ˆμŠ΅λ‹ˆλ‹€.",
"비정상 ν™œλ ₯μ§•ν›„(HR 96, BT 37.9℃)λ₯Ό μ •ν™•νžˆ ν¬ν•¨ν•˜κ³  μš°μ„  μˆœμœ„λ₯Ό λ‘μ—ˆμŠ΅λ‹ˆλ‹€.",
"μ£Όμš” 검사 κ²°κ³Ό(WBC 14.8, CRP 6.2)λ₯Ό ꡬ체적으둜 μ–ΈκΈ‰ν–ˆμŠ΅λ‹ˆλ‹€.",
"응급 수술 μ˜ˆμ • μ‹œκ°„κ³Ό μ€€λΉ„ 사항을 λͺ…ν™•νžˆ μ „λ‹¬ν–ˆμŠ΅λ‹ˆλ‹€.",
"NPO μœ μ§€ 및 ν•­μƒμ œ νˆ¬μ—¬ λ“± ν˜„μž¬ 치료 κ³„νšμ„ μ •ν™•νžˆ κΈ°μˆ ν–ˆμŠ΅λ‹ˆλ‹€."
],
"improvements": [
"톡증 점수(NRS 8/10)λ₯Ό Assessment μ„Ήμ…˜ μ•žλΆ€λΆ„μ— λͺ…μ‹œν•˜μ—¬ ν™˜μžμ˜ λΆˆνŽΈκ°μ„ κ°•μ‘°ν•˜μ„Έμš”.",
"IV 라인 μƒνƒœ(D5NS 80 mL/hr)λ₯Ό Assessment에 μΆ”κ°€ν•˜λ©΄ 더 μ™„μ „ν•©λ‹ˆλ‹€.",
"Recommendationμ—μ„œ 수술 ν›„ λͺ¨λ‹ˆν„°λ§ κ³„νš(V/S q4h, 합병증 κ΄€μ°°)을 ꡬ체적으둜 λͺ…μ‹œν•˜μ„Έμš”.",
"McBurney point 압톡, λ°˜λ°œν†΅ λ“± 신체 κ²€μ§„ μ†Œκ²¬μ„ Assessment에 ν¬ν•¨ν•˜λ©΄ μ’‹μŠ΅λ‹ˆλ‹€.",
"μž…μ› 경둜(응급싀 경유)와 μž…μ› μ‹œκ°„μ„ Background에 μΆ”κ°€ν•˜λ©΄ 완전성이 ν–₯μƒλ©λ‹ˆλ‹€."
],
"missing_critical_info": [
"IV 라인 μ’…λ₯˜ 및 μœ μ†",
"신체 κ²€μ§„ μ†Œκ²¬ (McBurney point 압톡, λ°˜λ°œν†΅)",
"수술 ν›„ λͺ¨λ‹ˆν„°λ§ κ³„νš"
],
"safety_concerns": [
"톡증 μ μˆ˜κ°€ λ†’μŒ(NRS 8/10)에도 λΆˆκ΅¬ν•˜κ³  톡증 관리 μš°μ„ μˆœμœ„κ°€ λͺ…ν™•ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.",
"비정상 ν™œλ ₯μ§•ν›„(λ°œμ—΄ 37.9℃)에 λŒ€ν•œ 지속적 λͺ¨λ‹ˆν„°λ§ κ³„νšμ΄ ꡬ체적이지 μ•ŠμŠ΅λ‹ˆλ‹€."
],
"clinical_reasoning": {{
"situation_assessment": "학생은 ν™˜μžμ˜ μ£Όν˜Έμ†Œμ™€ ν˜„μž¬ μƒνƒœλ₯Ό λͺ…ν™•νžˆ νŒŒμ•…ν•˜κ³  μžˆμœΌλ‚˜, ν†΅μ¦μ˜ 심각성을 μΆ©λΆ„νžˆ κ°•μ‘°ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.",
"background_completeness": "진단λͺ…κ³Ό 검사 κ²°κ³ΌλŠ” 잘 ν¬ν•¨ν–ˆμœΌλ‚˜, μž…μ› κ²½λ‘œμ™€ μ‹œκ°„μ— λŒ€ν•œ 정보가 λΆ€μ‘±ν•©λ‹ˆλ‹€.",
"assessment_quality": "ν™œλ ₯징후와 μ£Όμš” 검사 κ²°κ³Όλ₯Ό ν¬ν•¨ν–ˆμœΌλ‚˜, 신체 κ²€μ§„ μ†Œκ²¬κ³Ό IV 라인 정보가 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.",
"recommendation_specificity": "수술 κ³„νšμ€ λͺ…ν™•ν•˜λ‚˜, 수술 μ „ν›„ ꡬ체적인 κ°„ν˜Έ μ€‘μž¬ 및 λͺ¨λ‹ˆν„°λ§ κ³„νšμ΄ λ―Έν‘ν•©λ‹ˆλ‹€."
}},
"grade_level": "B+ (우수)",
"pass_fail": "Pass",
"detailed_feedback": "μ „λ°˜μ μœΌλ‘œ SBAR ꡬ쑰λ₯Ό 잘 μ΄ν•΄ν•˜κ³  있으며, ν™˜μžμ˜ 핡심 문제λ₯Ό νŒŒμ•…ν•˜μ—¬ μΈμˆ˜μΈκ³„ν–ˆμŠ΅λ‹ˆλ‹€. 특히 비정상 ν™œλ ₯징후와 μ£Όμš” 검사 κ²°κ³Όλ₯Ό μ •ν™•νžˆ ν¬ν•¨ν•œ 점이 μš°μˆ˜ν•©λ‹ˆλ‹€.\\n\\n**강점:** Situationμ—μ„œ ν™˜μžμ˜ μ£Όν˜Έμ†Œ(μš°ν•˜λ³΅λΆ€ 톡증)와 이동 양상을 λͺ…ν™•νžˆ κΈ°μˆ ν–ˆκ³ , Backgroundμ—μ„œ κΈ‰μ„± μΆ©μˆ˜μ—Ό 진단과 CT μ†Œκ²¬μ„ ꡬ체적으둜 μ–ΈκΈ‰ν–ˆμŠ΅λ‹ˆλ‹€. Recommendationμ—μ„œ 응급 수술 μ‹œκ°„μ„ λͺ…μ‹œν•œ 것도 μ’‹μŠ΅λ‹ˆλ‹€.\\n\\n**κ°œμ„ μ‚¬ν•­:** Assessmentμ—μ„œ 톡증 점수(NRS 8/10)λ₯Ό 더 μ•žλΆ€λΆ„μ— κ°•μ‘°ν•˜κ³ , IV 라인 μƒνƒœμ™€ 신체 κ²€μ§„ μ†Œκ²¬(McBurney point 압톡, λ°˜λ°œν†΅)을 μΆ”κ°€ν•˜λ©΄ 완전성이 ν–₯μƒλ©λ‹ˆλ‹€. Recommendationμ—μ„œ 수술 ν›„ V/S λͺ¨λ‹ˆν„°λ§ μ£ΌκΈ°(q4h)와 합병증 κ΄€μ°° ν•­λͺ©μ„ ꡬ체적으둜 λͺ…μ‹œν•˜μ„Έμš”.\\n\\n**ν™˜μž μ•ˆμ „:** 높은 톡증 μ μˆ˜μ™€ λ°œμ—΄μ— λŒ€ν•œ 지속적 관찰이 ν•„μš”ν•˜λ©°, 이λ₯Ό μΈμˆ˜μΈκ³„ μ‹œ 더 κ°•μ‘°ν•΄μ•Ό ν•©λ‹ˆλ‹€. NPO μœ μ§€μ™€ ν•­μƒμ œ νˆ¬μ—¬λŠ” 잘 μ–ΈκΈ‰ν–ˆμŠ΅λ‹ˆλ‹€.\\n\\n**싀무 적용:** μž„μƒμ—μ„œ μΆ©λΆ„νžˆ μ‚¬μš© κ°€λŠ₯ν•œ μˆ˜μ€€μ΄λ‚˜, 신체 κ²€μ§„ μ†Œκ²¬κ³Ό 라인 정보λ₯Ό μΆ”κ°€ν•˜λ©΄ λ”μš± 완성도 높은 μΈμˆ˜μΈκ³„κ°€ 될 κ²ƒμž…λ‹ˆλ‹€."
}}
```
**평가 μ‹œ μ£Όμ˜μ‚¬ν•­:**
1. 긍정적인 츑면을 λ¨Όμ € μ–ΈκΈ‰ν•˜κ³  건섀적인 ν”Όλ“œλ°± 제곡
2. ꡬ체적인 κ°œμ„  방법 μ œμ‹œ (μ˜ˆμ‹œ 포함)
3. ν™˜μž μ•ˆμ „κ³Ό κ΄€λ ¨λœ μ€‘μš” 정보 λˆ„λ½ μ‹œ λͺ…ν™•νžˆ 지적
4. μ‹€μ œ μž„μƒ 적용 κ°€λŠ₯μ„± κ³ λ €
5. ν•™μƒμ˜ μˆ˜μ€€μ— λ§žλŠ” κΈ°λŒ€μΉ˜ μ„€μ • (κ³Όλ„ν•œ λΉ„νŒ μ§€μ–‘)
"""
return prompt
def _create_default_evaluation(self) -> dict:
"""API 였λ₯˜ μ‹œ κΈ°λ³Έ 평가 κ²°κ³Ό λ°˜ν™˜"""
return {
"total_score": 0,
"category_scores": {
"completeness": 0,
"accuracy": 0,
"clarity": 0,
"priority": 0,
},
"strengths": ["평가λ₯Ό μ§„ν–‰ν•  수 μ—†μŠ΅λ‹ˆλ‹€."],
"improvements": ["API 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‚˜μ€‘μ— λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."],
"detailed_feedback": "평가 μ‹œμŠ€ν…œμ— λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. κ΄€λ¦¬μžμ—κ²Œ λ¬Έμ˜ν•˜μ„Έμš”.",
}
def chat_feedback(
self,
user_message: str,
chat_history: list,
scenario_data: dict,
patient_data: dict,
) -> str:
"""
λŒ€ν™”ν˜• SBAR ν”Όλ“œλ°± 제곡
Args:
user_message: ν•™μƒμ˜ ν˜„μž¬ λ©”μ‹œμ§€
chat_history: 이전 λŒ€ν™” 기둝 [(role, message), ...]
scenario_data: μ‹œλ‚˜λ¦¬μ˜€ 데이터 (EMR 정보)
patient_data: ν™˜μž κΈ°λ³Έ 정보
Returns:
AI ν”Όλ“œλ°± λ©”μ‹œμ§€
"""
prompt = self._create_chat_feedback_prompt(
user_message, chat_history, scenario_data, patient_data
)
try:
response = self.model.generate_content(
prompt,
generation_config={
"temperature": 0.8,
"top_p": 0.95,
"max_output_tokens": 1500,
},
)
return response.text.strip()
except Exception as e:
print(f"Gemini API 였λ₯˜: {e}")
return "μ£„μ†‘ν•©λ‹ˆλ‹€. μΌμ‹œμ μΈ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
def _create_chat_feedback_prompt(
self, user_message: str, chat_history: list, scenario_data: dict, patient_data: dict
) -> str:
"""λŒ€ν™”ν˜• ν”Όλ“œλ°± ν”„λ‘¬ν”„νŠΈ 생성"""
# 이전 λŒ€ν™” 기둝 ν¬λ§·νŒ…
history_text = ""
if chat_history:
for role, message in chat_history:
if role == "user":
history_text += f"\n학생: {message}\n"
else:
history_text += f"\nAI ꡐ수: {message}\n"
prompt = f"""당신은 20λ…„ κ²½λ ₯의 ν•œκ΅­ κ°„ν˜Έ ꡐ윑 μ „λ¬Έκ°€μ΄μž κ°„ν˜Έν•™ κ΅μˆ˜μž…λ‹ˆλ‹€.
학생이 SBAR ν˜•μ‹μ˜ μΈμˆ˜μΈκ³„λ₯Ό μ—°μŠ΅ν•˜κ³  있으며, λŒ€ν™”λ₯Ό 톡해 ν”Όλ“œλ°±μ„ μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
## πŸ“‹ ν™˜μž 정보 (EMR 데이터)
- **ν™˜μžλͺ…**: {patient_data.get('name')} ({patient_data.get('age')}μ„Έ {patient_data.get('gender')})
- **진단λͺ…**: {patient_data.get('diagnosis')}
- **μž…μ›μΌ**: {patient_data.get('admission_date')}
- **μ•Œλ ˆλ₯΄κΈ°**: {patient_data.get('allergies')}
## πŸ₯ μΈμˆ˜μΈκ³„ 상황
- **μ‹œλ‚˜λ¦¬μ˜€**: {scenario_data.get('title')}
- **인계 상황**: {scenario_data.get('handoff_situation')}
### πŸ“Š ν™œλ ₯μ§•ν›„ (Vital Signs)
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
### πŸ§ͺ 검사 κ²°κ³Ό (Laboratory Results)
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
### πŸ’Š μ˜μ‚¬ 처방 (Physician Orders)
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
### πŸ“ κ°„ν˜Έ 기둝 (Nursing Notes)
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
---
## πŸ’¬ 이전 λŒ€ν™” 기둝
{history_text if history_text else "(첫 λŒ€ν™”μž…λ‹ˆλ‹€)"}
---
## πŸŽ“ ν•™μƒμ˜ ν˜„μž¬ λ©”μ‹œμ§€
{user_message}
---
## πŸ“ ν”Όλ“œλ°± κ°€μ΄λ“œλΌμΈ
1. **μΉœμ ˆν•˜κ³  격렀적인 톀**: 학생이 νŽΈμ•ˆν•˜κ²Œ μ§ˆλ¬Έν•˜κ³  κ°œμ„ ν•  수 μžˆλ„λ‘ 지원
2. **SBAR ꡬ쑰 μ€€μˆ˜ 확인**: 학생이 μž‘μ„±ν•œ λ‚΄μš©μ΄ SBAR ν˜•μ‹μ„ λ”°λ₯΄λŠ”μ§€ 확인
3. **ꡬ체적인 κ°œμ„  μ œμ•ˆ**: λ§‰μ—°ν•œ ν”Όλ“œλ°±μ΄ μ•„λ‹Œ ꡬ체적인 μ˜ˆμ‹œ 제곡
4. **μ€‘μš” 정보 λˆ„λ½ 지적**: EMR 데이터에 μžˆλŠ” μ€‘μš”ν•œ 정보가 λΉ μ‘Œλ‹€λ©΄ μ–ΈκΈ‰
5. **ν™˜μž μ•ˆμ „ κ°•μ‘°**: ν™˜μž μ•ˆμ „κ³Ό κ΄€λ ¨λœ μ •λ³΄λŠ” 특히 κ°•μ‘°
6. **단계적 κ°œμ„ **: ν•œ λ²ˆμ— λͺ¨λ“  것을 고치렀 ν•˜μ§€ 말고, μš°μ„ μˆœμœ„κ°€ 높은 것뢀터 μ œμ•ˆ
7. **μ΅œμ’… 제좜 μ „**: 점수λ₯Ό λ§€κΈ°μ§€ 말고, κ°œμ„  λ°©ν–₯만 μ œμ‹œ
## 응닡 ν˜•μ‹
- μžμ—°μŠ€λŸ¬μš΄ λŒ€ν™”μ²΄λ‘œ μ‘λ‹΅ν•˜μ„Έμš”.
- 학생이 μž˜ν•œ 뢀뢄을 λ¨Όμ € μΉ­μ°¬ν•˜μ„Έμš”.
- κ°œμ„ μ΄ ν•„μš”ν•œ 뢀뢄은 "~ν•˜λ©΄ 더 쒋을 것 κ°™μ•„μš”", "~λ₯Ό μΆ”κ°€ν•΄λ³΄λŠ” 건 μ–΄λ–¨κΉŒμš”?" 같은 μ œμ•ˆ ν˜•μ‹μœΌλ‘œ μ œμ‹œν•˜μ„Έμš”.
- ν•„μš”μ‹œ μ˜ˆμ‹œλ₯Ό λ“€μ–΄ μ„€λͺ…ν•˜μ„Έμš”.
- 학생이 "μ΅œμ’… 제좜"을 μ–ΈκΈ‰ν•˜μ§€ μ•ŠλŠ” ν•œ, μ μˆ˜λ‚˜ 등급을 λ§€κΈ°μ§€ λ§ˆμ„Έμš”.
응닡을 μž‘μ„±ν•΄μ£Όμ„Έμš”:
"""
return prompt
def evaluate_conversation(
self,
chat_history: list,
scenario_data: dict,
patient_data: dict
) -> dict:
"""
전체 λŒ€ν™”λ₯Ό λΆ„μ„ν•˜μ—¬ 질적 ν”Όλ“œλ°± 제곡 (점수 μ—†μŒ)
Args:
chat_history: 전체 λŒ€ν™” 기둝 [[user_msg, assistant_msg], ...]
scenario_data: μ‹œλ‚˜λ¦¬μ˜€ 데이터 (EMR 정보)
patient_data: ν™˜μž κΈ°λ³Έ 정보
Returns:
평가 κ²°κ³Ό λ”•μ…”λ„ˆλ¦¬ (점수 없이 질적 ν”Όλ“œλ°±λ§Œ)
"""
prompt = self._create_conversation_evaluation_prompt(
chat_history, scenario_data, patient_data
)
try:
response = self.model.generate_content(
prompt,
generation_config={
"temperature": 0.7,
"top_p": 0.95,
"max_output_tokens": 2048,
},
)
# JSON 응닡 νŒŒμ‹±
result_text = response.text.strip()
# Markdown μ½”λ“œ 블둝 제거
if result_text.startswith("```"):
result_text = result_text.split("```")[1]
if result_text.startswith("json"):
result_text = result_text[4:]
result_text = result_text.strip()
evaluation = json.loads(result_text)
return evaluation
except json.JSONDecodeError as e:
print(f"JSON νŒŒμ‹± 였λ₯˜: {e}")
print(f"응닡 ν…μŠ€νŠΈ: {response.text}")
return self._create_default_evaluation_conversation()
except Exception as e:
print(f"Gemini API 였λ₯˜: {e}")
return self._create_default_evaluation_conversation()
def _create_conversation_evaluation_prompt(
self, chat_history: list, scenario_data: dict, patient_data: dict
) -> str:
"""전체 λŒ€ν™” 평가 ν”„λ‘¬ν”„νŠΈ 생성"""
# 전체 λŒ€ν™” λ‚΄μš© ν¬λ§·νŒ…
conversation_text = ""
for user_msg, bot_msg in chat_history:
if user_msg:
conversation_text += f"\n**학생**: {user_msg}\n"
if bot_msg:
conversation_text += f"\n**AI ꡐ수**: {bot_msg}\n"
prompt = f"""당신은 20λ…„ κ²½λ ₯의 ν•œκ΅­ κ°„ν˜Έ ꡐ윑 μ „λ¬Έκ°€μ΄μž κ°„ν˜Έν•™ κ΅μˆ˜μž…λ‹ˆλ‹€.
κ°„ν˜Έ 학생이 AI κ΅μˆ˜μ™€ λ‚˜λˆˆ λŒ€ν™”λ₯Ό λΆ„μ„ν•˜μ—¬ 건섀적인 μ’…ν•© ν”Όλ“œλ°±μ„ μ œκ³΅ν•΄μ£Όμ„Έμš”.
**μ€‘μš”**:
- λŒ€ν™”μ—μ„œ "학생"이 적힌 뢀뢄이 μ‹€μ œ ν•™μŠ΅ν•˜λŠ” κ°„ν˜Έ ν•™μƒμž…λ‹ˆλ‹€.
- "ν™˜μž"λŠ” EMR 정보에 λ‚˜μ˜¨ ν™˜μžμž…λ‹ˆλ‹€.
- 학생이 ν™˜μžμ— λŒ€ν•œ μΈμˆ˜μΈκ³„ λ‚΄μš©μ„ μž‘μ„±ν•˜λŠ” 것을 ν‰κ°€ν•΄μ£Όμ„Έμš”.
## πŸ“‹ ν™˜μž 정보 (EMR 데이터 - 학생이 μΈμˆ˜μΈκ³„ν•΄μ•Ό ν•  ν™˜μž)
- **ν™˜μžλͺ…**: {patient_data.get('name')} ({patient_data.get('age')}μ„Έ {patient_data.get('gender')})
- **진단λͺ…**: {patient_data.get('diagnosis')}
- **μž…μ›μΌ**: {patient_data.get('admission_date')}
- **μ•Œλ ˆλ₯΄κΈ°**: {patient_data.get('allergies')}
## πŸ₯ μΈμˆ˜μΈκ³„ 상황
- **μ‹œλ‚˜λ¦¬μ˜€**: {scenario_data.get('title')}
- **인계 상황**: {scenario_data.get('handoff_situation')}
### πŸ“Š ν™œλ ₯μ§•ν›„ (Vital Signs)
{json.dumps(scenario_data.get('vitals', {}), ensure_ascii=False, indent=2)}
### πŸ§ͺ 검사 κ²°κ³Ό (Laboratory Results)
{json.dumps(scenario_data.get('labs', {}), ensure_ascii=False, indent=2)}
### πŸ’Š μ˜μ‚¬ 처방 (Physician Orders)
{json.dumps(scenario_data.get('orders', []), ensure_ascii=False, indent=2)}
### πŸ“ κ°„ν˜Έ 기둝 (Nursing Notes)
{json.dumps(scenario_data.get('nursing_notes', []), ensure_ascii=False, indent=2)}
---
## πŸ’¬ 학생과 AI ꡐ수 κ°„ λŒ€ν™” λ‚΄μš©
**학생이 μž‘μ„±ν•œ μΈμˆ˜μΈκ³„ λ‚΄μš©κ³Ό AI ꡐ수의 ν”Όλ“œλ°±μ΄ ν¬ν•¨λœ 전체 λŒ€ν™”:**
{conversation_text if conversation_text else "(λŒ€ν™” μ—†μŒ)"}
**μœ„ λŒ€ν™”μ—μ„œ 학생이 ν™˜μž({patient_data.get('name')})에 λŒ€ν•œ μΈμˆ˜μΈκ³„λ₯Ό μ–΄λ–»κ²Œ μž‘μ„±ν–ˆλŠ”μ§€ λΆ„μ„ν•˜μ„Έμš”.**
---
## πŸ“ 평가 κ°€μ΄λ“œ
**μ€‘μš”:**
- ❌ 점수λ₯Ό λ§€κΈ°μ§€ λ§ˆμ„Έμš” (total_score, category_scores μ‚¬μš© κΈˆμ§€)
- βœ… 질적 ν”Όλ“œλ°±μ— μ§‘μ€‘ν•˜μ„Έμš”
- βœ… λŒ€ν™”μ—μ„œ λ‚˜νƒ€λ‚œ 강점을 ꡬ체적으둜 μ–ΈκΈ‰ν•˜μ„Έμš”
- βœ… κ°œμ„ ν•  점을 κ±΄μ„€μ μœΌλ‘œ μ œμ‹œν•˜μ„Έμš”
- βœ… λˆ„λ½λœ μ€‘μš” 정보λ₯Ό νŒŒμ•…ν•˜μ„Έμš”
- βœ… ν™˜μž μ•ˆμ „κ³Ό κ΄€λ ¨λœ μ£Όμ˜μ‚¬ν•­μ„ κ°•μ‘°ν•˜μ„Έμš”
- βœ… ν•™μŠ΅μ„ κ²©λ €ν•˜λŠ” 톀을 μœ μ§€ν•˜μ„Έμš”
**평가 κΈ°μ€€:**
1. **μ™„μ „μ„±**: ν•„μˆ˜ 정보(ν™˜μž 신원, 진단, ν™œλ ₯μ§•ν›„, 처방 λ“±) 포함 μ—¬λΆ€
2. **μ •ν™•μ„±**: 의료 μ •λ³΄μ˜ μ •ν™•μ„±
3. **λͺ…λ£Œμ„±**: μ˜μ‚¬μ†Œν†΅μ˜ λͺ…ν™•μ„±
4. **μš°μ„ μˆœμœ„**: μ€‘μš”ν•œ μ •λ³΄μ˜ μš°μ„  전달
5. **ν™˜μž μ•ˆμ „**: ν™˜μž μ•ˆμ „κ³Ό κ΄€λ ¨λœ 정보 κ°•μ‘°
---
## πŸ“€ 좜λ ₯ ν˜•μ‹ (JSON)
λ°˜λ“œμ‹œ μ•„λž˜ JSON ν˜•μ‹μœΌλ‘œλ§Œ μ‘λ‹΅ν•˜μ„Έμš”:
```json
{{
"strengths": [
"ꡬ체적으둜 μž˜ν•œ 점 1",
"ꡬ체적으둜 μž˜ν•œ 점 2",
"ꡬ체적으둜 μž˜ν•œ 점 3"
],
"improvements": [
"ꡬ체적으둜 κ°œμ„ ν•  점 1",
"ꡬ체적으둜 κ°œμ„ ν•  점 2",
"ꡬ체적으둜 κ°œμ„ ν•  점 3"
],
"detailed_feedback": "ν•™μƒμ˜ μ „λ°˜μ μΈ ν•™μŠ΅ μƒνƒœμ— λŒ€ν•œ 쒅합적인 의견. 길게 μž‘μ„±ν•˜λ˜ ꡬ체적으둜.",
"missing_critical_info": [
"λˆ„λ½λœ μ€‘μš” 정보 1",
"λˆ„λ½λœ μ€‘μš” 정보 2"
],
"safety_concerns": [
"ν™˜μž μ•ˆμ „ κ΄€λ ¨ μ£Όμ˜μ‚¬ν•­ 1",
"ν™˜μž μ•ˆμ „ κ΄€λ ¨ μ£Όμ˜μ‚¬ν•­ 2"
]
}}
```
**μ°Έκ³ :**
- 점수 ν•„λ“œ(total_score, category_scores)λŠ” ν¬ν•¨ν•˜μ§€ λ§ˆμ„Έμš”
- λ°°μ—΄ ν•­λͺ©μ€ μ΅œμ†Œ 2-3개 이상 μž‘μ„±ν•˜μ„Έμš”
- detailed_feedback은 200자 이상 μž‘μ„±ν•˜μ„Έμš”
- ν•œκΈ€λ‘œ μž‘μ„±ν•˜μ„Έμš”
"""
return prompt
def _create_default_evaluation_conversation(self) -> dict:
"""평가 μ‹€νŒ¨ μ‹œ κΈ°λ³Έ κ²°κ³Ό λ°˜ν™˜ (점수 없이)"""
return {
"strengths": ["λŒ€ν™” λ‚΄μš©μ„ λΆ„μ„ν–ˆμŠ΅λ‹ˆλ‹€."],
"improvements": ["평가 μ‹œμŠ€ν…œ 였λ₯˜λ‘œ ꡬ체적인 ν”Όλ“œλ°±μ„ μ œκ³΅ν•  수 μ—†μŠ΅λ‹ˆλ‹€."],
"detailed_feedback": "평가 μ‹œμŠ€ν…œμ— λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. λ‚˜μ€‘μ— λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.",
"missing_critical_info": [],
"safety_concerns": []
}