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) - 피드백 수집 기능
This commit is contained in:
351
backend/db/paai_logger.py
Normal file
351
backend/db/paai_logger.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
}
|
||||
59
backend/db/paai_logs_schema.sql
Normal file
59
backend/db/paai_logs_schema.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- PAAI (Pharmacist Assistant AI) 로그 스키마
|
||||
-- 생성일: 2026-03-04
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paai_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 요청 정보
|
||||
pre_serial TEXT, -- 처방번호
|
||||
patient_code TEXT, -- 환자코드 (CusCode)
|
||||
patient_name TEXT, -- 환자명 (마스킹: 김**)
|
||||
|
||||
-- 질병 정보
|
||||
disease_code_1 TEXT, -- St1 (상병코드1)
|
||||
disease_name_1 TEXT, -- 상병명1
|
||||
disease_code_2 TEXT, -- St2 (상병코드2)
|
||||
disease_name_2 TEXT, -- 상병명2
|
||||
|
||||
-- 처방 정보
|
||||
current_medications TEXT, -- JSON: 현재 처방 [{code, name, dosage, ...}]
|
||||
current_med_count INTEGER, -- 현재 처방 약품 수
|
||||
previous_serial TEXT, -- 이전 처방번호
|
||||
previous_medications TEXT, -- JSON: 이전 처방
|
||||
prescription_changes TEXT, -- JSON: {added, removed, changed}
|
||||
|
||||
-- OTC 이력
|
||||
otc_history TEXT, -- JSON: {purchases, frequent_items}
|
||||
otc_visit_count INTEGER, -- OTC 구매 횟수
|
||||
|
||||
-- KIMS 상호작용
|
||||
kims_drug_codes TEXT, -- JSON: 검사한 KD코드 배열
|
||||
kims_drug_count INTEGER, -- 검사한 약품 수
|
||||
kims_interactions TEXT, -- JSON: 상호작용 결과
|
||||
kims_interaction_count INTEGER, -- 상호작용 건수
|
||||
kims_has_severe BOOLEAN DEFAULT 0, -- 심각한 상호작용 (severity 1,2)
|
||||
kims_response_time_ms INTEGER, -- KIMS API 응답시간
|
||||
|
||||
-- AI 분석
|
||||
ai_prompt TEXT, -- AI에 전달한 프롬프트
|
||||
ai_model TEXT, -- 사용된 모델
|
||||
ai_response TEXT, -- JSON: AI 분석 결과
|
||||
ai_response_time_ms INTEGER, -- AI 응답 시간
|
||||
ai_token_count INTEGER, -- 토큰 사용량
|
||||
|
||||
-- 상태
|
||||
status TEXT DEFAULT 'pending', -- pending, kims_done, success, error
|
||||
error_message TEXT,
|
||||
|
||||
-- 피드백
|
||||
feedback_useful INTEGER, -- 1=유용, 0=아님, NULL=미응답
|
||||
feedback_comment TEXT -- 약사 코멘트
|
||||
);
|
||||
|
||||
-- 인덱스
|
||||
CREATE INDEX IF NOT EXISTS idx_paai_created ON paai_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_paai_patient ON paai_logs(patient_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_paai_status ON paai_logs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_paai_serial ON paai_logs(pre_serial);
|
||||
CREATE INDEX IF NOT EXISTS idx_paai_severe ON paai_logs(kims_has_severe);
|
||||
Reference in New Issue
Block a user