From 59a55d6b22667364a263d06c241ee5e70eb00354 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Thu, 5 Mar 2026 09:30:22 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20PAAI=20Clawdbot=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HTTP API → WebSocket Gateway 방식으로 변경 - clawdbot_client.py의 ask_clawdbot() 함수 활용 - 시스템 프롬프트 분리 --- backend/pmr_api.py | 203 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 164 insertions(+), 39 deletions(-) diff --git a/backend/pmr_api.py b/backend/pmr_api.py index 302aead..14ff637 100644 --- a/backend/pmr_api.py +++ b/backend/pmr_api.py @@ -1111,52 +1111,48 @@ def build_paai_prompt( def call_clawdbot_ai(prompt: str) -> dict: - """Clawdbot AI 호출 (HTTP API)""" - import requests as http_requests + """Clawdbot AI 호출 (WebSocket Gateway)""" import json + import re + from services.clawdbot_client import ask_clawdbot + + PAAI_SYSTEM_PROMPT = """당신은 경험 많은 약사입니다. +처방 데이터를 분석하여 약사에게 유용한 정보를 제공합니다. +반드시 요청된 JSON 형식으로만 응답하세요.""" try: - # Clawdbot Gateway API 호출 - response = http_requests.post( - 'http://localhost:8765/api/chat', - json={ - 'message': prompt, - 'session': 'paai-analysis', - 'timeout': 60 - }, - timeout=65 + # Clawdbot Gateway WebSocket API 호출 + ai_text = ask_clawdbot( + message=prompt, + session_id='paai-analysis', + system_prompt=PAAI_SYSTEM_PROMPT, + timeout=60, + model='anthropic/claude-sonnet-4-5' # 빠른 Sonnet 사용 ) - if response.status_code == 200: - result = response.json() - # AI 응답에서 JSON 파싱 시도 - ai_text = result.get('response', '') + if not ai_text: + logging.warning("[PAAI] Clawdbot 응답 없음") + return generate_fallback_response(prompt) + + # JSON 블록 추출 + try: + json_match = re.search(r'\{[\s\S]*\}', ai_text) + if json_match: + return json.loads(json_match.group()) + except Exception as parse_err: + logging.warning(f"[PAAI] JSON 파싱 실패: {parse_err}") + + # JSON 파싱 실패 시 텍스트 그대로 반환 + return { + 'prescription_insight': ai_text[:500] if ai_text else '분석 결과 없음', + 'kims_analysis': '', + 'cautions': [], + 'otc_recommendations': [], + 'counseling_points': [] + } - # 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}") + logging.error(f"[PAAI] Clawdbot AI 호출 오류: {e}") return generate_fallback_response(prompt) @@ -1193,3 +1189,132 @@ def paai_feedback(): except Exception as e: logging.error(f"PAAI 피드백 저장 오류: {e}") return jsonify({'success': False, 'error': str(e)}), 500 + + +# ──────────────────────────────────────────────────────────────────────────────── +# PAAI 어드민 페이지 +# ──────────────────────────────────────────────────────────────────────────────── + +@pmr_bp.route('/admin') +def paai_admin_page(): + """PAAI 어드민 대시보드 페이지""" + return render_template('pmr_admin.html') + + +@pmr_bp.route('/api/admin/stats') +def paai_admin_stats(): + """PAAI 통계 API""" + from db.paai_logger import get_stats + + try: + stats = get_stats() + return jsonify({'success': True, 'stats': stats}) + except Exception as e: + logging.error(f"PAAI 통계 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@pmr_bp.route('/api/admin/logs') +def paai_admin_logs(): + """PAAI 로그 목록 API""" + from db.paai_logger import get_recent_logs + + try: + limit = request.args.get('limit', 50, type=int) + status = request.args.get('status') + has_severe = request.args.get('has_severe') + date = request.args.get('date') + patient_name = request.args.get('patient_name') + + # has_severe 파싱 + if has_severe == 'true': + has_severe = True + elif has_severe == 'false': + has_severe = False + else: + has_severe = None + + logs = get_recent_logs( + limit=limit, + status=status, + has_severe=has_severe, + date=date + ) + + # 환자명 필터링 (클라이언트 사이드에서 하기엔 데이터가 많을 수 있음) + if patient_name: + logs = [log for log in logs if patient_name.lower() in (log.get('patient_name') or '').lower()] + + return jsonify({'success': True, 'logs': logs, 'count': len(logs)}) + + except Exception as e: + logging.error(f"PAAI 로그 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@pmr_bp.route('/api/admin/log/') +def paai_admin_log_detail(log_id): + """PAAI 로그 상세 API""" + from db.paai_logger import get_log_detail + + try: + log = get_log_detail(log_id) + if not log: + return jsonify({'success': False, 'error': '로그를 찾을 수 없습니다'}), 404 + + return jsonify({'success': True, 'log': log}) + + except Exception as e: + logging.error(f"PAAI 로그 상세 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@pmr_bp.route('/api/admin/feedback-stats') +def paai_admin_feedback_stats(): + """피드백 통계 API (일별)""" + from db.paai_logger import DB_PATH + import sqlite3 + from datetime import datetime, timedelta + + try: + if not DB_PATH.exists(): + return jsonify({'success': True, 'stats': []}) + + conn = sqlite3.connect(str(DB_PATH)) + cursor = conn.cursor() + + # 최근 30일 일별 통계 + cursor.execute(""" + SELECT + DATE(created_at) as date, + COUNT(*) as total, + SUM(CASE WHEN feedback_useful = 1 THEN 1 ELSE 0 END) as useful, + SUM(CASE WHEN feedback_useful = 0 THEN 1 ELSE 0 END) as not_useful, + SUM(CASE WHEN feedback_useful IS NULL THEN 1 ELSE 0 END) as no_feedback, + SUM(CASE WHEN kims_has_severe = 1 THEN 1 ELSE 0 END) as severe, + AVG(ai_response_time_ms) as avg_ai_time + FROM paai_logs + WHERE created_at >= date('now', '-30 days') + GROUP BY DATE(created_at) + ORDER BY date DESC + """) + + rows = cursor.fetchall() + stats = [] + for row in rows: + stats.append({ + 'date': row[0], + 'total': row[1], + 'useful': row[2] or 0, + 'not_useful': row[3] or 0, + 'no_feedback': row[4] or 0, + 'severe': row[5] or 0, + 'avg_ai_time': int(row[6]) if row[6] else 0 + }) + + conn.close() + return jsonify({'success': True, 'stats': stats}) + + except Exception as e: + logging.error(f"피드백 통계 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500