ClinicalTrialV2 / app.py
高橋慧
stage final
eaf91d7
raw
history blame
29.1 kB
import gradio as gr
import pandas as pd
import time
import traceback
import os
import requests
# 完全版のimportエラー対策(段階的フォールバック)
LANGCHAIN_AVAILABLE = False
FULL_VERSION = False
try:
from langchain_groq import ChatGroq
from langchain_openai import ChatOpenAI
LANGCHAIN_AVAILABLE = True
print("✅ LangChain基本ライブラリが利用可能です")
except ImportError as e:
print(f"⚠️ LangChain基本ライブラリが利用できません: {e}")
try:
from OpenAITools.FetchTools import fetch_clinical_trials
from OpenAITools.CrinicalTrialTools import SimpleClinicalTrialAgent, GraderAgent, LLMTranslator, generate_ex_question_English
if LANGCHAIN_AVAILABLE:
FULL_VERSION = True
print("✅ 完全版モジュールが正常にロードされました")
except ImportError as e:
print(f"⚠️ 完全版モジュールのインポートに失敗: {e}")
print("軽量版モードで動作します")
# 環境変数チェック
def check_environment():
"""環境変数をチェックし、不足している場合は警告"""
missing_vars = []
# GROQ_API_KEYは必須
groq_key_available = bool(os.getenv("GROQ_API_KEY"))
if not groq_key_available:
missing_vars.append("GROQ_API_KEY")
# OPENAI_API_KEYはオプション
openai_key_available = bool(os.getenv("OPENAI_API_KEY"))
if not openai_key_available:
print("ℹ️ OPENAI_API_KEY が設定されていません(オプション機能)")
if missing_vars:
print(f"⚠️ 必須環境変数が設定されていません: {', '.join(missing_vars)}")
print("AI評価機能が制限される可能性があります。")
# GROQ_API_KEYがあれば完全版として動作可能
return groq_key_available
# 環境変数チェック実行
env_ok = check_environment()
# モデルとエージェントの安全な初期化
def safe_init_agents():
"""エージェントを安全に初期化"""
if not FULL_VERSION:
return None, None, None
try:
groq = ChatGroq(model_name="llama3-70b-8192", temperature=0)
translator = LLMTranslator(groq)
criteria_agent = SimpleClinicalTrialAgent(groq)
grader_agent = GraderAgent(groq)
print("✅ AIエージェントが正常に初期化されました")
return translator, criteria_agent, grader_agent
except Exception as e:
print(f"❌ エージェント初期化エラー: {e}")
return None, None, None
# エージェント初期化
translator, CriteriaCheckAgent, grader_agent = safe_init_agents()
# エラーハンドリング付きでエージェント評価を実行する関数
def evaluate_with_retry(agent, criteria, question, max_retries=3):
"""エラーハンドリング付きでエージェント評価を実行"""
if agent is None:
return "評価エラー: エージェントが初期化されていません。API keyを確認してください。"
for attempt in range(max_retries):
try:
return agent.evaluate_eligibility(criteria, question)
except Exception as e:
if "missing variables" in str(e):
print(f"プロンプトテンプレートエラー (試行 {attempt + 1}/{max_retries}): {e}")
return "評価エラー: プロンプトテンプレートの設定に問題があります"
elif "no healthy upstream" in str(e) or "InternalServerError" in str(e):
print(f"Groqサーバーエラー (試行 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(2)
continue
else:
return "評価エラー: サーバーに接続できませんでした"
elif "API key" in str(e) or "authentication" in str(e).lower():
return "評価エラー: API keyが無効または設定されていません"
else:
print(f"予期しないエラー (試行 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(1)
continue
else:
return f"評価エラー: {str(e)}"
return "評価エラー: 最大リトライ回数に達しました"
def evaluate_grade_with_retry(agent, judgment, max_retries=3):
"""エラーハンドリング付きでグレード評価を実行"""
if agent is None:
return "unclear"
for attempt in range(max_retries):
try:
return agent.evaluate_eligibility(judgment)
except Exception as e:
if "no healthy upstream" in str(e) or "InternalServerError" in str(e):
print(f"Groqサーバーエラー (グレード評価 - 試行 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(2)
continue
else:
return "unclear"
elif "API key" in str(e) or "authentication" in str(e).lower():
return "unclear"
else:
print(f"予期しないエラー (グレード評価 - 試行 {attempt + 1}/{max_retries}): {e}")
if attempt < max_retries - 1:
time.sleep(1)
continue
else:
return "unclear"
return "unclear"
# 基本的なClinicalTrials.gov API呼び出し(軽量版)
def fetch_clinical_trials_basic(cancer_name):
"""基本的な臨床試験データ取得(requestsのみ使用)"""
try:
search_expr = f"{cancer_name} SEARCH[Location](AREA[LocationCountry]Japan AND AREA[LocationStatus]Recruiting)"
base_url = "https://clinicaltrials.gov/api/v2/studies"
params = {
"query.titles": search_expr,
"pageSize": 20 # 軽量版では20件に制限
}
print(f"基本API呼び出し: {cancer_name}")
response = requests.get(base_url, params=params)
if response.status_code == 200:
data = response.json()
studies = data.get('studies', [])
data_list = []
for study in studies:
nctId = study['protocolSection']['identificationModule'].get('nctId', 'Unknown')
title = study['protocolSection']['identificationModule'].get('briefTitle', 'no title')
conditions = ', '.join(study['protocolSection']['conditionsModule'].get('conditions', ['No conditions listed']))
summary = study['protocolSection']['descriptionModule'].get('briefSummary', 'no summary')
# 場所情報の抽出
locations_list = study['protocolSection'].get('contactsLocationsModule', {}).get('locations', [])
japan_locations = []
for location in locations_list:
if location.get('country') == 'Japan':
city = location.get('city', 'Unknown City')
japan_locations.append(city)
primaryCompletionDate = study['protocolSection']['statusModule'].get('primaryCompletionDateStruct', {}).get('date', 'Unknown Date')
eligibilityCriteria = study['protocolSection']['eligibilityModule'].get('eligibilityCriteria', 'Unknown')
data_list.append({
"NCTID": nctId,
"Title": title,
"Primary Completion Date": primaryCompletionDate,
"Cancer": conditions,
"Summary": summary,
"Japanes Locations": ', '.join(set(japan_locations)) if japan_locations else "No Japan locations",
"Eligibility Criteria": eligibilityCriteria
})
return data_list
else:
print(f"API呼び出し失敗: {response.status_code}")
return []
except Exception as e:
print(f"基本API呼び出しエラー: {e}")
return []
# 軽量版データ生成関数
def generate_sample_data(age, sex, tumor_type, GeneMutation, Meseable, Biopsiable):
"""サンプルデータを生成(辞書のリスト形式)"""
try:
if not all([age, sex, tumor_type]):
return []
sample_data = [
{
"NCTID": "NCT12345678",
"AgentGrade": "yes",
"Title": f"Clinical Trial for {tumor_type} in {sex} patients",
"AgentJudgment": f"{age}{sex}{tumor_type}患者は参加可能です。詳細な検査結果により最終判断が必要です。",
"Japanes Locations": "Tokyo, Osaka",
"Primary Completion Date": "2025-12-31",
"Cancer": tumor_type,
"Summary": f"Phase II study evaluating new treatment for {tumor_type}",
"Eligibility Criteria": f"Age 18-75, confirmed {tumor_type}, adequate organ function"
},
{
"NCTID": "NCT87654321",
"AgentGrade": "no",
"Title": f"Alternative treatment for {tumor_type}",
"AgentJudgment": f"{age}{sex}{tumor_type}患者は年齢制限により参加できません。",
"Japanes Locations": "Kyoto, Fukuoka",
"Primary Completion Date": "2026-06-30",
"Cancer": tumor_type,
"Summary": f"Comparative study of standard vs experimental therapy for {tumor_type}",
"Eligibility Criteria": f"Age 20-65, {tumor_type} with specific mutations, ECOG 0-1"
},
{
"NCTID": "NCT11111111",
"AgentGrade": "unclear",
"Title": f"Experimental therapy for {tumor_type} with {GeneMutation}",
"AgentJudgment": f"{age}{sex}{tumor_type}患者の参加は追加情報が必要で不明確です。",
"Japanes Locations": "Nagoya, Sendai",
"Primary Completion Date": "2025-09-15",
"Cancer": tumor_type,
"Summary": f"Early phase trial testing combination therapy for {tumor_type}",
"Eligibility Criteria": f"Age 18-80, advanced {tumor_type}, previous treatment failure"
}
]
return sample_data
except Exception as e:
print(f"サンプルデータ生成エラー: {e}")
return []
# 基本版データ生成関数(ClinicalTrials.gov API使用、AI評価なし)
def generate_basic_data(age, sex, tumor_type, GeneMutation, Meseable, Biopsiable):
"""基本版のデータ生成(API使用、AI評価なし)"""
try:
if not all([age, sex, tumor_type]):
return []
# 実際のAPI呼び出し
data_list = fetch_clinical_trials_basic(tumor_type)
if not data_list:
print("臨床試験データが見つかりませんでした")
return []
# AI評価なしのプレースホルダーを追加
for item in data_list:
item['AgentJudgment'] = f'基本版:{age}{sex}{tumor_type}患者への詳細評価にはAI機能が必要です'
item['AgentGrade'] = 'unclear'
print(f"基本版評価完了。結果: {len(data_list)} 件")
return data_list
except Exception as e:
print(f"基本版データ生成中に予期しないエラー: {e}")
traceback.print_exc()
return []
# 完全版データ生成関数(AI評価付き)
def generate_full_data(age, sex, tumor_type, GeneMutation, Meseable, Biopsiable):
"""完全版のデータ生成(実際のAPI使用 + AI評価)"""
try:
if not all([age, sex, tumor_type]):
return []
# 日本語の腫瘍タイプを英語に翻訳
try:
if translator is not None:
TumorName = translator.translate(tumor_type)
print(f"腫瘍タイプ翻訳: {tumor_type}{TumorName}")
else:
print("翻訳エージェントが利用できません。元の値を使用します。")
TumorName = tumor_type
except Exception as e:
print(f"翻訳エラー: {e}")
TumorName = tumor_type
# 質問文を生成
try:
ex_question = generate_ex_question_English(age, sex, TumorName, GeneMutation, Meseable, Biopsiable)
print(f"生成された質問: {ex_question}")
except Exception as e:
print(f"質問生成エラー: {e}")
return []
# 臨床試験データの取得
try:
print(f"臨床試験データを検索中: {TumorName}")
df = fetch_clinical_trials(TumorName)
if df.empty:
print("臨床試験データが見つかりませんでした")
return []
print(f"取得した臨床試験数: {len(df)}")
# DataFrameを辞書のリストに変換
data_list = df.to_dict('records')
except Exception as e:
print(f"臨床試験データ取得エラー: {e}")
return []
# AI評価の実行(最大10件まで)
evaluation_limit = min(len(data_list), 10)
print(f"AI評価実行: {evaluation_limit} 件を処理します")
for i, item in enumerate(data_list[:evaluation_limit]):
try:
print(f"評価中 ({i+1}/{evaluation_limit}): {item['NCTID']}")
target_criteria = item['Eligibility Criteria']
# エラーハンドリング付きで評価実行
agent_judgment = evaluate_with_retry(CriteriaCheckAgent, target_criteria, ex_question)
agent_grade = evaluate_grade_with_retry(grader_agent, agent_judgment)
# データの更新
item['AgentJudgment'] = agent_judgment
item['AgentGrade'] = agent_grade
except Exception as e:
print(f"NCTID {item['NCTID']} の評価中にエラー: {e}")
item['AgentJudgment'] = f"エラー: {str(e)}"
item['AgentGrade'] = "unclear"
# 評価されなかった残りのアイテムにはプレースホルダーを設定
for item in data_list[evaluation_limit:]:
item['AgentJudgment'] = f"完全版:{age}{sex}{tumor_type}患者(評価制限により未処理)"
item['AgentGrade'] = "unclear"
print(f"完全版評価完了。結果: {len(data_list)} 件(うち{evaluation_limit}件をAI評価)")
return data_list
except Exception as e:
print(f"完全版データ生成中に予期しないエラー: {e}")
traceback.print_exc()
return []
# HTMLテーブル生成関数
def create_html_table(data, show_grade=True):
"""データをHTMLテーブルに変換"""
if not data:
return "<div style='text-align: center; padding: 20px; color: #666;'>📄 データがありません</div>"
# CSS スタイル
table_style = """
<style>
.clinical-table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
font-family: Arial, sans-serif;
font-size: 14px;
}
.clinical-table th {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
padding: 12px 8px;
text-align: left;
font-weight: bold;
color: #495057;
}
.clinical-table td {
border: 1px solid #dee2e6;
padding: 10px 8px;
vertical-align: top;
}
.grade-yes { background-color: #d4edda; }
.grade-no { background-color: #f8d7da; }
.grade-unclear { background-color: #fff3cd; }
.clinical-table tr:hover {
background-color: #f5f5f5;
}
.nctid-link {
color: #007bff;
text-decoration: none;
font-weight: bold;
}
.nctid-link:hover {
text-decoration: underline;
}
.title-cell {
max-width: 300px;
word-wrap: break-word;
}
.criteria-cell {
max-width: 400px;
word-wrap: break-word;
font-size: 12px;
}
</style>
"""
# テーブルヘッダー
html = table_style + '<table class="clinical-table">'
html += '<tr>'
html += '<th>NCTID</th>'
if show_grade:
html += '<th>Grade</th>'
html += '<th>Title</th>'
if show_grade:
html += '<th>AI Judgment</th>'
html += '<th>Japanese Locations</th>'
html += '<th>Completion Date</th>'
html += '<th>Cancer Type</th>'
html += '</tr>'
# データ行
for item in data:
grade = item.get('AgentGrade', 'unclear')
grade_class = f"grade-{grade}" if show_grade else ""
html += f'<tr class="{grade_class}">'
# NCTID(リンク付き)
nctid = item.get('NCTID', '')
html += f'<td><a href="https://clinicaltrials.gov/ct2/show/{nctid}" target="_blank" class="nctid-link">{nctid}</a></td>'
# Grade
if show_grade:
grade_emoji = {'yes': '✅', 'no': '❌', 'unclear': '❓'}.get(grade, '❓')
html += f'<td style="text-align: center;">{grade_emoji} {grade}</td>'
# Title
title = item.get('Title', '').strip()
html += f'<td class="title-cell">{title}</td>'
# AI Judgment
if show_grade:
judgment = item.get('AgentJudgment', '').strip()
html += f'<td class="criteria-cell">{judgment}</td>'
# Japanese Locations
locations = item.get('Japanes Locations', '').strip()
html += f'<td>{locations}</td>'
# Completion Date
completion_date = item.get('Primary Completion Date', '').strip()
html += f'<td>{completion_date}</td>'
# Cancer Type
cancer = item.get('Cancer', '').strip()
html += f'<td>{cancer}</td>'
html += '</tr>'
html += '</table>'
# 統計情報
if show_grade:
total = len(data)
yes_count = len([item for item in data if item.get('AgentGrade') == 'yes'])
no_count = len([item for item in data if item.get('AgentGrade') == 'no'])
unclear_count = len([item for item in data if item.get('AgentGrade') == 'unclear'])
stats_html = f"""
<div style='margin: 10px 0; padding: 10px; background-color: #f8f9fa; border-radius: 5px; font-size: 14px;'>
<strong>📊 統計:</strong>
合計 {total} 件 |
✅ 適格 {yes_count} 件 |
❌ 不適格 {no_count} 件 |
❓ 要検討 {unclear_count}
</div>
"""
html = stats_html + html
return html
# フィルタリング関数
def filter_data(data, grade):
"""データをフィルタリング"""
if not data:
return []
try:
if grade == "all":
return data
return [item for item in data if item.get('AgentGrade') == grade]
except Exception as e:
print(f"フィルタリングエラー: {e}")
return data
# システム状態の確認
def get_system_status():
"""システムの現在の状態を確認"""
groq_available = bool(os.getenv("GROQ_API_KEY"))
if FULL_VERSION and groq_available:
return "🟢 完全版", "リアルタイム検索 + AI適格性評価が利用可能です"
elif FULL_VERSION and not groq_available:
return "🟡 完全版(制限)", "GROQ_API_KEYが必要です(Settings → Variables and secrets で設定)"
elif LANGCHAIN_AVAILABLE:
return "🟡 基本版", "ClinicalTrials.gov検索のみ利用可能(AI評価にはGROQ_API_KEY必要)"
else:
return "🔴 軽量版", "サンプルデータのみ表示"
# CSV エクスポート関数
def export_to_csv(data):
"""データをCSVファイルとしてエクスポート"""
try:
if not data:
return None
# DataFrame に変換
df = pd.DataFrame(data)
# ファイルパス
file_path = "clinical_trials_data.csv"
df.to_csv(file_path, index=False, encoding='utf-8-sig')
return file_path
except Exception as e:
print(f"CSV エクスポートエラー: {e}")
return None
# Gradioインターフェースの作成
with gr.Blocks(title="臨床試験適格性評価", theme=gr.themes.Soft()) as demo:
gr.Markdown("## 🏥 臨床試験適格性評価インターフェース(完全版)")
# システム状態表示
status_level, status_message = get_system_status()
gr.Markdown(f"**システム状態**: {status_level} - {status_message}")
# 機能説明
groq_available = bool(os.getenv("GROQ_API_KEY"))
if FULL_VERSION and groq_available:
gr.Markdown("🚀 **利用可能機能**: ClinicalTrials.gov リアルタイム検索 + AI適格性評価 + データエクスポート")
gr.Markdown("🤖 **AI機能**: Groq Llama3-70B による自動適格性判断 + 3段階グレード評価")
elif FULL_VERSION:
gr.Markdown("🔧 **利用可能機能**: リアルタイム検索 + 基本評価(AI機能にはGROQ_API_KEY必要)")
gr.Markdown("⚠️ **API設定が必要**: Settings → Variables and secrets で GROQ_API_KEY を設定してください")
elif LANGCHAIN_AVAILABLE:
gr.Markdown("🔧 **利用可能機能**: ClinicalTrials.gov検索 + 基本評価 + データエクスポート")
else:
gr.Markdown("📋 **利用可能機能**: サンプルデータ表示 + フィルタリング")
gr.Markdown("💡 **使用方法**: 患者情報を入力してボタンをクリックしてください。")
# 各種入力フィールド
with gr.Row():
with gr.Column():
age_input = gr.Textbox(label="Age", placeholder="例: 65", value="65")
sex_input = gr.Dropdown(choices=["男性", "女性"], label="Sex", value="男性")
tumor_type_input = gr.Textbox(label="Tumor Type", placeholder="例: gastric cancer", value="gastric cancer")
with gr.Column():
gene_mutation_input = gr.Textbox(label="Gene Mutation", placeholder="例: HER2", value="HER2")
measurable_input = gr.Dropdown(choices=["有り", "無し", "不明"], label="Measurable Tumor", value="有り")
biopsiable_input = gr.Dropdown(choices=["有り", "無し", "不明"], label="Biopsiable Tumor", value="有り")
# 結果表示エリア(HTMLテーブル)
results_html = gr.HTML(label="Clinical Trials Results")
# 内部状態用
data_state = gr.State(value=[])
# ボタン類
with gr.Row():
if FULL_VERSION and groq_available:
generate_button = gr.Button("🤖 AI適格性評価付き検索(完全版)", variant="primary")
elif FULL_VERSION:
generate_button = gr.Button("🔍 リアルタイム検索(GROQ_API_KEY設定後にAI評価有効)", variant="primary")
elif LANGCHAIN_AVAILABLE:
generate_button = gr.Button("📡 ClinicalTrials.gov検索(基本版)", variant="primary")
else:
generate_button = gr.Button("📋 サンプルデータ表示", variant="primary")
with gr.Row():
yes_button = gr.Button("✅ Show Eligible Trials", variant="secondary")
no_button = gr.Button("❌ Show Ineligible Trials", variant="secondary")
unclear_button = gr.Button("❓ Show Unclear Trials", variant="secondary")
all_button = gr.Button("📊 Show All Trials", variant="secondary")
with gr.Row():
download_button = gr.Button("💾 Download CSV")
# ダウンロードファイル
download_output = gr.File(label="Download CSV", visible=False)
# プログレス表示
progress_text = gr.Textbox(label="Processing Status", value="Ready", interactive=False)
# イベントハンドリング
def update_data_and_display(age, sex, tumor_type, gene_mutation, measurable, biopsiable):
"""データ生成と表示更新"""
try:
groq_available = bool(os.getenv("GROQ_API_KEY"))
if FULL_VERSION and groq_available:
progress_msg = "🤖 AI適格性評価付きで実際の臨床試験データを検索中..."
data = generate_full_data(age, sex, tumor_type, gene_mutation, measurable, biopsiable)
elif FULL_VERSION:
progress_msg = "🔍 実際の臨床試験データを検索中(AI評価にはGROQ_API_KEY必要)..."
data = generate_basic_data(age, sex, tumor_type, gene_mutation, measurable, biopsiable)
elif LANGCHAIN_AVAILABLE:
progress_msg = "📡 ClinicalTrials.govから基本データを検索中..."
data = generate_basic_data(age, sex, tumor_type, gene_mutation, measurable, biopsiable)
else:
progress_msg = "📋 サンプルデータを生成中..."
data = generate_sample_data(age, sex, tumor_type, gene_mutation, measurable, biopsiable)
if data:
html_table = create_html_table(data)
final_progress = f"✅ 完了: {len(data)} 件の臨床試験が見つかりました"
if FULL_VERSION and env_ok:
ai_count = len([item for item in data if 'エラー' not in item.get('AgentJudgment', '')])
final_progress += f"(うち最大10件をAI評価済み)"
else:
html_table = "<div style='text-align: center; padding: 20px; color: #666;'>⚠️ 該当する臨床試験が見つかりませんでした</div>"
final_progress = "⚠️ 該当する臨床試験が見つかりませんでした"
return html_table, data, final_progress
except Exception as e:
error_msg = f"❌ エラー: {str(e)}"
error_html = f"<div style='text-align: center; padding: 20px; color: #d32f2f;'>{error_msg}</div>"
print(f"データ更新エラー: {e}")
return error_html, [], error_msg
def filter_and_show(data, grade):
"""フィルタリングと表示更新"""
try:
filtered_data = filter_data(data, grade)
html_table = create_html_table(filtered_data)
return html_table
except Exception as e:
print(f"フィルタリングエラー: {e}")
return create_html_table(data)
def download_csv(data):
"""CSV ダウンロード処理"""
try:
if not data:
return None
return export_to_csv(data)
except Exception as e:
print(f"ダウンロードエラー: {e}")
return None
# ボタンイベント
generate_button.click(
fn=update_data_and_display,
inputs=[age_input, sex_input, tumor_type_input, gene_mutation_input, measurable_input, biopsiable_input],
outputs=[results_html, data_state, progress_text]
)
yes_button.click(
fn=lambda data: filter_and_show(data, "yes"),
inputs=[data_state],
outputs=[results_html]
)
no_button.click(
fn=lambda data: filter_and_show(data, "no"),
inputs=[data_state],
outputs=[results_html]
)
unclear_button.click(
fn=lambda data: filter_and_show(data, "unclear"),
inputs=[data_state],
outputs=[results_html]
)
all_button.click(
fn=lambda data: filter_and_show(data, "all"),
inputs=[data_state],
outputs=[results_html]
)
download_button.click(
fn=download_csv,
inputs=[data_state],
outputs=[download_output]
)
# フッター情報
gr.Markdown("---")
with gr.Row():
groq_available_footer = bool(os.getenv("GROQ_API_KEY"))
gr.Markdown("🔬 **技術情報**: ClinicalTrials.gov API + LangChain + Groq Llama3-70B")
gr.Markdown("📝 **AI機能状況**: " + ("AI評価機能有効" if (FULL_VERSION and groq_available_footer) else "GROQ_API_KEY設定後にAI機能有効化"))
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
debug=False,
show_error=True
)