"""
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'
请输入 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''
)
# 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"