pharmacy-pos-qr-system/backend/utils/animal_chat_logger.py
thug0bin 5d7a8fc3f4 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
- 통계: 총 대화, 평균 응답시간, 총 토큰, 총 비용
2026-03-08 15:17:11 +09:00

278 lines
8.5 KiB
Python

# -*- coding: utf-8 -*-
"""
동물약 챗봇 로깅 모듈
- SQLite에 대화 로그 저장
- 각 단계별 소요시간, 토큰, 비용 기록
"""
import os
import json
import sqlite3
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, field, asdict
logger = logging.getLogger(__name__)
# DB 경로
DB_PATH = Path(__file__).parent.parent / "db" / "animal_chat_logs.db"
SCHEMA_PATH = Path(__file__).parent.parent / "db" / "animal_chat_logs_schema.sql"
# GPT-4o-mini 가격 (USD per 1K tokens)
INPUT_COST_PER_1K = 0.00015 # $0.15 / 1M = $0.00015 / 1K
OUTPUT_COST_PER_1K = 0.0006 # $0.60 / 1M = $0.0006 / 1K
@dataclass
class ChatLogEntry:
"""챗봇 로그 엔트리"""
session_id: str = ""
# 입력
user_message: str = ""
history_length: int = 0
# MSSQL
mssql_drug_count: int = 0
mssql_duration_ms: int = 0
# PostgreSQL
pgsql_rag_count: int = 0
pgsql_duration_ms: int = 0
# LanceDB
vector_results_count: int = 0
vector_top_scores: List[float] = field(default_factory=list)
vector_sources: List[str] = field(default_factory=list)
vector_duration_ms: int = 0
# OpenAI
openai_model: str = ""
openai_prompt_tokens: int = 0
openai_completion_tokens: int = 0
openai_total_tokens: int = 0
openai_cost_usd: float = 0.0
openai_duration_ms: int = 0
# 출력
assistant_response: str = ""
products_mentioned: List[str] = field(default_factory=list)
# 메타
total_duration_ms: int = 0
error: str = ""
def calculate_cost(self):
"""토큰 기반 비용 계산"""
self.openai_cost_usd = (
self.openai_prompt_tokens * INPUT_COST_PER_1K / 1000 +
self.openai_completion_tokens * OUTPUT_COST_PER_1K / 1000
)
def init_db():
"""DB 초기화 (테이블 생성)"""
try:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
if SCHEMA_PATH.exists():
schema = SCHEMA_PATH.read_text(encoding='utf-8')
conn.executescript(schema)
else:
# 스키마 파일 없으면 인라인 생성
conn.executescript("""
CREATE TABLE IF NOT EXISTS chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
user_message TEXT,
history_length INTEGER,
mssql_drug_count INTEGER,
mssql_duration_ms INTEGER,
pgsql_rag_count INTEGER,
pgsql_duration_ms INTEGER,
vector_results_count INTEGER,
vector_top_scores TEXT,
vector_sources TEXT,
vector_duration_ms INTEGER,
openai_model TEXT,
openai_prompt_tokens INTEGER,
openai_completion_tokens INTEGER,
openai_total_tokens INTEGER,
openai_cost_usd REAL,
openai_duration_ms INTEGER,
assistant_response TEXT,
products_mentioned TEXT,
total_duration_ms INTEGER,
error TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_chat_created ON chat_logs(created_at);
""")
conn.commit()
conn.close()
logger.info(f"동물약 챗봇 로그 DB 초기화: {DB_PATH}")
return True
except Exception as e:
logger.error(f"DB 초기화 실패: {e}")
return False
def log_chat(entry: ChatLogEntry) -> Optional[int]:
"""
챗봇 대화 로그 저장
Returns:
저장된 로그 ID (실패시 None)
"""
try:
# 비용 계산
entry.calculate_cost()
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
cursor.execute("""
INSERT INTO chat_logs (
session_id, user_message, history_length,
mssql_drug_count, mssql_duration_ms,
pgsql_rag_count, pgsql_duration_ms,
vector_results_count, vector_top_scores, vector_sources, vector_duration_ms,
openai_model, openai_prompt_tokens, openai_completion_tokens,
openai_total_tokens, openai_cost_usd, openai_duration_ms,
assistant_response, products_mentioned,
total_duration_ms, error
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
entry.session_id,
entry.user_message,
entry.history_length,
entry.mssql_drug_count,
entry.mssql_duration_ms,
entry.pgsql_rag_count,
entry.pgsql_duration_ms,
entry.vector_results_count,
json.dumps(entry.vector_top_scores),
json.dumps(entry.vector_sources),
entry.vector_duration_ms,
entry.openai_model,
entry.openai_prompt_tokens,
entry.openai_completion_tokens,
entry.openai_total_tokens,
entry.openai_cost_usd,
entry.openai_duration_ms,
entry.assistant_response,
json.dumps(entry.products_mentioned),
entry.total_duration_ms,
entry.error
))
log_id = cursor.lastrowid
conn.commit()
conn.close()
logger.debug(f"챗봇 로그 저장: ID={log_id}, tokens={entry.openai_total_tokens}")
return log_id
except Exception as e:
logger.error(f"로그 저장 실패: {e}")
return None
def get_logs(
limit: int = 100,
offset: int = 0,
date_from: str = None,
date_to: str = None,
error_only: bool = False
) -> List[Dict]:
"""로그 조회"""
try:
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = "SELECT * FROM chat_logs WHERE 1=1"
params = []
if date_from:
query += " AND created_at >= ?"
params.append(date_from)
if date_to:
query += " AND created_at <= ?"
params.append(date_to + " 23:59:59")
if error_only:
query += " AND error IS NOT NULL AND error != ''"
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
params.extend([limit, offset])
cursor.execute(query, params)
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"로그 조회 실패: {e}")
return []
def get_stats(date_from: str = None, date_to: str = None) -> Dict:
"""통계 조회"""
try:
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
query = """
SELECT
COUNT(*) as total_chats,
AVG(total_duration_ms) as avg_duration_ms,
SUM(openai_total_tokens) as total_tokens,
SUM(openai_cost_usd) as total_cost_usd,
AVG(openai_total_tokens) as avg_tokens,
SUM(CASE WHEN error IS NOT NULL AND error != '' THEN 1 ELSE 0 END) as error_count,
AVG(vector_duration_ms) as avg_vector_ms,
AVG(openai_duration_ms) as avg_openai_ms
FROM chat_logs
WHERE 1=1
"""
params = []
if date_from:
query += " AND created_at >= ?"
params.append(date_from)
if date_to:
query += " AND created_at <= ?"
params.append(date_to + " 23:59:59")
cursor.execute(query, params)
row = cursor.fetchone()
conn.close()
if row:
return {
'total_chats': row[0] or 0,
'avg_duration_ms': round(row[1] or 0),
'total_tokens': row[2] or 0,
'total_cost_usd': round(row[3] or 0, 4),
'avg_tokens': round(row[4] or 0),
'error_count': row[5] or 0,
'avg_vector_ms': round(row[6] or 0),
'avg_openai_ms': round(row[7] or 0)
}
return {}
except Exception as e:
logger.error(f"통계 조회 실패: {e}")
return {}
# 모듈 로드 시 DB 초기화
init_db()