Files
exAichatbot_agent/exAiChatBot-chatbot2.0-agent/scripts/chat_history.py
T
Macbook 4b86b2a660 Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포
- local-dev 로컬 개발 환경

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 22:57:30 +09:00

284 lines
10 KiB
Python

"""
chat_history.py
────────────────────────────────────────────
MongoDB 대화 이력 관리 모듈
"""
import os
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Optional
from pymongo import MongoClient, DESCENDING
from pymongo.errors import PyMongoError
from handlers.suggestion_handler import strip_suggestion_block
# 환경 변수
MONGO_HOST = os.getenv("MONGO_HOST", "localhost")
MONGO_PORT = int(os.getenv("MONGO_PORT", "27017"))
MONGO_USER = os.getenv("MONGO_USER", "exlink")
MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "!wkcproqkf1")
MONGO_DATABASE = os.getenv("MONGO_DATABASE", "chat_history")
MONGO_COLLECTION = os.getenv("MONGO_COLLECTION", "rag_conversations")
MONGO_TTL_DAYS = int(os.getenv("MONGO_TTL_DAYS", "30")) # 데이터 보관 기간 (일 단위, 기본값: 30일)
class ChatHistoryManager:
"""MongoDB 기반 대화 이력 관리"""
def __init__(self):
"""MongoDB 연결 초기화"""
try:
# MongoDB 연결 문자열 (데이터베이스별 인증)
# 연결: mongodb://localhost:27017/
# 인증: chat_history 데이터베이스에서 exlink/!wkcproqkf1
if MONGO_USER and MONGO_PASSWORD:
connection_string = f"mongodb://{MONGO_USER}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}/{MONGO_DATABASE}?authSource={MONGO_DATABASE}"
else:
connection_string = f"mongodb://{MONGO_HOST}:{MONGO_PORT}/"
# 연결 풀링 설정 (고트래픽 대응)
self.client = MongoClient(
connection_string,
serverSelectionTimeoutMS=5000,
maxPoolSize=50, # 최대 연결 수 (기본값: 100)
minPoolSize=10, # 최소 연결 수 (기본값: 0)
maxIdleTimeMS=45000, # 유휴 연결 유지 시간
)
self.db = self.client[MONGO_DATABASE]
self.collection = self.db[MONGO_COLLECTION]
# 인덱스 생성 (성능 최적화)
# background=True: 인덱스 생성 시 DB 블로킹 방지
self.collection.create_index(
[("bot_id", 1), ("timestamp", -1)], # -1 = DESCENDING
background=True,
name="idx_bot_timestamp"
)
# TTL 인덱스 생성 (자동 삭제)
ttl_seconds = MONGO_TTL_DAYS * 24 * 60 * 60 # 일 단위 → 초 단위 변환
self.collection.create_index(
[("timestamp", 1)],
expireAfterSeconds=ttl_seconds,
background=True,
name="idx_ttl"
)
print(f"[ChatHistory] MongoDB 연결 성공: {MONGO_HOST}:{MONGO_PORT}/{MONGO_DATABASE}")
print(f"[ChatHistory] TTL 설정: {MONGO_TTL_DAYS}일 ({ttl_seconds}초) 후 자동 삭제")
except PyMongoError as e:
print(f"[ChatHistory] MongoDB 연결 실패: {e}")
raise
def save_conversation(
self,
bot_id: Optional[str],
user_query: str,
ai_response: str,
matched_questions: List[str],
scores: List[float],
metadata: Optional[Dict] = None
) -> str:
"""
대화 기록 저장
Args:
bot_id: 봇 ID (없으면 None)
user_query: 사용자 질문
ai_response: AI 답변
matched_questions: 매칭된 질문 목록
scores: 매칭 점수
metadata: 추가 메타데이터
Returns:
저장된 문서의 ObjectId (문자열)
"""
try:
doc = {
"bot_id": bot_id, # None 허용
"user_query": user_query,
"ai_response": ai_response,
"matched_questions": matched_questions,
"scores": scores,
"metadata": metadata or {},
"timestamp": datetime.now(timezone.utc)
}
result = self.collection.insert_one(doc)
print(f"[ChatHistory] 대화 저장 완료: bot_id={bot_id}, id={result.inserted_id}")
return str(result.inserted_id)
except PyMongoError as e:
print(f"[ChatHistory] 저장 실패: {e}")
raise
def get_recent_history(
self,
bot_id: Optional[str],
hours: int = 24,
limit: int = 10
) -> List[Dict]:
"""
최근 대화 이력 조회 (24시간 이내)
Args:
bot_id: 봇 ID (None이면 전체 조회)
hours: 조회 시간 범위 (기본 24시간)
limit: 최대 조회 개수
Returns:
대화 이력 리스트 (최신순)
"""
try:
# 시간 필터 (UTC 기준)
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=hours)
# 쿼리 구성 (인덱스 순서에 맞춤: bot_id → timestamp)
query = {}
if bot_id is not None:
query["bot_id"] = bot_id
query["timestamp"] = {"$gte": cutoff_time}
# 조회 (복합 인덱스 활용: bot_id + timestamp)
cursor = self.collection.find(query).sort("timestamp", DESCENDING).limit(limit)
# 결과 변환
history = []
for doc in cursor:
history.append({
"user_query": doc.get("user_query"),
"ai_response": doc.get("ai_response"),
"timestamp": doc.get("timestamp").isoformat() if doc.get("timestamp") else None
})
print(f"[ChatHistory] 이력 조회: bot_id={bot_id}, {len(history)}개")
return list(reversed(history)) # 시간순 정렬 (오래된 것 → 최신)
except PyMongoError as e:
print(f"[ChatHistory] 조회 실패: {e}")
return []
def get_context_for_llm(
self,
bot_id: Optional[str],
hours: int = 24,
max_conversations: int = 10
) -> str:
"""
LLM에 전달할 대화 컨텍스트 생성
Args:
bot_id: 봇 ID
hours: 조회 시간 범위
max_conversations: 최대 대화 개수
Returns:
포맷된 대화 이력 문자열
"""
history = self.get_recent_history(bot_id, hours, max_conversations)
if not history:
return ""
# 포맷팅 (추천 블록 제외 — LLM용 본문 전문, get_messages_for_llm과 동일)
context_parts = ["【이전 대화 이력】"]
for i, conv in enumerate(history, 1):
body = strip_suggestion_block(conv.get("ai_response"))
context_parts.append(
f"[대화 {i}]\n"
f"고객: {conv['user_query']}\n"
f"상담원: {body}"
)
return "\n\n".join(context_parts)
def get_messages_for_llm(
self,
bot_id: Optional[str],
hours: int = 24,
max_conversations: int = 10
) -> List[Dict[str, str]]:
"""
LLM messages format용 대화 이력 (표준 chat completion)
Args:
bot_id: 봇 ID
hours: 조회 시간 범위
max_conversations: 최대 대화 개수
Returns:
[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
"""
history = self.get_recent_history(bot_id, hours, max_conversations)
if not history:
return []
# messages 포맷으로 변환 (추천 블록 제외 — LLM용 본문만)
messages = []
for conv in history:
user_content = str(conv.get("user_query") or "").strip()
assistant_content = strip_suggestion_block(conv.get("ai_response")).strip()
if not user_content or not assistant_content:
continue
messages.append({"role": "user", "content": user_content})
messages.append({
"role": "assistant",
"content": assistant_content,
})
return messages
def cleanup_old_records(self, days: int = 7):
"""
오래된 기록 정리 (TTL 인덱스와 별개로 수동 정리)
Args:
days: 보관 일수
"""
try:
cutoff_time = datetime.now(timezone.utc) - timedelta(days=days)
result = self.collection.delete_many({"timestamp": {"$lt": cutoff_time}})
print(f"[ChatHistory] 정리 완료: {result.deleted_count}개 삭제")
except PyMongoError as e:
print(f"[ChatHistory] 정리 실패: {e}")
def __del__(self):
"""연결 종료"""
if hasattr(self, 'client'):
self.client.close()
# 싱글톤 인스턴스
_chat_history_manager = None
def get_chat_history_manager() -> ChatHistoryManager:
"""ChatHistoryManager 싱글톤 인스턴스 반환"""
global _chat_history_manager
if _chat_history_manager is None:
_chat_history_manager = ChatHistoryManager()
return _chat_history_manager
if __name__ == "__main__":
# 테스트
manager = get_chat_history_manager()
# 저장 테스트
doc_id = manager.save_conversation(
user_id="test_user_123",
user_query="테스트 질문입니다",
ai_response="테스트 답변입니다",
matched_questions=["관련 질문 1", "관련 질문 2"],
scores=[0.95, 0.88]
)
print(f"저장된 ID: {doc_id}")
# 조회 테스트
history = manager.get_recent_history("test_user_123")
print(f"조회 결과: {len(history)}개")
# 컨텍스트 생성 테스트
context = manager.get_context_for_llm("test_user_123")
print(f"컨텍스트:\n{context}")