#!/usr/bin/env python3 """ PatSnap Pharma Intelligence — Hugging Face Space Demo Product-grade multi-module AI agent for life science intelligence. Modules: - Agent Chat (natural language → tool orchestration → report) - Target Intelligence (靶点全景) - Drug Exploration (药物管线) - Disease Investigation (疾病格局) - Company Profiling (公司分析) - Clinical Trials (临床试验) """ import os, json, asyncio, time, re from collections import Counter from datetime import datetime from typing import Optional, Dict, List, Tuple import gradio as gr # ============================================================================= # CONFIGURATION # ============================================================================= API_KEY = os.getenv("PATSNAP_API_KEY", "") HF_TOKEN = os.getenv("HF_TOKEN", "") # optional: enable LLM agent mode SERVER_URL = f"https://connect.patsnap.com/096456/logic-mcp?apikey={API_KEY}" # Module definitions MODULES = { "target": {"icon": "🎯", "label": "Target Intelligence", "label_cn": "靶点全景", "tool": "ls_drug_search", "entity": "target", "desc": "Analyze any biomedical target — biology, drugs, pipeline, trials."}, "drug": {"icon": "💊", "label": "Drug Exploration", "label_cn": "药物管线", "tool": "ls_drug_search", "entity": "drug", "desc": "Search drugs by target, disease, mechanism, or company."}, "disease": {"icon": "🏥", "label": "Disease Investigation", "label_cn": "疾病格局", "tool": "ls_drug_search", "entity": "disease", "desc": "Understand disease landscape — epidemiology, treatments, pipeline."}, "company": {"icon": "🏢", "label": "Company Profiling", "label_cn": "公司分析", "tool": "ls_organization_pipeline_fetch", "entity": "company", "desc": "Profile pharma companies — pipeline, deals, therapeutic focus."}, "trial": {"icon": "🧪", "label": "Clinical Trials", "label_cn": "临床试验", "tool": "ls_clinical_trial_search", "entity": "trial", "desc": "Explore clinical trials by target, disease, phase, or sponsor."}, } # Language: 'en' or 'zh' DEFAULT_LANG = "en" # ============================================================================= # MOCK DATA — Comprehensive, module-specific # ============================================================================= MOCK_TARGETS = { "EGFR": { "name": "EGFR", "full_name": "Epidermal Growth Factor Receptor", "family": "ErbB/HER receptor tyrosine kinase", "class": "Kinase", "indication_count": 12, "drug_count": 28, "pdb_ids": ["1M17", "1XKK", "2J5F"], "pathways": ["RAS/RAF/MEK/ERK", "PI3K/AKT/mTOR", "JAK/STAT"], "mutation_hotspots": ["L858R (exon 21)", "exon 19 deletion", "T790M (exon 20)", "C797S (exon 20)"], "approved_drugs": [ {"name": "Osimertinib", "type": "Small molecule", "gen": "3rd-gen TKI", "year": 2015, "indications": ["NSCLC (T790M+)", "NSCLC (1L EGFRm)", "NSCLC (adjuvant)"], "company": "AstraZeneca"}, {"name": "Gefitinib", "type": "Small molecule", "gen": "1st-gen TKI", "year": 2003, "indications": ["NSCLC (EGFRm)"], "company": "AstraZeneca"}, {"name": "Erlotinib", "type": "Small molecule", "gen": "1st-gen TKI", "year": 2004, "indications": ["NSCLC", "Pancreatic"], "company": "Roche/Genentech"}, {"name": "Afatinib", "type": "Small molecule", "gen": "2nd-gen TKI", "year": 2013, "indications": ["NSCLC (EGFRm)"], "company": "Boehringer Ingelheim"}, {"name": "Dacomitinib", "type": "Small molecule", "gen": "2nd-gen TKI", "year": 2018, "indications": ["NSCLC (EGFRm)"], "company": "Pfizer"}, {"name": "Cetuximab", "type": "Monoclonal antibody", "gen": "mAb", "year": 2004, "indications": ["CRC", "HNSCC"], "company": "Merck KGaA / BMS"}, {"name": "Panitumumab", "type": "Monoclonal antibody", "gen": "mAb", "year": 2006, "indications": ["CRC"], "company": "Amgen"}, {"name": "Amivantamab", "type": "Bispecific antibody", "gen": "BsAb", "year": 2021, "indications": ["NSCLC (ex20ins)"], "company": "Janssen"}, {"name": "Patritumab deruxtecan", "type": "ADC", "gen": "ADC (HER3)", "year": 2024, "indications": ["NSCLC (post-TKI)"], "company": "Daiichi Sankyo / Merck"}, ], "pipeline_summary": { "phase_3": 45, "phase_2": 120, "phase_1": 85, "preclinical": 200, "hot_topics": ["4th-gen TKIs (C797S)", "Bispecific ADCs", "PROTAC degraders", "Combination with immunotherapy", "Brain-penetrant TKIs"], }, "competitive_landscape": "Highly competitive — every major pharma has an EGFR asset. " "Innovation is focused on resistance mechanisms and next-gen modalities.", }, "HER2": { "name": "HER2", "full_name": "Human Epidermal Growth Factor Receptor 2", "family": "ErbB/HER receptor tyrosine kinase", "class": "Kinase", "indication_count": 5, "drug_count": 15, "pdb_ids": ["1N8Z", "3PP0"], "pathways": ["RAS/RAF/MEK/ERK", "PI3K/AKT/mTOR"], "mutation_hotspots": ["Amplification (breast/gastric)", "Exon 20 mutations (NSCLC)"], "approved_drugs": [ {"name": "Trastuzumab", "type": "Monoclonal antibody", "gen": "mAb", "year": 1998, "indications": ["HER2+ Breast Cancer", "HER2+ Gastric Cancer"], "company": "Roche"}, {"name": "Trastuzumab deruxtecan", "type": "ADC", "gen": "ADC", "year": 2019, "indications": ["HER2+ Breast Cancer", "HER2-low Breast Cancer", "HER2+ Gastric Cancer", "HER2-mutant NSCLC"], "company": "Daiichi Sankyo / AstraZeneca"}, {"name": "Pertuzumab", "type": "Monoclonal antibody", "gen": "mAb", "year": 2012, "indications": ["HER2+ Breast Cancer"], "company": "Roche"}, {"name": "Lapatinib", "type": "Small molecule", "gen": "TKI", "year": 2007, "indications": ["HER2+ Breast Cancer"], "company": "Novartis"}, {"name": "Tucatinib", "type": "Small molecule", "gen": "TKI", "year": 2020, "indications": ["HER2+ Breast Cancer (CNS mets)"], "company": "Seagen / Merck"}, ], "pipeline_summary": { "phase_3": 25, "phase_2": 80, "phase_1": 55, "preclinical": 130, "hot_topics": ["HER2-low targeting", "Bispecific ADCs", "Brain metastasis"], }, "competitive_landscape": "HER2 ADC space is the current battleground. Enhertu dominates; " "competitors focus on differentiation via payload, DAR, or epitope.", }, "PD-L1": { "name": "PD-L1", "full_name": "Programmed Death-Ligand 1", "family": "B7 immune checkpoint", "class": "Immune checkpoint ligand", "indication_count": 20, "drug_count": 20, "approved_drugs": [ {"name": "Atezolizumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb", "year": 2016, "indications": ["NSCLC", "SCLC", "Urothelial", "HCC"], "company": "Roche"}, {"name": "Durvalumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb", "year": 2017, "indications": ["NSCLC (Stage III)", "SCLC", "Biliary Tract"], "company": "AstraZeneca"}, {"name": "Avelumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb", "year": 2017, "indications": ["Merkel Cell", "Urothelial", "RCC"], "company": "Merck KGaA / Pfizer"}, ], "pipeline_summary": {"phase_3": 35, "phase_2": 60, "phase_1": 40, "preclinical": 100}, "competitive_landscape": "PD-L1 is a companion to PD-1 — the focus is on combination strategies " "and predictive biomarker development.", }, } MOCK_COMPANIES = { "Roche": { "name": "Roche", "ticker": "ROG.SW", "headquarters": "Basel, Switzerland", "employees": "~100,000", "2024_revenue": "$65.4B", "therapeutic_areas": ["Oncology", "Neuroscience", "Ophthalmology", "Immunology", "Infectious Disease"], "flagship_drugs": [ {"name": "Trastuzumab (Herceptin)", "target": "HER2", "sales": "$3.2B", "phase": "Approved"}, {"name": "Atezolizumab (Tecentriq)", "target": "PD-L1", "sales": "$4.8B", "phase": "Approved"}, {"name": "Bevacizumab (Avastin)", "target": "VEGF", "sales": "$2.1B", "phase": "Approved"}, {"name": "Trastuzumab deruxtecan (co-developed)", "target": "HER2", "sales": "$3.5B", "phase": "Approved"}, ], "pipeline_count": {"approved": 28, "phase_3": 15, "phase_2": 35, "phase_1": 22}, "recent_deals": [ "Acquired Carmot Therapeutics (obesity) — $2.7B upfront (2023)", "Acquired Telavant (IBD) — $7.1B (2023)", ], "strategy": "Roche combines strong internal R&D with strategic bolt-on acquisitions. " "Oncology remains the core, with growing investment in immunology and metabolic disease.", }, "AstraZeneca": { "name": "AstraZeneca", "ticker": "AZN.L", "headquarters": "Cambridge, UK", "employees": "~90,000", "2024_revenue": "$54.1B", "therapeutic_areas": ["Oncology", "CVRM", "Respiratory & Immunology", "Rare Disease"], "flagship_drugs": [ {"name": "Osimertinib (Tagrisso)", "target": "EGFR", "sales": "$6.8B", "phase": "Approved"}, {"name": "Durvalumab (Imfinzi)", "target": "PD-L1", "sales": "$4.5B", "phase": "Approved"}, {"name": "Trastuzumab deruxtecan (co-developed)", "target": "HER2", "sales": "$3.5B", "phase": "Approved"}, {"name": "Dapagliflozin (Farxiga)", "target": "SGLT2", "sales": "$7.1B", "phase": "Approved"}, ], "pipeline_count": {"approved": 22, "phase_3": 12, "phase_2": 28, "phase_1": 18}, "recent_deals": [ "Acquired Gracell Biotechnologies (CAR-T) — $1.2B (2023)", "Acquired Icosavax (RSV/hMPV vaccine) — $1.1B (2023)", "Acquired Fusion Pharmaceuticals (radiopharma) — $2.4B (2024)", ], "strategy": "AZ's oncology portfolio is anchored by Tagrisso, Imfinzi, and Enhertu. " "Actively expanding into cell therapy, radiopharmaceuticals, and ADCs.", }, } MOCK_DISEASES = { "NSCLC": { "name": "Non-Small Cell Lung Cancer", "global_incidence": "~2.2M new cases/year (2024)", "mortality": "~1.8M deaths/year", "5yr_survival": "Stage I: 65%, Stage IV: 8%", "major_mutations": ["EGFR (15-20%)", "KRAS (25-30%)", "ALK (3-5%)", "ROS1 (1-2%)", "BRAF (1-3%)", "MET exon 14 (3%)", "RET (1-2%)"], "approved_drugs_count": 45, "drug_classes": ["TKIs (1st/2nd/3rd gen)", "Immune checkpoint inhibitors", "ADCs", "Bispecific antibodies", "Chemotherapy"], "key_drugs": [ {"name": "Osimertinib", "target": "EGFR", "setting": "1L EGFRm ± adjuvant"}, {"name": "Pembrolizumab", "target": "PD-1", "setting": "1L PD-L1 ≥50% ± chemo"}, {"name": "Amivantamab", "target": "EGFR/MET", "setting": "ex20ins"}, {"name": "Sotorasib", "target": "KRAS G12C", "setting": "2L+"}, {"name": "Lorlatinib", "target": "ALK", "setting": "1L ALK+"}, ], "market_size": "$32B (2024), projected $48B by 2030", "pipeline": {"phase_3": 85, "phase_2": 150, "phase_1": 100}, "key_trends": ["Perioperative immunotherapy", "MRD-guided adjuvant therapy", "Antibody-drug conjugates expanding", "Bispecifics entering 1L"], }, "Breast Cancer": { "name": "Breast Cancer", "global_incidence": "~2.3M new cases/year (2024)", "mortality": "~685K deaths/year", "subtypes": ["HR+/HER2- (70%)", "HER2+ (15-20%)", "TNBC (10-15%)"], "approved_drugs_count": 52, "key_drugs": [ {"name": "Trastuzumab deruxtecan", "target": "HER2", "setting": "HER2+ and HER2-low"}, {"name": "Sacituzumab govitecan", "target": "Trop-2", "setting": "TNBC, HR+/HER2-"}, {"name": "Palbociclib", "target": "CDK4/6", "setting": "HR+/HER2- 1L"}, {"name": "Olaparib", "target": "PARP", "setting": "BRCA1/2-mutated"}, ], "market_size": "$28B (2024)", "key_trends": ["CDK4/6 moving to adjuvant", "ADCs dominating HER2 space", "Immunotherapy for TNBC", "Oral SERDs entering market"], }, } MOCK_DRUG_SEARCH = { # Returned by any drug search; keyed by target/disease "default": [ {"name": "Osimertinib", "target": "EGFR", "type": "Small molecule", "highest_phase": "Approved", "first_approved": "2015-11-13", "indications": ["NSCLC"], "company": "AstraZeneca"}, {"name": "Gefitinib", "target": "EGFR", "type": "Small molecule", "highest_phase": "Approved", "first_approved": "2003-05-05", "indications": ["NSCLC"], "company": "AstraZeneca"}, {"name": "Cetuximab", "target": "EGFR", "type": "Monoclonal antibody", "highest_phase": "Approved", "first_approved": "2004-02-12", "indications": ["CRC", "HNSCC"], "company": "Merck KGaA / BMS"}, {"name": "Trastuzumab deruxtecan", "target": "HER2", "type": "ADC", "highest_phase": "Approved", "first_approved": "2019-12-20", "indications": ["Breast Cancer", "Gastric Cancer", "NSCLC"], "company": "Daiichi Sankyo / AstraZeneca"}, {"name": "Pembrolizumab", "target": "PD-1", "type": "Monoclonal antibody", "highest_phase": "Approved", "first_approved": "2014-09-04", "indications": ["Melanoma", "NSCLC", "HNSCC", "cHL"], "company": "Merck (MSD)"}, {"name": "Amivantamab", "target": "EGFR/MET", "type": "Bispecific antibody", "highest_phase": "Approved", "first_approved": "2021-05-21", "indications": ["NSCLC (ex20ins)"], "company": "Janssen"}, {"name": "Sotorasib", "target": "KRAS G12C", "type": "Small molecule", "highest_phase": "Approved", "first_approved": "2021-05-28", "indications": ["NSCLC (KRAS G12C)"], "company": "Amgen"}, {"name": "Sacituzumab govitecan", "target": "Trop-2", "type": "ADC", "highest_phase": "Approved", "first_approved": "2020-04-22", "indications": ["TNBC", "HR+/HER2- Breast Cancer"], "company": "Gilead"}, ] } # ============================================================================= # CSS — Product-grade styling # ============================================================================= CUSTOM_CSS = """ /* ===== Design Tokens ===== */ :root { --navy-950: #020617; --navy-900: #0a1628; --navy-800: #122540; --navy-700: #1a3050; --navy-600: #1e3a5f; --teal-500: #0891b2; --teal-400: #06b6d4; --teal-50: #ecfeff; --emerald-500: #10b981; --emerald-50: #ecfdf5; --surface: #ffffff; --bg: #f8fafc; --bg-alt: #f1f5f9; --text: #1e293b; --text-secondary: #64748b; --text-tertiary: #94a3b8; --border: #e2e8f0; --border-light: #f1f5f9; --radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --radius-xl: 20px; --shadow-xs: 0 1px 2px rgba(15,23,42,0.04); --shadow-sm: 0 1px 3px rgba(15,23,42,0.06); --shadow: 0 1px 3px rgba(15,23,42,0.08), 0 1px 2px rgba(15,23,42,0.04); --shadow-md: 0 4px 6px -1px rgba(15,23,42,0.08), 0 2px 4px -2px rgba(15,23,42,0.04); --font: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; --font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace; } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); .gradio-container { max-width: 100% !important; padding: 0 32px !important; margin: 0 auto !important; font-family: var(--font) !important; background: var(--bg) !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Force ALL text dark — override Gradio Soft theme everywhere */ .gradio-container .prose, .gradio-container .prose *, .gradio-container .md, .gradio-container .md *, .gradio-container [class*="markdown"], .gradio-container [class*="markdown"] * { color: var(--text) !important; } .gradio-container .prose h2, .gradio-container .md h2, .gradio-container [class*="markdown"] h2 { color: var(--navy-950) !important; font-weight: 700 !important; } .gradio-container .prose strong, .gradio-container .md strong, .gradio-container [class*="markdown"] strong { color: var(--navy-900) !important; } .gradio-container label, .gradio-container .label-text, .gradio-container .label-container span { color: var(--text) !important; font-weight: 500 !important; } .gradio-container td { color: var(--text) !important; } .gradio-container th { color: var(--text-secondary) !important; } /* Ensure header text is always white and visible */ .header-container, .header-container * { color: #ffffff !important; } .header-title { color: #ffffff !important; } .header-subtitle { color: rgba(255, 255, 255, 0.85) !important; } /* ===== Header ===== */ .header-container { background: linear-gradient(135deg, var(--navy-950) 0%, var(--navy-900) 30%, var(--navy-700) 70%, var(--navy-600) 100%); padding: 40px 48px 34px; margin-bottom: 28px; color: white; position: relative; overflow: hidden; border-bottom: 1px solid rgba(255,255,255,0.06); } .header-container::before { content: ''; position: absolute; top: -120px; right: -80px; width: 420px; height: 420px; background: radial-gradient(circle, rgba(6,182,212,0.13) 0%, rgba(6,182,212,0.04) 40%, transparent 70%); border-radius: 50%; pointer-events: none; } .header-container::after { content: ''; position: absolute; bottom: -60px; left: 30%; width: 300px; height: 300px; background: radial-gradient(circle, rgba(16,185,129,0.08) 0%, transparent 70%); border-radius: 50%; pointer-events: none; } .header-row { display: flex; align-items: flex-start; justify-content: space-between; position: relative; z-index: 1; } .header-title { font-size: 26px; font-weight: 700; margin: 0 0 10px 0; letter-spacing: -0.4px; line-height: 1.2; color: #ffffff; } .header-subtitle { font-size: 14px; opacity: 0.68; margin: 0; font-weight: 400; line-height: 1.6; max-width: 520px; color: rgba(255,255,255,0.85); } .header-badges { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; } .header-badge { display: inline-flex; align-items: center; gap: 5px; background: rgba(255,255,255,0.08); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); padding: 5px 16px; border-radius: 20px; font-size: 11.5px; font-weight: 500; letter-spacing: 0.02em; border: 1px solid rgba(255,255,255,0.10); color: rgba(255,255,255,0.8); } /* ===== Tabs ===== */ .tabs { border: none !important; } .tab-nav { background: transparent !important; border-bottom: 1px solid var(--border) !important; border-radius: 0 !important; padding: 0 4px !important; box-shadow: none !important; margin-bottom: 24px !important; gap: 4px !important; } .tab-nav button { border-radius: 8px 8px 0 0 !important; padding: 10px 20px !important; font-size: 13.5px !important; font-weight: 500 !important; border: none !important; color: #475569 !important; transition: color 0.15s ease, background 0.15s ease !important; background: transparent !important; border-bottom: 2px solid transparent !important; margin-bottom: -1px !important; cursor: pointer !important; } .tab-nav button:hover { color: var(--text) !important; background: var(--bg-alt) !important; } .tab-nav button.selected { background: transparent !important; color: #020617 !important; border-bottom: 2px solid #020617 !important; box-shadow: none !important; font-weight: 600 !important; } .tab-nav button:focus-visible { outline: 2px solid var(--navy-600) !important; outline-offset: -2px !important; border-radius: 8px 8px 0 0 !important; } /* Ensure all text elements are readable */ .gradio-container * { color: inherit !important; } .gradio-container button:not(.btn-primary):not(.example-chip) { color: #475569 !important; } .gradio-container button:not(.btn-primary):not(.example-chip):hover { color: #1e293b !important; } /* ===== Input ===== */ .agent-input textarea, .agent-input input { border-radius: var(--radius) !important; border: 1.5px solid var(--border) !important; padding: 14px 16px !important; font-size: 14.5px !important; line-height: 1.6 !important; transition: border-color 0.15s ease, box-shadow 0.15s ease !important; resize: none !important; background: var(--surface) !important; color: var(--text) !important; } .agent-input textarea:focus, .agent-input input:focus { border-color: var(--navy-800) !important; box-shadow: 0 0 0 3px rgba(18,37,64,0.08) !important; outline: none !important; } /* Example chips */ .example-chip { border-radius: 20px !important; padding: 6px 16px !important; font-size: 12.5px !important; font-weight: 500 !important; border: 1px solid var(--border) !important; background: var(--surface) !important; color: var(--text-secondary) !important; cursor: pointer !important; transition: all 0.15s ease !important; white-space: nowrap !important; min-width: unset !important; height: auto !important; line-height: 1.4 !important; } .example-chip:hover { border-color: var(--teal-400) !important; color: var(--teal-500) !important; background: var(--teal-50) !important; } .example-chip:focus-visible { outline: 2px solid var(--teal-400) !important; outline-offset: 1px !important; } /* Primary button */ .btn-primary { background: var(--navy-950) !important; color: white !important; border: none !important; border-radius: var(--radius) !important; padding: 11px 32px !important; font-size: 14px !important; font-weight: 600 !important; cursor: pointer !important; transition: background 0.15s ease, box-shadow 0.15s ease !important; letter-spacing: 0.01em; } .btn-primary:hover { background: var(--navy-800) !important; box-shadow: var(--shadow-md); } .btn-primary:focus-visible { outline: 2px solid var(--navy-600) !important; outline-offset: 2px !important; } /* Secondary button */ .btn-secondary { background: var(--bg-alt) !important; color: #475569 !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 10px 24px !important; font-size: 14px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.15s ease !important; } .btn-secondary:hover { background: var(--border) !important; color: #1e293b !important; } .btn-secondary:focus-visible { outline: 2px solid var(--navy-600) !important; outline-offset: 2px !important; } /* ===== Thinking Steps ===== */ .thinking-steps { background: linear-gradient(135deg, #f8faff, #f0fdfa); border: 1px solid #ccfbf1; border-radius: var(--radius); padding: 14px 18px; margin: 16px 0; } .thinking-step { padding: 3px 0; color: #115e59; font-size: 13px; line-height: 1.6; } /* ===== Cards ===== */ .card { background: var(--surface); border-radius: var(--radius-lg); padding: 24px; box-shadow: var(--shadow-xs); border: 1px solid var(--border); margin-bottom: 16px; transition: box-shadow 0.2s ease; } .card:hover { box-shadow: var(--shadow-sm); } /* ===== Report Content ===== */ .report { color: var(--text); font-size: 14.5px; line-height: 1.75; } .report h2 { font-size: 20px; font-weight: 700; margin-top: 28px; margin-bottom: 12px; color: var(--navy-950); letter-spacing: -0.3px; } .report h3 { font-size: 15px; font-weight: 600; margin-top: 20px; margin-bottom: 8px; color: var(--text); } .report table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 13.5px; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border); } .report th { background: var(--bg-alt); padding: 10px 14px; text-align: left; font-weight: 600; font-size: 11.5px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); } .report td { padding: 10px 14px; border-bottom: 1px solid var(--border-light); color: var(--text); } .report tr:last-child td { border-bottom: none; } .report tr:hover td { background: #fafcff; } /* ===== Status Badge ===== */ .badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; } .badge-approved { background: #dcfce7; color: #166534; } .badge-phase3 { background: #dbeafe; color: #1e40af; } .badge-phase2 { background: #fef3c7; color: #92400e; } .badge-phase1 { background: #fce7f3; color: #9d174d; } /* ===== Footer ===== */ .footer { text-align: center; padding: 28px 16px; color: var(--text-tertiary); font-size: 12px; border-top: 1px solid var(--border-light); margin-top: 48px; letter-spacing: 0.02em; } .footer strong { color: var(--text-secondary); font-weight: 600; } /* ===== Animations ===== */ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.35s ease-out; } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } .fade-in { animation: none; } } /* Misc */ footer { display: none !important; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } """ # ============================================================================= # MCP CLIENT # ============================================================================= _mcp_tools_cache: Optional[List[str]] = None async def get_mcp_tools() -> List[str]: """Fetch available MCP tool names with caching.""" global _mcp_tools_cache if _mcp_tools_cache is not None: return _mcp_tools_cache if not API_KEY: return [] try: from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client(SERVER_URL, timeout=15, sse_read_timeout=15) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.list_tools() _mcp_tools_cache = [t.name for t in result.tools] return _mcp_tools_cache except Exception as e: print(f"[MCP] Tool discovery failed: {e}") return [] async def call_mcp_tool(tool_name: str, args: dict) -> Optional[dict]: """Call a specific MCP tool. Returns parsed JSON or None on failure.""" if not API_KEY: return None try: from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client(SERVER_URL, timeout=30, sse_read_timeout=30) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool(tool_name, arguments=args) if result.content: text = result.content[0].text return json.loads(text) if isinstance(text, str) else text except Exception as e: print(f"[MCP] {tool_name} failed: {e}") return None # ============================================================================= # AGENT ENGINE — Intent Parsing & Tool Routing # ============================================================================= # Entity extraction patterns TARGET_KEYWORDS = { "egfr": "EGFR", "her2": "HER2", "her-2": "HER2", "erbb2": "HER2", "pd-l1": "PD-L1", "pdl1": "PD-L1", "pd-1": "PD-1", "pd1": "PD-1", "braf": "BRAF", "alk": "ALK", "ros1": "ROS1", "kras": "KRAS", "vegf": "VEGF", "vegfr": "VEGFR", "ctla-4": "CTLA-4", "ctla4": "CTLA-4", "stat3": "STAT3", "met": "c-MET", "c-met": "c-MET", "ret": "RET", "ntrk": "NTRK", "fgfr": "FGFR", "parp": "PARP", "cdk4": "CDK4/6", "cdk4/6": "CDK4/6", "btk": "BTK", "jak": "JAK", "flt3": "FLT3", "idh1": "IDH1", "idh2": "IDH2", "tp53": "TP53", "p53": "TP53", "brca1": "BRCA1", "brca2": "BRCA2", "trop-2": "Trop-2", "trop2": "Trop-2", "claudin": "Claudin 18.2", "cldn18": "Claudin 18.2", } DISEASE_KEYWORDS = { "non-small cell lung cancer": "NSCLC", "nsclc": "NSCLC", "lung cancer": "Lung Cancer", "breast cancer": "Breast Cancer", "triple-negative breast": "Triple-Negative Breast Cancer", "tnbc": "Triple-Negative Breast Cancer", "colorectal": "Colorectal Cancer", "crc": "Colorectal Cancer", "melanoma": "Melanoma", "pancreatic": "Pancreatic Cancer", "hepatocellular": "Hepatocellular Carcinoma", "hcc": "Hepatocellular Carcinoma", "liver cancer": "Hepatocellular Carcinoma", "gastric": "Gastric Cancer", "stomach cancer": "Gastric Cancer", "leukemia": "Leukemia", "lymphoma": "Lymphoma", "ovarian": "Ovarian Cancer", "prostate": "Prostate Cancer", "multiple myeloma": "Multiple Myeloma", "mm": "Multiple Myeloma", "renal cell": "Renal Cell Carcinoma", "rcc": "Renal Cell Carcinoma", "bladder": "Urothelial Carcinoma", "urothelial": "Urothelial Carcinoma", "head and neck": "Head and Neck Cancer", "hnscc": "Head and Neck Cancer", "glioblastoma": "Glioblastoma", "gbm": "Glioblastoma", "alzheimer": "Alzheimer's Disease", "parkinson": "Parkinson's Disease", "diabetes": "Diabetes", "obesity": "Obesity", } COMPANY_KEYWORDS = { "roche": "Roche", "genentech": "Roche", "novartis": "Novartis", "pfizer": "Pfizer", "merck": "Merck (MSD)", "msd": "Merck (MSD)", "bristol-myers": "Bristol-Myers Squibb", "bms": "Bristol-Myers Squibb", "astrazeneca": "AstraZeneca", "az": "AstraZeneca", "johnson": "Johnson & Johnson", "jnj": "Johnson & Johnson", "janssen": "Johnson & Johnson", "sanofi": "Sanofi", "gsk": "GlaxoSmithKline", "abbvie": "AbbVie", "amgen": "Amgen", "gilead": "Gilead", "lilly": "Eli Lilly", "eli lilly": "Eli Lilly", "moderna": "Moderna", "biontech": "BioNTech", "daiichi": "Daiichi Sankyo", "beigene": "BeiGene", "百济": "BeiGene", "innovent": "Innovent", "信达": "Innovent", "hengrui": "Hengrui", "恒瑞": "Hengrui", "akebia": "Akebia", } PHASE_KEYWORDS = { "phase 3": "phase_3", "phase iii": "phase_3", "phase iii": "phase_3", "pivotal": "phase_3", "phase 2": "phase_2", "phase ii": "phase_2", "phase 1": "phase_1", "phase i": "phase_1", "first-in-human": "phase_1", "fih": "phase_1", "approved": "approved", "marketed": "approved", "launched": "approved", "preclinical": "preclinical", } MODULE_KEYWORDS = { "target": ["target", "靶点", "receptor", "kinase", "protein", "gene", "mutation", "pathway", "inhibitor target", "antibody target", "drug target"], "drug": ["drug", "药物", "medicine", "inhibitor", "antibody", "therapy", "treatment regimen", "approved drug", "pipeline drug", "molecule", "compound", "modality"], "disease": ["disease", "疾病", "cancer", "tumor", "indication", "适应症", "epidemiology", "patients", "prevalence", "incidence", "mortality"], "company": ["company", "公司", "pharma", "biotech", "pipeline of", "portfolio", "acquisition", "merger", "partner", "revenue"], "trial": ["trial", "试验", "clinical", "nct", "enrollment", "endpoint", "randomized", "double-blind", "phase 3 trial", "phase 2 trial", "phase 1 trial"], } def parse_intent(query: str) -> Dict: """ Parse a natural language query to determine: - module (target/drug/disease/company/trial) - entities (targets, diseases, companies, phases) - mcp_args (for direct MCP call) - confidence """ lower = query.lower() result = { "module": "target", # default "entities": {"targets": [], "diseases": [], "companies": [], "phases": []}, "mcp_args": {"limit": 10}, "confidence": 0.0, "thinking": [], } # Extract entities for kw, val in TARGET_KEYWORDS.items(): if kw in lower and val not in result["entities"]["targets"]: result["entities"]["targets"].append(val) for kw, val in DISEASE_KEYWORDS.items(): if kw in lower and val not in result["entities"]["diseases"]: result["entities"]["diseases"].append(val) for kw, val in COMPANY_KEYWORDS.items(): if kw in lower and val not in result["entities"]["companies"]: result["entities"]["companies"].append(val) for kw, val in PHASE_KEYWORDS.items(): if kw in lower: result["entities"]["phases"].append(val) # Determine module by scoring keyword matches scores = {m: 0 for m in MODULE_KEYWORDS} for module, keywords in MODULE_KEYWORDS.items(): for kw in keywords: if kw in lower: scores[module] += 1 best_module = max(scores, key=scores.get) max_score = scores[best_module] # Heuristic overrides based on entities found if result["entities"]["targets"] and not result["entities"]["diseases"] and not result["entities"]["companies"]: if max_score == 0 or best_module == "drug": result["module"] = "target" if scores["target"] >= scores["drug"] else "drug" else: result["module"] = best_module elif result["entities"]["diseases"] and not result["entities"]["targets"]: result["module"] = "disease" elif result["entities"]["companies"] and not result["entities"]["targets"]: result["module"] = "company" elif "trial" in lower or "clinical" in lower or "enrollment" in lower: result["module"] = "trial" else: result["module"] = best_module if max_score > 0 else "target" # Build MCP args based on module mod = result["module"] if mod == "target" and result["entities"]["targets"]: result["mcp_args"] = {"target": result["entities"]["targets"][:3], "limit": 10} result["thinking"].append(f"🔍 Detected target query → searching for: {', '.join(result['entities']['targets'][:3])}") elif mod == "drug": args = {"limit": 10} if result["entities"]["targets"]: args["target"] = result["entities"]["targets"][:3] if result["entities"]["diseases"]: args["disease"] = result["entities"]["diseases"][:3] if result["entities"]["phases"]: args["highest_phase"] = result["entities"]["phases"][:3] result["mcp_args"] = args result["thinking"].append(f"🔍 Detected drug query → searching with {json.dumps(args)}") elif mod == "disease" and result["entities"]["diseases"]: result["mcp_args"] = {"disease": result["entities"]["diseases"][:3], "limit": 10} result["thinking"].append(f"🔍 Detected disease query → searching for: {', '.join(result['entities']['diseases'][:3])}") elif mod == "company" and result["entities"]["companies"]: result["mcp_args"] = {"company": result["entities"]["companies"][:3], "limit": 10} result["thinking"].append(f"🔍 Detected company query → searching for: {', '.join(result['entities']['companies'][:3])}") elif mod == "trial": args = {"limit": 10} if result["entities"]["targets"]: args["target"] = result["entities"]["targets"][:3] if result["entities"]["diseases"]: args["disease"] = result["entities"]["diseases"][:3] if result["entities"]["phases"]: args["phase"] = result["entities"]["phases"][:3] result["mcp_args"] = args result["thinking"].append(f"🔍 Detected trial query → searching with {json.dumps(args)}") # Confidence entity_count = sum(len(v) for v in result["entities"].values()) result["confidence"] = min(entity_count * 0.25 + scores[result["module"]] * 0.15, 0.95) return result # ============================================================================= # REPORT BUILDERS # ============================================================================= def _badge(phase: str) -> str: """Generate an HTML badge for a drug phase.""" phase_lower = phase.lower() if "approved" in phase_lower: cls = "badge-approved" elif "phase 3" in phase_lower or "phase iii" in phase_lower: cls = "badge-phase3" elif "phase 2" in phase_lower or "phase ii" in phase_lower: cls = "badge-phase2" elif "phase 1" in phase_lower or "phase i" in phase_lower: cls = "badge-phase1" else: cls = "badge-phase2" return f'{phase}' def build_target_report(target_data: Dict) -> str: """Build a structured target intelligence report.""" name = target_data.get("name", "Unknown") full = target_data.get("full_name", "") family = target_data.get("family", "N/A") cls = target_data.get("class", "N/A") drugs = target_data.get("approved_drugs", []) pipeline = target_data.get("pipeline_summary", {}) pathways = target_data.get("pathways", []) mutations = target_data.get("mutation_hotspots", []) landscape = target_data.get("competitive_landscape", "") report = [] report.append(f"## 🎯 {name} — Target Intelligence Report") report.append("") # Overview card report.append("### 📋 Overview") report.append(f"| Property | Value |") report.append(f"|----------|-------|") report.append(f"| **Full Name** | {full} |") report.append(f"| **Family** | {family} |") report.append(f"| **Class** | {cls} |") report.append(f"| **Approved Drugs** | {len(drugs)} |") report.append("") # Pathways if pathways: report.append("### 🧬 Signaling Pathways") for p in pathways: report.append(f"- {p}") report.append("") # Mutations if mutations: report.append("### 🔬 Key Mutations / Variants") for m in mutations: report.append(f"- {m}") report.append("") # Approved Drugs Table if drugs: report.append(f"### 💊 Approved Drugs ({len(drugs)})") report.append("| Drug | Type | Generation | Year | Indications | Company |") report.append("|------|------|-----------|------|-------------|---------|") for d in drugs: inds = ", ".join(d.get("indications", [])[:2]) if len(d.get("indications", [])) > 2: inds += f" +{len(d['indications']) - 2} more" report.append(f"| {d['name']} | {d['type']} | {d.get('gen', '-')} | " f"{d['year']} | {inds} | {d.get('company', '-')} |") report.append("") # Pipeline if pipeline: report.append("### 🔬 Pipeline Overview") report.append(f"| Phase | Count |") report.append(f"|-------|-------|") for phase in ["phase_3", "phase_2", "phase_1", "preclinical"]: label = phase.replace("_", " ").title() report.append(f"| {label} | {pipeline.get(phase, 'N/A')} |") report.append("") hot = pipeline.get("hot_topics", []) if hot: report.append("**🔥 Hot Topics:**") for t in hot: report.append(f"- {t}") report.append("") # Competitive landscape if landscape: report.append("### 🏔️ Competitive Landscape") report.append(landscape) report.append("") report.append("---") report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*") return "\n".join(report) def _normalize_drug_item(item: Dict) -> Dict: """Normalize server response fields to internal schema.""" if "display_name_en" in item or "drug_type_view" in item: drug_types = item.get("drug_type_view", []) type_str = ", ".join(d.get("display_name_en", "") for d in drug_types if d.get("display_name_en")) or "N/A" # Indications from research_disease_view (fetch) or indication_view (search) indications = item.get("research_disease_view", []) or item.get("indication_view", []) ind_list = [d.get("display_name_en", "") for d in indications if d.get("display_name_en")] or [] # Company from originator_org_master_entity_id_view (fetch) or originator_view (search) orgs = item.get("originator_org_master_entity_id_view", []) or item.get("originator_view", []) or item.get("organization_view", []) company = ", ".join(d.get("display_name_en", "") for d in orgs if d.get("display_name_en")) or "N/A" # Phase from global_highest_dev_status_view (fetch) or highest_phase_view (search) phase = item.get("global_highest_dev_status_view") or item.get("highest_phase_view", {}) phase_str = phase.get("display_name_en", "") if isinstance(phase, dict) else str(phase) if phase else "N/A" return { "name": item.get("display_name_en") or item.get("display_name_cn") or "N/A", "type": type_str, "highest_phase": phase_str, "first_approved": item.get("first_approved_date", ""), "indications": ind_list, "company": company, } return item def _normalize_items(items: List[Dict]) -> List[Dict]: """Normalize a list of server response items.""" return [_normalize_drug_item(i) for i in items] # ============================================================================= # ENTITY EXTRACTORS — Shape live MCP data into report-friendly dicts # ============================================================================= def _profile_text(item: Dict) -> str: """Extract a profile description string from profile/profile_v2 fields.""" for key in ("profile_v2", "profile"): val = item.get(key) if isinstance(val, list) and val: content = val[0].get("content", "") if content: return content return "" def _aliases(item: Dict, max_n: int = 6) -> List[str]: """Extract English aliases from a target/disease item.""" aliases = item.get("alias", []) or [] out, seen = [], set() for a in aliases: if isinstance(a, dict) and a.get("lang") == "EN": n = a.get("name", "").strip() if n and n not in seen: seen.add(n) out.append(n) if len(out) >= max_n: break return out def extract_target(item: Dict) -> Dict: """Extract structured target info from ls_target_fetch result.""" return { "name": item.get("display_name_en") or item.get("display_name_cn", "N/A"), "aliases": _aliases(item), "profile": _profile_text(item), "drug_count": item.get("drug_count_roll_up") or item.get("drug_count", 0), "dev_drug_count": item.get("dev_drug_count_roll_up") or item.get("dev_drug_count", 0), "disease_count": item.get("disease_count_roll_up") or item.get("disease_count", 0), "uniprot_id": (item.get("uniprot_id") or [None])[0], "hgnc_id": (item.get("hgnc_id") or [None])[0], "chembl_id": (item.get("chembl_id") or [None])[0], "organism": item.get("organisms") or item.get("source", ""), } def extract_disease(item: Dict) -> Dict: """Extract structured disease info from ls_disease_fetch result.""" return { "name": item.get("display_name_en") or item.get("display_name_cn", "N/A"), "aliases": _aliases(item), "profile": _profile_text(item), "dev_drug_count": item.get("dev_drug_count_roll_up") or item.get("dev_drug_count", 0), "mesh_id": item.get("mesh_id"), "umls_cui": (item.get("umls_cui") or [None])[0], } def extract_organization(item: Dict) -> Dict: """Extract structured org info from ls_organization_fetch result.""" country = item.get("country_id_view") or {} fdate = item.get("founded_date") founded_year = None if fdate and isinstance(fdate, int): s = str(fdate) if len(s) >= 4: founded_year = s[:4] return { "name": item.get("display_name_en") or item.get("name_en", "N/A"), "description": item.get("short_description_en") or (item.get("long_description_en", "") or "")[:400], "website": item.get("website", ""), "country": country.get("display_name_en", "") if isinstance(country, dict) else "", "founded": founded_year, "employees": item.get("employee_number"), "stock_exchange": item.get("stock_exchange_code", ""), "stock_symbol": item.get("stock_symbol", ""), "ownership": ", ".join(item.get("ownership_type", []) or []), "drug_count": item.get("drug_count_roll_up") or item.get("drug_count", 0), "dev_drug_count": item.get("dev_drug_count_roll_up") or item.get("dev_drug_count", 0), "patent_count": item.get("patent_phs_count_roll_up") or item.get("patent_phs_count", 0), } def extract_pipeline_drug(item: Dict) -> Dict: """Extract a drug record from ls_organization_pipeline_fetch.""" targets = item.get("targets", []) or [] target_str = ", ".join(t.get("display_name_en", "") for t in targets if t.get("display_name_en")) or "—" statuses = item.get("status_tables", []) or [] indications = [] phases_seen = set() best_phase = "" for s in statuses: d = s.get("disease_id_view", {}) or {} name = d.get("display_name_en", "") if name and name not in indications: indications.append(name) ds = s.get("dev_status") if isinstance(ds, list): for entry in ds: view = entry.get("dev_status_id_view", {}) or {} ph = view.get("display_name_en", "") if ph and ph not in phases_seen: phases_seen.add(ph) if not best_phase: best_phase = ph elif isinstance(ds, dict): ph = ds.get("display_name_en", "") if ph and ph not in phases_seen: phases_seen.add(ph) if not best_phase: best_phase = ph return { "name": item.get("display_name_en") or item.get("display_name_cn", "N/A"), "targets": target_str, "indications": indications[:3], "phase": best_phase or "—", } def extract_trial(item: Dict) -> Dict: """Extract structured trial info from ls_clinical_trial_fetch result.""" phase = item.get("clinical_phase") or {} phase_str = phase.get("display_name_en", "") if isinstance(phase, dict) else str(phase) sponsors = item.get("sponsor_organization", []) or [] sponsor_str = ", ".join(s.get("display_name_en", "") for s in sponsors if s.get("display_name_en")) diseases = item.get("disease", []) or [] disease_str = ", ".join(d.get("display_name_en", "") for d in diseases if d.get("display_name_en")) drugs = item.get("experiment_drug", []) or [] drug_str = ", ".join(d.get("display_name_en", "") for d in drugs if d.get("display_name_en")) return { "nct": item.get("registration_number", ""), "title": item.get("trial_title", ""), "status": item.get("trial_status", ""), "phase": phase_str, "sponsor": sponsor_str, "disease": disease_str, "drugs": drug_str, } def build_drug_report(items: List[Dict], query_summary: str, total: int = 0) -> str: """Build a drug pipeline report from search results.""" if not items: return f"## 💊 Drug Search: {query_summary}\n\n📭 No results found. Try a different query." drug_types = Counter() companies = Counter() years = [] phases = Counter() for item in items: drug_types[item.get("type", "Unknown")] += 1 companies[item.get("company", "Unknown")] += 1 phases[item.get("highest_phase", "Unknown")] += 1 date = item.get("first_approved", "") if date and date != "N/A": try: years.append(int(date.split("-")[0])) except (ValueError, IndexError): pass report = [] report.append(f"## 💊 {query_summary}") report.append(f"*{len(items)} results{' of ' + str(total) if total else ''}*") report.append("") # Stats row report.append("### 📊 Summary") stats = [] if drug_types: top = drug_types.most_common(3) stats.append(f"**Top Types:** " + " · ".join(f"{t} ({c})" for t, c in top)) if phases: top_p = phases.most_common(3) stats.append(f"**Phases:** " + " · ".join(f"{p} ({c})" for p, c in top_p)) if years: stats.append(f"**Timeline:** {min(years)} – {max(years)}") if companies: top_c = companies.most_common(3) stats.append(f"**Top Companies:** " + " · ".join(f"{c} ({n})" for c, n in top_c)) for s in stats: report.append(f"- {s}") report.append("") # Drug table report.append("### 🧪 Pipeline Details") report.append("| Drug | Type | Phase | Indications | Company |") report.append("|------|------|-------|-------------|---------|") for item in items[:15]: name = item.get("name", "N/A") dtype = item.get("type", "N/A") phase = item.get("highest_phase", "N/A") inds = ", ".join(item.get("indications", ["N/A"])[:2]) comp = item.get("company", "N/A") report.append(f"| {name} | {dtype} | {_badge(phase)} | {inds} | {comp} |") report.append("") report.append("---") report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*") return "\n".join(report) def build_disease_report(disease_data: Dict) -> str: """Build a disease landscape report.""" name = disease_data.get("name", "Unknown") report = [] report.append(f"## 🏥 {name} — Disease Landscape") report.append("") report.append("### 📊 Epidemiology") for key in ["global_incidence", "mortality", "5yr_survival"]: if key in disease_data: label = key.replace("5yr", "5-Year ").replace("_", " ").title() report.append(f"- **{label}:** {disease_data[key]}") report.append("") mutations = disease_data.get("major_mutations", []) if mutations: report.append("### 🧬 Key Driver Mutations") for m in mutations: report.append(f"- {m}") report.append("") key_drugs = disease_data.get("key_drugs", []) if key_drugs: report.append("### 💊 Key Therapies") report.append("| Drug | Target | Setting |") report.append("|------|--------|---------|") for d in key_drugs: report.append(f"| {d['name']} | {d['target']} | {d.get('setting', '-')} |") report.append("") report.append("### 📈 Market & Pipeline") for key in ["market_size", "approved_drugs_count"]: if key in disease_data: label = key.replace("_", " ").title().replace("Drugs", "Drugs").replace("Count", "Count") report.append(f"- **{label}:** {disease_data[key]}") pipeline = disease_data.get("pipeline", {}) if pipeline: report.append(f"- **Pipeline:** Phase 3: {pipeline.get('phase_3', 'N/A')} | " f"Phase 2: {pipeline.get('phase_2', 'N/A')} | Phase 1: {pipeline.get('phase_1', 'N/A')}") report.append("") trends = disease_data.get("key_trends", []) if trends: report.append("### 🔥 Key Trends") for t in trends: report.append(f"- {t}") report.append("") report.append("---") report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*") return "\n".join(report) def build_company_report(company_data: Dict) -> str: """Build a company profile report.""" name = company_data.get("name", "Unknown") report = [] report.append(f"## 🏢 {name} — Company Profile") report.append("") report.append("### 📋 Company Overview") for key in ["ticker", "headquarters", "employees", "2024_revenue"]: if key in company_data: label = key.replace("_", " ").title().replace("2024", "2024") report.append(f"- **{label}:** {company_data[key]}") report.append("") ta = company_data.get("therapeutic_areas", []) if ta: report.append(f"**Therapeutic Areas:** " + " · ".join(ta)) report.append("") flagship = company_data.get("flagship_drugs", []) if flagship: report.append("### 💊 Flagship Drugs") report.append("| Drug | Target | Sales | Phase |") report.append("|------|--------|-------|-------|") for d in flagship: report.append(f"| {d['name']} | {d['target']} | {d['sales']} | {_badge(d['phase'])} |") report.append("") pipeline = company_data.get("pipeline_count", {}) if pipeline: report.append("### 🔬 Pipeline Overview") report.append(f"| Phase | Count |") report.append(f"|-------|-------|") for phase in ["approved", "phase_3", "phase_2", "phase_1"]: label = phase.replace("_", " ").title() report.append(f"| {label} | {pipeline.get(phase, 'N/A')} |") report.append("") deals = company_data.get("recent_deals", []) if deals: report.append("### 🤝 Recent Strategic Deals") for d in deals: report.append(f"- {d}") report.append("") strategy = company_data.get("strategy", "") if strategy: report.append(f"### 🎯 Strategic Outlook\n{strategy}") report.append("") report.append("---") report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*") return "\n".join(report) def build_agent_thinking(intent: Dict, mcp_available: bool, mcp_result: Optional[Dict]) -> str: """Build the agent thinking trace HTML.""" steps = [] steps.append(f"🧠 **Intent:** {intent['module'].title()} query (confidence: {intent['confidence']:.0%})") if intent["entities"]["targets"]: steps.append(f"🎯 **Targets:** {', '.join(intent['entities']['targets'])}") if intent["entities"]["diseases"]: steps.append(f"🏥 **Diseases:** {', '.join(intent['entities']['diseases'])}") if intent["entities"]["companies"]: steps.append(f"🏢 **Companies:** {', '.join(intent['entities']['companies'])}") for think in intent.get("thinking", []): steps.append(think) if mcp_available: if mcp_result: total = mcp_result.get("total", 0) items = len(mcp_result.get("items", [])) steps.append(f"✅ **MCP Live Data:** Retrieved {items} of {total} records") else: steps.append(f"⚠️ **MCP:** No results from live API, using knowledge base") else: steps.append(f"📚 **Source:** Knowledge base (no API key configured)") return "\n".join(f"
AI-powered drug discovery intelligence. Explore targets, pipelines, diseases, companies, and clinical trials — all in one place.