- 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 - 통계: 총 대화, 평균 응답시간, 총 토큰, 총 비용
278 lines
8.5 KiB
Python
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()
|