4b86b2a660
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
"""
|
|
감정 분석 모듈
|
|
──────────────
|
|
사용자 질문의 감정 상태를 세밀하게 분석
|
|
"""
|
|
|
|
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})"
|