pharmacy-pos-qr-system/docs/pubmed-graphrag-workflow copy.md
시골약사 032795c0fa docs: GraphRAG 및 그래프 DB 전환 기획 문서 추가
약국 POS 시스템의 GraphRAG 기반 추천 시스템 구축 관련 문서:

## 핵심 설계 문서

1. 질병코드기반 제품추천.md
   - ICD-10 질병 코드 활용 추천 시스템 설계
   - 계층 구조 (질병 → 질병군 → 제품군 → 개별 제품)
   - 처방전 기반 추천 알고리즘

2. complex-product-graph-modeling.md
   - 복합제(비맥스제트 등) 그래프 모델링
   - 성분 간 시너지 효과 표현
   - 복합 증상 매칭 쿼리 예시

3. pubmed-graphrag-workflow.md
   - PubMed → GraphRAG 전체 워크플로우 (5단계)
   - 논문 검색, 근거 추출, 지식 그래프 구축
   - MCP Server 개발 가이드

## 그래프 DB 비교 및 평가

4. sqlite-graph-evaluation.md
   - SQLite vs SQLite-Graph vs Neo4j 비교
   - 현 시점(2026-01) 평가: 기존 SQL 유지 권장
   - 6개월 후 재평가 계획

5. opensource-graph-db-comparison.md
   - 오픈소스 그래프 DB 비교 (Neo4j, ArangoDB 등)

6. 온톨로지로전환.md
   - 관계형 DB → 온톨로지 구조 전환 가이드
   - PubMed RAG 활용 방안
   - 추론 규칙 설계

## PubMed GraphRAG 활용

7. pycnogenol-multi-indication-graphrag.md
   - 피크노제놀 다중 적응증 GraphRAG 구축 사례
   - 7가지 적응증별 근거 수준

8. grpahrag_아쉬아간다.md
   - Ashwagandha GraphRAG 구축 사례

9. pubdmed이용ai.md
   - PubMed + AI 통합 활용 가이드

## 추가 워크플로우

10. pubmed-graphrag-workflow_next.md
    - 다음 단계 워크플로우

11. PostgresGRAPH전환.md
    - PostgreSQL + Apache AGE 전환 가이드

모든 문서는 한국어로 작성되었으며, 코드 예시는 영어로 포함.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 21:04:56 +09:00

73 KiB

PubMed 기반 GraphRAG 지식 그래프 구축 워크플로우

약국 업셀링 및 AI 추천 시스템을 위한 근거 기반 의약품 지식 그래프 구축 방법론

작성일: 2026-01-24 목적: MCP Server 또는 AI Agent 개발을 위한 표준 워크플로우 문서화


📋 목차

  1. 개요
  2. 전체 워크플로우
  3. 단계별 상세 프로세스
  4. Python 스크립트 템플릿
  5. GraphRAG 지식 그래프 구조
  6. 데이터베이스 스키마
  7. MCP Server 개발 가이드
  8. AI Agent 개발 가이드
  9. 실제 사례 연구
  10. 참고 자료

개요

🎯 목표

과학적 근거(PubMed 논문)에 기반한 의약품 추천 시스템 구축:

  • 근거 기반 추천: PMID 인용으로 신뢰도 향상
  • 관계 기반 추론: 약물-증상-부작용 관계 그래프
  • 자동화 가능: MCP/Agent로 확장 가능한 구조

🔧 사용 기술 스택

┌─────────────────────────────────────────────────────────┐
│  PubMed (NCBI)                                          │
│  └─ Biopython (Entrez API)                             │
└─────────────────────────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────┐
│  Python Scripts                                         │
│  ├─ pubmed_search.py     (논문 검색)                   │
│  ├─ extract_evidence.py  (근거 추출)                   │
│  └─ build_knowledge_graph.py  (지식 그래프 구축)      │
└─────────────────────────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────┐
│  Knowledge Graph (SQLite)                               │
│  ├─ Entities (약물, 증상, 부작용)                      │
│  ├─ Relationships (약물-증상, 약물-부작용)             │
│  └─ Evidence (PMID, 신뢰도, 인용)                      │
└─────────────────────────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────┐
│  AI Recommendation System                               │
│  ├─ Flask API (추천 엔드포인트)                        │
│  ├─ OpenAI GPT-4 (추론)                                │
│  └─ MCP Server (향후 확장)                             │
└─────────────────────────────────────────────────────────┘

📊 구축된 사례

  1. CoQ10 + Statin 근육병증 (PMID: 30371340)
  2. Ashwagandha 수면 개선 (PMID: 34559859)
  3. Naproxen 심혈관 안전성 (PMID: 27959716)

전체 워크플로우

🔄 5단계 프로세스

┌──────────────────────────────────────────────────────────────┐
│ STEP 1: 주제 선정 및 검색어 설계                            │
├──────────────────────────────────────────────────────────────┤
│ Input:  비즈니스 요구사항 (예: "Statin 부작용 관리")        │
│ Output: PubMed 검색어 (예: "statin AND coq10 AND muscle")   │
│ Tool:   수동 검색 + AI 보조                                  │
└──────────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────────┐
│ STEP 2: PubMed 논문 검색                                     │
├──────────────────────────────────────────────────────────────┤
│ Input:  검색어, 필터 조건 (연도, 연구 유형)                 │
│ Output: PMID 리스트, 논문 메타데이터                         │
│ Tool:   Biopython Entrez.esearch()                           │
└──────────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────────┐
│ STEP 3: 논문 내용 분석 및 근거 추출                         │
├──────────────────────────────────────────────────────────────┤
│ Input:  PMID, 초록/전문                                      │
│ Output: 핵심 발견, 효과 크기, 신뢰도                         │
│ Tool:   Biopython Entrez.efetch() + NLP/Manual               │
└──────────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────────┐
│ STEP 4: 지식 그래프 구축                                     │
├──────────────────────────────────────────────────────────────┤
│ Input:  Entity(약물, 증상), Relationship, Evidence           │
│ Output: Knowledge Triples (Subject-Predicate-Object)         │
│ Tool:   SQLite DB + Python                                   │
└──────────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────────┐
│ STEP 5: AI 추천 시스템 통합                                  │
├──────────────────────────────────────────────────────────────┤
│ Input:  환자 프로필 (증상, 복용약, 위험인자)                │
│ Output: 추천 약물 + 근거(PMID) + 신뢰도                      │
│ Tool:   Flask API + OpenAI GPT-4 + GraphRAG                  │
└──────────────────────────────────────────────────────────────┘

단계별 상세 프로세스

STEP 1: 주제 선정 및 검색어 설계

1.1 비즈니스 요구사항 분석

목표: 약국에서 실제 업셀링/추천이 필요한 시나리오 식별

예시:

고객 시나리오:
"고지혈증 약(Statin) 먹고 근육통이 있어요"
    ↓
비즈니스 질문:
"Statin 근육통에 CoQ10이 효과적인가?"
    ↓
검색 전략:
- 주요 개념: Statin, CoQ10, Myopathy
- 관계: 치료 효과 (Therapeutic effect)
- 연구 유형: RCT, Meta-analysis

1.2 검색어 설계 (Boolean Logic)

# 기본 패턴
search_query = "{Drug} AND {Condition} AND {Outcome}"

# 예시 1: CoQ10 + Statin
query_1 = "statin AND coq10 AND muscle"
query_1_advanced = "(statin OR atorvastatin) AND (coenzyme q10 OR ubiquinone) AND (myopathy OR muscle pain)"

# 예시 2: Ashwagandha 수면
query_2 = "ashwagandha AND sleep AND insomnia"
query_2_advanced = "(ashwagandha OR withania somnifera) AND (sleep quality OR insomnia) AND (randomized controlled trial)"

# 예시 3: Naproxen 심혈관 안전성
query_3 = "naproxen AND cardiovascular AND safety AND NSAIDs"

1.3 필터 조건

filters = {
    "publication_type": [
        "Meta-Analysis",
        "Randomized Controlled Trial",
        "Systematic Review"
    ],
    "publication_date": "2015-2024",  # 최근 10년
    "language": "English",
    "species": "Humans"
}

STEP 2: PubMed 논문 검색

2.1 환경 설정

# 패키지 설치
pip install biopython python-dotenv

# .env 파일 설정
PUBMED_EMAIL=pharmacy@example.com
# PUBMED_API_KEY=xxx  # Optional (10 req/sec), 없으면 3 req/sec

2.2 검색 스크립트 기본 구조

"""
PubMed 논문 검색 템플릿
"""
import os
from Bio import Entrez
from dotenv import load_dotenv

load_dotenv()

# NCBI 설정
Entrez.email = os.getenv('PUBMED_EMAIL')
api_key = os.getenv('PUBMED_API_KEY')
if api_key:
    Entrez.api_key = api_key


def search_pubmed(query, max_results=10, filters=None):
    """
    PubMed에서 논문 검색

    Args:
        query (str): 검색어
        max_results (int): 최대 결과 수
        filters (dict): 필터 조건

    Returns:
        list: PMID 리스트
    """
    try:
        # 검색
        handle = Entrez.esearch(
            db="pubmed",
            term=query,
            retmax=max_results,
            sort="relevance",  # or "pub_date"
            mindate=filters.get('mindate') if filters else None,
            maxdate=filters.get('maxdate') if filters else None
        )

        record = Entrez.read(handle)
        handle.close()

        pmids = record["IdList"]
        total_count = int(record["Count"])

        print(f"총 {total_count}건 검색됨, 상위 {len(pmids)}건 조회")

        return pmids

    except Exception as e:
        print(f"[ERROR] 검색 실패: {e}")
        return []


def fetch_paper_details(pmids):
    """
    PMID로 논문 상세 정보 가져오기

    Args:
        pmids (list): PMID 리스트

    Returns:
        list: 논문 정보 리스트
    """
    try:
        handle = Entrez.efetch(
            db="pubmed",
            id=pmids,
            rettype="medline",
            retmode="xml"
        )

        papers = Entrez.read(handle)
        handle.close()

        results = []

        for paper in papers['PubmedArticle']:
            article = paper['MedlineCitation']['Article']

            # 기본 정보 추출
            pmid = str(paper['MedlineCitation']['PMID'])
            title = article.get('ArticleTitle', '')

            # 초록
            abstract_parts = article.get('Abstract', {}).get('AbstractText', [])
            if abstract_parts:
                if isinstance(abstract_parts, list):
                    abstract = ' '.join([str(part) for part in abstract_parts])
                else:
                    abstract = str(abstract_parts)
            else:
                abstract = ""

            # 저널 정보
            journal = article.get('Journal', {}).get('Title', '')
            pub_date = article.get('Journal', {}).get('JournalIssue', {}).get('PubDate', {})
            year = pub_date.get('Year', '')

            # 저자
            authors = article.get('AuthorList', [])
            author_list = []
            for author in authors[:3]:  # 처음 3명만
                last = author.get('LastName', '')
                init = author.get('Initials', '')
                if last:
                    author_list.append(f"{last} {init}")

            authors_str = ', '.join(author_list)
            if len(authors) > 3:
                authors_str += ' et al.'

            results.append({
                'pmid': pmid,
                'title': title,
                'abstract': abstract,
                'journal': journal,
                'year': year,
                'authors': authors_str,
                'url': f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
            })

        return results

    except Exception as e:
        print(f"[ERROR] 논문 정보 가져오기 실패: {e}")
        return []


# 사용 예시
if __name__ == '__main__':
    query = "statin AND coq10 AND muscle"

    # 1. 검색
    pmids = search_pubmed(query, max_results=5)

    # 2. 상세 정보
    papers = fetch_paper_details(pmids)

    # 3. 결과 출력
    for paper in papers:
        print(f"\nPMID: {paper['pmid']}")
        print(f"제목: {paper['title']}")
        print(f"저자: {paper['authors']}")
        print(f"저널: {paper['journal']} ({paper['year']})")
        print(f"링크: {paper['url']}")
        print(f"초록: {paper['abstract'][:200]}...")

STEP 3: 논문 내용 분석 및 근거 추출

3.1 핵심 정보 추출

목표: 논문에서 추천 시스템에 필요한 정보만 추출

추출 항목:

evidence_template = {
    'pmid': str,           # 논문 ID
    'study_type': str,     # RCT, Meta-analysis, Cohort, etc.
    'participants': int,   # 연구 대상자 수
    'intervention': str,   # 약물/치료
    'comparator': str,     # 비교 대상 (placebo, other drug)
    'outcome': str,        # 결과 지표
    'effect_size': float,  # 효과 크기 (SMD, OR, HR 등)
    'p_value': float,      # 통계적 유의성
    'confidence_interval': tuple,  # 95% CI
    'adverse_events': list,  # 부작용
    'conclusion': str,     # 결론
    'reliability': float   # 신뢰도 (0.0-1.0)
}

3.2 신뢰도 계산 알고리즘

def calculate_reliability(paper):
    """
    논문 신뢰도 계산

    근거:
    - 연구 유형 (Meta-analysis > RCT > Cohort > Case report)
    - 참가자 수 (많을수록 높음)
    - 저널 임팩트 팩터 (높을수록 높음)
    - 통계적 유의성 (P < 0.05)
    """
    score = 0.0

    # 1. 연구 유형 (40점)
    study_type_scores = {
        'Meta-Analysis': 0.40,
        'Systematic Review': 0.38,
        'Randomized Controlled Trial': 0.35,
        'Cohort Study': 0.25,
        'Case-Control Study': 0.20,
        'Case Report': 0.10
    }
    score += study_type_scores.get(paper['study_type'], 0.15)

    # 2. 참가자 수 (20점)
    n = paper.get('participants', 0)
    if n >= 1000:
        score += 0.20
    elif n >= 500:
        score += 0.15
    elif n >= 100:
        score += 0.10
    elif n >= 50:
        score += 0.05

    # 3. 저널 임팩트 (20점)
    high_impact_journals = ['NEJM', 'Lancet', 'JAMA', 'BMJ']
    if any(j in paper.get('journal', '') for j in high_impact_journals):
        score += 0.20
    elif 'PLoS' in paper.get('journal', ''):
        score += 0.12
    else:
        score += 0.08

    # 4. 통계적 유의성 (10점)
    p_value = paper.get('p_value', 1.0)
    if p_value < 0.001:
        score += 0.10
    elif p_value < 0.01:
        score += 0.08
    elif p_value < 0.05:
        score += 0.05

    # 5. 최근성 (10점)
    year = int(paper.get('year', 2000))
    if year >= 2020:
        score += 0.10
    elif year >= 2015:
        score += 0.07
    elif year >= 2010:
        score += 0.04

    return min(score, 1.0)  # 최대 1.0

3.3 효과 크기 파싱 (NLP 또는 수동)

import re

def extract_effect_size(abstract):
    """
    초록에서 효과 크기 추출

    패턴:
    - SMD: Standardized Mean Difference
    - OR: Odds Ratio
    - HR: Hazard Ratio
    - RR: Relative Risk
    """
    patterns = {
        'SMD': r'SMD[:\s]*([-\d.]+)',
        'OR': r'OR[:\s]*([\d.]+)',
        'HR': r'HR[:\s]*([\d.]+)',
        'RR': r'RR[:\s]*([\d.]+)',
        'P-value': r'[Pp][=<\s]*([\d.]+)'
    }

    results = {}
    for key, pattern in patterns.items():
        match = re.search(pattern, abstract)
        if match:
            results[key] = float(match.group(1))

    return results


# 예시
abstract = """
Ashwagandha showed significant effect on sleep (SMD -0.59;
95% CI -0.75 to -0.42; P<0.001).
"""

effects = extract_effect_size(abstract)
# {'SMD': -0.59, 'P-value': 0.001}

STEP 4: 지식 그래프 구축

4.1 Knowledge Triple 구조

"""
Subject - Predicate - Object 형태의 트리플
"""

# 기본 구조
triple = {
    'subject': str,      # 주어 (약물, 증상, 환자 프로필)
    'predicate': str,    # 관계 (TREATS, CAUSES, CONTRAINDICATES)
    'object': str,       # 목적어 (증상, 부작용, 결과)
    'evidence_pmid': str,  # 근거 논문
    'reliability': float,  # 신뢰도
    'metadata': dict     # 추가 정보
}


# 예시: CoQ10 + Statin
triples_coq10 = [
    {
        'subject': 'Statin',
        'predicate': 'INHIBITS',
        'object': 'CoQ10_synthesis',
        'evidence_pmid': '25655639',
        'reliability': 0.90,
        'metadata': {'mechanism': 'HMG-CoA reductase inhibition'}
    },
    {
        'subject': 'CoQ10_deficiency',
        'predicate': 'CAUSES',
        'object': 'Muscle_weakness',
        'evidence_pmid': '30371340',
        'reliability': 0.95,
        'metadata': {'effect_size': 'SMD -2.28'}
    },
    {
        'subject': 'CoQ10_supplement',
        'predicate': 'REDUCES',
        'object': 'Statin_myopathy',
        'evidence_pmid': '30371340',
        'reliability': 0.95,
        'metadata': {
            'study_type': 'Meta-analysis',
            'participants': 575,
            'p_value': 0.001
        }
    },
    {
        'subject': 'PMID:30371340',
        'predicate': 'SUPPORTS',
        'object': 'CoQ10->Statin_myopathy',
        'reliability': 0.95,
        'metadata': {
            'title': 'Effects of CoQ10 on Statin-Induced Myopathy',
            'journal': 'JAHA',
            'year': 2018
        }
    }
]


# 예시: Naproxen vs 다른 NSAID
triples_naproxen = [
    {
        'subject': 'Naproxen',
        'predicate': 'HAS_LOWEST',
        'object': 'CV_Risk_Among_NSAIDs',
        'evidence_pmid': '27959716',
        'reliability': 0.99,
        'metadata': {
            'study_type': 'RCT',
            'participants': 24081,
            'journal': 'NEJM'
        }
    },
    {
        'subject': 'Naproxen',
        'predicate': 'SAFER_THAN',
        'object': 'Diclofenac',
        'evidence_pmid': '27959716',
        'reliability': 0.99,
        'metadata': {'cv_event_rate': '2.5% vs 2.7%'}
    },
    {
        'subject': 'Patient_with_HTN',
        'predicate': 'RECOMMEND',
        'object': 'Naproxen_over_Diclofenac',
        'evidence_pmid': '27959716',
        'reliability': 0.95,
        'metadata': {'reasoning': 'Lower CV risk'}
    }
]

4.2 Entity 분류

entity_types = {
    'Drug': [
        'Statin', 'Atorvastatin', 'Simvastatin',
        'CoQ10', 'Ubiquinone',
        'Naproxen', 'Ibuprofen', 'Diclofenac',
        'Ashwagandha'
    ],
    'Condition': [
        'Myopathy', 'Muscle_weakness', 'Muscle_pain',
        'Insomnia', 'Poor_sleep_quality',
        'Hypertension', 'Diabetes'
    ],
    'Symptom': [
        'Pain', 'Weakness', 'Fatigue', 'Cramp'
    ],
    'Adverse_Event': [
        'GI_bleeding', 'Myocardial_infarction', 'Stroke'
    ],
    'Patient_Profile': [
        'Elderly', 'Patient_with_HTN', 'Patient_with_DM'
    ],
    'Evidence': [
        'PMID:30371340', 'PMID:27959716', 'PMID:34559859'
    ]
}

4.3 Relationship 유형

relationship_types = {
    # 약물 작용
    'TREATS': '약물이 증상/질환을 치료함',
    'REDUCES': '약물이 증상을 감소시킴',
    'INHIBITS': '약물이 생합성/경로를 억제함',
    'ACTIVATES': '약물이 수용체/경로를 활성화함',

    # 부작용
    'CAUSES': '약물이 부작용을 유발함',
    'INCREASES_RISK': '약물이 위험을 증가시킨',

    # 비교
    'SAFER_THAN': '약물 A가 약물 B보다 안전함',
    'MORE_EFFECTIVE_THAN': '약물 A가 약물 B보다 효과적',
    'EQUIVALENT_TO': '약물 A와 약물 B가 동등함',

    # 금기/주의
    'CONTRAINDICATED_IN': '특정 환자군에서 금기',
    'CAUTION_IN': '특정 환자군에서 주의',

    # 추천
    'RECOMMEND': '환자 프로필에 따른 추천',
    'PREFER': '우선 선택',

    # 근거
    'SUPPORTS': '논문이 관계를 지지함',
    'REFUTES': '논문이 관계를 반박함'
}

STEP 5: AI 추천 시스템 통합

5.1 GraphRAG 쿼리 패턴

def query_knowledge_graph(patient_profile, symptom):
    """
    환자 프로필과 증상에 따른 추천 약물 검색

    Args:
        patient_profile: {'age': 65, 'conditions': ['HTN', 'DM']}
        symptom: 'Knee_pain'

    Returns:
        추천 약물 + 근거 + 추론 경로
    """

    # 1. 증상에 효과적인 약물 검색
    effective_drugs = graph.query("""
        SELECT d.name, r.reliability, e.pmid
        FROM drugs d
        JOIN relationships r ON d.id = r.subject_id
        JOIN evidence e ON r.evidence_id = e.id
        WHERE r.predicate = 'TREATS'
          AND r.object_id = (SELECT id FROM entities WHERE name = ?)
        ORDER BY r.reliability DESC
    """, (symptom,))

    # 2. 환자 프로필에 안전한 약물 필터링
    for condition in patient_profile['conditions']:
        # 금기 약물 제외
        contraindicated = graph.query("""
            SELECT d.name
            FROM drugs d
            JOIN relationships r ON d.id = r.subject_id
            WHERE r.predicate = 'CONTRAINDICATED_IN'
              AND r.object_id = (SELECT id FROM entities WHERE name = ?)
        """, (f"Patient_with_{condition}",))

        effective_drugs = [
            drug for drug in effective_drugs
            if drug not in contraindicated
        ]

    # 3. 추천 순위화 (신뢰도 + 안전성)
    recommendations = []
    for drug in effective_drugs:
        # 안전성 스코어
        safety_score = graph.query("""
            SELECT AVG(r.reliability)
            FROM relationships r
            WHERE r.subject_id = (SELECT id FROM entities WHERE name = ?)
              AND r.predicate IN ('SAFER_THAN', 'LOW_RISK')
        """, (drug['name'],))

        # 종합 스코어
        total_score = drug['reliability'] * 0.7 + safety_score * 0.3

        recommendations.append({
            'drug': drug['name'],
            'score': total_score,
            'evidence': drug['pmid'],
            'reliability': drug['reliability']
        })

    return sorted(recommendations, key=lambda x: x['score'], reverse=True)

5.2 추론 경로 생성

def generate_reasoning_path(patient, recommended_drug):
    """
    추천 이유를 설명하는 추론 경로 생성

    Returns:
        추론 단계 리스트
    """
    path = []

    # 1. 환자 상태 식별
    path.append(f"환자: {patient['age']}세, {', '.join(patient['conditions'])}")

    # 2. 위험 인자 평가
    for condition in patient['conditions']:
        risk = graph.query("""
            SELECT r.object, e.pmid
            FROM relationships r
            JOIN evidence e ON r.evidence_id = e.id
            WHERE r.subject_id = (SELECT id FROM entities WHERE name = ?)
              AND r.predicate = 'INCREASES_RISK'
        """, (condition,))

        if risk:
            path.append(f"{condition}{risk['object']} 위험 증가")

    # 3. 부적합 약물 제외
    contraindicated = graph.query("""
        SELECT d.name, r.reliability, e.pmid
        FROM drugs d
        JOIN relationships r ON d.id = r.subject_id
        JOIN evidence e ON r.evidence_id = e.id
        WHERE r.predicate = 'CONTRAINDICATED_IN'
          AND r.object_id IN (
              SELECT id FROM entities
              WHERE name IN (?, ?)
          )
    """, tuple(f"Patient_with_{c}" for c in patient['conditions']))

    for drug in contraindicated:
        path.append(f"{drug['name']}: 부적합 (근거: PMID:{drug['pmid']})")

    # 4. 추천 약물 선택 이유
    recommendation_reason = graph.query("""
        SELECT r.predicate, r.object, e.pmid, r.reliability
        FROM relationships r
        JOIN evidence e ON r.evidence_id = e.id
        WHERE r.subject_id = (SELECT id FROM entities WHERE name = ?)
          AND r.predicate IN ('SAFER_THAN', 'MORE_EFFECTIVE_THAN')
    """, (recommended_drug,))

    path.append(
        f"{recommended_drug}: {recommendation_reason['predicate']} "
        f"(근거: PMID:{recommendation_reason['pmid']}, "
        f"신뢰도: {recommendation_reason['reliability']:.0%})"
    )

    return path


# 사용 예시
patient = {
    'age': 65,
    'conditions': ['HTN', 'DM'],
    'symptom': 'Knee_pain'
}

recommended_drug = 'Naproxen'
reasoning_path = generate_reasoning_path(patient, recommended_drug)

"""
출력:
[
    "환자: 65세, HTN, DM",
    "HTN → 심혈관 질환 위험 증가",
    "Diclofenac: 부적합 (근거: PMID:27959716)",
    "Naproxen: SAFER_THAN Diclofenac (근거: PMID:27959716, 신뢰도: 99%)"
]
"""

Python 스크립트 템플릿

📝 표준 템플릿 구조

"""
[주제] 연구 분석 스크립트

목적: PubMed에서 [주제] 관련 논문 검색 및 GraphRAG 지식 그래프 구축
작성일: YYYY-MM-DD
"""

import sys
import os

# UTF-8 인코딩 강제 (Windows 한글 깨짐 방지)
if sys.platform == 'win32':
    import io
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

from Bio import Entrez
from dotenv import load_dotenv
import sqlite3
import json

load_dotenv()

# NCBI Entrez 설정
Entrez.email = os.getenv('PUBMED_EMAIL', 'test@example.com')
api_key = os.getenv('PUBMED_API_KEY')
if api_key:
    Entrez.api_key = api_key


# ============================================================
# STEP 1: PubMed 검색
# ============================================================

def search_pubmed(query, max_results=5):
    """PubMed 논문 검색"""
    try:
        print("=" * 80)
        print(f"검색: {query}")
        print("=" * 80)

        handle = Entrez.esearch(
            db="pubmed",
            term=query,
            retmax=max_results,
            sort="relevance"
        )
        record = Entrez.read(handle)
        handle.close()

        pmids = record["IdList"]
        total_count = int(record["Count"])

        print(f"[OK] 총 {total_count}건 검색됨, 상위 {len(pmids)}건 조회\n")

        return pmids

    except Exception as e:
        print(f"[ERROR] 검색 실패: {e}")
        return []


def fetch_paper_details(pmids):
    """PMID로 논문 상세 정보 가져오기"""
    try:
        handle = Entrez.efetch(
            db="pubmed",
            id=pmids,
            rettype="medline",
            retmode="xml"
        )
        papers = Entrez.read(handle)
        handle.close()

        results = []

        for idx, paper in enumerate(papers['PubmedArticle'], 1):
            article = paper['MedlineCitation']['Article']
            pmid = str(paper['MedlineCitation']['PMID'])
            title = article.get('ArticleTitle', '')

            # 초록 추출
            abstract_parts = article.get('Abstract', {}).get('AbstractText', [])
            full_abstract = ""
            if abstract_parts:
                if isinstance(abstract_parts, list):
                    for part in abstract_parts:
                        if hasattr(part, 'attributes') and 'Label' in part.attributes:
                            label = part.attributes['Label']
                            full_abstract += f"\n\n**{label}**\n{str(part)}"
                        else:
                            full_abstract += f"\n{str(part)}"
                else:
                    full_abstract = str(abstract_parts)

            # 메타데이터
            journal = article.get('Journal', {}).get('Title', '')
            pub_date = article.get('Journal', {}).get('JournalIssue', {}).get('PubDate', {})
            year = pub_date.get('Year', '')

            result = {
                'pmid': pmid,
                'title': title,
                'abstract': full_abstract.strip(),
                'journal': journal,
                'year': year
            }

            results.append(result)

            # 출력
            print(f"[{idx}] PMID: {pmid}")
            print(f"제목: {title}")
            print(f"저널: {journal} ({year})")
            print(f"링크: https://pubmed.ncbi.nlm.nih.gov/{pmid}/")
            print("-" * 80)
            print(f"초록:\n{full_abstract}")
            print("=" * 80)
            print()

        return results

    except Exception as e:
        print(f"[ERROR] 논문 정보 가져오기 실패: {e}")
        return []


# ============================================================
# STEP 2: 지식 그래프 구축
# ============================================================

def build_knowledge_graph(papers):
    """논문 데이터로 지식 그래프 구축"""

    knowledge_triples = []

    for paper in papers:
        # 여기서 논문 내용 분석하여 트리플 생성
        # (실제로는 NLP 또는 수동 분석 필요)

        # 예시: 효과 관계 추출
        if 'effective' in paper['abstract'].lower():
            knowledge_triples.append({
                'subject': '[Drug]',
                'predicate': 'EFFECTIVE_FOR',
                'object': '[Condition]',
                'evidence_pmid': paper['pmid'],
                'reliability': calculate_reliability(paper)
            })

    return knowledge_triples


def calculate_reliability(paper):
    """논문 신뢰도 계산"""
    score = 0.0

    # 연구 유형 (초록에서 키워드 추출)
    abstract_lower = paper['abstract'].lower()
    if 'meta-analysis' in abstract_lower:
        score += 0.40
    elif 'randomized' in abstract_lower:
        score += 0.35
    else:
        score += 0.20

    # 저널 임팩트
    high_impact = ['NEJM', 'Lancet', 'JAMA', 'BMJ', 'JAHA']
    if any(j in paper.get('journal', '') for j in high_impact):
        score += 0.30
    else:
        score += 0.15

    # 최근성
    year = int(paper.get('year', 2000))
    if year >= 2020:
        score += 0.20
    elif year >= 2015:
        score += 0.15
    else:
        score += 0.10

    # P-value (초록에서 추출)
    if 'p<0.001' in abstract_lower or 'p < 0.001' in abstract_lower:
        score += 0.10
    elif 'p<0.05' in abstract_lower or 'p < 0.05' in abstract_lower:
        score += 0.05

    return min(score, 1.0)


def save_to_database(knowledge_triples):
    """지식 그래프를 SQLite DB에 저장"""

    db_path = os.path.join(os.path.dirname(__file__), 'db', 'knowledge_graph.db')
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    try:
        # 테이블 생성
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS knowledge_triples (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                subject TEXT NOT NULL,
                predicate TEXT NOT NULL,
                object TEXT NOT NULL,
                evidence_pmid TEXT,
                reliability REAL,
                metadata TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)

        # 데이터 삽입
        for triple in knowledge_triples:
            cursor.execute("""
                INSERT INTO knowledge_triples
                (subject, predicate, object, evidence_pmid, reliability, metadata)
                VALUES (?, ?, ?, ?, ?, ?)
            """, (
                triple['subject'],
                triple['predicate'],
                triple['object'],
                triple.get('evidence_pmid'),
                triple.get('reliability'),
                json.dumps(triple.get('metadata', {}))
            ))

        conn.commit()
        print(f"[OK] {len(knowledge_triples)}개 트리플 저장 완료")

    except Exception as e:
        print(f"[ERROR] DB 저장 실패: {e}")
        conn.rollback()
    finally:
        conn.close()


# ============================================================
# STEP 3: 분석 및 시각화
# ============================================================

def analyze_findings(papers):
    """연구 결과 분석 및 요약"""

    print("\n" + "=" * 80)
    print("연구 결과 분석")
    print("=" * 80)

    # 효과 크기 추출 (간단한 예시)
    for paper in papers:
        print(f"\nPMID: {paper['pmid']}")
        print(f"제목: {paper['title']}")
        print(f"신뢰도: {calculate_reliability(paper):.0%}")

        # 핵심 발견 추출 (키워드 기반)
        if 'significant' in paper['abstract'].lower():
            print("✅ 통계적으로 유의미한 결과")

        if 'safe' in paper['abstract'].lower():
            print("✅ 안전성 확인")


def print_graphrag_structure():
    """GraphRAG 활용 예시 출력"""

    print("\n" + "=" * 80)
    print("GraphRAG 지식 그래프 구조 예시")
    print("=" * 80)

    example = '''
knowledge_triples = [
    # Entity-Relationship-Entity
    ("[Drug]", "TREATS", "[Condition]"),
    ("[Drug]", "CAUSES", "[Side_Effect]"),
    ("[Drug_A]", "SAFER_THAN", "[Drug_B]"),

    # Evidence
    ("PMID:xxxxxxx", "SUPPORTS", "[Drug]->TREATS->[Condition]"),
    ("PMID:xxxxxxx", "RELIABILITY", "0.95"),

    # Patient Profile
    ("[Patient_with_HTN]", "RECOMMEND", "[Drug]"),
    ("[Patient_with_HTN]", "AVOID", "[Drug_B]")
]

# AI 추천 예시
recommendation = {
    "patient": {"age": 65, "conditions": ["HTN", "DM"]},
    "symptom": "[Symptom]",
    "recommended_drug": "[Drug]",
    "reasoning_path": [
        "환자: 고혈압 + 당뇨 → 심혈관 위험군",
        "[Drug_B]: 부적합 (PMID:xxxxxxx)",
        "[Drug]: 가장 안전 (PMID:xxxxxxx, 신뢰도: 95%)"
    ],
    "evidence": {
        "pmid": "xxxxxxx",
        "finding": "[Key Finding]",
        "reliability": 0.95
    }
}
    '''

    print(example)


# ============================================================
# MAIN
# ============================================================

def main():
    """메인 실행"""

    print("\n" + "=" * 80)
    print("[주제] 연구 분석")
    print("=" * 80)

    # 1. PubMed 검색
    query = "[검색어]"
    pmids = search_pubmed(query, max_results=5)

    if not pmids:
        print("[WARNING] 검색 결과 없음")
        return

    # 2. 논문 상세 정보
    papers = fetch_paper_details(pmids)

    # 3. 지식 그래프 구축
    knowledge_triples = build_knowledge_graph(papers)
    save_to_database(knowledge_triples)

    # 4. 결과 분석
    analyze_findings(papers)

    # 5. GraphRAG 구조 출력
    print_graphrag_structure()

    print("\n" + "=" * 80)
    print(f"총 {len(papers)}개 논문 분석 완료")
    print("=" * 80)


if __name__ == '__main__':
    main()

GraphRAG 지식 그래프 구조

🗄️ Entity 정의

-- entities 테이블
CREATE TABLE entities (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    type TEXT NOT NULL,  -- Drug, Condition, Symptom, Patient_Profile, etc.
    description TEXT,
    synonyms TEXT,  -- JSON array
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 예시 데이터
INSERT INTO entities (name, type, description, synonyms) VALUES
('Naproxen', 'Drug', '비스테로이드성 소염진통제', '["나프록센", "Aleve"]'),
('Statin', 'Drug', 'HMG-CoA 환원효소 억제제', '["스타틴"]'),
('CoQ10', 'Drug', '코엔자임 Q10', '["Ubiquinone", "유비퀴논"]'),
('Myopathy', 'Condition', '근육병증', '["근육통", "Muscle pain"]'),
('Hypertension', 'Condition', '고혈압', '["HTN", "High blood pressure"]');

🔗 Relationship 정의

-- relationships 테이블
CREATE TABLE relationships (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    subject_id INTEGER NOT NULL,
    predicate TEXT NOT NULL,
    object_id INTEGER NOT NULL,
    evidence_id INTEGER,
    reliability REAL DEFAULT 0.5,
    metadata TEXT,  -- JSON
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (subject_id) REFERENCES entities(id),
    FOREIGN KEY (object_id) REFERENCES entities(id),
    FOREIGN KEY (evidence_id) REFERENCES evidence(id)
);

-- 인덱스
CREATE INDEX idx_subject ON relationships(subject_id);
CREATE INDEX idx_predicate ON relationships(predicate);
CREATE INDEX idx_object ON relationships(object_id);

📚 Evidence 정의

-- evidence 테이블
CREATE TABLE evidence (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pmid TEXT UNIQUE NOT NULL,
    title TEXT,
    authors TEXT,
    journal TEXT,
    year INTEGER,
    study_type TEXT,  -- Meta-analysis, RCT, Cohort, etc.
    participants INTEGER,
    abstract TEXT,
    findings TEXT,  -- JSON
    reliability REAL,
    url TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 예시 데이터
INSERT INTO evidence (pmid, title, journal, year, study_type, participants, reliability) VALUES
('30371340', 'Effects of CoQ10 on Statin-Induced Myopathy', 'JAHA', 2018, 'Meta-analysis', 575, 0.95),
('27959716', 'CV Safety of Celecoxib, Naproxen, Ibuprofen', 'NEJM', 2016, 'RCT', 24081, 0.99),
('34559859', 'Effect of Ashwagandha on Sleep', 'PLoS One', 2021, 'Meta-analysis', 400, 0.90);

🔍 GraphRAG 쿼리 예시

-- 1. 특정 증상에 효과적인 약물 검색
SELECT
    e1.name AS drug,
    e2.name AS condition,
    r.reliability,
    ev.pmid,
    ev.title
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
JOIN evidence ev ON r.evidence_id = ev.id
WHERE r.predicate = 'TREATS'
  AND e2.name = 'Myopathy'
ORDER BY r.reliability DESC;


-- 2. 약물 간 안전성 비교
SELECT
    e1.name AS safer_drug,
    e2.name AS compared_to,
    r.reliability,
    ev.pmid,
    ev.journal,
    ev.year
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
JOIN evidence ev ON r.evidence_id = ev.id
WHERE r.predicate = 'SAFER_THAN'
  AND e1.type = 'Drug'
ORDER BY r.reliability DESC;


-- 3. 환자 프로필에 따른 추천
SELECT
    e1.name AS patient_profile,
    e2.name AS recommended_drug,
    r.reliability,
    ev.pmid,
    r.metadata
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
JOIN evidence ev ON r.evidence_id = ev.id
WHERE r.predicate = 'RECOMMEND'
  AND e1.name = 'Patient_with_HTN';


-- 4. 특정 PMID가 지지하는 모든 관계
SELECT
    e1.name AS subject,
    r.predicate,
    e2.name AS object,
    r.reliability
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
JOIN evidence ev ON r.evidence_id = ev.id
WHERE ev.pmid = '27959716';


-- 5. 추론 경로 탐색 (2-hop)
WITH first_hop AS (
    SELECT
        e1.name AS start,
        r1.predicate AS pred1,
        e2.name AS middle,
        r1.object_id AS middle_id
    FROM relationships r1
    JOIN entities e1 ON r1.subject_id = e1.id
    JOIN entities e2 ON r1.object_id = e2.id
    WHERE e1.name = 'Statin'
)
SELECT
    fh.start,
    fh.pred1,
    fh.middle,
    r2.predicate AS pred2,
    e3.name AS end
FROM first_hop fh
JOIN relationships r2 ON fh.middle_id = r2.subject_id
JOIN entities e3 ON r2.object_id = e3.id;

-- 결과 예시:
-- Statin -> INHIBITS -> CoQ10_synthesis -> CAUSES -> Myopathy

데이터베이스 스키마

📐 전체 ERD

┌─────────────────────────────────────────────────────────────┐
│                      entities                               │
├─────────────────────────────────────────────────────────────┤
│ id (PK)            INTEGER                                  │
│ name               TEXT UNIQUE                              │
│ type               TEXT (Drug, Condition, etc.)             │
│ description        TEXT                                     │
│ synonyms           TEXT (JSON array)                        │
│ metadata           TEXT (JSON)                              │
│ created_at         TIMESTAMP                                │
└─────────────────────────────────────────────────────────────┘
                          ↑
                          │
                          │ (subject_id, object_id)
                          │
┌─────────────────────────────────────────────────────────────┐
│                   relationships                             │
├─────────────────────────────────────────────────────────────┤
│ id (PK)            INTEGER                                  │
│ subject_id (FK)    INTEGER → entities.id                    │
│ predicate          TEXT                                     │
│ object_id (FK)     INTEGER → entities.id                    │
│ evidence_id (FK)   INTEGER → evidence.id                    │
│ reliability        REAL (0.0-1.0)                           │
│ metadata           TEXT (JSON)                              │
│ created_at         TIMESTAMP                                │
└─────────────────────────────────────────────────────────────┘
                          ↓
                          │ (evidence_id)
                          ↓
┌─────────────────────────────────────────────────────────────┐
│                      evidence                               │
├─────────────────────────────────────────────────────────────┤
│ id (PK)            INTEGER                                  │
│ pmid               TEXT UNIQUE                              │
│ title              TEXT                                     │
│ authors            TEXT                                     │
│ journal            TEXT                                     │
│ year               INTEGER                                  │
│ study_type         TEXT                                     │
│ participants       INTEGER                                  │
│ abstract           TEXT                                     │
│ findings           TEXT (JSON)                              │
│ reliability        REAL                                     │
│ url                TEXT                                     │
│ created_at         TIMESTAMP                                │
└─────────────────────────────────────────────────────────────┘

💾 SQLite 스키마 생성 스크립트

-- knowledge_graph.sql

-- 1. Entities 테이블
CREATE TABLE IF NOT EXISTS entities (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    type TEXT NOT NULL CHECK(type IN (
        'Drug', 'Condition', 'Symptom', 'Adverse_Event',
        'Patient_Profile', 'Biomarker', 'Mechanism'
    )),
    description TEXT,
    synonyms TEXT,  -- JSON: ["synonym1", "synonym2"]
    metadata TEXT,  -- JSON: {"key": "value"}
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_entity_name ON entities(name);
CREATE INDEX idx_entity_type ON entities(type);


-- 2. Relationships 테이블
CREATE TABLE IF NOT EXISTS relationships (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    subject_id INTEGER NOT NULL,
    predicate TEXT NOT NULL CHECK(predicate IN (
        'TREATS', 'REDUCES', 'INHIBITS', 'ACTIVATES',
        'CAUSES', 'INCREASES_RISK', 'DECREASES_RISK',
        'SAFER_THAN', 'MORE_EFFECTIVE_THAN', 'EQUIVALENT_TO',
        'CONTRAINDICATED_IN', 'CAUTION_IN', 'RECOMMEND', 'PREFER',
        'SUPPORTS', 'REFUTES'
    )),
    object_id INTEGER NOT NULL,
    evidence_id INTEGER,
    reliability REAL DEFAULT 0.5 CHECK(reliability >= 0.0 AND reliability <= 1.0),
    strength REAL,  -- Effect size (optional)
    metadata TEXT,  -- JSON: {"p_value": 0.001, "ci": [0.5, 0.9]}
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (subject_id) REFERENCES entities(id) ON DELETE CASCADE,
    FOREIGN KEY (object_id) REFERENCES entities(id) ON DELETE CASCADE,
    FOREIGN KEY (evidence_id) REFERENCES evidence(id) ON DELETE SET NULL
);

CREATE INDEX idx_rel_subject ON relationships(subject_id);
CREATE INDEX idx_rel_predicate ON relationships(predicate);
CREATE INDEX idx_rel_object ON relationships(object_id);
CREATE INDEX idx_rel_evidence ON relationships(evidence_id);
CREATE INDEX idx_rel_reliability ON relationships(reliability DESC);


-- 3. Evidence 테이블
CREATE TABLE IF NOT EXISTS evidence (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pmid TEXT UNIQUE NOT NULL,
    title TEXT NOT NULL,
    authors TEXT,
    journal TEXT,
    year INTEGER,
    study_type TEXT CHECK(study_type IN (
        'Meta-Analysis', 'Systematic Review', 'RCT',
        'Cohort Study', 'Case-Control Study', 'Case Report', 'Review'
    )),
    participants INTEGER,
    abstract TEXT,
    findings TEXT,  -- JSON: {"outcome": "value", "effect_size": 0.5}
    reliability REAL CHECK(reliability >= 0.0 AND reliability <= 1.0),
    url TEXT,
    doi TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_evidence_pmid ON evidence(pmid);
CREATE INDEX idx_evidence_year ON evidence(year DESC);
CREATE INDEX idx_evidence_study_type ON evidence(study_type);
CREATE INDEX idx_evidence_reliability ON evidence(reliability DESC);


-- 4. Triggers (자동 업데이트)
CREATE TRIGGER update_entity_timestamp
AFTER UPDATE ON entities
BEGIN
    UPDATE entities SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

CREATE TRIGGER update_relationship_timestamp
AFTER UPDATE ON relationships
BEGIN
    UPDATE relationships SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

CREATE TRIGGER update_evidence_timestamp
AFTER UPDATE ON evidence
BEGIN
    UPDATE evidence SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;


-- 5. Views (자주 사용하는 쿼리)
CREATE VIEW IF NOT EXISTS v_drug_recommendations AS
SELECT
    e1.name AS patient_profile,
    e2.name AS recommended_drug,
    r.predicate,
    r.reliability,
    ev.pmid,
    ev.title,
    ev.year,
    r.metadata
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
LEFT JOIN evidence ev ON r.evidence_id = ev.id
WHERE r.predicate IN ('RECOMMEND', 'PREFER')
  AND e1.type = 'Patient_Profile'
  AND e2.type = 'Drug'
ORDER BY r.reliability DESC;


CREATE VIEW IF NOT EXISTS v_drug_safety_comparison AS
SELECT
    e1.name AS safer_drug,
    e2.name AS compared_to,
    r.predicate,
    r.reliability,
    ev.pmid,
    ev.journal,
    ev.year,
    ev.study_type,
    ev.participants
FROM relationships r
JOIN entities e1 ON r.subject_id = e1.id
JOIN entities e2 ON r.object_id = e2.id
LEFT JOIN evidence ev ON r.evidence_id = ev.id
WHERE r.predicate = 'SAFER_THAN'
  AND e1.type = 'Drug'
  AND e2.type = 'Drug'
ORDER BY r.reliability DESC, ev.participants DESC;


-- 6. 샘플 데이터 삽입
-- (예시: Naproxen)
INSERT OR IGNORE INTO entities (name, type, description) VALUES
('Naproxen', 'Drug', '비스테로이드성 소염진통제'),
('Ibuprofen', 'Drug', '비스테로이드성 소염진통제'),
('Diclofenac', 'Drug', '비스테로이드성 소염진통제'),
('Myocardial_Infarction', 'Adverse_Event', '심근경색'),
('Stroke', 'Adverse_Event', '뇌졸중'),
('Patient_with_HTN', 'Patient_Profile', '고혈압 환자');

INSERT OR IGNORE INTO evidence (pmid, title, journal, year, study_type, participants, reliability) VALUES
('27959716', 'CV Safety of Celecoxib, Naproxen, Ibuprofen', 'NEJM', 2016, 'RCT', 24081, 0.99);

INSERT OR IGNORE INTO relationships (subject_id, predicate, object_id, evidence_id, reliability) VALUES
((SELECT id FROM entities WHERE name='Naproxen'),
 'SAFER_THAN',
 (SELECT id FROM entities WHERE name='Diclofenac'),
 (SELECT id FROM evidence WHERE pmid='27959716'),
 0.99);

MCP Server 개발 가이드

🔌 MCP (Model Context Protocol) 개요

MCP는 AI 모델이 외부 데이터 소스에 접근할 수 있도록 하는 프로토콜입니다.

📦 MCP Server 구조

mcp-pubmed-graphrag/
├── server.py           # MCP 서버 메인
├── tools/
│   ├── search_pubmed.py      # PubMed 검색 도구
│   ├── fetch_paper.py         # 논문 상세 조회
│   ├── query_graph.py         # GraphRAG 쿼리
│   └── recommend_drug.py      # 약물 추천
├── resources/
│   ├── knowledge_graph.db     # SQLite DB
│   └── pubmed_cache/          # 논문 캐시
├── config.json
└── README.md

🛠️ MCP Server 구현 예시

"""
MCP Server: PubMed GraphRAG

제공 기능:
1. PubMed 논문 검색
2. 지식 그래프 쿼리
3. 약물 추천 (근거 기반)
"""

from mcp.server import Server, Tool
from mcp.types import TextContent
import os
import sqlite3
from Bio import Entrez
from dotenv import load_dotenv

load_dotenv()

# MCP 서버 초기화
server = Server("pubmed-graphrag")

# Entrez 설정
Entrez.email = os.getenv('PUBMED_EMAIL')


# ============================================================
# Tool 1: PubMed 검색
# ============================================================

@server.tool()
async def search_pubmed(query: str, max_results: int = 5) -> TextContent:
    """
    PubMed에서 논문 검색

    Args:
        query: 검색어
        max_results: 최대 결과 수

    Returns:
        검색 결과 (PMID, 제목, 초록)
    """
    try:
        handle = Entrez.esearch(
            db="pubmed",
            term=query,
            retmax=max_results,
            sort="relevance"
        )
        record = Entrez.read(handle)
        handle.close()

        pmids = record["IdList"]

        # 상세 정보 가져오기
        handle = Entrez.efetch(
            db="pubmed",
            id=pmids,
            rettype="medline",
            retmode="xml"
        )
        papers = Entrez.read(handle)
        handle.close()

        results = []
        for paper in papers['PubmedArticle']:
            pmid = str(paper['MedlineCitation']['PMID'])
            title = paper['MedlineCitation']['Article'].get('ArticleTitle', '')

            results.append({
                'pmid': pmid,
                'title': title,
                'url': f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/"
            })

        return TextContent(
            type="text",
            text=json.dumps(results, ensure_ascii=False, indent=2)
        )

    except Exception as e:
        return TextContent(
            type="text",
            text=f"Error: {str(e)}"
        )


# ============================================================
# Tool 2: 지식 그래프 쿼리
# ============================================================

@server.tool()
async def query_knowledge_graph(
    entity: str,
    relationship_type: str = None
) -> TextContent:
    """
    지식 그래프에서 Entity 관련 관계 검색

    Args:
        entity: Entity 이름 (예: "Naproxen")
        relationship_type: 관계 유형 (옵션, 예: "SAFER_THAN")

    Returns:
        관련 관계 및 근거
    """
    try:
        db_path = os.path.join(
            os.path.dirname(__file__),
            'resources',
            'knowledge_graph.db'
        )
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        # 쿼리 구성
        if relationship_type:
            query = """
                SELECT
                    e1.name AS subject,
                    r.predicate,
                    e2.name AS object,
                    r.reliability,
                    ev.pmid,
                    ev.title
                FROM relationships r
                JOIN entities e1 ON r.subject_id = e1.id
                JOIN entities e2 ON r.object_id = e2.id
                LEFT JOIN evidence ev ON r.evidence_id = ev.id
                WHERE e1.name = ? AND r.predicate = ?
                ORDER BY r.reliability DESC
            """
            cursor.execute(query, (entity, relationship_type))
        else:
            query = """
                SELECT
                    e1.name AS subject,
                    r.predicate,
                    e2.name AS object,
                    r.reliability,
                    ev.pmid,
                    ev.title
                FROM relationships r
                JOIN entities e1 ON r.subject_id = e1.id
                JOIN entities e2 ON r.object_id = e2.id
                LEFT JOIN evidence ev ON r.evidence_id = ev.id
                WHERE e1.name = ?
                ORDER BY r.reliability DESC
            """
            cursor.execute(query, (entity,))

        results = []
        for row in cursor.fetchall():
            results.append({
                'subject': row[0],
                'predicate': row[1],
                'object': row[2],
                'reliability': row[3],
                'evidence_pmid': row[4],
                'evidence_title': row[5]
            })

        conn.close()

        return TextContent(
            type="text",
            text=json.dumps(results, ensure_ascii=False, indent=2)
        )

    except Exception as e:
        return TextContent(
            type="text",
            text=f"Error: {str(e)}"
        )


# ============================================================
# Tool 3: 약물 추천 (GraphRAG)
# ============================================================

@server.tool()
async def recommend_drug(
    symptom: str,
    patient_conditions: list = None
) -> TextContent:
    """
    환자 프로필 기반 약물 추천

    Args:
        symptom: 증상 (예: "Knee_pain")
        patient_conditions: 환자 기저질환 (예: ["HTN", "DM"])

    Returns:
        추천 약물 + 근거 + 추론 경로
    """
    try:
        db_path = os.path.join(
            os.path.dirname(__file__),
            'resources',
            'knowledge_graph.db'
        )
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()

        # 1. 증상에 효과적인 약물 검색
        cursor.execute("""
            SELECT
                e1.name AS drug,
                r.reliability,
                ev.pmid,
                ev.title
            FROM relationships r
            JOIN entities e1 ON r.subject_id = e1.id
            JOIN entities e2 ON r.object_id = e2.id
            LEFT JOIN evidence ev ON r.evidence_id = ev.id
            WHERE r.predicate IN ('TREATS', 'REDUCES')
              AND e2.name = ?
            ORDER BY r.reliability DESC
        """, (symptom,))

        effective_drugs = cursor.fetchall()

        # 2. 금기 약물 제외
        if patient_conditions:
            for condition in patient_conditions:
                cursor.execute("""
                    SELECT e1.name
                    FROM relationships r
                    JOIN entities e1 ON r.subject_id = e1.id
                    JOIN entities e2 ON r.object_id = e2.id
                    WHERE r.predicate = 'CONTRAINDICATED_IN'
                      AND e2.name = ?
                """, (f"Patient_with_{condition}",))

                contraindicated = [row[0] for row in cursor.fetchall()]
                effective_drugs = [
                    drug for drug in effective_drugs
                    if drug[0] not in contraindicated
                ]

        # 3. 추천 구성
        if effective_drugs:
            top_drug = effective_drugs[0]
            recommendation = {
                'recommended_drug': top_drug[0],
                'reliability': top_drug[1],
                'evidence_pmid': top_drug[2],
                'evidence_title': top_drug[3],
                'reasoning': [
                    f"증상: {symptom}",
                    f"추천 약물: {top_drug[0]}",
                    f"근거: PMID:{top_drug[2]}",
                    f"신뢰도: {top_drug[1]:.0%}"
                ]
            }
        else:
            recommendation = {
                'error': '적합한 약물을 찾을 수 없습니다.'
            }

        conn.close()

        return TextContent(
            type="text",
            text=json.dumps(recommendation, ensure_ascii=False, indent=2)
        )

    except Exception as e:
        return TextContent(
            type="text",
            text=f"Error: {str(e)}"
        )


# ============================================================
# MCP Server 실행
# ============================================================

if __name__ == "__main__":
    server.run()

🚀 MCP Server 사용 예시

# Claude Desktop에서 MCP Server 사용

# 1. PubMed 검색
result = await mcp.call_tool(
    "search_pubmed",
    {
        "query": "statin AND coq10 AND muscle",
        "max_results": 5
    }
)

# 2. 지식 그래프 쿼리
result = await mcp.call_tool(
    "query_knowledge_graph",
    {
        "entity": "Naproxen",
        "relationship_type": "SAFER_THAN"
    }
)

# 3. 약물 추천
result = await mcp.call_tool(
    "recommend_drug",
    {
        "symptom": "Knee_pain",
        "patient_conditions": ["HTN", "DM"]
    }
)

AI Agent 개발 가이드

🤖 Agent 아키텍처

┌─────────────────────────────────────────────────────────────┐
│                     AI Agent                                │
│  (Claude, GPT-4, or Custom LLM)                             │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│                  Tool Orchestrator                          │
│  - 적절한 도구 선택                                         │
│  - 추론 경로 생성                                           │
│  - 결과 통합                                                │
└─────────────────────────────────────────────────────────────┘
                          ↓
    ┌─────────────────────┴─────────────────────┐
    │                                           │
    ↓                                           ↓
┌─────────────────────┐           ┌──────────────────────┐
│  PubMed Search Tool │           │ GraphRAG Query Tool  │
│  - 논문 검색        │           │ - 지식 그래프 쿼리   │
│  - 근거 추출        │           │ - 추론 경로 생성     │
└─────────────────────┘           └──────────────────────┘

🧩 Agent Tool 구현

"""
AI Agent Tools for PubMed GraphRAG
"""

from typing import List, Dict, Optional
import sqlite3
import os
from Bio import Entrez


class PubMedGraphRAGAgent:
    """
    PubMed + GraphRAG 기반 약물 추천 Agent
    """

    def __init__(self, db_path: str, entrez_email: str):
        self.db_path = db_path
        Entrez.email = entrez_email


    def search_evidence(
        self,
        query: str,
        max_results: int = 5
    ) -> List[Dict]:
        """
        PubMed에서 근거 검색
        """
        try:
            handle = Entrez.esearch(
                db="pubmed",
                term=query,
                retmax=max_results,
                sort="relevance"
            )
            record = Entrez.read(handle)
            handle.close()

            pmids = record["IdList"]

            handle = Entrez.efetch(
                db="pubmed",
                id=pmids,
                rettype="medline",
                retmode="xml"
            )
            papers = Entrez.read(handle)
            handle.close()

            results = []
            for paper in papers['PubmedArticle']:
                pmid = str(paper['MedlineCitation']['PMID'])
                article = paper['MedlineCitation']['Article']

                results.append({
                    'pmid': pmid,
                    'title': article.get('ArticleTitle', ''),
                    'journal': article.get('Journal', {}).get('Title', ''),
                    'year': article.get('Journal', {}).get('JournalIssue', {}).get('PubDate', {}).get('Year', '')
                })

            return results

        except Exception as e:
            print(f"Error searching PubMed: {e}")
            return []


    def query_graph(
        self,
        entity: str,
        relation: Optional[str] = None
    ) -> List[Dict]:
        """
        지식 그래프 쿼리
        """
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()

        try:
            if relation:
                query = """
                    SELECT
                        e1.name, r.predicate, e2.name,
                        r.reliability, ev.pmid
                    FROM relationships r
                    JOIN entities e1 ON r.subject_id = e1.id
                    JOIN entities e2 ON r.object_id = e2.id
                    LEFT JOIN evidence ev ON r.evidence_id = ev.id
                    WHERE e1.name = ? AND r.predicate = ?
                """
                cursor.execute(query, (entity, relation))
            else:
                query = """
                    SELECT
                        e1.name, r.predicate, e2.name,
                        r.reliability, ev.pmid
                    FROM relationships r
                    JOIN entities e1 ON r.subject_id = e1.id
                    JOIN entities e2 ON r.object_id = e2.id
                    LEFT JOIN evidence ev ON r.evidence_id = ev.id
                    WHERE e1.name = ?
                """
                cursor.execute(query, (entity,))

            results = []
            for row in cursor.fetchall():
                results.append({
                    'subject': row[0],
                    'predicate': row[1],
                    'object': row[2],
                    'reliability': row[3],
                    'pmid': row[4]
                })

            return results

        finally:
            conn.close()


    def recommend(
        self,
        patient: Dict,
        symptom: str
    ) -> Dict:
        """
        환자 프로필 기반 약물 추천

        Args:
            patient: {'age': 65, 'conditions': ['HTN', 'DM']}
            symptom: 'Knee_pain'

        Returns:
            추천 결과 + 근거 + 추론 경로
        """
        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()

        try:
            # 1. 증상 치료 약물 검색
            cursor.execute("""
                SELECT
                    e1.name, r.reliability, ev.pmid, ev.title
                FROM relationships r
                JOIN entities e1 ON r.subject_id = e1.id
                JOIN entities e2 ON r.object_id = e2.id
                LEFT JOIN evidence ev ON r.evidence_id = ev.id
                WHERE r.predicate IN ('TREATS', 'REDUCES')
                  AND e2.name = ?
                  AND e1.type = 'Drug'
                ORDER BY r.reliability DESC
            """, (symptom,))

            candidates = cursor.fetchall()

            # 2. 금기 약물 제외
            reasoning_path = []
            reasoning_path.append(
                f"환자: {patient['age']}세, "
                f"{', '.join(patient.get('conditions', []))}"
            )

            for condition in patient.get('conditions', []):
                cursor.execute("""
                    SELECT e1.name, ev.pmid
                    FROM relationships r
                    JOIN entities e1 ON r.subject_id = e1.id
                    JOIN entities e2 ON r.object_id = e2.id
                    LEFT JOIN evidence ev ON r.evidence_id = ev.id
                    WHERE r.predicate = 'CONTRAINDICATED_IN'
                      AND e2.name = ?
                """, (f"Patient_with_{condition}",))

                contraindicated = cursor.fetchall()
                for drug, pmid in contraindicated:
                    candidates = [
                        c for c in candidates if c[0] != drug
                    ]
                    reasoning_path.append(
                        f"{drug}: 부적합 "
                        f"(PMID:{pmid})"
                    )

            # 3. 최종 추천
            if candidates:
                top_drug = candidates[0]

                # 안전성 검증
                cursor.execute("""
                    SELECT e2.name, r.reliability, ev.pmid
                    FROM relationships r
                    JOIN entities e2 ON r.object_id = e2.id
                    LEFT JOIN evidence ev ON r.evidence_id = ev.id
                    WHERE r.subject_id = (
                        SELECT id FROM entities WHERE name = ?
                    )
                    AND r.predicate = 'SAFER_THAN'
                    ORDER BY r.reliability DESC
                """, (top_drug[0],))

                safety_info = cursor.fetchall()
                if safety_info:
                    safer_than = safety_info[0]
                    reasoning_path.append(
                        f"{top_drug[0]}: {safer_than[0]}보다 안전 "
                        f"(PMID:{safer_than[2]}, "
                        f"신뢰도: {safer_than[1]:.0%})"
                    )

                recommendation = {
                    'drug': top_drug[0],
                    'reliability': top_drug[1],
                    'evidence': {
                        'pmid': top_drug[2],
                        'title': top_drug[3]
                    },
                    'reasoning_path': reasoning_path
                }
            else:
                recommendation = {
                    'error': '적합한 약물을 찾을 수 없습니다.',
                    'reasoning_path': reasoning_path
                }

            return recommendation

        finally:
            conn.close()


# 사용 예시
if __name__ == '__main__':
    agent = PubMedGraphRAGAgent(
        db_path='./db/knowledge_graph.db',
        entrez_email='pharmacy@example.com'
    )

    # 환자 프로필
    patient = {
        'age': 65,
        'conditions': ['HTN', 'DM']
    }

    # 추천
    result = agent.recommend(patient, 'Knee_pain')

    print(json.dumps(result, ensure_ascii=False, indent=2))

실제 사례 연구

📚 사례 1: CoQ10 + Statin 근육병증

시나리오: Statin 복용 환자의 근육통 관리

검색 쿼리

query = "statin AND coq10 AND muscle"

핵심 논문

  • PMID: 30371340 (JAHA 2018, Meta-analysis, n=575)
    • 제목: "Effects of CoQ10 on Statin-Induced Myopathy"
    • 결과: SMD -1.60 (근육 통증), P<0.001
    • 신뢰도: 0.95 (메타분석)

지식 그래프 트리플

triples = [
    ('Statin', 'INHIBITS', 'CoQ10_synthesis'),
    ('CoQ10_deficiency', 'CAUSES', 'Muscle_weakness'),
    ('CoQ10_supplement', 'REDUCES', 'Statin_myopathy'),
    ('PMID:30371340', 'SUPPORTS', 'CoQ10->Statin_myopathy')
]

AI 추천 출력

{
  "patient": {"conditions": ["Statin_user", "Muscle_pain"]},
  "recommendation": {
    "product": "CoQ10 100mg",
    "dosage": "하루 2회",
    "evidence": {
      "pmid": "30371340",
      "finding": "근육 통증 -1.60점 개선 (P<0.001)",
      "reliability": 0.95
    },
    "reasoning": [
      "Statin 복용 → CoQ10 합성 억제",
      "CoQ10 부족 → 미토콘드리아 기능 저하 → 근육통",
      "CoQ10 보충 → 근육 통증 유의미하게 개선",
      "근거: 메타분석 (n=575, PMID:30371340)"
    ]
  }
}

📚 사례 2: Ashwagandha 수면 개선

시나리오: 스트레스성 불면증 환자

검색 쿼리

query = "ashwagandha AND sleep AND insomnia"

핵심 논문

  • PMID: 34559859 (PLoS One 2021, Meta-analysis, n=400)
    • 제목: "Effect of Ashwagandha on Sleep"
    • 결과: SMD -0.59 (전체 수면), P<0.001
    • 신뢰도: 0.90

지식 그래프 트리플

triples = [
    ('Chronic_Stress', 'CAUSES', 'Insomnia'),
    ('Ashwagandha', 'REDUCES', 'Cortisol'),
    ('Ashwagandha', 'ACTIVATES', 'GABA_Receptor'),
    ('Low_Cortisol', 'IMPROVES', 'Sleep_Quality'),
    ('PMID:34559859', 'SUPPORTS', 'Ashwagandha->Sleep_Quality')
]

AI 추천 출력

{
  "patient": {"symptom": "Insomnia", "cause": "Chronic_Stress"},
  "recommendation": {
    "product": "Ashwagandha 300mg",
    "dosage": "하루 2회 (아침/저녁)",
    "duration": "최소 8주",
    "evidence": {
      "pmid": "34559859",
      "finding": "수면 품질 개선 (SMD -0.59, P<0.001)",
      "reliability": 0.90
    },
    "mechanism": [
      "코르티솔 감소 → 스트레스 완화",
      "GABA 수용체 활성화 → 수면 유도"
    ],
    "add_on": "멜라토닌 3mg (즉각적 효과)"
  }
}

📚 사례 3: Naproxen 심혈관 안전성

시나리오: 고혈압 환자의 관절통

검색 쿼리

query = "naproxen AND cardiovascular AND safety"

핵심 논문

  • PMID: 27959716 (NEJM 2016, RCT, n=24,081)
    • 제목: "CV Safety of Celecoxib, Naproxen, Ibuprofen"
    • 결과: Naproxen CV event 2.5% (최저)
    • 신뢰도: 0.99 (NEJM + 대규모 RCT)

지식 그래프 트리플

triples = [
    ('Naproxen', 'CV_EVENT_RATE', '2.5%'),
    ('Ibuprofen', 'CV_EVENT_RATE', '2.7%'),
    ('Diclofenac', 'CV_EVENT_RATE', '높음'),
    ('Naproxen', 'SAFER_THAN', 'Diclofenac'),
    ('Naproxen', 'SAFER_THAN', 'Ibuprofen'),
    ('Patient_with_HTN', 'RECOMMEND', 'Naproxen'),
    ('PMID:27959716', 'SUPPORTS', 'Naproxen->Lowest_CV_Risk')
]

AI 추천 출력

{
  "patient": {
    "age": 65,
    "conditions": ["HTN", "DM"],
    "symptom": "Knee_pain"
  },
  "recommendation": {
    "product": "Naproxen 250mg",
    "dosage": "하루 2회 (아침/저녁 식후)",
    "evidence": {
      "pmid": "27959716",
      "journal": "NEJM",
      "finding": "24,081명 RCT, CV event 2.5% (최저)",
      "reliability": 0.99
    },
    "reasoning": [
      "환자: 고혈압 + 당뇨 → 심혈관 위험군",
      "디클로페낙: CV risk 높음 → 부적합",
      "이부프로펜: CV event 2.7% → 차선책",
      "나프록센: CV event 2.5% (최저) → 최적",
      "근거: NEJM 2016 (PMID:27959716)"
    ],
    "add_on": "오메프라졸 20mg (위 보호)",
    "upselling_point": "심혈관 안전성 + 하루 2회 복용 편의성"
  }
}

참고 자료

📖 문서

🔧 코드 예시

  • backend/pubmed_search.py - PubMed 검색 템플릿
  • backend/ashwagandha_sleep_research.py - 실제 구현 예시
  • backend/naproxen_advantages_research.py - NSAID 비교 연구

🗃️ 데이터베이스

  • backend/db/knowledge_graph.db - 지식 그래프 DB
  • backend/db/mileage.db - 제품 카테고리 DB

📌 다음 단계

  1. 자동화: GitHub Actions로 매주 새 논문 자동 검색
  2. 확장: 더 많은 약물-증상 관계 추가
  3. MCP 배포: Claude Desktop MCP Server로 배포
  4. Agent 개발: LangChain/LlamaIndex 기반 Agent 구축
  5. 웹 UI: 약사용 대시보드 개발

문서 작성: Claude Code with Sonnet 4.5 버전: 1.0 최종 수정: 2026-01-24