feat(animal-chat): 로깅 시스템 구축

- SQLite DB: animal_chat_logs.db
- 로거 모듈: utils/animal_chat_logger.py
- 단계별 로깅:
  - MSSQL (보유 동물약): 개수, 소요시간
  - PostgreSQL (RAG): 개수, 소요시간
  - LanceDB (벡터 검색): 상위 N개, 유사도, 소스, 소요시간
  - OpenAI: 모델, 토큰(입력/출력), 비용, 소요시간
- Admin 페이지: /admin/animal-chat-logs
- API: /api/animal-chat-logs
- 통계: 총 대화, 평균 응답시간, 총 토큰, 총 비용
This commit is contained in:
thug0bin
2026-03-08 15:17:11 +09:00
parent be1e6c2bb7
commit 5d7a8fc3f4
4 changed files with 1012 additions and 7 deletions

View File

@@ -3142,6 +3142,13 @@ def api_animal_chat():
}
"""
try:
import time
from utils.animal_chat_logger import ChatLogEntry, log_chat
# 로그 엔트리 초기화
log_entry = ChatLogEntry()
total_start = time.time()
if not OPENAI_AVAILABLE:
return jsonify({
'success': False,
@@ -3157,12 +3164,24 @@ def api_animal_chat():
'message': '메시지를 입력해주세요.'
}), 400
# 보유 동물약 목록 조회
animal_drugs = _get_animal_drugs()
# 입력 로깅
last_user_msg = next((m['content'] for m in reversed(messages) if m.get('role') == 'user'), '')
log_entry.user_message = last_user_msg
log_entry.history_length = len(messages)
log_entry.session_id = data.get('session_id', '')
# APC가 있는 제품의 상세 정보 조회 (RAG)
# 보유 동물약 목록 조회 (MSSQL)
mssql_start = time.time()
animal_drugs = _get_animal_drugs()
log_entry.mssql_drug_count = len(animal_drugs)
log_entry.mssql_duration_ms = int((time.time() - mssql_start) * 1000)
# APC가 있는 제품의 상세 정보 조회 (PostgreSQL RAG)
pgsql_start = time.time()
apc_codes = [d['apc'] for d in animal_drugs if d.get('apc')]
rag_data = _get_animal_drug_rag(apc_codes) if apc_codes else {}
log_entry.pgsql_rag_count = len(rag_data)
log_entry.pgsql_duration_ms = int((time.time() - pgsql_start) * 1000)
available_products_text = ""
if animal_drugs:
@@ -3194,15 +3213,19 @@ def api_animal_chat():
# 벡터 DB 검색 (LanceDB RAG)
vector_context = ""
vector_start = time.time()
try:
from utils.animal_rag import get_animal_rag
# 마지막 사용자 메시지로 검색
last_user_msg = next((m['content'] for m in reversed(messages) if m.get('role') == 'user'), '')
if last_user_msg:
rag = get_animal_rag()
vector_results = rag.search(last_user_msg, n_results=3)
log_entry.vector_results_count = len(vector_results)
log_entry.vector_top_scores = [r.get('score', 0) for r in vector_results]
log_entry.vector_sources = [f"{r.get('source', '')}#{r.get('section', '')}" for r in vector_results]
vector_context = rag.get_context_for_chat(last_user_msg, n_results=3)
except Exception as e:
logging.warning(f"벡터 검색 실패 (무시): {e}")
log_entry.vector_duration_ms = int((time.time() - vector_start) * 1000)
# System Prompt 구성
system_prompt = ANIMAL_CHAT_SYSTEM_PROMPT.format(
@@ -3211,6 +3234,7 @@ def api_animal_chat():
)
# OpenAI API 호출
openai_start = time.time()
client = OpenAI(api_key=OPENAI_API_KEY)
api_messages = [{"role": "system", "content": system_prompt}]
@@ -3229,6 +3253,13 @@ def api_animal_chat():
ai_response = response.choices[0].message.content
# OpenAI 로깅
log_entry.openai_model = OPENAI_MODEL
log_entry.openai_prompt_tokens = response.usage.prompt_tokens
log_entry.openai_completion_tokens = response.usage.completion_tokens
log_entry.openai_total_tokens = response.usage.total_tokens
log_entry.openai_duration_ms = int((time.time() - openai_start) * 1000)
# 응답에서 언급된 보유 제품 찾기 (부분 매칭)
mentioned_products = []
# 공백 제거한 버전도 준비 (AI가 띄어쓰기 넣을 수 있음)
@@ -3289,6 +3320,12 @@ def api_animal_chat():
'category': drug.get('category') # 분류 (내부구충제, 심장사상충약 등)
})
# 최종 로깅
log_entry.assistant_response = ai_response
log_entry.products_mentioned = [p['name'] for p in mentioned_products[:5]]
log_entry.total_duration_ms = int((time.time() - total_start) * 1000)
log_chat(log_entry)
return jsonify({
'success': True,
'message': ai_response,
@@ -3299,18 +3336,27 @@ def api_animal_chat():
}
})
except RateLimitError:
except RateLimitError as e:
log_entry.error = f"RateLimitError: {e}"
log_entry.total_duration_ms = int((time.time() - total_start) * 1000)
log_chat(log_entry)
return jsonify({
'success': False,
'message': 'AI 사용량 한도에 도달했습니다. 잠시 후 다시 시도해주세요.'
}), 429
except APITimeoutError:
except APITimeoutError as e:
log_entry.error = f"APITimeoutError: {e}"
log_entry.total_duration_ms = int((time.time() - total_start) * 1000)
log_chat(log_entry)
return jsonify({
'success': False,
'message': 'AI 응답 시간이 초과되었습니다. 다시 시도해주세요.'
}), 504
except Exception as e:
logging.error(f"동물약 챗봇 오류: {e}")
log_entry.error = str(e)
log_entry.total_duration_ms = int((time.time() - total_start) * 1000)
log_chat(log_entry)
return jsonify({
'success': False,
'message': f'오류가 발생했습니다: {str(e)}'
@@ -8008,6 +8054,44 @@ def mobile_upload_page(session_id):
''', session_id=session_id, barcode=barcode)
# ============================================
# 동물약 챗봇 로그 API
# ============================================
@app.route('/admin/animal-chat-logs')
def admin_animal_chat_logs():
"""동물약 챗봇 로그 페이지"""
return render_template('admin_animal_chat_logs.html')
@app.route('/api/animal-chat-logs')
def api_animal_chat_logs():
"""동물약 챗봇 로그 조회 API"""
from utils.animal_chat_logger import get_logs, get_stats
date_from = request.args.get('date_from')
date_to = request.args.get('date_to')
error_only = request.args.get('error_only') == 'true'
limit = int(request.args.get('limit', 100))
offset = int(request.args.get('offset', 0))
logs = get_logs(
limit=limit,
offset=offset,
date_from=date_from,
date_to=date_to,
error_only=error_only
)
stats = get_stats(date_from=date_from, date_to=date_to)
return jsonify({
'success': True,
'logs': logs,
'stats': stats
})
if __name__ == '__main__':
import os