""" Drug Stability & Compatibility Analysis Platform ================================================= Simplified LLM-Driven Architecture Features: - Smart routing: Auto-detect analysis type from input - Compatibility Analysis: SMILES + Excipient → ProfessionalAnalyzer - Stability Analysis: File + Goal → LLM-driven analysis - Universal Analysis: Natural language questions → LLM response - User Authentication: Email registration, admin dashboard Design Principles: - Low barrier: Users describe what they want, system figures out how - LLM-driven: LLM understands intent, chooses methods, generates reports - Preserve expertise: Professional pharmaceutical knowledge in prompts """ import os import sys import tempfile from pathlib import Path from datetime import datetime from typing import Optional, Tuple, List, Dict, Any import streamlit as st # Add project root to path PROJECT_ROOT = Path(__file__).parent sys.path.insert(0, str(PROJECT_ROOT)) # Import auth module try: from utils.auth import ( register_user, login_user, is_admin, get_all_users, get_default_llm_config, set_default_llm_config ) AUTH_AVAILABLE = True except ImportError: AUTH_AVAILABLE = False # ============================================================================= # Page Config # ============================================================================= st.set_page_config( page_title="Pharma K 药物制剂相容性与稳定性分析专家系统", page_icon="🧪", layout="wide", initial_sidebar_state="expanded" ) # ============================================================================= # Custom CSS (Nordic Minimalism) # ============================================================================= def load_css(): css_path = Path(__file__).parent / "assets" / "style.css" if css_path.exists(): with open(css_path, "r", encoding="utf-8") as f: st.markdown(f"", unsafe_allow_html=True) load_css() # ============================================================================= # Initialize Components (Lazy Loading) # ============================================================================= @st.cache_resource def get_model_invoker(): """Get or create ModelInvoker instance.""" from layers.model_invoker import ModelInvoker return ModelInvoker() @st.cache_resource def get_professional_analyzer(): """Get or create ProfessionalAnalyzer for compatibility analysis.""" try: from layers.professional_analyzer import ProfessionalAnalyzer return ProfessionalAnalyzer() except ImportError as e: st.warning(f"ProfessionalAnalyzer not available: {e}") return None @st.cache_resource def get_molecule_renderer(): """Get or create MoleculeRenderer.""" try: from utils.molecule_renderer import MoleculeRenderer return MoleculeRenderer() except ImportError: return None # ============================================================================= # Smart Router # ============================================================================= def detect_analysis_type( smiles: str, excipient: str, goal: str, files: List ) -> str: """ Automatically detect which analysis mode to use. Returns: "compatibility" | "stability" | "general" | "none" """ has_smiles = bool(smiles and smiles.strip()) has_excipient = bool(excipient and excipient.strip()) has_files = bool(files) has_goal = bool(goal and goal.strip()) # Compatibility: SMILES + Excipient if has_smiles and has_excipient: return "compatibility" # Stability: Files + Goal if has_files and has_goal: return "stability" # General: Just a question if has_goal and not has_files and not has_smiles: return "general" return "none" # ============================================================================= # Report Branding Wrapper # ============================================================================= def strip_outer_containers(content: str) -> str: """ Remove outer HTML containers from LLM output to avoid nested boxes. IMPORTANT: Be careful not to remove actual content! """ import re # Remove and wrappers if present content = re.sub(r']*>', '', content, flags=re.IGNORECASE) content = re.sub(r']*>', '', content, flags=re.IGNORECASE) content = re.sub(r'', '', content, flags=re.IGNORECASE) content = re.sub(r'
.*?', '', content, flags=re.IGNORECASE | re.DOTALL) content = re.sub(r']*>', '', content, flags=re.IGNORECASE) content = re.sub(r'', '', content, flags=re.IGNORECASE) # Remove only specific header-style divs (short ones, not content-heavy) # Only match if the div is short (less than 200 chars including tags) def remove_short_header_divs(match): full_match = match.group(0) if len(full_match) < 300: # Only remove if it's a short header return '' return full_match # Keep longer content divs content = re.sub( r'