Spaces:
Runtime error
Runtime error
faisalAI27 commited on
Commit ·
8fbda8a
1
Parent(s): d7b0e52
polish and verification
Browse files- README.md +75 -44
- backend/.env.example +2 -11
- backend/README.md +4 -5
- backend/app/core/config.py +7 -1
- backend/app/main.py +1 -0
- backend/app/schemas.py +1 -0
- backend/app/services/explanation_service.py +14 -6
- docs/DEMO_EXAMPLES.md +81 -0
- docs/LIMITATIONS.md +18 -0
- docs/README.md +4 -0
- docs/TESTING_CHECKLIST.md +36 -0
- docs/api_contract.md +47 -49
- docs/architecture.md +44 -27
- docs/model_card.md +23 -14
- frontend/README.md +3 -2
- frontend/components/ResultCard.tsx +13 -0
- frontend/types.ts +1 -0
README.md
CHANGED
|
@@ -1,51 +1,72 @@
|
|
| 1 |
# Variant Risk Explainer
|
| 2 |
|
| 3 |
-
Variant Risk Explainer is a full-stack AI genomics research demo
|
| 4 |
|
| 5 |
-
This
|
| 6 |
|
| 7 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
```text
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
```
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
- Node.js 20+
|
| 25 |
-
- Google Colab for model training
|
| 26 |
-
- GRCh38 reference assets for training data preparation
|
| 27 |
|
| 28 |
-
##
|
| 29 |
|
| 30 |
```bash
|
| 31 |
cd backend
|
| 32 |
python -m venv .venv
|
| 33 |
source .venv/bin/activate
|
|
|
|
| 34 |
pip install -r requirements.txt
|
| 35 |
cp .env.example .env
|
| 36 |
-
uvicorn app.main:app --reload
|
| 37 |
```
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
Run
|
| 42 |
-
|
| 43 |
-
```bash
|
| 44 |
-
cd backend
|
| 45 |
-
pytest
|
| 46 |
-
```
|
| 47 |
-
|
| 48 |
-
## Frontend Quick Start
|
| 49 |
|
| 50 |
```bash
|
| 51 |
cd frontend
|
|
@@ -56,29 +77,39 @@ npm run dev
|
|
| 56 |
|
| 57 |
Open `http://localhost:3000`.
|
| 58 |
|
| 59 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
2. Follow the notebook cells to install dependencies, download ClinVar GRCh38 data, prepare examples, and fine-tune DNABERT-2.
|
| 65 |
-
3. Export the trained Hugging Face model directory.
|
| 66 |
-
4. Point the backend `MODEL_DIR` environment variable to that exported model directory.
|
| 67 |
|
| 68 |
## Data and Model Artifact Policy
|
| 69 |
|
| 70 |
-
|
| 71 |
-
- Trained model weights are not committed to GitHub.
|
| 72 |
-
- Use the training scripts or Colab notebook to regenerate datasets.
|
| 73 |
-
- Store trained models in Google Drive or local storage.
|
| 74 |
-
- For local evaluation, place the 20k alternate-sequence dataset in `training/csv_files_20k_alt/`.
|
| 75 |
-
- For backend use, place the model folder in `backend/models/final_model/` or set the backend model path environment variable.
|
| 76 |
-
- The local final model folder `training/training_model_files/` and the 20k CSV folder `training/csv_files_20k_alt/` are intentionally ignored by Git.
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
|
| 82 |
-
##
|
| 83 |
|
| 84 |
-
Predictions and explanations are experimental model outputs. They can be wrong, incomplete, biased by
|
|
|
|
| 1 |
# Variant Risk Explainer
|
| 2 |
|
| 3 |
+
Variant Risk Explainer is a full-stack AI genomics research demo. It uses a fine-tuned DNABERT-2 model to estimate whether a submitted DNA sequence looks more similar to benign/likely benign or pathogenic/likely pathogenic ClinVar examples.
|
| 4 |
|
| 5 |
+
This project is for AI/ML research and education only. It is not a medical device, not a diagnostic system, and must not be used for clinical decisions.
|
| 6 |
|
| 7 |
+
## Project Overview
|
| 8 |
+
|
| 9 |
+
- `training/`: ClinVar GRCh38 data preparation, DNABERT-2 training notebooks, local evaluation scripts.
|
| 10 |
+
- `backend/`: FastAPI inference API with DNABERT-2 prediction and explanation services.
|
| 11 |
+
- `frontend/`: Next.js demo UI with input form, backend status, result card, explanation, and history.
|
| 12 |
+
- `docs/`: Architecture notes, API contract, model card, demo examples, limitations, and testing checklist.
|
| 13 |
+
|
| 14 |
+
## Architecture
|
| 15 |
|
| 16 |
```text
|
| 17 |
+
User
|
| 18 |
+
↓
|
| 19 |
+
Next.js Frontend
|
| 20 |
+
↓
|
| 21 |
+
FastAPI Backend
|
| 22 |
+
↓
|
| 23 |
+
DNABERT-2 Prediction Service
|
| 24 |
+
↓
|
| 25 |
+
Explanation Layer
|
| 26 |
+
↓
|
| 27 |
+
Research/Demo Result
|
| 28 |
```
|
| 29 |
|
| 30 |
+
The frontend sends a DNA sequence to `POST /analyze`. The backend cleans and crops the sequence, runs the DNABERT-2 classifier, applies the tuned threshold, then returns probabilities, a research-only label, and a cautious explanation.
|
| 31 |
+
|
| 32 |
+
## Model Training Summary
|
| 33 |
+
|
| 34 |
+
- Base model: DNABERT-2
|
| 35 |
+
- Dataset: 20k ClinVar alternate-sequence dataset
|
| 36 |
+
- Genome build: GRCh38
|
| 37 |
+
- Task: binary research classification
|
| 38 |
+
- Label `0`: Benign / Likely benign
|
| 39 |
+
- Label `1`: Pathogenic / Likely pathogenic
|
| 40 |
+
- Decision threshold: `0.16`
|
| 41 |
+
|
| 42 |
+
## Final Metrics
|
| 43 |
|
| 44 |
+
| Metric | Value |
|
| 45 |
+
| --- | ---: |
|
| 46 |
+
| Accuracy | 0.5537 |
|
| 47 |
+
| Precision | 0.5384 |
|
| 48 |
+
| Recall | 0.7533 |
|
| 49 |
+
| F1 | 0.6280 |
|
| 50 |
+
| MCC | 0.1171 |
|
| 51 |
+
| AUC ROC | 0.5928 |
|
| 52 |
|
| 53 |
+
These metrics are limited and support demo use only, not clinical interpretation.
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
## Run Backend
|
| 56 |
|
| 57 |
```bash
|
| 58 |
cd backend
|
| 59 |
python -m venv .venv
|
| 60 |
source .venv/bin/activate
|
| 61 |
+
python -m pip install --upgrade pip
|
| 62 |
pip install -r requirements.txt
|
| 63 |
cp .env.example .env
|
| 64 |
+
uvicorn app.main:app --reload
|
| 65 |
```
|
| 66 |
|
| 67 |
+
Open `http://localhost:8000/docs`.
|
| 68 |
|
| 69 |
+
## Run Frontend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
```bash
|
| 72 |
cd frontend
|
|
|
|
| 77 |
|
| 78 |
Open `http://localhost:3000`.
|
| 79 |
|
| 80 |
+
## Environment Variables
|
| 81 |
+
|
| 82 |
+
Backend values live in `backend/.env`:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
MODEL_DIR=../training/training_model_files
|
| 86 |
+
MODEL_THRESHOLD=0.16
|
| 87 |
+
MODEL_MAX_LENGTH=512
|
| 88 |
+
MODEL_NAME=DNABERT-2 ClinVar 20k
|
| 89 |
+
DEVICE=auto
|
| 90 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 91 |
+
USE_AI_EXPLANATION=true
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
Frontend values live in `frontend/.env.local`:
|
| 95 |
|
| 96 |
+
```bash
|
| 97 |
+
NEXT_PUBLIC_API_URL=http://127.0.0.1:8000
|
| 98 |
+
```
|
| 99 |
|
| 100 |
+
Never commit `.env`, `.env.local`, API keys, datasets, or model weights.
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
## Data and Model Artifact Policy
|
| 103 |
|
| 104 |
+
Large files are intentionally ignored by Git:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
- trained model folders such as `training/training_model_files/`
|
| 107 |
+
- generated datasets such as `training/csv_files_20k_alt/`
|
| 108 |
+
- model weight files such as `.safetensors`, `.bin`, `.pt`, and `.ckpt`
|
| 109 |
+
- local environment files such as `.env` and `.env.local`
|
| 110 |
|
| 111 |
+
Use local storage, Google Drive, or another private artifact store for trained models and datasets.
|
| 112 |
|
| 113 |
+
## Demo Disclaimer
|
| 114 |
|
| 115 |
+
Predictions and explanations are experimental model outputs. They can be wrong, incomplete, biased by ClinVar labels, or invalid outside the training distribution. This project is intended only for AI/ML research demonstrations and must not be used for diagnosis, treatment, or medical decision-making.
|
backend/.env.example
CHANGED
|
@@ -3,14 +3,5 @@ MODEL_THRESHOLD=0.16
|
|
| 3 |
MODEL_MAX_LENGTH=512
|
| 4 |
MODEL_NAME=DNABERT-2 ClinVar 20k
|
| 5 |
DEVICE=auto
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
# Keep USE_OPENAI_EXPLANATION=false for the local rule-based explanation.
|
| 9 |
-
# To enable it, copy this file to backend/.env and paste your real key there.
|
| 10 |
-
USE_OPENAI_EXPLANATION=false
|
| 11 |
-
OPENAI_API_KEY=
|
| 12 |
-
OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
|
| 13 |
-
OPENAI_EXPLANATION_TIMEOUT=12
|
| 14 |
-
|
| 15 |
-
# Comma-separated origins for browser clients
|
| 16 |
-
ALLOWED_ORIGINS=http://localhost:3000
|
|
|
|
| 3 |
MODEL_MAX_LENGTH=512
|
| 4 |
MODEL_NAME=DNABERT-2 ClinVar 20k
|
| 5 |
DEVICE=auto
|
| 6 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 7 |
+
USE_AI_EXPLANATION=true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/README.md
CHANGED
|
@@ -58,10 +58,8 @@ Do not paste your OpenAI API key into source code or `.env.example`.
|
|
| 58 |
Paste it only into your local `backend/.env` file:
|
| 59 |
|
| 60 |
```bash
|
| 61 |
-
|
| 62 |
-
OPENAI_API_KEY=
|
| 63 |
-
OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
|
| 64 |
-
OPENAI_EXPLANATION_TIMEOUT=12
|
| 65 |
```
|
| 66 |
|
| 67 |
Then restart the backend. If the OpenAI key is missing, the package is not
|
|
@@ -131,6 +129,7 @@ print(response.json())
|
|
| 131 |
- `threshold`: decision threshold, currently `0.16`
|
| 132 |
- `sequence_length_used`: sequence length after optional center crop
|
| 133 |
- `explanation`: plain-language explanation of the model output
|
|
|
|
| 134 |
- `confidence_level`: rough confidence category based on model probability
|
| 135 |
- `recommendation`: safe research/demo recommendation
|
| 136 |
- `limitations`: important limitations to show users
|
|
@@ -138,7 +137,7 @@ print(response.json())
|
|
| 138 |
## Explanation Layer
|
| 139 |
|
| 140 |
The default explanation is generated by local backend rules. When
|
| 141 |
-
`
|
| 142 |
explanation paragraph in beginner-friendly language. The prediction,
|
| 143 |
probabilities, threshold, confidence level, recommendation, and limitations stay
|
| 144 |
controlled by backend logic.
|
|
|
|
| 58 |
Paste it only into your local `backend/.env` file:
|
| 59 |
|
| 60 |
```bash
|
| 61 |
+
USE_AI_EXPLANATION=true
|
| 62 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
|
|
|
|
|
| 63 |
```
|
| 64 |
|
| 65 |
Then restart the backend. If the OpenAI key is missing, the package is not
|
|
|
|
| 129 |
- `threshold`: decision threshold, currently `0.16`
|
| 130 |
- `sequence_length_used`: sequence length after optional center crop
|
| 131 |
- `explanation`: plain-language explanation of the model output
|
| 132 |
+
- `explanation_source`: `openai`, `rule-based`, or `rule-based-fallback`
|
| 133 |
- `confidence_level`: rough confidence category based on model probability
|
| 134 |
- `recommendation`: safe research/demo recommendation
|
| 135 |
- `limitations`: important limitations to show users
|
|
|
|
| 137 |
## Explanation Layer
|
| 138 |
|
| 139 |
The default explanation is generated by local backend rules. When
|
| 140 |
+
`USE_AI_EXPLANATION=true`, the backend asks OpenAI to rewrite only the
|
| 141 |
explanation paragraph in beginner-friendly language. The prediction,
|
| 142 |
probabilities, threshold, confidence level, recommendation, and limitations stay
|
| 143 |
controlled by backend logic.
|
backend/app/core/config.py
CHANGED
|
@@ -25,6 +25,12 @@ def _env_bool(name: str, default: bool = False) -> bool:
|
|
| 25 |
return value.strip().lower() in {"1", "true", "yes", "on"}
|
| 26 |
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
@dataclass(frozen=True)
|
| 29 |
class Settings:
|
| 30 |
app_name: str = "variant-risk-explainer"
|
|
@@ -35,7 +41,7 @@ class Settings:
|
|
| 35 |
model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
|
| 36 |
device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
|
| 37 |
max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
|
| 38 |
-
use_openai_explanation: bool =
|
| 39 |
openai_api_key: str = os.getenv("OPENAI_API_KEY", "").strip()
|
| 40 |
openai_explanation_model: str = (
|
| 41 |
os.getenv("OPENAI_EXPLANATION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
|
|
|
|
| 25 |
return value.strip().lower() in {"1", "true", "yes", "on"}
|
| 26 |
|
| 27 |
|
| 28 |
+
def _ai_explanation_enabled() -> bool:
|
| 29 |
+
if os.getenv("USE_AI_EXPLANATION") is not None:
|
| 30 |
+
return _env_bool("USE_AI_EXPLANATION", False)
|
| 31 |
+
return _env_bool("USE_OPENAI_EXPLANATION", False)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
@dataclass(frozen=True)
|
| 35 |
class Settings:
|
| 36 |
app_name: str = "variant-risk-explainer"
|
|
|
|
| 41 |
model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
|
| 42 |
device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
|
| 43 |
max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
|
| 44 |
+
use_openai_explanation: bool = _ai_explanation_enabled()
|
| 45 |
openai_api_key: str = os.getenv("OPENAI_API_KEY", "").strip()
|
| 46 |
openai_explanation_model: str = (
|
| 47 |
os.getenv("OPENAI_EXPLANATION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
|
backend/app/main.py
CHANGED
|
@@ -88,6 +88,7 @@ def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
|
|
| 88 |
model_name=prediction.model_name,
|
| 89 |
sequence_length_used=prediction.sequence_length_used,
|
| 90 |
explanation=explanation["explanation"],
|
|
|
|
| 91 |
confidence_level=explanation["confidence_level"],
|
| 92 |
recommendation=explanation["recommendation"],
|
| 93 |
limitations=explanation["limitations"],
|
|
|
|
| 88 |
model_name=prediction.model_name,
|
| 89 |
sequence_length_used=prediction.sequence_length_used,
|
| 90 |
explanation=explanation["explanation"],
|
| 91 |
+
explanation_source=explanation["explanation_source"],
|
| 92 |
confidence_level=explanation["confidence_level"],
|
| 93 |
recommendation=explanation["recommendation"],
|
| 94 |
limitations=explanation["limitations"],
|
backend/app/schemas.py
CHANGED
|
@@ -43,6 +43,7 @@ class AnalyzeResponse(BaseModel):
|
|
| 43 |
model_name: str
|
| 44 |
sequence_length_used: int
|
| 45 |
explanation: str
|
|
|
|
| 46 |
confidence_level: str
|
| 47 |
recommendation: str
|
| 48 |
limitations: list[str]
|
|
|
|
| 43 |
model_name: str
|
| 44 |
sequence_length_used: int
|
| 45 |
explanation: str
|
| 46 |
+
explanation_source: str
|
| 47 |
confidence_level: str
|
| 48 |
recommendation: str
|
| 49 |
limitations: list[str]
|
backend/app/services/explanation_service.py
CHANGED
|
@@ -101,12 +101,19 @@ def _generate_rule_based_explanation(
|
|
| 101 |
|
| 102 |
return {
|
| 103 |
"explanation": explanation,
|
|
|
|
| 104 |
"confidence_level": confidence_level,
|
| 105 |
"recommendation": RECOMMENDATION,
|
| 106 |
"limitations": LIMITATIONS,
|
| 107 |
}
|
| 108 |
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def _extract_json_object(text: str) -> dict:
|
| 111 |
try:
|
| 112 |
return json.loads(text)
|
|
@@ -136,7 +143,7 @@ def _generate_openai_explanation(
|
|
| 136 |
from openai import OpenAI
|
| 137 |
except ImportError:
|
| 138 |
print("OpenAI package is not installed. Using rule-based explanation.")
|
| 139 |
-
return fallback
|
| 140 |
|
| 141 |
context = {
|
| 142 |
"variant_name": variant_name,
|
|
@@ -169,20 +176,21 @@ def _generate_openai_explanation(
|
|
| 169 |
output_text = str(getattr(response, "output_text", "")).strip()
|
| 170 |
if not output_text:
|
| 171 |
print("OpenAI explanation response was empty. Using rule-based explanation.")
|
| 172 |
-
return fallback
|
| 173 |
|
| 174 |
parsed = _extract_json_object(output_text)
|
| 175 |
explanation = str(parsed.get("explanation", "")).strip()
|
| 176 |
if not explanation:
|
| 177 |
print("OpenAI explanation JSON did not include explanation. Using rule-based explanation.")
|
| 178 |
-
return fallback
|
| 179 |
|
| 180 |
enhanced = dict(fallback)
|
| 181 |
enhanced["explanation"] = explanation
|
|
|
|
| 182 |
return enhanced
|
| 183 |
except Exception as exc: # pragma: no cover - network/API failures vary.
|
| 184 |
print(f"OpenAI explanation failed: {type(exc).__name__}: {exc}. Using rule-based explanation.")
|
| 185 |
-
return fallback
|
| 186 |
|
| 187 |
|
| 188 |
def generate_explanation(
|
|
@@ -215,8 +223,8 @@ def generate_explanation(
|
|
| 215 |
if not use_openai:
|
| 216 |
return fallback
|
| 217 |
if not openai_api_key:
|
| 218 |
-
print("
|
| 219 |
-
return fallback
|
| 220 |
|
| 221 |
return _generate_openai_explanation(
|
| 222 |
fallback=fallback,
|
|
|
|
| 101 |
|
| 102 |
return {
|
| 103 |
"explanation": explanation,
|
| 104 |
+
"explanation_source": "rule-based",
|
| 105 |
"confidence_level": confidence_level,
|
| 106 |
"recommendation": RECOMMENDATION,
|
| 107 |
"limitations": LIMITATIONS,
|
| 108 |
}
|
| 109 |
|
| 110 |
|
| 111 |
+
def _fallback_with_source(fallback: dict) -> dict:
|
| 112 |
+
result = dict(fallback)
|
| 113 |
+
result["explanation_source"] = "rule-based-fallback"
|
| 114 |
+
return result
|
| 115 |
+
|
| 116 |
+
|
| 117 |
def _extract_json_object(text: str) -> dict:
|
| 118 |
try:
|
| 119 |
return json.loads(text)
|
|
|
|
| 143 |
from openai import OpenAI
|
| 144 |
except ImportError:
|
| 145 |
print("OpenAI package is not installed. Using rule-based explanation.")
|
| 146 |
+
return _fallback_with_source(fallback)
|
| 147 |
|
| 148 |
context = {
|
| 149 |
"variant_name": variant_name,
|
|
|
|
| 176 |
output_text = str(getattr(response, "output_text", "")).strip()
|
| 177 |
if not output_text:
|
| 178 |
print("OpenAI explanation response was empty. Using rule-based explanation.")
|
| 179 |
+
return _fallback_with_source(fallback)
|
| 180 |
|
| 181 |
parsed = _extract_json_object(output_text)
|
| 182 |
explanation = str(parsed.get("explanation", "")).strip()
|
| 183 |
if not explanation:
|
| 184 |
print("OpenAI explanation JSON did not include explanation. Using rule-based explanation.")
|
| 185 |
+
return _fallback_with_source(fallback)
|
| 186 |
|
| 187 |
enhanced = dict(fallback)
|
| 188 |
enhanced["explanation"] = explanation
|
| 189 |
+
enhanced["explanation_source"] = "openai"
|
| 190 |
return enhanced
|
| 191 |
except Exception as exc: # pragma: no cover - network/API failures vary.
|
| 192 |
print(f"OpenAI explanation failed: {type(exc).__name__}: {exc}. Using rule-based explanation.")
|
| 193 |
+
return _fallback_with_source(fallback)
|
| 194 |
|
| 195 |
|
| 196 |
def generate_explanation(
|
|
|
|
| 223 |
if not use_openai:
|
| 224 |
return fallback
|
| 225 |
if not openai_api_key:
|
| 226 |
+
print("AI explanation is enabled, but OPENAI_API_KEY is missing. Using rule-based explanation.")
|
| 227 |
+
return _fallback_with_source(fallback)
|
| 228 |
|
| 229 |
return _generate_openai_explanation(
|
| 230 |
fallback=fallback,
|
docs/DEMO_EXAMPLES.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Demo Examples
|
| 2 |
+
|
| 3 |
+
These examples use synthetic/demo DNA sequences only. They are not real clinical examples and should not be interpreted as medical evidence.
|
| 4 |
+
|
| 5 |
+
## Example 1: Demo Variant A
|
| 6 |
+
|
| 7 |
+
Request:
|
| 8 |
+
|
| 9 |
+
```json
|
| 10 |
+
{
|
| 11 |
+
"variant_name": "Demo Variant A",
|
| 12 |
+
"gene": "BRCA1",
|
| 13 |
+
"sequence": "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT",
|
| 14 |
+
"notes": "Synthetic short sequence for endpoint testing."
|
| 15 |
+
}
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
Example curl:
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
curl -X POST http://127.0.0.1:8000/analyze \
|
| 22 |
+
-H "Content-Type: application/json" \
|
| 23 |
+
-d '{
|
| 24 |
+
"variant_name": "Demo Variant A",
|
| 25 |
+
"gene": "BRCA1",
|
| 26 |
+
"sequence": "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT",
|
| 27 |
+
"notes": "Synthetic short sequence for endpoint testing."
|
| 28 |
+
}'
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Example 2: Demo Variant B
|
| 32 |
+
|
| 33 |
+
Request:
|
| 34 |
+
|
| 35 |
+
```json
|
| 36 |
+
{
|
| 37 |
+
"variant_name": "Demo Variant B",
|
| 38 |
+
"gene": "TP53",
|
| 39 |
+
"sequence": "TTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGC",
|
| 40 |
+
"notes": "Second synthetic sequence for frontend demo testing."
|
| 41 |
+
}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
Example curl:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
curl -X POST http://127.0.0.1:8000/analyze \
|
| 48 |
+
-H "Content-Type: application/json" \
|
| 49 |
+
-d '{
|
| 50 |
+
"variant_name": "Demo Variant B",
|
| 51 |
+
"gene": "TP53",
|
| 52 |
+
"sequence": "TTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGC",
|
| 53 |
+
"notes": "Second synthetic sequence for frontend demo testing."
|
| 54 |
+
}'
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Example 3: Invalid Sequence
|
| 58 |
+
|
| 59 |
+
Request:
|
| 60 |
+
|
| 61 |
+
```json
|
| 62 |
+
{
|
| 63 |
+
"variant_name": "Invalid Demo Variant",
|
| 64 |
+
"gene": "TP53",
|
| 65 |
+
"sequence": "ACGTXYZACGT",
|
| 66 |
+
"notes": "This should fail validation because X, Y, and Z are not valid DNA bases."
|
| 67 |
+
}
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Expected behavior:
|
| 71 |
+
|
| 72 |
+
- HTTP status: `400`
|
| 73 |
+
- Error message includes invalid DNA characters.
|
| 74 |
+
|
| 75 |
+
Example response:
|
| 76 |
+
|
| 77 |
+
```json
|
| 78 |
+
{
|
| 79 |
+
"detail": "sequence contains invalid DNA characters: XYZ"
|
| 80 |
+
}
|
| 81 |
+
```
|
docs/LIMITATIONS.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Limitations
|
| 2 |
+
|
| 3 |
+
Variant Risk Explainer is an AI/ML research demonstration only.
|
| 4 |
+
|
| 5 |
+
Key limitations:
|
| 6 |
+
|
| 7 |
+
- The model is trained on DNA sequence patterns only.
|
| 8 |
+
- The prediction does not include full clinical evidence.
|
| 9 |
+
- The prediction does not include family history.
|
| 10 |
+
- The prediction does not include population frequency.
|
| 11 |
+
- The prediction does not include functional studies.
|
| 12 |
+
- ClinVar labels can be incomplete, conflicting, biased, or updated over time.
|
| 13 |
+
- Model performance is limited, with AUC ROC around `0.5928`.
|
| 14 |
+
- The model may not generalize to variants outside the training distribution.
|
| 15 |
+
- The system is not for diagnosis or treatment decisions.
|
| 16 |
+
- The project is intended only for AI/ML research demonstration and education.
|
| 17 |
+
|
| 18 |
+
Any real genetic or medical decision requires validated clinical testing, clinical databases, and review by qualified clinical genetics professionals.
|
docs/README.md
CHANGED
|
@@ -5,3 +5,7 @@ This folder contains project documentation for the Variant Risk Explainer demo.
|
|
| 5 |
- `architecture.md`: high-level system design and data flow.
|
| 6 |
- `api_contract.md`: backend request and response schema.
|
| 7 |
- `model_card.md`: research model card and safety limitations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
- `architecture.md`: high-level system design and data flow.
|
| 6 |
- `api_contract.md`: backend request and response schema.
|
| 7 |
- `model_card.md`: research model card and safety limitations.
|
| 8 |
+
- `DEMO_EXAMPLES.md`: synthetic demo requests for frontend/backend testing.
|
| 9 |
+
- `LIMITATIONS.md`: project limitations and safety boundaries.
|
| 10 |
+
- `TESTING_CHECKLIST.md`: final pre-push and demo testing checklist.
|
| 11 |
+
- `MODEL_ARTIFACTS.md`: local model artifact handling notes.
|
docs/TESTING_CHECKLIST.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing Checklist
|
| 2 |
+
|
| 3 |
+
Use this checklist before pushing or presenting the demo.
|
| 4 |
+
|
| 5 |
+
## Backend
|
| 6 |
+
|
| 7 |
+
- [ ] `GET /health` returns `status: ok`.
|
| 8 |
+
- [ ] `GET /health` shows the expected model directory and threshold `0.16`.
|
| 9 |
+
- [ ] `POST /analyze` works for a valid synthetic DNA sequence.
|
| 10 |
+
- [ ] `POST /analyze` returns prediction probabilities and `prediction_class`.
|
| 11 |
+
- [ ] Invalid DNA sequence returns a clear HTTP `400` error.
|
| 12 |
+
- [ ] Response includes `explanation`.
|
| 13 |
+
- [ ] Response includes `explanation_source`.
|
| 14 |
+
- [ ] Response includes `confidence_level`, `recommendation`, `limitations`, and `disclaimer`.
|
| 15 |
+
- [ ] OpenAI explanation works when `USE_AI_EXPLANATION=true` and `OPENAI_API_KEY` is set.
|
| 16 |
+
- [ ] OpenAI fallback works if the key is missing or the OpenAI request fails.
|
| 17 |
+
|
| 18 |
+
## Frontend
|
| 19 |
+
|
| 20 |
+
- [ ] Frontend loads at `http://localhost:3000`.
|
| 21 |
+
- [ ] Backend status indicator shows connected/model loaded.
|
| 22 |
+
- [ ] Form accepts variant name, gene, sequence, and notes.
|
| 23 |
+
- [ ] Result card displays prediction label and risk level.
|
| 24 |
+
- [ ] Result card displays pathogenic and benign probabilities.
|
| 25 |
+
- [ ] Result card displays explanation and explanation source.
|
| 26 |
+
- [ ] Result card displays limitations and research/demo disclaimer.
|
| 27 |
+
- [ ] History panel records recent demo analyses.
|
| 28 |
+
|
| 29 |
+
## Security And Artifacts
|
| 30 |
+
|
| 31 |
+
- [ ] No `.env` files are tracked by Git.
|
| 32 |
+
- [ ] No `.env.local` files are tracked by Git.
|
| 33 |
+
- [ ] No OpenAI API key appears in tracked source, README, or docs files.
|
| 34 |
+
- [ ] Trained model folders are ignored by Git.
|
| 35 |
+
- [ ] Dataset folders are ignored by Git.
|
| 36 |
+
- [ ] Model weight files such as `.safetensors`, `.bin`, `.pt`, and `.ckpt` are ignored by Git.
|
docs/api_contract.md
CHANGED
|
@@ -12,17 +12,21 @@ http://localhost:8000
|
|
| 12 |
GET /health
|
| 13 |
```
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
```json
|
| 18 |
{
|
| 19 |
"status": "ok",
|
| 20 |
-
"
|
| 21 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
```
|
| 24 |
|
| 25 |
-
## Analyze Variant
|
| 26 |
|
| 27 |
```http
|
| 28 |
POST /analyze
|
|
@@ -33,70 +37,64 @@ Content-Type: application/json
|
|
| 33 |
|
| 34 |
```json
|
| 35 |
{
|
| 36 |
-
"
|
| 37 |
-
"
|
| 38 |
-
"
|
| 39 |
-
"
|
| 40 |
-
"gene": "BRAF",
|
| 41 |
-
"sequence_context": "ACGTACGTACGT"
|
| 42 |
}
|
| 43 |
```
|
| 44 |
|
| 45 |
-
### Fields
|
| 46 |
|
| 47 |
-
- `
|
| 48 |
-
- `
|
| 49 |
-
- `
|
| 50 |
-
- `
|
| 51 |
-
- `gene`: optional gene symbol for display and explanation context.
|
| 52 |
-
- `sequence_context`: optional GRCh38 sequence context around the variant.
|
| 53 |
|
| 54 |
### Response Body
|
| 55 |
|
| 56 |
```json
|
| 57 |
{
|
| 58 |
-
"
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"explanation": "Mock mode produced a deterministic research-only score from variant features. No clinical meaning should be inferred.",
|
| 73 |
"limitations": [
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
|
|
|
| 77 |
],
|
| 78 |
-
"disclaimer": "
|
| 79 |
}
|
| 80 |
```
|
| 81 |
|
| 82 |
-
###
|
| 83 |
|
| 84 |
-
- `
|
| 85 |
-
- `
|
| 86 |
-
- `
|
| 87 |
-
|
| 88 |
-
These labels are demo categories only and are not clinical classifications.
|
| 89 |
|
| 90 |
### Error Example
|
| 91 |
|
|
|
|
|
|
|
| 92 |
```json
|
| 93 |
{
|
| 94 |
-
"detail":
|
| 95 |
-
{
|
| 96 |
-
"type": "value_error",
|
| 97 |
-
"loc": ["body", "position"],
|
| 98 |
-
"msg": "Value error, position must be a positive GRCh38 coordinate"
|
| 99 |
-
}
|
| 100 |
-
]
|
| 101 |
}
|
| 102 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
GET /health
|
| 13 |
```
|
| 14 |
|
| 15 |
+
Example response:
|
| 16 |
|
| 17 |
```json
|
| 18 |
{
|
| 19 |
"status": "ok",
|
| 20 |
+
"model_loaded": true,
|
| 21 |
+
"device": "mps",
|
| 22 |
+
"model_dir": "../training/training_model_files",
|
| 23 |
+
"threshold": 0.16,
|
| 24 |
+
"model_name": "DNABERT-2 ClinVar 20k",
|
| 25 |
+
"load_error": null
|
| 26 |
}
|
| 27 |
```
|
| 28 |
|
| 29 |
+
## Analyze Variant Sequence
|
| 30 |
|
| 31 |
```http
|
| 32 |
POST /analyze
|
|
|
|
| 37 |
|
| 38 |
```json
|
| 39 |
{
|
| 40 |
+
"sequence": "ACGTACGTACGTACGT",
|
| 41 |
+
"variant_name": "Demo Variant A",
|
| 42 |
+
"gene": "BRCA1",
|
| 43 |
+
"notes": "Synthetic demo request"
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
```
|
| 46 |
|
| 47 |
+
### Request Fields
|
| 48 |
|
| 49 |
+
- `sequence`: required DNA sequence using only `A`, `C`, `G`, `T`, or `N`.
|
| 50 |
+
- `variant_name`: optional display name.
|
| 51 |
+
- `gene`: optional gene symbol for display/explanation context.
|
| 52 |
+
- `notes`: optional demo notes.
|
|
|
|
|
|
|
| 53 |
|
| 54 |
### Response Body
|
| 55 |
|
| 56 |
```json
|
| 57 |
{
|
| 58 |
+
"variant_name": "Demo Variant A",
|
| 59 |
+
"gene": "BRCA1",
|
| 60 |
+
"prediction_class": 1,
|
| 61 |
+
"prediction_label": "Pathogenic / Likely pathogenic",
|
| 62 |
+
"risk_level": "Elevated",
|
| 63 |
+
"benign_probability": 0.671463,
|
| 64 |
+
"pathogenic_probability": 0.328537,
|
| 65 |
+
"threshold": 0.16,
|
| 66 |
+
"model_name": "DNABERT-2 ClinVar 20k",
|
| 67 |
+
"sequence_length_used": 64,
|
| 68 |
+
"explanation": "The model estimated...",
|
| 69 |
+
"explanation_source": "openai",
|
| 70 |
+
"confidence_level": "Low model confidence",
|
| 71 |
+
"recommendation": "This result is for research/demo use only...",
|
|
|
|
| 72 |
"limitations": [
|
| 73 |
+
"The model uses DNA sequence patterns and does not replace clinical interpretation.",
|
| 74 |
+
"The model performance is limited, with test AUC around 0.5928.",
|
| 75 |
+
"The prediction does not include full clinical evidence, family history, population frequency, or functional studies.",
|
| 76 |
+
"The result should not be used for diagnosis or treatment decisions."
|
| 77 |
],
|
| 78 |
+
"disclaimer": "Research/demo use only. This model is not a clinical diagnostic system and must not be used for medical decisions."
|
| 79 |
}
|
| 80 |
```
|
| 81 |
|
| 82 |
+
### Explanation Sources
|
| 83 |
|
| 84 |
+
- `openai`: OpenAI successfully rewrote the explanation paragraph.
|
| 85 |
+
- `rule-based`: local rule-based explanation was used because AI explanation is disabled.
|
| 86 |
+
- `rule-based-fallback`: AI explanation was enabled, but the key was missing or the OpenAI request failed.
|
|
|
|
|
|
|
| 87 |
|
| 88 |
### Error Example
|
| 89 |
|
| 90 |
+
Invalid DNA characters return a clear error:
|
| 91 |
+
|
| 92 |
```json
|
| 93 |
{
|
| 94 |
+
"detail": "sequence contains invalid DNA characters: XZ"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
```
|
| 97 |
+
|
| 98 |
+
## Safety Boundary
|
| 99 |
+
|
| 100 |
+
All outputs are research/demo outputs only. They are not clinical classifications and must not be used for diagnosis, treatment, or medical decision-making.
|
docs/architecture.md
CHANGED
|
@@ -3,61 +3,78 @@
|
|
| 3 |
Variant Risk Explainer is split into four top-level folders:
|
| 4 |
|
| 5 |
```text
|
| 6 |
-
training/ ->
|
| 7 |
backend/ -> FastAPI inference API
|
| 8 |
frontend/ -> Next.js research demo UI
|
| 9 |
docs/ -> project documentation
|
| 10 |
```
|
| 11 |
|
| 12 |
-
##
|
| 13 |
|
| 14 |
```text
|
| 15 |
User
|
| 16 |
|
|
| 17 |
v
|
| 18 |
-
Next.js
|
| 19 |
|
|
| 20 |
| POST /analyze
|
| 21 |
v
|
| 22 |
-
FastAPI
|
| 23 |
|
|
| 24 |
-
| mock mode or trained DNABERT-2 model directory
|
| 25 |
v
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
```
|
| 28 |
|
| 29 |
-
##
|
| 30 |
-
|
| 31 |
-
1. Download ClinVar GRCh38 VCF data in Google Colab.
|
| 32 |
-
2. Load a GRCh38 reference FASTA.
|
| 33 |
-
3. Extract sequence windows around single nucleotide variants.
|
| 34 |
-
4. Map ClinVar clinical significance labels into research classes.
|
| 35 |
-
5. Fine-tune DNABERT-2 with Hugging Face Transformers.
|
| 36 |
-
6. Save an exported model directory for backend inference.
|
| 37 |
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
##
|
| 41 |
|
| 42 |
-
The
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
- `MODEL_MODE=trained`: require a trained model at `MODEL_DIR`.
|
| 48 |
-
- `MODEL_MODE=auto`: use the trained model when available, otherwise fall back to mock mode.
|
| 49 |
|
| 50 |
## Frontend Flow
|
| 51 |
|
| 52 |
The frontend provides:
|
| 53 |
|
| 54 |
-
-
|
| 55 |
-
-
|
| 56 |
-
-
|
| 57 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
## Safety Boundary
|
| 62 |
|
| 63 |
-
This is a research and educational demo. It does not diagnose disease, recommend care, or replace genetic counseling or clinical interpretation. All user-facing layers should preserve that boundary.
|
|
|
|
| 3 |
Variant Risk Explainer is split into four top-level folders:
|
| 4 |
|
| 5 |
```text
|
| 6 |
+
training/ -> ClinVar GRCh38 preparation, DNABERT-2 training, evaluation scripts
|
| 7 |
backend/ -> FastAPI inference API
|
| 8 |
frontend/ -> Next.js research demo UI
|
| 9 |
docs/ -> project documentation
|
| 10 |
```
|
| 11 |
|
| 12 |
+
## Runtime Flow
|
| 13 |
|
| 14 |
```text
|
| 15 |
User
|
| 16 |
|
|
| 17 |
v
|
| 18 |
+
Next.js Frontend
|
| 19 |
|
|
| 20 |
| POST /analyze
|
| 21 |
v
|
| 22 |
+
FastAPI Backend
|
| 23 |
|
|
|
|
|
| 24 |
v
|
| 25 |
+
DNABERT-2 Prediction Service
|
| 26 |
+
|
|
| 27 |
+
v
|
| 28 |
+
Explanation Layer
|
| 29 |
+
|
|
| 30 |
+
v
|
| 31 |
+
Research/Demo Result
|
| 32 |
```
|
| 33 |
|
| 34 |
+
## Backend Flow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
1. Load settings from `backend/.env`.
|
| 37 |
+
2. Load the DNABERT-2 tokenizer and sequence-classification model from `MODEL_DIR`.
|
| 38 |
+
3. Select device automatically: CUDA, then MPS, then CPU.
|
| 39 |
+
4. Clean the submitted DNA sequence.
|
| 40 |
+
5. Center crop sequences longer than `MODEL_MAX_LENGTH`.
|
| 41 |
+
6. Run DNABERT-2 in inference mode.
|
| 42 |
+
7. Apply the tuned pathogenic threshold, currently `0.16`.
|
| 43 |
+
8. Generate a cautious explanation.
|
| 44 |
+
9. Return prediction probabilities, label, explanation, limitations, and disclaimer.
|
| 45 |
|
| 46 |
+
## Explanation Layer
|
| 47 |
|
| 48 |
+
The explanation layer is designed to be safe for a research demo:
|
| 49 |
|
| 50 |
+
- `rule-based`: local deterministic explanation.
|
| 51 |
+
- `openai`: optional OpenAI-generated explanation paragraph.
|
| 52 |
+
- `rule-based-fallback`: local explanation used because AI explanation failed or was missing configuration.
|
| 53 |
|
| 54 |
+
The LLM, when enabled, rewrites only the explanation paragraph. It does not control the prediction, probabilities, threshold, confidence level, recommendation, limitations, or disclaimer.
|
|
|
|
|
|
|
| 55 |
|
| 56 |
## Frontend Flow
|
| 57 |
|
| 58 |
The frontend provides:
|
| 59 |
|
| 60 |
+
- backend health indicator
|
| 61 |
+
- DNA sequence input form
|
| 62 |
+
- loading and error states
|
| 63 |
+
- result card
|
| 64 |
+
- explanation source display
|
| 65 |
+
- local in-browser history panel
|
| 66 |
+
- research/demo disclaimer
|
| 67 |
+
|
| 68 |
+
## Training Flow
|
| 69 |
|
| 70 |
+
1. Prepare ClinVar GRCh38 records.
|
| 71 |
+
2. Extract reference sequence windows.
|
| 72 |
+
3. Build alternate-allele sequence windows.
|
| 73 |
+
4. Filter uncertain/conflicting labels.
|
| 74 |
+
5. Fine-tune DNABERT-2 on the alternate-sequence dataset.
|
| 75 |
+
6. Evaluate on held-out validation and test splits.
|
| 76 |
+
7. Export a self-contained Hugging Face model folder for backend inference.
|
| 77 |
|
| 78 |
## Safety Boundary
|
| 79 |
|
| 80 |
+
This is a research and educational demo. It does not diagnose disease, recommend care, or replace genetic counseling or clinical variant interpretation. All user-facing layers should preserve that boundary.
|
docs/model_card.md
CHANGED
|
@@ -28,10 +28,10 @@ GRCh38 is used consistently for ClinVar coordinates and reference sequence extra
|
|
| 28 |
|
| 29 |
The pipeline prepares examples from ClinVar GRCh38 VCF records. It focuses on single nucleotide variants with clinical significance labels that can be mapped into binary or compact research classes.
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
-
- `Pathogenic` and `Likely_pathogenic` -> `
|
| 34 |
-
- `Benign` and `Likely_benign` -> `
|
| 35 |
- conflicting, uncertain, or unsupported labels -> skipped or held out depending on notebook settings
|
| 36 |
|
| 37 |
## Inputs
|
|
@@ -40,22 +40,31 @@ The model receives sequence windows centered on a submitted variant. The demo sc
|
|
| 40 |
|
| 41 |
## Outputs
|
| 42 |
|
| 43 |
-
The backend maps model
|
| 44 |
|
| 45 |
-
- `
|
| 46 |
-
- `
|
| 47 |
-
|
|
|
|
| 48 |
|
| 49 |
## Evaluation
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
-
|
| 54 |
-
-
|
| 55 |
-
-
|
| 56 |
-
- Per-
|
| 57 |
-
-
|
| 58 |
-
- Gene and chromosome holdout experiments for stronger leakage checks.
|
| 59 |
|
| 60 |
## Limitations
|
| 61 |
|
|
|
|
| 28 |
|
| 29 |
The pipeline prepares examples from ClinVar GRCh38 VCF records. It focuses on single nucleotide variants with clinical significance labels that can be mapped into binary or compact research classes.
|
| 30 |
|
| 31 |
+
Final label mapping:
|
| 32 |
|
| 33 |
+
- `Pathogenic` and `Likely_pathogenic` -> `1`
|
| 34 |
+
- `Benign` and `Likely_benign` -> `0`
|
| 35 |
- conflicting, uncertain, or unsupported labels -> skipped or held out depending on notebook settings
|
| 36 |
|
| 37 |
## Inputs
|
|
|
|
| 40 |
|
| 41 |
## Outputs
|
| 42 |
|
| 43 |
+
The backend maps model probabilities into demo labels:
|
| 44 |
|
| 45 |
+
- `0`: Benign / Likely benign
|
| 46 |
+
- `1`: Pathogenic / Likely pathogenic
|
| 47 |
+
|
| 48 |
+
The current research threshold for class `1` is `0.16`.
|
| 49 |
|
| 50 |
## Evaluation
|
| 51 |
|
| 52 |
+
Final confirmed 20k alternate-sequence evaluation:
|
| 53 |
+
|
| 54 |
+
- Accuracy: `0.5537`
|
| 55 |
+
- Precision: `0.5384`
|
| 56 |
+
- Recall: `0.7533`
|
| 57 |
+
- F1: `0.6280`
|
| 58 |
+
- MCC: `0.1171`
|
| 59 |
+
- AUC ROC: `0.5928`
|
| 60 |
+
|
| 61 |
+
Recommended future evaluation:
|
| 62 |
|
| 63 |
+
- Larger independent test sets.
|
| 64 |
+
- Gene and chromosome holdout experiments.
|
| 65 |
+
- Leakage checks across related variants.
|
| 66 |
+
- Per-variant-type reporting for SNVs and indels.
|
| 67 |
+
- Calibration analysis across thresholds.
|
|
|
|
| 68 |
|
| 69 |
## Limitations
|
| 70 |
|
frontend/README.md
CHANGED
|
@@ -68,11 +68,12 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8001
|
|
| 68 |
5. Click `Analyze Variant`.
|
| 69 |
|
| 70 |
The result card shows benign/pathogenic probabilities, threshold, model name,
|
| 71 |
-
sequence length used after center cropping,
|
|
|
|
| 72 |
|
| 73 |
The explanation is generated from the backend model output and threshold. It is
|
| 74 |
not medical advice and is not a clinical interpretation. If the backend has
|
| 75 |
-
`
|
| 76 |
explanation paragraph. Do not put the OpenAI API key in the frontend; keep it in
|
| 77 |
`backend/.env` only.
|
| 78 |
|
|
|
|
| 68 |
5. Click `Analyze Variant`.
|
| 69 |
|
| 70 |
The result card shows benign/pathogenic probabilities, threshold, model name,
|
| 71 |
+
sequence length used after center cropping, a beginner-friendly explanation, and
|
| 72 |
+
the explanation source.
|
| 73 |
|
| 74 |
The explanation is generated from the backend model output and threshold. It is
|
| 75 |
not medical advice and is not a clinical interpretation. If the backend has
|
| 76 |
+
`USE_AI_EXPLANATION=true`, the backend may use OpenAI to improve the
|
| 77 |
explanation paragraph. Do not put the OpenAI API key in the frontend; keep it in
|
| 78 |
`backend/.env` only.
|
| 79 |
|
frontend/components/ResultCard.tsx
CHANGED
|
@@ -13,6 +13,16 @@ function asPercent(value: number): string {
|
|
| 13 |
return `${(value * 100).toFixed(1)}%`;
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
export function ResultCard({ error, isLoading, result }: ResultCardProps) {
|
| 17 |
if (isLoading) {
|
| 18 |
return (
|
|
@@ -101,6 +111,9 @@ export function ResultCard({ error, isLoading, result }: ResultCardProps) {
|
|
| 101 |
<p>
|
| 102 |
<strong>Confidence level:</strong> {result.confidence_level}
|
| 103 |
</p>
|
|
|
|
|
|
|
|
|
|
| 104 |
</div>
|
| 105 |
|
| 106 |
<div className="recommendationBlock">
|
|
|
|
| 13 |
return `${(value * 100).toFixed(1)}%`;
|
| 14 |
}
|
| 15 |
|
| 16 |
+
function explanationSourceLabel(source: string): string {
|
| 17 |
+
if (source === "openai") {
|
| 18 |
+
return "OpenAI";
|
| 19 |
+
}
|
| 20 |
+
if (source === "rule-based-fallback") {
|
| 21 |
+
return "Rule-based fallback";
|
| 22 |
+
}
|
| 23 |
+
return "Rule-based";
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
export function ResultCard({ error, isLoading, result }: ResultCardProps) {
|
| 27 |
if (isLoading) {
|
| 28 |
return (
|
|
|
|
| 111 |
<p>
|
| 112 |
<strong>Confidence level:</strong> {result.confidence_level}
|
| 113 |
</p>
|
| 114 |
+
<p>
|
| 115 |
+
<strong>Explanation source:</strong> {explanationSourceLabel(result.explanation_source)}
|
| 116 |
+
</p>
|
| 117 |
</div>
|
| 118 |
|
| 119 |
<div className="recommendationBlock">
|
frontend/types.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type AnalyzeResponse = {
|
|
| 17 |
model_name: string;
|
| 18 |
sequence_length_used: number;
|
| 19 |
explanation: string;
|
|
|
|
| 20 |
confidence_level: string;
|
| 21 |
recommendation: string;
|
| 22 |
limitations: string[];
|
|
|
|
| 17 |
model_name: string;
|
| 18 |
sequence_length_used: number;
|
| 19 |
explanation: string;
|
| 20 |
+
explanation_source: "rule-based" | "openai" | "rule-based-fallback" | string;
|
| 21 |
confidence_level: string;
|
| 22 |
recommendation: string;
|
| 23 |
limitations: string[];
|