- 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) - 피드백 수집 기능
352 lines
9.4 KiB
Python
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
|
|
}
|
|
}
|