""" Biopesticide-AI Gradio UI v2 -- high-end startup-grade interface. Redesigned with: - Hero section with project pitch and key metrics - Tabbed workflow: Design | Analytics | Safety | Regulatory | About - Data visualizations (efficacy chart, off-target heatmap, half-life chart) - Candidate cards instead of plain tables - CSV/JSON export buttons - Professional color scheme and typography - Loading states and empty states - Backend status strip with live model/LLM info Usage: python -m bioai.ui.gradio_app # launch on 0.0.0.0:7860 python -m bioai.ui.gradio_app --port 8080 python -m bioai.ui.gradio_app --share # public share link """ from __future__ import annotations import argparse import csv import io import json import os import sys import time from pathlib import Path import gradio as gr # Make sure we can import bioai from anywhere _PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(_PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(_PROJECT_ROOT)) from bioai.orchestrator import BiopesticideOrchestrator # noqa: E402 from bioai.sequence_utils import SAFETY_SPECIES, PEST_SPECIES # noqa: E402 from bioai.ui.charts import efficacy_bar_chart, offtarget_heatmap, halflife_chart # noqa: E402 # ───────────────────────────────────────────────────────────────────────────── # Branding # ───────────────────────────────────────────────────────────────────────────── TITLE = "Biopesticide-AI" TAGLINE = "Design species-specific dsRNA biopesticides in minutes, not months" SUBTITLE = "Local Llama 3.2 3B + PyTorch + 14-species safety panel + physics-informed fate model" EXAMPLES = [ ["Brown planthopper infestation in my rice paddy near Coimbatore, Tamil Nadu. Severity moderate, second generation this season.", 10], ["Fall armyworm outbreak in maize field in Karnataka. Severe damage on 30% of plants, spreading fast.", 10], ["Desert locust swarm reported in wheat fields of Rajasthan. Need rapid response biopesticide design.", 10], ["Colorado potato beetle devastating my potato crop in Himachal Pradesh. Resistance to neonicotinoids suspected.", 8], ["Tobacco whitefly infestation in tomato greenhouse in Maharashtra. Mild severity but persistent.", 5], ["Peach-potato aphid outbreak in vegetable garden. Organic farm, need bee-safe solution.", 5], ] # ───────────────────────────────────────────────────────────────────────────── # Singleton orchestrator # ───────────────────────────────────────────────────────────────────────────── _ORCHESTRATOR: BiopesticideOrchestrator | None = None def get_orchestrator() -> BiopesticideOrchestrator: global _ORCHESTRATOR if _ORCHESTRATOR is None: print("[gradio_app] initializing orchestrator...") _ORCHESTRATOR = BiopesticideOrchestrator() print(f"[gradio_app] backend = {type(_ORCHESTRATOR.ranker.sirna_model).__name__}, degraded_mode = {_ORCHESTRATOR.degraded_mode}") return _ORCHESTRATOR # ───────────────────────────────────────────────────────────────────────────── # HTML/CSS helpers # ───────────────────────────────────────────────────────────────────────────── CUSTOM_CSS = """ :root { --bioai-bg: #f4f5f6; --bioai-surface: #ffffff; --bioai-card: #ecedee; --bioai-accent: #2f86b2; --bioai-accent-2: #ba5a6a; --bioai-text: #242627; --bioai-muted: #71777a; --bioai-border: #a1b9c6; --bioai-success: #449f63; --bioai-warning: #b69045; --bioai-error: #964039; } .gradio-container { max-width: 1200px !important; } .bioai-hero { background: linear-gradient(135deg, #4a616c 0%, #2f86b2 100%); color: white; padding: 32px 28px; border-radius: 12px; margin-bottom: 20px; } .bioai-hero h1 { font-size: 32px !important; font-weight: 800 !important; margin: 0 0 8px 0 !important; letter-spacing: -0.5px; } .bioai-hero p { font-size: 14px !important; margin: 4px 0 !important; opacity: 0.92; } .bioai-hero .tagline { font-size: 18px !important; font-weight: 500 !important; margin: 12px 0 4px 0 !important; } .bioai-metric-row { display: flex; gap: 24px; margin-top: 20px; flex-wrap: wrap; } .bioai-metric { text-align: center; } .bioai-metric .num { font-size: 28px; font-weight: 800; color: white; } .bioai-metric .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.85; margin-top: 2px; } .bioai-card { background: var(--bioai-surface); border: 1px solid var(--bioai-border); border-radius: 8px; padding: 16px 20px; margin-bottom: 12px; } .bioai-status-strip { display: flex; gap: 8px; justify-content: center; margin: 8px 0 16px 0; flex-wrap: wrap; } .bioai-pill { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; color: white; } .bioai-stats-strip { display: flex; gap: 28px; justify-content: center; margin: 12px 0 20px 0; padding: 14px 0; border-top: 1px solid var(--bioai-border); border-bottom: 1px solid var(--bioai-border); flex-wrap: wrap; } .bioai-stat { text-align: center; } .bioai-stat .num { font-size: 22px; font-weight: 700; color: var(--bioai-accent); } .bioai-stat .lbl { font-size: 10px; color: var(--bioai-muted); text-transform: uppercase; letter-spacing: 0.5px; } .bioai-candidate-card { background: var(--bioai-surface); border: 1px solid var(--bioai-border); border-left: 4px solid var(--bioai-accent); border-radius: 6px; padding: 14px 18px; margin-bottom: 10px; } .bioai-candidate-card .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .bioai-candidate-card .rank { font-size: 11px; font-weight: 700; color: var(--bioai-accent); text-transform: uppercase; } .bioai-candidate-card .seq { font-family: monospace; font-size: 14px; color: var(--bioai-text); font-weight: 600; } .bioai-candidate-card .metrics { display: flex; gap: 16px; font-size: 12px; color: var(--bioai-muted); } .bioai-candidate-card .metric-val { font-weight: 700; color: var(--bioai-text); } .bioai-footer { text-align: center; color: var(--bioai-muted); font-size: 11px; padding: 16px 0; border-top: 1px solid var(--bioai-border); margin-top: 24px; } """ def _hero_html() -> str: return f"""
{TAGLINE}
{SUBTITLE}
No pest report parsed.
" species = pest.get("pest_species", pest.get("species", "unknown")) crop = pest.get("crop", "unknown") severity = pest.get("severity", "unknown") location = pest.get("location", "unknown") notes = pest.get("notes", "") notes_html = f"| Target species | {species} |
| Crop | {crop} |
| Severity | {severity} |
| Location | {location} |
No candidates generated. Run the design pipeline first.
" cards = [] for i, c in enumerate(candidates, 1): seq = c.get("sirna_seq", "") eff = c.get("efficacy", 0) ot = c.get("offtarget_max", 0) hl = c.get("half_life_hours", 0) score = c.get("final_score", 0) hl_days = hl / 24 # Risk tier color for the left border if score > 0.3: border_color = "#449f63" # success elif score > 0.15: border_color = "#b69045" # warning else: border_color = "#964039" # error cards.append(f"""Please describe your pest problem above.
" return ( empty_status, gr.update(value=[]), "No pest report parsed.
", "", None, None, None, # charts "", "_(no safety cards generated)_", "_(no regulatory memo generated)_", "", "", ) orch = get_orchestrator() t0 = time.time() try: result = orch.design(user_text, top_k=int(top_k)) except Exception as e: import traceback tb = traceback.format_exc() err = f"Pipeline error: {type(e).__name__}: {e}
{tb}"
return (err, gr.update(value=[]), "No pest report parsed.
", "", None, None, None, "", "_(no safety cards generated)_", "_(no regulatory memo generated)_", "", "") elapsed = time.time() - t0 candidates = result.get("candidates", []) status_html = _backend_status_html(orch) + _stats_strip(result, elapsed) candidates_rows = _candidates_to_dataframe(candidates) candidates_html = _candidate_cards_html(candidates) pest_html = _pest_report_html(result.get("pest_report", {})) safety_md = _safety_cards_md(result) memo_md = result.get("regulatory_memo", "_(no regulatory memo generated)_") csv_str = _export_csv(candidates) json_str = _export_json(result) # Generate charts efficacy_chart = efficacy_bar_chart(candidates) if candidates else None offtarget_chart = offtarget_heatmap(candidates, SAFETY_SPECIES) if candidates else None halflife_chart_path = halflife_chart(candidates) if candidates else None return ( status_html, candidates_rows, candidates_html, pest_html, "", efficacy_chart, offtarget_chart, halflife_chart_path, safety_md, memo_md, csv_str, json_str, ) # ───────────────────────────────────────────────────────────────────────────── # Gradio Blocks UI # ───────────────────────────────────────────────────────────────────────────── def build_ui() -> gr.Blocks: demo = gr.Blocks(title="Biopesticide-AI") with demo: # ─── Hero ───────────────────────────────────────────────────────── gr.HTML(_hero_html()) # ─── Initial status ─────────────────────────────────────────────── try: orch = get_orchestrator() initial_status = _backend_status_html(orch) + "Failed to initialize orchestrator: {e}
" status_box = gr.HTML(value=initial_status, label="Pipeline status") # ─── Main tabs ──────────────────────────────────────────────────── with gr.Tabs(): # ── Tab 1: Design ───────────────────────────────────────────── with gr.Tab("Design", id=0): gr.Markdown("### Describe your pest problem in plain English") user_text = gr.Textbox( label="Pest report", placeholder="e.g. 'Brown planthopper infestation in my rice paddy near Coimbatore, Tamil Nadu. Severity moderate, second generation this season.'", lines=4, value=EXAMPLES[0][0], ) with gr.Accordion("Advanced settings", open=False): top_k = gr.Slider(minimum=1, maximum=20, value=10, step=1, label="Top-K candidates to return") run_btn = gr.Button("Design dsRNA candidates", variant="primary", size="lg") gr.Examples( examples=EXAMPLES, inputs=[user_text, top_k], label="Try one of these preset pest reports", ) gr.Markdown("---") gr.Markdown("### Parsed pest report") pest_html = gr.HTML(value="Run the pipeline to see the parsed pest report.
") gr.Markdown("### Top candidates") candidates_html = gr.HTML(value="Run the pipeline to see ranked candidates.
") # Hidden dataframe for CSV export compatibility candidates_table = gr.Dataframe( visible=False, headers=["Rank", "siRNA", "Efficacy", "Off-target", "Half-life", "Score"], value=[], ) # ── Tab 2: Analytics ────────────────────────────────────────── with gr.Tab("Analytics", id=1): gr.Markdown("### Efficacy scores") efficacy_img = gr.Image(label="", show_label=False, height=350) gr.Markdown("### Off-target risk heatmap (candidates x 14 safety species)") offtarget_img = gr.Image(label="", show_label=False, height=400) gr.Markdown("### Environmental fate (predicted half-life)") halflife_img = gr.Image(label="", show_label=False, height=350) # ── Tab 3: Safety ───────────────────────────────────────────── with gr.Tab("Safety cards", id=2): gr.Markdown("### Per-candidate safety cards (generated by local Llama 3.2 3B)") safety_md = gr.Markdown(value="Run the pipeline to see safety cards for the top candidates.
") # ── Tab 4: Regulatory ───────────────────────────────────────── with gr.Tab("Regulatory memo", id=3): gr.Markdown("### EPA-style regulatory memo (generated by local Llama 3.2 3B)") memo_md = gr.Markdown(value="Run the pipeline to see the regulatory memo.
") # ── Tab 5: Export ───────────────────────────────────────────── with gr.Tab("Export", id=4): gr.Markdown("### Download design results") gr.Markdown("Export the top candidates as CSV or the full design result as JSON for downstream analysis.") csv_text = gr.Textbox(label="CSV (copy below or use the download button)", lines=10, interactive=False) csv_btn = gr.DownloadButton("Download CSV", value=None) json_text = gr.Textbox(label="JSON (copy below or use the download button)", lines=15, interactive=False) json_btn = gr.DownloadButton("Download JSON", value=None) # ── Tab 6: About ────────────────────────────────────────────── with gr.Tab("About", id=5): gr.Markdown(""" ### About Biopesticide-AI **Biopesticide-AI** is an end-to-end pipeline for designing dsRNA biopesticides against agricultural pests. It compresses the traditional 3-6 month wet-lab design loop into a 4-minute computational pipeline that any farmer, agronomist, or cooperative can run from a laptop. **Pipeline:** 1. Farmer describes pest problem in plain English 2. Local Ollama Llama 3.2 3B parses the report into a structured design spec 3. PyTorch backend tiles pest transcripts into 200-nt dsRNA precursors 4. Dicer-style dicing produces 21-nt siRNAs 5. Dilated CNN (HyenaDNA-inspired) scores each siRNA for efficacy 6. K-mer index checks off-target risk against 14 non-target species 7. Physics-Informed Neural Network predicts environmental half-life 8. Learned ranker combines all scores into a final candidate ranking 9. Llama 3.2 3B generates safety cards + EPA-style regulatory memo **14-species safety panel** covers pollinators (honeybee, bumblebee, leafcutter bee), beneficial predators (ladybug, lacewing), soil invertebrates (earthworm), aquatic organisms (water flea, zebrafish), livestock (cattle, zebu, chicken, sheep, pig), and human safety. **7 pest targets** include brown planthopper (rice), fall armyworm (maize), desert locust (wheat), striped stem borer (rice), peach-potato aphid (vegetables), Colorado potato beetle (potato), and tobacco whitefly (tomato). **Cost: $0 per design.** All compute is local. No cloud API spend. Built for the AMD Developer Hackathon Unicorn Track. MIT licensed. """) # ─── Footer ─────────────────────────────────────────────────────── gr.HTML( "" ) # ─── Wire up ────────────────────────────────────────────────────── run_btn.click( design_handler, inputs=[user_text, top_k], outputs=[ status_box, candidates_table, candidates_html, pest_html, status_box, # update status after run (same component) efficacy_img, offtarget_img, halflife_img, safety_md, memo_md, csv_text, json_text, ], ) return demo # ───────────────────────────────────────────────────────────────────────────── # Entry point # ───────────────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="Biopesticide-AI Gradio UI v2") parser.add_argument("--host", default="0.0.0.0", help="bind host (default 0.0.0.0)") parser.add_argument("--port", type=int, default=7860, help="bind port (default 7860)") parser.add_argument("--share", action="store_true", help="create a public share link") parser.add_argument("--max-threads", type=int, default=4, help="max concurrent requests") args = parser.parse_args() print("[gradio_app] pre-initializing orchestrator...") get_orchestrator() demo = build_ui() print(f"[gradio_app] launching on http://{args.host}:{args.port}") demo.launch( server_name=args.host, server_port=args.port, share=args.share, max_threads=args.max_threads, show_error=True, css=CUSTOM_CSS, ) if __name__ == "__main__": main()