""" 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": [] }