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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user