""" Drug Stability Prompt Orchestrator - Professional Bilingual UI A beautiful, professional UI for drug-excipient compatibility analysis. Features: - Bilingual support (Chinese/English) - Modern glassmorphism design - Gradient backgrounds - Smooth animations - Professional pharmaceutical industry aesthetics """ import os import sys import tempfile from pathlib import Path from datetime import datetime from typing import Optional, Tuple, List, Dict import gradio as gr # Add project root to path for imports PROJECT_ROOT = Path(__file__).parent sys.path.insert(0, str(PROJECT_ROOT)) from schemas.canonical_schema import AnalysisRequest from layers.input_normalizer import InputNormalizer from layers.prompt_orchestrator import PromptOrchestrator from layers.model_invoker import ModelInvoker from layers.output_normalizer import OutputNormalizer from layers.llm_providers import get_available_providers, LLMProvider from report.generator import ReportGenerator from utils.molecule_renderer import MoleculeRenderer from utils.stability_report_formatter import StabilityReportFormatter # ============================================================================= # Bilingual Text System # ============================================================================= TEXTS = { "app_title": { "cn": "🧪 药物-辅料相容性分析系统", "en": "🧪 API-Excipient Compatibility Analyzer" }, "app_subtitle": { "cn": "专业制剂研发智能预测平台", "en": "Intelligent Formulation R&D Prediction Platform" }, "tab_analysis": { "cn": "📊 相容性分析", "en": "📊 Compatibility Analysis" }, "tab_settings": { "cn": "⚙️ 系统设置", "en": "⚙️ Settings" }, "tab_about": { "cn": "ℹ️ 关于", "en": "ℹ️ About" }, "input_section": { "cn": "📋 输入信息", "en": "📋 Input Information" }, "api_smiles_label": { "cn": "API SMILES 结构式", "en": "API SMILES Notation" }, "api_smiles_placeholder": { "cn": "请输入药物分子的SMILES结构式...\n例如: CC(=O)Nc1ccc(O)cc1 (对乙酰氨基酚)", "en": "Enter SMILES notation of the drug molecule...\nExample: CC(=O)Nc1ccc(O)cc1 (Acetaminophen)" }, "structure_preview": { "cn": "分子结构预览", "en": "Molecular Structure Preview" }, "properties": { "cn": "理化性质", "en": "Physicochemical Properties" }, "excipient_label": { "cn": "辅料信息", "en": "Excipient Information" }, "excipient_placeholder": { "cn": "请输入辅料名称...\n例如: 乳糖, 微晶纤维素", "en": "Enter excipient name...\nExample: Lactose, Microcrystalline Cellulose" }, "stability_label": { "cn": "稳定性数据 (可选)", "en": "Stability Data (Optional)" }, "stability_placeholder": { "cn": "如有稳定性试验数据,请在此描述...", "en": "Enter stability study data if available..." }, "file_upload": { "cn": "上传文件 (可选)", "en": "Upload Files (Optional)" }, "btn_clear": { "cn": "🗑️ 清空", "en": "🗑️ Clear" }, "btn_analyze": { "cn": "🔬 开始分析", "en": "🔬 Start Analysis" }, "btn_example": { "cn": "📌 加载示例", "en": "📌 Load Example" }, "output_section": { "cn": "📊 分析报告", "en": "📊 Analysis Report" }, "status_label": { "cn": "状态", "en": "Status" }, "status_waiting": { "cn": "等待输入...", "en": "Waiting for input..." }, "status_complete": { "cn": "分析完成!", "en": "Analysis complete!" }, "download_html": { "cn": "下载 HTML 报告", "en": "Download HTML Report" }, "download_pdf": { "cn": "下载 PDF 报告", "en": "Download PDF Report" }, "settings_title": { "cn": "🔑 LLM 提供商设置", "en": "🔑 LLM Provider Settings" }, "settings_desc": { "cn": "选择您的LLM提供商并输入API密钥。如不配置,系统将以占位符模式运行。", "en": "Select your LLM provider and enter API key. System runs in placeholder mode if not configured." }, "provider_label": { "cn": "选择 LLM 提供商", "en": "Select LLM Provider" }, "api_key_label": { "cn": "API 密钥", "en": "API Key" }, "api_key_placeholder": { "cn": "请输入您的API密钥...", "en": "Enter your API key..." }, "btn_apply": { "cn": "✓ 应用设置", "en": "✓ Apply Settings" }, "status_not_configured": { "cn": "⚠️ 未配置LLM提供商(占位符模式)", "en": "⚠️ LLM provider not configured (placeholder mode)" }, "status_configured": { "cn": "✓ 已配置", "en": "✓ Configured" }, "about_title": { "cn": "关于本系统", "en": "About This System" }, "report_placeholder": { "cn": "请在左侧输入药物和辅料信息,然后点击\"开始分析\"按钮", "en": "Enter drug and excipient information on the left, then click \"Start Analysis\"" }, "error_missing_input": { "cn": "请填写API和辅料信息", "en": "Please fill in API and excipient information" }, "smiles_invalid": { "cn": "无效的SMILES结构式", "en": "Invalid SMILES notation" }, "smiles_waiting": { "cn": "输入SMILES后自动渲染", "en": "Structure renders after SMILES input" }, "rdkit_not_installed": { "cn": "RDKit未安装,无法渲染分子结构", "en": "RDKit not installed, cannot render structure" }, "language": { "cn": "English", "en": "中文" }, "example_scenario": { "cn": "**示例场景:** 对乙酰氨基酚 + 乳糖相容性分析", "en": "**Example:** Acetaminophen + Lactose compatibility analysis" }, } # LLM Provider choices (bilingual) LLM_PROVIDERS = [ ("OpenAI (ChatGPT)", "openai"), ("Google Gemini", "gemini"), ("Anthropic Claude", "claude"), ("xAI Grok", "grok"), ("Moonshot Kimi", "kimi"), ("Deepseek", "deepseek"), ("智谱清言 (GLM)", "zhipu"), ] # ============================================================================= # Molecular Renderer # ============================================================================= mol_renderer = MoleculeRenderer() # ============================================================================= # Core Analysis Pipeline # ============================================================================= class CompatibilityAnalyzer: """Main analyzer orchestrating the full analysis pipeline. Now uses the dual-phase ProfessionalAnalyzer for: - Phase 1: Deep chemical reasoning - Phase 2: Professional report writing """ def __init__(self): try: from layers.professional_analyzer import ProfessionalAnalyzer self.professional_analyzer = ProfessionalAnalyzer() self.model_invoker = self.professional_analyzer.model_invoker self._use_professional = True except ImportError: # Fallback to old analyzer if professional_analyzer not available self.model_invoker = ModelInvoker() self.professional_analyzer = None self._use_professional = False print("Warning: ProfessionalAnalyzer not available, using legacy analyzer") def set_llm_provider(self, provider: str, api_key: str): if provider and api_key: self.model_invoker.set_provider(provider, api_key) def analyze( self, api_text: str, excipient_text: str, stability_text: str = "", uploaded_files: List[str] = None, progress_callback=None, structure_image: str = None, lang: str = "cn", ) -> Tuple[str, Optional[str]]: """Run the full analysis pipeline using dual-phase approach.""" try: progress_msgs = { "cn": { "parsing": "正在解析分子结构...", "groups": "正在识别反应活性基团...", "rendering": "正在生成分子结构图...", "phase1": "Phase 1: 深度机理推理中...", "phase2": "Phase 2: 专业报告撰写中...", "complete": "分析完成!", }, "en": { "parsing": "Parsing molecular structure...", "groups": "Identifying reactive groups...", "rendering": "Generating structure image...", "phase1": "Phase 1: Deep reasoning analysis...", "phase2": "Phase 2: Professional report writing...", "complete": "Analysis complete!", } } # Extract input smiles = api_text.strip() excipient_name = excipient_text.strip() goal = stability_text.strip() # Check if professional analyzer is available if not self._use_professional or self.professional_analyzer is None: return '
ProfessionalAnalyzer not available. Please ensure professional_analyzer.py is deployed.
', None # ================================================================= # BRANCH 1: Stability Data Analysis (File-Driven) # Trigger if: No SMILES provided, but files and goal are present # ================================================================= if not smiles and (uploaded_files and goal): def stability_progress(progress: float, msg: str): if progress_callback: progress_callback(progress, msg) # 🔬 Use ADVANCED scientific modeling mode result = self.professional_analyzer.analyze_stability_advanced( goal=goal, file_paths=uploaded_files, api_info=f"API Info Provided: {bool(smiles)}", # Minimal context if any excipient_info=excipient_name, progress_callback=stability_progress ) if not result["success"]: return f'
稳定性分析失败: {result.get("error")}
', None # Format using PROFESSIONAL scientific report template html_report = StabilityReportFormatter.format_professional_report( analysis_result=result, goal=goal ) return html_report, None # ================================================================= # BRANCH 2: Compatibility Analysis (Structure-Driven) # Trigger if: SMILES is provided # ================================================================= elif smiles: # API Name determination api_name = smiles[:30] + "..." if len(smiles) > 30 else smiles # Run the professional dual-phase analysis def wrapped_progress(progress: float, msg: str): if progress_callback: if progress <= 0.2: display_msg = progress_msgs[lang]["parsing"] elif progress <= 0.3: display_msg = progress_msgs[lang]["phase1"] elif progress <= 0.7: display_msg = progress_msgs[lang]["phase1"] elif progress < 1.0: display_msg = progress_msgs[lang]["phase2"] else: display_msg = progress_msgs[lang]["complete"] progress_callback(progress, display_msg) result = self.professional_analyzer.analyze( smiles=smiles, excipient_name=excipient_name, api_name=api_name, progress_callback=wrapped_progress, ) if not result["success"]: error_msg = result.get("error", "Unknown error") return f'
相容性分析出错: {error_msg}
', None # Generate HTML report html_report = self.professional_analyzer.format_html_report( analysis_result=result, api_name=api_name, excipient_name=excipient_name, ) else: return '
请输入 API SMILES 结构(用于相容性分析)或 上传数据文件并填写分析目标(用于稳定性分析)。
', None # Add the structure image if we have one if result.get("structure_image") and "[结构图待生成]" in html_report: html_report = html_report.replace( "[结构图待生成]", f'Structure' ) # Try to generate PDF pdf_path = None try: from report.generator import ReportGenerator from schemas.canonical_schema import AnalysisResult # Create minimal AnalysisResult for PDF generation # Note: PDF generation may fail if WeasyPrint is not available with tempfile.NamedTemporaryFile(suffix='.html', delete=False, mode='w', encoding='utf-8') as tmp_html: tmp_html.write(html_report) tmp_html_path = tmp_html.name # Try WeasyPrint directly try: from weasyprint import HTML with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False, prefix=f"report_{result['report_id']}_") as tmp_pdf: HTML(string=html_report).write_pdf(tmp_pdf.name) pdf_path = tmp_pdf.name except ImportError: print("WeasyPrint not available, PDF generation skipped") except Exception as pdf_err: print(f"PDF generation failed: {pdf_err}") except Exception as e: print(f"PDF preparation failed: {e}") if progress_callback: progress_callback(1.0, progress_msgs[lang]["complete"]) return html_report, pdf_path except Exception as e: import traceback traceback.print_exc() error_msg = "分析出错" if lang == "cn" else "Analysis error" return f'
{error_msg}: {str(e)}
', None # ============================================================================= # Helper Functions # ============================================================================= def render_molecule(smiles: str, lang: str) -> Tuple[str, str, str]: """Render molecular structure.""" if not smiles or not smiles.strip(): msg = TEXTS["smiles_waiting"][lang] return f"
{msg}
", "", "" smiles = smiles.strip() if not mol_renderer.is_available: msg = TEXTS["rdkit_not_installed"][lang] return f"
{msg}
", "", "" svg = mol_renderer.render_2d_svg(smiles, 320, 240) if not svg: msg = TEXTS["smiles_invalid"][lang] return f"
{msg}
", "", "" props = mol_renderer.calculate_properties(smiles) props_html = "" if props: labels = { "cn": ["分子量", "LogP", "H-供体", "H-受体", "TPSA", "可旋转键"], "en": ["MW", "LogP", "HBD", "HBA", "TPSA", "RotBonds"] } props_html = f"""
{labels[lang][0]}{props['molecular_weight']}
{labels[lang][1]}{props['logp']}
{labels[lang][2]}{props['hbd']}
{labels[lang][3]}{props['hba']}
{labels[lang][4]}{props['tpsa']}
{labels[lang][5]}{props['rotatable_bonds']}
""" return f"
{svg}
", props_html, mol_renderer.get_data_uri(smiles, 320, 240) or "" def get_text(key: str, lang: str) -> str: """Get localized text.""" return TEXTS.get(key, {}).get(lang, key) # ============================================================================= # Professional CSS # ============================================================================= CUSTOM_CSS = """ /* ======================================== Global Styles & Variables ======================================== */ :root { --primary: #1a5f7a; --primary-light: #2d8bb8; --primary-dark: #134a5f; --accent: #57c5b6; --accent-light: #7dd3c8; --bg-dark: #0f1419; --bg-card: rgba(255, 255, 255, 0.95); --text-dark: #1a1a2e; --text-light: #6b7280; --border-light: rgba(0, 0, 0, 0.08); --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow-md: 0 4px 20px rgba(0, 0, 0, 0.12); --shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.16); --gradient-primary: linear-gradient(135deg, #1a5f7a 0%, #2d8bb8 50%, #57c5b6 100%); --gradient-accent: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --radius-sm: 8px; --radius-md: 12px; --radius-lg: 20px; } .gradio-container { font-family: 'Inter', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, sans-serif !important; background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%) !important; min-height: 100vh; } /* ======================================== Header Styles ======================================== */ .app-header { background: var(--gradient-primary); padding: 40px 30px; border-radius: var(--radius-lg); margin-bottom: 24px; text-align: center; box-shadow: var(--shadow-lg); position: relative; overflow: hidden; } .app-header::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%); animation: shimmer 15s infinite linear; } @keyframes shimmer { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .app-header h1 { margin: 0; font-size: 32px; font-weight: 700; color: white; text-shadow: 0 2px 10px rgba(0,0,0,0.2); position: relative; z-index: 1; } .app-header p { margin: 12px 0 0; font-size: 16px; color: rgba(255,255,255,0.9); font-weight: 400; position: relative; z-index: 1; } .lang-toggle { position: absolute; top: 20px; right: 20px; z-index: 10; } .lang-toggle button { background: rgba(255,255,255,0.2) !important; border: 1px solid rgba(255,255,255,0.3) !important; color: white !important; padding: 8px 16px !important; border-radius: 20px !important; font-weight: 500 !important; backdrop-filter: blur(10px); transition: all 0.3s ease !important; } .lang-toggle button:hover { background: rgba(255,255,255,0.3) !important; transform: translateY(-2px); } /* ======================================== Card Styles ======================================== */ .card { background: var(--bg-card); border-radius: var(--radius-md); padding: 24px; box-shadow: var(--shadow-md); border: 1px solid var(--border-light); backdrop-filter: blur(10px); } .card-header { font-size: 18px; font-weight: 600; color: var(--primary); margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid var(--accent); display: flex; align-items: center; gap: 10px; } /* ======================================== Molecule Display ======================================== */ .mol-display { background: linear-gradient(135deg, #f8fafc 0%, #e8f4f8 100%); border-radius: var(--radius-md); padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 260px; border: 1px solid var(--border-light); } .mol-placeholder, .mol-error { color: var(--text-light); font-size: 14px; text-align: center; padding: 40px; } .mol-error { color: #dc3545; } .props-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px; } .prop-item { background: linear-gradient(135deg, #f0f7ff 0%, #e8f4f8 100%); padding: 12px; border-radius: var(--radius-sm); text-align: center; border: 1px solid rgba(45, 139, 184, 0.1); } .prop-label { display: block; font-size: 11px; color: var(--text-light); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } .prop-value { display: block; font-size: 16px; font-weight: 600; color: var(--primary); } /* ======================================== Button Styles ======================================== */ .btn-primary { background: var(--gradient-primary) !important; color: white !important; border: none !important; padding: 14px 28px !important; border-radius: var(--radius-sm) !important; font-weight: 600 !important; font-size: 15px !important; box-shadow: var(--shadow-sm) !important; transition: all 0.3s ease !important; } .btn-primary:hover { transform: translateY(-3px) !important; box-shadow: var(--shadow-md) !important; } .btn-secondary { background: #f1f5f9 !important; color: var(--text-dark) !important; border: 1px solid var(--border-light) !important; padding: 14px 28px !important; border-radius: var(--radius-sm) !important; font-weight: 500 !important; transition: all 0.3s ease !important; } .btn-secondary:hover { background: #e2e8f0 !important; } .btn-example { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important; color: #92400e !important; border: 1px solid #fcd34d !important; padding: 10px 20px !important; border-radius: var(--radius-sm) !important; font-weight: 500 !important; } /* ======================================== Input Styles ======================================== */ .input-field textarea, .input-field input { border-radius: var(--radius-sm) !important; border: 1px solid #e2e8f0 !important; padding: 14px !important; font-size: 14px !important; transition: all 0.3s ease !important; } .input-field textarea:focus, .input-field input:focus { border-color: var(--primary-light) !important; box-shadow: 0 0 0 3px rgba(45, 139, 184, 0.1) !important; } /* ======================================== Report Display ======================================== */ .report-container { background: white; border-radius: var(--radius-md); border: 1px solid var(--border-light); min-height: 500px; box-shadow: var(--shadow-sm); overflow: hidden; } .report-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px; color: var(--text-light); text-align: center; padding: 40px; } .report-placeholder svg { width: 80px; height: 80px; margin-bottom: 20px; opacity: 0.5; } /* ======================================== Settings Panel ======================================== */ .settings-panel { background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border: 1px solid #fcd34d; border-radius: var(--radius-md); padding: 24px; } .settings-panel .label { color: #92400e; font-weight: 600; } /* ======================================== Status Badge ======================================== */ .status-badge { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; } .status-configured { background: #d1fae5; color: #065f46; } .status-pending { background: #fef3c7; color: #92400e; } /* ======================================== Animations ======================================== */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.5s ease forwards; } /* ======================================== Responsive Design ======================================== */ @media (max-width: 768px) { .app-header h1 { font-size: 24px; } .props-grid { grid-template-columns: repeat(2, 1fr); } } """ # ============================================================================= # Gradio Interface # ============================================================================= def create_interface() -> gr.Blocks: """Create the professional bilingual Gradio interface.""" analyzer = CompatibilityAnalyzer() with gr.Blocks( title="Drug-Excipient Compatibility Analyzer", css=CUSTOM_CSS, theme=gr.themes.Soft( primary_hue="blue", secondary_hue="cyan", font=["Inter", "Microsoft YaHei", "sans-serif"], ) ) as demo: # State variables lang_state = gr.State("cn") structure_state = gr.State("") mode_state = gr.State("compatibility") # 'compatibility' or 'stability' # =========== Header =========== with gr.Row(): with gr.Column(): header_html = gr.HTML("""

🧪 药物-辅料相容性分析系统

专业制剂研发智能预测平台

""") # Language toggle with gr.Row(): with gr.Column(scale=4): pass with gr.Column(scale=1): lang_btn = gr.Button("English", size="sm", elem_classes=["lang-toggle"]) # =========== Main Tabs =========== with gr.Tabs() as main_tabs: # ---------- Analysis Tab ---------- with gr.TabItem("📊 智能分析 (Analysis)", id="tab_analysis") as analysis_tab: with gr.Row(equal_height=False): # Left: Input with gr.Column(scale=2): input_header = gr.Markdown("### 📋 输入信息") # Use Tabs for Mode Selection with gr.Tabs() as input_tabs: # --- Mode 1: Compatibility --- with gr.TabItem("相容性预测 (Compatibility)", id="input_tab_compat"): gr.Markdown("##### 🧪 基于API结构预测与辅料的反应") smiles_input = gr.Textbox( label="API SMILES 结构式", placeholder="请输入药物分子的SMILES结构式...\n例如: CC(=O)Nc1ccc(O)cc1", lines=2, elem_classes=["input-field"], ) structure_html = gr.HTML( value="
输入SMILES后自动渲染
", ) props_html = gr.HTML(value="") excipient_input = gr.Textbox( label="辅料名称", placeholder="请输入辅料名称...\n例如: 乳糖", lines=1, elem_classes=["input-field"], ) with gr.Row(): example_compat_btn = gr.Button("📌 加载相容性示例", elem_classes=["btn-example"]) # --- Mode 2: Stability --- with gr.TabItem("稳定性分析 (Stability)", id="input_tab_stability"): gr.Markdown("##### 📈 基于实验数据评估趋势与效期") file_upload = gr.File( label="上传稳定性数据 (Excel/CSV/PDF/Word)", file_count="multiple", file_types=[".xlsx", ".xls", ".csv", ".txt", ".pdf", ".docx", ".doc", ".pptx", ".ppt"], ) stability_goal_input = gr.Textbox( label="分析目标与判断标准", placeholder="例如:基于长期试验数据(0,3,6月),评估杂质A增长趋势,判断是否满足24个月货架期要求...", lines=4, elem_classes=["input-field"], ) with gr.Row(): example_stab_btn = gr.Button("📌 加载稳定性示例", elem_classes=["btn-example"]) # Actions with gr.Row(elem_classes=["action-bar"]): clear_btn = gr.Button("🗑️ 清空重置", elem_classes=["btn-secondary"]) analyze_btn = gr.Button("🔬 开始专业分析", elem_classes=["btn-primary"]) # Right: Output with gr.Column(scale=3): output_header = gr.Markdown("### 📊 分析报告") status_text = gr.Textbox( label="当前状态", value="系统就绪,等待下达指令...", interactive=False, ) report_output = gr.HTML( value="""

请在左侧选择 [相容性] 或 [稳定性] 模式进行分析

""", elem_classes=["report-container"], ) with gr.Row(): download_html = gr.File(label="下载 HTML", visible=False) download_pdf = gr.File(label="下载 PDF", visible=False) # ---------- Settings Tab ---------- with gr.TabItem("⚙️ 系统设置", id="tab_settings") as settings_tab: settings_title = gr.Markdown("### 🔑 LLM 提供商设置") settings_desc = gr.Markdown("选择您的LLM提供商并输入API密钥。如不配置,系统将以占位符模式运行。") with gr.Group(elem_classes=["settings-panel"]): provider_dropdown = gr.Dropdown( label="选择 LLM 提供商", choices=LLM_PROVIDERS, value=None, ) api_key_input = gr.Textbox( label="API 密钥", placeholder="请输入您的API密钥...", type="password", ) provider_status = gr.Textbox( label="状态", value="⚠️ 未配置LLM提供商(占位符模式)", interactive=False, ) apply_btn = gr.Button("✓ 应用设置", elem_classes=["btn-primary"]) gr.Markdown(""" ### 📝 API密钥获取 | 提供商 | 网址 | |--------|------| | OpenAI | platform.openai.com | | Gemini | aistudio.google.com | | Claude | console.anthropic.com | | Grok | console.x.ai | | Kimi | platform.moonshot.cn | | Deepseek | platform.deepseek.com | | 智谱 | open.bigmodel.cn | """) # ---------- About Tab ---------- with gr.TabItem("ℹ️ 关于", id="tab_about"): gr.Markdown(""" ## 关于本系统 | About This System 本系统是一个专业的药物-辅料相容性分析平台,旨在帮助制剂研发人员快速评估药物与辅料之间的潜在相互作用风险。 This is a professional API-Excipient compatibility analysis platform designed to help formulation R&D scientists quickly assess potential interaction risks between drugs and excipients. --- ### 主要功能 | Key Features - **相容性预测** | Compatibility Prediction: 基于SMILES结构自动识别活性基团和反应风险 - **稳定性分析** | Stability Analysis: 基于实验数据(Excel/CSV)进行趋势分析与效期评估 - **专业报告** | Professional Report: 生成双语专业研发报告 (QbD/ICH准则) - **多模型支持** | Multi-LLM Support: 支持7种主流大语言模型 --- ### 技术栈 | Tech Stack - **Frontend**: Gradio with custom CSS - **Backend**: Python with modular layer architecture - **Chemistry**: RDKit for molecular rendering - **AI**: Multi-provider LLM integration --- ### 免责声明 | Disclaimer 本系统生成的分析报告仅供研发参考。相容性结论需通过实验验证(如加速稳定性试验)进行最终确认。 Analysis reports are for R&D reference only. Compatibility conclusions should be confirmed through experimental verification (e.g., accelerated stability testing). """) # =========== Event Handlers =========== def toggle_language(current_lang): new_lang = "en" if current_lang == "cn" else "cn" # Update header header = f"""

{get_text('app_title', new_lang)}

{get_text('app_subtitle', new_lang)}

""" # Return all updated components return ( new_lang, header, get_text('language', new_lang), f"### {get_text('input_section', new_lang)}", f"### {get_text('output_section', new_lang)}", get_text('status_waiting', new_lang), f"### {get_text('settings_title', new_lang)}", get_text('settings_desc', new_lang), get_text('status_not_configured', new_lang), get_text('example_scenario', new_lang), ) def update_structure(smiles, lang): svg, props, uri = render_molecule(smiles, lang) return svg, props, uri def apply_settings(provider, api_key, lang): if provider and api_key: analyzer.set_llm_provider(provider, api_key) name = dict(LLM_PROVIDERS).get(provider, provider) return f"✓ {get_text('status_configured', lang)} {name}" return get_text('status_not_configured', lang) def run_analysis(smiles, excipient, stability_goal, files, struct_img, lang, progress=gr.Progress()): # Validation Logic: Either SMILES+Excipient OR Files+Goal must be present has_compat_input = bool(smiles and excipient) has_stability_input = bool(files and stability_goal) if not has_compat_input and not has_stability_input: return ( f"
⚠️ 请至少完善一种分析模式的输入信息:
1. 相容性预测:需填写 SMILES 和 辅料名称
2. 稳定性分析:需上传数据文件 并 填写分析目标
", "输入不完整", gr.update(visible=False), gr.update(visible=False), ) file_paths = [f.name for f in files] if files else [] html, pdf = analyzer.analyze( api_text=smiles if smiles else "", excipient_text=excipient if excipient else "", stability_text=stability_goal if stability_goal else "", uploaded_files=file_paths, progress_callback=lambda v, m: progress(v, desc=m), structure_image=struct_img, lang=lang, ) html_file = None try: with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: f.write(html) html_file = f.name except: pass return ( html, get_text('status_complete', lang), gr.update(value=html_file, visible=html_file is not None), gr.update(value=pdf, visible=pdf is not None), ) def load_example_compat(): return "CC(=O)Nc1ccc(O)cc1", "乳糖", None, "" def load_example_stability(): # Create a dummy CSV for example import os dummy_path = "example_stability_data.csv" with open(dummy_path, "w", encoding="utf-8") as f: f.write("Time(Month),Assay(%),Impurity A(%),pH\n0,99.8,0.05,6.5\n3,99.2,0.12,6.4\n6,98.5,0.25,6.2") goal = "评估杂质A增长趋势,判断是否满足24个月效期标准(限度<0.5%)" return "", "", [dummy_path], goal def clear_all(lang): placeholder = f"
{get_text('smiles_waiting', lang)}
" return "", "", "", None, placeholder, "", "" # Wire events lang_btn.click( fn=toggle_language, inputs=[lang_state], outputs=[ lang_state, header_html, lang_btn, input_header, output_header, status_text, settings_title, settings_desc, provider_status, gr.Markdown() # dummy ], ) smiles_input.change( fn=update_structure, inputs=[smiles_input, lang_state], outputs=[structure_html, props_html, structure_state], ) apply_btn.click( fn=apply_settings, inputs=[provider_dropdown, api_key_input, lang_state], outputs=[provider_status], ) analyze_btn.click( fn=run_analysis, inputs=[smiles_input, excipient_input, stability_goal_input, file_upload, structure_state, lang_state], outputs=[report_output, status_text, download_html, download_pdf], ) example_compat_btn.click( fn=load_example_compat, outputs=[smiles_input, excipient_input, file_upload, stability_goal_input], ) example_stab_btn.click( fn=load_example_stability, outputs=[smiles_input, excipient_input, file_upload, stability_goal_input], ) clear_btn.click( fn=clear_all, inputs=[lang_state], outputs=[smiles_input, excipient_input, stability_goal_input, file_upload, structure_html, props_html, structure_state], ) return demo # ============================================================================= # Main # ============================================================================= def main(): demo = create_interface() demo.launch( server_name="0.0.0.0", server_port=7860, share=False, show_error=True, ) if __name__ == "__main__": main()