# Project export: ImmunoVerse: Composable In Silico Gene Therapy Screening

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: A decentralized network of AI specialist agents running patient-specific, systems-level safety screenings for gene therapies in seconds to catch clinical failures before the wet lab.
- Devpost: https://devpost.com/software/immunoverse-composable-in-silico-gene-therapy-screening
- GitHub: https://github.com/Gr1nx-bitbit/ai_hackathon_2026.git
- Video: https://www.youtube.com/embed/prQyDmv5K0c?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Solo Hack)
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

There isn't a very mature way to put this, but my inspiration stems from a childhood fear of death - more specifically, watching a scene where a dead skeleton lay on the ground in Disney's Tangled kicked this whole thing off. After worrying about it for years, I started wondering why we died in the first place. The strongest indicator seemed to be the accumulation of harmful genetic mutations over time: given enough mutations, cells either produce faulty proteins that the immune system flags for destruction, or they become senescent and stop dividing. This led me to rediscover the idea of extending longevity, or possibly even achieving biological immortality, by editing the genome back to healthier states, or toward beneficial mutations as our environments change. Gene therapy is the most credible path I've found toward that goal. But there's a gap between the vision and the clinic: the therapies that reach patients today fail at an alarming rate not because the science is wrong, but because safety problems are discovered too late. ImmunoVerse is my attempt to close that gap.

### What it does

ImmunoVerse screens gene therapy candidates for safety before they reach the wet lab. It is a patient-personalized, multi-stage in silico safety screening system for gene editing therapies. Given an edited protein sequence and a patient's HLA profile, it evaluates two independent failure modes before a therapy ever reaches the wet lab: Immunogenic rejection - will the immune system recognize the modified protein as foreign, mount a T-cell or antibody response, and destroy the edited cells? Systems-level disruption - even if the immune system tolerates the edit, will it destabilize cellular function by introducing a cryptic splice site, disrupting transcription factor binding, or triggering apoptotic signaling? The pipeline runs four stages in sequence: structural modeling (ESMFold + SASA surface exposure), HLA binding prediction (NetMHCpan Class I + II via IEDB), parallel immune reactivity analysis (T-cell and B-cell branches), and whole-transcriptome systems dynamics. A final LLM node - powered by ASI:One or Claude - synthesizes the results into a plain-language clinical report with a headline verdict, per-stage findings, risk rationale, and actionable mitigation suggestions. Each stage is implemented as an independent specialist agent registered on Agentverse. Any AI agent on the network can call individual stages directly or route through the orchestrator for a full pipeline run - making the system composable infrastructure for the broader biotech AI ecosystem, not just a one-off screening tool. The individual tools - ESMFold, NetMHCpan, BepiPred - are already used in research labs. They exist, but the integration doesn't. Commercial immunogenicity services are out there but are slow, expensive, and black boxes. No one has an open, composable, multi-agent pipeline that exposes each stage individually as a callable API.

### How we built it

The pipeline has two parallel implementations that share the same underlying tool layer: LangGraph StateGraph drives the Streamlit web app and terminal demo. The graph uses conditional edges for the Stage 1 retry loop (low-confidence structural prediction triggers a fallback re-run), parallel Send dispatch for the Stage 3 T-cell and B-cell branches, and a single aggregation node that computes the weighted risk vector before the LLM report node. Fetch AI uAgents provides the multi-agent network layer. Seven specialist agents, one per pipeline stage plus an orchestrator, are independently registered on Agentverse using register_chat_agent with cryptographic identity derived from seed phrases. The orchestrator holds the addresses of all specialists and routes messages through the Agentverse relay. Each agent can also be called directly by any Agentverse-compatible client. Real biology tools are wired at Stage 1 (ESMFold via the ESM Atlas REST API), Stage 2 (IEDB's NetMHCpan-4.1 and NetMHCIIpan-4.0), and Stage 3b (BepiPred via IEDB). Stages 3a (NetTCR-2.0) and 4 (GenBio AI AIDO) use high-fidelity mock implementations that replicate the expected output schemas. All tools share a common abstract base class layer, so swapping a mock for a real implementation requires no changes to the pipeline graph or agent logic.

### Challenges we ran into

Agentverse endpoint routing was the hardest bug to diagnose. When agents were registered with http://localhost:{port}/submit endpoints via the registration API, Agentverse attempted push-delivery to localhost - which it can't reach from its servers. The fix required running all agents with mailbox=True first so the uAgents runtime updates the Almanac with the relay URL, then re-running registration to store the correct endpoint. The client also needed to route via the Almanac rather than Agentverse's push-delivery system, or it would hit the same localhost failure. Parallel fan-out in LangGraph required understanding the Send primitive. Standard edges broadcast state to both Stage 3 branches simultaneously, but without Send the graph couldn't dispatch different payloads to each branch independently. Getting the join node to wait for both branches to complete before proceeding to Stage 4 required careful state merging. Schema consistency across both implementations was a recurring constraint. The Pydantic models powering the LangGraph state also needed to survive serialization through uAgents JSON message passing, which caught several field name mismatches between what the orchestrator sent and what the specialist agents expected. Real biology API quirks - IEDB's rate limits, ESMFold's sequence length constraints, and BepiPred's non-standard response format meaning each required custom retry logic and fallback handling.

### Accomplishments we're proud of

Running a biologically meaningful end-to-end screen from a raw sequence to plain-language clinical verdict in under 30 seconds, personalized to a patient's HLA haplotype, is something that would have taken a computational biologist days to do manually a few years ago. Getting seven specialist agents independently registered and callable on Agentverse, each with a clean public message contract, demonstrates that AI-native biomedical infrastructure is achievable today. The LOW_IMMUNOGENIC scenario is my favorite result: the HLA bindings are low and so there is little T-Cell reactivity which wouldn't reject the KRAS edit. However, the BepiPred B-Cell reactivity is high which means the edit would get flagged. Stage 4 catches the RAS/MAPK pathway disruption independently at the systems level. That's the case for always running the full pipeline - if there was neither high T-Cell or B-Cell reactivity experimentation would continue, but having insight into how these edits work on a cellular level can save a lot of time and money.

### What we learned

Designing message contracts first and implementations second makes multi-agent systems dramatically easier to debug. Every inter-agent message in ImmunoVerse is a typed Pydantic model with a single serialization path and so when something breaks, it's immediately obvious which stage sent the wrong shape. Agentverse's relay model (agent polls Agentverse, not the other way around) is the right mental model for agents running behind NAT or on developer laptops. The documentation undersells this; most of the routing confusion stemmed from conflating push-delivery with mailbox polling. Designing the risk scoring and clinical report to communicate uncertainty honestly - not overstating confidence in a result that's only as good as its weakest tool - required as much thought as the engineering.

### What's next

The immediate next step is retrospective validation - running the pipeline against gene therapy candidates with known clinical immunogenicity outcomes to measure sensitivity and specificity end-to-end. That's the study that moves this from a promising screening tool to an evidence-based one. The two mock stages are the next most important near-term targets. Wiring NetTCR-2.0 behind a local Docker service and connecting to the GenBio AI AIDO API for real transcriptome perturbation prediction would make the systems-level signal clinically actionable rather than illustrative. Beyond completing the tool layer, the most compelling direction is cohort-level analysis: given a proposed edit and a population's HLA frequency distribution, what fraction of patients would be at high risk? That's the question gene therapy developers actually need answered before Phase I. The composable Agentverse architecture makes it straightforward to fan out hundreds of personalized pipeline runs in parallel. Longer term, integrating with clinical trial databases to validate predictions against known immunogenicity outcomes would let the risk model be calibrated on real-world data - closing the loop between in silico prediction and clinical observation that is currently the biggest gap in the field.

## README (from the GitHub repository)

# Gene Therapy Safety Screener

Multi-stage, patient-personalised in silico immunogenicity and systems safety screening for gene editing therapies. Detects immune rejection risk and cell-autonomous disruption before a therapy reaches the wet lab.

## Problem

Gene therapy edits can trigger two independent failure modes:

1. **Immunogenic rejection** — the immune system recognises the modified protein as foreign, mounts a T-cell or antibody response, and destroys transduced cells.
2. **Systems disruption** — even if the immune system tolerates the edit, cellular machinery may not: cryptic splice sites, altered transcription factor binding, or apoptotic signalling.

Current in-clinic safety screening is slow, expensive, and happens late in development. This pipeline runs in seconds.

## Pipeline

```
Input: patient sequence + edit positions + HLA profile
  │
  ▼
Stage 1 — Structural Modelling
  ESMFold (ESM Atlas REST API) → pLDDT confidence + SASA surface exposure
  Low confidence → fallback retry
  │
  ▼
Stage 2 — HLA Binding Prediction
  IEDB REST API (NetMHCpan-4.1 Class I + NetMHCIIpan-4.0 Class II)
  Both gates clear → early exit (safe — no peptide can be presented)
  │
  ▼
Stage 3 — Immune Reactivity [parallel branches]
  3a: T-cell — NetTCR-2.0 TCR binding probability
  3b: B-cell — BepiPred via IEDB (linear epitope prediction)
  Either flag → early exit (high risk — adaptive immunity activated)
  │
  ▼
Stage 4 — Systems Dynamics
  Transcriptome perturbation, splice-site disruption, apoptosis signalling
  │
  ▼
Report — ASI:One or Claude (claude-opus-4-6)
  Structured clinical summary: headline, stage findings, risk rationale,
  mitigation suggestions, confidence caveats
  │
  ▼
Output: SAFE / CAUTION / HIGH RISK + risk vector + clinical report
```

## Architecture

The pipeline has two parallel implementations that share the same tool layer:

- **LangGraph graph** (`src/agents/graph.py`) — compiled StateGraph used by the Streamlit web app and the terminal demo
- **Fetch AI multi-agent bureau** (`fetch/bureau_multi.py`) — seven specialist uAgents (one per stage + an orchestrator) that communicate via the Agentverse message protocol

Both use the same `PipelineRequest` / `PipelineResponse` contract and the same underlying tools.

## Real Tools

| Stage | Tool | Status |
|---|---|---|
| Stage 1 | ESMFold via ESM Atlas REST API | `ESMFOLD_ENABLED=1` |
| Stage 2 | IEDB REST API (NetMHCpan / NetMHCIIpan) | `IEDB_ENABLED=1` |
| Stage 3b | BepiPred via IEDB REST API | `BEPIPRED_ENABLED=1` |
| Report | ASI:One | `ASI1_API_KEY=<key>` |
| Report | Claude (claude-opus-4-6) | `ANTHROPIC_API_KEY=<key>` |

Stages 3a (NetTCR-2.0) and 4 (GenBio AIDO) have mock implementations. The abstract base classes in `src/tools/base.py` map 1:1 to real implementations — no graph changes required to wire them.

## Quick Start

See [SETUP.md](SETUP.md) for full installation and configuration instructions.

```bash
# Install
uv sync

# Run the web app (recommended)
uv run streamlit run app.py

# Run the terminal demo (no API keys required)
uv run python demo.py

# Run with all available real tools
export ESMFOLD_ENABLED=1
export IEDB_ENABLED=1
export BEPIPRED_ENABLED=1
export ASI1_API_KEY=<your key>
uv run streamlit run app.py
```

## Fetch AI Multi-Agent Demo

Each of the 7 specialist agents + orchestrator runs in its own terminal with an Agentverse mailbox. The full pipeline routes through Agentverse's relay — agents are individually reachable from any Agentverse client.

```bash
# One-time setup: register all 7 agents on Agentverse
export AGENTVERSE_KEY=<your Agentverse API key>
uv run python register_agents.py

# Start all agents (one terminal each, all with AGENTVERSE_MAILBOX=1)
export AGENTVERSE_MAILBOX=1 && uv run python -m fetch.multi.orchestrator
export AGENTVERSE_MAILBOX=1 && uv run python -m fetch.multi.stage1_agent
# ... (repeat for stage2, stage3_tcr_agent, stage3_bcell_agent, stage4_agent, report_agent)

# Re-register after agents are running (updates Agentverse with relay URLs)
uv run python register_agents.py

# Run the full pipeline via the orchestrator — do NOT set AGENTVERSE_MAILBOX on the client
export ORCHESTRATOR_AGENT_ADDRESS=agent1q...   # from orchestrator startup log
uv run python -m fetch.demo_client --target orchestrator --scenario high_risk

# Call any individual stage directly
export STAGE2_AGENT_ADDRESS=agent1q...
uv run python -m fetch.demo_client --target stage2 --scenario high_risk
```

Available targets: `orchestrator`, `stage1`, `stage2`, `stage3-tcr`, `stage3-bcell`, `stage4`.  
Available scenarios: `high_risk`, `early_exit`, `systems_failure`, `all_clear`.

## Project Structure

```
src/
  models/pipeline.py         Pydantic schemas (stage I/O + PipelineState)
  tools/
    base.py                  Abstract base classes for each tool type
    registry.py              Tool factory — controls mock / real selection
    mock/                    Mock implementations (all 4 stages)
    real/
      structural_tool.py     ESMFold via ESM Atlas + HuggingFace
      hla_tool.py            IEDB NetMHCpan Class I + II
      bcell_tool.py          BepiPred via IEDB
    report/
      asi1_tool.py           ASI:One clinical report
      claude_tool.py         Claude (claude-opus-4-6) clinical report
      mock_tool.py           Canned scenario-specific reports
  agents/
    nodes.py                 LangGraph node functions and routing logic
    graph.py                 StateGraph definition and compilation
  scoring.py                 Risk vector aggregation

fetch/
  messages.py                Public message contract
  pipeline_agent.py          Monolithic uAgent
  bureau.py                  Monolithic bureau
  bureau_multi.py            Multi-agent bureau — starts 7 agents + orchestrator, no auto-run
  demo_client.py             Interactive client — full pipeline or individual stage
  client_agent.py            Batch client — sends all 4 scenarios to the orchestrator
  multi/                     Specialist agent implementations

register_agents.py           One-shot Agentverse registration for all 7 agents
app.py                       Streamlit web app
demo.py                      Rich terminal demo — 4 scenarios
```

## Scenarios

| Scenario | Description |
|---|---|
| A — HIGH_RISK | Full pipeline, strong HLA binder, TCR + B-cell reactivity flags, high_risk result |
| B — EARLY_EXIT | Pipeline terminates at Stage 2 (both HLA class thresholds clear) |
| C — SYSTEMS_FAILURE | Immune system tolerates the edit; Stage 4 detects cellular disruption |
| D — ALL_CLEAR | Full pipeline, safe result |


## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 286 KB.
- Anthropic (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Streamlit (technology) — detected in the code

## Codebase structure (from repository index)

### Files (59 of 59)

```
.gitignore
.python-version
.streamlit/config.toml
app.py
ARCHITECTURE.md
conftest.py
demo.py
fetch/__init__.py
fetch/bureau_multi.py
fetch/bureau.py
fetch/client_agent.py
fetch/demo_client.py
fetch/messages.py
fetch/multi/__init__.py
fetch/multi/messages.py
fetch/multi/orchestrator.py
fetch/multi/report_agent.py
fetch/multi/session.py
fetch/multi/stage1_agent.py
fetch/multi/stage2_agent.py
fetch/multi/stage3_bcell_agent.py
fetch/multi/stage3_tcr_agent.py
fetch/multi/stage4_agent.py
fetch/pipeline_agent.py
main.py
PROBLEM.md
pyproject.toml
README.md
register_agents.py
requirements.txt
SETUP.md
src/__init__.py
src/agents/__init__.py
src/agents/graph.py
src/agents/nodes.py
src/models/__init__.py
src/models/pipeline.py
src/scoring.py
src/tools/__init__.py
src/tools/base.py
src/tools/mock/__init__.py
src/tools/mock/stage1.py
src/tools/mock/stage2.py
src/tools/mock/stage3.py
src/tools/mock/stage4.py
src/tools/real/__init__.py
src/tools/real/bcell_tool.py
src/tools/real/hla_tool.py
src/tools/real/structural_tool.py
src/tools/registry.py
src/tools/report/__init__.py
src/tools/report/asi1_tool.py
src/tools/report/claude_tool.py
src/tools/report/context.py
src/tools/report/mock_tool.py
summary.md
tests/test_structural_tool.py
uagents_core.log
uv.lock
```

### Dependencies

- pyproject.toml: anthropic@>=0.111.0, biopython@>=1.87, langchain-core@>=0.2.0, langgraph@>=0.2.0, openai@>=2.43.0, plotly@>=6.8.0, pydantic@>=2.0.0, rich@>=13.0.0, streamlit@>=1.58.0, uagents@>=0.25.2
- requirements.txt: anthropic@>=0.40.0, biopython@>=1.81, langchain-core@>=0.2.0, langgraph@>=0.2.0, pydantic@>=2.0.0, requests@>=2.28.0, rich@>=13.0.0, uagents@>=0.25.0

### Recent commits (newest first)

- don't need slides
- changed names for pipeline scenarios
- fixed LangGraph stage 2 early exit bug
- updated pipeline so no early exits happen
- decomposed agents and registered them. created demos for accessing full pipeline through orchestrator or accessing individual stages
- project

## Key source files (fetched from GitHub, selected and truncated for size)

### summary.md

```markdown
# Multiscale In Silico Immunogenicity Pipeline

A patient-specific, multi-stage safety screening system for gene editing therapies. Given an edited protein sequence and a patient's HLA profile, the pipeline evaluates two independent failure modes before a therapy reaches the clinic:

1. **Immunogenic rejection** — will the immune system destroy the edited cells?
2. **Systems-level disruption** — will the edit destabilise cellular function even if the immune system tolerates it?

The pipeline is built on **LangGraph** (state machine), wrapped as a **Fetch AI uAgent** (network transport), and capped with an **LLM report node** (Claude or ASI:One) that translates raw scores into a plain-language clinical summary.

---

## Quick Links

| Document | What's inside |
|---|---|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Graph topology, all 5 stages, risk scoring formula, LLM report node, Fetch AI integration, mock/real swap pattern |
| [PROBLEM.md](PROBLEM.md) | The clinical problem, what this POC demonstrates, and what's blocking productionisation |
| [SETUP.md](SETUP.md) | Installation, environment variables, running the demo, Agentverse registration |

---

## 30-Second Overview

```
[Edited sequence + patient HLA profile]
            │
            ▼
      Stage 1 — Structural (AlphaFold3 / ESMFold + SASA)
            │  low pLDDT → retry once in fallback mode
            ▼
      Stage 2 — HLA Presentation (NetChop + NetMHCpan)
            │  both gates pass → early exit (safe)
            ▼
  ┌── Stage 3a — T-cell (NetTCR-2.0) ─────────────────┐
  └── Stage 3b — B-cell (DiscoTope-3.0) ───────────────┘
            │  either flags → early exit (high_risk)
            ▼
      Stage 4 — Systems Dynamics (GenBio AI AIDO)
            │
            ▼
      Aggregate → weighted risk vector (structural / immunogenic / reactivity / systems)
            │
            ▼
      Report → LLM clinical summary (Claude / ASI:One / mock)
```

**Four demo scenarios** illustrate different routing paths: full pipeline with high-risk result, early exit at Stage 2, cellular disruption with immune tolerance, and a clean all-clear.

---

## Stack

| Layer | Technology |
|---|---|
| Workflow orchestration | LangGraph `StateGraph` |
| Data models | Pydantic v2 |
| Agent network | Fetch AI uagents 0.25+ |
| LLM report | Claude `claude-opus-4-6` (adaptive thinking) |
| Terminal demo | Rich |
| All biology tools | Mocked (drop-in ABC interface for real tools) |

---

## Two Fetch AI Deployment Modes

**Monolithic** — one uAgent wraps the full LangGraph pipeline. Simple to register on Agentverse.

**Multi-agent** — each pipeline stage is an independent specialist agent connected via an orchestrator. Any stage can be called directly by other agents on the Agentverse network without running the full pipeline.

Both expose the same `PipelineRequest` / `PipelineResponse` message contract.

---

## Running It

```bash
uv sync

# LangGraph terminal demo (4 scenarios, no Fetch AI)
uv run pyth
[truncated — 331 more characters]
```

### ARCHITECTURE.md

```markdown
# Architecture

## Overview

The pipeline is a patient-specific, multi-stage in silico safety screening system for gene editing therapies. It evaluates both immunogenic risk (will the immune system destroy the edited cells?) and systems-level risk (will the edit destabilise cellular function even if the immune system tolerates it?).

The pipeline is built on **LangGraph** (state machine), wrapped as a **Fetch AI uAgent** (network transport), and capped with an **LLM report node** (Claude or ASI:One) that translates raw scores into a plain-language clinical report.

---

## Graph Topology

```
START
  │
  ▼
stage1 ──── low pLDDT ──► increment_retry ──► stage1 (retry once, fallback mode)
  │
  ▼ (high/medium confidence, or after retry)
stage2
  │
  ├── early exit (Class I %Rank >2.0 AND Class II %Rank >10.0) ──► aggregate
  │
  ▼ (binders detected)
  ├──────────────────────────────┐
  ▼  [parallel via Send]         ▼
stage3_tcr                  stage3_bcell
  │                              │
  └──────────┬───────────────────┘
             ▼
        stage3_join
             │
             ├── high_risk_flag ──► aggregate
             │
             ▼ (reactivity within tolerance)
           stage4
             │
             ▼
          aggregate
             │
             ▼
           report   ◄── Claude / ASI:One / mock
             │
            END
```

---

## Stages

### Stage 1 — Structural Modeling
**Real tool:** ESMFold via ESM Atlas REST API + BioPython — `src/tools/real/structural_tool.py` (enabled via `ESMFOLD_ENABLED=1`)  
**Mock:** `src/tools/mock/stage1.py`

Predicts 3D protein conformation and calculates per-residue Solvent Accessible Surface Area (SASA). The key output is whether the edited residues are surface-exposed (SASA > 30 Å²) — exposed edits are directly accessible to proteasomal machinery, B-cell receptors, and processing enzymes.

**ESMFold mode (`ESMFOLD_ENABLED=1`):** Submits the protein sequence to the ESM Atlas public REST API. The returned PDB file is parsed with BioPython's ShrakeRupley algorithm to compute per-residue SASA; per-residue pLDDT is read from the B-factor column. Sequences longer than 400 aa are automatically truncated to the edit zone ± 50 flanking residues before submission. Any API or parse failure returns a conservative fallback result (full exposure assumed) rather than raising — this naturally triggers the retry loop below.

**Retry loop:** If pLDDT < 50 (low structural confidence — typically disordered regions), the graph cycles once through `increment_retry → stage1` in fallback mode. Fallback mode skips structure prediction and conservatively assumes full surface exposure.

**Output:** `StructuralResult` — pLDDT score, SASA, exposure flag, confidence tier, fallback flag.

---

### Stage 2 — HLA Antigen Presentation
**Real tools:** NetChop-3.1 (proteasomal cleavage) + NetMHCpan-4.1 / NetMHCIIpan-4.0  
**Real tool:** IEDB REST API (HTTPS) — `src/tools/real/hla_tool.py` (Class I + Class II, enable
[truncated — 11792 more characters]
```

### requirements.txt

```
langgraph>=0.2.0
langchain-core>=0.2.0
pydantic>=2.0.0
rich>=13.0.0
uagents>=0.25.0
anthropic>=0.40.0

# Real tools — shared HTTP client
# No model download needed for Stage 1 or Stage 2.
requests>=2.28.0

# Real Stage 1 — ESMFold (ESM Atlas REST API) + PDB parsing
# Enable with: export ESMFOLD_ENABLED=1
biopython>=1.81

```

### pyproject.toml

```
[project]
name = "ai-hackathon-2026"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
    "anthropic>=0.111.0",
    "biopython>=1.87",
    "langchain-core>=0.2.0",
    "langgraph>=0.2.0",
    "openai>=2.43.0",
    "plotly>=6.8.0",
    "pydantic>=2.0.0",
    "rich>=13.0.0",
    "streamlit>=1.58.0",
    "uagents>=0.25.2",
]

```

### main.py

```python
def main():
    print("Hello from ai-hackathon-2026!")


if __name__ == "__main__":
    main()

```

### app.py

```python
"""
Multiscale In Silico Immunogenicity Pipeline — Streamlit Web App

Run:
    uv run streamlit run app.py
"""

import os
import time
from typing import Optional

import streamlit as st

from src.agents.graph import build_graph
from src.models.pipeline import (
    PipelineInput,
    PipelineState,
    RiskVector,
    ClinicalReport,
)

# ---------------------------------------------------------------------------
# Page config
# ---------------------------------------------------------------------------

st.set_page_config(
    page_title="Gene Therapy Safety Screener",
    layout="wide",
    initial_sidebar_state="expanded",
)

# Palette (complements the navy theme in config.toml)
_BG        = "#0f172a"   # deep navy — matches backgroundColor
_BG2       = "#1e293b"   # lighter navy — matches secondaryBackgroundColor
_BG3       = "#0d2137"   # card borders / subtle containers
_ACCENT    = "#4f8ef7"   # slate blue — matches primaryColor
_TEXT_DIM  = "#94a3b8"   # muted slate

_RED    = "#e05c5c"      # high risk
_AMBER  = "#d4924a"      # caution
_TEAL   = "#34b899"      # safe / good signal
_PURPLE = "#7c6af7"      # strong binder
_VIOLET = "#a78bda"      # weak binder

st.markdown(f"""
<style>
    .block-container {{ padding-top: 2rem; }}
    .pill-real {{
        background: #0d2e24; color: {_TEAL}; font-size: 0.72rem;
        padding: 1px 8px; border-radius: 10px; font-weight: 600;
    }}
    .pill-mock {{
        background: {_BG2}; color: {_TEXT_DIM}; font-size: 0.72rem;
        padding: 1px 8px; border-radius: 10px;
    }}
    .pill-avail {{
        background: #2e1f08; color: {_AMBER}; font-size: 0.72rem;
        padding: 1px 8px; border-radius: 10px; font-weight: 600;
    }}
    div[data-testid="metric-container"] {{
        background: {_BG2}; border-radius: 8px; padding: 12px;
        border: 1px solid #2d3f57;
    }}
    div[data-testid="stTabs"] button {{
        font-size: 0.88rem; letter-spacing: 0.02em;
    }}
</style>
""", unsafe_allow_html=True)

# ---------------------------------------------------------------------------
# Preset scenarios
# ---------------------------------------------------------------------------

PRESETS: dict[str, PipelineInput] = {
    "A — High Risk: Immune Rejection": PipelineInput(
        patient_id="HIGH_RISK",
        sequence=(
            "MVLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHFDLSHGSAQVKGHGKKVADALTNAVAHVDDMPNAL"
            "SALSDLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLDKFLASVSTVLTSKYR"
        ),
        edit_positions=[47, 48, 49, 50, 51, 52, 53],
        hla_profile=["HLA-A*02:01", "HLA-B*07:02", "HLA-C*07:02", "HLA-DRB1*01:01", "HLA-DQB1*05:01"],
    ),
    "B — B-Cell Epitope + RAS/MAPK Systems Failure": PipelineInput(
        patient_id="BCELL_AND_SYSTEMS",
        sequence=(
            "MTEYKLVVVGAGGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQEEYSAMRDQYMRT"
            "GEGFLCVFAINNTKSFEDIHHQRQEIKRVKDSEDVPMVLVGNKCDLPARTVETRQAQDLARSYGIPYIETSAKTR"
        ),
        edit_positions=[12, 13],
        hla_profile=["HLA-A*01:01", "HLA-B*08:01", "HLA-DRB1*03:01"],
    ),
    "C — Systems Failure: Cellular Disruption": PipelineInput(
        patient_id="SYSTEMS_FAILURE",
        sequence=(
            "MALSLEAPQMAVVSREALVALVQERQKKLAKQEEEDLKKLEKEAEKELRQRQERLKQEREKMLMEQLEKRLQAL"
            "EEAQRREAEHLRRQLTDLQEELMKKLNREAFKQLEEERQLKVELEEMQRREDELRQKLEEELRKAQEELRRTLEDKKE"
        ),
        edit_positions=[22, 23, 24, 25],
        hla_profile=["HLA-A*02:01", "HLA-B*35:01", "HLA-DRB1*04:01"],
    ),
    "D — Stage 3b Flag: B-Cell Epitope Detected, Systems Clear": PipelineInput(
        patient_id="BCELL_ONLY",
        sequence=(
            "MSHHWGYGKHNGPEHWHKDFPIAKGERQSPVDIDTHTAKYDPSLKPLSVSYDQATSLRILNNGAAFNVEFDDSQDKAVL"
            "KGGPLDGTYRLIQFHFHWGSLDGQGSEHTVDKKKYAAELHLVHWNTKYGDFGTAAQQPDGLAVLGIFLKVGSAKPGLQKVVDVLDSIK"
        ),
        edit_positions=[8, 9],
        hla_profile=["HLA-A*03:01", "HLA-B*07:02", "HLA-DRB1*15:01"],
    ),
    "Custom": None,
}

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

@st.cache_resource
def get_pipeline():
    return build_graph()


def _tool_pill(is_real: bool, has_real: bool = False) -> str:
    """
    Three-state pill:
      is_real=True               → green  "real"
      is_real=False, has_real    → amber  "mock / real"  (real impl exists, just not enabled)
      is_real=False, not has_real → grey  "mock"          (no real impl available)
    """
    if is_real:
        return '<span class="pill-real">real</span>'
    if has_real:
        return '<span class="pill-avail">mock / real</span>'
    return '<span class="pill-mock">mock</span>'


def _active_tools() -> dict[str, bool]:
    return {
        "stage1":  os.getenv("ESMFOLD_ENABLED",  "").lower() in ("1", "true"),
        "stage2":  os.getenv("IEDB_ENABLED",      "").lower() in ("1", "true"),
        "stage3b": os.getenv("BEPIPRED_ENABLED",  "").lower() in ("1", "true"),
        "stage4":  False,
        "report":  bool(os.getenv("ASI1_API_KEY") or os.getenv("ANTHROPIC_API_KEY")),
    }


def _report_label() -> str:
    if os.getenv("ASI1_API_KEY"):
        return "ASI:One"
    if os.getenv("ANTHROPIC_API_KEY"):
        return "Claude"
    return "mock"


def _score_color(score: float) -> str:
    if score >= 0.55:
        return _RED
    if score >= 0.25:
        return _AMBER
    return _TEAL


def _risk_bar(score: float, width: int = 160) -> str:
    pct = int(score * 100)
    color = _score_color(score)
    return (
        f'<div style="background:#1e3a5f;border-radius:4px;width:{width}px;height:8px;margin-top:6px;">'
        f'<div style="background:{color};border-radius:4px;width:{pct}%;height:8px;"></div>'
        f'</div>'
    )


# ---------------------------------------------------------------------------
# Stage renderers
# ---------------------------------------------------------------------------


[truncated — 21747 more characters]
```

### conftest.py

```python
import sys
import os

# Ensure the project root is on sys.path so `uv run python tests/...` and
# `uv run pytest` both resolve `src.*` imports correctly.
sys.path.insert(0, os.path.dirname(__file__))

```

### register_agents.py

```python
"""
Register all 7 pipeline agents on Agentverse.

Usage:
    export AGENTVERSE_KEY=<your Agentverse API key>
    uv run python register_agents.py

Each agent is registered with its existing seed (so the address is identical
to what your running agents use) and its local port as the endpoint.

Required env vars:
    AGENTVERSE_KEY   — Agentverse API key (from agentverse.ai account settings)

Optional env vars (override default seeds if you customised them):
    ORCHESTRATOR_AGENT_SEED, STAGE1_AGENT_SEED, STAGE2_AGENT_SEED,
    STAGE3_TCR_AGENT_SEED, STAGE3_BCELL_AGENT_SEED,
    STAGE4_AGENT_SEED, REPORT_AGENT_SEED
"""

import os
import sys

from uagents_core.utils.registration import (
    register_chat_agent,
    RegistrationRequestCredentials,
)

AGENTVERSE_KEY = os.environ.get("AGENTVERSE_KEY")
if not AGENTVERSE_KEY:
    print("ERROR: AGENTVERSE_KEY env var is not set.")
    print("Get your API key from https://agentverse.ai (account settings).")
    sys.exit(1)

AGENTS = [
    {
        "name": "immunogenicity-orchestrator",
        "seed_env": "ORCHESTRATOR_AGENT_SEED",
        "seed_default": "imm_orchestrator_agent_seed_2026",
        "port": int(os.getenv("ORCHESTRATOR_AGENT_PORT", "8016")),
        "description": (
            "Orchestrates the multi-stage gene therapy safety pipeline. "
            "Accepts PipelineRequest, routes across specialist agents, "
            "and returns a PipelineResponse with a risk vector and clinical summary."
        ),
    },
    {
        "name": "stage1-structural",
        "seed_env": "STAGE1_AGENT_SEED",
        "seed_default": "imm_stage1_agent_seed_2026",
        "port": int(os.getenv("STAGE1_AGENT_PORT", "8010")),
        "description": (
            "Stage 1 — Structural modelling. Predicts 3D protein conformation "
            "and surface exposure (SASA) for an edited sequence using ESMFold."
        ),
    },
    {
        "name": "stage2-hla",
        "seed_env": "STAGE2_AGENT_SEED",
        "seed_default": "imm_stage2_agent_seed_2026",
        "port": int(os.getenv("STAGE2_AGENT_PORT", "8011")),
        "description": (
            "Stage 2 — HLA antigen presentation. Scores peptide binding affinity "
            "against a patient's HLA profile (Class I + II) via NetMHCpan."
        ),
    },
    {
        "name": "stage3-tcr",
        "seed_env": "STAGE3_TCR_AGENT_SEED",
        "seed_default": "imm_stage3_tcr_agent_seed_2026",
        "port": int(os.getenv("STAGE3_TCR_AGENT_PORT", "8012")),
        "description": (
            "Stage 3a — T-cell reactivity. Scores TCR binding probability "
            "for the top HLA-presented peptide using NetTCR-2.0."
        ),
    },
    {
        "name": "stage3-bcell",
        "seed_env": "STAGE3_BCELL_AGENT_SEED",
        "seed_default": "imm_stage3_bcell_agent_seed_2026",
        "port": int(os.getenv("STAGE3_BCELL_AGENT_PORT", "8013")),
        "description": (
            "Stage 3b — B-cell reactivity. Predicts linear B-cell epitopes "
            "in the edit zone via BepiPred (IEDB REST API)."
        ),
    },
    {
        "name": "stage4-systems",
        "seed_env": "STAGE4_AGENT_SEED",
        "seed_default": "imm_stage4_agent_seed_2026",
        "port": int(os.getenv("STAGE4_AGENT_PORT", "8014")),
        "description": (
            "Stage 4 — Systems dynamics. Simulates transcriptome perturbation, "
            "cryptic splice events, and apoptotic signalling after a gene edit."
        ),
    },
    {
        "name": "report-agent",
        "seed_env": "REPORT_AGENT_SEED",
        "seed_default": "imm_report_agent_seed_2026",
        "port": int(os.getenv("REPORT_AGENT_PORT", "8015")),
        "description": (
            "Report — Risk aggregation and LLM clinical summary. Combines stage "
            "outputs into a weighted risk vector and generates a physician-readable "
            "report via Claude or ASI:One."
        ),
    },
]


def main():
    print(f"Registering {len(AGENTS)} agents on Agentverse...\n")
    success = 0
    failed = 0

    for agent in AGENTS:
        seed = os.getenv(agent["seed_env"], agent["seed_default"])
        endpoint = f"http://localhost:{agent['port']}/submit"

        try:
            ok = register_chat_agent(
                name=agent["name"],
                endpoint=endpoint,
                active=True,
                credentials=RegistrationRequestCredentials(
                    agentverse_api_key=AGENTVERSE_KEY,
                    agent_seed_phrase=seed,
                ),
                description=agent["description"],
                metadata={"categories": ["healthcare", "bioinformatics"]},
            )
            if ok:
                print(f"  OK  {agent['name']} (port {agent['port']})")
                success += 1
            else:
                print(f"  FAIL  {agent['name']} — register_chat_agent returned False")
                failed += 1
        except Exception as exc:
            print(f"  FAIL  {agent['name']} — {exc}")
            failed += 1

    print(f"\n{success}/{len(AGENTS)} agents registered.")
    if failed:
        print("Re-run to retry failed registrations.")
        sys.exit(1)


if __name__ == "__main__":
    main()

```

### demo.py

```python
"""
Immunogenicity Pipeline — POC Demo

Runs two scenarios to demonstrate the agentic routing logic:

  Scenario A: HIGH_RISK        — full pipeline, strong immune response, high-risk result
  Scenario B: BCELL_AND_SYSTEMS — B-cell epitope detected + Stage 4 catches RAS/MAPK disruption
  Scenario D: BCELL_ONLY       — Stage 3b B-cell epitope detected, systems clear

Run:
    python demo.py
"""

from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich import box
from rich.markdown import Markdown

from src.agents.graph import build_graph
from src.models.pipeline import PipelineInput, PipelineState, RiskVector, ClinicalReport

console = Console()

# ---------------------------------------------------------------------------
# Scenario definitions
# ---------------------------------------------------------------------------

SCENARIOS: list[PipelineInput] = [
    PipelineInput(
        patient_id="HIGH_RISK",
        sequence=(
            "MVLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHFDLSHGSAQVKGHGKKVADALTNAVAHVDDMPNAL"
            "SALSDLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTPAVHASLDKFLASVSTVLTSKYR"
        ),
        edit_positions=[47, 48, 49, 50, 51, 52, 53],
        hla_profile=["HLA-A*02:01", "HLA-B*07:02", "HLA-C*07:02", "HLA-DRB1*01:01", "HLA-DQB1*05:01"],
    ),
    PipelineInput(
        patient_id="BCELL_AND_SYSTEMS",
        sequence=(
            "MTEYKLVVVGAGGVGKSALTIQLIQNHFVDEYDPTIEDSYRKQVVIDGETCLLDILDTAGQEEYSAMRDQYMRT"
            "GEGFLCVFAINNTKSFEDIHHQRQEIKRVKDSEDVPMVLVGNKCDLPARTVETRQAQDLARSYGIPYIETSAKTR"
        ),
        edit_positions=[12, 13],
        hla_profile=["HLA-A*01:01", "HLA-B*08:01", "HLA-DRB1*03:01"],
    ),
    PipelineInput(
        patient_id="SYSTEMS_FAILURE",
        sequence=(
            "MALSLEAPQMAVVSREALVALVQERQKKLAKQEEEDLKKLEKEAEKELRQRQERLKQEREKMLMEQLEKRLQAL"
            "EEAQRREAEHLRRQLTDLQEELMKKLNREAFKQLEEERQLKVELEEMQRREDELRQKLEEELRKAQEELRRTLEDKKE"
        ),
        edit_positions=[22, 23, 24, 25],
        hla_profile=["HLA-A*02:01", "HLA-B*35:01", "HLA-DRB1*04:01"],
    ),
    PipelineInput(
        patient_id="BCELL_ONLY",
        sequence=(
            "MSHHWGYGKHNGPEHWHKDFPIAKGERQSPVDIDTHTAKYDPSLKPLSVSYDQATSLRILNNGAAFNVEFDDSQDKAVL"
            "KGGPLDGTYRLIQFHFHWGSLDGQGSEHTVDKKKYAAELHLVHWNTKYGDFGTAAQQPDGLAVLGIFLKVGSAKPGLQKVVDVLDSIK"
        ),
        edit_positions=[8, 9],
        hla_profile=["HLA-A*03:01", "HLA-B*07:02", "HLA-DRB1*15:01"],
    ),
]

SCENARIO_LABELS = {
    "HIGH_RISK":       "Scenario A — Immune Rejection Risk (Full Pipeline)",
    "BCELL_AND_SYSTEMS": "Scenario B — B-Cell Epitope + RAS/MAPK Systems Failure",
    "SYSTEMS_FAILURE":   "Scenario C — Cellular Disruption (Immune System Tolerates, Cell Does Not)",
    "BCELL_ONLY":        "Scenario D — Stage 3b Flag: B-Cell Epitope Detected, Systems Clear",
}


# ---------------------------------------------------------------------------
# Display helpers
# ---------------------------------------------------------------------------

def _risk_color(score: float) -> str:
    if score >= 0.55:
        return "bold red"
    if score >= 0.25:
        return "bold yellow"
    return "bold green"


def _recommendation_badge(rec: str) -> Text:
    styles = {
        "safe": ("SAFE", "bold white on green"),
        "caution": ("CAUTION", "bold black on yellow"),
        "high_risk": ("HIGH RISK", "bold white on red"),
    }
    label, style = styles.get(rec, ("UNKNOWN", "bold"))
    return Text(f" {label} ", style=style)


def print_stage_header(stage: str, detail: str = "") -> None:
    console.print(f"\n  [dim]▸[/dim] [bold cyan]{stage}[/bold cyan]" + (f"  [dim]{detail}[/dim]" if detail else ""))


def print_clinical_report(report: ClinicalReport) -> None:
    import os
    llm_label = (
        "claude-opus-4-6"
        if os.getenv("ANTHROPIC_API_KEY") and os.getenv("REPORT_LLM", "auto") != "mock"
        else "mock"
    )
    console.print(
        Panel(
            f"[bold]{report.headline}[/bold]",
            title=f"[cyan]Clinical Report[/cyan]  [dim]({llm_label})[/dim]",
            border_style="cyan",
        )
    )

    console.print("\n  [bold]Stage Findings[/bold]")
    for finding in report.stage_findings:
        console.print(f"  [dim]•[/dim] {finding}\n")

    console.print(f"  [bold]Risk Rationale[/bold]\n  {report.risk_rationale}\n")

    if report.mitigation_suggestions:
        console.print("  [bold]Mitigation Suggestions[/bold]")
        for i, sug in enumerate(report.mitigation_suggestions, 1):
            console.print(f"  [yellow]{i}.[/yellow] {sug}\n")

    console.print(f"  [dim]Confidence note: {report.confidence_note}[/dim]\n")


def print_risk_vector(rv: RiskVector) -> None:
    table = Table(box=box.SIMPLE, show_header=True, header_style="bold white")
    table.add_column("Dimension", style="cyan", width=22)
    table.add_column("Score", justify="right", width=8)
    table.add_column("Bar", width=30)

    dimensions = [
        ("Structural",    rv.structural_risk),
        ("Immunogenic",   rv.immunogenic_risk),
        ("Reactivity",    rv.reactivity_risk),
        ("Systems",       rv.systems_risk),
        ("─" * 20, None),
        ("Overall",       rv.overall_risk),
    ]

    for name, score in dimensions:
        if score is None:
            table.add_row(f"[dim]{name}[/dim]", "", "")
            continue
        bar_len = int(score * 28)
        bar = "█" * bar_len + "░" * (28 - bar_len)
        color = _risk_color(score)
        table.add_row(name, f"[{color}]{score:.3f}[/{color}]", f"[{color}]{bar}[/{color}]")

    console.print(table)

    console.print(
        f"\n  Recommendation: {_recommendation_badge(rv.recommendation)}"
    )
    console.print(f"\n  [dim]{rv.summary}[/dim]")


# ---------------------------------------------------------------------------
# Pipeline runner with step-by-step output
# ------------------------------------------------------------------
[truncated — 4610 more characters]
```

### fetch/bureau.py

```python
"""
Bureau — runs the pipeline agent and client agent together in one process.

Use this for local end-to-end testing without needing Agentverse or two
separate terminal windows.

    uv run python -m fetch.bureau

The client agent sends all four demo scenarios on startup and logs each
response as it arrives.
"""

from uagents import Bureau

from fetch.pipeline_agent import agent as pipeline_agent
from fetch.client_agent import create_client_agent

# Inject the pipeline agent's address so the client knows where to send requests.
# Both agents share the same process, so the address is available immediately.
client_agent = create_client_agent(pipeline_address=pipeline_agent.address)

bureau = Bureau(agents=[pipeline_agent, client_agent])

if __name__ == "__main__":
    bureau.run()

```

[39 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]