pharmacy-pos-qr-system/backend/db/paai_logger.py
thug0bin 1b33f82fd4 feat: PAAI (Pharmacist Assistant AI) 기능 구현
- PAAI 로그 테이블 스키마 (paai_logs_schema.sql)
- PAAI 로거 모듈 (db/paai_logger.py)
- /pmr/api/paai/analyze API 엔드포인트
- KIMS API 연동 (KD코드 기반 상호작용 조회)
- Clawdbot AI 연동 (HTTP API)
- PMR 화면 PAAI 버튼 및 모달
- Admin 페이지 (/admin/paai)
- 피드백 수집 기능
2026-03-05 00:36:51 +09:00

352 lines
9.4 KiB
Python

"""
PAAI (Pharmacist Assistant AI) 로깅 모듈
- API 호출/응답 SQLite 저장
- 분석 결과 및 피드백 관리
"""
import sqlite3
import json
import os
from datetime import datetime, timedelta
from pathlib import Path
# DB 파일 경로
DB_PATH = Path(__file__).parent / 'paai_logs.db'
def init_db():
"""DB 초기화 (테이블 생성)"""
schema_path = Path(__file__).parent / 'paai_logs_schema.sql'
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
with open(schema_path, 'r', encoding='utf-8') as f:
schema = f.read()
cursor.executescript(schema)
conn.commit()
conn.close()
print(f"PAAI 로그 DB 초기화 완료: {DB_PATH}")
def create_log(
pre_serial: str = None,
patient_code: str = None,
patient_name: str = None,
disease_code_1: str = None,
disease_name_1: str = None,
disease_code_2: str = None,
disease_name_2: str = None,
current_medications: list = None,
previous_serial: str = None,
previous_medications: list = None,
prescription_changes: dict = None,
otc_history: dict = None
) -> int:
"""
PAAI 분석 로그 생성 (초기 상태)
Returns:
log_id: 생성된 로그 ID
"""
if not DB_PATH.exists():
init_db()
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
current_medications = current_medications or []
previous_medications = previous_medications or []
otc_history = otc_history or {}
# 환자명 마스킹
masked_name = None
if patient_name:
masked_name = patient_name[0] + '*' * (len(patient_name) - 1) if len(patient_name) > 1 else patient_name
cursor.execute("""
INSERT INTO paai_logs (
pre_serial, patient_code, patient_name,
disease_code_1, disease_name_1, disease_code_2, disease_name_2,
current_medications, current_med_count,
previous_serial, previous_medications, prescription_changes,
otc_history, otc_visit_count,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
""", (
pre_serial,
patient_code,
masked_name,
disease_code_1,
disease_name_1,
disease_code_2,
disease_name_2,
json.dumps(current_medications, ensure_ascii=False),
len(current_medications),
previous_serial,
json.dumps(previous_medications, ensure_ascii=False),
json.dumps(prescription_changes, ensure_ascii=False) if prescription_changes else None,
json.dumps(otc_history, ensure_ascii=False),
otc_history.get('visit_count', 0)
))
log_id = cursor.lastrowid
conn.commit()
conn.close()
return log_id
def update_kims_result(
log_id: int,
kims_drug_codes: list = None,
kims_interactions: list = None,
kims_response_time_ms: int = 0
):
"""KIMS 상호작용 결과 업데이트"""
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
kims_drug_codes = kims_drug_codes or []
kims_interactions = kims_interactions or []
# 심각한 상호작용 여부 (severity 1 또는 2)
has_severe = any(
str(i.get('severity', '5')) in ['1', '2']
for i in kims_interactions
)
cursor.execute("""
UPDATE paai_logs SET
kims_drug_codes = ?,
kims_drug_count = ?,
kims_interactions = ?,
kims_interaction_count = ?,
kims_has_severe = ?,
kims_response_time_ms = ?,
status = 'kims_done'
WHERE id = ?
""", (
json.dumps(kims_drug_codes, ensure_ascii=False),
len(kims_drug_codes),
json.dumps(kims_interactions, ensure_ascii=False),
len(kims_interactions),
1 if has_severe else 0,
kims_response_time_ms,
log_id
))
conn.commit()
conn.close()
def update_ai_result(
log_id: int,
ai_prompt: str = None,
ai_model: str = None,
ai_response: dict = None,
ai_response_time_ms: int = 0,
ai_token_count: int = None
):
"""AI 분석 결과 업데이트"""
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
cursor.execute("""
UPDATE paai_logs SET
ai_prompt = ?,
ai_model = ?,
ai_response = ?,
ai_response_time_ms = ?,
ai_token_count = ?,
status = 'success'
WHERE id = ?
""", (
ai_prompt,
ai_model,
json.dumps(ai_response, ensure_ascii=False) if ai_response else None,
ai_response_time_ms,
ai_token_count,
log_id
))
conn.commit()
conn.close()
def update_error(log_id: int, error_message: str):
"""에러 상태 업데이트"""
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
cursor.execute("""
UPDATE paai_logs SET
status = 'error',
error_message = ?
WHERE id = ?
""", (error_message, log_id))
conn.commit()
conn.close()
def update_feedback(log_id: int, useful: bool, comment: str = None):
"""피드백 업데이트"""
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
cursor.execute("""
UPDATE paai_logs SET
feedback_useful = ?,
feedback_comment = ?
WHERE id = ?
""", (1 if useful else 0, comment, log_id))
conn.commit()
conn.close()
def get_recent_logs(
limit: int = 100,
status: str = None,
has_severe: bool = None,
date: str = None
) -> list:
"""최근 로그 조회"""
if not DB_PATH.exists():
return []
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = "SELECT * FROM paai_logs WHERE 1=1"
params = []
if status:
query += " AND status = ?"
params.append(status)
if has_severe is not None:
query += " AND kims_has_severe = ?"
params.append(1 if has_severe else 0)
if date:
query += " AND DATE(created_at) = ?"
params.append(date)
query += " ORDER BY created_at DESC LIMIT ?"
params.append(limit)
cursor.execute(query, params)
rows = cursor.fetchall()
result = []
for row in rows:
log = dict(row)
# JSON 필드 파싱
for field in ['current_medications', 'previous_medications', 'prescription_changes',
'otc_history', 'kims_drug_codes', 'kims_interactions', 'ai_response']:
if log.get(field):
try:
log[field] = json.loads(log[field])
except:
pass
result.append(log)
conn.close()
return result
def get_log_detail(log_id: int) -> dict:
"""로그 상세 조회"""
if not DB_PATH.exists():
return None
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM paai_logs WHERE id = ?", (log_id,))
row = cursor.fetchone()
if not row:
conn.close()
return None
log = dict(row)
# JSON 필드 파싱
for field in ['current_medications', 'previous_medications', 'prescription_changes',
'otc_history', 'kims_drug_codes', 'kims_interactions', 'ai_response']:
if log.get(field):
try:
log[field] = json.loads(log[field])
except:
pass
conn.close()
return log
def get_stats() -> dict:
"""통계 조회"""
if not DB_PATH.exists():
return {
'total': 0,
'today': 0,
'success_rate': 0,
'avg_response_time': 0,
'severe_count': 0
}
conn = sqlite3.connect(str(DB_PATH))
cursor = conn.cursor()
today = datetime.now().strftime('%Y-%m-%d')
# 전체 건수
cursor.execute("SELECT COUNT(*) FROM paai_logs")
total = cursor.fetchone()[0]
# 오늘 건수
cursor.execute("SELECT COUNT(*) FROM paai_logs WHERE DATE(created_at) = ?", (today,))
today_count = cursor.fetchone()[0]
# 성공률
cursor.execute("SELECT COUNT(*) FROM paai_logs WHERE status = 'success'")
success_count = cursor.fetchone()[0]
success_rate = (success_count / total * 100) if total > 0 else 0
# 평균 응답시간
cursor.execute("SELECT AVG(ai_response_time_ms) FROM paai_logs WHERE ai_response_time_ms > 0")
avg_time = cursor.fetchone()[0] or 0
# 심각한 상호작용 건수 (오늘)
cursor.execute("""
SELECT COUNT(*) FROM paai_logs
WHERE DATE(created_at) = ? AND kims_has_severe = 1
""", (today,))
severe_count = cursor.fetchone()[0]
# 피드백 통계
cursor.execute("SELECT COUNT(*) FROM paai_logs WHERE feedback_useful = 1")
useful_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM paai_logs WHERE feedback_useful IS NOT NULL")
feedback_total = cursor.fetchone()[0]
conn.close()
return {
'total': total,
'today': today_count,
'success_rate': round(success_rate, 1),
'avg_response_time': int(avg_time),
'severe_count': severe_count,
'feedback': {
'useful': useful_count,
'total': feedback_total,
'rate': round(useful_count / feedback_total * 100, 1) if feedback_total > 0 else 0
}
}