""" 감정 분석 모듈 ────────────── 사용자 질문의 감정 상태를 세밀하게 분석 """ from typing import Dict, List, Optional from dataclasses import dataclass @dataclass class Emotion: """감정 분석 결과""" primary: str # 주요 감정: angry, frustrated, satisfied, confused, worried, neutral intensity: float # 강도: 0.0 (약함) ~ 1.0 (강함) confidence: float # 신뢰도: 0.0 ~ 1.0 matched_keywords: List[str] # 매칭된 키워드 def needs_empathy(self) -> bool: """공감이 필요한 감정인지 (부정적 감정)""" return self.primary in ["angry", "frustrated", "worried"] def is_positive(self) -> bool: """긍정적 감정인지""" return self.primary == "satisfied" def needs_clarification(self) -> bool: """명확한 설명이 필요한지 (혼란)""" return self.primary == "confused" class EmotionDetector: """사용자 감정 분석기""" # 감정별 키워드 및 강도 EMOTION_PATTERNS = { "angry": { "keywords": { # (키워드, 강도) "화나": 0.9, "화남": 0.9, "분노": 1.0, "열받": 0.9, "짜증": 0.7, "짜증나": 0.8, "짜증남": 0.8, "불만": 0.6, "불쾌": 0.7, "기분 나쁘": 0.6, "최악": 0.9, "엉망": 0.7, "터무니": 0.8, "너무해": 0.8, "심각": 0.7, "문제": 0.5, "불친절": 0.7, "무례": 0.8, "실망": 0.6 }, "description": "화남/분노" }, "frustrated": { "keywords": { "답답": 0.8, "막막": 0.7, "곤란": 0.6, "어렵": 0.5, "힘들": 0.6, "난감": 0.7, "복잡": 0.5, "이해가 안": 0.6, "잘 모르": 0.5, "왜 안": 0.6, "계속": 0.4, "여전": 0.5, "해결이 안": 0.7, "안 되": 0.6 }, "description": "답답함/막막함" }, "satisfied": { "keywords": { "감사": 0.8, "고마워": 0.8, "고맙": 0.8, "좋": 0.7, "훌륭": 0.9, "최고": 0.9, "도움": 0.7, "해결": 0.8, "완벽": 0.9, "잘": 0.6, "쉽": 0.6, "편리": 0.7, "만족": 0.9, "훌륭": 0.9 }, "description": "만족/긍정" }, "confused": { "keywords": { "모르겠": 0.8, "헷갈": 0.9, "혼란": 0.9, "무슨": 0.6, "뭐가": 0.6, "이해가": 0.6, "뜻이": 0.5, "의미": 0.5, "차이": 0.5, "구별": 0.6, "잘 안": 0.6 }, "description": "혼란/이해 부족" }, "worried": { "keywords": { "걱정": 0.8, "불안": 0.9, "염려": 0.7, "겁나": 0.8, "무섭": 0.7, "두렵": 0.7, "조심": 0.5, "주의": 0.5, "위험": 0.6, "문제가": 0.6, "괜찮": 0.5 }, "description": "걱정/불안" } } def detect(self, query: str, ts: Optional[str] = None) -> Emotion: """감정 분석 Args: query: 사용자 질문 ts: 타임스탬프 (로깅용) Returns: Emotion 객체 """ query_lower = query.lower().strip() # 빈 문자열 체크 if not query_lower: return Emotion( primary="neutral", intensity=0.0, confidence=1.0, matched_keywords=[] ) # 각 감정별 점수 계산 emotion_scores = {} for emotion_name, config in self.EMOTION_PATTERNS.items(): matched = [] total_intensity = 0.0 for keyword, intensity in config["keywords"].items(): if keyword in query_lower: matched.append(keyword) total_intensity += intensity if matched: # 평균 강도 계산 avg_intensity = total_intensity / len(matched) # 매칭 개수와 강도를 결합한 점수 score = len(matched) * avg_intensity emotion_scores[emotion_name] = { "score": score, "intensity": min(1.0, avg_intensity), "matched": matched } # 매칭된 감정이 없으면 중립 if not emotion_scores: return Emotion( primary="neutral", intensity=0.0, confidence=1.0, matched_keywords=[] ) # 가장 높은 점수의 감정 선택 best_emotion = max(emotion_scores.items(), key=lambda x: x[1]["score"]) emotion_name = best_emotion[0] emotion_data = best_emotion[1] # 신뢰도 계산 (매칭 개수와 강도 기반) confidence = min(0.95, 0.6 + (emotion_data["score"] * 0.1)) result = Emotion( primary=emotion_name, intensity=emotion_data["intensity"], confidence=confidence, matched_keywords=emotion_data["matched"] ) if ts: print(f"[EmotionDetector] {ts} 감정 분석: {result.primary} " f"(강도: {result.intensity:.2f}, 신뢰도: {result.confidence:.2f}, " f"키워드: {result.matched_keywords})") return result def get_emotion_summary(self, emotion: Emotion) -> str: """감정 요약 문자열""" description = self.EMOTION_PATTERNS.get(emotion.primary, {}).get("description", emotion.primary) intensity_label = "강함" if emotion.intensity > 0.7 else "중간" if emotion.intensity > 0.4 else "약함" return f"{description} ({intensity_label})"