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 = [] if not os.getenv("GROQ_API_KEY"): missing_vars.append("GROQ_API_KEY") if not os.getenv("OPENAI_API_KEY"): missing_vars.append("OPENAI_API_KEY") if missing_vars: print(f"⚠️ 環境変数が設定されていません: {', '.join(missing_vars)}") print("一部の機能が制限される可能性があります。") return len(missing_vars) == 0 # 環境変数チェック実行 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 "
📄 データがありません
" # CSS スタイル table_style = """ """ # テーブルヘッダー html = table_style + '' html += '' html += '' if show_grade: html += '' html += '' if show_grade: html += '' html += '' html += '' html += '' html += '' # データ行 for item in data: grade = item.get('AgentGrade', 'unclear') grade_class = f"grade-{grade}" if show_grade else "" html += f'' # NCTID(リンク付き) nctid = item.get('NCTID', '') html += f'' # Grade if show_grade: grade_emoji = {'yes': '✅', 'no': '❌', 'unclear': '❓'}.get(grade, '❓') html += f'' # Title title = item.get('Title', '').strip() html += f'' # AI Judgment if show_grade: judgment = item.get('AgentJudgment', '').strip() html += f'' # Japanese Locations locations = item.get('Japanes Locations', '').strip() html += f'' # Completion Date completion_date = item.get('Primary Completion Date', '').strip() html += f'' # Cancer Type cancer = item.get('Cancer', '').strip() html += f'' html += '' html += '
NCTIDGradeTitleAI JudgmentJapanese LocationsCompletion DateCancer Type
{nctid}{grade_emoji} {grade}{title}{judgment}{locations}{completion_date}{cancer}
' # 統計情報 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"""
📊 統計: 合計 {total} 件 | ✅ 適格 {yes_count} 件 | ❌ 不適格 {no_count} 件 | ❓ 要検討 {unclear_count} 件
""" 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(): """システムの現在の状態を確認""" if FULL_VERSION and env_ok: return "🟢 完全版", "リアルタイム検索 + AI適格性評価が利用可能です" elif FULL_VERSION and not env_ok: return "🟡 完全版(制限)", "AI機能は利用可能ですが、API keyの設定をお願いします" elif LANGCHAIN_AVAILABLE and env_ok: return "🟡 基本版", "ClinicalTrials.gov API検索が可能です(AI評価機能は制限)" elif LANGCHAIN_AVAILABLE: return "🟡 基本版", "API検索可能(環境変数要設定)" 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}") # 機能説明 if FULL_VERSION and env_ok: gr.Markdown("🚀 **利用可能機能**: ClinicalTrials.gov リアルタイム検索 + AI適格性評価 + データエクスポート") gr.Markdown("🤖 **AI機能**: Groq Llama3-70B による自動適格性判断 + 3段階グレード評価") elif FULL_VERSION: gr.Markdown("🔧 **利用可能機能**: リアルタイム検索 + 基本評価(AI機能は環境変数設定後に有効化)") 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 env_ok: generate_button = gr.Button("🤖 AI適格性評価付き検索(完全版)", variant="primary") elif FULL_VERSION: generate_button = gr.Button("🔍 リアルタイム検索(環境変数設定後に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: if FULL_VERSION and env_ok: progress_msg = "🤖 AI適格性評価付きで実際の臨床試験データを検索中..." data = generate_full_data(age, sex, tumor_type, gene_mutation, measurable, biopsiable) elif FULL_VERSION: progress_msg = "🔍 実際の臨床試験データを検索中(AI評価は環境変数設定後に有効化)..." 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 = "
⚠️ 該当する臨床試験が見つかりませんでした
" final_progress = "⚠️ 該当する臨床試験が見つかりませんでした" return html_table, data, final_progress except Exception as e: error_msg = f"❌ エラー: {str(e)}" error_html = f"
{error_msg}
" 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(): gr.Markdown("🔬 **技術情報**: ClinicalTrials.gov API + LangChain + Groq Llama3-70B") gr.Markdown("📝 **完全版状況**: " + ("AI評価機能有効" if (FULL_VERSION and env_ok) else "環境変数設定後にAI機能有効化")) if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, share=False, debug=False, show_error=True )