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:
thug0bin
2026-03-05 00:36:51 +09:00
parent 141b211f07
commit 1b33f82fd4
6 changed files with 1755 additions and 0 deletions

View File

@@ -816,3 +816,380 @@ def get_patient_otc_history(cus_code):
except Exception as e:
logging.error(f"환자 OTC 구매 이력 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ─────────────────────────────────────────────────────────────
# PAAI (Pharmacist Assistant AI) API
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/paai/analyze', methods=['POST'])
def paai_analyze():
"""
PAAI 분석 API
Request:
{
"pre_serial": "P20260304001",
"cus_code": "00001234",
"patient_name": "홍길동",
"disease_info": {
"code_1": "M170", "name_1": "무릎골관절염",
"code_2": "K299", "name_2": "상세불명의 위십이지장염"
},
"current_medications": [
{"code": "055101150", "name": "록소프로펜정", "dosage": 1, "frequency": 3, "days": 7}
],
"previous_serial": "P20260225001",
"previous_medications": [...],
"otc_history": {...}
}
"""
import requests as http_requests
import time as time_module
from db.paai_logger import create_log, update_kims_result, update_ai_result, update_error
start_time = time_module.time()
log_id = None
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '요청 데이터가 없습니다.'}), 400
pre_serial = data.get('pre_serial')
cus_code = data.get('cus_code')
patient_name = data.get('patient_name')
disease_info = data.get('disease_info', {})
current_medications = data.get('current_medications', [])
previous_serial = data.get('previous_serial')
previous_medications = data.get('previous_medications', [])
otc_history = data.get('otc_history', {})
# 처방 변화 분석
prescription_changes = analyze_prescription_changes(
current_medications, previous_medications
)
# 1. 로그 생성
log_id = create_log(
pre_serial=pre_serial,
patient_code=cus_code,
patient_name=patient_name,
disease_code_1=disease_info.get('code_1'),
disease_name_1=disease_info.get('name_1'),
disease_code_2=disease_info.get('code_2'),
disease_name_2=disease_info.get('name_2'),
current_medications=current_medications,
previous_serial=previous_serial,
previous_medications=previous_medications,
prescription_changes=prescription_changes,
otc_history=otc_history
)
# 2. KD코드 추출 (9자리 KIMS 코드)
kd_codes = []
drug_names = []
for med in current_medications:
code = med.get('code', '')
if code and len(str(code)) == 9:
kd_codes.append(str(code))
drug_names.append(med.get('name', ''))
# 3. KIMS API 호출 (약품 2개 이상인 경우)
kims_interactions = []
kims_start = time_module.time()
if len(kd_codes) >= 2:
try:
kims_url = "https://api2.kims.co.kr/api/interaction/info"
kims_headers = {
'Authorization': 'Basic VFNQTUtSOg==',
'Content-Type': 'application/json',
'Accept': 'application/json; charset=utf-8'
}
kims_payload = {'KDCodes': kd_codes}
kims_response = http_requests.get(
kims_url,
headers=kims_headers,
data=__import__('json').dumps(kims_payload),
timeout=10,
verify=False
)
if kims_response.status_code == 200:
kims_data = kims_response.json()
if kims_data.get('Message') == 'SUCCESS':
kims_interactions = kims_data.get('InteractionList', [])
except Exception as kims_err:
logging.warning(f"KIMS API 오류 (무시하고 계속): {kims_err}")
kims_time = int((time_module.time() - kims_start) * 1000)
# KIMS 결과 로그 업데이트
update_kims_result(
log_id=log_id,
kims_drug_codes=kd_codes,
kims_interactions=kims_interactions,
kims_response_time_ms=kims_time
)
# 4. AI 프롬프트 생성
ai_prompt = build_paai_prompt(
disease_info=disease_info,
current_medications=current_medications,
prescription_changes=prescription_changes,
kims_interactions=kims_interactions,
otc_history=otc_history
)
# 5. Clawdbot AI 호출 (WebSocket)
ai_start = time_module.time()
ai_response = call_clawdbot_ai(ai_prompt)
ai_time = int((time_module.time() - ai_start) * 1000)
# AI 결과 로그 업데이트
update_ai_result(
log_id=log_id,
ai_prompt=ai_prompt,
ai_model='claude-sonnet-4',
ai_response=ai_response,
ai_response_time_ms=ai_time
)
total_time = int((time_module.time() - start_time) * 1000)
return jsonify({
'success': True,
'log_id': log_id,
'analysis': ai_response,
'kims_summary': {
'drug_count': len(kd_codes),
'interaction_count': len(kims_interactions),
'has_severe': any(str(i.get('Severity', '5')) in ['1', '2'] for i in kims_interactions)
},
'timing': {
'kims_ms': kims_time,
'ai_ms': ai_time,
'total_ms': total_time
}
})
except Exception as e:
logging.error(f"PAAI 분석 오류: {e}")
if log_id:
update_error(log_id, str(e))
return jsonify({'success': False, 'error': str(e)}), 500
def analyze_prescription_changes(current: list, previous: list) -> dict:
"""처방 변화 분석"""
current_codes = {m.get('code'): m for m in current}
previous_codes = {m.get('code'): m for m in previous}
added = []
removed = []
changed = []
same = []
# 추가된 약품
for code, med in current_codes.items():
if code not in previous_codes:
added.append(med)
else:
# 변경 여부 확인
prev_med = previous_codes[code]
changes = []
for field in ['dosage', 'frequency', 'days']:
if med.get(field) != prev_med.get(field):
changes.append({
'field': field,
'from': prev_med.get(field),
'to': med.get(field)
})
if changes:
changed.append({'medication': med, 'changes': changes})
else:
same.append(med)
# 중단된 약품
for code, med in previous_codes.items():
if code not in current_codes:
removed.append(med)
return {
'added': added,
'removed': removed,
'changed': changed,
'same': same
}
def build_paai_prompt(
disease_info: dict,
current_medications: list,
prescription_changes: dict,
kims_interactions: list,
otc_history: dict
) -> str:
"""AI 프롬프트 생성"""
# 질병 정보
diseases = []
if disease_info.get('code_1'):
diseases.append(f"[{disease_info['code_1']}] {disease_info.get('name_1', '')}")
if disease_info.get('code_2'):
diseases.append(f"[{disease_info['code_2']}] {disease_info.get('name_2', '')}")
# 현재 처방
med_lines = []
for med in current_medications:
line = f"- {med.get('name', '?')}: {med.get('dosage', 0)}× {med.get('frequency', 0)}× {med.get('days', 0)}"
med_lines.append(line)
# 처방 변화
change_lines = []
if prescription_changes.get('added'):
names = [m.get('name', '?') for m in prescription_changes['added']]
change_lines.append(f"- 추가: {', '.join(names)}")
if prescription_changes.get('removed'):
names = [m.get('name', '?') for m in prescription_changes['removed']]
change_lines.append(f"- 중단: {', '.join(names)}")
if prescription_changes.get('changed'):
for item in prescription_changes['changed']:
med = item['medication']
changes = item['changes']
change_desc = ', '.join([f"{c['field']}: {c['from']}{c['to']}" for c in changes])
change_lines.append(f"- 변경: {med.get('name', '?')} ({change_desc})")
# KIMS 상호작용
kims_lines = []
for inter in kims_interactions:
severity = inter.get('Severity', 5)
severity_text = {1: '🔴 금기', 2: '🟠 경고', 3: '🟡 주의', 4: '참고', 5: '정보'}.get(int(severity), '정보')
drug1 = inter.get('Drug1Name', '?')
drug2 = inter.get('Drug2Name', '?')
desc = inter.get('InteractionDesc', '')[:100]
kims_lines.append(f"- [{severity_text}] {drug1} + {drug2}: {desc}")
# OTC 이력
otc_lines = []
if otc_history.get('frequent_items'):
for item in otc_history['frequent_items'][:5]:
otc_lines.append(f"- {item.get('name', '?')} ({item.get('count', 0)}회 구매)")
prompt = f"""당신은 약사를 보조하는 AI입니다. 환자 정보와 KIMS 상호작용 데이터를 바탕으로 분석해주세요.
## 환자 질병
{chr(10).join(diseases) if diseases else '- 정보 없음'}
## 현재 처방
{chr(10).join(med_lines) if med_lines else '- 정보 없음'}
## 처방 변화 (vs 이전)
{chr(10).join(change_lines) if change_lines else '- 변화 없음'}
## KIMS 약물 상호작용
{chr(10).join(kims_lines) if kims_lines else '- 상호작용 없음'}
## OTC 구매 이력
{chr(10).join(otc_lines) if otc_lines else '- 이력 없음'}
## 요청
다음 형식의 JSON으로 간결하게 답변해주세요:
{{
"prescription_insight": "처방 변화에 대한 분석 (1-2문장)",
"kims_analysis": "KIMS 상호작용 해석 및 임상적 의미 (1-2문장)",
"cautions": ["복용 주의사항 1", "복용 주의사항 2"],
"otc_recommendations": [
{{"product": "추천 OTC명", "reason": "추천 이유 (현재 처방과 상호작용 없음 확인)"}}
],
"counseling_points": ["상담 포인트 1", "상담 포인트 2"]
}}
"""
return prompt
def call_clawdbot_ai(prompt: str) -> dict:
"""Clawdbot AI 호출 (HTTP API)"""
import requests as http_requests
import json
try:
# Clawdbot Gateway API 호출
response = http_requests.post(
'http://localhost:8765/api/chat',
json={
'message': prompt,
'session': 'paai-analysis',
'timeout': 60
},
timeout=65
)
if response.status_code == 200:
result = response.json()
# AI 응답에서 JSON 파싱 시도
ai_text = result.get('response', '')
# JSON 블록 추출
try:
import re
json_match = re.search(r'\{[\s\S]*\}', ai_text)
if json_match:
return json.loads(json_match.group())
except:
pass
# JSON 파싱 실패 시 텍스트 그대로 반환
return {
'prescription_insight': ai_text[:200] if ai_text else '분석 결과 없음',
'kims_analysis': '',
'cautions': [],
'otc_recommendations': [],
'counseling_points': []
}
else:
raise Exception(f"Clawdbot API 오류: {response.status_code}")
except http_requests.exceptions.ConnectionError:
# Clawdbot 연결 안됨 - 기본 응답 생성
return generate_fallback_response(prompt)
except Exception as e:
logging.error(f"Clawdbot AI 호출 오류: {e}")
return generate_fallback_response(prompt)
def generate_fallback_response(prompt: str) -> dict:
"""Clawdbot 연결 실패 시 기본 응답"""
return {
'prescription_insight': 'AI 분석 서비스에 연결할 수 없습니다. KIMS 상호작용 정보를 직접 확인해주세요.',
'kims_analysis': '',
'cautions': ['AI 분석 불가 - 직접 검토 필요'],
'otc_recommendations': [],
'counseling_points': [],
'_fallback': True
}
@pmr_bp.route('/api/paai/feedback', methods=['POST'])
def paai_feedback():
"""PAAI 피드백 저장"""
from db.paai_logger import update_feedback
try:
data = request.get_json()
log_id = data.get('log_id')
useful = data.get('useful')
comment = data.get('comment')
if not log_id:
return jsonify({'success': False, 'error': 'log_id 필요'}), 400
update_feedback(log_id, useful, comment)
return jsonify({'success': True})
except Exception as e:
logging.error(f"PAAI 피드백 저장 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500