CoolFace
Apppublic

mondalsou/lead_optimization_agent

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
app.py1945 linesDownload Raw Back to root
1"""2Lead Optimization Agent — Streamlit UI3Run:  streamlit run app.py4"""5import html6import json7import os8import re9import sys10import time11from datetime import datetime12from pathlib import Path13import streamlit as st14import pandas as pd15import matplotlib; matplotlib.use("Agg")16import matplotlib.pyplot as plt17import plotly.graph_objects as go18import anthropic19 20sys.path.insert(0, os.path.dirname(__file__))21from agent_utils import tool_executor, is_valid_smiles22 23# ─── Page config ──────────────────────────────────────────────────────────────24st.set_page_config(25    page_title="Lead Optimization Agent",26    page_icon="🧬",27    layout="wide",28    initial_sidebar_state="expanded",29)30 31# ─── Custom CSS ───────────────────────────────────────────────────────────────32st.markdown("""33<style>34@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Space+Grotesk:wght@400;500;700&display=swap');35 36:root {37    --bg: #f7f0e8;38    --bg-soft: #fffaf5;39    --panel: rgba(255, 251, 247, 0.84);40    --panel-strong: #fffdf9;41    --ink: #19212a;42    --muted: #5f6b76;43    --line: rgba(25, 33, 42, 0.10);44    --teal: #0f766e;45    --teal-soft: #d7f3ee;46    --sand: #f2e3cf;47    --coral: #d97757;48    --blue: #2563eb;49    --gold: #b7791f;50    --green: #15803d;51    --red: #b42318;52    --shadow: 0 24px 55px rgba(49, 38, 30, 0.10);53}54 55html, body, [class*="css"] {56    font-family: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;57    color: var(--ink);58}59 60[data-testid="stAppViewContainer"] {61    background:62        radial-gradient(circle at top left, rgba(215, 243, 238, 0.7), transparent 28%),63        radial-gradient(circle at top right, rgba(242, 227, 207, 0.95), transparent 34%),64        linear-gradient(180deg, #fffaf5 0%, #f8efe6 48%, #f5ebe2 100%);65}66 67[data-testid="stHeader"] {68    background: transparent;69}70 71.block-container {72    padding-top: 2rem;73    padding-bottom: 4rem;74    max-width: 1320px;75}76 77[data-testid="stSidebar"] {78    background: linear-gradient(180deg, #193a3b 0%, #224c4d 100%);79    border-right: 1px solid rgba(255, 255, 255, 0.10);80}81 82[data-testid="stSidebar"] * {83    color: #edf7f5;84}85 86[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p {87    color: rgba(237, 247, 245, 0.78);88}89 90[data-testid="stSidebar"] .stSelectbox label,91[data-testid="stSidebar"] .stTextInput label,92[data-testid="stSidebar"] .stTextArea label,93[data-testid="stSidebar"] .stSlider label {94    color: #f7fbfa;95    font-weight: 600;96}97 98[data-testid="stSidebar"] .stTextInput input,99[data-testid="stSidebar"] .stTextArea textarea,100[data-testid="stSidebar"] .stSelectbox [data-baseweb="select"] > div {101    background: #fffaf7 !important;102    border: 1px solid rgba(255, 255, 255, 0.18);103    border-radius: 16px;104    color: #19212a !important;105    caret-color: #19212a;106}107 108[data-testid="stSidebar"] .stTextInput input,109[data-testid="stSidebar"] .stTextArea textarea,110[data-testid="stSidebar"] .stSelectbox [data-baseweb="select"] * {111    color: #19212a !important;112}113 114[data-testid="stSidebar"] .stTextInput input::placeholder,115[data-testid="stSidebar"] .stTextArea textarea::placeholder {116    color: #7c8b99 !important;117}118 119[data-testid="stSidebar"] .stTextInput > div,120[data-testid="stSidebar"] .stTextArea > div,121[data-testid="stSidebar"] .stSelectbox > div {122    background: transparent;123}124 125[data-testid="stSidebar"] .stButton > button,126[data-testid="stSidebar"] .stDownloadButton > button {127    background: #fffaf7 !important;128    color: #19212a !important;129    border: 1px solid rgba(255, 255, 255, 0.18) !important;130}131 132[data-testid="stSidebar"] .stButton > button *,133[data-testid="stSidebar"] .stDownloadButton > button * {134    color: #19212a !important;135}136 137[data-testid="stSidebar"] .stButton > button:hover,138[data-testid="stSidebar"] .stDownloadButton > button:hover {139    background: #fff5ef !important;140    border-color: rgba(255, 255, 255, 0.28) !important;141}142 143[data-testid="stSidebar"] .stButton > button:disabled,144[data-testid="stSidebar"] .stDownloadButton > button:disabled {145    background: rgba(255, 250, 247, 0.82) !important;146    color: #64748b !important;147    border-color: rgba(255, 255, 255, 0.12) !important;148    opacity: 1 !important;149    box-shadow: none;150}151 152[data-testid="stSidebar"] .stButton > button:disabled *,153[data-testid="stSidebar"] .stDownloadButton > button:disabled * {154    color: #64748b !important;155}156 157[data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] {158    background: #fffaf7 !important;159    border: 1px solid rgba(255, 255, 255, 0.18) !important;160}161 162[data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] * {163    color: #19212a !important;164}165 166[data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] small {167    color: #7c8b99 !important;168}169 170[data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] button {171    background: #f7f0e8 !important;172    color: #19212a !important;173    border: 1px solid rgba(25, 33, 42, 0.12) !important;174    box-shadow: none;175}176 177[data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] button * {178    color: #19212a !important;179}180 181[data-testid="stSidebar"] .stSlider [data-baseweb="slider"] > div > div {182    background: rgba(255, 255, 255, 0.20);183}184 185[data-testid="stSidebar"] .stSlider [role="slider"] {186    background: #f7c59f;187    border-color: #f7c59f;188}189 190.stButton > button {191    border-radius: 999px;192    padding: 0.78rem 1.15rem;193    font-weight: 700;194    letter-spacing: 0.01em;195    border: 1px solid rgba(25, 33, 42, 0.10);196    box-shadow: 0 10px 24px rgba(25, 33, 42, 0.08);197    transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;198}199 200.stButton > button:hover {201    transform: translateY(-1px);202    box-shadow: 0 14px 28px rgba(25, 33, 42, 0.12);203}204 205.stButton > button[kind="primary"] {206    background: linear-gradient(135deg, #1d847c 0%, #0f766e 100%);207    color: #ffffff;208    border-color: transparent;209}210 211.stButton > button[kind="secondary"] {212    background: rgba(255, 253, 249, 0.76);213    color: var(--ink);214}215 216.stTabs [data-baseweb="tab-list"] {217    gap: 0.45rem;218    background: rgba(255, 255, 255, 0.52);219    padding: 0.4rem;220    border: 1px solid var(--line);221    border-radius: 999px;222    width: fit-content;223    box-shadow: 0 10px 24px rgba(25, 33, 42, 0.05);224}225 226.stTabs [data-baseweb="tab"] {227    border-radius: 999px;228    padding: 0.8rem 1.1rem;229    color: var(--muted);230    font-weight: 600;231    height: auto;232}233 234.stTabs [aria-selected="true"] {235    background: var(--panel-strong);236    color: var(--ink);237}238 239.hero-shell {240    position: relative;241    overflow: hidden;242    background:243        radial-gradient(circle at top right, rgba(255, 255, 255, 0.78), transparent 28%),244        linear-gradient(135deg, rgba(215, 243, 238, 0.96) 0%, rgba(255, 250, 245, 0.98) 46%, rgba(242, 227, 207, 0.88) 100%);245    border: 1px solid rgba(25, 33, 42, 0.08);246    border-radius: 32px;247    padding: 2rem;248    box-shadow: var(--shadow);249    margin-bottom: 1.15rem;250}251 252.hero-shell::after {253    content: "";254    position: absolute;255    width: 240px;256    height: 240px;257    top: -85px;258    right: -60px;259    border-radius: 50%;260    background: rgba(15, 118, 110, 0.08);261}262 263.hero-grid {264    position: relative;265    z-index: 1;266    display: grid;267    grid-template-columns: minmax(0, 1.4fr) minmax(290px, 0.8fr);268    gap: 1.4rem;269    align-items: start;270}271 272.hero-eyebrow,273.section-kicker {274    display: inline-flex;275    align-items: center;276    gap: 0.35rem;277    padding: 0.35rem 0.75rem;278    border-radius: 999px;279    background: rgba(255, 255, 255, 0.58);280    border: 1px solid rgba(25, 33, 42, 0.08);281    color: var(--teal);282    font-size: 0.76rem;283    font-weight: 700;284    text-transform: uppercase;285    letter-spacing: 0.08em;286}287 288.hero-title {289    margin: 0.8rem 0 0.7rem;290    font-size: clamp(2.3rem, 4vw, 4.3rem);291    line-height: 0.95;292    letter-spacing: -0.04em;293    color: var(--ink);294}295 296.hero-title .accent {297    display: block;298    font-family: "Instrument Serif", Georgia, serif;299    font-style: italic;300    font-weight: 400;301    letter-spacing: -0.02em;302}303 304.hero-text {305    max-width: 48rem;306    font-size: 0.98rem;307    line-height: 1.6;308    color: var(--muted);309    margin: 0;310}311 312.hero-pill-row {313    display: flex;314    flex-wrap: wrap;315    gap: 0.55rem;316    margin-top: 1rem;317}318 319.hero-pill {320    background: rgba(255, 255, 255, 0.66);321    border: 1px solid rgba(25, 33, 42, 0.08);322    border-radius: 999px;323    padding: 0.5rem 0.8rem;324    font-size: 0.88rem;325    color: var(--ink);326}327 328.hero-panel,329.overview-card,330.metric-card,331.mol-card,332.best-banner,333.empty-card {334    background: var(--panel);335    backdrop-filter: blur(14px);336    border: 1px solid rgba(25, 33, 42, 0.08);337    box-shadow: var(--shadow);338}339 340.hero-panel {341    border-radius: 24px;342    padding: 1.35rem;343}344 345.hero-panel-title {346    font-size: 0.78rem;347    color: var(--muted);348    text-transform: uppercase;349    letter-spacing: 0.08em;350    font-weight: 700;351}352 353.hero-stat-grid {354    display: grid;355    gap: 0.85rem;356    margin-top: 1rem;357}358 359.hero-stat {360    display: grid;361    gap: 0.18rem;362    padding-bottom: 0.8rem;363    border-bottom: 1px solid rgba(25, 33, 42, 0.08);364}365 366.hero-stat:last-child {367    border-bottom: 0;368    padding-bottom: 0;369}370 371.hero-stat-label {372    font-size: 0.8rem;373    text-transform: uppercase;374    letter-spacing: 0.08em;375    color: var(--muted);376}377 378.hero-stat-value {379    font-size: 1.2rem;380    font-weight: 700;381    color: var(--ink);382}383 384.overview-card,385.empty-card {386    border-radius: 24px;387    padding: 1.3rem 1.35rem;388    min-height: 100%;389}390 391.overview-label {392    font-size: 0.8rem;393    color: var(--muted);394    text-transform: uppercase;395    letter-spacing: 0.08em;396    font-weight: 700;397}398 399.overview-title {400    font-size: 1.25rem;401    font-weight: 700;402    color: var(--ink);403    margin: 0.45rem 0 0.65rem;404}405 406.overview-text,407.small-note,408.check-copy {409    color: var(--muted);410    line-height: 1.55;411    font-size: 0.9rem;412}413 414.mono-preview {415    margin-top: 0.9rem;416    padding: 0.8rem 0.95rem;417    border-radius: 18px;418    background: rgba(25, 33, 42, 0.04);419    border: 1px solid rgba(25, 33, 42, 0.08);420    font-family: "SFMono-Regular", ui-monospace, monospace;421    font-size: 0.82rem;422    overflow-wrap: anywhere;423}424 425.brief-lead {426    color: var(--muted);427    line-height: 1.6;428    font-size: 0.95rem;429    margin: 0.15rem 0 0;430}431 432.brief-targets {433    margin-top: 0.9rem;434    display: grid;435    gap: 0.5rem;436}437 438.brief-target {439    display: flex;440    gap: 0.6rem;441    align-items: flex-start;442    color: var(--ink);443    font-size: 0.93rem;444    line-height: 1.45;445}446 447.brief-target::before {448    content: "";449    width: 8px;450    height: 8px;451    border-radius: 999px;452    margin-top: 0.38rem;453    flex-shrink: 0;454    background: linear-gradient(135deg, #0f766e 0%, #d97757 100%);455}456 457.brief-meta {458    margin-top: 0.9rem;459    font-size: 0.82rem;460    color: #7c8b99;461    text-transform: uppercase;462    letter-spacing: 0.06em;463    font-weight: 700;464}465 466.readiness-list {467    display: grid;468    gap: 0.9rem;469    margin-top: 1rem;470}471 472.readiness-item {473    display: grid;474    grid-template-columns: auto 1fr;475    gap: 0.75rem;476    align-items: start;477}478 479.readiness-chip {480    min-width: 78px;481    text-align: center;482    border-radius: 999px;483    padding: 0.42rem 0.65rem;484    font-size: 0.72rem;485    font-weight: 700;486    text-transform: uppercase;487    letter-spacing: 0.08em;488}489 490.chip-ready { background: rgba(21, 128, 61, 0.12); color: var(--green); }491.chip-warn  { background: rgba(217, 119, 87, 0.14); color: var(--coral); }492 493.section-heading {494    margin: 1.8rem 0 1rem;495}496 497.section-title {498    margin: 0.5rem 0 0;499    font-size: 1.55rem;500    line-height: 1.1;501    letter-spacing: -0.03em;502    color: var(--ink);503}504 505.section-copy {506    color: var(--muted);507    margin: 0.35rem 0 0;508    max-width: 42rem;509    font-size: 0.92rem;510}511 512.metric-card {513    border-radius: 24px;514    padding: 1.35rem 1.2rem;515    text-align: center;516}517 518.metric-label {519    font-size: 0.76rem;520    color: var(--muted);521    text-transform: uppercase;522    letter-spacing: 0.08em;523    margin-bottom: 0.45rem;524    font-weight: 700;525}526 527.metric-before {528    font-size: 0.88rem;529    color: var(--muted);530}531 532.metric-after {533    font-size: 2rem;534    line-height: 1;535    font-weight: 700;536    margin: 0.4rem 0 0.3rem;537    letter-spacing: -0.04em;538}539 540.metric-delta {541    font-size: 0.9rem;542    font-weight: 700;543}544 545.best-banner {546    border-radius: 26px;547    padding: 1.25rem 1.35rem;548    margin-top: 1rem;549}550 551.best-banner-label {552    display: inline-flex;553    align-items: center;554    gap: 0.35rem;555    padding: 0.35rem 0.75rem;556    border-radius: 999px;557    background: rgba(21, 128, 61, 0.12);558    color: var(--green);559    font-size: 0.76rem;560    text-transform: uppercase;561    letter-spacing: 0.08em;562    font-weight: 700;563}564 565.best-banner-title {566    font-size: 1.4rem;567    line-height: 1.1;568    margin: 0.75rem 0 0.45rem;569    color: var(--ink);570}571 572.best-banner-copy {573    color: var(--muted);574    margin: 0;575    font-size: 0.93rem;576    line-height: 1.55;577}578 579.best-banner-code {580    margin-top: 0.9rem;581    padding: 0.9rem 1rem;582    border-radius: 18px;583    background: rgba(25, 33, 42, 0.05);584    border: 1px solid rgba(25, 33, 42, 0.08);585    font-family: "SFMono-Regular", ui-monospace, monospace;586    overflow-wrap: anywhere;587}588 589.result-pill-row {590    display: flex;591    flex-wrap: wrap;592    gap: 0.55rem;593    margin-top: 0.9rem;594}595 596.result-pill {597    border-radius: 999px;598    padding: 0.45rem 0.75rem;599    background: rgba(255, 255, 255, 0.7);600    border: 1px solid rgba(25, 33, 42, 0.08);601    color: var(--ink);602    font-size: 0.87rem;603}604 605.mol-card {606    border-radius: 28px;607    padding: 1.45rem;608    margin-bottom: 1.25rem;609    border-left: 5px solid rgba(25, 33, 42, 0.14);610}611 612.mol-card.best  { border-left-color: var(--green); }613.mol-card.good  { border-left-color: var(--blue); }614.mol-card.start { border-left-color: #7c8b99; }615 616.mol-structure-panel {617    display: flex;618    align-items: center;619    justify-content: center;620    min-height: 320px;621    padding: 1rem;622    border-radius: 24px;623    background: linear-gradient(180deg, #fbfcfe 0%, #eef3f8 100%);624    border: 1px solid rgba(71, 85, 105, 0.16);625    overflow: hidden;626}627 628.mol-structure-panel svg {629    width: 100%;630    height: auto;631    max-height: 300px;632}633 634.mol-structure-fallback {635    width: 100%;636    padding: 1rem;637    border-radius: 18px;638    background: rgba(255, 255, 255, 0.7);639    border: 1px dashed rgba(25, 33, 42, 0.14);640}641 642.change-note {643    margin-top: 0.55rem;644    font-size: 0.78rem;645    color: #b45309;646    font-weight: 700;647    letter-spacing: 0.03em;648}649 650.round-badge {651    display: inline-block;652    border-radius: 999px;653    padding: 0.42rem 0.78rem;654    font-size: 0.76rem;655    font-weight: 700;656    letter-spacing: 0.08em;657    text-transform: uppercase;658    margin-bottom: 0.35rem;659}660 661.badge-best    { background: rgba(21, 128, 61, 0.12); color: var(--green); }662.badge-start   { background: rgba(25, 33, 42, 0.07); color: #596674; }663.badge-attempt { background: rgba(37, 99, 235, 0.10); color: #1d4ed8; }664 665.score-section { margin: 0.75rem 0 0.1rem; }666 667.score-row {668    display: flex;669    align-items: center;670    margin: 0.72rem 0;671    gap: 0.7rem;672}673 674.score-label {675    font-size: 0.86rem;676    color: var(--muted);677    width: 170px;678    flex-shrink: 0;679}680 681.score-bar-bg {682    flex: 1;683    background: rgba(25, 33, 42, 0.08);684    border-radius: 999px;685    height: 10px;686    overflow: hidden;687}688 689.score-bar {690    height: 100%;691    border-radius: 999px;692    transition: width 0.3s ease;693}694 695.score-value {696    font-size: 0.88rem;697    font-weight: 700;698    width: 58px;699    text-align: right;700    flex-shrink: 0;701}702 703.score-delta {704    font-size: 0.76rem;705    width: 52px;706    text-align: right;707    flex-shrink: 0;708}709 710.green { color: var(--green); }711.red   { color: var(--red); }712.grey  { color: #8c99a5; }713 714.alert-pill {715    display: inline-block;716    border-radius: 999px;717    padding: 0.42rem 0.72rem;718    font-size: 0.76rem;719    font-weight: 700;720    margin-top: 0.35rem;721}722 723.pill-green  { background: rgba(21, 128, 61, 0.12); color: var(--green); }724.pill-yellow { background: rgba(183, 121, 31, 0.14); color: var(--gold); }725.pill-red    { background: rgba(180, 35, 24, 0.12); color: var(--red); }726 727.reasoning-box {728    background: linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.82));729    border-radius: 22px;730    padding: 0.9rem 1rem;731    margin-top: 1rem;732    border: 1px solid rgba(25, 33, 42, 0.08);733}734 735.reasoning-title {736    font-size: 0.76rem;737    font-weight: 700;738    letter-spacing: 0.08em;739    text-transform: uppercase;740    color: var(--teal);741    margin-bottom: 0.45rem;742}743 744.reasoning-text {745    font-size: 0.92rem;746    color: #32404d;747    line-height: 1.65;748}749 750.page-subtitle {751    font-size: 0.98rem;752    color: var(--muted);753    margin-top: -0.2rem;754    margin-bottom: 0;755}756 757.empty-grid {758    display: grid;759    grid-template-columns: repeat(3, minmax(0, 1fr));760    gap: 1rem;761    margin-top: 0.8rem;762}763 764.empty-step {765    font-size: 0.76rem;766    font-weight: 700;767    letter-spacing: 0.08em;768    text-transform: uppercase;769    color: var(--teal);770}771 772.empty-title {773    font-size: 1.1rem;774    font-weight: 700;775    margin: 0.5rem 0 0.45rem;776    color: var(--ink);777}778 779.empty-copy {780    margin: 0;781    color: var(--muted);782    line-height: 1.55;783    font-size: 0.9rem;784}785 786div[data-testid="stAlert"] {787    border-radius: 20px;788    border: 1px solid rgba(25, 33, 42, 0.08);789}790 791@media (max-width: 980px) {792    .hero-grid,793    .empty-grid {794        grid-template-columns: 1fr;795    }796 797    .score-label {798        width: 120px;799    }800}801</style>802""", unsafe_allow_html=True)803 804 805# ─── Helpers ──────────────────────────────────────────────────────────────────806def score_color(val, low, mid, high, invert=False):807    """Return hex color based on thresholds."""808    if invert:809        val = -val; low, mid, high = -high, -mid, -low810    if val >= high:   return "#16a34a"811    if val >= mid:    return "#f59e0b"812    return "#dc2626"813 814def bar_html(label, value, display, bar_pct, color, delta_str=""):815    return f"""816<div class="score-row">817  <span class="score-label">{label}</span>818  <div class="score-bar-bg">819    <div class="score-bar" style="width:{bar_pct:.0f}%;background:{color}"></div>820  </div>821  <span class="score-value" style="color:{color}">{display}</span>822  <span class="score-delta {'green' if delta_str.startswith('+') else 'red' if delta_str.startswith('-') else 'grey'}">{delta_str}</span>823</div>"""824 825def mol_scores_html(c, start=None):826    """Render score bars for one candidate."""827    def delta(key, fmt="+.2f"):828        if start is None: return ""829        d = (c.get(key) or 0) - (start.get(key) or 0)830        return f"{d:{fmt}}" if d != 0 else ""831 832    bbb   = (c.get("bbb_probability") or 0) * 100833    cns   = c.get("cns_mpo_score") or 0834    qed   = (c.get("qed_score") or 0) * 100835    flex  = c.get("rotatable_bonds") or 0836    alerts= c.get("num_alerts") or 0837 838    out = '<div class="score-section">'839    out += bar_html("Brain Penetration",  bbb,  f"{bbb:.0f}%",  bbb,840                    score_color(bbb, 50, 70, 80), delta("bbb_probability", "+.0%").replace("%","pp") if start else "")841    out += bar_html("CNS Activity Score", cns,  f"{cns:.1f}/5", cns/5*100,842                    score_color(cns, 3, 4, 4.5), delta("cns_mpo_score") if start else "")843    out += bar_html("Drug Likeness",      qed,  f"{qed:.0f}%",  qed,844                    score_color(qed, 49, 60, 67), delta("qed_score", "+.0%").replace("%","pp") if start else "")845    out += bar_html("Flexibility",        flex, f"{flex} bonds", max(0, 100-flex*10),846                    score_color(flex, 9, 7, 5, invert=True), delta("rotatable_bonds", "+d") if start else "")847 848    # Safety pill849    alert_color = "pill-green" if alerts == 0 else ("pill-yellow" if alerts <= 2 else "pill-red")850    alert_text  = "No safety alerts" if alerts == 0 else f"{alerts} safety alert{'s' if alerts>1 else ''}"851    out += f'<div style="margin-top:10px"><span class="alert-pill {alert_color}">{alert_text}</span></div>'852    out += "</div>"853    return out854 855def escape_html(text):856    return html.escape(str(text or "")).replace("\n", "<br>")857 858def truncate_text(text, limit=220):859    clean = " ".join(str(text or "").split())860    if len(clean) <= limit:861        return clean862    return clean[: limit - 1].rstrip() + "…"863 864def render_section_heading(title, subtitle="", kicker="Overview"):865    st.markdown(866        f"""867<div class="section-heading">868  <span class="section-kicker">{escape_html(kicker)}</span>869  <h2 class="section-title">{escape_html(title)}</h2>870  {f'<p class="section-copy">{escape_html(subtitle)}</p>' if subtitle else ''}871</div>872""",873        unsafe_allow_html=True,874    )875 876def readiness_item(label, detail, ready):877    chip_class = "chip-ready" if ready else "chip-warn"878    chip_text = "Ready" if ready else "Needed"879    return f"""880<div class="readiness-item">881  <div class="readiness-chip {chip_class}">{chip_text}</div>882  <div>883    <div class="overview-label">{escape_html(label)}</div>884    <div class="check-copy">{escape_html(detail)}</div>885  </div>886</div>887"""888 889def summarize_reasoning(text, is_start=False, limit=190):890    clean = str(text or "")891    clean = re.sub(r"```.*?```", " ", clean, flags=re.S)892    clean = re.sub(r"^#{1,6}\s*", "", clean, flags=re.M)893    clean = clean.replace("---", " ")894    clean = clean.replace("**", "").replace("`", "")895    clean = re.sub(r"\s+", " ", clean).strip()896 897    if not clean:898        return ""899 900    if "Rationale:" in clean:901        rationale = clean.split("Rationale:", 1)[1].strip()902    else:903        rationale = clean904 905    sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", rationale) if s.strip()]906    if not sentences:907        return truncate_text(clean, limit)908 909    if is_start:910        return truncate_text(sentences[0], limit)911 912    keywords = (913        "replace",914        "switch",915        "move",916        "moving",917        "add",918        "adding",919        "remove",920        "reduce",921        "shift",922        "place",923        "placing",924        "orientation",925        "scaffold",926    )927    selected = next((s for s in sentences if any(k in s.lower() for k in keywords)), sentences[0])928    selected = re.sub(r"^(let's|let me)\s+", "", selected, flags=re.I)929    selected = re.sub(r"^very close!?[\s,:-]*", "", selected, flags=re.I)930    selected = re.sub(r"^rationale:\s*", "", selected, flags=re.I)931    return truncate_text(selected, limit)932 933def _metric_priority_phrase(current, previous):934    if previous is None:935        return ""936 937    improvements = []938    bbb_delta = (current.get("bbb_probability") or 0) - (previous.get("bbb_probability") or 0)939    cns_delta = (current.get("cns_mpo_score") or 0) - (previous.get("cns_mpo_score") or 0)940    qed_delta = (current.get("qed_score") or 0) - (previous.get("qed_score") or 0)941    flex_delta = (previous.get("rotatable_bonds") or 0) - (current.get("rotatable_bonds") or 0)942    alert_delta = (previous.get("num_alerts") or 0) - (current.get("num_alerts") or 0)943 944    if bbb_delta > 0.015:945        improvements.append("improve brain penetration")946    if cns_delta > 0.05:947        improvements.append("lift CNS MPO")948    if qed_delta > 0.015:949        improvements.append("improve overall drug-likeness")950    if flex_delta > 0:951        improvements.append("reduce conformational flexibility")952    if alert_delta > 0:953        improvements.append("reduce structural risk")954 955    if not improvements:956        return "fine-tune the CNS profile"957    if len(improvements) == 1:958        return improvements[0]959    return f"{improvements[0]} and {improvements[1]}"960 961def _structural_change_phrase(reasoning):962    text = str(reasoning or "").lower()963 964    checks = [965        (("amide", "nitrile"), "replaced the amide with a nitrile handle"),966        (("n-methyl",), "N-methylated the side-chain amine"),967        (("fluoro",), "introduced a fluorine on the side chain"),968        (("fluorine",), "introduced a fluorine on the side chain"),969        (("methyl",), "added a small methyl substituent"),970        (("pyridine",), "swapped the phenyl ring for a pyridine"),971        (("rigid",), "rigidified the scaffold"),972        (("fused ring",), "rigidified the aryl region with a fused ring"),973        (("naphthal",), "expanded the aryl system to a fused bicyclic ring"),974        (("bioisostere",), "made a bioisosteric swap"),975    ]976    for needles, phrase in checks:977        if all(needle in text for needle in needles):978            return phrase979 980    match = re.search(r"replace(?:d)?\s+(.+?)\s+with\s+(.+?)(?:[.,;]|$)", text)981    if match:982        before = truncate_text(match.group(1), 35)983        after = truncate_text(match.group(2), 35)984        return f"replaced {before} with {after}"985 986    return "changed the highlighted region"987 988def summarize_candidate_change(current, previous=None):989    reasoning = current.get("reasoning") or ""990    if previous is None:991        qed  = (current.get("qed_score") or 0) * 100992        cns  = current.get("cns_mpo_score") or 0993        bbb  = (current.get("bbb_probability") or 0) * 100994        logs = current.get("log_s") or 0995        flex = current.get("rotatable_bonds") or 0996        return truncate_text(997            f"Baseline readout: drug-likeness {qed:.0f}%, CNS score {cns:.2f}/5, "998            f"brain penetration {bbb:.0f}%, solubility logS {logs:.2f}, "999            f"{flex} rotatable bonds. This is the starting point for optimization.",1000            220,1001        )1002 1003    change_phrase = _structural_change_phrase(reasoning)1004    why_phrase = _metric_priority_phrase(current, previous)1005    return truncate_text(1006        f"We {change_phrase} to {why_phrase}.",1007        210,1008    )1009 1010def brief_title_from_name(name):1011    text = str(name or "").strip()1012    if not text or text == "Custom molecule":1013        return "Custom lead brief"1014    return text.replace("→", "to")1015 1016def brief_body_html(goal_text):1017    text = str(goal_text or "").strip()1018    if not text:1019        return '<p class="brief-lead">Add the target profile you want the agent to optimise toward.</p>'1020 1021    sections = [part.strip() for part in text.split("\n\n") if part.strip()]1022    intro = sections[0] if sections else text1023    intro = intro.replace("Targets:", "").strip()1024    intro_lines = [line.strip(" -") for line in intro.splitlines() if line.strip()]1025    intro_text = " ".join(intro_lines[:2]).strip()1026 1027    targets = []1028    for section in sections[1:]:1029        for line in section.splitlines():1030            stripped = line.strip()1031            if stripped.startswith("-"):1032                targets.append(stripped.lstrip("- ").strip())1033 1034    if not targets:1035        for line in text.splitlines():1036            stripped = line.strip()1037            if stripped.startswith("-"):1038                targets.append(stripped.lstrip("- ").strip())1039 1040    html_parts = [f'<p class="brief-lead">{escape_html(truncate_text(intro_text, 260))}</p>']1041    if targets:1042        targets_html = "".join(1043            f'<div class="brief-target">{escape_html(truncate_text(item, 110))}</div>'1044            for item in targets[:4]1045        )1046        html_parts.append(f'<div class="brief-targets">{targets_html}</div>')1047    return "".join(html_parts)1048 1049def normalize_candidates(raw_candidates):1050    if not isinstance(raw_candidates, list):1051        raise ValueError("Saved run must contain a list of candidates.")1052 1053    normalized = []1054    for idx, item in enumerate(raw_candidates):1055        if not isinstance(item, dict):1056            continue1057        row = dict(item)1058        row["mol_index"] = row.get("mol_index", row.get("round", idx))1059        row["input_smiles"] = row.get("input_smiles") or row.get("canonical_smiles") or ""1060        row["reasoning"] = row.get("reasoning") or ""1061        normalized.append(row)1062 1063    if not normalized:1064        raise ValueError("No valid candidates found in saved run.")1065    return normalized1066 1067def build_run_payload(smiles, goal, candidates, preset_name, source="live"):1068    return {1069        "version": 1,1070        "saved_at": datetime.now().isoformat(timespec="seconds"),1071        "source": source,1072        "preset_name": preset_name,1073        "starting_smiles": smiles,1074        "goal": goal,1075        "candidate_count": len(candidates),1076        "candidates": candidates,1077    }1078 1079def load_run_payload(payload):1080    if isinstance(payload, list):1081        candidates = normalize_candidates(payload)1082        meta = {1083            "preset_name": "Saved run",1084            "starting_smiles": candidates[0].get("input_smiles", ""),1085            "goal": "",1086            "saved_at": "",1087            "source": "upload",1088        }1089        return candidates, meta1090 1091    if not isinstance(payload, dict):1092        raise ValueError("Unsupported saved run format.")1093 1094    candidates = normalize_candidates(payload.get("candidates") or payload.get("results") or [])1095    meta = {1096        "preset_name": payload.get("preset_name") or "Saved run",1097        "starting_smiles": payload.get("starting_smiles") or payload.get("smiles") or candidates[0].get("input_smiles", ""),1098        "goal": payload.get("goal") or "",1099        "saved_at": payload.get("saved_at") or "",1100        "source": payload.get("source") or "upload",1101    }1102    return candidates, meta1103 1104def apply_loaded_run(payload, source_label):1105    candidates, meta = load_run_payload(payload)1106    meta["source_label"] = source_label1107    st.session_state.candidates = candidates1108    st.session_state.completed = True1109    st.session_state.run_meta = meta1110    st.session_state.run_notice = f"Loaded {len(candidates)} candidates from {source_label}."1111 1112def save_run_snapshot(payload, runs_dir, latest_path):1113    runs_dir.mkdir(parents=True, exist_ok=True)1114    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")1115    run_path = runs_dir / f"run_{stamp}.json"1116    text = json.dumps(payload, indent=2)1117    latest_path.write_text(text, encoding="utf-8")1118    run_path.write_text(text, encoding="utf-8")1119    return run_path1120 1121def mol_svg_markup(smiles, previous_smiles=None):1122    """Return inline SVG markup for a SMILES string, optionally highlighting the changed region."""1123    try:1124        from rdkit import Chem1125        from rdkit.Chem import rdFMCS1126        from rdkit.Chem.Draw import rdMolDraw2D1127        mol = Chem.MolFromSmiles(smiles)1128        if mol is None:1129            return None, False1130 1131        highlight_atoms = []1132        highlight_bonds = []1133        highlight_atom_colors = {}1134        highlight_bond_colors = {}1135        highlight_atom_radii = {}1136 1137        if previous_smiles:1138            previous = Chem.MolFromSmiles(previous_smiles)1139            if previous is not None:1140                mcs = rdFMCS.FindMCS(1141                    [previous, mol],1142                    ringMatchesRingOnly=True,1143                    completeRingsOnly=True,1144                    atomCompare=rdFMCS.AtomCompare.CompareElements,1145                    bondCompare=rdFMCS.BondCompare.CompareOrder,1146                    timeout=2,1147                )1148                if mcs and mcs.smartsString:1149                    patt = Chem.MolFromSmarts(mcs.smartsString)1150                    if patt is not None:1151                        match = mol.GetSubstructMatch(patt)1152                        if match:1153                            core_atoms = set(match)1154                            highlight_atoms = [a.GetIdx() for a in mol.GetAtoms() if a.GetIdx() not in core_atoms]1155                            highlight_bonds = [1156                                b.GetIdx()1157                                for b in mol.GetBonds()1158                                if b.GetBeginAtomIdx() in highlight_atoms or b.GetEndAtomIdx() in highlight_atoms1159                            ]1160                            highlight_atom_colors = {idx: (0.98, 0.83, 0.18) for idx in highlight_atoms}1161                            highlight_bond_colors = {idx: (0.98, 0.83, 0.18) for idx in highlight_bonds}1162                            highlight_atom_radii = {idx: 0.5 for idx in highlight_atoms}1163 1164        Chem.rdDepictor.Compute2DCoords(mol)1165        drawer = rdMolDraw2D.MolDraw2DSVG(420, 300)1166        opts = drawer.drawOptions()1167        opts.bondLineWidth = 21168        opts.padding = 0.051169        opts.multipleBondOffset = 0.181170        opts.fixedBondLength = 421171        opts.clearBackground = False1172        opts.fillHighlights = False1173        opts.highlightRadius = 0.31174        opts.highlightBondWidthMultiplier = 181175        opts.atomHighlightsAreCircles = True1176        opts.useDefaultAtomPalette()1177        opts.updateAtomPalette({1178            6: (0.11, 0.16, 0.23),  # carbon1179            7: (0.10, 0.35, 0.82),  # nitrogen1180            8: (0.84, 0.15, 0.16),  # oxygen1181            9: (0.00, 0.62, 0.69),  # fluorine1182            15: (0.58, 0.20, 0.78), # phosphorus1183            16: (0.84, 0.45, 0.08), # sulfur1184            17: (0.09, 0.55, 0.35), # chlorine1185            35: (0.65, 0.25, 0.16), # bromine1186            53: (0.43, 0.34, 0.78), # iodine1187        })1188        opts.setHighlightColour((0.95, 0.69, 0.12))1189        opts.setBackgroundColour((1.0, 1.0, 1.0, 0.0))1190        rdMolDraw2D.PrepareAndDrawMolecule(1191            drawer,1192            mol,1193            highlightAtoms=highlight_atoms,1194            highlightAtomColors=highlight_atom_colors,1195            highlightAtomRadii=highlight_atom_radii,1196            highlightBonds=highlight_bonds,1197            highlightBondColors=highlight_bond_colors,1198        )1199        drawer.FinishDrawing()1200        svg = drawer.GetDrawingText()

Showing the first 1,200 of 1945 lines. Download the file for the rest.