약국 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>
2357 lines
73 KiB
Markdown
2357 lines
73 KiB
Markdown
# PubMed 기반 GraphRAG 지식 그래프 구축 워크플로우
|
|
|
|
> 약국 업셀링 및 AI 추천 시스템을 위한 근거 기반 의약품 지식 그래프 구축 방법론
|
|
|
|
**작성일**: 2026-01-24
|
|
**목적**: MCP Server 또는 AI Agent 개발을 위한 표준 워크플로우 문서화
|
|
|
|
---
|
|
|
|
## 📋 목차
|
|
|
|
1. [개요](#개요)
|
|
2. [전체 워크플로우](#전체-워크플로우)
|
|
3. [단계별 상세 프로세스](#단계별-상세-프로세스)
|
|
4. [Python 스크립트 템플릿](#python-스크립트-템플릿)
|
|
5. [GraphRAG 지식 그래프 구조](#graphrag-지식-그래프-구조)
|
|
6. [데이터베이스 스키마](#데이터베이스-스키마)
|
|
7. [MCP Server 개발 가이드](#mcp-server-개발-가이드)
|
|
8. [AI Agent 개발 가이드](#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)
|
|
|
|
```python
|
|
# 기본 패턴
|
|
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 필터 조건
|
|
|
|
```python
|
|
filters = {
|
|
"publication_type": [
|
|
"Meta-Analysis",
|
|
"Randomized Controlled Trial",
|
|
"Systematic Review"
|
|
],
|
|
"publication_date": "2015-2024", # 최근 10년
|
|
"language": "English",
|
|
"species": "Humans"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### STEP 2: PubMed 논문 검색
|
|
|
|
#### 2.1 환경 설정
|
|
|
|
```bash
|
|
# 패키지 설치
|
|
pip install biopython python-dotenv
|
|
|
|
# .env 파일 설정
|
|
PUBMED_EMAIL=pharmacy@example.com
|
|
# PUBMED_API_KEY=xxx # Optional (10 req/sec), 없으면 3 req/sec
|
|
```
|
|
|
|
#### 2.2 검색 스크립트 기본 구조
|
|
|
|
```python
|
|
"""
|
|
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 핵심 정보 추출
|
|
|
|
**목표**: 논문에서 추천 시스템에 필요한 정보만 추출
|
|
|
|
**추출 항목**:
|
|
```python
|
|
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 신뢰도 계산 알고리즘
|
|
|
|
```python
|
|
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 또는 수동)
|
|
|
|
```python
|
|
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 구조
|
|
|
|
```python
|
|
"""
|
|
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 분류
|
|
|
|
```python
|
|
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 유형
|
|
|
|
```python
|
|
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 쿼리 패턴
|
|
|
|
```python
|
|
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 추론 경로 생성
|
|
|
|
```python
|
|
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 스크립트 템플릿
|
|
|
|
### 📝 표준 템플릿 구조
|
|
|
|
```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 정의
|
|
|
|
```sql
|
|
-- 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 정의
|
|
|
|
```sql
|
|
-- 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 정의
|
|
|
|
```sql
|
|
-- 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 쿼리 예시
|
|
|
|
```sql
|
|
-- 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 스키마 생성 스크립트
|
|
|
|
```sql
|
|
-- 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 구현 예시
|
|
|
|
```python
|
|
"""
|
|
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 사용 예시
|
|
|
|
```python
|
|
# 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 구현
|
|
|
|
```python
|
|
"""
|
|
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 복용 환자의 근육통 관리
|
|
|
|
#### 검색 쿼리
|
|
```python
|
|
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 (메타분석)
|
|
|
|
#### 지식 그래프 트리플
|
|
```python
|
|
triples = [
|
|
('Statin', 'INHIBITS', 'CoQ10_synthesis'),
|
|
('CoQ10_deficiency', 'CAUSES', 'Muscle_weakness'),
|
|
('CoQ10_supplement', 'REDUCES', 'Statin_myopathy'),
|
|
('PMID:30371340', 'SUPPORTS', 'CoQ10->Statin_myopathy')
|
|
]
|
|
```
|
|
|
|
#### AI 추천 출력
|
|
```json
|
|
{
|
|
"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 수면 개선
|
|
|
|
**시나리오**: 스트레스성 불면증 환자
|
|
|
|
#### 검색 쿼리
|
|
```python
|
|
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
|
|
|
|
#### 지식 그래프 트리플
|
|
```python
|
|
triples = [
|
|
('Chronic_Stress', 'CAUSES', 'Insomnia'),
|
|
('Ashwagandha', 'REDUCES', 'Cortisol'),
|
|
('Ashwagandha', 'ACTIVATES', 'GABA_Receptor'),
|
|
('Low_Cortisol', 'IMPROVES', 'Sleep_Quality'),
|
|
('PMID:34559859', 'SUPPORTS', 'Ashwagandha->Sleep_Quality')
|
|
]
|
|
```
|
|
|
|
#### AI 추천 출력
|
|
```json
|
|
{
|
|
"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 심혈관 안전성
|
|
|
|
**시나리오**: 고혈압 환자의 관절통
|
|
|
|
#### 검색 쿼리
|
|
```python
|
|
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)
|
|
|
|
#### 지식 그래프 트리플
|
|
```python
|
|
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 추천 출력
|
|
```json
|
|
{
|
|
"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회 복용 편의성"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 참고 자료
|
|
|
|
### 📖 문서
|
|
|
|
- **NCBI E-utilities**: https://www.ncbi.nlm.nih.gov/books/NBK25501/
|
|
- **Biopython Tutorial**: https://biopython.org/DIST/docs/tutorial/Tutorial.html
|
|
- **MCP Protocol**: https://modelcontextprotocol.io/
|
|
- **GraphRAG**: https://microsoft.github.io/graphrag/
|
|
|
|
### 🔧 코드 예시
|
|
|
|
- `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
|