Agent 2.0 exdev 서버 배포 스택
- server-dev start/stop/deploy 및 Gitea push 자동 배포 - local-dev 로컬 개발 환경 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
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}")
|
||||
|
||||
Reference in New Issue
Block a user